dsh-sessions-manager 3.2.2 → 3.4.0

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/README.en.md CHANGED
@@ -14,33 +14,20 @@
14
14
 
15
15
  A persistent DSH plugin (host + browser halves). Starting with v3.0.0, it covers both the **settings panel** and the **main sidebar**, so frequent session actions don't require opening Settings.
16
16
 
17
- ## Changelog
18
-
19
- - **v3.2.2**: engineering hardening, no behavior change. Sidebar decision logic extracted to `src/client/logic.js` (dot semantics / drop validation / fiber row detection) with 13 new regression tests; CI now smoke-tests each runtime version against real cordis and tracks runtime drift.
20
- - **v3.2.1**: zstd session-log safety fix (frame-boundary scan mismatching zstd magic inside compressed data).
21
- - **v3.2.0**: sidebar cross-workspace drag & drop + full session management workbench.
22
-
23
- ## What's new in v3.2
24
-
25
- - **Cross-workspace sidebar drag-and-drop**: drop a session on a workspace heading to move it. It coexists with DSH's native same-group sorting and provides target highlighting, same-workspace protection, failure feedback, and a keyboard-accessible menu fallback.
26
- - **Complete session-management workspace**: All / Active / Archived / Recycle bin in one place, with full search, workspace filters, creation-time or title sorting, batch actions, and result counts.
27
- - **Reliable recycle-bin lifecycle**: soft delete, in-place restore, single permanent purge, empty-all, 7 / 30 / 90-day retention, and log-integrity verification, backed by schema v2, serialized mutations, and atomic writes.
28
- - **No post-delete resurrection**: permanent purge persists a tombstone first, waits for the live Session and persistence controller to retire, removes the log, and rebuilds workspace indexes so neither sessions nor an empty Ungrouped section reappear.
29
- - **Archive-state round trips**: a trashed session returns to the active or archived state it had before deletion; concurrent archive and restore mutations are serialized to prevent duplicate IDs, overwrites, and races.
30
- - **Authoritative cold-start titles**: the latest log-folded `session/title` corrects stale sidebar caches before a renamed session is opened.
31
- - **Safe migration for open sessions**: moving synchronizes the log header, persistence state, live Session, and WorkspaceRegistry, with backup, verification, and rollback on failure.
32
- - **Consistent and accessible UI**: tags use DSH's official 4px radius; destructive actions are confirmed, busy / empty / error states are explicit, and every drag action has a keyboard path.
33
-
34
17
  ## Features
35
18
 
36
19
  ### Settings panel: Session Manager
37
20
 
38
- - **Unified panel**: four top-level views — **All / Active / Archived / Recycle bin** — with search across title, session ID, and workspace, plus workspace filters and creation-time/title sorting.
21
+ - **Unified panel**: five top-level views — **All / Active / Archived / Starred / Recycle bin** — with search across title, session ID, and workspace, plus workspace filters and creation-time/title sorting. A maintenance row below the filters holds the **Auto-archive** and **Storage usage** tools, which expand on demand instead of taking up a view.
22
+ - **Starred sessions**: an always-visible star button on each session row toggles starring optimistically with rollback on failure; the Starred view is orthogonal to DSH's active/archived states and they stack. The star index is plugin-owned (schema v3) and never touches DSH logs; stars are cleaned up automatically when a session is permanently purged.
39
23
  - **Cold-title synchronization**: the sidebar is corrected from the latest log-backed `session/title`, so renamed cold sessions no longer need to be opened before showing their current name.
40
24
  - **Sidebar workspace drag-and-drop**: drag a session onto a workspace heading to move it, with a highlighted drop target, same-workspace protection, failure feedback, and the More → Move session menu retained for keyboard access.
41
25
  - **Archive / Restore**: archive hides a session from the sidebar; restore unarchives it and puts it back into its original workspace group.
42
26
  - **Move to a workspace**: pick an **existing workspace**, or enter a **new directory path** (auto-created); the new-path mode also lets you open the **native OS directory picker** with a **「浏览…」** button. The session's working directory and log are migrated together, and even an open session can be moved safely.
43
27
  - **Session details**: expand any session to see **disk usage**, **turns / steps / user·assistant messages / tool calls / image attachments** stats, **tool-usage breakdown**, **search·fetch records**, the **write/edit file list** (already filtered for paths that no longer exist on disk), and **lineage** (parent session / child sessions / subagents).
28
+ - **Export**: the detail panel offers two actions — "**Download raw log (ZIP)**" goes straight to DSH's official `session.export` endpoint (subsessions + attachments included, hidden automatically when the persistence backend lacks raw-artifact support); "**Export Markdown**" renders the session into a human-readable transcript (front matter + per-turn sections + user / assistant / tool-call summaries, with no streaming-delta duplication).
29
+ - **Storage analysis**: a **Storage usage** button in the maintenance row expands on demand to aggregate per-workspace disk usage of session logs (share bars + session counts) and list the Top 10 largest sessions. Read-only statistics that never modify any data; collapsed by default, so nothing is scanned until you open it.
30
+ - **Auto-archive**: optionally move sessions that have been **inactive for 30 / 60 / 90 days** into **Archived**, **off by default**. The session you are currently using and any starred session (that guard can be turned off) are never auto-archived; it only sets an archived flag and never deletes data, so recovery is always one click away. The check is lazily triggered when you open the panel, at most once per day, and can also be run manually with "Check now".
44
31
  - **Batch multi-select**: select-all / archive selected / restore selected / delete selected (single confirmation for batch delete).
45
32
 
46
33
  ### Main sidebar: session ⋯ menu augmentation
@@ -77,14 +64,25 @@ The plugin hides DSH's own status dot and re-renders it using the palette above,
77
64
 
78
65
  ## Screenshots
79
66
 
67
+ <details>
68
+ <summary>Expand screenshots (settings panel / auto-archive / storage analysis / starred / recycle bin / session details / sidebar menu)</summary>
69
+
80
70
  ![Sidebar ⋯ menu: mark unread, move session, delete session](assets/screenshot-session-submenu.png)
81
71
 
82
- ![Settings panel "Session Manager"](assets/screenshot-session-settingsmenu.png)
72
+ ![Settings panel "Session Manager"](assets/screenshot-session-settings.png)
73
+
74
+ ![Auto-archive panel (inline sheet in the maintenance bar)](assets/screenshot-session-autoarch.png)
75
+
76
+ ![Storage analysis (inline sheet in the maintenance bar)](assets/screenshot-session-storage.png)
77
+
78
+ ![Starred (favorites) view](assets/screenshot-session-starred.png)
83
79
 
