dsh-session-manager 0.7.4 → 0.7.5

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
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 0.7.5 — 2026-10-22
6
+
7
+ - **fix(security)**: Enforce centralized API authentication on all `/session-manager/api/*` routes using DSH official `ctx.connection.admit(req)`. Enforce strict Owner-only authorization (`peer.principal.kind === "owner"`), reject untrusted, unauthenticated, empty, or non-owner requests before body reading and database queries, and fail closed with HTTP 503 (`auth-unavailable`) if connection service is missing or unready, preventing unauthorized access across public tunnels, reverse proxies, and local networks (#43).
8
+ - **fix(move)**: Synchronize the effective `workingDirectory` projection upon moving sessions across workspaces. For active sessions, call official `workingDirectory.set(agent, path)` when active or fall back to appending `working-directory/change` events to fold the projection in real time, and flush to disk via projection cache; for cold sessions, rewrite headers when un-diverged or append change events for sequential logs. For unmigrated packed rows with conflicting historical working-directory events, safely refuse moves with HTTP 409 (`working-directory-sync-unsafe`) before any file write to prevent destination directory drift upon restart (#42).
9
+ - **fix(ui)**: Align session header action buttons with native DSH session title bar style: unify height to 28px, remove idle border and background (transparent ghost buttons), add standard 6px border-radius, use official hover and active tokens, style danger buttons cleanly, and replace raw Unicode emojis with crisp 14×14 SVG vector icons (#44).
10
+ - **fix(ui)**: Optimize session header and manager panel layout: set header slot order to 0 to sit immediately after static session preset and team entries and before dynamic subagent and task status indicators; switch header action buttons to a fixed-width (flex-shrink: 0, 28px square) icon-only layout with corner priority badge; in session manager panel row actions, convert buttons matching header (archive, favorite, review, edit, move, delete) to clean icon-only buttons with hover tooltips while keeping non-header ("Open", "Migrate") and bulk action buttons with visible text labels (#46).
11
+ - **test**: Add comprehensive test suites for API authentication gating (`issue-43-api-auth.test.mjs`), cross-workspace working directory synchronization (`issue-42-working-directory-after-move.test.mjs`), header action button styling with SVG vector icons (`issue-44-header-styles.test.mjs`), and header/panel row layout optimization (`issue-46-header-layout.test.mjs`).
12
+ - **compat**: Compatible with DSH Desktop v0.2.1-rc.2 (and v0.2.0-rc.2).
13
+
5
14
  ## 0.7.4 — 2026-10-20
6
15
 
7
16
  - **fix(workspace)**: Repair workspace session membership on DSH 0.2.x. New and forked sessions are now attributed to the workspace owning their `cwd` as soon as the session is announced (runtime repair, no restart needed), and historical sessions whose `cwd` matches a registered workspace are re-registered on startup — all through the official DSH workspace entity API (#41).
package/README.md CHANGED
@@ -20,7 +20,7 @@ Verified with the official DSH Desktop client; see [5 Compatibility](#5-compatib
20
20
 
21
21
  - **Delete** is irreversible and always asks for confirmation. Subagent sessions and transient blank placeholders (no persisted artifact) cannot be deleted.
22
22
  - **Archive / Unarchive** moves a session in and out of the active list without touching its disk content.
23
- - **Move to workspace** keeps history, title, archive state and derived-session relationships intact, rewrites the session's `cwd` to the target workspace, and updates the live writer's header in place so any pending tool calls keep landing on the new path. When the session's `cwd` already equals the target workspace path but its membership was never recorded, only the missing membership is repaired: no `cwd` rewrite, no artifact rewrite or move, and no session restart.
23
+ - **Move to workspace** keeps history, title, archive state and derived-session relationships intact, rewrites the session's `cwd` to the target workspace, synchronizes the runtime `workingDirectory` projection for active agents, and updates the live writer's header in place so any pending tool calls keep landing on the new path. When the session's `cwd` already equals the target workspace path but its membership was never recorded, only the missing membership is repaired: no `cwd` rewrite, no artifact rewrite or move, and no session restart.
24
24
  - **Migrate Agent preset** rewrites the latest `agent-preset/selected` event (or the session header if no such event exists) so a session whose preset was renamed or removed can resume. Message history is never altered.
25
25
 
26
26
  ### 1.2 Session shortcuts
@@ -66,7 +66,7 @@ Verified with the official DSH Desktop client; see [5 Compatibility](#5-compatib
66
66
 
67
67
  ### 2.1 Title bar
68
68
 
69
- The right side of the title area exposes actions for the **current session**: **Archive / Unarchive**, **Tags / Notes**, **Move to workspace**, **Delete session**.
69
+ The right side of the title area exposes actions for the **current session**: **Archive / Unarchive**, **Favorites**, **Review-later**, **Tags / Notes**, **Move to workspace**, **Delete session**, and **More actions** (`⋯`), matching native DSH title-bar button sizing (28px height, transparent ghost style) and vector SVG icon design.
70
70
 
71
71
  ### 2.2 Sessions and workspaces management entry and interface
72
72
 
@@ -137,7 +137,8 @@ After changing plugin dependencies, `package.json`, bundle configuration, or oth
137
137
  - Preset migration separates backup, publication and rollback. If rollback fails, recovery files are retained and their paths are included in the error; do not remove them.
138
138
  - **Workspace archiving rollback protection**: archiving is refused if active sessions are present, and batch session archiving automatically rolls back on partial failures to preserve state consistency.
139
139
  - On startup, sessions whose `cwd` names a registered workspace but whose membership record is missing are re-registered through the official DSH API (idempotent). Incomplete scans skip pruning only: existing membership is never dropped, and live sessions or memberships added during the scan are always preserved.
140
- - Plugin mutations are serialized per session and request bodies are limited to 64 KiB. This queue supplements, rather than replaces, DSH's own persistence coordination.
140
+ - Plugin mutations are serialized per session and request bodies are limited to 64 KiB. This queue supplements, rather than replaces, DSH's own persistence coordination.
141
+ - **Centralized API authentication**: All `/session-manager/api/*` routes are protected by DSH official `connection.admit()` and `requestRejection()` gates, strictly enforcing Owner-only privileges (`peer.principal.kind === "owner"`) and failing closed (HTTP 503) if connection services are absent, ensuring that exposed network tunnels or local proxies cannot bypass access control.
141
142
 
142
143
  ## 5 Compatibility
143
144
 
@@ -147,7 +148,7 @@ The table below lists verified version combinations between DeepSeek Harness (DS
147
148
 
148
149
  | DeepSeek Harness (DSH) | dsh-session-manager | Verified Environment |
149
150
  | :--- | :--- | :--- |
150
- | **v0.2.1-rc.2** | `0.7.4`, `0.7.3`, `0.7.2` | Desktop |
151
+ | **v0.2.1-rc.2** | `0.7.5`, `0.7.4`, `0.7.3`, `0.7.2` | Desktop |
151
152
  | **v0.2.0-rc.2** | `0.7.1`, `0.7.0`, `0.6.4`, `0.6.3`, `0.6.2`, `0.5.4` | Desktop |
152
153
  | **v0.1.7-rc.2** | `0.5.3` | Desktop / Web UI |
153
154
  | **v0.1.7-rc.1** | `0.5.2` | Desktop / Web UI |
package/README.zh.md CHANGED
@@ -20,7 +20,7 @@ DSH 会话与工作区管理——工作区:归档与显示。会话:跨工
20
20
 
21
21
  - **删除**:不可逆操作,UI 始终要求二次确认。subagent 会话和尚未落盘的空白会话占位不能删除。
22
22
  - **归档 / 移出归档**:把会话移出或移回主列表,不删除磁盘内容。
23
- - **移动至工作区**:跨工作区移动时保留历史、标题、归档状态和派生会话关系,同时把 `cwd` 重写为目标工作区,并就地更新 live writer 的 header,使挂起的工具调用继续落到新路径。若会话的 `cwd` 本来就等于目标工作区、只是归属未登记,则只补全归属记录,不改写 `cwd`、不重写或移动工件、不重启会话。
23
+ - **移动至工作区**:跨工作区移动时保留历史、标题、归档状态和派生会话关系,同时把 `cwd` 重写为目标工作区,实时同步活动 Agent 的运行时 `workingDirectory` 投影,并就地更新 live writer 的 header,使挂起的工具调用继续落到新路径。若会话的 `cwd` 本来就等于目标工作区、只是归属未登记,则只补全归属记录,不改写 `cwd`、不重写或移动工件、不重启会话。
24
24
  - **迁移 Agent 预设**:原预设被改名或删除导致会话无法恢复时,可修复该会话。迁移会就地重写最后一条 `agent-preset/selected` 事件(若从未记录则修改会话 header),不改动历史消息。
25
25
 
26
26
  ### 1.2 会话快捷管理
@@ -64,7 +64,7 @@ DSH 会话与工作区管理——工作区:归档与显示。会话:跨工
64
64
 
65
65
  ### 2.1 标题栏入口
66
66
 
67
- 标题栏右侧对**当前会话**提供:**归档 / 移出归档**、**标签 / 备注**、**移动至工作区**、**删除会话**。
67
+ 标题栏右侧对**当前会话**提供:**归档 / 移出归档**、**收藏**、**待回看**、**标签 / 备注**、**移动至工作区**、**删除会话**以及**更多操作**(`⋯`),与 DSH 原生顶栏按钮规范完全对齐(28px 高度、无边框幽灵透明样式、SVG 矢量描边图标)。
68
68
 
69
69
  ### 2.2 会话与工作区管理入口与界面
70
70
 
@@ -136,6 +136,7 @@ dsh plugin --profile web add <本仓库路径>
136
136
  - **工作区归档事务保护**:若工作区内存在运行中会话则拒绝归档;批量归档会话若中途遇到异常会自动回滚已变更状态,避免部分成功导致状态不一致。
137
137
  - 启动时若发现 `cwd` 对应已注册工作区但归属记录遗漏的会话,会通过 DSH 官方接口补全登记(幂等);扫描不完整时只跳过清理,不会误删现有归属,也不会清除仍在内存中或扫描期间新加入的会话。
138
138
  - 同一会话的插件写操作按顺序执行;请求体限制为 64 KiB。该队列不替代 DSH 自身的持久化写入协调。
139
+ - **集中 API 鉴权与安全门禁**:所有 `/session-manager/api/*` 接口统一接入 DSH 官方 `connection.admit()` / `requestRejection()` 校验,严格限制仅 Owner 权限主体可调用,服务未就绪时默认拒绝(HTTP 503 Fail-Closed),有效阻断公网隧道或反向代理暴露下的未授权访问。
139
140
 
140
141
  ## 5 兼容性
141
142
 
@@ -145,7 +146,7 @@ dsh plugin --profile web add <本仓库路径>
145
146
 
146
147
  | DeepSeek Harness (DSH) | dsh-session-manager | 验证环境 |
147
148
  | :--- | :--- | :--- |
148
- | **v0.2.1-rc.2** | `0.7.4`, `0.7.3`, `0.7.2` | Desktop |
149
+ | **v0.2.1-rc.2** | `0.7.5`, `0.7.4`, `0.7.3`, `0.7.2` | Desktop |
149
150
  | **v0.2.0-rc.2** | `0.7.1`, `0.7.0`, `0.6.4`, `0.6.3`, `0.6.2`, `0.5.4` | Desktop |
150
151
  | **v0.1.7-rc.2** | `0.5.3` | Desktop / Web UI |
151
152
  | **v0.1.7-rc.1** | `0.5.2` | Desktop / Web UI |
package/lib/client.js CHANGED
@@ -289,6 +289,7 @@ function isTopmostDialog(z) {
289
289
  "confirm.move.current": "当前工作区:{name}",
290
290
  "confirm.move.same": "请选择与当前工作区不同的目标工作区",
291
291
  "confirm.move.confirm": "确认移动",
292
+ "move.restartRequired": "会话已成功移动至目标工作区,但当前工作目录需要在重启 DSH 后生效。",
292
293
  "confirm.cancel": "取消",
293
294
  "busy.processing": "处理中…",
294
295
  "error.operation": "操作失败:{message}",
@@ -640,6 +641,7 @@ function isTopmostDialog(z) {
640
641
  "confirm.move.current": "Current workspace: {name}",
641
642
  "confirm.move.same": "Choose a target workspace different from the current workspace",
642
643
  "confirm.move.confirm": "Move",
644
+ "move.restartRequired": "Session moved to target workspace successfully, but working directory will take effect after restarting DSH.",
643
645
  "confirm.cancel": "Cancel",
644
646
  "busy.processing": "Working…",
645
647
  "error.operation": "Operation failed: {message}",
@@ -693,11 +695,18 @@ function isTopmostDialog(z) {
693
695
  };
694
696
 
695
697
  const css = [
696
- ".sm-header{display:flex;align-items:center;gap:6px}",
697
- ".sm-headerBtn{box-sizing:border-box;min-height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;align-items:center;gap:4px;padding:3px 10px;font-size:12px;line-height:18px;display:inline-flex;white-space:nowrap}",
698
- ".sm-headerBtn:hover:not(:disabled),.sm-headerBtn:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}",
698
+ ".sm-header{display:inline-flex;align-items:center;gap:6px;flex-wrap:nowrap}",
699
+ ".sm-header{flex-shrink:0}",
700
+ ".sm-headerBtn{display:inline-flex;align-items:center;justify-content:center;gap:4px;min-height:28px;height:28px;box-sizing:border-box;color:var(--dsw-alias-label-secondary);background:transparent;border:0;border-radius:6px;cursor:pointer;padding:0 6px;font-size:12px;line-height:18px;white-space:nowrap;transition:background .12s,color .12s;flex-shrink:0}",
701
+ ".sm-header .sm-headerBtn{width:28px;min-width:28px;padding:0;justify-content:center;position:relative}",
702
+ ".sm-header .sm-headerBtnLabel{display:none}",
703
+ ".sm-header .sm-priorityBadge{position:absolute;top:0;right:0;font-size:8px;line-height:9px;padding:0 2px;border-radius:3px;pointer-events:none}",
704
+ ".sm-headerBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
705
+ ".sm-headerBtn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
699
706
  ".sm-headerBtn:disabled{opacity:.5;cursor:not-allowed}",
700
- ".sm-headerBtn svg{flex:none}",
707
+ ".sm-headerBtn.sm-headerBtnActive{background:var(--dsw-alias-button-ghost-active-fill,rgba(0,0,0,.06));color:var(--dsw-alias-label-primary)}",
708
+ ".sm-headerBtn svg{width:14px;height:14px;display:block;margin:auto;flex-shrink:0}",
709
+ ".sm-headerBtn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
701
710
  ".sm-busy{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:28px;margin-left:4px}",
702
711
  ".sm-footer{display:flex}",
703
712
  ".sm-footerBtn{min-height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:6px;align-items:center;gap:6px;padding:3px 8px;font-size:12px;line-height:18px;display:inline-flex}",
@@ -745,8 +754,8 @@ function isTopmostDialog(z) {
745
754
  ".sm-moveItem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
746
755
  ".sm-moveItemSelected{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}",
747
756
  ".sm-moveItemName{color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}",
748
- ".sm-moveItemMeta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}.sm-panelModal{width:760px;max-width:92vw}.sm-panelModal .sm-rowActions{flex-wrap:wrap;justify-content:flex-end}.sm-headerMenu{position:absolute;right:0;top:100%;margin-top:4px;z-index:100005;display:flex;flex-direction:column;min-width:140px;padding:4px;background:var(--dsw-alias-bg-layer-2,#fff);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.15)}.sm-headerMenuItem{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:0 0;border:0;border-radius:6px;color:var(--dsw-alias-label-primary,#111);cursor:pointer;padding:6px 10px;font-size:13px;line-height:18px;white-space:nowrap}.sm-headerMenuItem:hover{background:var(--dsw-alias-interactive-bg-hover)}.sm-headerMenuItem:disabled{opacity:.5;cursor:not-allowed}.sm-headerMenuItem .sm-headerBtnIcon{width:20px;min-width:20px;text-align:center;flex-shrink:0}.sm-headerMenuItemDanger{color:var(--dsw-alias-state-error-primary,#d92d20)}.sm-headerMenuItemDanger:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}.sm-headerMenuUltraOnly{display:none!important}.sm-headerBtn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:32px;height:32px;box-sizing:border-box;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-button-floating-fill,#fff);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;cursor:pointer;padding:0 10px;font-size:13px;line-height:1;white-space:nowrap;transition:background .15s,color .15s,border-color .15s;flex-shrink:0}.sm-headerBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l1)}.sm-headerBtn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}.sm-headerBtn svg{width:16px;height:16px;display:block;margin:auto;flex-shrink:0}.sm-migratePanel{display:flex;flex-direction:column;gap:14px;min-width:480px}.sm-migrateRow{display:flex;flex-direction:column;gap:8px}.sm-migrateRow>label{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:16px;font-weight:500}.sm-migrateChips{display:flex;flex-wrap:wrap;gap:6px}.sm-migrateChip{padding:6px 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:transparent;cursor:pointer;font-size:13px;line-height:18px;color:var(--dsw-alias-label-primary);white-space:nowrap}.sm-migrateChip:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.sm-migrateChip:disabled{opacity:.5;cursor:not-allowed}.sm-migrateChipOn{background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-business-primary)}.sm-migrateEmpty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.sm-migratePreview{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}.sm-migrateWarn{padding:10px 12px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 30%,transparent);border-radius:8px;background:var(--dsw-alias-state-warn-tertiary,#fef5e7);color:var(--dsw-alias-label-secondary);font-size:13px;line-height:18px}.sm-migrateList{max-height:200px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:8px}.sm-headerBtnDanger{background:var(--dsw-alias-button-floating-fill,#fff);color:var(--dsw-alias-state-error-primary,#d92d20);border-color:var(--dsw-alias-state-error-primary,#d92d20);font-weight:500}.sm-headerBtnDanger:hover:not(:disabled){background:var(--dsw-alias-state-error-primary,#d92d20);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-error-primary,#d92d20)}.sm-header{display:inline-flex;align-items:center;gap:6px;flex-wrap:nowrap}.sm-headerSecondaryActions{display:inline-flex;align-items:center;gap:6px;flex-wrap:nowrap}.sm-headerCompactMenu{display:none;position:relative}.sm-headerCompactMenu.sm-headerCompactMenuOpen{position:relative;z-index:100004}.sm-headerCompactMenuBtn{width:32px;min-width:32px;height:32px;padding:0;justify-content:center;font-size:16px;line-height:1}.sm-headerBtnIcon{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;font-size:14px;line-height:1;vertical-align:middle;flex-shrink:0}.sm-headerBtn.sm-markIcon{width:32px;min-width:32px;height:32px;padding:0;justify-content:center}.sm-headerBtn.sm-markIcon .sm-headerBtnIcon{width:16px;height:16px;font-size:15px;line-height:1}.sm-headerCompactMenuBtn .sm-headerBtnIcon{width:16px;height:16px;font-size:16px;line-height:1}.sm-annotationToggleOpen{position:relative;z-index:100004}.sm-headerBtnActive{background:var(--dsw-alias-button-ghost-active-fill);color:var(--dsw-alias-label-primary)}@container (max-width:1000px){.sm-headerBtn{width:32px;min-width:32px;height:32px;padding:0;justify-content:center}.sm-headerBtnLabel,.sm-priorityText,.sm-priorityBadge{display:none}.sm-headerBtn.sm-headerBtn-withBadge{width:auto;min-width:32px;padding:0 6px}}@container (max-width:520px){.sm-headerSecondaryActions,.sm-headerSecondaryAction{display:none!important}.sm-headerCompactMenu{display:inline-flex}}@container (max-width:420px){.sm-header>:not(.sm-headerCompactMenu){display:none!important}.sm-header .sm-headerBtn:not(.sm-headerCompactMenuBtn){display:none!important}.sm-headerCompactMenu{display:inline-flex!important}.sm-headerMenu{right:auto!important;left:0!important}.sm-headerMenuUltraOnly{display:flex!important}}",
749
- ".sm-panelDialog{position:fixed;inset:0;margin:auto;transform:none;width:680px;max-width:calc(100vw - 32px);height:560px;max-height:calc(100vh - 32px);box-sizing:border-box;display:flex;flex-direction:column;gap:0;background:var(--sm-dialog-fill);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.25);overflow:hidden;z-index:1020}.sm-panelDialog .sm-nativeDialogHeader{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none;position:relative;min-height:46px;box-sizing:border-box}.sm-panelDialog .sm-nativeDialogTitle{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin:0;font-size:14px;line-height:20px;font-weight:600;min-width:0;max-width:calc(100% - 140px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center;pointer-events:auto;z-index:0}.sm-panelDialog .sm-nativeDialogHeaderActions{display:flex;align-items:center;gap:6px;flex:none;position:relative;z-index:1;margin-left:auto}.sm-nativeDialogFooter,.sm-bulkDialog .sm-nativeDialogFooter,.sm-confirmDialog .sm-nativeDialogFooter,.sm-migrateDialog .sm-nativeDialogFooter{display:flex;padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex:none;flex-wrap:wrap;justify-content:space-between;gap:6px;margin-top:0}.sm-panelDialog .sm-panel{display:flex;flex-direction:column;gap:6px;min-height:0;flex:1 1 auto;overflow:auto;padding:8px 12px}.sm-panelDialog .sm-list{display:flex;flex-direction:column;gap:2px;flex:1 1 auto;min-height:0;overflow:auto;scrollbar-gutter:stable}.sm-panelDialog .sm-row{display:flex;align-items:center;gap:8px;padding:6px 10px;min-height:36px;border:1px solid transparent;border-radius:8px;background:transparent;flex-shrink:0;transition:background .12s ease,border-color .12s ease}.sm-panelDialog .sm-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.sm-panelDialog .sm-rowCurrent{background:var(--dsw-alias-bg-module-platform);border-color:var(--dsw-alias-state-business-primary,#356ae6)}.sm-panelDialog .sm-rowMain{display:flex;align-items:stretch!important;gap:8px;min-width:0;flex:1 1 auto;overflow:visible;text-align:left!important}.sm-panelDialog .sm-rowTitle{min-width:0;flex:1 1 0%;width:100%;align-self:stretch;display:flex;align-items:center;justify-content:flex-start;gap:6px;font-size:12px;line-height:16px;overflow:hidden;text-align:left!important;direction:ltr!important;unicode-bidi:plaintext}.sm-confirmDialog{z-index:10000}.sm-confirmDialog.sm-nativeDialogLayer{position:fixed;left:calc(50vw + 184px);top:calc(50vh - 90px);width:360px;max-width:calc(100vw - 32px);max-height:calc(100vh - 32px);display:block;align-items:initial;justify-content:initial;box-sizing:border-box;background:transparent;border:0;box-shadow:none;padding:0;inset:calc(50vh - 90px) auto auto calc(50vw + 308px)}.sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialogBackdrop{display:none}.sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialog{position:relative;left:auto;top:auto;transform:none;width:auto;max-width:calc(100vw - 32px);max-height:calc(100vh - 32px);display:flex;flex-direction:column;box-sizing:border-box;background:var(--sm-dialog-fill);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.25);padding:0;overflow:hidden;animation:sm-confirmPop .14s ease-out}.sm-confirmDialog .sm-nativeDialogHeader{position:relative;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);display:flex;align-items:center;justify-content:center;min-height:44px;box-sizing:border-box}.sm-confirmDialog .sm-nativeDialogTitle{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin:0;font-size:14px;line-height:20px;font-weight:600;text-align:center;width:auto;max-width:calc(100% - 32px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sm-confirmDialog .sm-nativeDialogBody{padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0;max-height:calc(100vh - 120px);overflow-y:auto}.sm-confirmDialog .sm-nativeDialogDescription{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap}.sm-confirmDialog .sm-warn{display:flex;align-items:flex-start;gap:8px;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#ec1313) 8%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary,#ec1313) 25%,transparent);border-radius:8px;padding:8px 10px;margin-top:0}.sm-confirmDialog .sm-nativeDialogFooter{padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex-wrap:wrap;justify-content:space-between;gap:6px;margin-top:0}.sm-confirmDialog .sm-nativeDialogButton{min-height:28px;padding:3px 14px;font-size:12px;line-height:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;background:transparent;color:var(--dsw-alias-label-primary);text-align:center}.sm-confirmDialog .sm-nativeDialogCancel:hover:not(:disabled),.sm-migrateDialog .sm-nativeDialogCancel:hover:not(:disabled),.sm-updateDialog .sm-nativeDialogCancel:hover:not(:disabled),.sm-bulkDialog .sm-nativeDialogCancel:hover:not(:disabled),.sm-panelDialog .sm-nativeDialogCancel:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.sm-confirmDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-state-business-primary,#356ae6);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-business-primary,#356ae6)}.sm-confirmDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}@keyframes sm-confirmPop{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.sm-confirmDialog.sm-nativeDialog{padding:0;width:auto;max-width:none}.sm-panelDialog .sm-rowTitle>span:first-child{display:block;min-width:0;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;flex:1 1 auto;width:auto;text-align:left!important;direction:ltr!important;unicode-bidi:plaintext}.sm-panelDialog .sm-rowMeta{display:none}.sm-panelDialog .sm-rowActions{display:flex;gap:2px;flex:none;justify-content:flex-end}.sm-panelDialog .sm-rowActions .sm-rowBtn{min-height:22px;padding:0 7px;font-size:11px;line-height:18px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap}.sm-rowBtn.sm-markIcon{padding:0 6px;font-size:13px;line-height:18px}.sm-panelDialog .sm-rowActions .sm-rowBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.sm-panelDialog .sm-rowActions .sm-rowBtnDanger{color:var(--dsw-alias-state-error-primary)}.sm-panelDialog .sm-rowActions .sm-rowBtnDanger:hover{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary)}.sm-panelDialog .sm-filter{flex:none;padding:6px 12px 4px;gap:4px;display:flex;flex-wrap:wrap}.sm-panelDialog .sm-filterBtn{height:28px;min-height:28px;padding:3px 10px;font-size:12px;line-height:18px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;box-sizing:border-box;white-space:nowrap;flex-shrink:0;transition:all .15s ease}.sm-panelDialog .sm-filterBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.sm-panelDialog .sm-filterBtn:disabled{opacity:.4;cursor:not-allowed}.sm-panelDialog .sm-filterBtn.sm-on{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary)}.sm-panelDialog .sm-badge{font-size:10px;line-height:14px;padding:0 5px;border-radius:6px;flex:none}.sm-panelDialog .sm-error{padding:6px 10px;margin:0;font-size:12px;line-height:16px}.sm-panelDialog .sm-nativeDialogButton{min-height:28px;padding:3px 14px;font-size:12px;line-height:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:center}.sm-panelDialog .sm-nativeDialogCancel{background:transparent}.sm-panelDialog .sm-nativeDialogConfirm,.sm-annotationDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-state-business-primary,#356ae6);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-business-primary,#356ae6)}.sm-panelDialog .sm-nativeDialogConfirm:hover:not(:disabled),.sm-annotationDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}.sm-nativeDialogDanger{background:var(--dsw-alias-state-error-primary,#d92d20);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-error-primary,#d92d20)}.sm-nativeDialogDanger:hover:not(:disabled){filter:brightness(.92)}.sm-confirmDialog .sm-nativeDialogDanger,.sm-migrateDialog .sm-nativeDialogDanger,.sm-bulkDialog .sm-nativeDialogDanger{background:var(--dsw-alias-state-error-primary,#d92d20);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-error-primary,#d92d20)}.sm-confirmDialog .sm-nativeDialogDanger:hover:not(:disabled),.sm-migrateDialog .sm-nativeDialogDanger:hover:not(:disabled),.sm-bulkDialog .sm-nativeDialogDanger:hover:not(:disabled){filter:brightness(.92)}.sm-panelDialog .sm-empty{padding:18px 0;font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:center}.sm-panelDialog .sm-footError{font-size:11px;color:var(--dsw-alias-state-error-primary);margin-right:auto;line-height:20px}.sm-panelDialog .sm-moveLabel{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}.sm-migrateDialog .sm-nativeDialogFooter{flex-wrap:wrap;justify-content:space-between;gap:6px;margin-top:0}.sm-migrateDialog .sm-migrateChip{cursor:pointer;pointer-events:auto;position:relative;z-index:1}.sm-migrateDialog .sm-migrateChip[disabled]{opacity:.5;cursor:not-allowed}.sm-migrateDialog.sm-nativeDialogLayer{position:fixed;left:50vw;top:auto;bottom:48px;transform:translateX(-50%);width:min(560px,calc(100vw - 32px));max-height:none;display:block;align-items:initial;justify-content:initial;box-sizing:border-box;background:transparent;border:0;box-shadow:none;padding:0;inset:auto;z-index:10001}.sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialogBackdrop{display:none}.sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialog{position:relative;left:auto;top:auto;transform:none;width:360px;max-width:calc(100vw - 32px);max-height:min(70vh,520px);box-sizing:border-box;background-color:var(--sm-dialog-fill);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.45);overflow:hidden;padding:0;display:flex;flex-direction:column;gap:0}.sm-migrateDialog{position:fixed;left:50vw;top:auto;bottom:48px;transform:translateX(-50%);width:min(560px,calc(100vw - 32px));max-height:min(70vh,520px);box-sizing:border-box;display:flex;flex-direction:column;gap:0;background-color:var(--sm-dialog-fill);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.45);overflow:hidden;z-index:10001}.sm-migrateDialog .sm-nativeDialogHeader{padding:12px 18px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none;position:relative;display:flex;align-items:center;justify-content:center;min-height:44px;box-sizing:border-box}.sm-migrateDialog .sm-nativeDialogTitle{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin:0;font-size:14px;line-height:20px;font-weight:600;color:var(--dsw-alias-label-primary,#111);text-align:center;width:auto;max-width:calc(100% - 36px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sm-migrateDialog .sm-migratePanel{padding:12px 18px;display:flex;flex-direction:column;gap:14px}.sm-migrateDialog .sm-nativeDialogBody{padding:0}.sm-migrateDialog .sm-nativeDialogFooter{padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex:none;flex-wrap:wrap;justify-content:space-between;gap:6px;margin-top:0;gap:8px}.sm-migrateDialog .sm-migrateRow>label{color:var(--dsw-alias-label-secondary,#444)}.sm-migrateDialog .sm-migrateChip{padding:6px 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,#ddd);background-color:var(--dsw-alias-markdown-tag,#f1f3f5);color:var(--dsw-alias-label-primary,#111);font-size:13px;line-height:18px;cursor:pointer;pointer-events:auto;position:relative;z-index:1;white-space:nowrap}.sm-migrateDialog .sm-migrateChip:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover,#e8e8e8)}.sm-migrateDialog .sm-migrateChipOn{background-color:var(--dsw-alias-state-business-primary,#356ae6);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-business-primary,#356ae6)}.sm-migrateDialog .sm-migrateChip.sm-migrateChipOn:hover:not(:disabled){background-color:var(--dsw-alias-button-info-hover,#4576f0);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-button-info-hover,#4576f0);filter:none}.sm-migrateDialog .sm-migrateChipOn:hover:not(:disabled){filter:brightness(.92)}.sm-migrateDialog .sm-migrateChip[disabled]{opacity:.5;cursor:not-allowed}.sm-migrateDialog .sm-migrateEmpty{color:var(--dsw-alias-label-tertiary,#888)}.sm-migrateDialog .sm-migratePreview{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;background-color:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#111);font-size:13px;line-height:18px}.sm-migrateDialog .sm-migrateWarn{padding:10px 12px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 30%,transparent);border-radius:8px;background-color:var(--dsw-alias-state-warn-tertiary,#fef5e7);color:var(--dsw-alias-label-secondary,#444);font-size:13px;line-height:18px}.sm-migrateDialog .sm-nativeDialogButton{min-height:28px;padding:3px 14px;font-size:12px;line-height:20px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;cursor:pointer;background-color:transparent;color:var(--dsw-alias-label-primary,#111);text-align:center}.sm-migrateDialog .sm-nativeDialogButton:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover,#e8e8e8)}.sm-migrateDialog .sm-nativeDialogConfirm{background-color:var(--dsw-alias-state-business-primary,#356ae6);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-business-primary,#356ae6)}.sm-migrateDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}",
757
+ ".sm-moveItemMeta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}.sm-panelModal{width:760px;max-width:92vw}.sm-panelModal .sm-rowActions{flex-wrap:wrap;justify-content:flex-end}.sm-headerMenu{position:absolute;right:0;top:100%;margin-top:4px;z-index:100005;display:flex;flex-direction:column;min-width:140px;padding:4px;background:var(--dsw-alias-bg-layer-2,#fff);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.15)}.sm-headerMenuItem{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:0 0;border:0;border-radius:6px;color:var(--dsw-alias-label-primary,#111);cursor:pointer;padding:6px 10px;font-size:13px;line-height:18px;white-space:nowrap}.sm-headerMenuItem:hover{background:var(--dsw-alias-interactive-bg-hover)}.sm-headerMenuItem:disabled{opacity:.5;cursor:not-allowed}.sm-headerMenuItem .sm-headerBtnIcon{width:14px;min-width:14px;text-align:center;flex-shrink:0}.sm-headerMenuItemDanger{color:var(--dsw-alias-state-error-primary,#d92d20)}.sm-headerMenuItemDanger:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}.sm-headerMenuUltraOnly{display:none!important}.sm-migratePanel{display:flex;flex-direction:column;gap:14px;min-width:480px}.sm-migrateRow{display:flex;flex-direction:column;gap:8px}.sm-migrateRow>label{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:16px;font-weight:500}.sm-migrateChips{display:flex;flex-wrap:wrap;gap:6px}.sm-migrateChip{padding:6px 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:transparent;cursor:pointer;font-size:13px;line-height:18px;color:var(--dsw-alias-label-primary);white-space:nowrap}.sm-migrateChip:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.sm-migrateChip:disabled{opacity:.5;cursor:not-allowed}.sm-migrateChipOn{background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-business-primary)}.sm-migrateEmpty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.sm-migratePreview{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}.sm-migrateWarn{padding:10px 12px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 30%,transparent);border-radius:8px;background:var(--dsw-alias-state-warn-tertiary,#fef5e7);color:var(--dsw-alias-label-secondary);font-size:13px;line-height:18px}.sm-migrateList{max-height:200px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:8px}.sm-headerBtnDanger{background:transparent;color:var(--dsw-alias-state-error-primary,#d92d20);border:0;font-weight:500}.sm-headerBtnDanger:hover:not(:disabled){background:var(--dsw-alias-state-error-primary,#d92d20);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-error-primary,#d92d20)}.sm-headerSecondaryActions{display:inline-flex;align-items:center;gap:6px;flex-wrap:nowrap}.sm-headerCompactMenu{display:none;position:relative}.sm-headerCompactMenu.sm-headerCompactMenuOpen{position:relative;z-index:100004}.sm-headerCompactMenuBtn{width:28px;min-width:28px;height:28px;padding:0;justify-content:center;font-size:14px;line-height:1}.sm-headerBtnIcon{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;font-size:14px;line-height:1;vertical-align:middle;flex-shrink:0}.sm-headerBtn.sm-markIcon{width:28px;min-width:28px;height:28px;padding:0;justify-content:center}.sm-headerBtn.sm-markIcon .sm-headerBtnIcon{width:14px;height:14px;font-size:14px;line-height:1}.sm-headerCompactMenuBtn .sm-headerBtnIcon{width:14px;height:14px;font-size:14px;line-height:1}.sm-annotationToggleOpen{position:relative;z-index:100004}@container (max-width:1000px){.sm-headerBtn{width:28px;min-width:28px;height:28px;padding:0;justify-content:center}.sm-headerBtnLabel,.sm-priorityText,.sm-priorityBadge{display:none}.sm-headerBtn.sm-headerBtn-withBadge{width:auto;min-width:28px;padding:0 6px}}@container (max-width:520px){.sm-headerSecondaryActions,.sm-headerSecondaryAction{display:none!important}.sm-headerCompactMenu{display:inline-flex}}@container (max-width:420px){.sm-header>:not(.sm-headerCompactMenu){display:none!important}.sm-header .sm-headerBtn:not(.sm-headerCompactMenuBtn){display:none!important}.sm-headerCompactMenu{display:inline-flex!important}.sm-headerMenu{right:auto!important;left:0!important}.sm-headerMenuUltraOnly{display:flex!important}}",
758
+ ".sm-panelDialog{position:fixed;inset:0;margin:auto;transform:none;width:680px;max-width:calc(100vw - 32px);height:560px;max-height:calc(100vh - 32px);box-sizing:border-box;display:flex;flex-direction:column;gap:0;background:var(--sm-dialog-fill);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.25);overflow:hidden;z-index:1020}.sm-panelDialog .sm-nativeDialogHeader{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none;position:relative;min-height:46px;box-sizing:border-box}.sm-panelDialog .sm-nativeDialogTitle{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin:0;font-size:14px;line-height:20px;font-weight:600;min-width:0;max-width:calc(100% - 140px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:center;pointer-events:auto;z-index:0}.sm-panelDialog .sm-nativeDialogHeaderActions{display:flex;align-items:center;gap:6px;flex:none;position:relative;z-index:1;margin-left:auto}.sm-nativeDialogFooter,.sm-bulkDialog .sm-nativeDialogFooter,.sm-confirmDialog .sm-nativeDialogFooter,.sm-migrateDialog .sm-nativeDialogFooter{display:flex;padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex:none;flex-wrap:wrap;justify-content:space-between;gap:6px;margin-top:0}.sm-panelDialog .sm-panel{display:flex;flex-direction:column;gap:6px;min-height:0;flex:1 1 auto;overflow:auto;padding:8px 12px}.sm-panelDialog .sm-list{display:flex;flex-direction:column;gap:2px;flex:1 1 auto;min-height:0;overflow:auto;scrollbar-gutter:stable}.sm-panelDialog .sm-row{display:flex;align-items:center;gap:8px;padding:6px 10px;min-height:36px;border:1px solid transparent;border-radius:8px;background:transparent;flex-shrink:0;transition:background .12s ease,border-color .12s ease}.sm-panelDialog .sm-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.sm-panelDialog .sm-rowCurrent{background:var(--dsw-alias-bg-module-platform);border-color:var(--dsw-alias-state-business-primary,#356ae6)}.sm-panelDialog .sm-rowMain{display:flex;align-items:stretch!important;gap:8px;min-width:0;flex:1 1 auto;overflow:visible;text-align:left!important}.sm-panelDialog .sm-rowTitle{min-width:0;flex:1 1 0%;width:100%;align-self:stretch;display:flex;align-items:center;justify-content:flex-start;gap:6px;font-size:12px;line-height:16px;overflow:hidden;text-align:left!important;direction:ltr!important;unicode-bidi:plaintext}.sm-confirmDialog{z-index:10000}.sm-confirmDialog.sm-nativeDialogLayer{position:fixed;left:calc(50vw + 184px);top:calc(50vh - 90px);width:360px;max-width:calc(100vw - 32px);max-height:calc(100vh - 32px);display:block;align-items:initial;justify-content:initial;box-sizing:border-box;background:transparent;border:0;box-shadow:none;padding:0;inset:calc(50vh - 90px) auto auto calc(50vw + 308px)}.sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialogBackdrop{display:none}.sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialog{position:relative;left:auto;top:auto;transform:none;width:auto;max-width:calc(100vw - 32px);max-height:calc(100vh - 32px);display:flex;flex-direction:column;box-sizing:border-box;background:var(--sm-dialog-fill);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.25);padding:0;overflow:hidden;animation:sm-confirmPop .14s ease-out}.sm-confirmDialog .sm-nativeDialogHeader{position:relative;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);display:flex;align-items:center;justify-content:center;min-height:44px;box-sizing:border-box}.sm-confirmDialog .sm-nativeDialogTitle{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin:0;font-size:14px;line-height:20px;font-weight:600;text-align:center;width:auto;max-width:calc(100% - 32px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sm-confirmDialog .sm-nativeDialogBody{padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0;max-height:calc(100vh - 120px);overflow-y:auto}.sm-confirmDialog .sm-nativeDialogDescription{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap}.sm-confirmDialog .sm-warn{display:flex;align-items:flex-start;gap:8px;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#ec1313) 8%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary,#ec1313) 25%,transparent);border-radius:8px;padding:8px 10px;margin-top:0}.sm-confirmDialog .sm-nativeDialogFooter{padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex-wrap:wrap;justify-content:space-between;gap:6px;margin-top:0}.sm-confirmDialog .sm-nativeDialogButton{min-height:28px;padding:3px 14px;font-size:12px;line-height:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;background:transparent;color:var(--dsw-alias-label-primary);text-align:center}.sm-confirmDialog .sm-nativeDialogCancel:hover:not(:disabled),.sm-migrateDialog .sm-nativeDialogCancel:hover:not(:disabled),.sm-updateDialog .sm-nativeDialogCancel:hover:not(:disabled),.sm-bulkDialog .sm-nativeDialogCancel:hover:not(:disabled),.sm-panelDialog .sm-nativeDialogCancel:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.sm-confirmDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-state-business-primary,#356ae6);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-business-primary,#356ae6)}.sm-confirmDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}@keyframes sm-confirmPop{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.sm-confirmDialog.sm-nativeDialog{padding:0;width:auto;max-width:none}.sm-panelDialog .sm-rowTitle>span:first-child{display:block;min-width:0;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;flex:1 1 auto;width:auto;text-align:left!important;direction:ltr!important;unicode-bidi:plaintext}.sm-panelDialog .sm-rowMeta{display:none}.sm-panelDialog .sm-rowActions{display:flex;gap:4px;flex:none;align-items:center;justify-content:flex-end}.sm-panelDialog .sm-rowActions .sm-rowBtn{display:inline-flex;align-items:center;justify-content:center;min-height:22px;height:22px;box-sizing:border-box;padding:0 7px;font-size:11px;line-height:18px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;transition:background .12s,color .12s}.sm-panelDialog .sm-rowActions .sm-rowBtn.sm-rowBtnIcon,.sm-panelDialog .sm-rowActions .sm-rowBtn.sm-markIcon{width:22px;min-width:22px;padding:0;justify-content:center}.sm-panelDialog .sm-rowActions .sm-rowBtn.sm-markIcon .sm-headerBtnIcon,.sm-panelDialog .sm-rowActions .sm-rowBtn.sm-rowBtnIcon .sm-headerBtnIcon{width:14px;height:14px;font-size:14px;line-height:1}.sm-panelDialog .sm-rowActions .sm-rowBtn.sm-rowBtnIcon svg,.sm-panelDialog .sm-rowActions .sm-rowBtn.sm-markIcon svg{width:14px;height:14px;display:block;margin:auto;flex-shrink:0}.sm-rowActions .sm-rowBtnLabel{display:none}.sm-rowBtn.sm-markIcon{padding:0 6px;font-size:13px;line-height:18px}.sm-panelDialog .sm-rowActions .sm-rowBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.sm-panelDialog .sm-rowActions .sm-rowBtnActive{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.sm-panelDialog .sm-rowActions .sm-rowBtnDanger{color:var(--dsw-alias-state-error-primary)}.sm-panelDialog .sm-rowActions .sm-rowBtnDanger:hover{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary)}.sm-panelDialog .sm-filter{flex:none;padding:6px 12px 4px;gap:4px;display:flex;flex-wrap:wrap}.sm-panelDialog .sm-filterBtn{height:28px;min-height:28px;padding:3px 10px;font-size:12px;line-height:18px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;box-sizing:border-box;white-space:nowrap;flex-shrink:0;transition:all .15s ease}.sm-panelDialog .sm-filterBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.sm-panelDialog .sm-filterBtn:disabled{opacity:.4;cursor:not-allowed}.sm-panelDialog .sm-filterBtn.sm-on{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary)}.sm-panelDialog .sm-badge{font-size:10px;line-height:14px;padding:0 5px;border-radius:6px;flex:none}.sm-panelDialog .sm-error{padding:6px 10px;margin:0;font-size:12px;line-height:16px}.sm-panelDialog .sm-nativeDialogButton{min-height:28px;padding:3px 14px;font-size:12px;line-height:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:center}.sm-panelDialog .sm-nativeDialogCancel{background:transparent}.sm-panelDialog .sm-nativeDialogConfirm,.sm-annotationDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-state-business-primary,#356ae6);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-business-primary,#356ae6)}.sm-panelDialog .sm-nativeDialogConfirm:hover:not(:disabled),.sm-annotationDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}.sm-nativeDialogDanger{background:var(--dsw-alias-state-error-primary,#d92d20);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-error-primary,#d92d20)}.sm-nativeDialogDanger:hover:not(:disabled){filter:brightness(.92)}.sm-confirmDialog .sm-nativeDialogDanger,.sm-migrateDialog .sm-nativeDialogDanger,.sm-bulkDialog .sm-nativeDialogDanger{background:var(--dsw-alias-state-error-primary,#d92d20);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-error-primary,#d92d20)}.sm-confirmDialog .sm-nativeDialogDanger:hover:not(:disabled),.sm-migrateDialog .sm-nativeDialogDanger:hover:not(:disabled),.sm-bulkDialog .sm-nativeDialogDanger:hover:not(:disabled){filter:brightness(.92)}.sm-panelDialog .sm-empty{padding:18px 0;font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:center}.sm-panelDialog .sm-footError{font-size:11px;color:var(--dsw-alias-state-error-primary);margin-right:auto;line-height:20px}.sm-panelDialog .sm-moveLabel{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}.sm-migrateDialog .sm-nativeDialogFooter{flex-wrap:wrap;justify-content:space-between;gap:6px;margin-top:0}.sm-migrateDialog .sm-migrateChip{cursor:pointer;pointer-events:auto;position:relative;z-index:1}.sm-migrateDialog .sm-migrateChip[disabled]{opacity:.5;cursor:not-allowed}.sm-migrateDialog.sm-nativeDialogLayer{position:fixed;left:50vw;top:auto;bottom:48px;transform:translateX(-50%);width:min(560px,calc(100vw - 32px));max-height:none;display:block;align-items:initial;justify-content:initial;box-sizing:border-box;background:transparent;border:0;box-shadow:none;padding:0;inset:auto;z-index:10001}.sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialogBackdrop{display:none}.sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialog{position:relative;left:auto;top:auto;transform:none;width:360px;max-width:calc(100vw - 32px);max-height:min(70vh,520px);box-sizing:border-box;background-color:var(--sm-dialog-fill);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.45);overflow:hidden;padding:0;display:flex;flex-direction:column;gap:0}.sm-migrateDialog{position:fixed;left:50vw;top:auto;bottom:48px;transform:translateX(-50%);width:min(560px,calc(100vw - 32px));max-height:min(70vh,520px);box-sizing:border-box;display:flex;flex-direction:column;gap:0;background-color:var(--sm-dialog-fill);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.45);overflow:hidden;z-index:10001}.sm-migrateDialog .sm-nativeDialogHeader{padding:12px 18px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none;position:relative;display:flex;align-items:center;justify-content:center;min-height:44px;box-sizing:border-box}.sm-migrateDialog .sm-nativeDialogTitle{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin:0;font-size:14px;line-height:20px;font-weight:600;color:var(--dsw-alias-label-primary,#111);text-align:center;width:auto;max-width:calc(100% - 36px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sm-migrateDialog .sm-migratePanel{padding:12px 18px;display:flex;flex-direction:column;gap:14px}.sm-migrateDialog .sm-nativeDialogBody{padding:0}.sm-migrateDialog .sm-nativeDialogFooter{padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex:none;flex-wrap:wrap;justify-content:space-between;gap:6px;margin-top:0;gap:8px}.sm-migrateDialog .sm-migrateRow>label{color:var(--dsw-alias-label-secondary,#444)}.sm-migrateDialog .sm-migrateChip{padding:6px 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,#ddd);background-color:var(--dsw-alias-markdown-tag,#f1f3f5);color:var(--dsw-alias-label-primary,#111);font-size:13px;line-height:18px;cursor:pointer;pointer-events:auto;position:relative;z-index:1;white-space:nowrap}.sm-migrateDialog .sm-migrateChip:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover,#e8e8e8)}.sm-migrateDialog .sm-migrateChipOn{background-color:var(--dsw-alias-state-business-primary,#356ae6);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-business-primary,#356ae6)}.sm-migrateDialog .sm-migrateChip.sm-migrateChipOn:hover:not(:disabled){background-color:var(--dsw-alias-button-info-hover,#4576f0);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-button-info-hover,#4576f0);filter:none}.sm-migrateDialog .sm-migrateChipOn:hover:not(:disabled){filter:brightness(.92)}.sm-migrateDialog .sm-migrateChip[disabled]{opacity:.5;cursor:not-allowed}.sm-migrateDialog .sm-migrateEmpty{color:var(--dsw-alias-label-tertiary,#888)}.sm-migrateDialog .sm-migratePreview{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;background-color:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#111);font-size:13px;line-height:18px}.sm-migrateDialog .sm-migrateWarn{padding:10px 12px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 30%,transparent);border-radius:8px;background-color:var(--dsw-alias-state-warn-tertiary,#fef5e7);color:var(--dsw-alias-label-secondary,#444);font-size:13px;line-height:18px}.sm-migrateDialog .sm-nativeDialogButton{min-height:28px;padding:3px 14px;font-size:12px;line-height:20px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;cursor:pointer;background-color:transparent;color:var(--dsw-alias-label-primary,#111);text-align:center}.sm-migrateDialog .sm-nativeDialogButton:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover,#e8e8e8)}.sm-migrateDialog .sm-nativeDialogConfirm{background-color:var(--dsw-alias-state-business-primary,#356ae6);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:var(--dsw-alias-state-business-primary,#356ae6)}.sm-migrateDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}",
750
759
  // The .sm-tooltip rule below deliberately keeps !important: it targets
751
760
  // DSH's Tooltip primitive (used through renderTip), which ships its own
752
761
  // inline styles, so a plugin rule cannot win on specificity alone
@@ -2449,6 +2458,150 @@ function isTopmostDialog(z) {
2449
2458
  useEffect(() => { void annotationStore.load(refreshOnMount); }, [refreshOnMount]);
2450
2459
  return state;
2451
2460
  };
2461
+
2462
+ function HeaderStarIcon(props = {}) {
2463
+ const size = props.size || 14;
2464
+ const filled = !!props.filled;
2465
+ return h("svg", {
2466
+ viewBox: "0 0 24 24",
2467
+ width: size,
2468
+ height: size,
2469
+ fill: filled ? "var(--dsw-alias-state-warn-primary, #eac54f)" : "none",
2470
+ stroke: filled ? "var(--dsw-alias-state-warn-primary, #eac54f)" : "currentColor",
2471
+ strokeWidth: "1.5",
2472
+ strokeLinecap: "round",
2473
+ strokeLinejoin: "round",
2474
+ "aria-hidden": "true",
2475
+ style: { display: "block" }
2476
+ },
2477
+ h("polygon", { points: "12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" })
2478
+ );
2479
+ }
2480
+
2481
+ function HeaderClockIcon(props = {}) {
2482
+ const size = props.size || 14;
2483
+ return h("svg", {
2484
+ viewBox: "0 0 24 24",
2485
+ width: size,
2486
+ height: size,
2487
+ fill: "none",
2488
+ stroke: "currentColor",
2489
+ strokeWidth: "1.5",
2490
+ strokeLinecap: "round",
2491
+ strokeLinejoin: "round",
2492
+ "aria-hidden": "true",
2493
+ style: { display: "block" }
2494
+ },
2495
+ h("circle", { cx: 12, cy: 12, r: 9 }),
2496
+ h("polyline", { points: "12 7 12 12 15 14" })
2497
+ );
2498
+ }
2499
+
2500
+ function HeaderTagIcon(props = {}) {
2501
+ const size = props.size || 14;
2502
+ return h("svg", {
2503
+ viewBox: "0 0 24 24",
2504
+ width: size,
2505
+ height: size,
2506
+ fill: "none",
2507
+ stroke: "currentColor",
2508
+ strokeWidth: "1.5",
2509
+ strokeLinecap: "round",
2510
+ strokeLinejoin: "round",
2511
+ "aria-hidden": "true",
2512
+ style: { display: "block" }
2513
+ },
2514
+ h("path", { d: "M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" }),
2515
+ h("line", { x1: 7, y1: 7, x2: 7.01, y2: 7 })
2516
+ );
2517
+ }
2518
+
2519
+ function HeaderArchiveIcon(props = {}) {
2520
+ const size = props.size || 14;
2521
+ const isUnarchive = !!props.unarchive;
2522
+ return h("svg", {
2523
+ viewBox: "0 0 24 24",
2524
+ width: size,
2525
+ height: size,
2526
+ fill: "none",
2527
+ stroke: "currentColor",
2528
+ strokeWidth: "1.5",
2529
+ strokeLinecap: "round",
2530
+ strokeLinejoin: "round",
2531
+ "aria-hidden": "true",
2532
+ style: { display: "block" }
2533
+ },
2534
+ h("polyline", { points: "21 8 21 21 3 21 3 8" }),
2535
+ h("rect", { x: 1, y: 3, width: 22, height: 5, rx: 1 }),
2536
+ isUnarchive
2537
+ ? h(React.Fragment, null,
2538
+ h("polyline", { points: "10 12 12 10 14 12" }),
2539
+ h("line", { x1: 12, y1: 10, x2: 12, y2: 16 })
2540
+ )
2541
+ : h(React.Fragment, null,
2542
+ h("polyline", { points: "10 14 12 16 14 14" }),
2543
+ h("line", { x1: 12, y1: 16, x2: 12, y2: 10 })
2544
+ )
2545
+ );
2546
+ }
2547
+
2548
+ function HeaderMoveIcon(props = {}) {
2549
+ const size = props.size || 14;
2550
+ return h("svg", {
2551
+ viewBox: "0 0 24 24",
2552
+ width: size,
2553
+ height: size,
2554
+ fill: "none",
2555
+ stroke: "currentColor",
2556
+ strokeWidth: "1.5",
2557
+ strokeLinecap: "round",
2558
+ strokeLinejoin: "round",
2559
+ "aria-hidden": "true",
2560
+ style: { display: "block" }
2561
+ },
2562
+ h("polyline", { points: "17 4 21 8 17 12" }),
2563
+ h("line", { x1: 3, y1: 8, x2: 21, y2: 8 }),
2564
+ h("polyline", { points: "7 20 3 16 7 12" }),
2565
+ h("line", { x1: 21, y1: 16, x2: 3, y2: 16 })
2566
+ );
2567
+ }
2568
+
2569
+ function HeaderTrashIcon(props = {}) {
2570
+ const size = props.size || 14;
2571
+ return h("svg", {
2572
+ viewBox: "0 0 24 24",
2573
+ width: size,
2574
+ height: size,
2575
+ fill: "none",
2576
+ stroke: "currentColor",
2577
+ strokeWidth: "1.5",
2578
+ strokeLinecap: "round",
2579
+ strokeLinejoin: "round",
2580
+ "aria-hidden": "true",
2581
+ style: { display: "block" }
2582
+ },
2583
+ h("polyline", { points: "3 6 5 6 21 6" }),
2584
+ h("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })
2585
+ );
2586
+ }
2587
+
2588
+ function HeaderEllipsisIcon(props = {}) {
2589
+ const size = props.size || 14;
2590
+ return h("svg", {
2591
+ viewBox: "0 0 24 24",
2592
+ width: size,
2593
+ height: size,
2594
+ fill: "currentColor",
2595
+ stroke: "none",
2596
+ "aria-hidden": "true",
2597
+ style: { display: "block" }
2598
+ },
2599
+ h("circle", { cx: 12, cy: 12, r: 2 }),
2600
+ h("circle", { cx: 19, cy: 12, r: 2 }),
2601
+ h("circle", { cx: 5, cy: 12, r: 2 })
2602
+ );
2603
+ }
2604
+
2452
2605
  const annotationButtons = (id, title, t, state, className, onToggle, onEdit, disabled = false, editorOpen = false) => {
2453
2606
  const value = annotationOf(state, id);
2454
2607
  const busy = disabled || !state.loaded || !!state.error || state.pending.has(id);
@@ -2457,23 +2610,24 @@ function isTopmostDialog(z) {
2457
2610
  const favoriteLabel = favoriteDesc + ": " + title;
2458
2611
  const reviewLabel = reviewDesc + ": " + title;
2459
2612
  const isHeader = className === "sm-headerBtn";
2613
+ const priorityInfo = value.priority ? ` (P${value.priority})` : "";
2460
2614
  return h(React.Fragment, null,
2461
2615
  renderTip(favoriteDesc, h("button", { type: "button", className: className + " sm-markIcon" + (value.favorite ? " sm-markOn" : ""), disabled: busy, "aria-label": favoriteLabel, "aria-pressed": value.favorite, onClick: () => onToggle({ favorite: !value.favorite }) },
2462
- h("span", { className: "sm-headerBtnIcon" }, value.favorite ? "★" : "☆")
2616
+ h("span", { className: "sm-headerBtnIcon" }, HeaderStarIcon({ filled: value.favorite }))
2463
2617
  )),
2464
2618
  renderTip(reviewDesc, h("button", { type: "button", className: className + " sm-markIcon" + (value.reviewLater ? " sm-markOn" : ""), disabled: busy, "aria-label": reviewLabel, "aria-pressed": value.reviewLater, onClick: () => onToggle({ reviewLater: !value.reviewLater }) },
2465
- h("span", { className: "sm-headerBtnIcon" }, "◷")
2619
+ h("span", { className: "sm-headerBtnIcon" }, HeaderClockIcon())
2466
2620
  )),
2467
- renderTip(t("marks.edit.aria"), h("button", {
2621
+ renderTip(t("marks.edit.aria") + priorityInfo, h("button", {
2468
2622
  type: "button",
2469
- className: className + (isHeader ? " sm-headerSecondaryAction" : "") + (editorOpen ? " sm-annotationToggleOpen sm-headerBtnActive" : ""),
2623
+ className: className + (isHeader ? " sm-headerSecondaryAction" : " sm-rowBtnIcon") + (editorOpen ? " sm-annotationToggleOpen sm-headerBtnActive sm-rowBtnActive" : ""),
2470
2624
  disabled: busy,
2471
2625
  "aria-label": t("marks.edit.aria") + ": " + title,
2472
2626
  ...(isHeader ? { "aria-expanded": editorOpen } : {}),
2473
2627
  onClick: e => onEdit(e)
2474
2628
  },
2475
- isHeader ? h("span", { className: "sm-headerBtnIcon" }, "🏷") : null,
2476
- isHeader ? h("span", { className: "sm-headerBtnLabel" }, t("marks.header.edit")) : t("marks.edit"),
2629
+ h("span", { className: "sm-headerBtnIcon" }, HeaderTagIcon()),
2630
+ h("span", { className: isHeader ? "sm-headerBtnLabel" : "sm-rowBtnLabel" }, isHeader ? t("marks.header.edit") : t("marks.edit")),
2477
2631
  isHeader ? h("span", { className: "sm-priorityBadge sm-priority-" + (value.priority ?? 3) }, "P" + (value.priority ?? 3)) : null
2478
2632
  ))
2479
2633
  );
@@ -3773,7 +3927,7 @@ function isTopmostDialog(z) {
3773
3927
  ));
3774
3928
  }
3775
3929
 
3776
- const CURRENT_VERSION = "0.7.4";
3930
+ const CURRENT_VERSION = "0.7.5";
3777
3931
 
3778
3932
  function compareSemver(v1, v2) {
3779
3933
  if (typeof v1 !== "string" || typeof v2 !== "string") return 0;
@@ -5006,11 +5160,49 @@ function isTopmostDialog(z) {
5006
5160
  h("div", { className: "sm-rowMeta" }, shortPath(s.cwd) + (s.cwd ? " · " : "") + formatRelative(s.updatedAt, now)),
5007
5161
  h("div", { className: "sm-rowActions" },
5008
5162
  h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => { if (isArchived) { if (isWsArchived) { if (typeof window !== "undefined" && typeof window.alert === "function") { window.alert(t("error.workspaceArchived")); } return; } /* DSH refuses to surface an archived session as the current view: clicking "Open" on an archived row flashes an empty chat panel and bounces back to "no session selected". Unarchive first, then open. (issue #17.4) */ runRow(s.id, async () => { await onUnarchive(s.id); await onOpen(s.id); }); } else { onOpen(s.id); } } }, t("row.open")),
5009
- h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => runRow(s.id, () => { if (isArchived) { if (isWsArchived) { if (typeof window !== "undefined" && typeof window.alert === "function") { window.alert(t("error.workspaceArchived")); } return; } return onUnarchive(s.id); } return onArchive(s.id); }) }, t(isArchived ? "row.unarchive" : "row.archive")),
5163
+ renderTip(t(isArchived ? "row.unarchive" : "row.archive"), h("button", {
5164
+ type: "button",
5165
+ className: "sm-rowBtn sm-rowBtnIcon" + (isArchived ? " sm-rowBtnActive" : ""),
5166
+ disabled: busy,
5167
+ "aria-label": t(isArchived ? "row.unarchive" : "row.archive") + ": " + (s.displayTitle || s.id),
5168
+ onClick: () => runRow(s.id, () => {
5169
+ if (isArchived) {
5170
+ if (isWsArchived) {
5171
+ if (typeof window !== "undefined" && typeof window.alert === "function") {
5172
+ window.alert(t("error.workspaceArchived"));
5173
+ }
5174
+ return;
5175
+ }
5176
+ return onUnarchive(s.id);
5177
+ }
5178
+ return onArchive(s.id);
5179
+ })
5180
+ },
5181
+ h("span", { className: "sm-headerBtnIcon" }, HeaderArchiveIcon({ unarchive: isArchived })),
5182
+ h("span", { className: "sm-rowBtnLabel" }, t(isArchived ? "row.unarchive" : "row.archive"))
5183
+ )),
5010
5184
  annotationButtons(s.id, s.displayTitle || s.id, t, annotations, "sm-rowBtn", patch => runRow(s.id, () => annotationStore.save(s.id, patch)), () => { setAnnotationAnchor(null); setAnnotationFor(current => current?.id === s.id ? null : s); }, busy),
5011
- h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => { if (moveFor && moveFor.id === s.id) setMoveFor(null); else setMoveFor(s); } }, t("row.move")),
5185
+ renderTip(t("row.move"), h("button", {
5186
+ type: "button",
5187
+ className: "sm-rowBtn sm-rowBtnIcon" + (moveFor && moveFor.id === s.id ? " sm-rowBtnActive" : ""),
5188
+ disabled: busy,
5189
+ "aria-label": t("row.move") + ": " + (s.displayTitle || s.id),
5190
+ onClick: () => { if (moveFor && moveFor.id === s.id) setMoveFor(null); else setMoveFor(s); }
5191
+ },
5192
+ h("span", { className: "sm-headerBtnIcon" }, HeaderMoveIcon()),
5193
+ h("span", { className: "sm-rowBtnLabel" }, t("row.move"))
5194
+ )),
5012
5195
  h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => { if (migrateFor && migrateFor.id === s.id) setMigrateFor(null); else setMigrateFor(s); } }, t("row.migrate")),
5013
- h("button", { type: "button", className: "sm-rowBtn sm-rowBtnDanger", disabled: busy, onClick: () => { if (confirmFor && confirmFor.id === s.id) setConfirmFor(null); else setConfirmFor(s); } }, t("row.delete"))
5196
+ renderTip(t("row.delete"), h("button", {
5197
+ type: "button",
5198
+ className: "sm-rowBtn sm-rowBtnDanger sm-rowBtnIcon" + (confirmFor && confirmFor.id === s.id ? " sm-rowBtnActive" : ""),
5199
+ disabled: busy,
5200
+ "aria-label": t("row.delete") + ": " + (s.displayTitle || s.id),
5201
+ onClick: () => { if (confirmFor && confirmFor.id === s.id) setConfirmFor(null); else setConfirmFor(s); }
5202
+ },
5203
+ h("span", { className: "sm-headerBtnIcon" }, HeaderTrashIcon()),
5204
+ h("span", { className: "sm-rowBtnLabel" }, t("row.delete"))
5205
+ ))
5014
5206
  )
5015
5207
  )
5016
5208
  )
@@ -6096,7 +6288,7 @@ function isTopmostDialog(z) {
6096
6288
  isArchived ? onUnarchive(sessionId) : onArchive(sessionId)
6097
6289
  )
6098
6290
  },
6099
- h("span", { className: "sm-headerBtnIcon" }, isArchived ? "📤" : "📥"),
6291
+ h("span", { className: "sm-headerBtnIcon" }, HeaderArchiveIcon({ unarchive: isArchived })),
6100
6292
  h("span", { className: "sm-headerBtnLabel" }, isArchived ? (t ? t("row.unarchive") : "移出归档") : (t ? t("row.archive") : "归档"))
6101
6293
  )) : null,
6102
6294
  annotationButtons(sessionId, props.displayTitle || sessionId, t, annotations, "sm-headerBtn", patch => runWithAlert("annotations", () => annotationStore.save(sessionId, patch)), (e) => {
@@ -6132,7 +6324,7 @@ function isTopmostDialog(z) {
6132
6324
  }
6133
6325
  }
6134
6326
  },
6135
- h("span", { className: "sm-headerBtnIcon" }, "↔"),
6327
+ h("span", { className: "sm-headerBtnIcon" }, HeaderMoveIcon()),
6136
6328
  h("span", { className: "sm-headerBtnLabel" }, t ? t("row.move") : "移动至工作区")
6137
6329
  )),
6138
6330
  renderTip(t ? t("row.delete") : "删除会话", h("button", {
@@ -6154,7 +6346,7 @@ function isTopmostDialog(z) {
6154
6346
  }
6155
6347
  }
6156
6348
  },
6157
- h("span", { className: "sm-headerBtnIcon" }, "🗑"),
6349
+ h("span", { className: "sm-headerBtnIcon" }, HeaderTrashIcon()),
6158
6350
  h("span", { className: "sm-headerBtnLabel" }, t ? t("row.delete") : "删除会话")
6159
6351
  ))
6160
6352
  ),
@@ -6171,7 +6363,7 @@ function isTopmostDialog(z) {
6171
6363
  setMoreOpen(v => !v);
6172
6364
  }
6173
6365
  },
