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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tavern-harness",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Tavern Harness — 酒馆模式下的本地 AI Agent 助手(MyAgent-Android 的 Web 复刻)",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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 resolved; // Windows 无 POSIX 权限位(Git usr/bin 的 exe 显示 666),跳过 uid/writable 校验
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
- if (executable.uid === 0 && !writableByNonOwner) return resolved;
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 !== 'modal');
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: 'modal',
114
+ presentation: 'workspace',
115
115
  });
116
- onClose();
117
116
  };
118
117
 
119
118
  const confirmDelete = async () => {
120
- if (!pendingDelete) return;
121
- const workspaceDir = await applySessionWorkspace(sessionId);
122
- await deleteWorkspaceFile(pendingDelete.path, workspaceDir);
123
- setFiles((current) => {
124
- const next = (current ?? []).filter((path) => (
125
- pendingDelete.isDir ? !path.startsWith(pendingDelete.path) : path !== pendingDelete.path
126
- ));
127
- setLoadState(next.length === 0 ? 'empty' : 'ok');
128
- return next;
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
- <DeleteConfirmDialog
237
- title={t('header.fileManagerDeleteTitle')}
238
- itemName={pendingDelete.path}
239
- onCancel={() => setPendingDelete(null)}
240
- onConfirm={confirmDelete}
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.16",
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",
@@ -90,8 +90,8 @@ export interface ActiveDisplay {
90
90
  title?: string;
91
91
  /** 产生该展示的会话 id(读取文件时按其专属工作目录解析,null = 共享工作区) */
92
92
  sessionId: number | null;
93
- /** 初始展示方式;文件管理器使用大弹窗,file_display 默认使用小窗 */
94
- presentation?: 'pip' | 'modal';
93
+ /** 初始展示方式;文件管理器预览复用其遮罩层,file_display 默认使用小窗 */
94
+ presentation?: 'pip' | 'modal' | 'workspace';
95
95
  }
96
96
 
97
97
  /** 启动阶段:加载中 / 数据就绪 / 核心数据加载失败(可重试) */
@@ -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 {