84
80
  ![Recycle bin](assets/screenshot-session-trash.png)
85
81
 
86
82
  ![Session details: disk usage / stats / tool usage](assets/screenshot-session-details.png)
87
83
 
84
+ </details>
85
+
88
86
  ## Install
89
87
 
90
88
  ```sh
@@ -115,6 +113,8 @@ package.json npm metadata + dsh.bundle.patch + dsh.client (browser-half re
115
113
  cordis.patch.yml inserts this plugin's row into the profile bundle
116
114
  src/index.js host source (/archived-sessions/* JSON routes)
117
115
  src/client/index.jsx client source (React, settings.section + sidebar DOM augmentation)
116
+ src/auto-archive.js auto-archive settings (schema v4) + candidate-selection pure functions
117
+ src/storage-stats.js storage-usage aggregation (pure functions, by workspace / Top N)
118
118
  build.mjs esbuild build script (regenerates lib/)
119
119
  lib/index.js pre-built host (ESM)
120
120
  lib/client.js pre-built client (ModuleLoader CJS handshake)
@@ -144,6 +144,11 @@ lib/client.js pre-built client (ModuleLoader CJS handshake)
144
144
  | POST | `/archived-sessions/move` | Move a session to a target workspace `{ sessionId, targetPath }` |
145
145
  | POST | `/archived-sessions/details` | Session details (disk / stats / tools / fetch / files / lineage) `{ sessionId }` |
146
146
  | POST | `/archived-sessions/sidebar-state` | Authoritative sidebar titles, recycle-bin IDs, and permanent-deletion tombstones |
147
+ | POST | `/archived-sessions/star/set` | Star / unstar sessions `{ sessionId or sessionIds, starred }` |
148
+ | GET | `/archived-sessions/export-md?sessionId=` | Single-session Markdown export (human-readable transcript) |
149
+ | POST | `/archived-sessions/storage` | Storage-usage aggregation (per-workspace ranking + largest sessions) `{ topN }` |
150
+ | POST | `/archived-sessions/auto-archive/settings` | Read or update the auto-archive policy `{ inactiveDays, skipStarred }`; reading lazily triggers the daily check |
151
+ | POST | `/archived-sessions/auto-archive/run` | Run a one-off auto-archive check immediately (ignores the daily throttle) |
147
152
 
148
153
  > Deleting a session moves it to the recycle bin by default; only "permanently delete" inside the recycle bin physically removes the log. Permanently deleted sessions are hidden forever on the client side so they don't reappear in the sidebar or "ungrouped" due to DSH runtime caching.
149
154
 
package/README.md CHANGED
@@ -14,33 +14,20 @@
14
14
 
15
15
  一个 DSH 持久化插件(host + browser 双半)。v3.0.0 起同时覆盖「设置面板」与「主页侧边栏」两个入口,无需打开设置即可完成高频会话操作。
16
16
 
17
- ## 更新日志
18
-
19
- - **v3.2.2**:工程加固版,无功能变化。侧栏判定纯逻辑抽取到 `src/client/logic.js`(状态点语义 / 拖拽校验 / fiber 行识别),新增 13 个回归测试;CI 新增 runtime 版本冒烟(真实 cordis DI 契约校验 + 版本漂移追踪)。
20
- - **v3.2.1**:zstd 会话日志安全修复(帧边界定位误匹配压缩数据内的 magic)。
21
- - **v3.2.0**:侧栏跨工作区拖拽 + 完整会话管理工作台。
22
-
23
- ## v3.2 亮点
24
-
25
- - **侧栏跨工作区拖拽**:把会话拖到目标工作区标题即可完成迁移;与 DSH 原生同组排序兼容,提供目标高亮、同工作区拦截、失败反馈,并保留键盘可达的菜单入口。
26
- - **完整会话管理工作台**:全部 / 活动 / 已归档 / 回收站统一入口,加入全文搜索、工作区筛选、创建时间与标题排序、批量操作和结果计数。
27
- - **可靠回收站生命周期**:软删除、原位恢复、单条彻底删除、批量清空、7 / 30 / 90 天自动清理和日志完整性校验;索引采用 schema v2、串行变更和原子写入。
28
- - **删除后不再“复活”**:永久删除先持久化墓碑,再等待 live Session 与持久化控制器退役、物理删除日志并重建工作区索引,避免会话或空的「未分组」重新出现。
29
- - **归档状态往返一致**:活动或归档会话进入回收站后,恢复时回到删除前状态;并发归档 / 恢复串行执行,避免重复 ID、覆盖和竞态。
30
- - **冷启动标题权威同步**:从日志折叠最新 `session/title`,改名后的会话无需先打开即可在侧栏显示最新名称。
31
- - **打开会话也能安全迁移**:移动过程同步日志 header、持久化状态、live Session 与 WorkspaceRegistry,配合备份、校验和失败回滚。
32
- - **界面一致性与可访问性**:标签统一采用 DSH 官方 4px R 角;危险操作二次确认、忙碌 / 空 / 错误状态清晰,拖拽之外保留键盘操作路径。
33
-
34
17
  ## 功能
35
18
 
36
19
  ### 设置面板:会话管理
37
20
 
38
- - **统一面板**:顶部提供「全部 / 活动 / 已归档 / 回收站」四个视图,支持按标题、会话 ID、工作区搜索,按工作区筛选,并按创建时间或标题排序。
21
+ - **统一面板**:顶部提供「全部 / 活动 / 已归档 / 已收藏 / 回收站」五个视图,支持按标题、会话 ID、工作区搜索,按工作区筛选,并按创建时间或标题排序。筛选栏下方另有一行维护栏,收纳「自动归档」与「存储占用」两个按需展开的工具,不占用视图位置。
22
+ - **收藏(星标)**:会话行左侧常驻星标按钮,单击即收藏 / 取消(乐观更新、失败回滚);「已收藏」视图与 DSH 的活动 / 归档状态正交、可叠加;收藏索引为插件自有 schema v3,不触碰 DSH 日志,会话被彻底删除时自动清理。
39
23
  - **冷态标题同步**:侧栏使用日志中最新的 `session/title` 修正冷启动缓存,改名后的会话无需先打开即可显示新名称。
40
24
  - **侧栏跨工作区拖拽**:直接把会话拖到目标工作区标题即可切换工作区;目标高亮、同工作区拦截、失败反馈,并保留“更多 → 移动会话”作为键盘操作入口。
41
25
  - **归档 / 恢复**:归档把会话从侧栏隐藏;恢复取消归档并放回原工作区分组。
42
26
  - **移动到工作区**:任选**已有工作区**或**新建目录路径**(自动创建),新建目录支持点击 **「浏览…」** 调用系统目录选择窗口。会话的工作目录与日志一起迁移;即使会话处于打开状态也可安全移动。
43
27
  - **会话详情**:展开单条会话查看**磁盘占用**、**轮次 / 步骤 / 用户·助手消息 / 工具调用 / 图片附件**统计、**工具使用分布**、**搜索·抓取记录**、**write/edit 写过的文件列表**(已过滤磁盘上已不存在的路径),以及**血统**(父会话 / 子会话 / 子代理)。
28
+ - **导出**:详情面板底部提供两个入口——「**下载原始日志 (ZIP)**」直接走 DSH 官方 `session.export` 端点(含子会话与附件,持久化后端不支持时自动隐藏);「**导出 Markdown**」由本插件把会话渲染为人类可读对话记录(front matter + 按轮分节 + 用户 / 助手 / 工具调用摘要,流式增量不重复)。
29
+ - **存储占用分析**:维护栏的「存储占用」按钮按需展开,按工作区聚合会话日志的磁盘占用(占比条 + 会话数),并列出占用最大的会话 Top 10。纯只读统计,不修改任何数据;默认收起、展开时才统计,因此不会拖慢会话列表的加载。
30
+ - **自动归档**:可设为把 **30 / 60 / 90 天未活跃**的会话自动收进「已归档」,**默认关闭**。当前正在使用的会话与已收藏的会话(该保护可关闭)永不自动归档;只做归档标记,不删除任何数据,随时可恢复。检查在打开面板时惰性触发,每天最多一次,也可点「立即检查」手动执行。
44
31
  - **批量多选**:全选 / 批量归档 / 恢复所选 / 删除所选(批量删除一次二次确认)。
45
32
 
46
33
  ### 主页侧边栏:会话 ⋯ 菜单增强
@@ -77,14 +64,25 @@
77
64
 
78
65
  ## 截图
79
66
 
67
+ <details>
68
+ <summary>展开查看截图(设置面板 / 自动归档 / 存储占用 / 已收藏 / 回收站 / 会话详情 / 侧边栏菜单)</summary>
69
+
80
70
  ![主页侧边栏 ⋯ 菜单(标记未读、移动会话、删除会话)](assets/screenshot-session-submenu.png)
81
71
 
82
- ![设置面板「会话管理」](assets/screenshot-session-settingsmenu.png)
72
+ ![设置面板「会话管理」](assets/screenshot-session-settings.png)
73
+
74
+ ![自动归档面板(维护栏内联展开)](assets/screenshot-session-autoarch.png)
75
+
76
+ ![存储占用分析(维护栏内联展开)](assets/screenshot-session-storage.png)
77
+
78
+ ![已收藏(星标)视图](assets/screenshot-session-starred.png)
83
79
 
84
80
  ![回收站](assets/screenshot-session-trash.png)
85
81
 
86
82
  ![会话详情(磁盘占用 / 统计 / 工具使用)](assets/screenshot-session-details.png)
87
83
 
84
+ </details>
85
+
88
86
  ## 安装
89
87
 
90
88
  ```sh