6174
- h("span", { className: "sm-headerBtnIcon" }, "⋯")
6366
+ h("span", { className: "sm-headerBtnIcon" }, HeaderEllipsisIcon())
6175
6367
  )),
6176
6368
  moreOpen ? h("div", { className: "sm-headerMenu", role: "menu", style: menuStyle },
6177
6369
  (onArchive || onUnarchive) ? h("button", {
@@ -6183,7 +6375,7 @@ function isTopmostDialog(z) {
6183
6375
  runWithAlert("archive", () => isArchived ? onUnarchive(sessionId) : onArchive(sessionId));
6184
6376
  }
6185
6377
  },
6186
- h("span", { className: "sm-headerBtnIcon" }, isArchived ? "📤" : "📥"),
6378
+ h("span", { className: "sm-headerBtnIcon" }, HeaderArchiveIcon({ unarchive: isArchived })),
6187
6379
  h("span", null, isArchived ? (t ? t("row.unarchive") : "移出归档") : (t ? t("row.archive") : "归档"))
6188
6380
  ) : null,
6189
6381
  h("button", {
@@ -6196,7 +6388,7 @@ function isTopmostDialog(z) {
6196
6388
  runWithAlert("annotations", () => annotationStore.save(sessionId, { favorite: !currentAnnotation.favorite }));
6197
6389
  }
6198
6390
  },
6199
- h("span", { className: "sm-headerBtnIcon" }, currentAnnotation.favorite ? "★" : "☆"),
6391
+ h("span", { className: "sm-headerBtnIcon" }, HeaderStarIcon({ filled: currentAnnotation.favorite })),
6200
6392
  h("span", null, t ? t(currentAnnotation.favorite ? "marks.favorite.remove" : "marks.favorite.add") : (currentAnnotation.favorite ? "取消收藏" : "收藏会话"))
6201
6393
  ),
6202
6394
  h("button", {
@@ -6209,7 +6401,7 @@ function isTopmostDialog(z) {
6209
6401
  runWithAlert("annotations", () => annotationStore.save(sessionId, { reviewLater: !currentAnnotation.reviewLater }));
6210
6402
  }
6211
6403
  },
6212
- h("span", { className: "sm-headerBtnIcon" }, "◷"),
6404
+ h("span", { className: "sm-headerBtnIcon" }, HeaderClockIcon()),
6213
6405
  h("span", null, t ? t(currentAnnotation.reviewLater ? "marks.review.remove" : "marks.review.add") : (currentAnnotation.reviewLater ? "取消稍后回看" : "稍后回看"))
6214
6406
  ),
6215
6407
  h("button", {
@@ -6226,7 +6418,7 @@ function isTopmostDialog(z) {
6226
6418
  setAnnotationFor(curr => (curr?.id === sessionId ? null : { id: sessionId, displayTitle }));
6227
6419
  }
6228
6420
  },
6229
- h("span", { className: "sm-headerBtnIcon" }, "🏷"),
6421
+ h("span", { className: "sm-headerBtnIcon" }, HeaderTagIcon()),
6230
6422
  h("span", null, t ? t("marks.header.edit") : "标签/备注")
6231
6423
  ),
6232
6424
  h("button", {
@@ -6245,7 +6437,7 @@ function isTopmostDialog(z) {
6245
6437
  }
6246
6438
  }
6247
6439
  },
6248
- h("span", { className: "sm-headerBtnIcon" }, "↔"),
6440
+ h("span", { className: "sm-headerBtnIcon" }, HeaderMoveIcon()),
6249
6441
  h("span", null, t ? t("row.move") : "移动至工作区")
6250
6442
  ),
