Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 12 additions & 35 deletions src/apps/desktop/src/api/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3467,20 +3467,10 @@ fn build_remote_path_exists_command(parent: &str, candidate: &str) -> String {
)
}

fn build_remote_extraction_destination_available_command(
parent: &str,
candidate: &str,
allow_existing_directory: bool,
) -> String {
fn build_remote_extraction_destination_available_command(parent: &str, candidate: &str) -> String {
let parent = shell_quote_posix(parent);
let candidate = shell_quote_posix(&format!("./{}", candidate));
if allow_existing_directory {
format!(
"cd -- {parent} || exit 2; if [ -L {candidate} ]; then exit 1; fi; [ -d {candidate} ] || [ ! -e {candidate} ]"
)
} else {
format!("cd -- {parent} || exit 2; test ! -e {candidate} && test ! -L {candidate}")
}
format!("cd -- {parent} || exit 2; test ! -e {candidate} && test ! -L {candidate}")
}

fn next_available_local_archive_path(parent: &Path, base_name: &str, extension: &str) -> PathBuf {
Expand Down Expand Up @@ -3719,11 +3709,8 @@ pub async fn decompress_path(
let remote_dest_dir_name = loop {
let candidate =
extraction_destination_name_with_suffix(&remote_dest_base_name, remote_dest_suffix);
let check_command = build_remote_extraction_destination_available_command(
&remote_parent,
&candidate,
remote_dest_suffix == 0,
);
let check_command =
build_remote_extraction_destination_available_command(&remote_parent, &candidate);
let (stdout, stderr, code) = manager
.execute_command(cid, &check_command)
.await
Expand Down Expand Up @@ -4268,13 +4255,6 @@ fn next_available_local_extraction_path(parent: &Path, base_name: &str) -> Resul
for suffix in 0.. {
let candidate = parent.join(extraction_destination_name_with_suffix(base_name, suffix));
match std::fs::symlink_metadata(&candidate) {
Ok(metadata)
if suffix == 0
&& metadata.file_type().is_dir()
&& !metadata.file_type().is_symlink() =>
{
return Ok(candidate);
}
Ok(_) => continue,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(candidate),
Err(error) => {
Expand Down Expand Up @@ -4336,7 +4316,7 @@ mod archive_tests {
}

#[test]
fn local_extraction_path_avoids_existing_files_but_reuses_base_directory() {
fn local_extraction_path_uses_incrementing_suffixes_for_any_existing_item() {
let temp = tempfile::tempdir().expect("create temp dir");
std::fs::write(temp.path().join("aa"), "source").expect("write source file");
std::fs::create_dir(temp.path().join("aa (1)")).expect("create existing directory");
Expand All @@ -4347,11 +4327,11 @@ mod archive_tests {
temp.path().join("aa (2)")
);

std::fs::create_dir(temp.path().join("project")).expect("create merge destination");
std::fs::create_dir(temp.path().join("project")).expect("create existing directory");
assert_eq!(
next_available_local_extraction_path(temp.path(), "project")
.expect("reuse existing destination directory"),
temp.path().join("project")
.expect("choose unique extraction destination"),
temp.path().join("project (1)")
);
}

Expand All @@ -4368,17 +4348,14 @@ mod archive_tests {
let command = build_remote_extraction_destination_available_command(
"/home/work tree",
"中 文@!# (2)",
false,
);
assert!(command.contains("test ! -e './中 文@!# (2)'"));
assert!(command.contains("test ! -L './中 文@!# (2)'"));

let command = build_remote_extraction_destination_available_command(
"/home/work tree",
"中 文@!#",
true,
);
assert!(command.contains("[ -d './中 文@!#' ] || [ ! -e './中 文@!#' ]"));
let command =
build_remote_extraction_destination_available_command("/home/work tree", "中 文@!#");
assert!(command.contains("test ! -e './中 文@!#'"));
assert!(command.contains("test ! -L './中 文@!#'"));
}

#[test]
Expand Down
20 changes: 18 additions & 2 deletions src/web-ui/src/app/components/panels/FilesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import type {
import {
downloadWorkspaceFileToDisk,
joinWorkspaceTargetPath,
isFilePermissionError,
normalizeWorkspaceTargetDirectory,
pasteClipboardFilesToWorkspaceDirectory,
resolvePasteTargetDirectory,
Expand Down Expand Up @@ -79,6 +80,17 @@ function isAlreadyExistsError(error: unknown): boolean {
return /already exists|file exists|os error 17|os error 183|EEXIST/i.test(message);
}

function getPasteErrorMessage(
t: (key: string, options?: Record<string, unknown>) => string,
name: string,
error: string,
): string {
return t('notifications.pasteErrorDetail', {
name,
error: isFilePermissionError(error) ? t('notifications.pastePermissionDenied') : error,
});
}

/** Format a byte-per-second speed value for display, e.g. "1.4 MB/s". */
function formatSpeed(bytesPerSec: number): string {
return `${formatBytes(bytesPerSec)}/s`;
Expand Down Expand Up @@ -737,7 +749,7 @@ const FilesPanel: React.FC<FilesPanelProps> = ({
if (result.failedFiles.length > 0) {
const failedNames = result.failedFiles.map((entry) => {
const name = entry.path.split(/[/\\]/).pop() || entry.path;
return `${name}: ${entry.error}`;
return getPasteErrorMessage(t, name, entry.error);
}).join('\n');
notification.error(
t('notifications.pasteFailed', { count: result.failedFiles.length }) + `:\n${failedNames}`,
Expand All @@ -758,7 +770,11 @@ const FilesPanel: React.FC<FilesPanelProps> = ({
if (cancelledTransferIdsRef.current.has(id)) {
cancelledTransferIdsRef.current.delete(id);
} else {
notification.error(t('notifications.pasteFailed', { count: 1 }));
notification.error(
isFilePermissionError(error)
? t('notifications.pastePermissionDenied')
: t('notifications.pasteFailed', { count: 1 })
);
}
}
}, [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
import type { PluggableList } from 'unified';
import { visit } from 'unist-util-visit';
import { i18nService } from '@/infrastructure/i18n';
import { MermaidBlock } from './MermaidBlock';
Expand Down Expand Up @@ -255,6 +256,9 @@ const sanitizeSchema = {
},
};

const MARKDOWN_REMARK_PLUGINS: PluggableList = [remarkGfm, remarkAutolinkComputerFileLinks];
const MARKDOWN_REHYPE_PLUGINS: PluggableList = [rehypeRaw, [rehypeSanitize, sanitizeSchema]];

function remarkAutolinkComputerFileLinks() {
return (tree: any) => {
visit(tree, 'text', (node: any, index: number | undefined, parent: any) => {
Expand Down Expand Up @@ -1420,15 +1424,15 @@ export const Markdown = React.memo<MarkdownProps>(({
]);

const wrapperClassName = `markdown-renderer ${className}`.trim();
const basicMarkdownRenderer = (
const basicMarkdownRenderer = useMemo(() => (
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkAutolinkComputerFileLinks]}
rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeSchema]]}
remarkPlugins={MARKDOWN_REMARK_PLUGINS}
rehypePlugins={MARKDOWN_REHYPE_PLUGINS}
components={components}
>
{markdownContent}
</ReactMarkdown>
);
), [components, markdownContent]);

return (
<div className={wrapperClassName} data-bf-component="markdown" data-bf-part="root" data-bf-state={isStreaming ? 'streaming' : undefined}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import type { Components } from 'react-markdown';
import remarkGfm from 'remark-gfm';
Expand All @@ -7,7 +7,7 @@ import rehypeKatex from 'rehype-katex';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import type { Options as RehypeSanitizeOptions } from 'rehype-sanitize';
import type { Pluggable } from 'unified';
import type { Pluggable, PluggableList } from 'unified';
import 'katex/dist/katex.min.css';

interface MarkdownMathRendererProps {
Expand All @@ -22,16 +22,27 @@ export const MarkdownMathRenderer: React.FC<MarkdownMathRendererProps> = ({
components,
sanitizeSchema,
remarkAutolinkComputerFileLinks,
}) => (
<div data-bf-component="markdown" data-bf-part="math">
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath, remarkAutolinkComputerFileLinks]}
rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeSchema], rehypeKatex]}
components={components}
>
{markdownContent}
</ReactMarkdown>
</div>
);
}) => {
const remarkPlugins = useMemo<PluggableList>(
() => [remarkGfm, remarkMath, remarkAutolinkComputerFileLinks],
[remarkAutolinkComputerFileLinks],
);
const rehypePlugins = useMemo<PluggableList>(
() => [rehypeRaw, [rehypeSanitize, sanitizeSchema], rehypeKatex],
[sanitizeSchema],
);

return (
<div data-bf-component="markdown" data-bf-part="math">
<ReactMarkdown
remarkPlugins={remarkPlugins}
rehypePlugins={rehypePlugins}
components={components}
>
{markdownContent}
</ReactMarkdown>
</div>
);
};

export default MarkdownMathRenderer;
2 changes: 2 additions & 0 deletions src/web-ui/src/locales/en-US/panels/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@
"pasteSuccessFolders": "Successfully pasted {{count}} folders",
"pasteSuccessItems": "Successfully pasted {{count}} items",
"pasteFailed": "{{count}} files failed to paste",
"pastePermissionDenied": "Permission denied. Check the source file and destination folder permissions.",
"pasteErrorDetail": "{{name}}: {{error}}",
"pasteNoFiles": "No files in clipboard",
"pastingFiles": "Pasting {{count}} files to {{target}}...",
"openExplorerFailed": "Failed to open file explorer: {{error}}",
Expand Down
2 changes: 2 additions & 0 deletions src/web-ui/src/locales/en-US/tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
"retry": "Retry",
"loading": "Loading...",
"loadFailed": "Load failed",
"unsupportedFileType": "Unsupported file type",
"largeFilePreview": "The file is too large; showing the first 8 MB as a read-only preview",
"fileNotFound": "File not found",
"fileTooLarge": "File is too large; only text files smaller than 15 MB can be opened",
"permissionDenied": "Permission denied",
"networkError": "Network error",
"saveFailedWithMessage": "Failed to save file: {{message}}",
Expand Down
2 changes: 2 additions & 0 deletions src/web-ui/src/locales/zh-CN/panels/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@
"pasteSuccessFolders": "成功粘贴 {{count}} 个文件夹",
"pasteSuccessItems": "成功粘贴 {{count}} 项",
"pasteFailed": "{{count}} 个文件粘贴失败",
"pastePermissionDenied": "没有权限。请检查源文件和目标文件夹的访问权限。",
"pasteErrorDetail": "{{name}}:{{error}}",
"pasteNoFiles": "剪贴板中没有文件",
"pastingFiles": "正在粘贴 {{count}} 个文件到 {{target}}...",
"openExplorerFailed": "打开文件管理器失败: {{error}}",
Expand Down
2 changes: 2 additions & 0 deletions src/web-ui/src/locales/zh-CN/tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
"retry": "重试",
"loading": "加载中...",
"loadFailed": "加载失败",
"unsupportedFileType": "不支持的文件类型",
"largeFilePreview": "文件过大,已打开前 8 MB 只读预览",
"fileNotFound": "文件不存在",
"fileTooLarge": "文件过大,仅支持打开小于 15 MB 的文本文件",
"permissionDenied": "权限不足",
"networkError": "网络错误",
"saveFailedWithMessage": "保存文件失败: {{message}}",
Expand Down
2 changes: 2 additions & 0 deletions src/web-ui/src/locales/zh-TW/panels/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@
"pasteSuccessFolders": "成功粘貼 {{count}} 個資料夾",
"pasteSuccessItems": "成功粘貼 {{count}} 項",
"pasteFailed": "{{count}} 個檔案粘貼失敗",
"pastePermissionDenied": "沒有權限。請檢查來源檔案和目標資料夾的存取權限。",
"pasteErrorDetail": "{{name}}:{{error}}",
"pasteNoFiles": "剪貼板中沒有檔案",
"pastingFiles": "正在粘貼 {{count}} 個檔案到 {{target}}...",
"openExplorerFailed": "開啟檔案管理器失敗: {{error}}",
Expand Down
2 changes: 2 additions & 0 deletions src/web-ui/src/locales/zh-TW/tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
"retry": "重試",
"loading": "載入中...",
"loadFailed": "載入失敗",
"unsupportedFileType": "不支援的檔案類型",
"largeFilePreview": "檔案過大,已開啟前 8 MB 唯讀預覽",
"fileNotFound": "檔案不存在",
"fileTooLarge": "檔案過大,僅支援開啟小於 15 MB 的文字檔案",
"permissionDenied": "權限不足",
"networkError": "網絡錯誤",
"saveFailedWithMessage": "儲存檔案失敗: {{message}}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export class ContextMenuManager {

async show(event: MouseEvent | React.MouseEvent): Promise<void> {
if (this.isShowing) {
this.hide();
await this.hide();
}

this.isShowing = true;
Expand Down
9 changes: 8 additions & 1 deletion src/web-ui/src/tools/editor/components/CodeEditor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,18 @@ $_editor-padding-desktop: 32px;
flex: 1;
min-height: 0;
position: relative;

> div {
height: 100%;
}
}

&.is-error {
> .code-editor-tool__content,
> .editor-status-bar {
display: none;
}
}

// Overlay Styles
&__loading-overlay,
Expand Down
Loading
Loading