dsh-session-manager 0.4.11 → 0.5.1

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/lib/client.js CHANGED
@@ -4,7 +4,8 @@
4
4
  * Renders the title-bar action cluster (Archive / Unarchive, Move to
5
5
  * workspace, Delete session) and the sidebar-foot Session manager panel
6
6
  * (Open / Archive / Unarchive / Move / Migrate preset / Delete per row,
7
- * plus the panel's own filter pills and toggle-close behaviour). No build
7
+ * plus search, workspace/archive/annotation filters, sorting and toggle-close behaviour).
8
+ * Favorites, review-later flags, tags, notes and priority share a separate metadata store. No build
8
9
  * step: DSH loads the file as a Cordis client bundle via its inject
9
10
  * declaration in package.json.
10
11
  */
@@ -43,6 +44,68 @@ window.__ModuleLoader__.load({
43
44
  const API = "/session-manager/api";
44
45
 
45
46
  const zh = {
47
+ "marks.filter.favorite": "收藏",
48
+ "marks.filter.favorite.aria": "收藏筛选",
49
+ "marks.filter.review": "待回看",
50
+ "marks.filter.review.aria": "待回看筛选",
51
+ "marks.filter.tags": "按标签筛选",
52
+ "marks.filter.priority": "按优先级筛选",
53
+ "marks.favorite.add": "收藏会话",
54
+ "marks.favorite.remove": "取消收藏",
55
+ "marks.review.add": "标记待回看",
56
+ "marks.review.remove": "取消待回看",
57
+ "marks.edit": "标签/备注",
58
+ "marks.header.edit": "标签/备注",
59
+ "marks.edit.aria": "编辑标签、备注和优先级",
60
+ "marks.title": "会话标记",
61
+ "marks.favorite": "收藏",
62
+ "marks.review": "待回看",
63
+ "marks.tags": "标签",
64
+ "marks.tags.placeholder": "用逗号分隔,最多 20 个标签,每个 32 字符(重复自动合并)。例如:排障,方案",
65
+ "marks.tags.help": "最多 20 个标签,每个 32 字符;重复标签会自动合并",
66
+ "marks.note": "备注",
67
+ "marks.note.placeholder": "例如:等待 Windows 环境验证;下次继续检查回滚逻辑(最多 2000 字符)",
68
+ "marks.privacy": "仅用于会话整理,不写入聊天记录,也不会自动发送给模型。",
69
+ "marks.priority": "优先级",
70
+ "marks.priority.all": "全部优先级",
71
+ "marks.priority.1": "1 · 最高",
72
+ "marks.priority.2": "2 · 高",
73
+ "marks.priority.3": "3 · 普通",
74
+ "marks.priority.4": "4 · 低",
75
+ "marks.priority.5": "5 · 最低",
76
+ "marks.priority.help": "1 最高,5 最低,默认 3(普通)",
77
+ "marks.tags.all": "全部标签",
78
+ "marks.tags.none": "无标签",
79
+ "marks.tags.missing": "{tag}(无匹配)",
80
+ "marks.save": "保存标记",
81
+ "marks.saving": "保存中…",
82
+ "marks.cancel": "取消",
83
+ "marks.loading": "正在加载会话标记…",
84
+ "marks.error": "会话标记加载失败:{message}",
85
+ "marks.retry": "重试标记加载",
86
+ "marks.conflict": "标记已在其他窗口更新。当前输入仍保留;请载入最新内容后再编辑。",
87
+ "marks.reload": "载入最新内容",
88
+ "marks.discard": "放弃当前未保存的标记修改?",
89
+ "marks.note.exists": "有备注",
90
+ "marks.sort": "优先级(1 → 5)",
91
+ "marks.note.exists": "有备注",
92
+ "marks.import.copy": "复制 Prompt",
93
+ "marks.import.copy.copied": "已复制,请粘贴到当前对话生成结果",
94
+ "marks.import.copy.failed": "复制失败:{message}",
95
+ "marks.import.paste": "导入",
96
+ "marks.import.paste.label": "AI 返回结果",
97
+ "marks.import.paste.help": "把模型返回的 JSON 粘贴到下方输入框,再点击“导入”",
98
+ "marks.import.paste.aria": "AI 返回的 JSON",
99
+ "marks.import.paste.placeholder": "支持 json 代码块或纯 JSON,例如: { \"tags\": [\"标签1\", \"标签2\"], \"note\": \"会话备注\" }",
100
+ "marks.import.paste.clear": "清空输入框",
101
+ "marks.import.empty": "请先把 AI 返回的 JSON 粘贴到上方输入框",
102
+ "marks.import.unknown": "未知字段",
103
+ "marks.import.discard": "导入将覆盖当前未保存的修改,是否继续?",
104
+ "marks.import.failure": "导入失败:{message}",
105
+ "marks.import.warnings": "已导入,但有以下调整:",
106
+ "marks.import.success": "已从剪贴板导入 AI 结果",
107
+ "marks.tags.clear": "清空标签",
108
+ "marks.note.clear": "清空备注",
46
109
  "header.aria": "会话操作",
47
110
  "menu.open": "打开",
48
111
  "menu.archive": "归档会话",
@@ -53,6 +116,22 @@ window.__ModuleLoader__.load({
53
116
  "footer.aria": "会话管理",
54
117
  "footer.label": "会话管理",
55
118
  "panel.title": "会话管理",
119
+ "panel.search": "搜索标题、ID、标签或备注",
120
+ "panel.workspace": "工作区",
121
+ "panel.workspace.all": "全部工作区",
122
+ "panel.workspace.ungrouped": "未分组",
123
+ "panel.workspace.loading": "正在加载工作区…",
124
+ "panel.workspace.error": "工作区加载失败:{message}",
125
+ "panel.workspace.retry": "重试",
126
+ "panel.sort": "排序",
127
+ "panel.sort.unavailable": "当前 DSH 未提供创建时间,暂按原顺序显示",
128
+ "panel.sort.updated-desc": "最近更新",
129
+ "panel.sort.updated-asc": "最早更新",
130
+ "panel.sort.created-desc": "最新创建",
131
+ "panel.sort.created-asc": "最早创建",
132
+ "panel.count": "显示 {shown} / {total} 个会话",
133
+ "panel.reset": "重置筛选",
134
+ "panel.empty.filtered": "没有匹配的会话,请调整搜索或筛选条件",
56
135
  "panel.filter.all": "全部",
57
136
  "panel.filter.active": "未归档",
58
137
  "panel.filter.archived": "已归档",
@@ -94,6 +173,68 @@ window.__ModuleLoader__.load({
94
173
  "workspace.sessions": "{count} 个会话"
95
174
  };
96
175
  const en = {
176
+ "marks.filter.favorite": "Favorites",
177
+ "marks.filter.favorite.aria": "Favorites filter",
178
+ "marks.filter.review": "Review later",
179
+ "marks.filter.review.aria": "Review later filter",
180
+ "marks.filter.tags": "Filter by tag",
181
+ "marks.filter.priority": "Filter by priority",
182
+ "marks.favorite.add": "Favorite session",
183
+ "marks.favorite.remove": "Remove favorite",
184
+ "marks.review.add": "Mark for review",
185
+ "marks.review.remove": "Clear review flag",
186
+ "marks.edit": "Tags/Notes",
187
+ "marks.header.edit": "Tags/Notes",
188
+ "marks.edit.aria": "Edit tags, notes and priority",
189
+ "marks.title": "Session annotations",
190
+ "marks.favorite": "Favorite",
191
+ "marks.review": "Review later",
192
+ "marks.tags": "Tags",
193
+ "marks.tags.placeholder": "Separate with commas. Up to 20 tags, 32 chars each (duplicates merged). e.g. bug, design",
194
+ "marks.tags.help": "Up to 20 tags, 32 characters each; duplicates are merged",
195
+ "marks.note": "Note",
196
+ "marks.note.placeholder": "For example: awaiting Windows verification; check rollback next (up to 2000 characters)",
197
+ "marks.privacy": "For organization only. Not written into chat history or automatically sent to the model.",
198
+ "marks.priority": "Priority",
199
+ "marks.priority.all": "All priorities",
200
+ "marks.priority.1": "1 · Highest",
201
+ "marks.priority.2": "2 · High",
202
+ "marks.priority.3": "3 · Normal",
203
+ "marks.priority.4": "4 · Low",
204
+ "marks.priority.5": "5 · Lowest",
205
+ "marks.priority.help": "1 is highest, 5 is lowest. Default is 3 (Normal).",
206
+ "marks.tags.all": "All tags",
207
+ "marks.tags.none": "No tags",
208
+ "marks.tags.missing": "{tag} (no matches)",
209
+ "marks.save": "Save annotations",
210
+ "marks.saving": "Saving…",
211
+ "marks.cancel": "Cancel",
212
+ "marks.loading": "Loading session annotations…",
213
+ "marks.error": "Could not load annotations: {message}",
214
+ "marks.retry": "Retry annotations",
215
+ "marks.conflict": "Another window changed these annotations. Your draft is preserved; load the latest version before editing again.",
216
+ "marks.reload": "Load latest",
217
+ "marks.discard": "Discard unsaved annotation changes?",
218
+ "marks.note.exists": "Has note",
219
+ "marks.sort": "Priority (1 → 5)",
220
+ "marks.note.exists": "Has note",
221
+ "marks.import.copy": "Copy Prompt",
222
+ "marks.import.copy.copied": "Copied. Paste it into the current conversation to generate the result.",
223
+ "marks.import.copy.failed": "Copy failed: {message}",
224
+ "marks.import.paste": "Import",
225
+ "marks.import.paste.label": "AI response",
226
+ "marks.import.paste.help": "Paste the JSON returned by the model into the box below, then click Import.",
227
+ "marks.import.paste.aria": "JSON returned by the model",
228
+ "marks.import.paste.placeholder": "Supports json code fences or plain JSON, e.g. { \"tags\": [\"tag1\", \"tag2\"], \"note\": \"session note\" }",
229
+ "marks.import.paste.clear": "Clear input",
230
+ "marks.import.empty": "Paste the JSON returned by the model into the box above first.",
231
+ "marks.import.unknown": "unknown field",
232
+ "marks.import.discard": "Importing will overwrite your unsaved edits. Continue?",
233
+ "marks.import.failure": "Import failed: {message}",
234
+ "marks.import.warnings": "Imported, with adjustments:",
235
+ "marks.import.success": "Imported AI result from the clipboard",
236
+ "marks.tags.clear": "Clear tags",
237
+ "marks.note.clear": "Clear note",
97
238
  "header.aria": "Session actions",
98
239
  "menu.open": "Open",
99
240
  "menu.archive": "Archive session",
@@ -104,6 +245,22 @@ window.__ModuleLoader__.load({
104
245
  "footer.aria": "Session manager",
105
246
  "footer.label": "Sessions",
106
247
  "panel.title": "Session manager",
248
+ "panel.search": "Search title, ID, tags or notes",
249
+ "panel.workspace": "Workspace",
250
+ "panel.workspace.all": "All workspaces",
251
+ "panel.workspace.ungrouped": "Ungrouped",
252
+ "panel.workspace.loading": "Loading workspaces…",
253
+ "panel.workspace.error": "Could not load workspaces: {message}",
254
+ "panel.workspace.retry": "Retry",
255
+ "panel.sort": "Sort by",
256
+ "panel.sort.unavailable": "Creation times are unavailable in this DSH version; showing source order.",
257
+ "panel.sort.updated-desc": "Recently updated",
258
+ "panel.sort.updated-asc": "Least recently updated",
259
+ "panel.sort.created-desc": "Newest created",
260
+ "panel.sort.created-asc": "Oldest created",
261
+ "panel.count": "Showing {shown} / {total} sessions",
262
+ "panel.reset": "Reset filters",
263
+ "panel.empty.filtered": "No matching sessions. Adjust your search or filters.",
107
264
  "panel.filter.all": "All",
108
265
  "panel.filter.active": "Active",
109
266
  "panel.filter.archived": "Archived",
@@ -189,17 +346,33 @@ window.__ModuleLoader__.load({
189
346
  ".sm-moveItem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
190
347
  ".sm-moveItemSelected{border-color:var(--dsw-alias-accent-primary);background:var(--dsw-alias-accent-primary-bg)}",
191
348
  ".sm-moveItemName{color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}",
192
- ".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%;z-index:50;display:flex;flex-direction:column;min-width:160px;padding:4px;background:var(--dsw-alias-surface-l1);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.12)}.sm-headerMenuItem{display:flex;align-items:center;width:100%;text-align:left;background:0 0;border:0;border-radius:6px;color:var(--dsw-alias-label-primary);cursor:pointer;padding:6px 10px;font-size:13px;line-height:18px}.sm-headerMenuItem:hover{background:var(--dsw-alias-interactive-bg-hover)}.sm-headerMenuItemDanger{color:var(--dsw-alias-state-error-primary)}.sm-headerBtn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:32px;height:auto;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;padding:4px 12px;font-size:13px;line-height:18px;white-space:nowrap;transition:background .15s,color .15s,border-color .15s}.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-accent-primary);outline-offset:1px}.sm-headerBtn svg{width:16px;height:16px}.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-accent-primary);color:#fff;border-color:var(--dsw-alias-accent-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-fill-l2);color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}.sm-migrateWarn{padding:10px 12px;border:1px solid rgba(217,119,6,.25);border-radius:8px;background:rgba(217,119,6,.08);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-headerBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l1)}.sm-headerBtnDanger{background:var(--dsw-alias-state-error-tertiary);color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);font-weight:500}.sm-headerBtnDanger:hover:not(:disabled){background:#d92d20!important;color:#fff!important;border-color:#d92d20!important}.sm-header{display:inline-flex;align-items:center;gap:6px}.sm-headerBtnActive{background:var(--dsw-alias-fill-l3);color:var(--dsw-alias-label-primary)}",
193
- ".sm-panelDialog{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:560px;max-width:calc(100vw - 32px);max-height:540px;height:auto;box-sizing:border-box;display:flex;flex-direction:column;gap:0;background:var(--dsw-alias-surface-l1,#fff);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:9999;opacity:1}.sm-panelDialog .sm-nativeDialogHeader{padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none}.sm-panelDialog .sm-nativeDialogTitle{margin:0;font-size:14px;line-height:20px;font-weight:600}.sm-panelDialog .sm-nativeDialogFooter{padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex:none;flex-wrap:wrap;justify-content:flex-end;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}.sm-panelDialog .sm-row{display:flex;align-items:center;gap:8px;padding:0 10px;height:36px;min-height:36px;border:1px solid transparent;border-radius:8px;background:transparent}.sm-panelDialog .sm-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.sm-panelDialog .sm-rowCurrent{background:var(--dsw-alias-fill-l2);border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-panelDialog .sm-rowMain{display:flex;align-items:stretch!important;gap:8px;min-width:0;flex:1 1 auto;overflow:hidden;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}.sm-confirmDialog{z-index:10000}.sm-confirmDialog.sm-nativeDialogLayer{position:fixed;left:calc(50vw + 184px);top:calc(50vh - 90px);width:300px;max-width: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: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;box-sizing:border-box;background:var(--dsw-alias-surface-l1,#fff);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{padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee)}.sm-confirmDialog .sm-nativeDialogTitle{margin:0;font-size:14px;line-height:20px;font-weight:600}.sm-confirmDialog .sm-nativeDialogBody{padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}.sm-confirmDialog .sm-nativeDialogDescription{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap}.sm-confirmDialog .sm-warn{padding:6px 10px;font-size:12px;line-height:16px;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:flex-end;gap:8px;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)}.sm-confirmDialog .sm-nativeDialogCancel:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.sm-confirmDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-state-error-primary);color:#fff;border-color:var(--dsw-alias-state-error-primary)}.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;height:22px;padding:0 8px;font-size:11px;line-height:14px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}.sm-panelDialog .sm-rowActions .sm-rowBtn:hover{background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-primary)}.sm-panelDialog .sm-rowActions .sm-rowBtnDanger{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{min-height:24px;padding:2px 10px;font-size:11px;line-height:16px;border-radius:4px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}.sm-panelDialog .sm-filterBtn.sm-on{background:var(--dsw-alias-fill-l2);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 12px;font-size:12px;line-height:18px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer}.sm-panelDialog .sm-nativeDialogCancel{background:transparent}.sm-panelDialog .sm-nativeDialogGhost{background:transparent;border-color:var(--dsw-alias-border-l2)}.sm-panelDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:var(--dsw-alias-accent-primary,#356ae6)}.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:flex-end;gap:8px;margin-top:14px}.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:auto;max-height:min(70vh,520px);box-sizing:border-box;background-color:#ffffff;background-color:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid #d4d4d8;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;opacity:1}.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:#ffffff;background-color:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.45);overflow:hidden;opacity:1;z-index:10001;backdrop-filter:none;-webkit-backdrop-filter:none}.sm-migrateDialog .sm-nativeDialogHeader{padding:12px 18px;border-bottom:1px solid #eee;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogTitle{margin:0;font-size:15px;line-height:22px;font-weight:600;color:#111;color:var(--dsw-alias-label-primary,#111)}.sm-migrateDialog .sm-migratePanel{padding:12px 18px;display:flex;flex-direction:column;gap:14px;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogBody{padding:0;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogFooter{padding:12px 18px;border-top:1px solid #eee;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex:none;flex-wrap:wrap;justify-content:flex-end;gap:8px;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-migrateRow>label{color:#444;color:var(--dsw-alias-label-secondary,#444)}.sm-migrateDialog .sm-migrateChip{padding:6px 12px;border-radius:999px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);background-color:#f5f5f5;background-color:var(--dsw-alias-fill-l2,#f5f5f5);color:#111;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:#e8e8e8;background-color:var(--dsw-alias-interactive-bg-hover,#e8e8e8)}.sm-migrateDialog .sm-migrateChipOn{background-color:#356ae6;background-color:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:#356ae6;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-migrateDialog .sm-migrateChip.sm-migrateChipOn:hover:not(:disabled){background-color:#4576f0;background-color:var(--dsw-alias-accent-primary-hover,#4576f0);color:#fff;border-color:#4576f0;border-color:var(--dsw-alias-accent-primary-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:#888;color:var(--dsw-alias-label-tertiary,#888)}.sm-migrateDialog .sm-migratePreview{padding:10px 12px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;background-color:#f5f5f5;background-color:var(--dsw-alias-fill-l2,#f5f5f5);color:#111;color:var(--dsw-alias-label-primary,#111);font-size:13px;line-height:18px}.sm-migrateDialog .sm-migrateWarn{padding:10px 12px;border:1px solid rgba(217,119,6,.25);border-radius:8px;background-color:rgba(217,119,6,.08);color:#444;color:var(--dsw-alias-label-secondary,#444);font-size:13px;line-height:18px}.sm-migrateDialog .sm-nativeDialogButton{min-height:32px;padding:4px 14px;font-size:13px;line-height:20px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;cursor:pointer;background-color:#fff;background-color:var(--dsw-alias-fill-l2,#fff);color:#111;color:var(--dsw-alias-label-primary,#111)}.sm-migrateDialog .sm-nativeDialogButton:hover:not(:disabled){background-color:#e8e8e8;background-color:var(--dsw-alias-interactive-bg-hover,#e8e8e8)}.sm-migrateDialog .sm-nativeDialogConfirm{background-color:#356ae6;background-color:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:#356ae6;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-migrateDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}",
349
+ ".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%;z-index:50;display:flex;flex-direction:column;min-width:160px;padding:4px;background:var(--dsw-alias-surface-l1);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.12)}.sm-headerMenuItem{display:flex;align-items:center;width:100%;text-align:left;background:0 0;border:0;border-radius:6px;color:var(--dsw-alias-label-primary);cursor:pointer;padding:6px 10px;font-size:13px;line-height:18px}.sm-headerMenuItem:hover{background:var(--dsw-alias-interactive-bg-hover)}.sm-headerMenuItemDanger{color:var(--dsw-alias-state-error-primary)}.sm-headerBtn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:32px;height:auto;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;padding:4px 12px;font-size:13px;line-height:18px;white-space:nowrap;transition:background .15s,color .15s,border-color .15s}.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-accent-primary);outline-offset:1px}.sm-headerBtn svg{width:16px;height:16px}.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-accent-primary);color:#fff;border-color:var(--dsw-alias-accent-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-fill-l2);color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}.sm-migrateWarn{padding:10px 12px;border:1px solid rgba(217,119,6,.25);border-radius:8px;background:rgba(217,119,6,.08);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-headerBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l1)}.sm-headerBtnDanger{background:var(--dsw-alias-state-error-tertiary);color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);font-weight:500}.sm-headerBtnDanger:hover:not(:disabled){background:#d92d20!important;color:#fff!important;border-color:#d92d20!important}.sm-header{display:inline-flex;align-items:center;gap:6px}.sm-annotationToggleOpen{position:relative;z-index:10004}.sm-headerBtnActive{background:var(--dsw-alias-fill-l3);color:var(--dsw-alias-label-primary)}",
350
+ ".sm-panelDialog{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:600px;max-width:calc(100vw - 32px);max-height:560px;height:auto;box-sizing:border-box;display:flex;flex-direction:column;gap:0;background:var(--dsw-alias-surface-l1,#fff);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:9999;opacity:1}.sm-panelDialog .sm-nativeDialogHeader{padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none}.sm-panelDialog .sm-nativeDialogTitle{margin:0;font-size:14px;line-height:20px;font-weight:600}.sm-panelDialog .sm-nativeDialogFooter{padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex:none;flex-wrap:wrap;justify-content:flex-end;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}.sm-panelDialog .sm-row{display:flex;align-items:center;gap:8px;padding:0 10px;height:36px;min-height:36px;border:1px solid transparent;border-radius:8px;background:transparent}.sm-panelDialog .sm-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.sm-panelDialog .sm-rowCurrent{background:var(--dsw-alias-fill-l2);border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-panelDialog .sm-rowMain{display:flex;align-items:stretch!important;gap:8px;min-width:0;flex:1 1 auto;overflow:hidden;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}.sm-confirmDialog{z-index:10000}.sm-confirmDialog.sm-nativeDialogLayer{position:fixed;left:calc(50vw + 184px);top:calc(50vh - 90px);width:300px;max-width: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: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;box-sizing:border-box;background:var(--dsw-alias-surface-l1,#fff);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{padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee)}.sm-confirmDialog .sm-nativeDialogTitle{margin:0;font-size:14px;line-height:20px;font-weight:600}.sm-confirmDialog .sm-nativeDialogBody{padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}.sm-confirmDialog .sm-nativeDialogDescription{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap}.sm-confirmDialog .sm-warn{padding:6px 10px;font-size:12px;line-height:16px;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:flex-end;gap:8px;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)}.sm-confirmDialog .sm-nativeDialogCancel:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.sm-confirmDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-state-error-primary);color:#fff;border-color:var(--dsw-alias-state-error-primary)}.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;height:22px;padding:0 8px;font-size:11px;line-height:14px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}.sm-panelDialog .sm-rowActions .sm-rowBtn:hover{background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-primary)}.sm-panelDialog .sm-rowActions .sm-rowBtnDanger{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{min-height:24px;padding:2px 10px;font-size:11px;line-height:16px;border-radius:4px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}.sm-panelDialog .sm-filterBtn.sm-on{background:var(--dsw-alias-fill-l2);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 12px;font-size:12px;line-height:18px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer}.sm-panelDialog .sm-nativeDialogCancel{background:transparent}.sm-panelDialog .sm-nativeDialogGhost{background:transparent;border-color:var(--dsw-alias-border-l2)}.sm-panelDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:var(--dsw-alias-accent-primary,#356ae6)}.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:flex-end;gap:8px;margin-top:14px}.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:auto;max-height:min(70vh,520px);box-sizing:border-box;background-color:#ffffff;background-color:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid #d4d4d8;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;opacity:1}.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:#ffffff;background-color:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.45);overflow:hidden;opacity:1;z-index:10001;backdrop-filter:none;-webkit-backdrop-filter:none}.sm-migrateDialog .sm-nativeDialogHeader{padding:12px 18px;border-bottom:1px solid #eee;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogTitle{margin:0;font-size:15px;line-height:22px;font-weight:600;color:#111;color:var(--dsw-alias-label-primary,#111)}.sm-migrateDialog .sm-migratePanel{padding:12px 18px;display:flex;flex-direction:column;gap:14px;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogBody{padding:0;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogFooter{padding:12px 18px;border-top:1px solid #eee;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex:none;flex-wrap:wrap;justify-content:flex-end;gap:8px;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-migrateRow>label{color:#444;color:var(--dsw-alias-label-secondary,#444)}.sm-migrateDialog .sm-migrateChip{padding:6px 12px;border-radius:999px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);background-color:#f5f5f5;background-color:var(--dsw-alias-fill-l2,#f5f5f5);color:#111;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:#e8e8e8;background-color:var(--dsw-alias-interactive-bg-hover,#e8e8e8)}.sm-migrateDialog .sm-migrateChipOn{background-color:#356ae6;background-color:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:#356ae6;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-migrateDialog .sm-migrateChip.sm-migrateChipOn:hover:not(:disabled){background-color:#4576f0;background-color:var(--dsw-alias-accent-primary-hover,#4576f0);color:#fff;border-color:#4576f0;border-color:var(--dsw-alias-accent-primary-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:#888;color:var(--dsw-alias-label-tertiary,#888)}.sm-migrateDialog .sm-migratePreview{padding:10px 12px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;background-color:#f5f5f5;background-color:var(--dsw-alias-fill-l2,#f5f5f5);color:#111;color:var(--dsw-alias-label-primary,#111);font-size:13px;line-height:18px}.sm-migrateDialog .sm-migrateWarn{padding:10px 12px;border:1px solid rgba(217,119,6,.25);border-radius:8px;background-color:rgba(217,119,6,.08);color:#444;color:var(--dsw-alias-label-secondary,#444);font-size:13px;line-height:18px}.sm-migrateDialog .sm-nativeDialogButton{min-height:32px;padding:4px 14px;font-size:13px;line-height:20px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;cursor:pointer;background-color:#fff;background-color:var(--dsw-alias-fill-l2,#fff);color:#111;color:var(--dsw-alias-label-primary,#111)}.sm-migrateDialog .sm-nativeDialogButton:hover:not(:disabled){background-color:#e8e8e8;background-color:var(--dsw-alias-interactive-bg-hover,#e8e8e8)}.sm-migrateDialog .sm-nativeDialogConfirm{background-color:#356ae6;background-color:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:#356ae6;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-migrateDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}",
194
351
  ".sm-panelDialog .sm-nativeDialogHeader{display:flex;align-items:center;gap:8px}.sm-panelHeaderClose{min-height:26px;padding:2px 9px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary,#555);cursor:pointer;font-size:12px;line-height:18px}.sm-panelHeaderClose:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#eee);color:var(--dsw-alias-label-primary,#111)}.sm-panelHeaderClose:disabled{opacity:.5;cursor:not-allowed}.sm-migrateCurrent{padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;background:var(--dsw-alias-fill-l2,#f5f5f5);font-size:13px;line-height:18px;color:var(--dsw-alias-label-primary,#111)}.sm-migrateConfirmLayer.sm-confirmDialog.sm-nativeDialogLayer{width:300px;inset:calc(50vh - 90px) auto auto calc(50vw + 308px)}.sm-migrateConfirmLayer.sm-confirmDialog.sm-nativeDialogLayer .sm-migrateDialog{position:relative;left:auto;top:auto;bottom:auto;transform:none;width:100%;max-width:none;max-height:min(70vh,520px);z-index:auto}.sm-migrateConfirmLayer .sm-migratePanel{min-width:0}.sm-migrateConfirmLayer .sm-nativeDialogButton.sm-nativeDialogConfirm:hover:not(:disabled){background-color:#4576f0;background-color:var(--dsw-alias-accent-primary-hover,#4576f0);color:#fff;border-color:#4576f0;border-color:var(--dsw-alias-accent-primary-hover,#4576f0);filter:none}",
195
352
 
196
353
  // Theme colors are scoped to the plugin dialogs. DSH exposes its
197
354
  // actual theme through `body[data-ds-dark-theme]` and color-scheme;
198
355
  // keeping these values local avoids changing DSH's global variables.
199
356
  "[data-sm-theme=light] .sm-panelDialog,[data-sm-theme=light] .sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialog,[data-sm-theme=light] .sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialog{--dsw-alias-surface-l1:#fff;--dsw-alias-label-primary:#1f1f1f;--dsw-alias-label-secondary:#555;--dsw-alias-label-tertiary:#888;--dsw-alias-border-l1:#d4d4d8;--dsw-alias-border-l2:#e5e5e5;--dsw-alias-fill-l2:#f5f5f5;--dsw-alias-interactive-bg-hover:#eee;--dsw-alias-state-error-primary:#d92d20;--dsw-alias-state-warning-primary:#d97706;--dsw-alias-accent-primary:#356ae6;--dsw-alias-accent-primary-bg:rgba(53,106,230,.12);background:#fff!important;background-color:#fff!important;color:#1f1f1f!important;border-color:#e5e5e5!important}",
200
- "[data-sm-theme=dark] .sm-panelDialog,[data-sm-theme=dark] .sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialog,[data-sm-theme=dark] .sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialog{--dsw-alias-surface-l1:#1f1f23;--dsw-alias-label-primary:#ececec;--dsw-alias-label-secondary:#b3b3b3;--dsw-alias-label-tertiary:#999;--dsw-alias-border-l1:#555;--dsw-alias-border-l2:#3a3a40;--dsw-alias-fill-l2:#2a2a30;--dsw-alias-interactive-bg-hover:rgba(255,255,255,.06);--dsw-alias-state-error-primary:#f47171;--dsw-alias-state-warning-primary:#f0a040;--dsw-alias-accent-primary:#5d8aff;--dsw-alias-accent-primary-bg:rgba(93,138,255,.18);background:#1f1f23!important;background-color:#1f1f23!important;color:#ececec!important;border-color:#3a3a40!important}",
357
+ "[data-sm-theme=dark] .sm-panelDialog,[data-sm-theme=dark] .sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialog,[data-sm-theme=dark] .sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialog{--dsw-alias-surface-l1:#1f1f23;--dsw-alias-label-primary:#ececec;--dsw-alias-label-secondary:#b3b3b3;--dsw-alias-label-tertiary:#999;--dsw-alias-border-l1:#555;--dsw-alias-border-l2:#3a3a40;--dsw-alias-fill-l1:#1a1a1e;--dsw-alias-fill-l2:#2a2a30;--dsw-alias-interactive-bg-hover:rgba(255,255,255,.06);--dsw-alias-state-error-primary:#f47171;--dsw-alias-state-warning-primary:#f0a040;--dsw-alias-accent-primary:#5d8aff;--dsw-alias-accent-primary-bg:rgba(93,138,255,.18);background:#1f1f23!important;background-color:#1f1f23!important;color:#ececec!important;border-color:#3a3a40!important}","[data-sm-theme=dark] .sm-header{--dsw-alias-surface-l1:#1f1f23;--dsw-alias-label-primary:#ececec;--dsw-alias-label-secondary:#b3b3b3;--dsw-alias-label-tertiary:#999;--dsw-alias-border-l1:#555;--dsw-alias-border-l2:#3a3a40;--dsw-alias-fill-l1:#1a1a1e;--dsw-alias-fill-l2:#2a2a30;--dsw-alias-interactive-bg-hover:rgba(255,255,255,.06);--dsw-alias-accent-primary:#5d8aff;--dsw-alias-accent-primary-bg:rgba(93,138,255,.18)}",
201
358
  "[data-sm-theme=light] .sm-panelDialog .sm-nativeDialogHeader,[data-sm-theme=light] .sm-panelDialog .sm-nativeDialogFooter,[data-sm-theme=light] .sm-confirmDialog .sm-nativeDialogHeader,[data-sm-theme=light] .sm-confirmDialog .sm-nativeDialogFooter,[data-sm-theme=light] .sm-migrateDialog .sm-nativeDialogHeader,[data-sm-theme=light] .sm-migrateDialog .sm-nativeDialogBody,[data-sm-theme=light] .sm-migrateDialog .sm-nativeDialogFooter{background:#fff!important;background-color:#fff!important;border-color:#e5e5e5!important}",
202
359
  "[data-sm-theme=dark] .sm-panelDialog .sm-nativeDialogHeader,[data-sm-theme=dark] .sm-panelDialog .sm-nativeDialogFooter,[data-sm-theme=dark] .sm-confirmDialog .sm-nativeDialogHeader,[data-sm-theme=dark] .sm-confirmDialog .sm-nativeDialogFooter,[data-sm-theme=dark] .sm-migrateDialog .sm-nativeDialogHeader,[data-sm-theme=dark] .sm-migrateDialog .sm-nativeDialogBody,[data-sm-theme=dark] .sm-migrateDialog .sm-nativeDialogFooter{background:#1f1f23!important;background-color:#1f1f23!important;border-color:#3a3a40!important}",
360
+ ".sm-panelDialog{max-height:min(640px,calc(100vh - 32px))}.sm-panelDialog .sm-panel{min-width:0;overflow:hidden}",
361
+ ".sm-panelTools{display:flex;flex-direction:column;gap:8px;flex:none;padding:2px 0 6px}",
362
+ ".sm-queryInput,.sm-viewSelect{box-sizing:border-box;width:100%;min-width:0;height:32px;padding:5px 9px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:7px;background:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);font:inherit;font-size:12px;line-height:20px}",
363
+ ".sm-queryInput::placeholder{color:var(--dsw-alias-label-tertiary,#888)}.sm-queryInput:focus-visible,.sm-viewSelect:focus-visible,.sm-resetFilters:focus-visible{outline:2px solid var(--dsw-alias-accent-primary,#356ae6);outline-offset:2px}.sm-viewSelect:disabled{opacity:.6;cursor:wait}",
364
+ ".sm-panelSelects{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px}.sm-viewField{display:flex;flex-direction:column;gap:3px;min-width:0;color:var(--dsw-alias-label-secondary,#666);font-size:11px;line-height:16px}",
365
+ ".sm-filterBar{display:flex;align-items:center;justify-content:space-between;gap:6px;flex-wrap:wrap}.sm-resultSummary{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary,#888)}.sm-resetFilters{padding:2px 0;border:0;background:none;color:var(--dsw-alias-accent-primary,#356ae6);font:inherit;cursor:pointer}.sm-resetFilters:hover{text-decoration:underline}.sm-workspaceNotice{display:flex;gap:8px;align-items:center;font-size:12px;color:var(--dsw-alias-label-secondary,#666)}",
366
+ "@media(max-width:540px){.sm-panelSelects{grid-template-columns:minmax(0,1fr)}.sm-panelDialog .sm-row{height:auto;min-height:36px;flex-shrink:0;flex-wrap:wrap;padding:6px 8px}.sm-panelDialog .sm-rowMain{flex-basis:100%}.sm-panelDialog .sm-rowActions{width:100%;min-width:0;flex:1 1 100%;justify-content:flex-start}}",
367
+ ".sm-panelDialog:not(.sm-annotationSurface){width:680px}.sm-panelDialog .sm-row{height:auto;min-height:36px;flex-shrink:0;flex-wrap:wrap;padding:5px 8px}.sm-panelDialog .sm-rowMain{flex:1 1 200px;flex-direction:column;gap:3px}.sm-panelDialog .sm-rowActions{margin-left:auto;max-width:100%}.sm-rowAnnotations{display:flex;align-items:center;gap:5px;min-width:0;font-size:10px;line-height:16px}",
368
+ ".sm-markIcon{min-width:25px;padding-left:5px;padding-right:5px;font-size:15px}.sm-markIcon.sm-markOn{color:var(--dsw-alias-accent-primary,#356ae6)!important;background:var(--dsw-alias-fill-l2,#eee)}.sm-priorityBadge{padding:0 5px;border-radius:4px;background:var(--dsw-alias-fill-l2,#eee);color:var(--dsw-alias-label-secondary,#666)}.sm-priority-1{color:#c2410c;background:rgba(234,88,12,.12)}.sm-priority-2{color:#a16207;background:rgba(202,138,4,.12)}",
369
+ "[data-sm-theme=dark] .sm-priority-1{color:#fdba74}[data-sm-theme=dark] .sm-priority-2{color:#fde68a}",
370
+ ".sm-tagChip,.sm-noteBadge{max-width:88px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:0;border-radius:4px;background:var(--dsw-alias-fill-l2,#eee);color:var(--dsw-alias-label-secondary,#666);font:inherit;line-height:16px;padding:0 5px;cursor:pointer}.sm-noteBadge{background:transparent}.sm-tagChip:hover,.sm-noteBadge:hover{color:var(--dsw-alias-accent-primary,#356ae6)}",
371
+ ".sm-markFilters{display:grid;grid-template-columns:auto auto minmax(100px,1fr) minmax(100px,1fr);align-items:end;gap:6px}.sm-markFilterToggle{height:32px;white-space:nowrap}.sm-markFilterToggle:disabled{opacity:.5;cursor:not-allowed}.sm-header{flex-wrap:wrap}",
372
+ ".sm-annotationDialog.sm-nativeDialogLayer{position:fixed;display:block;pointer-events:none;z-index:10002;background:transparent}.sm-annotationDialog .sm-nativeDialogBackdrop{display:none}.sm-annotationDialog.sm-annotationDialog-anchored.sm-nativeDialogLayer{inset:auto;background:transparent}.sm-annotationDialog.sm-confirmDialog.sm-nativeDialogLayer:not(.sm-annotationDialog-anchored){inset:50% auto auto calc(50vw + 308px);transform:translateY(-50%);max-height:calc(100vh - 32px)}.sm-panelDialog.sm-annotationSurface{position:absolute;width:460px;max-height:min(680px,calc(100vh - 32px));pointer-events:auto;z-index:10003;box-shadow:0 16px 48px rgba(0,0,0,.25)}.sm-panelDialog.sm-annotationSurface.sm-annotationSurface-anchored{position:relative;transform:none;left:auto;top:auto;margin:0}.sm-annotationCloseHint{margin:0 16px 8px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}",
373
+ ".sm-annotationBody{padding:14px 16px;display:flex;flex-direction:column;gap:12px;min-height:0;overflow:auto;flex:1 1 auto}.sm-annotationTarget{font-size:12px;line-height:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary,#666)}.sm-annotationChecks{display:grid;grid-template-columns:auto 1fr;column-gap:14px;row-gap:4px;align-items:center;font-size:13px}.sm-annotationChecks>label{display:flex;gap:6px;align-items:center;cursor:pointer}.sm-annotationChecks .sm-annotationPriority{display:inline-flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:inherit}.sm-annotationChecks .sm-prioritySelectInline{height:24px;padding:2px 6px;font-size:12px;line-height:18px;min-width:104px;width:auto;border-radius:6px;color:var(--dsw-alias-label-primary,#111)}.sm-annotationChecks>small{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#888);justify-self:start}.sm-annotationHint,.sm-annotationBody small{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#888)}",
374
+ ".sm-noteInput{box-sizing:border-box;width:100%;min-height:60px;resize:vertical;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:7px;padding:8px;background:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);font:inherit;font-size:13px;line-height:19px}.sm-noteInput:focus-visible{outline:2px solid var(--dsw-alias-accent-primary,#356ae6);outline-offset:2px}.sm-markConflict{display:flex;flex-direction:column;gap:5px;font-size:12px;padding:8px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:6px}.sm-fieldWithClear .sm-fieldRow{position:relative;display:flex;align-items:flex-start;gap:4px}.sm-fieldRow .sm-queryInput{flex:1 1 auto;min-width:0}.sm-fieldRowNote{align-items:stretch}.sm-fieldClear{position:absolute;top:5px;right:6px;width:22px;height:22px;border:0;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:4px;font-size:14px;line-height:18px;display:inline-flex;align-items:center;justify-content:center;padding:0}.sm-fieldClear:hover,.sm-fieldClear:focus-visible{color:var(--dsw-alias-accent-primary);background:var(--dsw-alias-interactive-bg-hover);outline:none}.sm-fieldClearNote{top:6px}.sm-importGroup{display:flex;flex-direction:column;gap:8px;padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);background:var(--dsw-alias-fill-l1,#fafbfc)}.sm-importGroupButtons{display:flex;justify-content:space-between;align-items:center;gap:8px}.sm-importBtn{cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-surface-l1,#fff);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;padding:5px 12px;font-size:12px;line-height:18px}.sm-importBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.sm-importBtn:disabled{opacity:.5;cursor:not-allowed}.sm-importStatus{margin:0 16px;padding:8px 10px;font-size:12px;line-height:18px;background:var(--dsw-alias-accent-primary-bg,rgba(53,106,230,.08));color:var(--dsw-alias-label-secondary);border-radius:6px;white-space:pre-wrap;word-break:break-word}",
375
+ "@media(max-width:600px){.sm-markFilters{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.sm-panelDialog .sm-row{flex-wrap:wrap}.sm-panelDialog .sm-rowMain{flex-basis:100%}.sm-panelDialog .sm-rowActions{width:100%;min-width:0;flex:1 1 100%;justify-content:flex-start}}",
203
376
  ].join("");
204
377
 
205
378
  // Auto-follow DSH's theme. We detect DSH's current theme from
@@ -323,9 +496,374 @@ window.__ModuleLoader__.load({
323
496
 
324
497
  const h = (type, props, ...children) => React.createElement(type, props, ...children);
325
498
  const callApi = (path, payload) => fetch(API + path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload || {}) }).then((r) => r.json());
326
- const fetchApi = (path) => fetch(API + path).then((r) => r.json());
499
+ const fetchApi = (path, options) => fetch(API + path, options).then((r) => r.json());
500
+
501
+ const EMPTY_ANNOTATION = Object.freeze({ favorite: false, reviewLater: false, tags: Object.freeze([]), note: "", priority: 3, revision: 0 });
502
+ const validAnnotation = value => value && typeof value.favorite === "boolean" && typeof value.reviewLater === "boolean" && Array.isArray(value.tags) && value.tags.every(tag => typeof tag === "string") && typeof value.note === "string" && (value.priority === null || Number.isInteger(value.priority) && value.priority >= 1 && value.priority <= 5) && Number.isSafeInteger(value.revision) && value.revision >= 0;
503
+ const annotationOf = (state, id) => Object.hasOwn(state.entries, id) ? state.entries[id] : EMPTY_ANNOTATION;
504
+ const annotationStore = (() => {
505
+ let state = { entries: {}, loaded: false, loading: false, error: null, pending: new Set() };
506
+ const listeners = new Set();
507
+ let tail = Promise.resolve(); let loading; let refreshAgain = false;
508
+ const publish = patch => { state = { ...state, ...patch }; for (const listener of [...listeners]) listener(); };
509
+ const enqueue = operation => { const result = tail.then(operation); tail = result.catch(() => {}); return result; };
510
+ const apiError = data => Object.assign(new Error(data?.error || "Annotation request failed"), { code: data?.code });
511
+ const store = {
512
+ notify: null,
513
+ getSnapshot: () => state,
514
+ subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
515
+ hasSubscribers: () => listeners.size > 0,
516
+ load(force = false) {
517
+ if (loading) { if (force) refreshAgain = true; return loading; }
518
+ if (state.loaded && !force) return Promise.resolve();
519
+ publish({ loading: true, error: null });
520
+ loading = enqueue(async () => {
521
+ const data = await fetchApi("/annotations", { cache: "no-store" });
522
+ if (!data?.ok) throw apiError(data);
523
+ const entries = data.result?.annotations;
524
+ if (!entries || typeof entries !== "object" || Array.isArray(entries)
525
+ || Object.values(entries).some(value => !validAnnotation(value))) throw new Error("Invalid annotation response");
526
+ publish({ entries, loaded: true, error: null });
527
+ }).catch(error => { publish({ error: error instanceof Error ? error.message : String(error) }); })
528
+ .finally(() => { loading = null; publish({ loading: false }); if (refreshAgain) { refreshAgain = false; void store.load(true); } });
529
+ return loading;
530
+ },
531
+ save(id, patch, expectedRevision) {
532
+ if (state.pending.has(id)) return Promise.reject(new Error("This session's annotations are being saved"));
533
+ if (!state.loaded || state.error) return Promise.reject(new Error(state.error || "Annotations have not loaded yet"));
534
+ publish({ pending: new Set([...state.pending, id]) });
535
+ return enqueue(async () => {
536
+ const data = await callApi("/annotations", { sessionId: id, patch, ...(expectedRevision !== undefined ? { expectedRevision } : {}) });
537
+ if (!data?.ok) throw apiError(data);
538
+ const annotation = data.result?.annotation;
539
+ if (!validAnnotation(annotation)) throw new Error("Invalid annotation response");
540
+ publish({ entries: { ...state.entries, [id]: annotation } });
541
+ try { store.notify?.(); } catch (_) { /* cross-tab sync is best-effort */ }
542
+ return annotation;
543
+ }).finally(() => { const pending = new Set(state.pending); pending.delete(id); publish({ pending }); });
544
+ }
545
+ };
546
+ return store;
547
+ })();
548
+ const useAnnotations = (refreshOnMount = false) => {
549
+ const state = useSyncExternalStore(annotationStore.subscribe, annotationStore.getSnapshot, annotationStore.getSnapshot);
550
+ useEffect(() => { void annotationStore.load(refreshOnMount); }, [refreshOnMount]);
551
+ return state;
552
+ };
553
+ const annotationButtons = (id, title, t, state, className, onToggle, onEdit, disabled = false, editorOpen = false) => {
554
+ const value = annotationOf(state, id);
555
+ const busy = disabled || !state.loaded || !!state.error || state.pending.has(id);
556
+ const favoriteLabel = t(value.favorite ? "marks.favorite.remove" : "marks.favorite.add") + ": " + title;
557
+ const reviewLabel = t(value.reviewLater ? "marks.review.remove" : "marks.review.add") + ": " + title;
558
+ return h(React.Fragment, null,
559
+ h("button", { type: "button", className: className + " sm-markIcon" + (value.favorite ? " sm-markOn" : ""), disabled: busy, "aria-label": favoriteLabel, title: favoriteLabel, "aria-pressed": value.favorite, onClick: () => onToggle({ favorite: !value.favorite }) }, value.favorite ? "★" : "☆"),
560
+ h("button", { type: "button", className: className + " sm-markIcon" + (value.reviewLater ? " sm-markOn" : ""), disabled: busy, "aria-label": reviewLabel, title: reviewLabel, "aria-pressed": value.reviewLater, onClick: () => onToggle({ reviewLater: !value.reviewLater }) }, "◷"),
561
+ h("button", { type: "button", className: className + (editorOpen ? " sm-annotationToggleOpen sm-headerBtnActive" : ""), disabled: busy, "aria-label": t("marks.edit.aria") + ": " + title, title: t("marks.edit.aria"), ...(className === "sm-headerBtn" ? { "aria-expanded": editorOpen } : {}), onClick: e => onEdit(e) }, t(className === "sm-headerBtn" ? "marks.header.edit" : "marks.edit"), className === "sm-headerBtn" ? h("span", { className: "sm-priorityBadge sm-priority-" + (value.priority ?? 3), title: t("marks.priority." + (value.priority ?? 3)) }, "P" + (value.priority ?? 3)) : null)
562
+ );
563
+ };
564
+
565
+
566
+ // Inline clipboard parser mirroring lib/clipboard-parser.js. The server
567
+ // re-validates the patch via annotationStore.save, so this helper only
568
+ // needs to be permissive enough to surface obvious mistakes early.
569
+ const TAG_LIMITS = { tags: 20, tagLength: 32, noteLength: 2000 };
570
+ const TAG_REJECT_RE = /[,,\x00-\x1f]/;
571
+ // Recover the most useful JSON object from a possibly noisy
572
+ // clipboard string. Tries, in order:
573
+ // 1. JSON.parse on the trimmed text.
574
+ // 2. JSON.parse inside a Markdown \`\`\`json ... \`\`\` fence.
575
+ // 3. JSON.parse on the first balanced {...} the scanner finds.
576
+ // 4. JSON.parse after repairing common AI mistakes
577
+ // (smart quotes, full-width punctuation, stray backslashes,
578
+ // trailing commas).
579
+ // Every failing attempt appends the underlying error so the user can
580
+ // see what actually went wrong.
581
+ const parseImportedAnnotation = raw => {
582
+ if (typeof raw !== "string" || raw.trim() === "") return { ok: false, error: "剪贴板为空 / Clipboard is empty" };
583
+ let work = raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw;
584
+ work = work.replace(/^\s+|\s+$/g, "");
585
+ const fence = work.match(/^```(?:json|JSON)?\s*([\s\S]*?)\s*```$/);
586
+ const scanForObject = text => {
587
+ const len = text.length;
588
+ for (let i = 0; i < len; i++) {
589
+ if (text[i] !== "{") continue;
590
+ let depth = 0, inString = false, escape = false;
591
+ for (let j = i; j < len; j++) {
592
+ const c = text[j];
593
+ if (inString) {
594
+ if (escape) escape = false;
595
+ else if (c === String.fromCharCode(92)) escape = true;
596
+ else if (c === String.fromCharCode(34)) inString = false;
597
+ continue;
598
+ }
599
+ if (c === String.fromCharCode(34)) inString = true;
600
+ else if (c === "{") depth++;
601
+ else if (c === "}") { depth--; if (depth === 0) return text.slice(i, j + 1); }
602
+ }
603
+ }
604
+ return null;
605
+ };
606
+ const repairLooseJson = text => text
607
+ // Smart quotes from Chinese / English typography.
608
+ .replace(/[“”]/g, "\"")
609
+ .replace(/[‘’]/g, "'")
610
+ // Stray single backslashes that aren't part of a valid JSON escape.
611
+ // Walk the string char-by-char to avoid touching \", \, \/, \n etc.
612
+ .replace(/\\(?![\\"/bfnrtu])/g, "\\\\");
613
+ const attempts = [];
614
+ const tryParse = (label, value) => {
615
+ try { return JSON.parse(value); }
616
+ catch (error) { attempts.push(label + ": " + error.message); return null; }
617
+ };
618
+ let parsed;
619
+ const primary = fence ? fence[1] : work;
620
+ parsed = tryParse("原始", primary) || tryParse("修复引号/反斜杠", repairLooseJson(primary));
621
+ if (parsed === null) {
622
+ const scanned = scanForObject(work);
623
+ if (scanned !== null) {
624
+ parsed = tryParse("扫描对象", scanned) || tryParse("扫描对象+修复", repairLooseJson(scanned));
625
+ }
626
+ }
627
+ if (parsed === null) {
628
+ return { ok: false, error: "剪贴板中的 JSON 无法解析 — " + attempts.join(";") };
629
+ }
630
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
631
+ return { ok: false, error: "剪贴板内容不是 JSON 对象" };
632
+ }
633
+ const allowed = new Set(["tags", "note", "priority"]);
634
+ const warnings = [];
635
+ for (const key of Object.keys(parsed)) if (!allowed.has(key)) warnings.push({ field: key, kind: "ignored" });
636
+ let tags = [];
637
+ if ("tags" in parsed) {
638
+ if (!Array.isArray(parsed.tags)) return { ok: false, error: "tags must be an array of strings" };
639
+ const seen = new Set();
640
+ for (const item of parsed.tags) {
641
+ if (typeof item !== "string") { warnings.push({ field: "tags", kind: "rejected" }); continue; }
642
+ const tag = item.trim();
643
+ if (!tag) continue;
644
+ if (tag.length > TAG_LIMITS.tagLength || TAG_REJECT_RE.test(tag)) {
645
+ warnings.push({ field: "tags", kind: "rejected", value: tag });
646
+ continue;
647
+ }
648
+ const key = tag.toLowerCase();
649
+ if (seen.has(key)) continue;
650
+ seen.add(key);
651
+ if (tags.length >= TAG_LIMITS.tags) { warnings.push({ field: "tags", kind: "truncated" }); continue; }
652
+ tags.push(tag);
653
+ }
654
+ }
655
+ let note = "";
656
+ if ("note" in parsed) {
657
+ if (typeof parsed.note !== "string") return { ok: false, error: "note must be a string" };
658
+ if (parsed.note.includes(String.fromCharCode(0))) return { ok: false, error: "note must not contain NUL characters" };
659
+ if (parsed.note.length > TAG_LIMITS.noteLength) {
660
+ warnings.push({ field: "note", kind: "truncated", from: parsed.note.length, to: TAG_LIMITS.noteLength });
661
+ note = parsed.note.slice(0, TAG_LIMITS.noteLength);
662
+ } else note = parsed.note;
663
+ }
664
+ let priority;
665
+ if ("priority" in parsed) {
666
+ // AI-returned null is treated as the default priority (3 = Normal) so
667
+ // the UI never has to special-case unset entries.
668
+ if (parsed.priority === null) priority = 3;
669
+ else if (Number.isInteger(parsed.priority) && parsed.priority >= 1 && parsed.priority <= 5) priority = parsed.priority;
670
+ else warnings.push({ field: "priority", kind: "rejected", value: parsed.priority });
671
+ }
672
+ const annotation = { tags, note };
673
+ if (priority !== undefined) annotation.priority = priority;
674
+ return { ok: true, annotation, warnings };
675
+ };
676
+ const CLIPBOARD_PROMPT_TEXT = {
677
+ zh: [
678
+ "请根据当前对话内容,为这个会话生成整理标记。",
679
+ "",
680
+ "要求:",
681
+ "1. 生成最多 20 个标签,每个标签最多 32 个字符。",
682
+ "2. 标签应简洁、具体,便于之后搜索。",
683
+ "3. 标签不要重复,不要生成无意义的标签。",
684
+ "4. 生成一段不超过 2000 个字符的备注,总结本次对话的主题、结论、未完成事项或下一步行动。",
685
+ "5. 不要编造对话中没有出现的信息。",
686
+ "6. 只返回合法 JSON,不要使用 Markdown 代码块,不要附加解释。",
687
+ "",
688
+ "返回格式:",
689
+ "{",
690
+ ' "tags": ["标签1", "标签2"],',
691
+ ' "note": "会话备注"',
692
+ "}"
693
+ ].join(String.fromCharCode(10)),
694
+ en: [
695
+ "Summarize this conversation into session annotations.",
696
+ "",
697
+ "Requirements:",
698
+ "1. Up to 20 tags, at most 32 characters each.",
699
+ "2. Tags should be concise and help future searches.",
700
+ "3. Do not repeat tags or invent meaningless ones.",
701
+ "4. Write a note under 2000 characters summarizing the topic, conclusions, outstanding items and next steps.",
702
+ "5. Do not fabricate details that are not present in the conversation.",
703
+ "6. Return only valid JSON, no Markdown fences, no extra commentary.",
704
+ "",
705
+ "Return shape:",
706
+ "{",
707
+ ' "tags": ["tag1", "tag2"],',
708
+ ' "note": "session note"',
709
+ "}"
710
+ ].join(String.fromCharCode(10))
711
+ };
712
+ const promptFor = lang => CLIPBOARD_PROMPT_TEXT[lang] || CLIPBOARD_PROMPT_TEXT.zh;
713
+
714
+ function AnnotationDialog({ sessionId, displayTitle, t, onClose, anchor }) {
715
+ const annotations = useAnnotations();
716
+ const latest = annotationOf(annotations, sessionId);
717
+ const [base, setBase] = useState(() => latest);
718
+ const [favorite, setFavorite] = useState(() => latest.favorite);
719
+ const [reviewLater, setReviewLater] = useState(() => latest.reviewLater);
720
+ const [tagsText, setTagsText] = useState(() => latest.tags.join(", "));
721
+ const [note, setNote] = useState(() => latest.note);
722
+ const [priority, setPriority] = useState(() => String(latest.priority ?? 3));
723
+ const [busy, setBusy] = useState(false);
724
+ const [error, setError] = useState(null);
725
+ const dialogRef = useRef(null);
726
+ const dirty = favorite !== base.favorite || reviewLater !== base.reviewLater || tagsText !== base.tags.join(", ") || note !== base.note || priority !== String(base.priority ?? 3);
727
+ const cancel = () => { if (busy || dirty && !window.confirm(t("marks.discard"))) return false; onClose(); return true; };
728
+ const reload = () => {
729
+ if (dirty && !window.confirm(t("marks.discard"))) return;
730
+ setBase(latest); setFavorite(latest.favorite); setReviewLater(latest.reviewLater);
731
+ setTagsText(latest.tags.join(", ")); setNote(latest.note); setPriority(String(latest.priority ?? 3)); setError(null);
732
+ };
733
+ const save = async () => {
734
+ if (busy || annotations.pending.has(sessionId)) return;
735
+ setBusy(true); setError(null);
736
+ try {
737
+ await annotationStore.save(sessionId, { favorite, reviewLater, tags: tagsText.split(/[,,\n]/).map(tag => tag.trim()).filter(Boolean), note, priority: Number(priority) }, base.revision);
738
+ onClose();
739
+ } catch (error) {
740
+ setError(error instanceof Error ? error.message : String(error));
741
+ if (error?.code === "annotation-conflict") await annotationStore.load(true);
742
+ } finally { setBusy(false); }
743
+ };
744
+ const escStateRef = useRef({}); escStateRef.current = { busy, cancel };
745
+ useEffect(() => {
746
+ const handler = e => {
747
+ if (e.key !== "Escape" || e.isComposing || e.keyCode === 229) return;
748
+ if (e.defaultPrevented) return;
749
+ if (escStateRef.current.busy) return;
750
+ e.preventDefault(); e.stopPropagation();
751
+ if (escStateRef.current.cancel()) {
752
+ const ae = document.activeElement;
753
+ if (ae && typeof ae.blur === "function" && ae !== document.body) ae.blur();
754
+ }
755
+ };
756
+ window.addEventListener("keydown", handler);
757
+ return () => window.removeEventListener("keydown", handler);
758
+ }, []);
759
+
760
+ const onKeyDown = e => {
761
+ if (e.key !== "Tab" || !dialogRef.current) return;
762
+ const controls = [...dialogRef.current.querySelectorAll('button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled)')];
763
+ if (!controls.length) return;
764
+ const first = controls[0], last = controls[controls.length - 1];
765
+ if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
766
+ else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
767
+ };
768
+ const [importStatus, setImportStatus] = useState(null);
769
+ const copyPrompt = async () => {
770
+ if (busy) return;
771
+ const text = promptFor(activeLanguage);
772
+ try {
773
+ if (navigator && navigator.clipboard && typeof navigator.clipboard.writeText === "function") await navigator.clipboard.writeText(text);
774
+ else { const ta = document.createElement("textarea"); ta.value = text; ta.setAttribute("readonly", ""); ta.style.position = "absolute"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); }
775
+ setImportStatus(t("marks.import.copy.copied"));
776
+ } catch (err) { setImportStatus(t("marks.import.copy.failed", { message: err instanceof Error ? err.message : String(err) })); }
777
+ };
778
+ const [importText, setImportText] = useState("");
779
+ const pasteFromInput = () => {
780
+ if (busy) return;
781
+ const text = (importText || "").trim();
782
+ if (!text) { setImportStatus(t("marks.import.failure", { message: t("marks.import.empty") })); return; }
783
+ if (dirty && !window.confirm(t("marks.import.discard"))) { setImportStatus(null); return; }
784
+ const result = parseImportedAnnotation(text);
785
+ if (!result.ok) { setImportStatus(t("marks.import.failure", { message: result.error })); return; }
786
+ const ann = result.annotation;
787
+ if (Array.isArray(ann.tags)) setTagsText(ann.tags.join(", "));
788
+ if (typeof ann.note === "string") setNote(ann.note);
789
+ if ("priority" in ann) setPriority(String(ann.priority ?? 3));
790
+ setError(null);
791
+ const warningLines = (result.warnings || []).map(w => {
792
+ const field = w.field || t("marks.import.unknown");
793
+ const detail = w.kind === "truncated" && typeof w.from === "number" && typeof w.to === "number"
794
+ ? field + ": " + w.from + " -> " + w.to
795
+ : w.kind === "rejected" && w.value !== undefined ? field + ": " + JSON.stringify(w.value)
796
+ : w.kind ? field + " (" + w.kind + ")" : field;
797
+ return " - " + detail + (w.reason ? " - " + w.reason : "");
798
+ });
799
+ if (warningLines.length) setImportStatus(t("marks.import.warnings") + String.fromCharCode(10) + warningLines.join(String.fromCharCode(10)));
800
+ else setImportStatus(t("marks.import.success"));
801
+ };
802
+ // Detect the active UI language by probing a key both dictionaries define.
803
+ // Falls back to the cached window flag (set elsewhere if available) and ultimately
804
+ // to "zh" so the prompt never reads empty.
805
+ const detectLanguage = (translator) => {
806
+ if (typeof window !== "undefined" && window.__smActiveLanguage) return window.__smActiveLanguage;
807
+ const sample = translator("marks.favorite");
808
+ if (typeof sample === "string") {
809
+ if (sample === "Favorite") return "en";
810
+ if (sample === "收藏") return "zh";
811
+ }
812
+ return "zh";
813
+ };
814
+ const activeLanguage = detectLanguage(t);
815
+ const hasAnchor = anchor && typeof anchor.top === "number" && typeof anchor.left === "number";
816
+ const layerClass = "sm-nativeDialogLayer sm-annotationDialog sm-confirmDialog" + (hasAnchor ? " sm-annotationDialog-anchored" : "");
817
+ const surfaceClass = "sm-nativeDialog sm-panelDialog sm-annotationSurface" + (hasAnchor ? " sm-annotationSurface-anchored" : "");
818
+ // Default position matches ConfirmDialog / MoveDialog so all inner
819
+ // dialogs line up at top calc(50vh - 90px), left calc(50vw + 308px).
820
+ const layerStyle = hasAnchor
821
+ ? { position: "fixed", top: anchor.top + "px", left: anchor.left + "px", zIndex: 10002 }
822
+ : null;
823
+ return h("div", { className: layerClass, style: layerStyle, role: "presentation" },
824
+ h("section", { className: surfaceClass, role: "dialog", "aria-labelledby": "sm-annotation-title", ref: dialogRef, onKeyDown },
825
+ h("div", { className: "sm-nativeDialogHeader" }, h("h2", { id: "sm-annotation-title", className: "sm-nativeDialogTitle" }, t("marks.title"))),
826
+ h("div", { className: "sm-annotationBody" },
827
+ h("div", { className: "sm-annotationTarget", title: displayTitle || sessionId }, displayTitle || sessionId),
828
+ h("div", { className: "sm-annotationChecks" },
829
+ h("label", null, h("input", { type: "checkbox", "aria-label": t("marks.favorite"), checked: favorite, disabled: busy, onChange: e => setFavorite(e.target.checked) }), t("marks.favorite")),
830
+ h("label", { className: "sm-annotationPriority" },
831
+ h("span", null, t("marks.priority")),
832
+ h("select", { className: "sm-viewSelect sm-prioritySelectInline", "aria-label": t("marks.priority"), value: priority, disabled: busy, onChange: e => setPriority(e.target.value) },
833
+ [1,2,3,4,5].map(value => h("option", { key: value, value: String(value) }, t("marks.priority." + value)))
834
+ )
835
+ ),
836
+ h("label", null, h("input", { type: "checkbox", "aria-label": t("marks.review"), checked: reviewLater, disabled: busy, onChange: e => setReviewLater(e.target.checked) }), t("marks.review")),
837
+ h("small", null, t("marks.priority.help"))
838
+ ),
839
+ h("label", { className: "sm-viewField sm-fieldWithClear" }, h("span", null, t("marks.tags")), h("div", { className: "sm-fieldRow sm-fieldRowNote" }, h("textarea", { className: "sm-noteInput", "aria-label": t("marks.tags"), value: tagsText, maxLength: 1000, autoFocus: true, rows: 3, placeholder: t("marks.tags.placeholder"), disabled: busy, onChange: e => setTagsText(e.target.value) }), tagsText && !busy ? h("button", { type: "button", className: "sm-fieldClear sm-fieldClearNote", "aria-label": t("marks.tags.clear"), title: t("marks.tags.clear"), onClick: () => setTagsText("") }, String.fromCharCode(10005)) : null)),
840
+ h("label", { className: "sm-viewField sm-fieldWithClear" }, h("span", null, t("marks.note")), h("div", { className: "sm-fieldRow sm-fieldRowNote" }, h("textarea", { className: "sm-noteInput", "aria-label": t("marks.note"), value: note, maxLength: 2000, rows: 3, placeholder: t("marks.note.placeholder"), disabled: busy, onChange: e => setNote(e.target.value) }), note && !busy ? h("button", { type: "button", className: "sm-fieldClear sm-fieldClearNote", "aria-label": t("marks.note.clear"), title: t("marks.note.clear"), onClick: () => setNote("") }, String.fromCharCode(10005)) : null)),
841
+ latest.revision !== base.revision ? h("div", { className: "sm-markConflict", role: "status" }, t("marks.conflict"), h("button", { type: "button", className: "sm-resetFilters", disabled: busy, onClick: reload }, t("marks.reload"))) : null,
842
+ annotations.error ? h("div", { className: "sm-error", role: "alert" }, t("marks.error", { message: annotations.error }), h("button", { type: "button", className: "sm-resetFilters", disabled: busy || annotations.loading, onClick: () => annotationStore.load(true) }, t("marks.retry"))) : null,
843
+ error ? h("div", { className: "sm-error", role: "alert" }, error) : null
844
+ ),
845
+ h("div", { className: "sm-importGroup" },
846
+ h("div", { className: "sm-importGroupButtons" },
847
+ h("button", { type: "button", className: "sm-importBtn", disabled: busy || !importText.trim(), onClick: pasteFromInput }, t("marks.import.paste")),
848
+ h("button", { type: "button", className: "sm-importBtn", disabled: busy, onClick: copyPrompt }, t("marks.import.copy"))),
849
+ h("div", { className: "sm-fieldWithClear" },
850
+ h("div", { className: "sm-fieldRow sm-fieldRowNote" },
851
+ h("textarea", { className: "sm-noteInput", "aria-label": t("marks.import.paste.aria"), value: importText, placeholder: t("marks.import.paste.placeholder"), disabled: busy, rows: 3, onChange: e => setImportText(e.target.value) }),
852
+ importText && !busy ? h("button", { type: "button", className: "sm-fieldClear sm-fieldClearNote", "aria-label": t("marks.import.paste.clear"), title: t("marks.import.paste.clear"), onClick: () => setImportText("") }, String.fromCharCode(10005)) : null))),
853
+ importStatus ? h("div", { className: "sm-importStatus", role: "status" }, importStatus) : null,
854
+ h("div", { className: "sm-nativeDialogFooter" },
855
+ h("button", { type: "button", className: "sm-nativeDialogButton", disabled: busy, onClick: cancel }, t("marks.cancel")),
856
+ h("button", { type: "button", className: "sm-nativeDialogButton sm-nativeDialogConfirm", disabled: busy || !annotations.loaded || !!annotations.error || latest.revision !== base.revision, onClick: save }, t(busy ? "marks.saving" : "marks.save"))
857
+ )
858
+ )
859
+ );
860
+ }
861
+
862
+
327
863
  const formatRelative = (ts, now) => {
328
- const diff = Math.max(0, now - ts);
864
+ const timestamp = sessionTime(ts);
865
+ if (timestamp === null) return "—";
866
+ const diff = Math.max(0, now - timestamp);
329
867
  const s = Math.floor(diff / 1000);
330
868
  if (s < 60) return s + "s";
331
869
  const m = Math.floor(s / 60);
@@ -382,12 +920,12 @@ window.__ModuleLoader__.load({
382
920
  window.addEventListener("keydown", handler);
383
921
  return () => window.removeEventListener("keydown", handler);
384
922
  }, [open]);
385
- const R = (typeof window !== "undefined") ? window.__smReact : null;
386
- const createPortal = (R && typeof R.createPortal === "function") ? R.createPortal : null;
387
- const body = (typeof document === "undefined") ? null : document.body;
923
+ // Render inline as a sibling of the panel; createPortal to document.body broke under some hosts. Migrate and AnnotationDialog render inline; do the same here.
924
+
925
+
388
926
  if (!open) return null;
389
- if (!body) return tree || null;
390
- const tree = h("div", {
927
+
928
+ return h("div", {
391
929
  className: "sm-nativeDialogLayer sm-confirmDialog",
392
930
  style: props.anchor && typeof props.anchor.top === "number"
393
931
  ? { position: "fixed", top: props.anchor.top + "px", left: props.anchor.left + "px", zIndex: 10001 }
@@ -424,14 +962,6 @@ window.__ModuleLoader__.load({
424
962
  )
425
963
  )
426
964
  );
427
- if (createPortal) return createPortal(tree, body);
428
- // Default: render inline. position:fixed escapes the sidebar
429
- // overflow:hidden ancestor at the DOM/CSS level, so the dialog
430
- // appears next to the panel. Keeping the dialog in the same
431
- // React tree as the panel preserves the onClick wiring (cancel /
432
- // confirm call setConfirmFor, which only works when the click
433
- // handler lives in the same root).
434
- return tree;
435
965
  }
436
966
 
437
967
  function MoveDialog(props) {
@@ -560,8 +1090,8 @@ window.__ModuleLoader__.load({
560
1090
  useEffect(() => {
561
1091
  if (!open || !sessionId) return;
562
1092
  let alive = true;
563
- setScan(null);
564
- setTarget("");
1093
+ // Skip setScan(null)/setTarget("") -- no-ops on initial mount that
1094
+ // would force an extra render cycle, slowing dialog open.
565
1095
  fetchApi("/preset-scan?sessionId=" + encodeURIComponent(sessionId)).then((d) => {
566
1096
  if (!alive) return;
567
1097
  if (d && d.ok) {
@@ -690,6 +1220,46 @@ window.__ModuleLoader__.load({
690
1220
  consumePendingMove() { const v = this.pendingMove; this.pendingMove = null; return v; }
691
1221
  };
692
1222
 
1223
+ const DEFAULT_SORT = "updated-desc";
1224
+ const SORT_ORDERS = ["updated-desc", "updated-asc", "created-desc", "created-asc", "priority"];
1225
+ const workspaceValue = (id) => "workspace:" + id;
1226
+ const pathKey = (value) => {
1227
+ if (typeof value !== "string" || value === "") return "";
1228
+ const path = value.replace(/\\/g, "/").replace(/\/+$/, "") || "/";
1229
+ return /^[a-z]:(?:\/|$)/i.test(path) || path.startsWith("//") ? path.toLowerCase() : path;
1230
+ };
1231
+ const sessionTime = (value) => {
1232
+ if (typeof value !== "number" && typeof value !== "string") return null;
1233
+ if (typeof value === "string" && value.trim() === "") return null;
1234
+ const time = typeof value === "number" || /^\d+(?:\.\d+)?$/.test(value) ? Number(value) : Date.parse(value);
1235
+ return Number.isFinite(time) ? time : null;
1236
+ };
1237
+ const indexWorkspaces = (workspaces) => {
1238
+ const bySession = new Map();
1239
+ const byPath = new Map();
1240
+ const ids = new Set();
1241
+ for (const workspace of workspaces) {
1242
+ ids.add(workspace.id);
1243
+ const key = pathKey(workspace.path);
1244
+ if (key) {
1245
+ if (!byPath.has(key)) byPath.set(key, new Set());
1246
+ byPath.get(key).add(workspace.id);
1247
+ }
1248
+ for (const id of Array.isArray(workspace.sessionIds) ? workspace.sessionIds : []) {
1249
+ if (!bySession.has(id)) bySession.set(id, new Set());
1250
+ bySession.get(id).add(workspace.id);
1251
+ }
1252
+ }
1253
+ return { bySession, byPath, ids };
1254
+ };
1255
+ const rowWorkspaces = (session, index) => {
1256
+ // Registry membership includes archived sessions and takes precedence
1257
+ // over a stale cwd. Older metadata can fall back to workspaceId/cwd.
1258
+ if (index.bySession.has(session.id)) return index.bySession.get(session.id);
1259
+ if (index.ids.has(session.workspaceId)) return new Set([session.workspaceId]);
1260
+ return index.byPath.get(pathKey(session.cwd)) ?? new Set();
1261
+ };
1262
+
693
1263
  function SessionManagerPanel(props) {
694
1264
  const t = props.t;
695
1265
  const open = props.open;
@@ -702,17 +1272,34 @@ window.__ModuleLoader__.load({
702
1272
  const onRemove = props.onRemove;
703
1273
  const onMove = props.onMove;
704
1274
  const onMigrate = props.onMigrate;
1275
+ const annotations = useAnnotations(true);
1276
+ const marksReady = annotations.loaded && !annotations.error;
1277
+ const [onlyFavorite, setOnlyFavorite] = useState(false);
1278
+ const [onlyReview, setOnlyReview] = useState(false);
1279
+ const [tagFilter, setTagFilter] = useState("all");
1280
+ const [priorityFilter, setPriorityFilter] = useState("all");
1281
+ const [annotationFor, setAnnotationFor] = useState(null);
1282
+ const [annotationAnchor, setAnnotationAnchor] = useState(null);
705
1283
  const list = useSessions((s) => s);
706
- const archivedIds = useWorkspaces((s) => s.archivedSessionIds);
1284
+ const workspaceSnapshot = useWorkspaces((s) => s);
1285
+ const archivedIds = workspaceSnapshot.archivedSessionIds || [];
707
1286
  const [filter, setFilter] = useState("all");
1287
+ const [query, setQuery] = useState("");
1288
+ const [workspaceFilter, setWorkspaceFilter] = useState("all");
1289
+ const [sortOrder, setSortOrder] = useState(DEFAULT_SORT);
708
1290
  const [busyId, setBusyId] = useState(null);
709
1291
  const [error, setError] = useState(null);
710
1292
  const [now, setNow] = useState(() => Date.now());
711
1293
  const [moveFor, setMoveFor] = useState(null);
712
1294
  const [confirmFor, setConfirmFor] = useState(null);
713
1295
  const [workspaces, setWorkspaces] = useState([]);
1296
+ const [createdTimes, setCreatedTimes] = useState({});
1297
+ const [workspacesLoading, setWorkspacesLoading] = useState(true);
1298
+ const [workspacesError, setWorkspacesError] = useState(null);
1299
+ const [workspaceRefresh, setWorkspaceRefresh] = useState(0);
714
1300
  const [migrating, setMigrating] = useState(false);
715
1301
  const [migrateFor, setMigrateFor] = useState(null);
1302
+ const sessionMembershipKey = useMemo(() => [...list.ids].sort().join("\0"), [list.ids]);
716
1303
  useEffect(() => {
717
1304
  const timer = setInterval(() => setNow(Date.now()), 30000);
718
1305
  return () => clearInterval(timer);
@@ -720,11 +1307,24 @@ window.__ModuleLoader__.load({
720
1307
  useEffect(() => {
721
1308
  if (!open) return;
722
1309
  let alive = true;
723
- fetchApi("/workspaces").then((data) => {
724
- if (alive && data.ok) setWorkspaces(data.result.workspaces || []);
1310
+ const controller = new AbortController();
1311
+ setWorkspacesLoading(true);
1312
+ setWorkspacesError(null);
1313
+ fetchApi("/workspaces", { signal: controller.signal }).then((data) => {
1314
+ if (!data?.ok || !Array.isArray(data.result?.workspaces)) throw new Error(data?.error || "Invalid workspace response");
1315
+ const next = data.result.workspaces.filter(workspace => workspace && typeof workspace.id === "string");
1316
+ if (!alive) return;
1317
+ setWorkspaces(next);
1318
+ const times = data.result.sessionCreatedAt;
1319
+ setCreatedTimes(times && typeof times === "object" && !Array.isArray(times) ? times : {});
1320
+ setWorkspaceFilter(value => value.startsWith("workspace:") && !next.some(workspace => workspaceValue(workspace.id) === value) ? "all" : value);
1321
+ }).catch(error => {
1322
+ if (alive) setWorkspacesError(error instanceof Error ? error.message : String(error));
1323
+ }).finally(() => {
1324
+ if (alive) setWorkspacesLoading(false);
725
1325
  });
726
- return () => { alive = false; };
727
- }, [open]);
1326
+ return () => { alive = false; controller.abort(); };
1327
+ }, [open, workspaceSnapshot, Boolean(moveFor), workspaceRefresh, sessionMembershipKey]);
728
1328
  // Drain any pending action queued by a header button. The panel is
729
1329
  // the single owner of every inner dialog, so opening it with a
730
1330
  // pending action pre-populates the corresponding dialog state.
@@ -735,22 +1335,57 @@ window.__ModuleLoader__.load({
735
1335
  setMoveFor({ id: pending.sessionId, displayTitle: pending.displayTitle || pending.sessionId, workspaceId: pending.workspaceId || "" });
736
1336
  }
737
1337
  }, [open]);
738
- useEffect(() => {
739
- if (moveFor) {
740
- fetchApi("/workspaces").then((data) => {
741
- if (data.ok) setWorkspaces(data.result.workspaces || []);
742
- });
743
- }
744
- }, [moveFor]);
745
1338
  const archived = useMemo(() => new Set(archivedIds), [archivedIds]);
1339
+ const workspaceIndex = useMemo(() => indexWorkspaces(workspaces), [workspaces]);
1340
+ const allRows = useMemo(() => list.ids.map(id => list.byId[id])
1341
+ .filter(session => session != null && session.origin !== "subagent" && (!session.blank || session.id === list.current)), [list]);
1342
+ const availableTags = useMemo(() => {
1343
+ const tags = new Map();
1344
+ for (const session of allRows) for (const tag of annotationOf(annotations, session.id).tags) if (!tags.has(tag.toLowerCase())) tags.set(tag.toLowerCase(), tag);
1345
+ return [...tags].sort((a, b) => a[1].localeCompare(b[1]));
1346
+ }, [allRows, annotations.entries]);
1347
+ useEffect(() => {
1348
+ // A removed last tag must not leave an invisible active filter while
1349
+ // the select falls back to displaying "All tags".
1350
+ if (annotations.loaded && !annotations.error && tagFilter.startsWith("tag:")
1351
+ && !availableTags.some(([key]) => key === tagFilter.slice(4))) setTagFilter("all");
1352
+ }, [availableTags, tagFilter, annotations.loaded, annotations.error]);
746
1353
  const rows = useMemo(() => {
747
- const all = list.ids
748
- .map((id) => list.byId[id])
749
- .filter((s) => s !== undefined && s.origin !== "subagent" && (!s.blank || s.id === list.current));
750
- if (filter === "active") return all.filter((s) => !archived.has(s.id));
751
- if (filter === "archived") return all.filter((s) => archived.has(s.id));
752
- return all;
753
- }, [list, archived, filter]);
1354
+ const needle = query.trim().toLowerCase();
1355
+ const selected = allRows.filter(session => {
1356
+ if (filter === "active" && archived.has(session.id)) return false;
1357
+ if (filter === "archived" && !archived.has(session.id)) return false;
1358
+ const mark = annotationOf(annotations, session.id);
1359
+ if (onlyFavorite && !mark.favorite || onlyReview && !mark.reviewLater) return false;
1360
+ if (tagFilter === "none" && mark.tags.length !== 0) return false;
1361
+ if (tagFilter.startsWith("tag:") && !mark.tags.some(tag => tag.toLowerCase() === tagFilter.slice(4))) return false;
1362
+ if (priorityFilter !== "all" && (mark.priority ?? 3) !== Number(priorityFilter)) return false;
1363
+ const title = session.blank ? t("panel.session.new") : session.displayTitle || session.id;
1364
+ if (needle && ![title, session.id, mark.note, ...mark.tags].some(value => value.toLowerCase().includes(needle))) return false;
1365
+ if (workspaceFilter !== "all") {
1366
+ const memberships = rowWorkspaces(session, workspaceIndex);
1367
+ if (workspaceFilter === "ungrouped" ? memberships.size !== 0 : !memberships.has(workspaceFilter.slice("workspace:".length))) return false;
1368
+ }
1369
+ return true;
1370
+ });
1371
+ const field = sortOrder.startsWith("created-") ? "createdAt" : "updatedAt";
1372
+ const direction = sortOrder === "priority" || sortOrder.endsWith("-asc") ? 1 : -1;
1373
+ // Sort a derived array only. Equal timestamps preserve source order;
1374
+ // missing/invalid timestamps stay last in either direction. Priority
1375
+ // is normalized to 1-5 (legacy null treated as 3) so unset rows sort
1376
+ // alongside priority 3.
1377
+ const timeFor = session => {
1378
+ if (sortOrder === "priority") return annotationOf(annotations, session.id).priority ?? 3;
1379
+ return field === "createdAt" ? sessionTime(session.createdAt) ?? sessionTime(createdTimes[session.id]) : sessionTime(session.updatedAt);
1380
+ };
1381
+ return selected.map((session, rank) => ({ session, rank, time: timeFor(session) }))
1382
+ .sort((a, b) => a.time === b.time ? a.rank - b.rank : a.time === null ? 1 : b.time === null ? -1 : direction * (a.time - b.time))
1383
+ .map(item => item.session);
1384
+ }, [allRows, archived, filter, query, workspaceFilter, workspaceIndex, sortOrder, createdTimes, annotations.entries, onlyFavorite, onlyReview, tagFilter, priorityFilter, t]);
1385
+ const creationTimeUnavailable = sortOrder.startsWith("created-") && allRows.length > 0
1386
+ && !allRows.some(session => (sessionTime(session.createdAt) ?? sessionTime(createdTimes[session.id])) !== null);
1387
+ const hasViewChanges = onlyFavorite || onlyReview || tagFilter !== "all" || priorityFilter !== "all" || query !== "" || filter !== "all" || workspaceFilter !== "all" || sortOrder !== DEFAULT_SORT;
1388
+ const resetView = () => { setQuery(""); setFilter("all"); setWorkspaceFilter("all"); setSortOrder(DEFAULT_SORT); setOnlyFavorite(false); setOnlyReview(false); setTagFilter("all"); setPriorityFilter("all"); };
754
1389
  const runRow = async (sessionId, fn) => {
755
1390
  if (busyId !== null) return;
756
1391
  setBusyId(sessionId);
@@ -767,14 +1402,17 @@ window.__ModuleLoader__.load({
767
1402
  type: "button",
768
1403
  key: id,
769
1404
  className: "sm-filterBtn" + (filter === id ? " sm-on" : ""),
1405
+ "aria-pressed": filter === id,
770
1406
  onClick: () => setFilter(id)
771
1407
  }, label);
772
1408
  const renderRow = (s) => {
773
1409
  const isArchived = archived.has(s.id);
774
1410
  const isCurrent = s.id === list.current;
775
1411
  const busy = busyId === s.id;
1412
+ const mark = annotationOf(annotations, s.id);
776
1413
  return h("div", {
777
1414
  key: s.id,
1415
+ "data-session-id": s.id,
778
1416
  className: "sm-row" + (isCurrent ? " sm-rowCurrent" : "")
779
1417
  },
780
1418
  h("div", { className: "sm-rowMain" },
@@ -784,11 +1422,18 @@ window.__ModuleLoader__.load({
784
1422
  s.running ? h("span", { className: "sm-badge sm-badgeRunning" }, t("panel.running")) : null,
785
1423
  isCurrent ? h("span", { className: "sm-badge sm-badgeCurrent" }, t("panel.current")) : null
786
1424
  ),
1425
+ mark.priority !== null || mark.tags.length || mark.note ? h("div", { className: "sm-rowAnnotations" },
1426
+ h("span", { className: "sm-priorityBadge sm-priority-" + (mark.priority ?? 3), title: t("marks.priority." + (mark.priority ?? 3)) }, "P" + (mark.priority ?? 3)),
1427
+ mark.tags.slice(0, 2).map(tag => h("button", { key: tag, type: "button", className: "sm-tagChip", title: tag, onClick: () => setTagFilter("tag:" + tag.toLowerCase()) }, tag)),
1428
+ mark.tags.length > 2 ? h("span", { title: mark.tags.join(", ") }, "+" + (mark.tags.length - 2)) : null,
1429
+ mark.note ? h("button", { type: "button", className: "sm-noteBadge", title: t("marks.note.exists"), onClick: () => { setAnnotationAnchor(null); setAnnotationFor(current => current?.id === s.id ? null : s); } }, "✎") : null
1430
+ ) : null,
787
1431
  h("div", { className: "sm-rowMeta" }, shortPath(s.cwd) + (s.cwd ? " · " : "") + formatRelative(s.updatedAt, now))
788
1432
  ),
789
1433
  h("div", { className: "sm-rowActions" },
790
1434
  h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => onOpen(s.id) }, t("row.open")),
791
1435
  h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => runRow(s.id, () => isArchived ? onUnarchive(s.id) : onArchive(s.id)) }, t(isArchived ? "row.unarchive" : "row.archive")),
1436
+ 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),
792
1437
  h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => { if (moveFor && moveFor.id === s.id) setMoveFor(null); else setMoveFor(s); } }, t("row.move")),
793
1438
  h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => { if (migrateFor && migrateFor.id === s.id) setMigrateFor(null); else setMigrateFor(s); } }, t("row.migrate")),
794
1439
  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"))
@@ -796,7 +1441,7 @@ window.__ModuleLoader__.load({
796
1441
  );
797
1442
  };
798
1443
  const rowsList = rows.length === 0
799
- ? h("div", { className: "sm-empty" }, filter === "archived" ? t("panel.empty.archived") : t("panel.empty"))
1444
+ ? h("div", { className: "sm-empty" }, allRows.length > 0 ? t("panel.empty.filtered") : filter === "archived" ? t("panel.empty.archived") : t("panel.empty"))
800
1445
  : h("div", { className: "sm-list" }, rows.map(renderRow));
801
1446
  const onPanelClose = () => {
802
1447
  if (busyId !== null) return;
@@ -814,15 +1459,16 @@ window.__ModuleLoader__.load({
814
1459
  // returns early when any inner dialog is open so the child
815
1460
  // dialog's own window listener gets the first shot at Esc.
816
1461
  const escStateRef = useRef({});
817
- escStateRef.current = { busyId, confirmFor, moveFor, migrateFor, onPanelClose };
1462
+ escStateRef.current = { busyId, confirmFor, moveFor, migrateFor, annotationFor, onPanelClose };
818
1463
  useEffect(() => {
819
1464
  if (!open) return;
820
1465
  const handler = (e) => {
821
1466
  if (e.key !== "Escape") return;
822
1467
  if (e.defaultPrevented) return;
1468
+ if (e.isComposing || e.keyCode === 229) return;
823
1469
  const s = escStateRef.current;
824
1470
  if (s.busyId !== null) return;
825
- if (s.confirmFor || s.moveFor || s.migrateFor) return;
1471
+ if (s.confirmFor || s.moveFor || s.migrateFor || s.annotationFor) return;
826
1472
  e.preventDefault();
827
1473
  e.stopPropagation();
828
1474
  s.onPanelClose();
@@ -858,16 +1504,72 @@ window.__ModuleLoader__.load({
858
1504
  }, t("panel.close"))
859
1505
  ),
860
1506
  h("div", { className: "sm-panel" },
861
- h("div", { className: "sm-filter" },
862
- filterBtn("all", t("panel.filter.all")),
863
- filterBtn("active", t("panel.filter.active")),
864
- filterBtn("archived", t("panel.filter.archived"))
1507
+ h("div", { className: "sm-panelTools" },
1508
+ h("input", {
1509
+ type: "search", className: "sm-queryInput", value: query,
1510
+ placeholder: t("panel.search"), "aria-label": t("panel.search"), autoComplete: "off",
1511
+ onChange: event => setQuery(event.target.value)
1512
+ }),
1513
+ h("div", { className: "sm-panelSelects" },
1514
+ h("label", { className: "sm-viewField" },
1515
+ h("span", null, t("panel.workspace")),
1516
+ h("select", {
1517
+ className: "sm-viewSelect", "aria-label": t("panel.workspace"), value: workspaceFilter,
1518
+ disabled: workspacesLoading || workspacesError !== null,
1519
+ onChange: event => setWorkspaceFilter(event.target.value)
1520
+ },
1521
+ h("option", { value: "all" }, t("panel.workspace.all")),
1522
+ h("option", { value: "ungrouped" }, t("panel.workspace.ungrouped")),
1523
+ workspaces.map(workspace => h("option", { key: workspace.id, value: workspaceValue(workspace.id), title: workspace.path || workspace.id }, workspace.title || workspace.name || workspace.id))
1524
+ )
1525
+ ),
1526
+ h("label", { className: "sm-viewField" },
1527
+ h("span", null, t("panel.sort")),
1528
+ h("select", {
1529
+ className: "sm-viewSelect", "aria-label": t("panel.sort"), value: sortOrder,
1530
+ onChange: event => setSortOrder(event.target.value)
1531
+ }, SORT_ORDERS.map(value => h("option", { key: value, value, disabled: value === "priority" && !marksReady }, t(value === "priority" ? "marks.sort" : "panel.sort." + value))))
1532
+ )
1533
+ ),
1534
+ h("div", { className: "sm-markFilters" },
1535
+ h("button", { type: "button", className: "sm-filterBtn sm-markFilterToggle" + (onlyFavorite ? " sm-on" : ""), "aria-label": t("marks.filter.favorite.aria"), "aria-pressed": onlyFavorite, disabled: !marksReady, onClick: () => setOnlyFavorite(value => !value) }, "★ " + t("marks.filter.favorite")),
1536
+ h("button", { type: "button", className: "sm-filterBtn sm-markFilterToggle" + (onlyReview ? " sm-on" : ""), "aria-label": t("marks.filter.review.aria"), "aria-pressed": onlyReview, disabled: !marksReady, onClick: () => setOnlyReview(value => !value) }, "◷ " + t("marks.filter.review")),
1537
+ h("label", { className: "sm-viewField" }, h("span", null, t("marks.tags")), h("select", { className: "sm-viewSelect", "aria-label": t("marks.filter.tags"), value: tagFilter, disabled: !marksReady, onChange: e => setTagFilter(e.target.value) },
1538
+ h("option", { value: "all" }, t("marks.tags.all")), h("option", { value: "none" }, t("marks.tags.none")),
1539
+ availableTags.map(([key, tag]) => h("option", { key, value: "tag:" + key }, tag)),
1540
+ tagFilter.startsWith("tag:") && !availableTags.some(([key]) => "tag:" + key === tagFilter) ? h("option", { value: tagFilter }, t("marks.tags.missing", { tag: tagFilter.slice(4) })) : null
1541
+ )),
1542
+ h("label", { className: "sm-viewField" }, h("span", null, t("marks.priority")), h("select", { className: "sm-viewSelect", "aria-label": t("marks.filter.priority"), value: priorityFilter, disabled: !marksReady, onChange: e => setPriorityFilter(e.target.value) },
1543
+ h("option", { value: "all" }, t("marks.priority.all")),
1544
+ [1,2,3,4,5].map(value => h("option", { key: value, value: String(value) }, t("marks.priority." + value)))
1545
+ ))
1546
+ ),
1547
+ h("div", { className: "sm-filterBar" },
1548
+ h("div", { className: "sm-filter" },
1549
+ filterBtn("all", t("panel.filter.all")),
1550
+ filterBtn("active", t("panel.filter.active")),
1551
+ filterBtn("archived", t("panel.filter.archived"))
1552
+ ),
1553
+ h("div", { className: "sm-resultSummary" },
1554
+ h("span", { role: "status", "aria-live": "polite", "aria-atomic": "true" }, t("panel.count", { shown: rows.length, total: allRows.length })),
1555
+ hasViewChanges ? h("button", { type: "button", className: "sm-resetFilters", onClick: resetView }, t("panel.reset")) : null
1556
+ )
1557
+ ),
1558
+ annotations.loading && !annotations.loaded ? h("div", { className: "sm-workspaceNotice", role: "status" }, t("marks.loading")) : null,
1559
+ annotations.error ? h("div", { className: "sm-workspaceNotice", role: "alert" }, t("marks.error", { message: annotations.error }), h("button", { type: "button", className: "sm-resetFilters", onClick: () => annotationStore.load(true) }, t("marks.retry"))) : null,
1560
+ creationTimeUnavailable && !workspacesLoading ? h("div", { className: "sm-workspaceNotice", role: "status" }, t("panel.sort.unavailable")) : null,
1561
+ workspacesLoading ? h("div", { className: "sm-workspaceNotice", role: "status" }, t("panel.workspace.loading")) : null,
1562
+ workspacesError ? h("div", { className: "sm-workspaceNotice", role: "alert" },
1563
+ h("span", null, t("panel.workspace.error", { message: workspacesError })),
1564
+ h("button", { type: "button", className: "sm-resetFilters", onClick: () => setWorkspaceRefresh(value => value + 1) }, t("panel.workspace.retry"))
1565
+ ) : null
865
1566
  ),
866
1567
  error ? h("div", { className: "sm-error", role: "alert" }, t("error.operation", { message: error })) : null,
867
1568
  rowsList
868
1569
  )
869
1570
  )
870
1571
  );
1572
+
871
1573
  const deleteDialog = confirmFor === null ? null : h(ConfirmDialog, {
872
1574
  open: true,
873
1575
  onClose: () => { if (busyId === null) setConfirmFor(null); },
@@ -899,7 +1601,10 @@ window.__ModuleLoader__.load({
899
1601
  onConfirm: (targetWorkspaceId) => {
900
1602
  const target = moveFor;
901
1603
  setMoveFor(null);
902
- runRow(target.id, () => onMove(target.id, targetWorkspaceId));
1604
+ runRow(target.id, async () => {
1605
+ await onMove(target.id, targetWorkspaceId);
1606
+ setWorkspaceRefresh(value => value + 1);
1607
+ });
903
1608
  }
904
1609
  });
905
1610
  const migrateDialog = migrateFor === null ? null : h(MigratePresetDialog, {
@@ -932,13 +1637,18 @@ window.__ModuleLoader__.load({
932
1637
  // (z-index 9999). Clicking inside the panel that the inner dialog
933
1638
  // blocks is not possible because the inner backdrop covers the
934
1639
  // panel area entirely (the inner modal is centered).
935
- const primary = moveDialog || deleteDialog || migrateDialog;
936
- return h(React.Fragment, null, panelModal, primary);
1640
+ const annotationDialog = annotationFor ? h(AnnotationDialog, { key: annotationFor.id, sessionId: annotationFor.id, displayTitle: annotationFor.displayTitle || annotationFor.id, t, anchor: annotationAnchor, onClose: () => { setAnnotationFor(null); setAnnotationAnchor(null); } }) : null;
1641
+ // Render panel + ALL inner dialogs together; z-index ordering puts
1642
+ // whichever dialog is open above the panel.
1643
+ return h(React.Fragment, null, panelModal, annotationDialog, moveDialog, deleteDialog, migrateDialog);
937
1644
  }
938
1645
 
939
- function HeaderAction(props) {
1646
+ function HeaderAction(props) {
940
1647
  const t = props.t;
941
1648
  const sessionId = props.sessionId;
1649
+ const annotations = useAnnotations();
1650
+ const [annotationFor, setAnnotationFor] = useState(null);
1651
+ const [annotationAnchor, setAnnotationAnchor] = useState(null);
942
1652
  // Slot framework spreads inject-factory return into top-level props,
943
1653
  // so the action callbacks live directly on props (not under .actions).
944
1654
  const onArchive = props.onArchive, onUnarchive = props.onUnarchive;
@@ -982,6 +1692,7 @@ window.__ModuleLoader__.load({
982
1692
  };
983
1693
  const archiveLabelKey = isArchived ? "row.unarchive" : "row.archive";
984
1694
  return h(React.Fragment, null,
1695
+ annotationFor ? h(AnnotationDialog, { key: annotationFor.id, sessionId: annotationFor.id, displayTitle: annotationFor.displayTitle, t, anchor: annotationAnchor, onClose: () => { setAnnotationFor(null); setAnnotationAnchor(null); } }) : null,
985
1696
  h("div", { className: "sm-header" },
986
1697
  h("button", {
987
1698
  type: "button",
@@ -991,6 +1702,18 @@ window.__ModuleLoader__.load({
991
1702
  isArchived ? onUnarchive(sessionId) : onArchive(sessionId)
992
1703
  )
993
1704
  }, isArchived ? (t ? t("row.unarchive") : "移出归档") : (t ? t("row.archive") : "归档")),
1705
+
1706
+ annotationButtons(sessionId, props.displayTitle || sessionId, t, annotations, "sm-headerBtn", patch => runWithAlert("annotations", () => annotationStore.save(sessionId, patch)), (e) => {
1707
+ const nextOpen = !(annotationFor && annotationFor.id === sessionId);
1708
+ setMoveFor(null); setConfirmFor(null);
1709
+ if (nextOpen && e && e.currentTarget) {
1710
+ const r = e.currentTarget.getBoundingClientRect();
1711
+ setAnnotationAnchor({ top: Math.round(r.top + r.height + 6), left: Math.max(8, Math.round(r.right - 460)) });
1712
+ } else setAnnotationAnchor(null);
1713
+ setAnnotationFor(current => current?.id === sessionId ? null : { id: sessionId, displayTitle: props.displayTitle || sessionId });
1714
+ }, false, annotationFor?.id === sessionId),
1715
+ annotations.error ? h("button", { type: "button", className: "sm-headerBtn", title: t("marks.error", { message: annotations.error }), "aria-label": t("marks.retry"), onClick: () => annotationStore.load(true) }, "!") : null,
1716
+
994
1717
  h("button", {
995
1718
  type: "button",
996
1719
  className: "sm-headerBtn" + (moveFor ? " sm-headerBtnActive" : ""),
@@ -1068,7 +1791,7 @@ window.__ModuleLoader__.load({
1068
1791
 
1069
1792
  class SafePanel extends React.Component {
1070
1793
  constructor(p) { super(p); this.state = { failed: false }; }
1071
- getDerivedStateFromError() { return { failed: true }; }
1794
+ static getDerivedStateFromError() { return { failed: true }; }
1072
1795
  componentDidCatch(error) { try { console.error("[dsh-session-manager] SessionManagerPanel crashed:", error); } catch (_) {} }
1073
1796
  render() {
1074
1797
  if (this.state.failed) return null;
@@ -1115,9 +1838,41 @@ window.__ModuleLoader__.load({
1115
1838
  }
1116
1839
 
1117
1840
  function apply(ctx) {
1841
+ ctx.effect(() => {
1842
+ const refresh = () => { if (annotationStore.hasSubscribers()) void annotationStore.load(true); };
1843
+ window.addEventListener("focus", refresh);
1844
+ let channel;
1845
+ try {
1846
+ if (typeof BroadcastChannel === "function") {
1847
+ channel = new BroadcastChannel("dsh-session-manager-annotations-v1");
1848
+ channel.onmessage = event => { if (event.data?.type === "changed") refresh(); };
1849
+ annotationStore.notify = () => channel.postMessage({ type: "changed" });
1850
+ }
1851
+ } catch (_) { /* unavailable browser messaging: focus refresh still works */ }
1852
+ return () => { window.removeEventListener("focus", refresh); channel?.close(); annotationStore.notify = null; };
1853
+ }, "session-manager: annotation synchronization");
1118
1854
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "session-manager: dictionaries");
1119
1855
  const actions = () => ({
1120
- onOpen: (sessionId) => { ctx.sessions.open(sessionId); },
1856
+ // DSH 0.1.6+ removed ctx.sessions.open(); the navigation owner is
1857
+ // `ctx.uiWorkspace.openSession()`, and the bare SessionController only
1858
+ // exposes `retain()`. Pick the live surface so the row's "Open" button
1859
+ // actually navigates on every supported host build.
1860
+ onOpen: (sessionId) => {
1861
+ const uiWorkspace = ctx.uiWorkspace;
1862
+ if (uiWorkspace && typeof uiWorkspace.openSession === "function") {
1863
+ uiWorkspace.openSession(sessionId);
1864
+ return;
1865
+ }
1866
+ const sessions = ctx.sessions;
1867
+ if (sessions && typeof sessions.retain === "function") {
1868
+ // The bare retain() returns a SessionReference whose release()
1869
+ // tears down the in-memory binding; the user normally expects the
1870
+ // session to stay open, so a header-less build simply keeps it.
1871
+ sessions.retain(sessionId, { source: "mainView" });
1872
+ return;
1873
+ }
1874
+ throw new Error("session-manager: no Session open API on this DSH host");
1875
+ },
1121
1876
  onArchive: async (sessionId) => { await ctx.workspaces.archiveSession(sessionId); },
1122
1877
  onUnarchive: async (sessionId) => {
1123
1878
  const response = await callApi("/unarchive", { sessionId });
@@ -1126,6 +1881,8 @@ window.__ModuleLoader__.load({
1126
1881
  onRemove: async (sessionId) => {
1127
1882
  const response = await callApi("/delete", { sessionId });
1128
1883
  if (!response.ok) throw new Error(response.error || "delete failed");
1884
+ await annotationStore.load(true);
1885
+ if (response.result?.warning) window.alert(response.result.warning);
1129
1886
  const current = ctx.sessions.list.getSnapshot().current;
1130
1887
  if (current === sessionId) ctx.sessions.clear();
1131
1888
  },
@@ -1187,7 +1944,7 @@ window.__ModuleLoader__.load({
1187
1944
  }, FooterAction));
1188
1945
  }
1189
1946
 
1190
- const inject = ["slots", "sessions", "workspaces", "locale"];
1947
+ const inject = ["slots", "sessions", "workspaces", "locale", "uiWorkspace"];
1191
1948
 
1192
1949
  exports.apply = apply;
1193
1950
  exports.inject = inject;