6251
6443
  h("button", {
@@ -6264,7 +6456,7 @@ function isTopmostDialog(z) {
6264
6456
  }
6265
6457
  }
6266
6458
  },
6267
- h("span", { className: "sm-headerBtnIcon" }, "🗑"),
6459
+ h("span", { className: "sm-headerBtnIcon" }, HeaderTrashIcon()),
6268
6460
  h("span", null, t ? t("row.delete") : "删除会话")
6269
6461
  )
6270
6462
  ) : null
@@ -6976,6 +7168,12 @@ function isTopmostDialog(z) {
6976
7168
  const wasCurrent = ctx.sessions.list.getSnapshot().current === sessionId;
6977
7169
  const response = await callApi("/move", { sessionId, targetWorkspaceId });
6978
7170
  if (!response.ok) throw new Error(response.error || "move failed");
7171
+ if (response.result?.restartRequired) {
7172
+ try {
7173
+ const msg = (typeof t === "function" ? t("move.restartRequired") : null) || "会话已成功移动至目标工作区,但当前工作目录需要在重启 DSH 后生效。";
7174
+ window.alert(msg);
7175
+ } catch (_) {}
7176
+ }
6979
7177
  const refreshMovedSession = async () => {
6980
7178
  await Promise.allSettled([
6981
7179
  typeof ctx.workspaces?.refresh === "function" ? ctx.workspaces.refresh() : Promise.resolve(),
@@ -7050,7 +7248,7 @@ function isTopmostDialog(z) {
7050
7248
  ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
7051
7249
  name: "conversation.session.header.actions",
7052
7250
  id: "session-manager-header",
7053
- order: 40,
7251
+ order: 0,
7054
7252
  locale: NS,
7055
7253
  inject: () => actions()
7056
7254
  }, HeaderAction));
package/lib/index.js CHANGED
@@ -28,7 +28,7 @@ import { basename, dirname, join } from "node:path";
28
28
  import { randomBytes } from "node:crypto";
29
29
  import { createAnnotationStore } from "./annotation-store.js";
30
30
  import { createWorkspaceArchiveStore, assertWorkspaceId } from "./workspace-archive-store.js";
31
- import { createDshAdapter, normalizeSessionHeader, compareSemver, detectPluginSource, isHmrActive, resolveProfileDir } from "./compat/dsh-adapter.js";
31
+ import { createDshAdapter, getService, normalizeSessionHeader, compareSemver, detectPluginSource, isHmrActive, resolveProfileDir } from "./compat/dsh-adapter.js";
32
32
  import { ARTIFACT_NAMES, assertSessionId, assertSessionDirectory, candidateSessionDirs, encodeFsSessionSegment, decodeFsSessionSegment, encodeProjectSegment, encodeSessionSegment, parseSessionGenerationFromFilename, readSessionHeader, readSessionFile, writeTempFile, replaceSessionFile } from "./session-files.js";
33
33
 
34
34
  export const name = "dsh-session-manager";
@@ -39,7 +39,8 @@ export const inject = [
39
39
  "sessions",
40
40
  "agents",
41
41
  "sessionPersistence",
42
- "agentPresets"
42
+ "agentPresets",
43
+ "connection"
43
44
  ];
44
45
 
45
46
  /** Like JSON.stringify but swallows circular-reference/BigInt errors so the
@@ -385,7 +386,7 @@ function verifyInstalledPackage(profileDir, expectedVersion) {
385
386
 
386
387
  export function apply(ctx) {
387
388
  try {
388
- const buildId = "dsh-session-manager v0.7.4 (workspace-archive)";
389
+ const buildId = "dsh-session-manager v0.7.5 (workspace-archive)";
389
390
  if (typeof ctx?.logger?.info === "function") ctx.logger.info(buildId);
390
391
  else if (typeof console !== "undefined" && console?.info) console.info(buildId);
391
392
  } catch (_) { /* ignore logging failures */ }
@@ -1044,6 +1045,115 @@ ${rest}`, "utf8");
1044
1045
  return await run();
1045
1046
  };
1046
1047
 
1048
+ /**
1049
+ * Issue #42: record the working-directory change a move implies.
1050
+ *
1051
+ * A cross-workspace move rewrites the artifact header's `cwd`, but DSH
1052
+ * folds the `workingDirectory` projection from the session log: the unit
1053
+ * initializes from `header.cwd` only while it has no checkpoint, and both
1054
+ * the projection cache and every `working-directory/change` event
1055
+ * supersede that initial value. A live session therefore keeps reporting
1056
+ * its pre-move directory -- the projection state is never re-initialized --
1057
+ * and the cache checkpoints that stale state under the NEW header identity,
1058
+ * so a rebuild cannot recover the target either. Route the change through
1059
+ * the host's own path (the `workingDirectory` service is what a user `cd`
1060
+ * uses) so the log gains the event the projection folds from; without that
1061
+ * service, append the equivalent event directly, which is the same shape
1062
+ * the host's `commit()` writes.
1063
+ *
1064
+ * @param agent - the live Agent owning the moved session, when one exists.
1065
+ * @param session - the live Session owning the moved log.
1066
+ * @param targetPath - the destination workspace directory.
1067
+ * @returns `{status}`: "synced" (host service committed the change),
1068
+ * "recorded" (event appended directly) or "restart-required" (the host
1069
+ * rejected the change; the move itself has already succeeded).
1070
+ */
1071
+ const rehomeLiveWorkingDirectory = async ({ agent, session, targetPath }) => {
1072
+ const service = getService(ctx, "workingDirectory");
1073
+ if (service !== undefined && typeof service.set === "function" && agent !== undefined) {
1074
+ try {
1075
+ const cwd = await service.set(agent, targetPath);
1076
+ return { status: "synced", cwd: typeof cwd === "string" && cwd !== "" ? cwd : targetPath };
1077
+ } catch (error) {
1078
+ const isFiberError = error?.code === "INACTIVE_EFFECT" ||
1079
+ String(error?.message || error).toLowerCase().includes("inactive") ||
1080
+ String(error?.message || error).toLowerCase().includes("fiber");
1081
+ if (!isFiberError) {
1082
+ ctx.logger.warn(`session-manager: moved "${safeJson(session?.id)}" but the host rejected the working-directory change: ${safeErrorMessage(error)}`);
1083
+ return { status: "restart-required", reason: safeErrorMessage(error) };
1084
+ }
1085
+ }
1086
+ }
1087
+ if (session === undefined || session === null || typeof session.append !== "function") {
1088
+ return { status: "restart-required", reason: "no live session to record the working-directory change" };
1089
+ }
1090
+ try {
1091
+ session.append("working-directory/change", { cwd: targetPath });
1092
+ return { status: "recorded", cwd: targetPath };
1093
+ } catch (error) {
1094
+ ctx.logger.warn(`session-manager: moved "${safeJson(session.id)}" but could not record the working-directory change: ${safeErrorMessage(error)}`);
1095
+ return { status: "restart-required", reason: safeErrorMessage(error) };
1096
+ }
1097
+ };
1098
+
1099
+ /**
1100
+ * Issue #42 (cold sessions): append the `working-directory/change` event a
1101
+ * live move records, but only when the stored log already carries one --
1102
+ * otherwise the rewritten `header.cwd` alone initializes the projection on
1103
+ * the next resume and touching the log would be gratuitous. A stale change
1104
+ * event would otherwise fold the pre-move directory back in. Rows whose
1105
+ * `seq` does not match their position (packed physical rows) cannot be
1106
+ * extended without the persistence codec, so the body is left untouched and
1107
+ * the caller reports that a restart is required.
1108
+ *
1109
+ * @param rest - the artifact body (everything after the header line).
1110
+ * @param targetPath - the destination workspace directory.
1111
+ * @returns `{rest, status}` with the body to publish.
1112
+ */
1113
+ const rehomeColdWorkingDirectory = (rest, targetPath) => {
1114
+ const nl = String.fromCharCode(10);
1115
+ const terminated = rest.endsWith(nl);
1116
+ const body = terminated ? rest.slice(0, -1) : rest;
1117
+ if (body === "") return { rest, status: "header-only" };
1118
+ const lines = body.split(nl);
1119
+ let lastChange;
1120
+ let hasPackedOrUnparseableRows = false;
1121
+ let hasNonSequentialSeq = false;
1122
+ for (let index = 0; index < lines.length; index += 1) {
1123
+ let event;
1124
+ try {
1125
+ event = JSON.parse(lines[index]);
1126
+ } catch {
1127
+ hasPackedOrUnparseableRows = true;
1128
+ continue;
1129
+ }
1130
+ if (event === null || typeof event !== "object") {
1131
+ hasPackedOrUnparseableRows = true;
1132
+ continue;
1133
+ }
1134
+ if (event.seq !== index) {
1135
+ hasNonSequentialSeq = true;
1136
+ }
1137
+ if (event.type === "working-directory/change") {
1138
+ lastChange = event.data?.cwd;
1139
+ }
1140
+ }
1141
+ if (lastChange === undefined) return { rest, status: "header-only" };
1142
+ if (lastChange === targetPath) return { rest, status: "already-target" };
1143
+ if (hasPackedOrUnparseableRows || hasNonSequentialSeq) {
1144
+ const error = new Error("无法安全同步会话工作目录(会话日志包含压缩行且已有工作目录变更历史),为防止工作目录不一致,已取消移动。原始会话保持原状");
1145
+ error.code = "working-directory-sync-unsafe";
1146
+ throw error;
1147
+ }
1148
+ const appended = JSON.stringify({
1149
+ type: "working-directory/change",
1150
+ seq: lines.length,
1151
+ time: Date.now(),
1152
+ data: { cwd: targetPath }
1153
+ });
1154
+ return { rest: `${body}${nl}${appended}${nl}`, status: "recorded" };
1155
+ };
1156
+
1047
1157
  /**
1048
1158
  * TRUE cross-workspace move. See the module docstring for the full design:
1049
1159
  * re-home the stored cwd, migrate the artifact, then swap accounting.
@@ -1196,7 +1306,14 @@ ${rest}`, "utf8");
1196
1306
  throw new Error("目标位置已有会话工件,拒绝覆盖");
