tavern-harness 0.1.15 → 0.1.16

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,28 @@
3
3
  > 本文件由 `scripts/changelog.mjs` 从 `changelog/v*.md` 生成,请勿手动编辑。
4
4
  > 版本对比链接见文末;源文件位于仓库 `changelog/` 目录。
5
5
 
6
+ ## [0.1.16] - 2026-10-25
7
+
8
+ ### 中文
9
+
10
+ #### 修复
11
+
12
+ - 修复工作区文件管理器在触屏设备上删除按钮不可见、文件夹无法删除的问题:删除按钮现在始终显示,文件夹及其内容可以一并删除,`public` 共享链接仍不可删除
13
+
14
+ #### 内部
15
+
16
+ - 工作区删除接口支持递归删除目录并拒绝删除 `public` 软链接,补充对应测试
17
+
18
+ ### English
19
+
20
+ #### Fixed
21
+
22
+ - 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
23
+
24
+ #### Internal
25
+
26
+ - The workspace delete endpoint now removes directories recursively and refuses to delete the `public` symlink, with tests
27
+
6
28
  ## [0.1.15] - 2026-10-25
7
29
 
8
30
  ### 中文
@@ -444,6 +466,7 @@
444
466
  - Vite + React + Zustand + Dexie stack with browser-side local persistence
445
467
 
446
468
  <!-- 链接定义 -->
469
+ [0.1.16]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.15...v0.1.16
447
470
  [0.1.15]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.14...v0.1.15
448
471
  [0.1.14]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.13...v0.1.14
449
472
  [0.1.13]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.12...v0.1.13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tavern-harness",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Tavern Harness — 酒馆模式下的本地 AI Agent 助手(MyAgent-Android 的 Web 复刻)",
5
5
  "private": false,
6
6
  "type": "module",
