tavern-harness 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/package.json +1 -1
- package/sandbox-server.mjs +24 -7
- package/src/components/FileDisplayModal.tsx +4 -3
- package/src/components/WorkspaceFileManagerModal.tsx +66 -43
- package/src/core/tools/generatedSkillExecutor.ts +1 -1
- package/src/generated/changelog.json +75 -1
- package/src/store/store.ts +2 -2
- package/src/theme/views.css +18 -11
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,52 @@
|
|
|
3
3
|
> 本文件由 `scripts/changelog.mjs` 从 `changelog/v*.md` 生成,请勿手动编辑。
|
|
4
4
|
> 版本对比链接见文末;源文件位于仓库 `changelog/` 目录。
|
|
5
5
|
|
|
6
|
+
## [0.1.17] - 2026-10-25
|
|
7
|
+
|
|
8
|
+
### 中文
|
|
9
|
+
|
|
10
|
+
#### 改进
|
|
11
|
+
|
|
12
|
+
- 在工作区文件管理器中预览文件时不再关闭管理器:预览复用管理器的遮罩层,关闭预览即可回到原来的浏览位置
|
|
13
|
+
- 文件删除确认改为管理器内的弹出对话框,删除过程中按钮显示加载状态,失败时会提示原因,不再静默关闭
|
|
14
|
+
|
|
15
|
+
#### 修复
|
|
16
|
+
|
|
17
|
+
- 修复在 Termux / Android 环境下 shell 命令无法解析的问题:现在会从 Termux 的 `$PREFIX/bin` 中查找受信任的命令,并保留 `head` 等 applet 的原始名称
|
|
18
|
+
|
|
19
|
+
### English
|
|
20
|
+
|
|
21
|
+
#### Changed
|
|
22
|
+
|
|
23
|
+
- Previewing a file from the workspace file manager no longer closes the manager: the preview reuses the manager's overlay, and closing it returns you to the same browsing position
|
|
24
|
+
- Delete confirmation is now a dialog inside the manager; the delete button shows a loading state while the request runs, and failures surface the reason instead of closing silently
|
|
25
|
+
|
|
26
|
+
#### Fixed
|
|
27
|
+
|
|
28
|
+
- Fixed shell commands failing to resolve on Termux / Android: trusted commands are now looked up in Termux's `$PREFIX/bin`, and applets such as `head` keep their original name
|
|
29
|
+
|
|
30
|
+
## [0.1.16] - 2026-10-25
|
|
31
|
+
|
|
32
|
+
### 中文
|
|
33
|
+
|
|
34
|
+
#### 修复
|
|
35
|
+
|
|
36
|
+
- 修复工作区文件管理器在触屏设备上删除按钮不可见、文件夹无法删除的问题:删除按钮现在始终显示,文件夹及其内容可以一并删除,`public` 共享链接仍不可删除
|
|
37
|
+
|
|
38
|
+
#### 内部
|
|
39
|
+
|
|
40
|
+
- 工作区删除接口支持递归删除目录并拒绝删除 `public` 软链接,补充对应测试
|
|
41
|
+
|
|
42
|
+
### English
|
|
43
|
+
|
|
44
|
+
#### Fixed
|
|
45
|
+
|
|
46
|
+
- Fixed the delete button staying hidden on touch devices and folders not being deletable in the workspace file manager: the button is now always visible, folders and their contents can be removed together, and the `public` shared link remains protected
|
|
47
|
+
|
|
48
|
+
#### Internal
|
|
49
|
+
|
|
50
|
+
- The workspace delete endpoint now removes directories recursively and refuses to delete the `public` symlink, with tests
|
|
51
|
+
|
|
6
52
|
## [0.1.15] - 2026-10-25
|
|
7
53
|
|
|
8
54
|
### 中文
|
|
@@ -444,6 +490,8 @@
|
|
|
444
490
|
- Vite + React + Zustand + Dexie stack with browser-side local persistence
|
|
445
491
|
|
|
446
492
|
<!-- 链接定义 -->
|
|
493
|
+
[0.1.17]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.16...v0.1.17
|
|
494
|
+
[0.1.16]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.15...v0.1.16
|
|
447
495
|
[0.1.15]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.14...v0.1.15
|
|
448
496
|
[0.1.14]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.13...v0.1.14
|
|
449
497
|
[0.1.13]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.12...v0.1.13
|
package/package.json
CHANGED
package/sandbox-server.mjs
CHANGED
|
@@ -336,9 +336,12 @@ const SHELL_ALLOWLIST_GROUPS = {
|
|
|
336
336
|
};
|
|
337
337
|
const SHELL_ALLOWED_REAL = new Set(Object.values(SHELL_ALLOWLIST_GROUPS).flat());
|
|
338
338
|
|
|
339
|
+
const TERMUX_BIN = process.platform === 'android' && process.env.PREFIX
|
|
340
|
+
? join(process.env.PREFIX, 'bin')
|
|
341
|
+
: null;
|
|
339
342
|
const TRUSTED_COMMAND_DIRS = (IS_WIN
|
|
340
343
|
? [GIT_USR_BIN]
|
|
341
|
-
: ['/bin', '/usr/bin', '/usr/sbin', '/sbin'])
|
|
344
|
+
: ['/bin', '/usr/bin', '/usr/sbin', '/sbin', TERMUX_BIN])
|
|
342
345
|
.filter(Boolean)
|
|
343
346
|
.map((dir) => {
|
|
344
347
|
try {
|
|
@@ -358,10 +361,15 @@ function resolveTrustedCommand(command) {
|
|
|
358
361
|
try {
|
|
359
362
|
const resolved = realpathSync(candidate);
|
|
360
363
|
if (!resolved.startsWith(trustedDir + sep)) return null;
|
|
361
|
-
if (IS_WIN) return
|
|
364
|
+
if (IS_WIN) return candidate; // Windows 无 POSIX 权限位(Git usr/bin 的 exe 显示 666),跳过 uid/writable 校验
|
|
362
365
|
const executable = statSync(resolved);
|
|
363
366
|
const writableByNonOwner = (executable.mode & 0o022) !== 0;
|
|
364
|
-
|
|
367
|
+
const trustedSystemExecutable = executable.uid === 0 && !writableByNonOwner;
|
|
368
|
+
const trustedTermuxExecutable = trustedDir === TERMUX_BIN
|
|
369
|
+
&& executable.uid === process.getuid()
|
|
370
|
+
&& !writableByNonOwner;
|
|
371
|
+
// Preserve the applet path: resolving /system/bin/head to toybox loses the applet name.
|
|
372
|
+
if (trustedSystemExecutable || trustedTermuxExecutable) return candidate;
|
|
365
373
|
} catch { /* try next candidate */ }
|
|
366
374
|
}
|
|
367
375
|
}
|
|
@@ -670,11 +678,20 @@ const server = createServer(async (req, res) => {
|
|
|
670
678
|
throw new Error('Path is outside the workspace');
|
|
671
679
|
}
|
|
672
680
|
const fp = resolve(base, rel);
|
|
673
|
-
if (!existsSync(fp)
|
|
681
|
+
if (!existsSync(fp)) throw new Error('File or directory not found');
|
|
682
|
+
const entry = lstatSync(fp);
|
|
683
|
+
if (entry.isSymbolicLink() && rel === 'public') {
|
|
684
|
+
throw new Error('The public workspace link cannot be deleted');
|
|
685
|
+
}
|
|
686
|
+
if (!entry.isFile() && !entry.isDirectory()) throw new Error('Path is not a file or directory');
|
|
674
687
|
if (!deletingPublicLink) assertInside(base, realpathSync(fp));
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
688
|
+
if (entry.isDirectory()) {
|
|
689
|
+
rmSync(fp, { recursive: true, force: false });
|
|
690
|
+
} else {
|
|
691
|
+
await new Promise((resolveDelete, rejectDelete) => {
|
|
692
|
+
unlink(fp, (err) => (err ? rejectDelete(err) : resolveDelete()));
|
|
693
|
+
});
|
|
694
|
+
}
|
|
678
695
|
res.end(JSON.stringify({ ok: true, path: rel }));
|
|
679
696
|
} else {
|
|
680
697
|
// file_list:返回会话工作目录内的相对路径(分组目录)
|
|
@@ -128,7 +128,7 @@ export function FileDisplayModal() {
|
|
|
128
128
|
setTextWrap(true);
|
|
129
129
|
if (isNewDisplay) {
|
|
130
130
|
setPipRect(createPipRect());
|
|
131
|
-
setIsPictureInPicture(active.presentation
|
|
131
|
+
setIsPictureInPicture(active.presentation === 'pip' || active.presentation == null);
|
|
132
132
|
}
|
|
133
133
|
}, [active]);
|
|
134
134
|
|
|
@@ -263,6 +263,7 @@ export function FileDisplayModal() {
|
|
|
263
263
|
|
|
264
264
|
const title = active?.title?.trim() || (active ? basenameOf(active.path) : '');
|
|
265
265
|
const ext = active ? extensionOf(active.path) : '';
|
|
266
|
+
const isWorkspacePreview = active?.presentation === 'workspace';
|
|
266
267
|
|
|
267
268
|
const close = () => setActiveDisplay(null);
|
|
268
269
|
|
|
@@ -485,10 +486,10 @@ export function FileDisplayModal() {
|
|
|
485
486
|
|
|
486
487
|
return createPortal(
|
|
487
488
|
<>
|
|
488
|
-
{!isPictureInPicture && <div key="overlay" className="overlay" />}
|
|
489
|
+
{!isPictureInPicture && !isWorkspacePreview && <div key="overlay" className="overlay" />}
|
|
489
490
|
<div
|
|
490
491
|
key="display-container"
|
|
491
|
-
className={isPictureInPicture ? 'display-pip card' : 'modal-root'}
|
|
492
|
+
className={isPictureInPicture ? 'display-pip card' : 'modal-root display-modal-root'}
|
|
492
493
|
style={isPictureInPicture
|
|
493
494
|
? { left: pipRect.x, top: pipRect.y, width: pipRect.width, height: pipRect.height }
|
|
494
495
|
: undefined}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { Modal, Icon } from './shared';
|
|
3
|
-
import { DeleteConfirmDialog } from './DeleteConfirmDialog';
|
|
4
3
|
import { useT } from '../core/i18n';
|
|
5
4
|
import { applySessionWorkspace } from '../core/tools/toolExecutor';
|
|
6
5
|
import { deleteWorkspaceFile, listSessionWorkspaceFiles, openSessionWorkspace } from '../core/tools/generatedSkillExecutor';
|
|
@@ -11,7 +10,7 @@ import { useStore } from '../store/store';
|
|
|
11
10
|
// - 会话头部的工作目录标签点击打开
|
|
12
11
|
// - 只展示当前会话专属磁盘工作区(sandbox_workspace/)
|
|
13
12
|
// - 目录点击进入、支持返回上级与面包屑;文件点击复用 file_display 弹窗预览
|
|
14
|
-
// -
|
|
13
|
+
// - 可在确认后删除文件或目录(public 软链接除外)
|
|
15
14
|
// ============================================================
|
|
16
15
|
|
|
17
16
|
function extOf(path: string): string {
|
|
@@ -59,6 +58,7 @@ export function WorkspaceFileManagerModal({ sessionId, onClose }: { sessionId: n
|
|
|
59
58
|
const [files, setFiles] = useState<string[] | null>(null);
|
|
60
59
|
const [loadState, setLoadState] = useState<'loading' | 'ok' | 'empty' | 'error'>('loading');
|
|
61
60
|
const [pendingDelete, setPendingDelete] = useState<Node | null>(null);
|
|
61
|
+
const [deleting, setDeleting] = useState(false);
|
|
62
62
|
const [openingLocation, setOpeningLocation] = useState(false);
|
|
63
63
|
// 当前浏览目录('' = 根;否则为以 / 结尾的会话内相对路径)
|
|
64
64
|
const [dir, setDir] = useState('');
|
|
@@ -111,20 +111,29 @@ export function WorkspaceFileManagerModal({ sessionId, onClose }: { sessionId: n
|
|
|
111
111
|
kind: kindFromPath(node.path),
|
|
112
112
|
title: node.name,
|
|
113
113
|
sessionId,
|
|
114
|
-
presentation: '
|
|
114
|
+
presentation: 'workspace',
|
|
115
115
|
});
|
|
116
|
-
onClose();
|
|
117
116
|
};
|
|
118
117
|
|
|
119
118
|
const confirmDelete = async () => {
|
|
120
|
-
if (!pendingDelete) return;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
119
|
+
if (!pendingDelete || deleting) return;
|
|
120
|
+
setDeleting(true);
|
|
121
|
+
try {
|
|
122
|
+
const workspaceDir = await applySessionWorkspace(sessionId);
|
|
123
|
+
await deleteWorkspaceFile(pendingDelete.path, workspaceDir);
|
|
124
|
+
setFiles((current) => {
|
|
125
|
+
const next = (current ?? []).filter((path) => (
|
|
126
|
+
pendingDelete.isDir ? !path.startsWith(pendingDelete.path) : path !== pendingDelete.path
|
|
127
|
+
));
|
|
128
|
+
setLoadState(next.length === 0 ? 'empty' : 'ok');
|
|
129
|
+
return next;
|
|
130
|
+
});
|
|
131
|
+
setPendingDelete(null);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
addToast((error as Error).message, 'error');
|
|
134
|
+
} finally {
|
|
135
|
+
setDeleting(false);
|
|
136
|
+
}
|
|
128
137
|
};
|
|
129
138
|
|
|
130
139
|
const openWorkspaceLocation = async () => {
|
|
@@ -197,44 +206,58 @@ export function WorkspaceFileManagerModal({ sessionId, onClose }: { sessionId: n
|
|
|
197
206
|
<span className="ws-fm-name">..</span>
|
|
198
207
|
</div>
|
|
199
208
|
)}
|
|
200
|
-
{visible.map((n) =>
|
|
201
|
-
n.isDir
|
|
202
|
-
|
|
209
|
+
{visible.map((n) => {
|
|
210
|
+
const isPublicLink = n.isDir && n.path === 'public/';
|
|
211
|
+
return (
|
|
212
|
+
<div
|
|
213
|
+
key={n.path}
|
|
214
|
+
className="ws-fm-row"
|
|
215
|
+
role="button"
|
|
216
|
+
onClick={() => n.isDir ? setDir(n.path) : openFile(n)}
|
|
217
|
+
title={n.path}
|
|
218
|
+
>
|
|
203
219
|
<span className="ws-fm-arrow" />
|
|
204
|
-
<Icon name=
|
|
205
|
-
<span className=
|
|
220
|
+
<Icon name={n.isDir ? 'folder' : 'file'} size={14} />
|
|
221
|
+
<span className={`ws-fm-name${n.isDir ? '' : ' mono'}`}>{n.name}{n.isDir ? '/' : ''}</span>
|
|
222
|
+
{!isPublicLink && (
|
|
223
|
+
<span className="ws-fm-actions">
|
|
224
|
+
<button
|
|
225
|
+
className="icon-btn ws-fm-delete"
|
|
226
|
+
title={t('common.delete')}
|
|
227
|
+
aria-label={t('common.delete')}
|
|
228
|
+
onClick={(event) => {
|
|
229
|
+
event.stopPropagation();
|
|
230
|
+
setPendingDelete(n);
|
|
231
|
+
}}
|
|
232
|
+
><Icon name="trash" size={13} /></button>
|
|
233
|
+
</span>
|
|
234
|
+
)}
|
|
206
235
|
</div>
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
<span className="ws-fm-arrow" />
|
|
210
|
-
<Icon name="file" size={14} />
|
|
211
|
-
<span className="ws-fm-name mono">{n.name}</span>
|
|
212
|
-
<span className="ws-fm-actions">
|
|
213
|
-
<span className="ws-fm-open"><Icon name="eye" size={12} /></span>
|
|
214
|
-
<button
|
|
215
|
-
className="icon-btn ws-fm-delete"
|
|
216
|
-
title={t('common.delete')}
|
|
217
|
-
aria-label={t('common.delete')}
|
|
218
|
-
onClick={(event) => {
|
|
219
|
-
event.stopPropagation();
|
|
220
|
-
setPendingDelete(n);
|
|
221
|
-
}}
|
|
222
|
-
><Icon name="trash" size={13} /></button>
|
|
223
|
-
</span>
|
|
224
|
-
</div>
|
|
225
|
-
)
|
|
226
|
-
)}
|
|
236
|
+
);
|
|
237
|
+
})}
|
|
227
238
|
{visible.length === 0 && <div className="ws-fm-empty">{t('header.fileManagerEmpty')}</div>}
|
|
228
239
|
</div>
|
|
229
240
|
)}
|
|
230
241
|
</div>
|
|
231
242
|
{pendingDelete && (
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
243
|
+
<div className="ws-fm-delete-layer" role="dialog" aria-modal="true" aria-labelledby="ws-fm-delete-title">
|
|
244
|
+
<div className="ws-fm-delete-dialog card fade-up">
|
|
245
|
+
<div className="modal-head">
|
|
246
|
+
<span id="ws-fm-delete-title" style={{ fontWeight: 800, fontSize: 15 }}>{t('header.fileManagerDeleteTitle')}</span>
|
|
247
|
+
<button className="icon-btn" onClick={() => setPendingDelete(null)} disabled={deleting} title={t('common.close')}>
|
|
248
|
+
<Icon name="x" />
|
|
249
|
+
</button>
|
|
250
|
+
</div>
|
|
251
|
+
<div className="modal-body">{t('common.confirmDelete', { name: pendingDelete.path })}</div>
|
|
252
|
+
<div className="modal-foot">
|
|
253
|
+
<button className="btn" onClick={() => setPendingDelete(null)} disabled={deleting}>{t('common.cancel')}</button>
|
|
254
|
+
<button className="btn btn-danger" onClick={() => void confirmDelete()} disabled={deleting}>
|
|
255
|
+
{deleting ? <span className="spinner" style={{ width: 13, height: 13 }} /> : <Icon name="trash" size={13} />}
|
|
256
|
+
{t('common.delete')}
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
238
261
|
)}
|
|
239
262
|
</Modal>
|
|
240
263
|
);
|
|
@@ -462,7 +462,7 @@ export async function writeWorkspaceFileTextFor(
|
|
|
462
462
|
return diskFileWrite(normalizedPath, content, false, workspaceDir);
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
-
/**
|
|
465
|
+
/** 删除工作区中的文件或目录。 */
|
|
466
466
|
export async function deleteWorkspaceFile(path: string, workspaceDir: string): Promise<void> {
|
|
467
467
|
const normalizedPath = normalizePath(path);
|
|
468
468
|
if (!normalizedPath) throw new Error('Invalid path');
|
|
@@ -1,7 +1,81 @@
|
|
|
1
1
|
{
|
|
2
|
-
"latest": "0.1.
|
|
2
|
+
"latest": "0.1.17",
|
|
3
3
|
"repo": "https://github.com/Rannichan/Tavern-Harness",
|
|
4
4
|
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.1.17",
|
|
7
|
+
"date": "2026-10-25",
|
|
8
|
+
"breaking": false,
|
|
9
|
+
"compare": "v0.1.16...v0.1.17",
|
|
10
|
+
"groups": {
|
|
11
|
+
"zh-CN": [
|
|
12
|
+
{
|
|
13
|
+
"kind": "changed",
|
|
14
|
+
"items": [
|
|
15
|
+
"在工作区文件管理器中预览文件时不再关闭管理器:预览复用管理器的遮罩层,关闭预览即可回到原来的浏览位置",
|
|
16
|
+
"文件删除确认改为管理器内的弹出对话框,删除过程中按钮显示加载状态,失败时会提示原因,不再静默关闭"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"kind": "fixed",
|
|
21
|
+
"items": [
|
|
22
|
+
"修复在 Termux / Android 环境下 shell 命令无法解析的问题:现在会从 Termux 的 `$PREFIX/bin` 中查找受信任的命令,并保留 `head` 等 applet 的原始名称"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"en": [
|
|
27
|
+
{
|
|
28
|
+
"kind": "changed",
|
|
29
|
+
"items": [
|
|
30
|
+
"Previewing a file from the workspace file manager no longer closes the manager: the preview reuses the manager's overlay, and closing it returns you to the same browsing position",
|
|
31
|
+
"Delete confirmation is now a dialog inside the manager; the delete button shows a loading state while the request runs, and failures surface the reason instead of closing silently"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"kind": "fixed",
|
|
36
|
+
"items": [
|
|
37
|
+
"Fixed shell commands failing to resolve on Termux / Android: trusted commands are now looked up in Termux's `$PREFIX/bin`, and applets such as `head` keep their original name"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"version": "0.1.16",
|
|
45
|
+
"date": "2026-10-25",
|
|
46
|
+
"breaking": false,
|
|
47
|
+
"compare": "v0.1.15...v0.1.16",
|
|
48
|
+
"groups": {
|
|
49
|
+
"zh-CN": [
|
|
50
|
+
{
|
|
51
|
+
"kind": "fixed",
|
|
52
|
+
"items": [
|
|
53
|
+
"修复工作区文件管理器在触屏设备上删除按钮不可见、文件夹无法删除的问题:删除按钮现在始终显示,文件夹及其内容可以一并删除,`public` 共享链接仍不可删除"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"kind": "internal",
|
|
58
|
+
"items": [
|
|
59
|
+
"工作区删除接口支持递归删除目录并拒绝删除 `public` 软链接,补充对应测试"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"en": [
|
|
64
|
+
{
|
|
65
|
+
"kind": "fixed",
|
|
66
|
+
"items": [
|
|
67
|
+
"Fixed the delete button staying hidden on touch devices and folders not being deletable in the workspace file manager: the button is now always visible, folders and their contents can be removed together, and the `public` shared link remains protected"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"kind": "internal",
|
|
72
|
+
"items": [
|
|
73
|
+
"The workspace delete endpoint now removes directories recursively and refuses to delete the `public` symlink, with tests"
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
},
|
|
5
79
|
{
|
|
6
80
|
"version": "0.1.15",
|
|
7
81
|
"date": "2026-10-25",
|
package/src/store/store.ts
CHANGED
|
@@ -90,8 +90,8 @@ export interface ActiveDisplay {
|
|
|
90
90
|
title?: string;
|
|
91
91
|
/** 产生该展示的会话 id(读取文件时按其专属工作目录解析,null = 共享工作区) */
|
|
92
92
|
sessionId: number | null;
|
|
93
|
-
/**
|
|
94
|
-
presentation?: 'pip' | 'modal';
|
|
93
|
+
/** 初始展示方式;文件管理器预览复用其遮罩层,file_display 默认使用小窗 */
|
|
94
|
+
presentation?: 'pip' | 'modal' | 'workspace';
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/** 启动阶段:加载中 / 数据就绪 / 核心数据加载失败(可重试) */
|
package/src/theme/views.css
CHANGED
|
@@ -1409,9 +1409,13 @@
|
|
|
1409
1409
|
font-size: 13px;
|
|
1410
1410
|
}
|
|
1411
1411
|
/* ---------- 文件展示弹窗(file_display) ---------- */
|
|
1412
|
+
.display-modal-root {
|
|
1413
|
+
z-index: 902;
|
|
1414
|
+
}
|
|
1412
1415
|
.display-modal {
|
|
1413
1416
|
width: min(880px, calc(100vw - 32px));
|
|
1414
1417
|
height: min(82%, 720px);
|
|
1418
|
+
overflow: hidden;
|
|
1415
1419
|
}
|
|
1416
1420
|
.display-pip {
|
|
1417
1421
|
position: fixed;
|
|
@@ -1769,21 +1773,10 @@
|
|
|
1769
1773
|
align-items: center;
|
|
1770
1774
|
gap: 4px;
|
|
1771
1775
|
}
|
|
1772
|
-
.ws-fm-open {
|
|
1773
|
-
display: inline-flex;
|
|
1774
|
-
align-items: center;
|
|
1775
|
-
opacity: 0.55;
|
|
1776
|
-
}
|
|
1777
1776
|
.ws-fm-delete {
|
|
1778
1777
|
width: 26px;
|
|
1779
1778
|
height: 26px;
|
|
1780
1779
|
color: var(--text-faint);
|
|
1781
|
-
opacity: 0;
|
|
1782
|
-
}
|
|
1783
|
-
.ws-fm-row:hover .ws-fm-open,
|
|
1784
|
-
.ws-fm-row:hover .ws-fm-delete,
|
|
1785
|
-
.ws-fm-delete:focus-visible {
|
|
1786
|
-
opacity: 1;
|
|
1787
1780
|
}
|
|
1788
1781
|
.ws-fm-delete:hover {
|
|
1789
1782
|
color: var(--danger);
|
|
@@ -1799,6 +1792,20 @@
|
|
|
1799
1792
|
gap: 10px;
|
|
1800
1793
|
flex: 1;
|
|
1801
1794
|
}
|
|
1795
|
+
.ws-fm-delete-layer {
|
|
1796
|
+
position: fixed;
|
|
1797
|
+
inset: 0;
|
|
1798
|
+
z-index: 902;
|
|
1799
|
+
display: grid;
|
|
1800
|
+
place-items: center;
|
|
1801
|
+
padding: 16px;
|
|
1802
|
+
pointer-events: auto;
|
|
1803
|
+
}
|
|
1804
|
+
.ws-fm-delete-dialog {
|
|
1805
|
+
width: min(420px, 100%);
|
|
1806
|
+
background: var(--surface);
|
|
1807
|
+
box-shadow: var(--shadow-lg);
|
|
1808
|
+
}
|
|
1802
1809
|
|
|
1803
1810
|
/* ---------- 版本记录(设置 → 关于) ---------- */
|
|
1804
1811
|
.settings-about-section {
|