1197
1307
  } catch (error) { if (error.code !== "ENOENT") throw error; }
1198
1308
  if (oldPath === newPath) throw new Error("会话源路径与目标路径相同,但 cwd 不一致,拒绝覆盖");
1199
- const bytes = await encodeArtifact(JSON.stringify(newHeader), rest, newPath.endsWith(".zstd"));
1309
+ // Issue #42: a cold log is the only authority its working-directory
1310
+ // projection folds from on the next resume, so re-home it together
1311
+ // with the artifact header. Live sessions get the equivalent event
1312
+ // through the host path once the artifact has been published.
1313
+ const bodyRehome = liveSession === void 0
1314
+ ? rehomeColdWorkingDirectory(rest, targetPath)
1315
+ : { rest, status: "live" };
1316
+ const bytes = await encodeArtifact(JSON.stringify(newHeader), bodyRehome.rest, newPath.endsWith(".zstd"));
1200
1317
  await mkdir(dirname(newPath), { recursive: true });
1201
1318
  const tempNew = await writeTempFile(newPath, bytes);
1202
1319
  const oldHidden = `${oldPath}.${randomBytes(6).toString("hex")}.tmp`;
@@ -1297,7 +1414,7 @@ ${rest}`, "utf8");
1297
1414
  await rm(dirname(oldPath), { recursive: true, force: true });
1298
1415
  } catch (error) { ctx.logger.warn(`session-manager: old directory retained: ${String(error)}`); }
1299
1416
 
1300
- movedArtifact = { header: newHeader, rest };
1417
+ movedArtifact = { header: newHeader, rest: bodyRehome.rest, workingDirectory: bodyRehome.status };
1301
1418
 
1302
1419
  return {
1303
1420
  ok: true,
@@ -1329,7 +1446,34 @@ ${rest}`, "utf8");
1329
1446
  ctx.logger.warn(`session-manager: post-move cache refresh failed for "${sessionId}": ${String(error)}`);