@@ -670,11 +670,20 @@ const server = createServer(async (req, res) => {
670
670
  throw new Error('Path is outside the workspace');
671
671
  }
672
672
  const fp = resolve(base, rel);
673
- if (!existsSync(fp) || !lstatSync(fp).isFile()) throw new Error('File not found');
673
+ if (!existsSync(fp)) throw new Error('File or directory not found');
674
+ const entry = lstatSync(fp);
675
+ if (entry.isSymbolicLink() && rel === 'public') {
676
+ throw new Error('The public workspace link cannot be deleted');
677
+ }
678
+ if (!entry.isFile() && !entry.isDirectory()) throw new Error('Path is not a file or directory');
674
679
  if (!deletingPublicLink) assertInside(base, realpathSync(fp));
675
- await new Promise((resolveDelete, rejectDelete) => {
676
- unlink(fp, (err) => (err ? rejectDelete(err) : resolveDelete()));
677
- });
680
+ if (entry.isDirectory()) {
681
+ rmSync(fp, { recursive: true, force: false });
682
+ } else {
683
+ await new Promise((resolveDelete, rejectDelete) => {
684
+ unlink(fp, (err) => (err ? rejectDelete(err) : resolveDelete()));
685
+ });
686
+ }
678
687
  res.end(JSON.stringify({ ok: true, path: rel }));
679
688
  } else {
680
689
  // file_list:返回会话工作目录内的相对路径(分组目录)
@@ -11,7 +11,7 @@ import { useStore } from '../store/store';
11
11
  // - 会话头部的工作目录标签点击打开
12
12
  // - 只展示当前会话专属磁盘工作区(sandbox_workspace/)
13
13
  // - 目录点击进入、支持返回上级与面包屑;文件点击复用 file_display 弹窗预览
14
- // - 可在确认后删除文件(不删除目录)
14
+ // - 可在确认后删除文件或目录(public 软链接除外)
15
15
  // ============================================================
16
16
 
17
17
  function extOf(path: string): string {
@@ -121,7 +121,9 @@ export function WorkspaceFileManagerModal({ sessionId, onClose }: { sessionId: n
121
121
  const workspaceDir = await applySessionWorkspace(sessionId);
122
122
  await deleteWorkspaceFile(pendingDelete.path, workspaceDir);
123
123
  setFiles((current) => {
124
- const next = (current ?? []).filter((path) => path !== pendingDelete.path);
124
+ const next = (current ?? []).filter((path) => (
125
+ pendingDelete.isDir ? !path.startsWith(pendingDelete.path) : path !== pendingDelete.path
126
+ ));
125
127
  setLoadState(next.length === 0 ? 'empty' : 'ok');
126
128
  return next;
127
129
  });
@@ -197,33 +199,35 @@ export function WorkspaceFileManagerModal({ sessionId, onClose }: { sessionId: n
197
199
  <span className="ws-fm-name">..</span>
198
200
  </div>
199
201
  )}
200
- {visible.map((n) =>
201
- n.isDir ? (
202
- <div key={n.path} className="ws-fm-row" role="button" onClick={() => setDir(n.path)} title={n.path}>
202
+ {visible.map((n) => {
203
+ const isPublicLink = n.isDir && n.path === 'public/';
204
+ return (
205
+ <div
206
+ key={n.path}
207
+ className="ws-fm-row"
208
+ role="button"
209
+ onClick={() => n.isDir ? setDir(n.path) : openFile(n)}
210
+ title={n.path}
211
+ >
203
212
  <span className="ws-fm-arrow" />
204
- <Icon name="folder" size={14} />
205
- <span className="ws-fm-name">{n.name}/</span>
213
+ <Icon name={n.isDir ? 'folder' : 'file'} size={14} />
214
+ <span className={`ws-fm-name${n.isDir ? '' : ' mono'}`}>{n.name}{n.isDir ? '/' : ''}</span>
215
+ {!isPublicLink && (
216
+ <span className="ws-fm-actions">
217
+ <button
218
+ className="icon-btn ws-fm-delete"
219
+ title={t('common.delete')}
220
+ aria-label={t('common.delete')}
221
+ onClick={(event) => {
222
+ event.stopPropagation();
223
+ setPendingDelete(n);
224
+ }}
225
+ ><Icon name="trash" size={13} /></button>
226
+ </span>
227
+ )}
206
228
  </div>
207
- ) : (
208
- <div key={n.path} className="ws-fm-row" role="button" onClick={() => openFile(n)} title={n.path}>
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
- )}
229
+ );
230
+ })}
227
231
  {visible.length === 0 && <div className="ws-fm-empty">{t('header.fileManagerEmpty')}</div>}
228
232
  </div>
229
233
  )}
@@ -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,43 @@
1
1
  {
2
- "latest": "0.1.15",
2
+ "latest": "0.1.16",
3
3
  "repo": "https://github.com/Rannichan/Tavern-Harness",
4
4
  "entries": [
5
+ {
6
+ "version": "0.1.16",
7
+ "date": "2026-10-25",
8
+ "breaking": false,
9
+ "compare": "v0.1.15...v0.1.16",
10
+ "groups": {
11
+ "zh-CN": [
12
+ {
13
+ "kind": "fixed",
14
+ "items": [
15
+ "修复工作区文件管理器在触屏设备上删除按钮不可见、文件夹无法删除的问题:删除按钮现在始终显示,文件夹及其内容可以一并删除,`public` 共享链接仍不可删除"
16
+ ]
17
+ },
18
+ {
19
+ "kind": "internal",
20
+ "items": [
21
+ "工作区删除接口支持递归删除目录并拒绝删除 `public` 软链接,补充对应测试"
22
+ ]
23
+ }
24
+ ],
25
+ "en": [
26
+ {
27
+ "kind": "fixed",
28
+ "items": [
29
+ "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"
30
+ ]
31
+ },
32
+ {
33
+ "kind": "internal",
34
+ "items": [
35
+ "The workspace delete endpoint now removes directories recursively and refuses to delete the `public` symlink, with tests"
36
+ ]
37
+ }
38
+ ]
39
+ }
40
+ },
5
41
  {
6
42
  "version": "0.1.15",
7
43
  "date": "2026-10-25",
@@ -1769,21 +1769,10 @@
1769
1769
  align-items: center;
1770
1770
  gap: 4px;
1771
1771
  }
1772
- .ws-fm-open {
1773
- display: inline-flex;
1774
- align-items: center;
1775
- opacity: 0.55;
1776
- }
1777
1772
  .ws-fm-delete {
1778
1773
  width: 26px;
1779
1774
  height: 26px;
1780
1775
  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
1776
  }
1788
1777
  .ws-fm-delete:hover {
1789
1778
  color: var(--danger);