fix(files): hide desktop-only reveal action and label download in browser clients

Reveal-in-file-manager was always offered whenever the server exposed
revealPath, including in a plain browser tab where there is no local
file manager to reveal into. Gate it behind a new isBrowserClientRuntime
check (web platform, no Electron shell) and relabel the save action to
"Download" for that case, since it triggers a browser-style file
download rather than an in-place save.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Serhii Dziupin
2026-08-03 17:11:41 +03:00
co-authored by Claude Sonnet 5
parent 166b89d8db
commit f02969548a
14 changed files with 63 additions and 13 deletions
@@ -43,6 +43,7 @@ import { opencodeClient } from '@/lib/opencode/client';
import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
import { Icon } from "@/components/icon/Icon"; import { Icon } from "@/components/icon/Icon";
import { getContextFileOpenFailureMessage, validateContextFileOpen } from '@/lib/contextFileOpenGuard'; import { getContextFileOpenFailureMessage, validateContextFileOpen } from '@/lib/contextFileOpenGuard';
import { isBrowserClientRuntime } from '@/lib/desktop';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
type FileNode = { type FileNode = {
@@ -190,6 +191,7 @@ interface FileRowProps {
root: string; root: string;
isExpanded: boolean; isExpanded: boolean;
isActive: boolean; isActive: boolean;
isBrowserClient: boolean;
status?: FileStatus | null; status?: FileStatus | null;
badge?: { modified: number; added: number } | null; badge?: { modified: number; added: number } | null;
permissions: { permissions: {
@@ -211,6 +213,7 @@ const FileRow: React.FC<FileRowProps> = ({
root, root,
isExpanded, isExpanded,
isActive, isActive,
isBrowserClient,
status, status,
badge, badge,
permissions, permissions,
@@ -223,6 +226,9 @@ const FileRow: React.FC<FileRowProps> = ({
const { t } = useI18n(); const { t } = useI18n();
const isDir = node.type === 'directory'; const isDir = node.type === 'directory';
const { canRename, canCreateFile, canCreateFolder, canDelete, canReveal } = permissions; const { canRename, canCreateFile, canCreateFolder, canDelete, canReveal } = permissions;
const canDownload = !isDir && Boolean(downloadFile);
const canRevealPath = canReveal && !isBrowserClient;
const hasMenuActions = canRename || canCreateFile || canCreateFolder || canDelete || canDownload || canRevealPath;
// Menu open state is local to each row so opening a menu in one row // Menu open state is local to each row so opening a menu in one row
// never re-renders its siblings. Previously this state lived on the // never re-renders its siblings. Previously this state lived on the
@@ -231,10 +237,10 @@ const FileRow: React.FC<FileRowProps> = ({
const [rightClickOpen, setRightClickOpen] = React.useState(false); const [rightClickOpen, setRightClickOpen] = React.useState(false);
const handleContextMenu = React.useCallback((event?: React.MouseEvent) => { const handleContextMenu = React.useCallback((event?: React.MouseEvent) => {
if (!canRename && !canCreateFile && !canCreateFolder && !canDelete && !canReveal) return; if (!hasMenuActions) return;
event?.preventDefault(); event?.preventDefault();
setRightClickOpen(true); setRightClickOpen(true);
}, [canRename, canCreateFile, canCreateFolder, canDelete, canReveal]); }, [hasMenuActions]);
const handleInteraction = React.useCallback(() => { const handleInteraction = React.useCallback(() => {
if (isDir) { if (isDir) {
@@ -283,10 +289,10 @@ const FileRow: React.FC<FileRowProps> = ({
toast.error(t('sidebarFilesTree.toast.operationFailed')); toast.error(t('sidebarFilesTree.toast.operationFailed'));
}); });
}}> }}>
<Icon name="download" className="mr-2 h-4 w-4" /> {t('sidebarFilesTree.menu.save')} <Icon name="download" className="mr-2 h-4 w-4" /> {t(isBrowserClient ? 'sidebarFilesTree.menu.download' : 'sidebarFilesTree.menu.save')}
</Item> </Item>
)} )}
{canReveal && ( {canRevealPath && (
<Item onClick={(e: React.MouseEvent) => { e.stopPropagation(); onRevealPath(node.path); }}> <Item onClick={(e: React.MouseEvent) => { e.stopPropagation(); onRevealPath(node.path); }}>
<Icon name="folder-received" className="mr-2 h-4 w-4" /> {t(getRevealLabelKey())} <Icon name="folder-received" className="mr-2 h-4 w-4" /> {t(getRevealLabelKey())}
</Item> </Item>
@@ -362,7 +368,7 @@ const FileRow: React.FC<FileRowProps> = ({
</span> </span>
)} )}
</button> </button>
{(canRename || canCreateFile || canCreateFolder || canDelete || canReveal) && ( {hasMenuActions && (
<div className="absolute right-1 top-1/2 -translate-y-1/2 opacity-0 focus-within:opacity-100 group-hover:opacity-100"> <div className="absolute right-1 top-1/2 -translate-y-1/2 opacity-0 focus-within:opacity-100 group-hover:opacity-100">
<DropdownMenu <DropdownMenu
open={contextMenuOpen} open={contextMenuOpen}
@@ -406,6 +412,7 @@ const areFileRowPropsEqual = (prev: FileRowProps, next: FileRowProps): boolean =
&& prev.root === next.root && prev.root === next.root
&& prev.isExpanded === next.isExpanded && prev.isExpanded === next.isExpanded
&& prev.isActive === next.isActive && prev.isActive === next.isActive
&& prev.isBrowserClient === next.isBrowserClient
&& prev.status === next.status && prev.status === next.status
&& prev.badge === next.badge && prev.badge === next.badge
&& prev.permissions === next.permissions && prev.permissions === next.permissions
@@ -422,7 +429,8 @@ const MemoizedFileRow = React.memo(FileRow, areFileRowPropsEqual);
export const SidebarFilesTree: React.FC = () => { export const SidebarFilesTree: React.FC = () => {
const { t } = useI18n(); const { t } = useI18n();
const { files } = useRuntimeAPIs(); const { files, runtime } = useRuntimeAPIs();
const isBrowserClient = isBrowserClientRuntime(runtime.platform);
const currentDirectory = useEffectiveDirectory() ?? ''; const currentDirectory = useEffectiveDirectory() ?? '';
const root = normalizePath(currentDirectory.trim()); const root = normalizePath(currentDirectory.trim());
const showHidden = useDirectoryShowHidden(); const showHidden = useDirectoryShowHidden();
@@ -1045,6 +1053,7 @@ export const SidebarFilesTree: React.FC = () => {
root={root} root={root}
isExpanded={isExpanded} isExpanded={isExpanded}
isActive={isActive} isActive={isActive}
isBrowserClient={isBrowserClient}
status={!isDir ? getFileStatus(node.path) : undefined} status={!isDir ? getFileStatus(node.path) : undefined}
badge={isDir ? getFolderBadge(node.path) : undefined} badge={isDir ? getFolderBadge(node.path) : undefined}
permissions={fileRowPermissions} permissions={fileRowPermissions}
+13 -6
View File
@@ -70,7 +70,7 @@ import { Icon } from "@/components/icon/Icon";
import { useMessageTTS } from '@/hooks/useMessageTTS'; import { useMessageTTS } from '@/hooks/useMessageTTS';
import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry'; import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
import { getDefaultTheme } from '@/lib/theme/themes'; import { getDefaultTheme } from '@/lib/theme/themes';
import { openDesktopFileInApp, openDesktopPath } from '@/lib/desktop'; import { isBrowserClientRuntime, openDesktopFileInApp, openDesktopPath } from '@/lib/desktop';
import { useOpenInAppsStore } from '@/stores/useOpenInAppsStore'; import { useOpenInAppsStore } from '@/stores/useOpenInAppsStore';
import { eventMatchesShortcut, getEffectiveShortcutCombo } from '@/lib/shortcuts'; import { eventMatchesShortcut, getEffectiveShortcutCombo } from '@/lib/shortcuts';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
@@ -361,6 +361,7 @@ interface FileRowProps {
isExpanded: boolean; isExpanded: boolean;
isActive: boolean; isActive: boolean;
isMobile: boolean; isMobile: boolean;
isBrowserClient: boolean;
alwaysShowActions: boolean; alwaysShowActions: boolean;
status?: FileStatus | null; status?: FileStatus | null;
badge?: { modified: number; added: number } | null; badge?: { modified: number; added: number } | null;
@@ -388,6 +389,7 @@ const FileRow: React.FC<FileRowProps> = ({
isExpanded, isExpanded,
isActive, isActive,
isMobile, isMobile,
isBrowserClient,
alwaysShowActions, alwaysShowActions,
status, status,
badge, badge,
@@ -405,14 +407,17 @@ const FileRow: React.FC<FileRowProps> = ({
const { t } = useI18n(); const { t } = useI18n();
const isDir = node.type === 'directory'; const isDir = node.type === 'directory';
const { canRename, canCreateFile, canCreateFolder, canDelete, canReveal } = permissions; const { canRename, canCreateFile, canCreateFolder, canDelete, canReveal } = permissions;
const canDownload = !isDir && Boolean(downloadFile);
const canRevealPath = canReveal && !isBrowserClient;
const hasMenuActions = canRename || canCreateFile || canCreateFolder || canDelete || canDownload || canRevealPath;
const handleContextMenu = React.useCallback((event?: React.MouseEvent) => { const handleContextMenu = React.useCallback((event?: React.MouseEvent) => {
if (!canRename && !canCreateFile && !canCreateFolder && !canDelete && !canReveal) { if (!hasMenuActions) {
return; return;
} }
event?.preventDefault(); event?.preventDefault();
setRightClickMenuPath(node.path); setRightClickMenuPath(node.path);
}, [canRename, canCreateFile, canCreateFolder, canDelete, canReveal, node.path, setRightClickMenuPath]); }, [hasMenuActions, node.path, setRightClickMenuPath]);
const handleInteraction = React.useCallback(() => { const handleInteraction = React.useCallback(() => {
if (isDir) { if (isDir) {
@@ -474,10 +479,10 @@ const FileRow: React.FC<FileRowProps> = ({
toast.error(t('sidebarFilesTree.toast.operationFailed')); toast.error(t('sidebarFilesTree.toast.operationFailed'));
}); });
}}> }}>
<Icon name="download" className="mr-2 size-4" /> {t('sidebarFilesTree.menu.save')} <Icon name="download" className="mr-2 size-4" /> {t(isBrowserClient ? 'sidebarFilesTree.menu.download' : 'sidebarFilesTree.menu.save')}
</Item> </Item>
)} )}
{canReveal && ( {canRevealPath && (
<Item onClick={(e: React.MouseEvent) => { e.stopPropagation(); onRevealPath(node.path); }}> <Item onClick={(e: React.MouseEvent) => { e.stopPropagation(); onRevealPath(node.path); }}>
<Icon name="folder-received" className="mr-2 size-4" /> {t(getRevealLabelKey())} <Icon name="folder-received" className="mr-2 size-4" /> {t(getRevealLabelKey())}
</Item> </Item>
@@ -546,7 +551,7 @@ const FileRow: React.FC<FileRowProps> = ({
</span> </span>
)} )}
</button> </button>
{(canRename || canCreateFile || canCreateFolder || canDelete || canReveal) && ( {hasMenuActions && (
<div className={cn( <div className={cn(
"absolute right-1 top-1/2 -translate-y-1/2", "absolute right-1 top-1/2 -translate-y-1/2",
alwaysShowActions ? "opacity-100" : "opacity-0 focus-within:opacity-100 group-hover:opacity-100" alwaysShowActions ? "opacity-100" : "opacity-0 focus-within:opacity-100 group-hover:opacity-100"
@@ -720,6 +725,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
const { files, runtime } = useRuntimeAPIs(); const { files, runtime } = useRuntimeAPIs();
const { currentTheme, availableThemes, lightThemeId, darkThemeId } = useThemeSystem(); const { currentTheme, availableThemes, lightThemeId, darkThemeId } = useThemeSystem();
const { isMobile, isTablet, screenWidth } = useDeviceInfo(); const { isMobile, isTablet, screenWidth } = useDeviceInfo();
const isBrowserClient = isBrowserClientRuntime(runtime.platform);
const alwaysShowActions = isMobile || isTablet; const alwaysShowActions = isMobile || isTablet;
const showHidden = useDirectoryShowHidden(); const showHidden = useDirectoryShowHidden();
const showGitignored = useFilesViewShowGitignored(); const showGitignored = useFilesViewShowGitignored();
@@ -2302,6 +2308,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
isExpanded={isExpanded} isExpanded={isExpanded}
isActive={isActive} isActive={isActive}
isMobile={isMobile} isMobile={isMobile}
isBrowserClient={isBrowserClient}
alwaysShowActions={alwaysShowActions} alwaysShowActions={alwaysShowActions}
status={!isDir ? getFileStatus(node.path) : undefined} status={!isDir ? getFileStatus(node.path) : undefined}
badge={isDir ? getFolderBadge(node.path) : undefined} badge={isDir ? getFolderBadge(node.path) : undefined}
+15
View File
@@ -0,0 +1,15 @@
import { describe, expect, test } from 'bun:test';
import { isBrowserClientRuntime } from './desktop';
describe('browser client runtime', () => {
test('uses browser file behavior only outside the Electron shell', () => {
expect(isBrowserClientRuntime('web', false)).toBe(true);
expect(isBrowserClientRuntime('web', true)).toBe(false);
});
test('keeps desktop and VS Code runtime behavior out of browser-only flows', () => {
expect(isBrowserClientRuntime('desktop', false)).toBe(false);
expect(isBrowserClientRuntime('vscode', false)).toBe(false);
});
});
+10 -1
View File
@@ -1,4 +1,4 @@
import type { ProjectEntry, TerminalShell } from '@/lib/api/types'; import type { ProjectEntry, RuntimeAPIs, TerminalShell } from '@/lib/api/types';
import { getInjectedBootOutcome } from '@/lib/desktopBoot'; import { getInjectedBootOutcome } from '@/lib/desktopBoot';
import type { DraftStarterRef } from '@/lib/draftStarters'; import type { DraftStarterRef } from '@/lib/draftStarters';
import type { MobileKeyboardMode } from '@/lib/mobileKeyboardMode'; import type { MobileKeyboardMode } from '@/lib/mobileKeyboardMode';
@@ -562,6 +562,15 @@ export const isWebRuntime = (): boolean => {
return !isVSCodeRuntime(); return !isVSCodeRuntime();
}; };
/**
* Electron reuses the web RuntimeAPIs implementation, so distinguish a browser
* client from an Electron renderer with both the runtime descriptor and shell.
*/
export const isBrowserClientRuntime = (
platform: RuntimeAPIs['runtime']['platform'],
desktopShell = isDesktopShell(),
): boolean => platform === 'web' && !desktopShell;
export const getDesktopHomeDirectory = async (): Promise<string | null> => { export const getDesktopHomeDirectory = async (): Promise<string | null> => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const embedded = window.__OPENCHAMBER_HOME__; const embedded = window.__OPENCHAMBER_HOME__;
+1
View File
@@ -1225,6 +1225,7 @@ export const dict = {
'sidebarFilesTree.menu.rename': 'Rename', 'sidebarFilesTree.menu.rename': 'Rename',
'sidebarFilesTree.menu.copyPath': 'Copy Path', 'sidebarFilesTree.menu.copyPath': 'Copy Path',
'sidebarFilesTree.menu.save': 'Save', 'sidebarFilesTree.menu.save': 'Save',
'sidebarFilesTree.menu.download': 'Download',
'sidebarFilesTree.menu.newFile': 'New File', 'sidebarFilesTree.menu.newFile': 'New File',
'sidebarFilesTree.menu.newFolder': 'New Folder', 'sidebarFilesTree.menu.newFolder': 'New Folder',
'sidebarFilesTree.menu.delete': 'Delete', 'sidebarFilesTree.menu.delete': 'Delete',
+1
View File
@@ -1191,6 +1191,7 @@ export const dict: Record<I18nKey, string> = {
"sidebarFilesTree.menu.rename": "Cambiar nombre", "sidebarFilesTree.menu.rename": "Cambiar nombre",
"sidebarFilesTree.menu.copyPath": "Copiar ruta", "sidebarFilesTree.menu.copyPath": "Copiar ruta",
"sidebarFilesTree.menu.save": "Guardar", "sidebarFilesTree.menu.save": "Guardar",
"sidebarFilesTree.menu.download": "Descargar",
"sidebarFilesTree.menu.newFile": "Nuevo archivo", "sidebarFilesTree.menu.newFile": "Nuevo archivo",
"sidebarFilesTree.menu.newFolder": "Nueva carpeta", "sidebarFilesTree.menu.newFolder": "Nueva carpeta",
"sidebarFilesTree.menu.delete": "Eliminar", "sidebarFilesTree.menu.delete": "Eliminar",
+1
View File
@@ -1047,6 +1047,7 @@ export const dict = {
'sidebarFilesTree.menu.rename': 'Rebaptiser', 'sidebarFilesTree.menu.rename': 'Rebaptiser',
'sidebarFilesTree.menu.copyPath': 'Copier le chemin', 'sidebarFilesTree.menu.copyPath': 'Copier le chemin',
'sidebarFilesTree.menu.save': 'Sauvegarder', 'sidebarFilesTree.menu.save': 'Sauvegarder',
'sidebarFilesTree.menu.download': 'Télécharger',
'sidebarFilesTree.menu.newFile': 'Nouveau fichier', 'sidebarFilesTree.menu.newFile': 'Nouveau fichier',
'sidebarFilesTree.menu.newFolder': 'Nouveau dossier', 'sidebarFilesTree.menu.newFolder': 'Nouveau dossier',
'sidebarFilesTree.menu.delete': 'Supprimer', 'sidebarFilesTree.menu.delete': 'Supprimer',
+1
View File
@@ -1221,6 +1221,7 @@ export const dict: Record<I18nKey, string> = {
'sidebarFilesTree.menu.rename': '名前の変更', 'sidebarFilesTree.menu.rename': '名前の変更',
'sidebarFilesTree.menu.copyPath': 'パスをコピー', 'sidebarFilesTree.menu.copyPath': 'パスをコピー',
'sidebarFilesTree.menu.save': '保存', 'sidebarFilesTree.menu.save': '保存',
'sidebarFilesTree.menu.download': 'ダウンロード',
'sidebarFilesTree.menu.newFile': '新しいファイル', 'sidebarFilesTree.menu.newFile': '新しいファイル',
'sidebarFilesTree.menu.newFolder': '新しいフォルダ', 'sidebarFilesTree.menu.newFolder': '新しいフォルダ',
'sidebarFilesTree.menu.delete': '削除', 'sidebarFilesTree.menu.delete': '削除',
+1
View File
@@ -1228,6 +1228,7 @@ export const dict: Record<I18nKey, string> = {
'sidebarFilesTree.menu.rename': '이름 변경', 'sidebarFilesTree.menu.rename': '이름 변경',
'sidebarFilesTree.menu.copyPath': '경로 복사', 'sidebarFilesTree.menu.copyPath': '경로 복사',
'sidebarFilesTree.menu.save': '저장', 'sidebarFilesTree.menu.save': '저장',
'sidebarFilesTree.menu.download': '다운로드',
'sidebarFilesTree.menu.newFile': '새 파일', 'sidebarFilesTree.menu.newFile': '새 파일',
'sidebarFilesTree.menu.newFolder': '새 폴더', 'sidebarFilesTree.menu.newFolder': '새 폴더',
'sidebarFilesTree.menu.delete': '삭제', 'sidebarFilesTree.menu.delete': '삭제',
+1
View File
@@ -2708,6 +2708,7 @@ export const dict: Record<I18nKey, string> = {
'sidebarFilesTree.menu.newFolder': 'Nowy folder', 'sidebarFilesTree.menu.newFolder': 'Nowy folder',
'sidebarFilesTree.menu.rename': 'Zmień nazwę', 'sidebarFilesTree.menu.rename': 'Zmień nazwę',
'sidebarFilesTree.menu.save': 'Zapisz', 'sidebarFilesTree.menu.save': 'Zapisz',
'sidebarFilesTree.menu.download': 'Pobierz',
'sidebarFilesTree.search.clearAria': 'Wyczyść wyszukiwanie', 'sidebarFilesTree.search.clearAria': 'Wyczyść wyszukiwanie',
'sidebarFilesTree.search.placeholder': 'Szukaj plików...', 'sidebarFilesTree.search.placeholder': 'Szukaj plików...',
'sidebarFilesTree.state.loading': 'Ładowanie...', 'sidebarFilesTree.state.loading': 'Ładowanie...',
@@ -1191,6 +1191,7 @@ export const dict: Record<I18nKey, string> = {
"sidebarFilesTree.menu.rename": "Renomear", "sidebarFilesTree.menu.rename": "Renomear",
"sidebarFilesTree.menu.copyPath": "Copiar caminho", "sidebarFilesTree.menu.copyPath": "Copiar caminho",
"sidebarFilesTree.menu.save": "Salvar", "sidebarFilesTree.menu.save": "Salvar",
"sidebarFilesTree.menu.download": "Baixar",
"sidebarFilesTree.menu.newFile": "Novo arquivo", "sidebarFilesTree.menu.newFile": "Novo arquivo",
"sidebarFilesTree.menu.newFolder": "Nova pasta", "sidebarFilesTree.menu.newFolder": "Nova pasta",
"sidebarFilesTree.menu.delete": "Excluir", "sidebarFilesTree.menu.delete": "Excluir",
+1
View File
@@ -1191,6 +1191,7 @@ export const dict: Record<I18nKey, string> = {
"sidebarFilesTree.menu.rename": "Перейменувати", "sidebarFilesTree.menu.rename": "Перейменувати",
"sidebarFilesTree.menu.copyPath": "Копіювати шлях", "sidebarFilesTree.menu.copyPath": "Копіювати шлях",
"sidebarFilesTree.menu.save": "Зберегти", "sidebarFilesTree.menu.save": "Зберегти",
"sidebarFilesTree.menu.download": "Завантажити",
"sidebarFilesTree.menu.newFile": "Новий файл", "sidebarFilesTree.menu.newFile": "Новий файл",
"sidebarFilesTree.menu.newFolder": "Нова папка", "sidebarFilesTree.menu.newFolder": "Нова папка",
"sidebarFilesTree.menu.delete": "Видалити", "sidebarFilesTree.menu.delete": "Видалити",
@@ -1191,6 +1191,7 @@ export const dict: Record<I18nKey, string> = {
'sidebarFilesTree.menu.rename': '重命名', 'sidebarFilesTree.menu.rename': '重命名',
'sidebarFilesTree.menu.copyPath': '复制路径', 'sidebarFilesTree.menu.copyPath': '复制路径',
'sidebarFilesTree.menu.save': '保存', 'sidebarFilesTree.menu.save': '保存',
'sidebarFilesTree.menu.download': '下载',
'sidebarFilesTree.menu.newFile': '新建文件', 'sidebarFilesTree.menu.newFile': '新建文件',
'sidebarFilesTree.menu.newFolder': '新建文件夹', 'sidebarFilesTree.menu.newFolder': '新建文件夹',
'sidebarFilesTree.menu.delete': '删除', 'sidebarFilesTree.menu.delete': '删除',
@@ -1203,6 +1203,7 @@ export const dict: Record<I18nKey, string> = {
'sidebarFilesTree.menu.rename': '重新命名', 'sidebarFilesTree.menu.rename': '重新命名',
'sidebarFilesTree.menu.copyPath': '複製路徑', 'sidebarFilesTree.menu.copyPath': '複製路徑',
'sidebarFilesTree.menu.save': '儲存', 'sidebarFilesTree.menu.save': '儲存',
'sidebarFilesTree.menu.download': '下載',
'sidebarFilesTree.menu.newFile': '新增檔案', 'sidebarFilesTree.menu.newFile': '新增檔案',
'sidebarFilesTree.menu.newFolder': '新增資料夾', 'sidebarFilesTree.menu.newFolder': '新增資料夾',
'sidebarFilesTree.menu.delete': '刪除', 'sidebarFilesTree.menu.delete': '刪除',