dsh-session-manager 0.4.10 → 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/CHANGELOG.md +218 -185
- package/README.md +158 -109
- package/README.zh.md +84 -29
- package/lib/annotation-store.js +159 -0
- package/lib/client.js +811 -54
- package/lib/clipboard-parser.js +208 -0
- package/lib/compat/dsh-adapter.js +17 -0
- package/lib/compat/zstd-frames.js +2 -2
- package/lib/index.js +1111 -1247
- package/lib/session-files.js +180 -0
- package/package.json +10 -3
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
|
|
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
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
724
|
-
|
|
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
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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, () =>
|
|
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
|
|
936
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|