chore: remove Chinese text from file preview components (#987)

This commit is contained in:
paisley
2026-05-07 13:39:34 +08:00
committed by GitHub
parent 35ae535646
commit 4e4ebd1092
14 changed files with 128 additions and 127 deletions

View File

@@ -2,19 +2,19 @@
* Right-side artifact panel — the WorkBuddy-style split-pane sidebar
* shown next to the Chat conversation. Hosts three top-level tabs:
*
* - 变更 (changes): side-by-side diff for the focused file only (no
* - Changes: side-by-side diff for the focused file only (no
* in-panel file list — open a file from the runs file cards below
* the graph, or “查看文件变更” picks the latest change).
* - 预览 (preview): rendered preview of whichever file is currently
* the graph, or “View file changes” picks the latest change).
* - Preview: rendered preview of whichever file is currently
* focused (Markdown → rendered, code → syntax-highlighted). Shares
* `focusedFile` with the changes tab so switching tabs keeps
* context.
* - 工作空间 (browser): read-only workspace tree + file preview,
* - Workspace (browser): read-only workspace tree + file preview,
* scoped to the current agent's `agent.workspace`.
*
* Open/close + tab + focused-file state lives in the
* `useArtifactPanel` zustand store so any part of the page (file cards,
* toolbar buttons, "查看文件变更 →" links) can drive it.
* toolbar buttons, "View file changes →" links) can drive it.
*/
import { useLayoutEffect, useMemo } from 'react';
import { Eye, FileEdit, FolderOpen, FolderTree, X } from 'lucide-react';
@@ -36,7 +36,7 @@ export interface ArtifactPanelProps {
files: GeneratedFile[];
/** Currently selected agent (drives the workspace tab). */
agent: AgentSummary | null;
/** Used to mark "本轮新增" badges on the workspace tree. */
/** Used to mark "Added this run" badges on the workspace tree. */
runStartedAt?: number | null;
/** Bumping this number triggers a workspace tree reload. */
refreshSignal?: number;
@@ -56,7 +56,7 @@ export function ArtifactPanel({ files, agent, runStartedAt, refreshSignal }: Art
const handleRevealFocusedFile = () => {
if (!focusedFile) return;
invokeIpc('shell:showItemInFolder', focusedFile.filePath).catch(() => {
toast.error(t('filePreview.errors.openInFinderFailed', '无法在文件管理器中显示'));
toast.error(t('filePreview.errors.openInFinderFailed', 'Could not reveal in file manager'));
});
};
@@ -68,7 +68,7 @@ export function ArtifactPanel({ files, agent, runStartedAt, refreshSignal }: Art
<PanelTabButton
testId="artifact-panel-action-open-folder"
icon={<FolderOpen className="h-3.5 w-3.5" />}
label={t('generatedFiles.openFolder', '打开文件夹')}
label={t('generatedFiles.openFolder', 'Open folder')}
active={false}
onClick={handleRevealFocusedFile}
/>
@@ -76,7 +76,7 @@ export function ArtifactPanel({ files, agent, runStartedAt, refreshSignal }: Art
<PanelTabButton
testId="artifact-panel-tab-changes"
icon={<FileEdit className="h-3.5 w-3.5" />}
label={t('artifactPanel.tabs.changes', '变更')}
label={t('artifactPanel.tabs.changes', 'Changes')}
active={visibleTab === 'changes'}
onClick={() => setTab('changes')}
/>
@@ -84,7 +84,7 @@ export function ArtifactPanel({ files, agent, runStartedAt, refreshSignal }: Art
<PanelTabButton
testId="artifact-panel-tab-preview"
icon={<Eye className="h-3.5 w-3.5" />}
label={t('artifactPanel.tabs.preview', '预览')}
label={t('artifactPanel.tabs.preview', 'Preview')}
active={visibleTab === 'preview'}
onClick={() => setTab('preview')}
/>
@@ -92,7 +92,7 @@ export function ArtifactPanel({ files, agent, runStartedAt, refreshSignal }: Art
<PanelTabButton
testId="artifact-panel-tab-browser"
icon={<FolderTree className="h-3.5 w-3.5" />}
label={t('artifactPanel.tabs.browser', '工作空间')}
label={t('artifactPanel.tabs.browser', 'Workspace')}
active={visibleTab === 'browser'}
onClick={() => setTab('browser')}
/>
@@ -104,7 +104,7 @@ export function ArtifactPanel({ files, agent, runStartedAt, refreshSignal }: Art
size="icon"
className="h-7 w-7 shrink-0"
onClick={close}
aria-label={t('filePreview.actions.close', '关闭')}
aria-label={t('filePreview.actions.close', 'Close')}
>
<X className="h-4 w-4 pointer-events-none" />
</Button>
@@ -182,7 +182,7 @@ interface ChangesTabProps {
/**
* Full-width diff for the focused file. Which file is focused comes from
* the runs file cards or “查看文件变更 →” (auto-first); switching workspace
* the runs file cards or “View file changes →” (auto-first); switching workspace
* tabs does not bring back a sidebar list.
*/
function ChangesTab({ files, focusedFile, onFocus }: ChangesTabProps) {
@@ -211,7 +211,7 @@ function ChangesTab({ files, focusedFile, onFocus }: ChangesTabProps) {
if (!focusedFile && uniqueFiles.length === 0) {
return (
<div className="flex h-full items-center justify-center px-6 text-center text-sm text-muted-foreground">
{t('artifactPanel.changes.empty', '本会话尚无文件变更')}
{t('artifactPanel.changes.empty', 'No file changes in this session yet')}
</div>
);
}
@@ -222,7 +222,7 @@ function ChangesTab({ files, focusedFile, onFocus }: ChangesTabProps) {
</div>
) : (
<div className="flex h-full items-center justify-center px-6 text-center text-sm text-muted-foreground">
{t('artifactPanel.changes.selectFileHint', '请点击对话中的文件卡片,或「查看文件变更」')}
{t('artifactPanel.changes.selectFileHint', 'Click a file card in the conversation, or choose “View file changes”')}
</div>
);
}
@@ -237,12 +237,12 @@ function PreviewTab({ focusedFile }: PreviewTabProps) {
return (
<div className="flex h-full flex-col items-center justify-center gap-2 px-6 text-center">
<p className="text-sm font-medium text-foreground">
{t('artifactPanel.preview.emptyTitle', '尚未选择文件')}
{t('artifactPanel.preview.emptyTitle', 'No file selected')}
</p>
<p className="max-w-md text-xs text-muted-foreground">
{t(
'artifactPanel.preview.emptyHint',
'请先点击对话里的文件卡片打开侧栏并选中文件。',
'Click a file card in the conversation to open the sidebar and select a file first.',
)}
</p>
</div>

View File

@@ -9,8 +9,8 @@
* intentionally removed — path / size are visible in the header bar.
*
* The `mode` prop narrows the tab set for callers that want a single,
* fixed view (e.g. the artifact panel's 预览 tab forces `preview`, and
* the 变更 tab's right pane forces `diff`).
* fixed view (e.g. the artifact panel's preview tab forces `preview`, and
* the changes tab's right pane forces `diff`).
*
* Used by:
* - `FilePreviewOverlay` for the Skills detail Sheet (read-only).
@@ -93,7 +93,7 @@ type LoadState =
type Tab = 'source' | 'preview' | 'diff';
function tabsForFile(file: FilePreviewTarget, mode: FilePreviewBodyMode): Tab[] {
// Diff-only mode short-circuits. Callers (e.g. the 变更 tab's right
// Diff-only mode short-circuits. Callers (e.g. the changes tab's right
// pane) want a pure git-style diff with no tab strip — but only for
// formats where inline diff is actually supported.
if (mode === 'diff') return supportsInlineDiff(file) ? ['diff'] : [];
@@ -345,15 +345,15 @@ export function FilePreviewBody({
const res = await writeTextFile(file.filePath, draft);
if (!res.ok) throw new Error(res.error ?? 'unknown');
setState({ status: 'ready', content: draft, size, readOnly: false });
toast.success(t('filePreview.toast.saved', '已保存到磁盘'));
toast.success(t('filePreview.toast.saved', 'Saved to disk'));
} catch (err) {
const code = err instanceof Error ? err.message : String(err);
const localized =
code === 'outsideSandbox'
? t('filePreview.errors.outsideSandbox', '路径越界,已拒绝写入')
? t('filePreview.errors.outsideSandbox', 'Path is outside the workspace; write denied')
: code === 'readOnlyRoot'
? t('filePreview.errors.readOnlyRoot', '该文件位于只读位置(如内置技能),无法修改')
: t('filePreview.toast.saveFailed', { defaultValue: '保存失败:{{error}}', error: code });
? t('filePreview.errors.readOnlyRoot', 'This file is in a read-only location (such as a built-in skill) and cannot be edited')
: t('filePreview.toast.saveFailed', { defaultValue: 'Save failed: {{error}}', error: code });
toast.error(localized);
} finally {
setSaving(false);
@@ -367,7 +367,7 @@ export function FilePreviewBody({
const handleOpenInFinder = useCallback(() => {
invokeIpc('shell:showItemInFolder', file.filePath).catch(() => {
toast.error(t('filePreview.errors.openInFinderFailed', '无法在文件管理器中显示'));
toast.error(t('filePreview.errors.openInFinderFailed', 'Could not reveal in file manager'));
});
}, [file, t]);
@@ -376,7 +376,7 @@ export function FilePreviewBody({
await confirmAndOpenFile({ filePath: file.filePath, fileName: file.fileName, size, t });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
toast.error(t('filePreview.errors.openFailed', { defaultValue: '打开失败:{{error}}', error: message }));
toast.error(t('filePreview.errors.openFailed', { defaultValue: 'Open failed: {{error}}', error: message }));
}
}, [file, size, t]);
@@ -387,30 +387,30 @@ export function FilePreviewBody({
<div className="space-y-1.5">
<p className="text-sm font-medium text-foreground">
{directOpen
? t('filePreview.errors.largeBinaryOpenTitle', '该文件较大,暂不支持内置预览')
: t('filePreview.errors.unsupportedFormatTitle', '此文件格式暂不支持内置预览或变更')}
? t('filePreview.errors.largeBinaryOpenTitle', 'This file is too large for inline preview')
: t('filePreview.errors.unsupportedFormatTitle', 'This file format is not supported for inline preview or diff')}
</p>
<p className="max-w-md text-xs leading-relaxed text-muted-foreground">
{directOpen
? t('filePreview.errors.largeBinaryOpenHint', {
defaultValue: '当前文件为 {{size}}ClawX 不提供内置预览。你可以确认后直接使用系统默认应用打开。',
defaultValue: 'This file is {{size}}. ClawX does not provide an inline preview for it. You can confirm to open it directly in your system default app.',
size: formatFileSize(size ?? 0) || '> 2MB',
})
: t(
'filePreview.errors.unsupportedFormatHint',
'当前仅支持文本/Markdown 等可直接读取的文件进行内置预览与变更对比。请在文件管理器中打开该文件。',
'Only directly readable files such as text and Markdown support inline preview and diff. Please open this file in your file manager.',
)}
</p>
</div>
<div className="flex flex-wrap items-center justify-center gap-2">
{directOpen && (
<Button size="sm" onClick={handleOpenDirectly}>
{t('filePreview.actions.openDirectly', '直接打开')}
{t('filePreview.actions.openDirectly', 'Open directly')}
</Button>
)}
<Button variant="outline" size="sm" onClick={handleOpenInFinder}>
<FolderOpen className="mr-2 h-4 w-4" />
{t('filePreview.actions.openInFinder', '在文件管理器中显示')}
{t('filePreview.actions.openInFinder', 'Show in file manager')}
</Button>
</div>
</div>
@@ -435,23 +435,23 @@ export function FilePreviewBody({
<p>
{directOpen
? t('filePreview.errors.largeBinaryOpenHint', {
defaultValue: '当前文件为 {{size}}ClawX 不提供内置预览。你可以确认后直接使用系统默认应用打开。',
defaultValue: 'This file is {{size}}. ClawX does not provide an inline preview for it. You can confirm to open it directly in your system default app.',
size: formatFileSize(state.size ?? size ?? 0) || '> 2MB',
})
: t('filePreview.errors.tooLarge', {
defaultValue: '文件过大({{size}}),已禁用预览',
defaultValue: 'File is too large ({{size}}); preview disabled',
size: formatFileSize(state.size ?? 0) || '> 2MB',
})}
</p>
<div className="flex flex-wrap items-center justify-center gap-2">
{directOpen && (
<Button size="sm" onClick={handleOpenDirectly}>
{t('filePreview.actions.openDirectly', '直接打开')}
{t('filePreview.actions.openDirectly', 'Open directly')}
</Button>
)}
<Button variant="outline" size="sm" onClick={handleOpenInFinder}>
<FolderOpen className="mr-2 h-4 w-4" />
{t('filePreview.actions.openInFinder', '在文件管理器中显示')}
{t('filePreview.actions.openInFinder', 'Show in file manager')}
</Button>
</div>
</div>
@@ -460,10 +460,10 @@ export function FilePreviewBody({
if (state.status === 'binary') {
return (
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 text-center text-sm text-muted-foreground">
<p>{t('filePreview.errors.binary', '二进制文件不支持文本预览')}</p>
<p>{t('filePreview.errors.binary', 'Binary files do not support text preview')}</p>
<Button variant="outline" size="sm" onClick={handleOpenInFinder}>
<FolderOpen className="mr-2 h-4 w-4" />
{t('filePreview.actions.openInFinder', '在文件管理器中显示')}
{t('filePreview.actions.openInFinder', 'Show in file manager')}
</Button>
</div>
);
@@ -476,18 +476,18 @@ export function FilePreviewBody({
</div>
<div className="space-y-1.5">
<p className="text-sm font-medium text-foreground">
{t('filePreview.errors.outsideSandboxTitle', '无法读取此文件')}
{t('filePreview.errors.outsideSandboxTitle', 'Unable to read this file')}
</p>
<p className="max-w-md text-xs leading-relaxed text-muted-foreground">
{t(
'filePreview.errors.outsideSandboxHint',
'ClawX 无法读取这个路径。文件可能已被移动、删除,或当前账户没有访问权限。可在文件管理器中查看。',
'ClawX cannot read this path. The file may have been moved, deleted, or may not be accessible to the current account. You can inspect it in your file manager.',
)}
</p>
</div>
<Button variant="outline" size="sm" onClick={handleOpenInFinder}>
<FolderOpen className="mr-2 h-4 w-4" />
{t('filePreview.actions.openInFinder', '在文件管理器中显示')}
{t('filePreview.actions.openInFinder', 'Show in file manager')}
</Button>
</div>
);
@@ -496,14 +496,14 @@ export function FilePreviewBody({
const errMsg = state.message;
const hint =
errMsg === 'notFound'
? t('filePreview.errors.notFound', '文件不存在')
: t('filePreview.errors.loadFailed', { defaultValue: '加载失败:{{error}}', error: errMsg });
? t('filePreview.errors.notFound', 'File not found')
: t('filePreview.errors.loadFailed', { defaultValue: 'Load failed: {{error}}', error: errMsg });
return (
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 text-center text-sm text-muted-foreground">
<p>{hint}</p>
<Button variant="outline" size="sm" onClick={handleOpenInFinder}>
<FolderOpen className="mr-2 h-4 w-4" />
{t('filePreview.actions.openInFinder', '在文件管理器中显示')}
{t('filePreview.actions.openInFinder', 'Show in file manager')}
</Button>
</div>
);
@@ -517,9 +517,9 @@ export function FilePreviewBody({
<TabsList className="m-3 self-start">
{tabs.map((id) => (
<TabsTrigger key={id} value={id}>
{id === 'source' && t('filePreview.tabs.source', '源码')}
{id === 'preview' && t('filePreview.tabs.preview', '预览')}
{id === 'diff' && t('filePreview.tabs.changes', '变更')}
{id === 'source' && t('filePreview.tabs.source', 'Source')}
{id === 'preview' && t('filePreview.tabs.preview', 'Preview')}
{id === 'diff' && t('filePreview.tabs.changes', 'Changes')}
</TabsTrigger>
))}
</TabsList>
@@ -580,7 +580,7 @@ export function FilePreviewBody({
<MarkdownPreview source={draft ?? state.content} />
) : (
<div className="p-4 text-sm text-muted-foreground">
{t('filePreview.errors.noPreview', '该文件没有预览')}
{t('filePreview.errors.noPreview', 'No preview available for this file')}
</div>
)}
</TabsContent>
@@ -602,13 +602,13 @@ export function FilePreviewBody({
<p className="max-w-md text-sm text-muted-foreground">
{t(
'filePreview.diff.unavailable',
'本会话没有抓到这个文件的精确变更基线,无法生成 diff。',
'This session did not capture an exact baseline for this file, so a diff cannot be generated.',
)}
</p>
<p className="max-w-md text-2xs text-muted-foreground/90">
{t(
'filePreview.diff.unavailableHint',
'可点击顶部「预览」查看当前文件内容;若需精确差异,请在 Git 等工具中对比版本。',
'Use the Preview tab above to view the current file contents. For an exact diff, compare versions in Git or another external tool.',
)}
</p>
</div>
@@ -658,11 +658,11 @@ export function FilePreviewBody({
<>
<Button variant="ghost" size="sm" onClick={handleRevert} disabled={!dirty || saving}>
<Undo2 className="mr-1 h-3.5 w-3.5" />
{t('filePreview.actions.revert', '撤销')}
{t('filePreview.actions.revert', 'Revert')}
</Button>
<Button size="sm" onClick={handleSave} disabled={!dirty || saving}>
<Save className="mr-1 h-3.5 w-3.5" />
{saving ? t('filePreview.actions.saving', '保存中...') : t('filePreview.actions.save', '保存')}
{saving ? t('filePreview.actions.saving', 'Saving...') : t('filePreview.actions.save', 'Save')}
</Button>
</>
)}

View File

@@ -44,7 +44,7 @@ export function GeneratedFilesPanel({
<div className={cn('space-y-2', className)}>
<div className="px-1">
<p className="text-xs font-semibold text-foreground/75">
{t('generatedFiles.title', { count: files.length, defaultValue: '文件变更({{count}} 个)' })}
{t('generatedFiles.title', { count: files.length, defaultValue: 'File changes ({{count}})' })}
</p>
</div>
<div className="flex flex-wrap gap-2">
@@ -74,7 +74,7 @@ export function GeneratedFilesPanel({
className="shrink-0 rounded-full border border-black/8 bg-black/[0.045] px-1.5 py-0.5 text-2xs text-foreground/70 dark:border-white/10 dark:bg-white/[0.06] dark:text-foreground/75"
>
<FolderOpen className="mr-1 h-3 w-3" />
{t('generatedFiles.openFolder', '打开文件夹')}
{t('generatedFiles.openFolder', 'Open folder')}
</Badge>
</button>
);
@@ -110,8 +110,8 @@ export function GeneratedFilesPanel({
className="shrink-0 rounded-full border border-black/8 bg-black/[0.045] px-1.5 py-0.5 text-2xs text-foreground/70 dark:border-white/10 dark:bg-white/[0.06] dark:text-foreground/75"
>
{file.action === 'created'
? t('generatedFiles.created', '新增')
: t('generatedFiles.modified', '修改')}
? t('generatedFiles.created', 'Created')
: t('generatedFiles.modified', 'Modified')}
</Badge>
</button>
);

View File

@@ -34,7 +34,7 @@ export default function ImageViewer({ filePath, fileName, className }: ImageView
size="icon"
className="h-8 w-8 rounded-full shadow-md"
onClick={() => setZoomed((v) => !v)}
title={zoomed ? '缩小' : '原始尺寸'}
title={zoomed ? 'Zoom out' : 'Actual size'}
>
{zoomed ? <ZoomOut className="h-4 w-4" /> : <ZoomIn className="h-4 w-4" />}
</Button>

View File

@@ -88,7 +88,7 @@ export function PanelResizeDivider({ containerRef, className }: PanelResizeDivid
'group relative z-10 hidden w-1.5 shrink-0 cursor-col-resize select-none lg:block',
className,
)}
title="拖动调整宽度"
title="Drag to resize width"
>
{/* Hairline (visible all the time) */}
<span

View File

@@ -129,7 +129,7 @@ export default function PdfViewer({
if (currentState.status === 'tooLarge') {
return (
<div className={cn('flex h-full items-center justify-center px-6 text-center text-sm text-muted-foreground', className)}>
{t('filePreview.errors.tooLarge', '文件过大,已禁用预览')}
{t('filePreview.errors.tooLarge', 'File too large; preview disabled')}
</div>
);
}
@@ -137,7 +137,7 @@ export default function PdfViewer({
return (
<div className={cn('flex h-full flex-col items-center justify-center gap-2 px-6 text-center text-sm text-destructive', className)}>
<p>
{t('filePreview.pdf.loadFailed', { defaultValue: 'PDF 加载失败:{{error}}', error: currentState.message })}
{t('filePreview.pdf.loadFailed', { defaultValue: 'PDF failed to load: {{error}}', error: currentState.message })}
</p>
</div>
);
@@ -174,7 +174,7 @@ export default function PdfViewer({
>
<iframe
src={withViewerParams(currentState.url)}
title={fileName ?? t('filePreview.pdf.title', 'PDF 预览')}
title={fileName ?? t('filePreview.pdf.title', 'PDF preview')}
className={cn(
'h-full w-full border-0 bg-white transition-opacity duration-200',
workspaceSurface && 'rounded-lg shadow-sm ring-1 ring-black/10 dark:ring-white/10',

View File

@@ -173,7 +173,7 @@ export default function SheetViewer({ filePath, fileName, className }: SheetView
title={sheet.name}
style={{ maxWidth: 220 }}
>
{sheet.name || t('filePreview.sheet.unnamedSheet', { defaultValue: '工作表 {{idx}}', idx: idx + 1 })}
{sheet.name || t('filePreview.sheet.unnamedSheet', { defaultValue: 'Sheet {{idx}}', idx: idx + 1 })}
</button>
));
}, [state, sheetIndex, handleSelectSheet, t]);
@@ -188,14 +188,14 @@ export default function SheetViewer({ filePath, fileName, className }: SheetView
if (state.status === 'tooLarge') {
return (
<div className={cn('flex h-full items-center justify-center px-6 text-center text-sm text-muted-foreground', className)}>
{t('filePreview.errors.tooLarge', '文件过大,已禁用预览')}
{t('filePreview.errors.tooLarge', 'File too large; preview disabled')}
</div>
);
}
if (state.status === 'error') {
return (
<div className={cn('flex h-full flex-col items-center justify-center gap-2 px-6 text-center text-sm text-destructive', className)}>
<p>{t('filePreview.sheet.loadFailed', { defaultValue: '表格加载失败:{{error}}', error: state.message })}</p>
<p>{t('filePreview.sheet.loadFailed', { defaultValue: 'Spreadsheet failed to load: {{error}}', error: state.message })}</p>
</div>
);
}
@@ -203,7 +203,7 @@ export default function SheetViewer({ filePath, fileName, className }: SheetView
if (!activeSheet) {
return (
<div className={cn('flex h-full items-center justify-center text-sm text-muted-foreground', className)}>
{t('filePreview.sheet.empty', { defaultValue: '该文件没有可展示的工作表' })}
{t('filePreview.sheet.empty', { defaultValue: 'This file has no sheets to display' })}
</div>
);
}
@@ -215,7 +215,7 @@ export default function SheetViewer({ filePath, fileName, className }: SheetView
<div ref={scrollerRef} className="min-h-0 flex-1 overflow-auto">
{isEmpty ? (
<div className="flex h-full items-center justify-center px-6 text-center text-sm text-muted-foreground">
{t('filePreview.sheet.emptySheet', { defaultValue: '当前工作表为空' })}
{t('filePreview.sheet.emptySheet', { defaultValue: 'This sheet is empty' })}
</div>
) : (
<table className="w-max min-w-full border-collapse text-xs">
@@ -274,13 +274,13 @@ export default function SheetViewer({ filePath, fileName, className }: SheetView
className="h-6 w-6"
onClick={handlePrev}
disabled={page <= 0}
title={t('filePreview.sheet.prevPage', '上一页')}
title={t('filePreview.sheet.prevPage', 'Previous page')}
>
<ChevronLeft className="h-3.5 w-3.5" />
</Button>
<span className="tabular-nums">
{t('filePreview.sheet.rowRange', {
defaultValue: ' {{from}}-{{to}} / {{total}}',
defaultValue: 'Rows {{from}}-{{to}} / {{total}}',
from: startRow + 1,
to: Math.min(totalRows, startRow + ROWS_PER_PAGE),
total: totalRows,
@@ -292,7 +292,7 @@ export default function SheetViewer({ filePath, fileName, className }: SheetView
className="h-6 w-6"
onClick={handleNext}
disabled={page >= totalPages - 1}
title={t('filePreview.sheet.nextPage', '下一页')}
title={t('filePreview.sheet.nextPage', 'Next page')}
>
<ChevronRight className="h-3.5 w-3.5" />
</Button>

View File

@@ -70,7 +70,7 @@ export function SkillFileSections({ baseDir, onOpen, className }: SkillFileSecti
if (error) {
return (
<div className={cn('rounded-xl border border-destructive/20 bg-destructive/10 px-4 py-3 text-xs text-destructive', className)}>
{t('detail.sections.scanFailed', { defaultValue: '扫描技能目录失败' })}
{t('detail.sections.scanFailed', { defaultValue: 'Failed to scan skill directory' })}
</div>
);
}
@@ -78,7 +78,7 @@ export function SkillFileSections({ baseDir, onOpen, className }: SkillFileSecti
if (isSkillFileGroupsEmpty(groups)) {
return (
<div className={cn('rounded-xl border border-black/10 bg-black/5 px-4 py-3 text-xs text-muted-foreground dark:border-white/10 dark:bg-white/5', className)}>
{t('detail.sections.empty', { defaultValue: '此技能未包含可预览的文件。' })}
{t('detail.sections.empty', { defaultValue: 'This skill does not contain any previewable files.' })}
</div>
);
}
@@ -86,26 +86,26 @@ export function SkillFileSections({ baseDir, onOpen, className }: SkillFileSecti
return (
<div className={cn('space-y-5', className)}>
<SkillFileSection
title={t('detail.sections.docs', { defaultValue: '文档' })}
description={t('detail.sections.docsDesc', { defaultValue: 'SKILL.md 与说明文档' })}
title={t('detail.sections.docs', { defaultValue: 'Docs' })}
description={t('detail.sections.docsDesc', { defaultValue: 'SKILL.md and documentation' })}
files={groups.docs}
onOpen={onOpen}
/>
<SkillFileSection
title={t('detail.sections.scripts', { defaultValue: '脚本' })}
description={t('detail.sections.scriptsDesc', { defaultValue: '可执行的脚本与命令' })}
title={t('detail.sections.scripts', { defaultValue: 'Scripts' })}
description={t('detail.sections.scriptsDesc', { defaultValue: 'Executable scripts and commands' })}
files={groups.scripts}
onOpen={onOpen}
/>
<SkillFileSection
title={t('detail.sections.hooks', { defaultValue: 'Hooks' })}
description={t('detail.sections.hooksDesc', { defaultValue: '注入到 OpenClaw 生命周期的钩子' })}
description={t('detail.sections.hooksDesc', { defaultValue: 'Hooks injected into the OpenClaw lifecycle' })}
files={groups.hooks}
onOpen={onOpen}
/>
<SkillFileSection
title={t('detail.sections.assets', { defaultValue: '资源' })}
description={t('detail.sections.assetsDesc', { defaultValue: '模板、参考资料与静态资源' })}
title={t('detail.sections.assets', { defaultValue: 'Assets' })}
description={t('detail.sections.assetsDesc', { defaultValue: 'Templates, references, and static assets' })}
files={groups.assets}
onOpen={onOpen}
/>

View File

@@ -2,7 +2,7 @@
* Inline workspace browser body — left tree + right preview.
*
* Strictly scoped to the current agent's `agent.workspace` directory.
* Used by `ArtifactPanel`'s 浏览器 tab (split-pane on the chat page).
* Used by `ArtifactPanel`'s browser tab (split-pane on the chat page).
*/
import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react';
import { ChevronRight, FolderOpen, RefreshCw } from 'lucide-react';
@@ -44,7 +44,7 @@ const RICH_PREVIEW_MAX_BYTES = 50 * 1024 * 1024;
export interface WorkspaceBrowserBodyProps {
agent: AgentSummary | null;
/** Used to mark "本轮新增" badges on the tree. */
/** Used to mark "Added this run" badges on the tree. */
runStartedAt?: number | null;
/** Bumping this number triggers a tree reload (e.g. after AI run idles). */
refreshSignal?: number;
@@ -212,14 +212,14 @@ export function WorkspaceBrowserBody({
const handleOpenWorkspaceInFinder = useCallback(() => {
if (!workspace) return;
invokeIpc('shell:openPath', workspace).catch(() => {
toast.error(t('filePreview.errors.openInFinderFailed', '无法在文件管理器中显示'));
toast.error(t('filePreview.errors.openInFinderFailed', 'Could not reveal in file manager'));
});
}, [workspace, t]);
const handleOpenSelectedInFinder = useCallback(() => {
if (!selectedNode || selectedNode.isDir) return;
invokeIpc('shell:showItemInFolder', selectedNode.absPath).catch(() => {
toast.error(t('filePreview.errors.openInFinderFailed', '无法在文件管理器中显示'));
toast.error(t('filePreview.errors.openInFinderFailed', 'Could not reveal in file manager'));
});
}, [selectedNode, t]);
@@ -238,7 +238,7 @@ export function WorkspaceBrowserBody({
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
toast.error(t('filePreview.errors.openFailed', { defaultValue: '打开失败:{{error}}', error: message }));
toast.error(t('filePreview.errors.openFailed', { defaultValue: 'Open failed: {{error}}', error: message }));
}
}, [selectedNode, fileState, t]);
@@ -263,15 +263,15 @@ export function WorkspaceBrowserBody({
return (
<div className="px-4 py-6 text-xs text-destructive">
{state.message === 'outsideSandbox'
? t('filePreview.errors.outsideSandbox', '路径越界,已拒绝读取')
: t('workspace.empty', '工作空间为空或无法访问')}
? t('filePreview.errors.outsideSandbox', 'Path is outside the workspace; read denied')
: t('workspace.empty', 'Workspace is empty or inaccessible')}
</div>
);
}
return (
<div className="space-y-1 overflow-y-auto">
<div className="px-3 py-2 text-2xs uppercase tracking-wide text-muted-foreground">
{t('workspace.title', '工作空间')}
{t('workspace.title', 'Workspace')}
{agent?.name ? <span className="ml-1 text-foreground/60">· {agent.name}</span> : null}
</div>
<FileTreeNodeList
@@ -284,7 +284,7 @@ export function WorkspaceBrowserBody({
/>
{state.truncated && (
<div className="mt-2 px-3 py-2 text-2xs text-muted-foreground/80">
{t('workspace.truncated', '目录过大,已截断显示 5000 个节点')}
{t('workspace.truncated', 'Directory too large; truncated to first 5000 nodes')}
</div>
)}
</div>
@@ -295,7 +295,7 @@ export function WorkspaceBrowserBody({
if (!selectedNode || selectedNode.isDir) {
return (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
{t('workspace.pickFile', '从左侧选择一个文件预览')}
{t('workspace.pickFile', 'Select a file on the left to preview')}
</div>
);
}
@@ -342,20 +342,20 @@ export function WorkspaceBrowserBody({
<p>
{directOpen
? t('filePreview.errors.largeBinaryOpenHint', {
defaultValue: '当前文件为 {{size}}ClawX 不提供内置预览。你可以确认后直接使用系统默认应用打开。',
defaultValue: 'This file is {{size}}. ClawX does not provide an inline preview for it. You can confirm to open it directly in your system default app.',
size: formatFileSize(fileState.size ?? 0) || '> 2MB',
})
: t('filePreview.errors.tooLarge', '文件过大,已禁用预览')}
: t('filePreview.errors.tooLarge', 'File too large; preview disabled')}
</p>
<div className="flex flex-wrap items-center justify-center gap-2">
{directOpen && (
<Button size="sm" onClick={handleOpenSelectedDirectly}>
{t('filePreview.actions.openDirectly', '直接打开')}
{t('filePreview.actions.openDirectly', 'Open directly')}
</Button>
)}
<Button variant="outline" size="sm" onClick={handleOpenSelectedInFinder}>
<FolderOpen className="mr-2 h-4 w-4" />
{t('filePreview.actions.openInFinder', '在文件管理器中显示')}
{t('filePreview.actions.openInFinder', 'Show in file manager')}
</Button>
</div>
</div>
@@ -368,20 +368,20 @@ export function WorkspaceBrowserBody({
<p>
{directOpen
? t('filePreview.errors.largeBinaryOpenHint', {
defaultValue: '当前文件为 {{size}}ClawX 不提供内置预览。你可以确认后直接使用系统默认应用打开。',
defaultValue: 'This file is {{size}}. ClawX does not provide an inline preview for it. You can confirm to open it directly in your system default app.',
size: formatFileSize(fileState.size ?? 0) || '> 2MB',
})
: t('filePreview.errors.binary', '二进制文件不支持文本预览')}
: t('filePreview.errors.binary', 'Binary files do not support text preview')}
</p>
<div className="flex flex-wrap items-center justify-center gap-2">
{directOpen && (
<Button size="sm" onClick={handleOpenSelectedDirectly}>
{t('filePreview.actions.openDirectly', '直接打开')}
{t('filePreview.actions.openDirectly', 'Open directly')}
</Button>
)}
<Button variant="outline" size="sm" onClick={handleOpenSelectedInFinder}>
<FolderOpen className="mr-2 h-4 w-4" />
{t('filePreview.actions.openInFinder', '在文件管理器中显示')}
{t('filePreview.actions.openInFinder', 'Show in file manager')}
</Button>
</div>
</div>
@@ -390,9 +390,9 @@ export function WorkspaceBrowserBody({
if (fileState.status === 'error') {
const errMsg = fileState.message;
const hint = errMsg === 'outsideSandbox'
? t('filePreview.errors.outsideSandbox', '路径越界,已拒绝读取')
? t('filePreview.errors.outsideSandbox', 'Path is outside the workspace; read denied')
: errMsg === 'notFound'
? t('filePreview.errors.notFound', '文件不存在')
? t('filePreview.errors.notFound', 'File not found')
: errMsg;
return (
<div className="flex h-full items-center justify-center px-6 text-center text-sm text-destructive">
@@ -407,30 +407,30 @@ export function WorkspaceBrowserBody({
<div className="space-y-1.5">
<p className="text-sm font-medium text-foreground">
{directOpen
? t('filePreview.errors.largeBinaryOpenTitle', '该文件较大,暂不支持内置预览')
: t('filePreview.errors.unsupportedFormatTitle', '此文件格式暂不支持内置预览或变更')}
? t('filePreview.errors.largeBinaryOpenTitle', 'This file is too large for inline preview')
: t('filePreview.errors.unsupportedFormatTitle', 'This file format is not supported for inline preview or diff')}
</p>
<p className="max-w-md text-xs leading-relaxed text-muted-foreground">
{directOpen
? t('filePreview.errors.largeBinaryOpenHint', {
defaultValue: '当前文件为 {{size}}ClawX 不提供内置预览。你可以确认后直接使用系统默认应用打开。',
defaultValue: 'This file is {{size}}. ClawX does not provide an inline preview for it. You can confirm to open it directly in your system default app.',
size: formatFileSize(fileState.size ?? 0) || '> 2MB',
})
: t(
'filePreview.errors.unsupportedFormatHint',
'当前仅支持文本/Markdown 等可直接读取的文件进行内置预览与变更对比。请在文件管理器中打开该文件。',
'Only directly readable files such as text and Markdown support inline preview and diff. Please open this file in your file manager.',
)}
</p>
</div>
<div className="flex flex-wrap items-center justify-center gap-2">
{directOpen && (
<Button size="sm" onClick={handleOpenSelectedDirectly}>
{t('filePreview.actions.openDirectly', '直接打开')}
{t('filePreview.actions.openDirectly', 'Open directly')}
</Button>
)}
<Button variant="outline" size="sm" onClick={handleOpenSelectedInFinder}>
<FolderOpen className="mr-2 h-4 w-4" />
{t('filePreview.actions.openInFinder', '在文件管理器中显示')}
{t('filePreview.actions.openInFinder', 'Show in file manager')}
</Button>
</div>
</div>
@@ -468,7 +468,7 @@ export function WorkspaceBrowserBody({
>
<div className="flex min-w-0 items-center gap-3">
<h2 className="truncate text-sm font-semibold">
{t('workspace.title', '工作空间')}
{t('workspace.title', 'Workspace')}
{agent?.name ? <span className="ml-2 font-normal text-foreground/70">· {agent.name}</span> : null}
</h2>
{workspace && !compact ? (
@@ -483,11 +483,11 @@ export function WorkspaceBrowserBody({
size="sm"
className="h-7 px-2 text-xs"
onClick={() => setShowHidden((v) => !v)}
title={t('workspace.actions.toggleHidden', '显示/隐藏隐藏文件')}
title={t('workspace.actions.toggleHidden', 'Show/hide hidden files')}
>
{showHidden
? t('workspace.actions.hideHidden', '隐藏隐藏文件')
: t('workspace.actions.showHidden', '显示隐藏文件')}
? t('workspace.actions.hideHidden', 'Hide hidden files')
: t('workspace.actions.showHidden', 'Show hidden files')}
</Button>
<Button
variant="ghost"
@@ -495,7 +495,7 @@ export function WorkspaceBrowserBody({
className="h-7 w-7"
onClick={reload}
disabled={state.status === 'loading'}
title={t('workspace.actions.refresh', '刷新')}
title={t('workspace.actions.refresh', 'Refresh')}
>
<RefreshCw className={cn('h-3.5 w-3.5', state.status === 'loading' && 'animate-spin')} />
</Button>
@@ -505,7 +505,7 @@ export function WorkspaceBrowserBody({
size="icon"
className="h-7 w-7"
onClick={handleOpenWorkspaceInFinder}
title={t('workspace.actions.openRootInFinder', '在文件管理器中显示根目录')}
title={t('workspace.actions.openRootInFinder', 'Show root folder in file manager')}
>
<FolderOpen className="h-3.5 w-3.5 pointer-events-none" />
</Button>
@@ -532,7 +532,7 @@ export function WorkspaceBrowserBody({
<span className="truncate font-mono">{selectedNode.relPath || selectedNode.name}</span>
{selectedNode.isFresh && (
<Badge variant="default" className="ml-1 text-2xs px-1.5 py-0">
{t('workspace.freshBadge', '本轮新增')}
{t('workspace.freshBadge', 'Added this run')}
</Badge>
)}
</div>

View File

@@ -22,11 +22,11 @@ export async function confirmAndOpenFile(params: {
const sizeLabel = typeof size === 'number' ? formatFileSize(size) : null;
const detail = [
t('filePreview.confirmOpen.detail', {
defaultValue: '该文件将使用系统默认应用打开。',
defaultValue: 'This file will be opened with your system default app.',
}),
sizeLabel
? t('filePreview.confirmOpen.size', {
defaultValue: '文件大小:{{size}}',
defaultValue: 'File size: {{size}}',
size: sizeLabel,
})
: null,
@@ -36,15 +36,15 @@ export async function confirmAndOpenFile(params: {
const result = await invokeIpc<{ response?: number }>('dialog:message', {
type: 'question',
buttons: [
t('filePreview.confirmOpen.cancel', { defaultValue: '取消' }),
t('filePreview.actions.openDirectly', { defaultValue: '直接打开' }),
t('filePreview.confirmOpen.cancel', { defaultValue: 'Cancel' }),
t('filePreview.actions.openDirectly', { defaultValue: 'Open directly' }),
],
defaultId: 1,
cancelId: 0,
noLink: true,
title: t('filePreview.confirmOpen.title', { defaultValue: '打开文件' }),
title: t('filePreview.confirmOpen.title', { defaultValue: 'Open file' }),
message: t('filePreview.confirmOpen.message', {
defaultValue: '确认直接打开「{{fileName}}」?',
defaultValue: 'Open “{{fileName}}” directly?',
fileName,
}),
detail,

View File

@@ -127,8 +127,7 @@ test.describe('ClawX chat file changes', () => {
root.classList.remove('dark');
root.classList.add('light');
});
await expect(page.getByRole('button', { name: '工作空间' })).toHaveCount(0);
await expect(page.getByText('查看文件变更')).toHaveCount(0);
await expect(page.getByTestId('artifact-panel')).toHaveCount(0);
const fileCard = page.getByRole('button', { name: /demo\.ts/ }).first();
await expect(fileCard).toBeVisible({ timeout: 30_000 });
@@ -136,7 +135,9 @@ test.describe('ClawX chat file changes', () => {
await expect(fileCard).toContainText('-1');
await fileCard.click();
await expect(page.locator('aside').getByRole('button', { name: '工作空间' })).toHaveCount(0);
const sidePanel = page.getByTestId('artifact-panel');
await expect(sidePanel).toBeVisible({ timeout: 30_000 });
await expect(sidePanel.getByTestId('artifact-panel-tab-browser')).toBeVisible();
await expect(fileCard).toContainText('demo.ts');
const diffBackground = page.getByTestId('monaco-diff-viewer').locator('.monaco-editor-background').first();
@@ -221,7 +222,7 @@ test.describe('ClawX chat file changes', () => {
await sidePanel.getByTestId('artifact-panel-tab-browser').click();
await sidePanel.getByTestId('artifact-panel-tab-preview').click();
await expect(sidePanel.getByRole('heading', { name: 'SKILL.md' })).toBeVisible();
await expect(sidePanel.getByText('尚未选择文件')).toHaveCount(0);
await expect(sidePanel.getByText('No file selected')).toHaveCount(0);
} finally {
await closeElectronApp(app);
}

View File

@@ -97,11 +97,11 @@ describe('ArtifactPanel', () => {
expect(screen.getByTestId('file-preview-body')).toHaveTextContent('SKILL.md');
fireEvent.click(screen.getByRole('button', { name: '工作空间' }));
fireEvent.click(screen.getByRole('button', { name: 'Workspace' }));
expect(screen.getByTestId('workspace-browser')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '预览' }));
fireEvent.click(screen.getByRole('button', { name: 'Preview' }));
expect(screen.getByTestId('file-preview-body')).toHaveTextContent('SKILL.md');
expect(screen.queryByText('尚未选择文件')).not.toBeInTheDocument();
expect(screen.queryByText('No file selected')).not.toBeInTheDocument();
});
});

View File

@@ -45,14 +45,14 @@ describe('FilePreviewBody', () => {
/>,
);
const openButton = await screen.findByRole('button', { name: '直接打开' });
const openButton = await screen.findByRole('button', { name: 'Open directly' });
expect(openButton).toBeVisible();
fireEvent.click(openButton);
await waitFor(() => {
expect(invokeIpc).toHaveBeenCalledWith('dialog:message', expect.objectContaining({
buttons: expect.arrayContaining(['直接打开']),
buttons: expect.arrayContaining(['Open directly']),
}));
expect(invokeIpc).toHaveBeenCalledWith('shell:openPath', '/tmp/large-report.pdf');
});

View File

@@ -6,7 +6,7 @@ import type { GeneratedFile } from '@/lib/generated-files';
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (_key: string, params?: Record<string, unknown>) => {
if (typeof params?.count === 'number') return `文件变更(${params.count} 个)`;
if (typeof params?.count === 'number') return `File changes (${params.count})`;
return '';
},
}),