From d444a8eadc2bb81bce995a55bef4448ac5fcd4cb Mon Sep 17 00:00:00 2001 From: yyh Date: Wed, 21 Jan 2026 16:53:01 +0800 Subject: [PATCH] feat: use blacklist approach for file editability in Monaco Editor Switch from whitelist to blacklist pattern for determining editable files. Files are now editable unless they are known binary types (audio, archives, executables, Office documents, fonts, etc.), enabling support for any runtime-generated text files without needing to add extensions one by one. --- .../skill/hooks/use-file-type-info.ts | 11 +-- .../skill/hooks/use-inline-create-node.ts | 4 +- .../workflow/skill/utils/file-utils.ts | 86 +++++++++++++++++-- 3 files changed, 86 insertions(+), 15 deletions(-) diff --git a/web/app/components/workflow/skill/hooks/use-file-type-info.ts b/web/app/components/workflow/skill/hooks/use-file-type-info.ts index 9495718f04..ab545c3f27 100644 --- a/web/app/components/workflow/skill/hooks/use-file-type-info.ts +++ b/web/app/components/workflow/skill/hooks/use-file-type-info.ts @@ -2,9 +2,9 @@ import type { AppAssetTreeView } from '@/types/app-asset' import { useMemo } from 'react' import { getFileExtension, - isCodeOrTextFile, isImageFile, isMarkdownFile, + isTextLikeFile, isVideoFile, } from '../utils/file-utils' @@ -17,24 +17,21 @@ export type FileTypeInfo = { isMediaFile: boolean } -/** - * Hook to determine file type information based on file node. - * Returns flags for markdown, code/text, image, video files. - */ export function useFileTypeInfo(fileNode: AppAssetTreeView | undefined): FileTypeInfo { return useMemo(() => { const ext = getFileExtension(fileNode?.name, fileNode?.extension) const markdown = isMarkdownFile(ext) - const codeOrText = isCodeOrTextFile(ext) const image = isImageFile(ext) const video = isVideoFile(ext) + const editable = isTextLikeFile(ext) + const codeOrText = editable && !markdown return { isMarkdown: markdown, isCodeOrText: codeOrText, isImage: image, isVideo: video, - isEditable: markdown || codeOrText, + isEditable: editable, isMediaFile: image || video, } }, [fileNode?.name, fileNode?.extension]) diff --git a/web/app/components/workflow/skill/hooks/use-inline-create-node.ts b/web/app/components/workflow/skill/hooks/use-inline-create-node.ts index 99a06af92a..5037d31b1b 100644 --- a/web/app/components/workflow/skill/hooks/use-inline-create-node.ts +++ b/web/app/components/workflow/skill/hooks/use-inline-create-node.ts @@ -12,7 +12,7 @@ import { useCreateAppAssetFolder, useRenameAppAssetNode, } from '@/service/use-app-asset' -import { getFileExtension, isCodeOrTextFile, isMarkdownFile } from '../utils/file-utils' +import { getFileExtension, isTextLikeFile } from '../utils/file-utils' import { createDraftTreeNode, insertDraftTreeNode } from '../utils/tree-utils' type UseInlineCreateNodeOptions = { @@ -86,7 +86,7 @@ export function useInlineCreateNode({ parentId: pendingCreateParentId, }) const extension = getFileExtension(trimmedName, createdFile.extension) - if (isMarkdownFile(extension) || isCodeOrTextFile(extension)) + if (isTextLikeFile(extension)) storeApi.getState().openTab(createdFile.id, { pinned: true }) Toast.notify({ type: 'success', diff --git a/web/app/components/workflow/skill/utils/file-utils.ts b/web/app/components/workflow/skill/utils/file-utils.ts index b3011fd7b1..c10cf36a57 100644 --- a/web/app/components/workflow/skill/utils/file-utils.ts +++ b/web/app/components/workflow/skill/utils/file-utils.ts @@ -2,10 +2,80 @@ import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/type const MARKDOWN_EXTENSIONS = ['md', 'markdown', 'mdx'] const CODE_EXTENSIONS = ['json', 'yaml', 'yml', 'toml', 'js', 'jsx', 'ts', 'tsx', 'py', 'schema'] -const TEXT_EXTENSIONS = ['txt', 'log', 'ini', 'env'] -const IGNORE_EXTENSIONS = ['gitignore', 'dockerignore', 'prettierignore', 'eslintignore', 'npmignore', 'hgignore'] -const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico'] -const VIDEO_EXTENSIONS = ['mp4', 'mov', 'webm', 'mpeg', 'mpg', 'm4v', 'avi'] +const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico', 'tiff', 'psd', 'heic', 'heif', 'avif'] +const VIDEO_EXTENSIONS = ['mp4', 'mov', 'webm', 'mpeg', 'mpg', 'm4v', 'avi', 'mkv', 'flv', 'wmv', '3gp'] + +const BINARY_EXTENSIONS = [ + 'mp3', + 'wav', + 'ogg', + 'flac', + 'm4a', + 'aac', + 'wma', + 'aiff', + 'opus', + 'zip', + 'tar', + 'gz', + 'rar', + '7z', + 'bz2', + 'xz', + 'tgz', + 'tbz2', + 'lz', + 'lzma', + 'cab', + 'iso', + 'dmg', + 'exe', + 'dll', + 'so', + 'dylib', + 'bin', + 'o', + 'obj', + 'class', + 'pyc', + 'pyo', + 'pyd', + 'wasm', + 'app', + 'msi', + 'deb', + 'rpm', + 'pdf', + 'doc', + 'docx', + 'xls', + 'xlsx', + 'ppt', + 'pptx', + 'odt', + 'ods', + 'odp', + 'rtf', + 'epub', + 'mobi', + 'ttf', + 'otf', + 'woff', + 'woff2', + 'eot', + 'db', + 'sqlite', + 'sqlite3', + 'mdb', + 'accdb', + 'jar', + 'war', + 'ear', + 'apk', + 'ipa', + 'aab', + 'lock', +] export function getFileExtension(name?: string, extension?: string): string { if (extension) @@ -31,8 +101,12 @@ export function isMarkdownFile(extension: string): boolean { return MARKDOWN_EXTENSIONS.includes(extension) } -export function isCodeOrTextFile(extension: string): boolean { - return CODE_EXTENSIONS.includes(extension) || TEXT_EXTENSIONS.includes(extension) || IGNORE_EXTENSIONS.includes(extension) +export function isBinaryFile(extension: string): boolean { + return BINARY_EXTENSIONS.includes(extension) +} + +export function isTextLikeFile(extension: string): boolean { + return !isBinaryFile(extension) && !isImageFile(extension) && !isVideoFile(extension) } export function isImageFile(extension: string): boolean {