1330
1447
  }
1331
1448
  }
1332
- // The agent is still live (no teardown). The session's in-memory
1449
+ // Issue #42: the artifact header was rewritten, but the folded
1450
+ // `workingDirectory` projection was not. Align it now -- after the
1451
+ // artifact has been published (a live session's change event must land in
1452
+ // the moved log) and after the registry swap.
1453
+ let workingDirectory = movedArtifact?.workingDirectory ?? "not-needed";
1454
+ let restartRequired = false;
1455
+ if (liveSession !== void 0) {
1456
+ const rehomed = await rehomeLiveWorkingDirectory({ agent: liveAgent, session: liveSession, targetPath });
1457
+ workingDirectory = rehomed.status;
1458
+ if (rehomed.status === "restart-required") restartRequired = true;
1459
+ if (rehomed.status === "synced" || rehomed.status === "recorded") {
1460
+ // The refold above seeds from the projection cache, so it cannot see
1461
+ // a change committed after the move: checkpoint what the live
1462
+ // session now actually folds (this flushes the new event first).
1463
+ if (cache !== void 0 && typeof cache.write === "function") {
1464
+ try {
1465
+ await cache.write(liveSession);
1466
+ } catch (error) {
1467
+ ctx.logger.warn(`session-manager: post-move working-directory checkpoint failed for "${sessionId}": ${safeErrorMessage(error)}`);
1468
+ }
1469
+ }
1470
+ }
1471
+ } else if (workingDirectory === "restart-required") {
1472
+ restartRequired = true;
1473
+ }
1474
+ result.workingDirectory = workingDirectory;
1475
+ result.restartRequired = restartRequired;
1476
+ // The agent is still live (no teardown). The session's in-memory
1333
1477
  // header was rewritten above and the workspace registry updated;