@@ -115,6 +113,8 @@ package.json npm 元数据 + dsh.bundle.patch + dsh.client(浏览器半
115
113
  cordis.patch.yml 向 profile bundle 插入本插件行
116
114
  src/index.js host 源码(/archived-sessions/* JSON 路由)
117
115
  src/client/index.jsx client 源码(React,settings.section + 侧栏 DOM 增强)
116
+ src/auto-archive.js 自动归档设置(schema v4)+ 候选判定纯函数
117
+ src/storage-stats.js 存储占用聚合(纯函数,按工作区 / Top N)
118
118
  build.mjs esbuild 构建脚本(本地开发时生成 lib/)
119
119
  lib/index.js 预构建 host(ESM)
120
120
  lib/client.js 预构建 client(ModuleLoader CJS handshake)
@@ -144,6 +144,11 @@ lib/client.js 预构建 client(ModuleLoader CJS handshake)
144
144
  | POST | `/archived-sessions/move` | 把会话移动到目标工作区 `{ sessionId, targetPath }` |
145
145
  | POST | `/archived-sessions/details` | 会话详情(磁盘/统计/工具/fetch/文件/血统)`{ sessionId }` |
146
146
  | POST | `/archived-sessions/sidebar-state` | 返回侧栏权威标题、回收站 ID 与永久删除墓碑 |
147
+ | POST | `/archived-sessions/star/set` | 收藏 / 取消收藏 `{ sessionId 或 sessionIds, starred }` |
148
+ | GET | `/archived-sessions/export-md?sessionId=` | 单会话 Markdown 导出(人类可读对话记录) |
149
+ | POST | `/archived-sessions/storage` | 存储占用聚合(按工作区排行 + 最大的会话)`{ topN }` |
150
+ | POST | `/archived-sessions/auto-archive/settings` | 读取或更新自动归档策略 `{ inactiveDays, skipStarred }`;读取时惰性触发每日检查 |
151
+ | POST | `/archived-sessions/auto-archive/run` | 立即执行一次自动归档检查(忽略每日节流) |
147
152
 
148
153
  > 删除会话默认进入回收站,只有回收站内的「彻底删除」才会物理移除日志。被彻底删除的会话由前端永久隐藏,避免 DSH 运行时缓存使其重新出现在侧栏或「未分组」中。
149
154
 
Binary file
package/lib/client.js CHANGED
@@ -75,6 +75,9 @@ function workspaceForNodes(nodes) {
75
75
  }
76
76
  return null;
77
77
  }
78
+ function starredOf(items) {
79
+ return (items || []).filter((item) => item && item.starred);
80
+ }
78
81
 
79
82
  // src/client/index.jsx
80
83
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -122,6 +125,11 @@ var CSS = `
122
125
  .archv-id{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10.5px;color:var(--dsw-alias-label-tertiary);flex:none}
123
126
  .archv-dot{color:var(--dsw-alias-border-l3);flex:none}
124
127
  .archv-check{width:15px;height:15px;accent-color:var(--dsw-alias-state-business-primary);flex:none;cursor:pointer}
128
+ .archv-star{appearance:none;width:24px;height:24px;flex:none;display:inline-flex;align-items:center;justify-content:center;border:none;background:0 0;border-radius:7px;cursor:pointer;color:var(--dsw-alias-label-tertiary);transition:color .15s ease,background-color .15s ease}
129
+ .archv-star:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
130
+ .archv-star svg{fill:none;stroke:currentColor;stroke-width:1.5;stroke-linejoin:round}
131
+ .archv-star-on,.archv-star-on:hover{color:var(--dsw-alias-state-business-primary)}
132
+ .archv-star-on svg{fill:currentColor}
125
133
  .archv-body{flex:1;min-width:0;display:flex;align-items:center;gap:12px}
126
134
  .archv-actions{display:flex;gap:8px;flex:none;flex-wrap:nowrap;justify-content:flex-end}
127
135
  .archv-btn{appearance:none;min-height:32px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-fill-subtle);color:var(--dsw-alias-label-secondary);border-radius:9px;font-size:12px;font-weight:500;cursor:pointer;white-space:nowrap;display:inline-flex;align-items:center;justify-content:center;gap:6px;text-align:center;transition:background-color .15s ease,border-color .15s ease,color .15s ease}
@@ -167,6 +175,9 @@ var CSS = `
167
175
  .dtl-k{font-size:11px;color:var(--dsw-alias-label-tertiary)}
168
176
  .dtl-v{font-size:12px;color:var(--dsw-alias-label-primary);word-break:break-all}
169
177
  .dtl-sec{margin-top:12px}
178
+ .dtl-export{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
179
+ .dtl-export .archv-btn{text-decoration:none}
180
+ .dtl-note{margin-top:8px;font-size:11px;color:var(--dsw-alias-label-tertiary);line-height:1.5}
170
181
  .dtl-sec-t{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:.03em;margin-bottom:6px}
171
182
  .dtl-tags{display:flex;flex-wrap:wrap;gap:6px}
172
183
  .dtl-tag{display:inline-flex;font-size:11px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-elevated);border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-tag);padding:2px 8px}
@@ -268,7 +279,7 @@ function SessionPanel({ workspacesSvc }) {
268
279
  const [sessions, setSessions] = (0, import_react.useState)(null);
269
280
  const [workspaces, setWorkspaces] = (0, import_react.useState)([]);
270
281
  const initialPrefs = (0, import_react.useRef)(loadPanelPrefs()).current;
271
- const [filter, setFilter] = (0, import_react.useState)(() => ["all", "active", "archived", "trash"].includes(initialPrefs.filter) ? initialPrefs.filter : "all");
282
+ const [filter, setFilter] = (0, import_react.useState)(() => ["all", "active", "archived", "starred", "trash"].includes(initialPrefs.filter) ? initialPrefs.filter : "all");
272
283
  const [query, setQuery] = (0, import_react.useState)("");
273
284
  const [workspaceFilter, setWorkspaceFilter] = (0, import_react.useState)(() => initialPrefs.workspaceFilter || "all");
274
285
  const [sortBy, setSortBy] = (0, import_react.useState)(() => ["newest", "oldest", "title"].includes(initialPrefs.sortBy) ? initialPrefs.sortBy : "newest");
@@ -291,8 +302,19 @@ function SessionPanel({ workspacesSvc }) {
291
302
  const [details, setDetails] = (0, import_react.useState)({});
292
303
  const [openDetails, setOpenDetails] = (0, import_react.useState)(null);
293
304
  const [detailsLoading, setDetailsLoading] = (0, import_react.useState)(null);
305
+ const [mdBusy, setMdBusy] = (0, import_react.useState)(null);
306
+ const [zipOk, setZipOk] = (0, import_react.useState)(true);
307
+ const [storage, setStorage] = (0, import_react.useState)(null);
308
+ const [storageBusy, setStorageBusy] = (0, import_react.useState)(false);
309
+ const [storageOpen, setStorageOpen] = (0, import_react.useState)(false);
310
+ const [storageError, setStorageError] = (0, import_react.useState)(null);
311
+ const [aa, setAa] = (0, import_react.useState)({ settings: { inactiveDays: 0, skipStarred: true }, lastRunAt: null, lastArchivedCount: 0 });
312
+ const [aaOpen, setAaOpen] = (0, import_react.useState)(false);
313
+ const [aaBusy, setAaBusy] = (0, import_react.useState)(false);
314
+ const zipChecked = (0, import_react.useRef)(false);
294
315
  const [openMenu, setOpenMenu] = (0, import_react.useState)(null);
295
316
  const timer = (0, import_react.useRef)(null);
317
+ const storageDirty = (0, import_react.useRef)(false);
296
318
  const menuRef = (0, import_react.useRef)(null);
297
319
  const dialogRef = (0, import_react.useRef)(null);
298
320
  const showToast = (msg) => {
@@ -315,6 +337,8 @@ function SessionPanel({ workspacesSvc }) {
315
337
  setConfirmBatch(false);
316
338
  if (!targetWs && works.items && works.items.length) setTargetWs(works.items[0].workspaceId);
317
339
  loadTrash();
340
+ if (storageOpen) loadStorage();
341
+ else storageDirty.current = true;
318
342
  }).catch((e) => setError(String(e && e.message || e)));
319
343
  };
320
344
  (0, import_react.useEffect)(() => {
@@ -329,6 +353,46 @@ function SessionPanel({ workspacesSvc }) {
329
353
  } catch (e) {
330
354
  }
331
355
  }, [filter, workspaceFilter, sortBy]);
356
+ (0, import_react.useEffect)(() => {
357
+ loadAutoArchive();
358
+ }, []);
359
+ const toggleStar = async (it) => {
360
+ const next = !it.starred;
361
+ setSessions((s) => s && s.map((x) => x.sessionId === it.sessionId ? { ...x, starred: next } : x));
362
+ try {
363
+ await postJSON("/archived-sessions/star/set", { sessionId: it.sessionId, starred: next });
364
+ } catch (e) {
365
+ setSessions((s) => s && s.map((x) => x.sessionId === it.sessionId ? { ...x, starred: !next } : x));
366
+ showToast("\u6536\u85CF\u5931\u8D25\uFF1A" + String(e && e.message || e));
367
+ }
368
+ };
369
+ const exportMarkdown = async (it) => {
370
+ if (mdBusy) return;
371
+ setMdBusy(it.sessionId);
372
+ try {
373
+ const res = await fetch("/archived-sessions/export-md?sessionId=" + encodeURIComponent(it.sessionId));
374
+ if (!res.ok) throw new Error("HTTP " + res.status);
375
+ const blob = await res.blob();
376
+ const url = URL.createObjectURL(blob);
377
+ const a = document.createElement("a");
378
+ a.href = url;
379
+ a.download = "dsh-session-" + it.sessionId + ".md";
380
+ document.body.appendChild(a);
381
+ a.click();
382
+ a.remove();
383
+ URL.revokeObjectURL(url);
384
+ showToast("\u5DF2\u5BFC\u51FA Markdown");
385
+ } catch (e) {
386
+ showToast("\u5BFC\u51FA\u5931\u8D25\uFF1A" + String(e && e.message || e));
387
+ } finally {
388
+ setMdBusy(null);
389
+ }
390
+ };
391
+ (0, import_react.useEffect)(() => {
392
+ if (openDetails === null || zipChecked.current) return;
393
+ zipChecked.current = true;
394
+ fetch("/api/session.export?sessionId=probe&includeDescendants=false", { method: "HEAD" }).then((res) => setZipOk(res.status !== 501)).catch(() => setZipOk(true));
395
+ }, [openDetails]);
332
396
  (0, import_react.useEffect)(() => {
333
397
  if (openMenu === null) return;
334
398
  const onDown = (e) => {
@@ -366,9 +430,10 @@ function SessionPanel({ workspacesSvc }) {
366
430
  };
367
431
  const archivedList = sessions ? sessions.filter((x) => x.archived) : [];
368
432
  const activeList = sessions ? sessions.filter((x) => !x.archived) : [];
433
+ const starredList = starredOf(sessions);
369
434
  const list = (0, import_react.useMemo)(() => {
370
435
  if (filter === "trash") return [];
371
- const base = filter === "archived" ? archivedList : filter === "active" ? activeList : sessions || [];
436
+ const base = filter === "archived" ? archivedList : filter === "active" ? activeList : filter === "starred" ? starredList : sessions || [];
372
437
  const needle = query.trim().toLocaleLowerCase();
373
438
  const filtered = base.filter((item) => {
374
439
  if (workspaceFilter !== "all" && (item.workspacePath || "") !== workspaceFilter) return false;
@@ -382,6 +447,7 @@ function SessionPanel({ workspacesSvc }) {
382
447
  });
383
448
  }, [sessions, filter, query, workspaceFilter, sortBy]);
384
449
  const selIds = Object.keys(selected).filter((k) => selected[k]);
450
+ const showSessionList = filter !== "trash";
385
451
  const toggle = (id) => setSelected((s) => ({ ...s, [id]: !s[id] }));
386
452
  const clearSel = () => setSelected({});
387
453
  const selectAll = () => {
@@ -448,6 +514,52 @@ function SessionPanel({ workspacesSvc }) {
448
514
  setError(String(e && e.message || e));
449
515
  });
450
516
  };
517
+ const loadStorage = () => {
518
+ setStorageBusy(true);
519
+ setStorageError(null);
520
+ postJSON("/archived-sessions/storage", { topN: 10 }).then((r) => {
521
+ setStorageBusy(false);
522
+ setStorage(r);
523
+ storageDirty.current = false;
524
+ }).catch((e) => {
525
+ setStorageBusy(false);
526
+ setStorageError(String(e && e.message || e));
527
+ });
528
+ };
529
+ const applyAa = (r) => setAa({ settings: r.settings || { inactiveDays: 0, skipStarred: true }, lastRunAt: r.lastRunAt ?? null, lastArchivedCount: r.lastArchivedCount || 0 });
530
+ const loadAutoArchive = () => {
531
+ postJSON("/archived-sessions/auto-archive/settings", {}).then(applyAa).catch(() => {
532
+ });
533
+ };
534
+ const updateAutoArchive = (patch) => {
535
+ if (aaBusy) return;
536
+ setAaBusy(true);
537
+ postJSON("/archived-sessions/auto-archive/settings", patch).then((r) => {
538
+ setAaBusy(false);
539
+ applyAa(r);
540
+ showToast("\u5DF2\u66F4\u65B0\u81EA\u52A8\u5F52\u6863\u7B56\u7565");
541
+ }).catch((e) => {
542
+ setAaBusy(false);
543
+ setError(String(e && e.message || e));
544
+ });
545
+ };
546
+ const runAutoArchive = () => {
547
+ if (aaBusy) return;
548
+ setAaBusy(true);
549
+ postJSON("/archived-sessions/auto-archive/run", {}).then((r) => {
550
+ setAaBusy(false);
551
+ applyAa(r);
552
+ const n = r.archived || 0;
553
+ if (r.skipped === "disabled") showToast("\u81EA\u52A8\u5F52\u6863\u672A\u542F\u7528");
554
+ else {
555
+ showToast(`\u5DF2\u81EA\u52A8\u5F52\u6863 ${n} \u4E2A\u4F1A\u8BDD`);
556
+ if (n > 0) refresh();
557
+ }
558
+ }).catch((e) => {
559
+ setAaBusy(false);
560
+ setError(String(e && e.message || e));
561
+ });
562
+ };
451
563
  const restoreTrash = (sid) => {
452
564
  if (trashBusy) return;
453
565
  setTrashBusy(sid);
@@ -676,6 +788,15 @@ function SessionPanel({ workspacesSvc }) {
676
788
  archivedList.length,
677
789
  ")"
678
790
  ] }),
791
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", role: "tab", "aria-selected": filter === "starred", className: "sess-fbtn" + (filter === "starred" ? " sess-fbtn-on" : ""), onClick: () => {
792
+ setFilter("starred");
793
+ clearSel();
794
+ setConfirmBatch(false);
795
+ }, children: [
796
+ "\u5DF2\u6536\u85CF (",
797
+ starredList.length,
798
+ ")"
799
+ ] }),
679
800
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", role: "tab", "aria-selected": filter === "trash", className: "sess-fbtn" + (filter === "trash" ? " sess-fbtn-on" : ""), onClick: () => {
680
801
  setFilter("trash");
681
802
  clearSel();
@@ -686,7 +807,89 @@ function SessionPanel({ workspacesSvc }) {
686
807
  ")"
687
808
  ] })
688
809
  ] }),
689
- filter !== "trash" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
810
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "maint-bar", children: [
811
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", className: "archv-btn" + (aa.settings.inactiveDays ? " archv-go" : ""), "aria-expanded": aaOpen, onClick: () => setAaOpen(!aaOpen), children: [
812
+ "\u81EA\u52A8\u5F52\u6863",
813
+ aa.settings.inactiveDays ? `\uFF1A${aa.settings.inactiveDays} \u5929\u672A\u6D3B\u8DC3` : "\uFF1A\u672A\u542F\u7528"
814
+ ] }),
815
+ aa.lastRunAt ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "maint-note", children: [
816
+ "\u4E0A\u6B21\u68C0\u67E5 ",
817
+ fmtDate(aa.lastRunAt),
818
+ "\uFF0C\u5F52\u6863 ",
819
+ aa.lastArchivedCount,
820
+ " \u4E2A"
821
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "maint-note", children: "\u5C1A\u672A\u68C0\u67E5" }),
822
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", className: "archv-btn maint-bar-right", "aria-expanded": storageOpen, onClick: () => {
823
+ const next = !storageOpen;
824
+ setStorageOpen(next);
825
+ if (next && (!storage || storageDirty.current) && !storageBusy) loadStorage();
826
+ }, children: [
827
+ "\u5B58\u50A8\u5360\u7528",
828
+ storage ? ` \xB7 ${fmtBytes(storage.totalBytes) || "0 B"}` : ""
829
+ ] })
830
+ ] }),
831
+ aaOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mv-sheet", "aria-label": "\u81EA\u52A8\u5F52\u6863\u8BBE\u7F6E", children: [
832
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mv-sheet-head", children: [
833
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "mv-sheet-title", children: "\u81EA\u52A8\u5F52\u6863" }),
834
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "mv-sheet-close", "aria-label": "\u5173\u95ED", onClick: () => setAaOpen(false), children: "\xD7" })
835
+ ] }),
836
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mv-field", children: [
837
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "mv-field-label", htmlFor: "dsm-aa-days", children: "\u5C06\u591A\u4E45\u672A\u6D3B\u8DC3\u7684\u4F1A\u8BDD\u81EA\u52A8\u5F52\u6863" }),
838
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { id: "dsm-aa-days", value: aa.settings.inactiveDays, disabled: aaBusy, onChange: (e) => updateAutoArchive({ inactiveDays: Number(e.target.value) }), children: [
839
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "0", children: "\u4E0D\u81EA\u52A8\u5F52\u6863" }),
840
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "30", children: "30 \u5929\u672A\u6D3B\u8DC3" }),
841
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "60", children: "60 \u5929\u672A\u6D3B\u8DC3" }),
842
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "90", children: "90 \u5929\u672A\u6D3B\u8DC3" })
843
+ ] })
844
+ ] }),
845
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "aa-check", children: [
846
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", checked: aa.settings.skipStarred !== false, disabled: aaBusy, onChange: (e) => updateAutoArchive({ skipStarred: e.target.checked }) }),
847
+ "\u8DF3\u8FC7\u5DF2\u6536\u85CF\u7684\u4F1A\u8BDD"
848
+ ] }),
849
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mv-foot", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn", disabled: aaBusy || !aa.settings.inactiveDays, onClick: runAutoArchive, children: aaBusy ? "\u68C0\u67E5\u4E2D\u2026" : "\u7ACB\u5373\u68C0\u67E5" }) }),
850
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtl-note", children: "\u81EA\u52A8\u5F52\u6863\u53EA\u662F\u628A\u4F1A\u8BDD\u6536\u8FDB\u300C\u5DF2\u5F52\u6863\u300D\uFF0C\u4E0D\u5220\u9664\u4EFB\u4F55\u6570\u636E\uFF0C\u968F\u65F6\u53EF\u6062\u590D\u3002\u5F53\u524D\u6B63\u5728\u4F7F\u7528\u7684\u4F1A\u8BDD\u6C38\u8FDC\u4E0D\u4F1A\u88AB\u81EA\u52A8\u5F52\u6863\u3002\u68C0\u67E5\u5728\u6253\u5F00\u672C\u9762\u677F\u65F6\u89E6\u53D1\uFF0C\u6BCF\u5929\u6700\u591A\u4E00\u6B21\u3002" })
851
+ ] }),
852
+ storageOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mv-sheet", "aria-label": "\u5B58\u50A8\u5360\u7528", children: [
853
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mv-sheet-head", children: [
854
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "mv-sheet-title", children: "\u5B58\u50A8\u5360\u7528" }),
855
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mv-sheet-actions", children: [
856
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn", disabled: storageBusy, onClick: loadStorage, children: storageBusy ? "\u7EDF\u8BA1\u4E2D\u2026" : "\u91CD\u65B0\u7EDF\u8BA1" }),
857
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "mv-sheet-close", "aria-label": "\u5173\u95ED", onClick: () => setStorageOpen(false), children: "\xD7" })
858
+ ] })
859
+ ] }),
860
+ storageError ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "archv-err", role: "alert", children: [
861
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: storageError }),
862
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-errretry", onClick: loadStorage, children: "\u91CD\u8BD5" })
863
+ ] }) : !storage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "archv-empty", children: "\u7EDF\u8BA1\u4E2D\u2026" }) : storage.sessionCount === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "archv-empty", children: "\u6682\u65E0\u4F1A\u8BDD\uFF0C\u6CA1\u6709\u53EF\u7EDF\u8BA1\u7684\u5B58\u50A8\u5360\u7528\u3002" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
864
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsm-storage-sum", children: [
865
+ "\u5171 ",
866
+ fmtBytes(storage.totalBytes) || "0 B",
867
+ " \xB7 ",
868
+ storage.sessionCount,
869
+ " \u4E2A\u4F1A\u8BDD",
870
+ storage.unknownSessions ? ` \xB7 ${storage.unknownSessions} \u4E2A\u5927\u5C0F\u672A\u77E5` : ""
871
+ ] }),
872
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsm-storage-list", children: storage.workspaces.map((w) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsm-storage-row", children: [
873
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsm-storage-name", title: w.path || "\u672A\u5206\u7EC4", children: w.title || (w.path ? pathName(w.path) : "\u672A\u5206\u7EC4") }),
874
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsm-storage-bar", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsm-storage-fill", style: { width: `${Math.round((w.share || 0) * 100)}%` } }) }),
875
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsm-storage-size", children: fmtBytes(w.bytes) || "\u2014" }),
876
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dsm-storage-count", children: [
877
+ w.sessions,
878
+ " \u4E2A"
879
+ ] })
880
+ ] }, w.key)) }),
881
+ storage.top.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtl-sec", children: [
882
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtl-sec-t", children: "\u5360\u7528\u6700\u5927\u7684\u4F1A\u8BDD" }),
883
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsm-storage-list", children: storage.top.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsm-storage-row", children: [
884
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsm-storage-name", title: s.sessionId, children: s.title || s.sessionId }),
885
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsm-storage-size", children: fmtBytes(s.sizeBytes) || "\u2014" }),
886
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsm-storage-count", children: s.workspaceTitle || (s.workspacePath ? pathName(s.workspacePath) : "\u672A\u5206\u7EC4") })
887
+ ] }, s.sessionId)) })
888
+ ] }),
889
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtl-note", children: "\u7EDF\u8BA1\u7684\u662F\u4F1A\u8BDD\u65E5\u5FD7\u6587\u4EF6\u7684\u78C1\u76D8\u5360\u7528\uFF08\u538B\u7F29\u540E\u7684\u5B9E\u9645\u5927\u5C0F\uFF09\uFF0C\u53EA\u8BFB\uFF0C\u4E0D\u4FEE\u6539\u4EFB\u4F55\u6570\u636E\u3002" })
890
+ ] })
891
+ ] }),
892
+ showSessionList && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
690
893
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "sess-tools", "aria-label": "\u67E5\u627E\u548C\u6574\u7406\u4F1A\u8BDD", children: [
691
894
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "sess-field", children: [
692
895
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "dsm-search", children: "\u641C\u7D22\u4F1A\u8BDD" }),
@@ -712,11 +915,11 @@ function SessionPanel({ workspacesSvc }) {
712
915
  "\u663E\u793A ",
713
916
  list.length,
714
917
  " \u4E2A\u4F1A\u8BDD",
715
- query || workspaceFilter !== "all" ? `\uFF0C\u5171 ${filter === "archived" ? archivedList.length : filter === "active" ? activeList.length : sessions.length} \u4E2A` : ""
918
+ query || workspaceFilter !== "all" ? `\uFF0C\u5171 ${filter === "archived" ? archivedList.length : filter === "active" ? activeList.length : filter === "starred" ? starredList.length : sessions.length} \u4E2A` : ""
716
919
  ] })
717
920
  ] }),
718
- filter !== "trash" && list.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "sess-batch", children: [
719
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "sess-btntext", children: selIds.length ? `\u5DF2\u9009 ${selIds.length} \u9879` : filter === "archived" ? `\u5171 ${archivedList.length} \u4E2A\u5F52\u6863\u4F1A\u8BDD` : `\u5171 ${sessions.length} \u4E2A\u4F1A\u8BDD\uFF08\u6D3B\u52A8 ${activeList.length} / \u5DF2\u5F52\u6863 ${archivedList.length}\uFF09` }),
921
+ showSessionList && list.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "sess-batch", children: [
922
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "sess-btntext", children: selIds.length ? `\u5DF2\u9009 ${selIds.length} \u9879` : filter === "archived" ? `\u5171 ${archivedList.length} \u4E2A\u5F52\u6863\u4F1A\u8BDD` : filter === "starred" ? `\u5171 ${starredList.length} \u4E2A\u6536\u85CF\u4F1A\u8BDD` : `\u5171 ${sessions.length} \u4E2A\u4F1A\u8BDD\uFF08\u6D3B\u52A8 ${activeList.length} / \u5DF2\u5F52\u6863 ${archivedList.length}\uFF09` }),
720
923
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn", disabled: list.length === 0, onClick: selectAll, children: "\u5168\u9009" }),
721
924
  selIds.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
722
925
  filter === "archived" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
@@ -727,7 +930,7 @@ function SessionPanel({ workspacesSvc }) {
727
930
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn", onClick: clearSel, children: "\u53D6\u6D88\u9009\u62E9" })
728
931
  ] })
729
932
  ] }),
730
- filter !== "trash" && list.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "archv-empty", children: query || workspaceFilter !== "all" ? "\u6CA1\u6709\u5339\u914D\u7684\u4F1A\u8BDD\u3002\u8BF7\u8C03\u6574\u641C\u7D22\u8BCD\u6216\u5DE5\u4F5C\u533A\u7B5B\u9009\u3002" : filter === "archived" ? "\u76EE\u524D\u6CA1\u6709\u5F52\u6863\u4F1A\u8BDD\u3002\u5728\u201C\u5168\u90E8\u201D\u91CC\u9009\u4E2D\u4F1A\u8BDD\u70B9\u201C\u5F52\u6863\u201D\u5373\u53EF\u6536\u7EB3\u8FDB\u6765\u3002" : filter === "active" ? "\u76EE\u524D\u6CA1\u6709\u6D3B\u52A8\u4F1A\u8BDD\u3002" : "\u6682\u65E0\u53EF\u7BA1\u7406\u7684\u4F1A\u8BDD\u3002" }) : filter !== "trash" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "archv-list", role: "list", children: list.map((it) => {
933
+ showSessionList && list.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "archv-empty", children: query || workspaceFilter !== "all" ? "\u6CA1\u6709\u5339\u914D\u7684\u4F1A\u8BDD\u3002\u8BF7\u8C03\u6574\u641C\u7D22\u8BCD\u6216\u5DE5\u4F5C\u533A\u7B5B\u9009\u3002" : filter === "archived" ? "\u76EE\u524D\u6CA1\u6709\u5F52\u6863\u4F1A\u8BDD\u3002\u5728\u201C\u5168\u90E8\u201D\u91CC\u9009\u4E2D\u4F1A\u8BDD\u70B9\u201C\u5F52\u6863\u201D\u5373\u53EF\u6536\u7EB3\u8FDB\u6765\u3002" : filter === "active" ? "\u76EE\u524D\u6CA1\u6709\u6D3B\u52A8\u4F1A\u8BDD\u3002" : filter === "starred" ? "\u8FD8\u6CA1\u6709\u6536\u85CF\u7684\u4F1A\u8BDD\u3002\u70B9\u51FB\u4F1A\u8BDD\u5DE6\u4FA7\u7684\u661F\u6807\u5373\u53EF\u6536\u85CF\u3002" : "\u6682\u65E0\u53EF\u7BA1\u7406\u7684\u4F1A\u8BDD\u3002" }) : showSessionList ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "archv-list", role: "list", children: list.map((it) => {
731
934
  const date = fmtDate(it.createdAt);
732
935
  const expanded = openMove === it.sessionId;
733
936
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "archv-card" + (expanded ? " archv-card-exp" : ""), role: "listitem", children: [
@@ -742,6 +945,21 @@ function SessionPanel({ workspacesSvc }) {
742
945
  "aria-label": "\u9009\u62E9 " + (it.title || it.sessionId)
743
946
  }
744
947
  ),
948
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
949
+ "button",
950
+ {
951
+ type: "button",
952
+ className: "archv-star" + (it.starred ? " archv-star-on" : ""),
953
+ "aria-pressed": !!it.starred,
954
+ "aria-label": (it.starred ? "\u53D6\u6D88\u6536\u85CF " : "\u6536\u85CF ") + (it.title || it.sessionId),
955
+ title: it.starred ? "\u53D6\u6D88\u6536\u85CF" : "\u6536\u85CF",
956
+ onClick: (e) => {
957
+ e.stopPropagation();
958
+ toggleStar(it);
959
+ },
960
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: "20", height: "20", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 2.5l2.9 5.9 6.6.9-4.8 4.6 1.2 6.5-5.9-3.1-5.9 3.1 1.2-6.5L2.5 9.3l6.6-.9z" }) })
961
+ }
962
+ ),
745
963
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "archv-body", children: [
746
964
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "archv-main", children: [
747
965
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "archv-name", title: it.title || "", children: it.title || "(\u65E0\u6807\u9898)" }),
@@ -911,6 +1129,24 @@ function SessionPanel({ workspacesSvc }) {
911
1129
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: d.lineage.subagents.join(", ") })
912
1130
  ] })
913
1131
  ] })
1132
+ ] }),
1133
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtl-sec", children: [
1134
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtl-sec-t", children: "\u5BFC\u51FA" }),
1135
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dtl-export", children: [
1136
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1137
+ "a",
1138
+ {
1139
+ className: "archv-btn",
1140
+ href: `/api/session.export?sessionId=${encodeURIComponent(it.sessionId)}&includeDescendants=true`,
1141
+ onClick: (e) => e.stopPropagation(),
1142
+ style: zipOk ? void 0 : { pointerEvents: "none", opacity: 0.45 },
1143
+ title: zipOk ? "\u542B\u5B50\u4F1A\u8BDD\u4E0E\u9644\u4EF6\uFF0C\u7531 DSH \u63D0\u4F9B" : "\u5F53\u524D\u6301\u4E45\u5316\u540E\u7AEF\u4E0D\u652F\u6301\u539F\u59CB\u65E5\u5FD7\u5BFC\u51FA",
1144
+ children: "\u4E0B\u8F7D\u539F\u59CB\u65E5\u5FD7 (ZIP)"
1145
+ }
1146
+ ),
1147
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "archv-btn", disabled: mdBusy === it.sessionId, onClick: () => exportMarkdown(it), children: mdBusy === it.sessionId ? "\u751F\u6210\u4E2D\u2026" : "\u5BFC\u51FA Markdown" })
1148
+ ] }),
1149
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dtl-note", children: "ZIP \u542B\u5B50\u4F1A\u8BDD\u4E0E\u9644\u4EF6\uFF0C\u7531 DSH \u63D0\u4F9B \xB7 Markdown \u4E3A\u672C\u63D2\u4EF6\u751F\u6210\u7684\u53EF\u8BFB\u5BF9\u8BDD\u8BB0\u5F55" })
914
1150
  ] })
915
1151
  ] });
916
1152
  })()
@@ -1035,6 +1271,21 @@ var SIDEBAR_AUG_CSS = `
1035
1271
  .dsm-trash-date{font-size:11px;color:var(--dsw-alias-label-tertiary);flex:none;white-space:nowrap}
1036
1272
  .dsm-trash-actions{display:flex;gap:6px;flex:none}.dsm-trash-actions .archv-btn{min-width:72px}
1037
1273
  .dsm-trash-empty{font-size:12px;color:var(--dsw-alias-label-tertiary);padding:6px 2px}
1274
+ .dsm-storage-sum{font-size:12px;color:var(--dsw-alias-label-secondary);margin-bottom:10px}
1275
+ .dsm-storage-list{display:flex;flex-direction:column;gap:6px}
1276
+ .dsm-storage-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-fill-elevated)}
1277
+ .dsm-storage-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;color:var(--dsw-alias-label-primary)}
1278
+ .dsm-storage-bar{flex:0 0 96px;height:6px;border-radius:999px;background:var(--dsw-alias-fill-subtle);overflow:hidden}
1279
+ .dsm-storage-fill{display:block;height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary)}
1280
+ .dsm-storage-size{font-size:12px;color:var(--dsw-alias-label-secondary);flex:none;white-space:nowrap}
1281
+ .dsm-storage-count{font-size:11px;color:var(--dsw-alias-label-tertiary);flex:none;white-space:nowrap;max-width:32%;overflow:hidden;text-overflow:ellipsis}
1282
+ .maint-bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 8px}
1283
+ .maint-bar-right{margin-left:auto}
1284
+ .maint-note{font-size:11px;color:var(--dsw-alias-label-tertiary)}
1285
+ .mv-sheet-actions{display:flex;align-items:center;gap:6px}
1286
+ .aa-check{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer}
1287
+ .aa-check input{width:15px;height:15px;accent-color:var(--dsw-alias-state-business-primary);cursor:pointer;flex:none}
1288
+ @media (max-width:640px){.dsm-storage-bar{display:none}.dsm-storage-count{max-width:40%}}
1038
1289
  `;
1039
1290
  var DSM_KEY_MANUAL = "dsm-manual-unread-v1";
1040
1291
  var dsmManualUnread = null;