chore: remove Chinese text from file preview components (#987)
This commit is contained in:
@@ -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 run’s 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 run’s file cards or “查看文件变更 →” (auto-first); switching workspace
|
||||
* the run’s 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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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 '';
|
||||
},
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user