1334
1478
  // future writes go through persistence.locate() against the new
1335
1479
  // cwd, so the file at the new location accumulates new events.
@@ -2295,9 +2439,80 @@ ${rest}`, "utf8");
2295
2439
  if (isBlank) { const e = new Error("blank"); e.code = "blank-skip"; throw e; }
2296
2440
  };
2297
2441
 
2298
- ctx.effect(() => ctx.webServer.register({ kind: "prefix",
2442
+ /**
2443
+ * Issue #43: Validate incoming request with DSH connection authentication.
2444
+ * Reuses DSH official Connection/Gateway authentication and authorization mechanisms.
2445
+ * Strict fail-closed: requires connection.admit() to succeed with a valid Owner peer.
2446
+ * Must execute before request body reading, JSON parsing, or business operations.
2447
+ */
2448
+ const authenticateApiRequest = (req) => {
2449
+ const connection = getService(ctx, "connection");
2450
+ if (!connection || typeof connection.admit !== "function") {
2451
+ return {
2452
+ ok: false,
2453
+ status: 503,
2454
+ code: "auth-unavailable",
2455
+ error: "DSH 认证服务未就绪或不可用,为保护数据安全已拒绝请求"
2456
+ };
2457
+ }
2458
+ try {
2459
+ const admission = connection.admit(req);
2460
+ if (!admission || typeof admission !== "object") {
2461
+ return {
2462
+ ok: false,
2463
+ status: 401,
2464
+ code: "unauthorized",
2465
+ error: "未授权:需要有效的 DSH 会话凭证"
2466
+ };
2467
+ }
2468
+ if ("rejection" in admission) {
2469
+ const status = typeof admission.rejection === "number" ? admission.rejection : 401;
2470
+ const error = status === 403
2471
+ ? "禁止访问:请求未通过 DSH 信任检查或跨站防护"
2472
+ : "未授权:需要有效的 DSH 会话凭证";
2473
+ return { ok: false, status, code: status === 403 ? "forbidden" : "unauthorized", error };
2474
+ }
2475
+ const peer = admission.peer;
2476
+ if (!peer || typeof peer !== "object") {
2477
+ return {
2478
+ ok: false,
2479
+ status: 401,
2480
+ code: "unauthorized",
2481
+ error: "未授权:缺少有效 Peer 凭证"
2482
+ };
2483
+ }
2484
+ const principalKind = peer.principal?.kind;
2485
+ if (principalKind !== "owner") {
2486
+ return {
2487
+ ok: false,
2488
+ status: 403,
2489
+ code: "forbidden",
2490
+ error: "禁止访问:会话管理属于特权操作,需要 owner 权限"
2491
+ };
2492
+ }
2493
+ return { ok: true, peer };
2494
+ } catch (err) {
2495
+ return {
2496
+ ok: false,
2497
+ status: 403,
2498
+ code: "auth-error",
2499
+ error: "认证检查失败: " + safeErrorMessage(err)
2500
+ };
2501
+ }
2502
+ };
2503
+
2504
+ ctx.effect(() => ctx.webServer.register({
2505
+ kind: "prefix",
2299
2506
  path: API_PREFIX,
2300
2507
  handler: async (req, res) => {
2508
+ const auth = authenticateApiRequest(req);
2509
+ if (!auth.ok) {
2510
+ return send(res, auth.status, {
2511
+ ok: false,
2512
+ code: auth.code,
2513
+ error: auth.error
2514
+ });
2515
+ }
2301
2516
  try {
2302
2517
  const url = new URL(req.url ?? "/", "http://localhost");
2303
2518
  const path = url.pathname.startsWith(API_PREFIX) ? url.pathname.slice(API_PREFIX.length) || "/" : "/";
@@ -2528,7 +2743,7 @@ ${rest}`, "utf8");
2528
2743
  return send(res, 200, { ok: true, result });
