fix(open-resource): open resource tool to open existing files (#3670)

* fix(open-resource): open resource tool to open existing files

* fix loading state

* address comment

* remove title
This commit is contained in:
Vikhyath Mondreti
2026-03-19 10:39:43 -07:00
committed by GitHub
parent cef321bda2
commit 27a41d4e33
8 changed files with 123 additions and 33 deletions
@@ -344,10 +344,10 @@ interface EmbeddedFileProps {
}
function EmbeddedFile({ workspaceId, fileId, previewMode }: EmbeddedFileProps) {
const { data: files = [], isLoading } = useWorkspaceFiles(workspaceId)
const { data: files = [], isLoading, isFetching } = useWorkspaceFiles(workspaceId)
const file = useMemo(() => files.find((f) => f.id === fileId), [files, fileId])
if (isLoading) return LOADING_SKELETON
if (isLoading || (isFetching && !file)) return LOADING_SKELETON
if (!file) {
return (
@@ -68,6 +68,8 @@ import type {
ListWorkspaceMcpServersParams,
MoveFolderParams,
MoveWorkflowParams,
OpenResourceParams,
OpenResourceType,
RenameFolderParams,
RenameWorkflowParams,
RunBlockParams,
@@ -77,6 +79,7 @@ import type {
SetGlobalWorkflowVariablesParams,
UpdateWorkflowParams,
UpdateWorkspaceMcpServerParams,
ValidOpenResourceParams,
} from './param-types'
import { PLATFORM_ACTIONS_CONTENT } from './platform-actions'
import { executeVfsGlob, executeVfsGrep, executeVfsList, executeVfsRead } from './vfs-tools'
@@ -105,6 +108,36 @@ import {
} from './workflow-tools'
const logger = createLogger('CopilotToolExecutor')
const VALID_OPEN_RESOURCE_TYPES = new Set<OpenResourceType>([
'workflow',
'table',
'knowledgebase',
'file',
])
function validateOpenResourceParams(
params: OpenResourceParams
): { success: true; params: ValidOpenResourceParams } | { success: false; error: string } {
if (!params.type) {
return { success: false, error: 'type is required' }
}
if (!VALID_OPEN_RESOURCE_TYPES.has(params.type)) {
return { success: false, error: `Invalid resource type: ${params.type}` }
}
if (!params.id) {
return { success: false, error: `${params.type} resources require \`id\`` }
}
return {
success: true,
params: {
type: params.type,
id: params.id,
},
}
}
type ManageCustomToolOperation = 'add' | 'edit' | 'delete' | 'list'
@@ -996,16 +1029,16 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
list: (p, c) => executeVfsList(p, c),
// Resource visibility
open_resource: async (p) => {
const resourceType = p.type as string | undefined
const resourceId = p.id as string | undefined
if (!resourceType || !resourceId) {
return { success: false, error: 'type and id are required' }
}
const validTypes = new Set(['workflow', 'table', 'knowledgebase', 'file'])
if (!validTypes.has(resourceType)) {
return { success: false, error: `Invalid resource type: ${resourceType}` }
open_resource: async (p: OpenResourceParams) => {
const validated = validateOpenResourceParams(p)
if (!validated.success) {
return { success: false, error: validated.error }
}
const params = validated.params
const resourceType = params.type
const resourceId = params.id
return {
success: true,
output: { message: `Opened ${resourceType} ${resourceId} for the user` },
@@ -15,6 +15,7 @@ import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
import { getTableById, queryRows } from '@/lib/table/service'
import {
downloadWorkspaceFile,
findWorkspaceFileRecord,
listWorkspaceFiles,
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
import { getWorkflowById } from '@/lib/workflows/utils'
@@ -178,9 +179,7 @@ export async function executeIntegrationToolDirect(
logger.warn('Skipping non-text sandbox input file', { fileName, ext })
continue
}
const record = allFiles.find(
(f) => f.name === fileName || f.name.normalize('NFC') === fileName.normalize('NFC')
)
const record = findWorkspaceFileRecord(allFiles, filePath)
if (!record) {
logger.warn('Sandbox input file not found', { fileName })
continue
@@ -202,3 +202,15 @@ export interface UpdateWorkspaceMcpServerParams {
export interface DeleteWorkspaceMcpServerParams {
serverId: string
}
export type OpenResourceType = 'workflow' | 'table' | 'knowledgebase' | 'file'
export interface OpenResourceParams {
type?: OpenResourceType
id?: string
}
export interface ValidOpenResourceParams {
type: OpenResourceType
id: string
}
@@ -28,7 +28,7 @@ import {
updateTagDefinition,
} from '@/lib/knowledge/tags/service'
import { StorageService } from '@/lib/uploads'
import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
import { resolveWorkspaceFileReference } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
import { getQueryStrategy, handleVectorOnlySearch } from '@/app/api/knowledge/search/utils'
const logger = createLogger('KnowledgeBaseServerTool')
@@ -235,13 +235,8 @@ export const knowledgeBaseServerTool: BaseServerTool<KnowledgeBaseArgs, Knowledg
}
}
const match = args.filePath.match(/^files\/(.+)$/)
const fileName = match ? match[1] : args.filePath
const kbWorkspaceId: string = targetKb.workspaceId
const files = await listWorkspaceFiles(kbWorkspaceId)
const fileRecord = files.find(
(f) => f.name === fileName || f.name.normalize('NFC') === fileName.normalize('NFC')
)
const fileRecord = await resolveWorkspaceFileReference(kbWorkspaceId, args.filePath)
if (!fileRecord) {
return {
@@ -26,7 +26,7 @@ import {
import type { ColumnDefinition, RowData, TableDefinition } from '@/lib/table/types'
import {
downloadWorkspaceFile,
listWorkspaceFiles,
resolveWorkspaceFileReference,
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
const logger = createLogger('UserTableServerTool')
@@ -40,15 +40,10 @@ async function resolveWorkspaceFile(
filePath: string,
workspaceId: string
): Promise<{ buffer: Buffer; name: string; type: string }> {
const match = filePath.match(/^files\/(.+)$/)
const fileName = match ? match[1] : filePath
const files = await listWorkspaceFiles(workspaceId)
const record = files.find(
(f) => f.name === fileName || f.name.normalize('NFC') === fileName.normalize('NFC')
)
const record = await resolveWorkspaceFileReference(workspaceId, filePath)
if (!record) {
throw new Error(
`File not found: "${fileName}". Use glob("files/*/meta.json") to list available files.`
`File not found: "${filePath}". Use glob("files/*/meta.json") to list available files.`
)
}
const buffer = await downloadWorkspaceFile(record)
+5 -4
View File
@@ -56,7 +56,10 @@ import {
import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils'
import { getKnowledgeBases } from '@/lib/knowledge/service'
import { listTables } from '@/lib/table/service'
import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
import {
findWorkspaceFileRecord,
listWorkspaceFiles,
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
import { hasWorkflowChanged } from '@/lib/workflows/comparison'
import { listCustomTools } from '@/lib/workflows/custom-tools/operations'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
@@ -397,9 +400,7 @@ export class WorkspaceVFS {
try {
const files = await listWorkspaceFiles(this._workspaceId)
const record = files.find(
(f) => f.name === fileName || f.name.normalize('NFC') === fileName.normalize('NFC')
)
const record = findWorkspaceFileRecord(files, fileName)
if (!record) return null
return readFileRecord(record)
} catch (err) {
@@ -328,6 +328,61 @@ export async function listWorkspaceFiles(
}
}
/**
* Normalize a workspace file reference to its display name.
* Supports raw names and VFS-style paths like `files/name`, `files/name/content`,
* and `files/name/meta.json`.
*/
export function normalizeWorkspaceFileReference(fileReference: string): string {
const trimmed = fileReference.trim().replace(/^\/+/, '')
if (trimmed.startsWith('files/')) {
const withoutPrefix = trimmed.slice('files/'.length)
if (withoutPrefix.endsWith('/meta.json')) {
return withoutPrefix.slice(0, -'/meta.json'.length)
}
if (withoutPrefix.endsWith('/content')) {
return withoutPrefix.slice(0, -'/content'.length)
}
return withoutPrefix
}
return trimmed
}
/**
* Find a workspace file record in an existing list from either its id or a VFS/name reference.
*/
export function findWorkspaceFileRecord(
files: WorkspaceFileRecord[],
fileReference: string
): WorkspaceFileRecord | null {
const exactIdMatch = files.find((file) => file.id === fileReference)
if (exactIdMatch) {
return exactIdMatch
}
const normalizedReference = normalizeWorkspaceFileReference(fileReference)
return (
files.find(
(file) =>
file.name === normalizedReference ||
file.name.normalize('NFC') === normalizedReference.normalize('NFC')
) ?? null
)
}
/**
* Resolve a workspace file record from either its id or a VFS/name reference.
*/
export async function resolveWorkspaceFileReference(
workspaceId: string,
fileReference: string
): Promise<WorkspaceFileRecord | null> {
const files = await listWorkspaceFiles(workspaceId)
return findWorkspaceFileRecord(files, fileReference)
}
/**
* Get a specific workspace file
*/