dsh-file-activity 0.4.6

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 ADDED
@@ -0,0 +1,90 @@
1
+ # Changelog
2
+
3
+ 本文件记录 dsh-file-activity 的所有版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
+
5
+ ## [0.4.6] - 2026-08-25
6
+
7
+ ### 变更
8
+
9
+ - **Server 端按 P2 模块拆分**:`lib/index.js`(434 行)按职责拆分为 fence/state/store/observer/api-route/media-route 子模块(入口 50 行),覆盖率/变异统计范围同步扩展至全部 server 文件(Stryker 变异 90.91% ≥ 70%)。
10
+ - **Client 端方案 B 拆分**:`client.src.js` 模板 + 13 个片段(`lib/parts/`)经 `scripts/build.mjs` 拼接生成 `client.js`;eslint 尺寸规则覆盖 src/parts 源码、构建产物排除。
11
+ - **client-render 测试零依赖化**:不再硬编码本机 react 绝对路径,改为自写 createElement stub(CI 跨平台可跑)。
12
+ - 行为不变(纯重构)。
13
+
14
+ ## [0.4.5] - 2026-08-24
15
+
16
+ ### 修复
17
+
18
+ - **状态文件 `sessions` 为 `null` 时崩溃**:`loadState` 只检查 `typeof sessions !== 'object'`,而 `typeof null === 'object'`——手改/损坏的状态文件若把 `sessions` 写成 `null`,加载后 `Object.values(null)` 抛 TypeError(unhandled rejection)。现在显式把 `null` 一并重置为空对象,降级为全新状态不崩溃(复现测试 `host-mutation.mjs` 已永久保留)。
19
+
20
+ ### 变更
21
+
22
+ - **质量门禁测试体系接入**:测试框架迁移至 Vitest(node:test → vitest,API 兼容);覆盖率门禁 `lines≥85 / branches≥75 / functions≥80`(当前实测 99.1/91.8/97.5);新增边界测试 `host-edge.mjs`(12 用例)与变异定向测试 `host-mutation.mjs`(49 用例);Stryker 变异测试达标(71.76% ≥ 70%);Gherkin 验收测试 11 场景/56 步骤(`test/features/`)随 `npm test` 运行。
23
+
24
+ ## [0.4.4] - 2026-08-24
25
+
26
+ ### 修复
27
+
28
+ - **工作区外文件的图片/PDF 预览不再破图**:浮动预览的媒体字节此前走侧边栏的 `/sidebar/file` 路由,而该路由只允许会话工作区(cwd)内的文件——agent 实际触碰过的 `/tmp` 临时文件等工作区外路径点击预览时一律 403,图片显示为破图、PDF 加载失败。现在媒体字节改由插件自身的 `/file-activity/file` 路由提供(**只放行该会话记录过的路径**:未记录 403、文件不存在 404、超 64MB 拒绝;fence 信任校验与侧边栏一致),图片在浮窗内正常渲染;PDF 因侧边栏内置查看器写死媒体路由,浮窗改用浏览器原生 PDF 帧 + 下载兜底。
29
+ - Markdown / 代码 / HTML 文本预览不受影响(侧边栏 `fs.read` 本身不限制工作区,`client-render.mjs` + 真实浏览器实测回归通过)。
30
+
31
+ ## [0.4.3] - 2026-08-23
32
+
33
+ ### 变更
34
+
35
+ - **README 补充现场效果图**:顶部新增真实现场效果图(侧边栏「文件活动」页签 最近访问/文件统计树 + 点击文件的浮窗预览),便于其他用户下载前了解功能。
36
+ - **发布包包含效果图**:`package.json` 的 `files` 加入 `assets/`,使 GitHub Release 的 tarball 一并包含效果图资源。
37
+
38
+ ## [0.4.2] - 2026-08-23
39
+
40
+ ### 修复
41
+
42
+ - **修复页签偶发"纯文字无样式"**:样式注入曾放在 `betterSidebar` 服务判空早退之后,HMR 重建/服务重载瞬间新实例可能跳过注入,导致已渲染的页签失去全部 CSS(纯白文本列表)。样式注入改为 `apply` 无条件最先执行(纯静态 CSS,不依赖任何服务),每个 fiber 持有自己的 `<style>` 元素,重建后至少保留一份。
43
+
44
+ ## [0.4.1] - 2026-08-23
45
+
46
+ ### 修复
47
+
48
+ - **会话切换不再残留上个会话的文件活动**:前端数据 store 改为按 `sessionId` 分桶(`bySession`),每个会话只渲染自己的数据桶——新建/切换会话瞬间立即显示该会话自己的记录(新会话直接为空),不再在 fetch 完成前显示上一个会话的残留数据;fetch 失败时也不再保留旧数据。切换会话时同时关闭上一个会话遗留的浮窗预览。
49
+
50
+ ## [0.4.0] - 2026-08-22
51
+
52
+ ### 变更
53
+
54
+ - **预览改为浮窗预览(复用侧边栏内置渲染)**:点击任意文件(最近访问 / 文件统计)弹出**悬浮预览窗**,窗口内容通过 `ctx.betterSidebar.matchFileViewer(path)` 复用侧边栏**内置的 file viewer** 渲染(代码语法高亮 / Markdown 富文本 / 图片 / PDF / HTML),并按 viewer 的 `fetchStrategy` 取内容(`fsRead` / `mediaUrl` / `custom`);不再打开侧边栏编辑器标签,也不自建预览组件。浮窗为轻量交互:**点击浮窗外任意处 / `Esc` 键关闭**,标题栏右上保留一个 `×` 关闭按钮;主体为可滚动区域,长文件或大图在窗内滚动查看。
55
+ - **UI 全面改版**(遵循 better-sidebar 设计语言):全部改用 DSH 语义 token(`--dsw-alias-*` 颜色、`--dsw-font-*` 字形、`--ds-*` 动效),flat 无阴影、hairline 细边、8px 圆角行 + hover 底色;样式表随 activation 注入、fiber teardown 卸载(HMR/禁用无残留)。
56
+ - **间距收紧 + 去冗余标题**:整体走 better-sidebar explorer 的紧凑规范(`2px 6px 8px` 容器、30px 行、box-sizing 缩进),移除内容区顶部的「文件活动」标题(tab 条已命名本页),刷新/清空改为右上角细颗粒图标按钮,内容紧贴 tab 条(更沉浸)。
57
+ - **文件夹 / 文件图标可区分**:目录行用**品牌强调色**的文件夹图标 + 折叠箭头 + **加粗**名字,文件行用**中性淡色**的文件图标 + 常规名字,彩色文件夹 vs 素色文档,一眼可辨。
58
+ - **最近访问只显示文件名**:不再展示完整绝对路径,只显示最后的文件名(悬停仍可见完整路径),减少视觉噪音。
59
+ - 操作类型用状态色徽标(新增 = success / 修改 = warn / 读取 = 品牌强调色),文件统计目录树与文件行改为彩色计数胶囊;文件/文件夹行加入图标与折叠箭头。
60
+ - 时间显示从 `HH:MM:SS` 改为**相对时间**(刚刚 / N 分钟前 / N 小时前 / N 天前 / MM/DD),悬停仍可查看完整时钟时间。
61
+
62
+ ## [0.3.0] - 2026-08-22
63
+
64
+ ### 变更
65
+
66
+ - 最近访问列表改为 **LRU**:同一文件只保留一条(重复访问移到列表最前、不重复显示),每会话最多保留 **5 条**(原 10 条);历史数据在加载时自动去重。
67
+ - 时间显示改为 **`HH:MM:SS` 时钟格式**(最近访问列表、文件统计行内时间、悬停提示的创建/最近访问时间),不再显示相对时间("刚刚 / x 分钟前")。
68
+ - **最近访问区块可折叠**:点击区块标题展开/收起列表。
69
+ - **文件统计目录树可收纳**:每个文件夹行可点击展开/收起(箭头指示状态),收起后隐藏其子目录与文件。
70
+
71
+ ## [0.2.0] - 2026-08-22
72
+
73
+ ### 变更
74
+
75
+ - 最近访问列表最多保留 **10 条**(原 300 条),避免列表过长。
76
+ - 文件统计新增时间信息:每个文件行内显示**最近访问时间**(相对时间),悬停可查看**创建时间**(首次接触时间)。
77
+ - 文件统计改为**树形目录**展示:按文件**绝对路径**组织成完整目录树(逐层嵌套,目录节点显示子树汇总计数),连续单子目录的路径链自动**压缩为点号标签**(`a/b/c` → `a.b.c`),不再使用扁平的「根目录」分组与点号拼接标签。
78
+ - 点击文件改为打开**浮窗预览**(不再在侧边栏 tab 栏打开):图片走媒体路由渲染、PDF 内嵌 iframe、文本/代码经 `fs.read` 滚动展示;浮窗内提供「在侧边栏打开」与「关闭」按钮。
79
+ - 浮窗样式改用 DSH 主题变量(`--dsw-alias-*`),深浅主题下均正确显示;文本加载失败时展示具体错误原因(如文件不存在),并支持相对路径按会话 cwd 解析。
80
+ - 浮窗**自动关闭**:鼠标移出浮窗约 0.5 秒后自动消失(移回则取消),无需手动点击关闭。
81
+
82
+ ## [0.1.0] - 2026-08-22
83
+
84
+ ### 新增
85
+
86
+ - 首个版本:基于 dsh-better-sidebar 的「文件活动」侧边栏页签。
87
+ - **最近访问**:按时间倒序记录文件读取 / 新增 / 修改事件,点击文件用侧边栏原生预览打开。
88
+ - **文件统计**:按文件统计读取 / 新增 / 修改次数,按文件夹平铺展示(多层文件夹以 `.` 拼接)。
89
+ - **默认启用 + 自动打开**:页签默认开启,会话首次打开自动展开(可在设置中关闭)。
90
+ - 状态按会话持久化到 `$DSH_HOME/file-activity.json`(防抖 + 原子写入)。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bsfeng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # dsh-file-activity
2
+
3
+ [![插件生态](https://img.shields.io/badge/插件生态-topic%20dsh--better--sidebar-4d6bfe)](https://github.com/topics/dsh-better-sidebar)
4
+
5
+ <div align="center">
6
+ <img alt="文件活动插件截图(最近访问 / 文件统计)" src="./assets/screenshot.png" width="340" />
7
+ <br />
8
+ <img alt="浮窗预览:点击文件复用侧边栏内置 Markdown 渲染" src="./assets/preview-float.png" width="340" />
9
+ </div>
10
+
11
+ **DSH 侧边栏文件活动插件**(基于 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 扩展):在 better-sidebar 中新增「文件活动」页签,记录 agent 工具与侧边栏自身的文件读取/新增/修改事件,提供**最近访问**(LRU)与**文件统计**(树形目录)两块视图,点击文件弹出浮窗预览。
12
+
13
+ ## 功能
14
+
15
+ 在 better-sidebar 中新增「文件活动」页签:
16
+
17
+ 1. **最近访问**:按 LRU 记录文件读取 / 新增 / 修改事件(agent 工具读写 + 侧边栏打开/编辑保存都会记录),**每会话最多保留 5 条,同一文件只出现一次**(再次访问会移到列表最前);区块标题可**点击折叠/展开**;点击任意文件即弹出**浮窗预览**,浮窗内**复用侧边栏内置渲染**(代码高亮 / Markdown 渲染 / 图片 / PDF / HTML,见下方「浮窗预览」)。
18
+ 2. **文件统计**:每个文件的「读取 / 新增 / 修改」次数,按文件**绝对路径**组织成**树形目录**;连续单子目录的路径链自动**压缩为点号标签**(如 `a/b/c` → `a.b.c`),文件直接列在下方;**每个文件夹行可点击展开/收起**(箭头指示状态);每个文件行内显示**相对时间**(刚刚 / N 分钟前 / N 小时前 / N 天前),悬停可查看**创建 / 最近访问**的完整时钟时间:
19
+
20
+ ```
21
+ Users/ ← 树根 = 绝对路径根(/)
22
+ bsfeng.IdeaProjects.my-dsh-plugins ← 连续单子目录压缩为一行
23
+ README.md
24
+ lib/
25
+ index.js
26
+ client.js
27
+ ```
28
+
29
+ 3. **默认启用**:页签注册后默认开启(无需在设置页勾选),且每个会话首次打开时**自动打开**本页(可在侧边栏设置中关闭自动打开)。
30
+
31
+ ## 工作原理
32
+
33
+ - **Server 端**(`lib/index.js`):监听 DSH `fs/observed` 事件捕获 agent 的 `read` / `write` / `edit` / `str_replace_editor` / `read_image` 等文件操作;提供 `/file-activity/api` 路由(`stats` / `record` / `clear`);状态按会话持久化到 `$DSH_HOME/file-activity.json`(防抖 + 原子写入)。
34
+ - `write` 通过每会话的已知文件表自动区分**新增**(首次接触)与**修改**(再次写入)。
35
+ - **Client 端**(`lib/client.js`):通过 `ctx.betterSidebar.registerTab` 注册页签;fetch 拦截捕获侧边栏自身操作(`/sidebar/api/fs.read`、`/sidebar/api/fs.write`、`/sidebar/file` 媒体预览)并上报 server;数据按会话(session)**分桶隔离**(`bySession`),每个会话只渲染自己的记录——新建/切换会话立即显示该会话的数据,**不会残留上一个会话的记录**;点击文件通过 `ctx.betterSidebar.matchFileViewer(path)` 匹配内置 viewer 并挂载其组件,打开浮窗预览。
36
+
37
+ ## 安装
38
+
39
+ 前置:已安装 `dsh-better-sidebar`(v0.12+,推荐 v0.14)。
40
+
41
+ ```sh
42
+ # 方式一:dsh plugin(推荐)
43
+ # 1) 克隆本仓库(任意目录)
44
+ git clone https://github.com/baosfeng/my-dsh-plugins.git
45
+ # 2) 以本地 link 方式安装(将 <仓库路径> 替换为上面的克隆目录)
46
+ dsh plugin --profile web add link:<仓库路径>/plugins/dsh-file-activity
47
+
48
+ # 方式二:手动
49
+ # 1) 克隆本仓库后,在 ~/.dsh/profiles/web/package.json 的 dependencies 增加:
50
+ # "dsh-file-activity": "link:<仓库路径>/plugins/dsh-file-activity"
51
+ # 2) 在 ~/.dsh/profiles/web 下执行 pnpm install
52
+ # 3) 在 ~/.dsh/profiles/web/cordis.patch.yml 增加:
53
+ # - insert:
54
+ # - id: file-activity
55
+ # name: 'dsh-file-activity'
56
+ ```
57
+
58
+ 装完后**硬刷新浏览器**(Cmd/Ctrl+Shift+R)。
59
+
60
+ > 编辑 profile 的 `cordis.patch.yml` 会在运行中通过 Cordis HMR 热挂载 server 端,无需重启 `dsh web`;client 端在页面刷新后生效。
61
+
62
+ ## 使用
63
+
64
+ - 侧边栏 `+` 菜单 → 文件活动(或会话开始自动打开)。
65
+ - 点击任意文件行 → 弹出**浮窗预览**:浮窗内**复用侧边栏内置的 file viewer** 渲染(代码语法高亮 / Markdown 富文本 / 图片 / PDF / HTML),不打开侧边栏编辑器标签,也不自建预览实现;**点击浮窗外任意处或按 `Esc` 即可关闭**,大文件可在浮窗内滚动。
66
+ - 顶部「刷新」手动重载,「清空」删除当前会话全部记录。
67
+
68
+ ### 浮窗预览(复用内置渲染)
69
+
70
+ 点击文件(最近访问 / 文件统计中的任意文件行)会打开一个**悬浮预览窗**,窗口内容由侧边栏**内置的文件查看器**(file viewer)渲染 —— 通过 `ctx.betterSidebar.matchFileViewer(path)` 拿到匹配的 viewer,取其 `component` 挂载,并按 viewer 的 `fetchStrategy` 取内容(`fsRead` 文本 / `mediaUrl` 媒体 / `custom` 自定义)。浮窗为轻量交互:**点击浮窗外任意处、按 `Esc` 或点标题栏右上 `×` 都能关闭**;浮窗主体是可滚动区域,长文件或大图在窗内滚动查看。
71
+
72
+ - **代码**(后台为侧边栏内置 CodeMirror 编辑器):语法高亮 + 行号;支持 预览/编辑 切换与保存。
73
+ - **Markdown**(`.md` / `.markdown`):渲染后的富文本预览,可切换 预览/编辑 模式。
74
+ - **图片**(png / jpg / jpeg / gif / webp / svg / bmp / ico / avif):通过媒体路由渲染。
75
+ - **PDF**:浏览器原生 PDF 查看器(浮窗内嵌,工具栏提供下载)。
76
+ - **HTML**:沙箱化渲染预览。
77
+ - 其余文件按内置 viewer 的 `matchFileViewer` 回退匹配(如 `binary-download` 下载未知二进制)。
78
+
79
+ > 💡 **工作区外文件也能预览**:文件活动记录的是 agent 实际触碰过的文件,路径可能在会话工作区之外(如 `/tmp` 临时文件)。侧边栏自带的媒体路由只允许工作区内文件,因此图片/PDF 的字节由插件自身的 `/file-activity/file` 路由提供——它按会话记录授权(只放行本会话记录过的路径,未记录的路径一律 403),fence 与侧边栏一致。Markdown/代码/HTML 文本则仍走侧边栏 `fs.read`(不被工作区限制)。
80
+
81
+ ## 配置
82
+
83
+ 「设置 → 侧边卡片 → 文件活动」卡片齿轮:`会话开始时自动打开`(默认开)。
84
+
85
+ ## 数据说明
86
+
87
+ - 按会话(session)隔离存储(服务端 `$DSH_HOME/file-activity.json` 按 sessionId 分桶 + 前端 `bySession` 分桶展示);**DSH 重启后历史数据自动恢复**(持久化加载);新建/切换会话不残留其他会话的记录。
88
+ - 最近访问列表每会话最多保留 5 条(LRU:同一文件仅一条,重复访问移到最前),文件统计不受条数限制。
89
+ - agent 的 `bash`/`git` 等命令内部的文件触碰不在此跟踪范围(只跟踪 DSH 原生文件工具与侧边栏操作)。
90
+
91
+ ## 依赖
92
+
93
+ | 依赖 | 用途 | 可选 |
94
+ |---|---|---|
95
+ | `dsh-better-sidebar` | 侧边栏页签注册 + 浮窗 file viewer | 否(前置,v0.12+,推荐 v0.14) |
96
+ | `cordis` | 插件运行时 | 是(宿主提供) |
97
+
98
+ ## 相关文档
99
+
100
+ → [文件活动追踪模块文档](../../docs/文件活动追踪/概述.md) · [需求清单](../../docs/文件活动追踪/需求清单.md) · [CHANGELOG](CHANGELOG.md)
Binary file
Binary file
@@ -0,0 +1,15 @@
1
+ # dsh-file-activity bundle patch: inserts the plugin row into the profile
2
+ # roster. The row mounts the host half (file-op tracking + stats API) and the
3
+ # client half (sidebar tab). Install with:
4
+ #
5
+ # git clone https://github.com/baosfeng/my-dsh-plugins.git
6
+ # dsh plugin --profile web add link:<仓库路径>/plugins/dsh-file-activity
7
+ #
8
+ # or manually: add the link dependency to the profile package.json, run
9
+ # `pnpm install` in the profile dir, and add this insert row to the profile's
10
+ # own `cordis.patch.yml`. Editing the profile patch file hot-mounts the row
11
+ # through Cordis HMR (no server restart needed; a browser hard refresh loads
12
+ # the client bundle).
13
+ - insert:
14
+ - id: file-activity
15
+ name: 'dsh-file-activity'
@@ -0,0 +1,81 @@
1
+ /**
2
+ * /file-activity/api route handler: record (POST), stats (GET), clear (POST).
3
+ * Every request passes the trust fence first; responses are JSON with
4
+ * cache-control: no-cache.
5
+ */
6
+ import { readJsonBody, sessionCwdOf, writeError, writeJson } from './http.js'
7
+ import { mapOp } from './state.js'
8
+
9
+ export function createApiHandler({ ctx, store, fence }) {
10
+ return async (request, response) => {
11
+ if (!fence(request)) {
12
+ writeJson(response, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden' } })
13
+ return
14
+ }
15
+ const url = new URL(request.url ?? '/', 'http://dsh.internal')
16
+ const method = apiMethodOf(url)
17
+ try {
18
+ if (method === 'record' && request.method === 'POST') {
19
+ await handleRecord(store, request, response)
20
+ return
21
+ }
22
+ if (method === 'stats' && request.method === 'GET') {
23
+ handleStats(ctx, store, url, response)
24
+ return
25
+ }
26
+ if (method === 'clear' && request.method === 'POST') {
27
+ await handleClear(store, request, response)
28
+ return
29
+ }
30
+ writeJson(response, 404, { ok: false, error: { message: 'unknown file-activity API method' } })
31
+ } catch (error) {
32
+ writeError(response, error)
33
+ }
34
+ }
35
+ }
36
+
37
+ /** Strip the /file-activity/api/ prefix; undefined for anything else. */
38
+ function apiMethodOf(url) {
39
+ const pathname = url.pathname
40
+ return pathname.startsWith('/file-activity/api/') ? pathname.slice('/file-activity/api/'.length) : undefined
41
+ }
42
+
43
+ /** POST /file-activity/api/record — client-reported sidebar operation. */
44
+ async function handleRecord(store, request, response) {
45
+ const payload = await readJsonBody(request)
46
+ const sessionId = typeof payload.sessionId === 'string' ? payload.sessionId : ''
47
+ const path = typeof payload.path === 'string' ? payload.path : ''
48
+ const op = typeof payload.op === 'string' ? payload.op : 'read'
49
+ store.record(sessionId, path, mapOp(op), Date.now())
50
+ writeJson(response, 200, { ok: true })
51
+ }
52
+
53
+ /** GET /file-activity/api/stats — recent + counts + session cwd. */
54
+ function handleStats(ctx, store, url, response) {
55
+ const sessionId = url.searchParams.get('sessionId') ?? ''
56
+ // Session working directory, for client-side relative-path display.
57
+ const cwd = sessionCwdOf(ctx, sessionId)
58
+ const session = store.state.sessions[sessionId]
59
+ if (session === undefined) {
60
+ writeJson(response, 200, { ok: true, value: { recent: [], counts: {}, cwd } })
61
+ return
62
+ }
63
+ // Snapshot the leaf fields only — no live objects cross the wire.
64
+ const recent = session.recent.map((entry) => ({ path: entry.path, op: entry.op, time: entry.time }))
65
+ const counts = {}
66
+ for (const [path, counter] of Object.entries(session.counts)) {
67
+ counts[path] = { read: counter.read, create: counter.create, modify: counter.modify, firstSeen: counter.firstSeen, lastSeen: counter.lastSeen }
68
+ }
69
+ writeJson(response, 200, { ok: true, value: { recent, counts, cwd } })
70
+ }
71
+
72
+ /** POST /file-activity/api/clear — wipe one session's records (persisted). */
73
+ async function handleClear(store, request, response) {
74
+ const payload = await readJsonBody(request)
75
+ const sessionId = typeof payload.sessionId === 'string' ? payload.sessionId : ''
76
+ if (sessionId !== '' && store.state.sessions[sessionId] !== undefined) {
77
+ delete store.state.sessions[sessionId]
78
+ store.schedulePersist()
79
+ }
80
+ writeJson(response, 200, { ok: true })
81
+ }