2529
2744
  }
2530
2745
  if (req.method === "GET" && path === "/update/check") {
2531
- const currentVersion = adapter.pluginVersion() || "0.7.4";
2746
+ const currentVersion = adapter.pluginVersion() || "0.7.5";
2532
2747
  const runningVersion = currentVersion;
2533
2748
  let installedVersion = runningVersion;
2534
2749
  const profileDir = resolveProfileDir(adapter, ctx);
@@ -2712,7 +2927,7 @@ ${rest}`, "utf8");
2712
2927
  };
2713
2928
  setActiveTask(taskRecord);
2714
2929
 
2715
- const currentVersion = adapter.pluginVersion() || "0.7.4";
2930
+ const currentVersion = adapter.pluginVersion() || "0.7.5";
2716
2931
  let installedVersion = currentVersion;
2717
2932
  if (profileDir) {
2718
2933
  const pkgFile = join(profileDir, "node_modules", "dsh-session-manager", "package.json");
@@ -3242,7 +3457,7 @@ ${rest}`, "utf8");
3242
3457
  }
3243
3458
  if (req.method === "GET" && path === "/update/status") {
3244
3459
  const profileDir = resolveProfileDir(adapter, ctx);
3245
- const currentVersion = adapter.pluginVersion() || "0.7.4";
3460
+ const currentVersion = adapter.pluginVersion() || "0.7.5";
3246
3461
  let installedVersion = currentVersion;
3247
3462
  if (profileDir) {
3248
3463
  const diskCheck = verifyInstalledPackage(profileDir);
@@ -3317,6 +3532,7 @@ ${rest}`, "utf8");
3317
3532
  ctx.logger.warn("session-manager: api error: " + safeErrorMessage(error) + "; json=" + safeJson(error));
3318
3533
  const status = error?.code === "bad-request" ? 400 : error?.code === "body-too-large" ? 413
3319
3534
  : error?.code === "annotation-conflict" || error?.code === "annotations-busy" ? 409
3535
+ : error?.code === "working-directory-sync-unsafe" ? 409
3320
3536
  : error?.code === "session-not-found" ? 404 : error?.code?.startsWith("annotations-") ? 503
3321
3537
  : error?.code === "workspace-membership-conflict" ? 409
3322
3538
  : error?.code === "workspace-attach-unavailable" ? 501 : 500;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-manager",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "description": "DeepSeek Harness session and workspace manager —— Workspaces: archive and visibility. Sessions: move, migrate presets, delete, favorite, review later, prioritize, tag, and annotate. Batch operations. | DSH 会话与工作区管理——工作区:归档与显示。会话:跨工作区移动、预设迁移、删除;收藏、待回看、优先级、标签、备注。批量操作。",
5
5
  "license": "MIT",
6
6
  "keywords": [