tavern-harness 0.1.16 → 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 +25 -0
- package/package.json +1 -1
- package/sandbox-server.mjs +11 -3
- package/src/components/FileDisplayModal.tsx +4 -3
- package/src/components/WorkspaceFileManagerModal.tsx +38 -19
- package/src/generated/changelog.json +39 -1
- package/src/store/store.ts +2 -2
- package/src/theme/views.css +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
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
|
+
|
|
6
30
|
## [0.1.16] - 2026-10-25
|
|
7
31
|
|
|
8
32
|
### 中文
|
|
@@ -466,6 +490,7 @@
|
|
|
466
490
|
- Vite + React + Zustand + Dexie stack with browser-side local persistence
|
|
467
491
|
|
|
468
492
|
<!-- 链接定义 -->
|
|
493
|
+
[0.1.17]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.16...v0.1.17
|
|
469
494
|
[0.1.16]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.15...v0.1.16
|
|
470
495
|
[0.1.15]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.14...v0.1.15
|
|
471
496
|
[0.1.14]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.13...v0.1.14
|
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
|
}
|
|
@@ -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';
|
|
@@ -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,22 +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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
+
}
|
|
130
137
|
};
|
|
131
138
|
|
|
132
139
|
const openWorkspaceLocation = async () => {
|
|
@@ -233,12 +240,24 @@ export function WorkspaceFileManagerModal({ sessionId, onClose }: { sessionId: n
|
|
|
233
240
|
)}
|
|
234
241
|
</div>
|
|
235
242
|
{pendingDelete && (
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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>
|
|
242
261
|
)}
|
|
243
262
|
</Modal>
|
|
244
263
|
);
|
|
@@ -1,7 +1,45 @@
|
|
|
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
|
+
},
|
|
5
43
|
{
|
|
6
44
|
"version": "0.1.16",
|
|
7
45
|
"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;
|
|
@@ -1788,6 +1792,20 @@
|
|
|
1788
1792
|
gap: 10px;
|
|
1789
1793
|
flex: 1;
|
|
1790
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
|
+
}
|
|
1791
1809
|
|
|
1792
1810
|
/* ---------- 版本记录(设置 → 关于) ---------- */
|
|
1793
1811
|
.settings-about-section {
|