dsh-session-manager 0.1.2 → 0.4.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/README.md +102 -100
- package/README.zh.md +77 -75
- package/lib/client.js +853 -348
- package/lib/index.js +1043 -5
- package/package.json +8 -4
package/lib/client.js
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @dsh-session-manager — Client half.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 2. sidebar.footer.action — a "会话管理" footer button that opens the full
|
|
9
|
-
* management panel (all sessions, archived filter, per-row open / archive /
|
|
10
|
-
* unarchive / delete with confirmation).
|
|
11
|
-
*
|
|
12
|
-
* Archive uses the built-in `workspaces.archiveSession` wire API; unarchive and
|
|
13
|
-
* delete go through this plugin's own host HTTP endpoints
|
|
14
|
-
* (/session-manager/api/unarchive, /session-manager/api/delete).
|
|
15
|
-
*
|
|
16
|
-
* Written as the loader's factory-form bundle by hand (no build step), using
|
|
17
|
-
* React.createElement instead of JSX so the file ships as-is.
|
|
3
|
+
* v0.3.0 — replaces 0.2.1 client.js which had V8 strict-mode parse errors.
|
|
4
|
+
* Functionally equivalent: header actions (open/archive/move/delete/manage),
|
|
5
|
+
* footer action (open session manager panel), and the panel itself with
|
|
6
|
+
* delete/move confirm dialogs. The "preset migration" feature is registered
|
|
7
|
+
* as a separate slot in a sibling client bundle.
|
|
18
8
|
*/
|
|
19
9
|
window.__ModuleLoader__.load({
|
|
20
10
|
id: "dsh-session-manager",
|
|
@@ -24,79 +14,39 @@ window.__ModuleLoader__.load({
|
|
|
24
14
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
25
15
|
|
|
26
16
|
const React = require("react");
|
|
27
|
-
const {
|
|
28
|
-
useState,
|
|
29
|
-
useEffect,
|
|
30
|
-
useMemo,
|
|
31
|
-
useRef,
|
|
32
|
-
useSyncExternalStore
|
|
33
|
-
} = React;
|
|
17
|
+
const { useState, useEffect, useMemo, useRef, useSyncExternalStore } = React;
|
|
34
18
|
const P = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
19
|
+
if (typeof window !== "undefined") {
|
|
20
|
+
window.__smReact = React;
|
|
21
|
+
window.__smReactDOM = React.createPortal ? React : null;
|
|
22
|
+
// Tiny imperative portal helper: takes a React element + a host
|
|
23
|
+
// DOM node, creates a React root on the host (lazily), and re-renders
|
|
24
|
+
// the element into it on each call. Used by ConfirmDialog to escape
|
|
25
|
+
// the sidebar overflow:hidden ancestor.
|
|
26
|
+
try {
|
|
27
|
+
const RDOM = require("react-dom/client");
|
|
28
|
+
if (RDOM && typeof RDOM.createRoot === "function") {
|
|
29
|
+
window.__smRenderInto = (tree, host) => {
|
|
30
|
+
if (!tree || !host) return;
|
|
31
|
+
try {
|
|
32
|
+
if (!host.__smRoot) host.__smRoot = RDOM.createRoot(host);
|
|
33
|
+
host.__smRoot.render(tree);
|
|
34
|
+
} catch (_) { /* ignore render failure */ }
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
} catch (_) { /* react-dom/client not available */ }
|
|
38
|
+
}
|
|
35
39
|
|
|
36
40
|
const NS = "session-manager";
|
|
37
41
|
const API = "/session-manager/api";
|
|
38
42
|
|
|
39
|
-
// ---------------------------------------------------------------------
|
|
40
|
-
// Styles (theme-token driven; matches the dsh web design language)
|
|
41
|
-
// ---------------------------------------------------------------------
|
|
42
|
-
const css = [
|
|
43
|
-
".sm-header{display:flex;align-items:center;gap:6px}",
|
|
44
|
-
".sm-headerBtn{box-sizing:border-box;min-height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;align-items:center;gap:4px;padding:3px 10px;font-size:12px;line-height:18px;display:inline-flex;white-space:nowrap}",
|
|
45
|
-
".sm-headerBtn:hover:not(:disabled),.sm-headerBtn:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
46
|
-
".sm-headerBtn:disabled{opacity:.5;cursor:not-allowed}",
|
|
47
|
-
".sm-headerBtn svg{flex:none}",
|
|
48
|
-
".sm-headerDanger{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}",
|
|
49
|
-
".sm-headerDanger:hover:not(:disabled){background:rgba(217,78,68,.1)}",
|
|
50
|
-
".sm-busy{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:28px;margin-left:4px}",
|
|
51
|
-
".sm-footer{display:flex}",
|
|
52
|
-
".sm-footerBtn{min-height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:6px;align-items:center;gap:6px;padding:3px 8px;font-size:12px;line-height:18px;display:inline-flex}",
|
|
53
|
-
".sm-footerBtn:hover,.sm-footerBtn:focus-visible{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
54
|
-
".sm-footerBtn svg{flex:none}",
|
|
55
|
-
".sm-panel{display:flex;flex-direction:column;gap:10px;min-height:280px;max-height:min(60vh,560px)}",
|
|
56
|
-
".sm-filter{display:flex;gap:4px;flex:none}",
|
|
57
|
-
".sm-filterBtn{cursor:pointer;color:var(--dsw-alias-label-secondary);background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:3px 12px;font-size:12px;line-height:18px}",
|
|
58
|
-
".sm-filterBtn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
59
|
-
".sm-filterBtn.sm-on{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-fill-l2);border-color:transparent}",
|
|
60
|
-
".sm-list{display:flex;flex-direction:column;gap:4px;min-height:0;overflow:auto;flex:1}",
|
|
61
|
-
".sm-row{box-sizing:border-box;display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px}",
|
|
62
|
-
".sm-row:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
63
|
-
".sm-rowCurrent{border-color:var(--dsw-alias-accent-primary,var(--dsw-alias-label-secondary))}",
|
|
64
|
-
".sm-rowMain{min-width:0;display:flex;flex-direction:column;gap:2px;flex:1}",
|
|
65
|
-
".sm-rowTitle{color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px;display:flex;align-items:center;gap:6px;min-width:0}",
|
|
66
|
-
".sm-rowTitle>span:first-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}",
|
|
67
|
-
".sm-rowMeta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}",
|
|
68
|
-
".sm-rowActions{display:flex;gap:4px;flex:none}",
|
|
69
|
-
".sm-badge{flex:none;border-radius:999px;padding:0 6px;font-size:10px;line-height:16px}",
|
|
70
|
-
".sm-badgeArchived{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l2)}",
|
|
71
|
-
".sm-badgeRunning{color:var(--dsw-alias-state-warning-primary,#d97706);background:rgba(217,119,6,.12)}",
|
|
72
|
-
".sm-badgeCurrent{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-fill-l2)}",
|
|
73
|
-
".sm-danger{color:var(--dsw-alias-state-error-primary) !important;border-color:var(--dsw-alias-state-error-primary) !important}",
|
|
74
|
-
".sm-dangerText{color:var(--dsw-alias-state-error-primary) !important}",
|
|
75
|
-
".sm-warn{display:flex;align-items:flex-start;gap:8px;color:var(--dsw-alias-label-secondary);background:rgba(217,119,6,.1);border:1px solid rgba(217,119,6,.35);border-radius:8px;padding:8px 10px;font-size:12px;line-height:18px}",
|
|
76
|
-
".sm-warn svg{flex:none;margin-top:1px}",
|
|
77
|
-
".sm-error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;min-height:18px}",
|
|
78
|
-
".sm-foot{display:flex;justify-content:flex-end;align-items:center;gap:8px;width:100%}",
|
|
79
|
-
".sm-footError{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;margin-right:auto}",
|
|
80
|
-
".sm-empty{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px;text-align:center;padding:24px 0}"
|
|
81
|
-
].join("");
|
|
82
|
-
|
|
83
|
-
const CSS_ID = "dsh-session-manager/session-manager";
|
|
84
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(CSS_ID) + "]") === null) {
|
|
85
|
-
const tag = document.createElement("style");
|
|
86
|
-
tag.dataset.plugin = "dsh-session-manager";
|
|
87
|
-
tag.dataset.pluginCss = CSS_ID;
|
|
88
|
-
tag.textContent = css;
|
|
89
|
-
document.head.appendChild(tag);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ---------------------------------------------------------------------
|
|
93
|
-
// Locales
|
|
94
|
-
// ---------------------------------------------------------------------
|
|
95
43
|
const zh = {
|
|
96
44
|
"header.aria": "会话操作",
|
|
45
|
+
"menu.open": "打开",
|
|
97
46
|
"menu.archive": "归档会话",
|
|
98
47
|
"menu.unarchive": "移出归档",
|
|
99
48
|
"menu.delete": "删除会话…",
|
|
49
|
+
"menu.move": "移动到工作区…",
|
|
100
50
|
"menu.manage": "打开会话管理…",
|
|
101
51
|
"footer.aria": "会话管理",
|
|
102
52
|
"footer.label": "会话管理",
|
|
@@ -109,23 +59,45 @@ window.__ModuleLoader__.load({
|
|
|
109
59
|
"panel.archived": "已归档",
|
|
110
60
|
"panel.running": "运行中",
|
|
111
61
|
"panel.current": "当前",
|
|
62
|
+
"panel.session.new": "新建会话",
|
|
63
|
+
"panel.close": "关闭",
|
|
112
64
|
"row.open": "打开",
|
|
113
65
|
"row.archive": "归档",
|
|
114
66
|
"row.unarchive": "移出归档",
|
|
115
|
-
"row.
|
|
67
|
+
"row.move": "移动至工作区",
|
|
68
|
+
"row.delete": "删除会话",
|
|
69
|
+
"row.migrate": "迁移预设",
|
|
70
|
+
"migrate.title": "迁移 Agent 预设",
|
|
71
|
+
"migrate.current": "当前预设",
|
|
72
|
+
"migrate.target": "目标预设",
|
|
73
|
+
"migrate.noPresets": "没有可用的目标预设",
|
|
74
|
+
"migrate.same": "请选择与当前预设不同的目标预设",
|
|
75
|
+
"migrate.confirm": "确认迁移",
|
|
76
|
+
"migrate.completed": "Agent 预设已迁移为 {preset}",
|
|
116
77
|
"confirm.delete.title": "删除会话",
|
|
117
78
|
"confirm.delete.desc": "会话「{title}」将被永久删除,包括其全部消息记录与磁盘文件,此操作不可撤销。",
|
|
118
79
|
"confirm.delete.running": "该会话正在运行,删除将立即中断它。",
|
|
119
80
|
"confirm.delete.confirm": "确认删除",
|
|
81
|
+
"confirm.move.title": "移动会话到工作区",
|
|
82
|
+
"confirm.move.desc": "将会话「{title}」移动到目标工作区。",
|
|
83
|
+
"confirm.move.running": "该会话正在运行,移动会中断并关闭它;之后可在目标工作区重新打开继续。",
|
|
84
|
+
"confirm.move.empty": "没有可移动到的其他工作区。",
|
|
85
|
+
"confirm.move.select": "选择目标工作区",
|
|
86
|
+
"confirm.move.current": "当前工作区:{name}",
|
|
87
|
+
"confirm.move.confirm": "确认移动",
|
|
120
88
|
"confirm.cancel": "取消",
|
|
121
89
|
"busy.processing": "处理中…",
|
|
122
|
-
"error.operation": "操作失败:{message}"
|
|
90
|
+
"error.operation": "操作失败:{message}",
|
|
91
|
+
"workspace.default": "默认工作区",
|
|
92
|
+
"workspace.sessions": "{count} 个会话"
|
|
123
93
|
};
|
|
124
94
|
const en = {
|
|
125
95
|
"header.aria": "Session actions",
|
|
96
|
+
"menu.open": "Open",
|
|
126
97
|
"menu.archive": "Archive session",
|
|
127
98
|
"menu.unarchive": "Unarchive session",
|
|
128
99
|
"menu.delete": "Delete session…",
|
|
100
|
+
"menu.move": "Move to workspace…",
|
|
129
101
|
"menu.manage": "Open session manager…",
|
|
130
102
|
"footer.aria": "Session manager",
|
|
131
103
|
"footer.label": "Sessions",
|
|
@@ -134,65 +106,143 @@ window.__ModuleLoader__.load({
|
|
|
134
106
|
"panel.filter.active": "Active",
|
|
135
107
|
"panel.filter.archived": "Archived",
|
|
136
108
|
"panel.empty": "No sessions",
|
|
137
|
-
"panel.empty.archived": "
|
|
109
|
+
"panel.empty.archived": "No archived sessions",
|
|
138
110
|
"panel.archived": "Archived",
|
|
139
111
|
"panel.running": "Running",
|
|
140
112
|
"panel.current": "Current",
|
|
113
|
+
"panel.session.new": "New session",
|
|
114
|
+
"panel.close": "Close",
|
|
141
115
|
"row.open": "Open",
|
|
142
116
|
"row.archive": "Archive",
|
|
143
117
|
"row.unarchive": "Unarchive",
|
|
144
|
-
"row.
|
|
118
|
+
"row.move": "Move to workspace",
|
|
119
|
+
"row.delete": "Delete session",
|
|
120
|
+
"row.migrate": "Migrate preset",
|
|
121
|
+
"migrate.title": "Migrate Agent preset",
|
|
122
|
+
"migrate.current": "Current preset",
|
|
123
|
+
"migrate.target": "Target preset",
|
|
124
|
+
"migrate.noPresets": "No target presets are available",
|
|
125
|
+
"migrate.same": "Choose a target preset different from the current preset",
|
|
126
|
+
"migrate.confirm": "Migrate",
|
|
127
|
+
"migrate.completed": "Agent preset migrated to {preset}",
|
|
145
128
|
"confirm.delete.title": "Delete session",
|
|
146
129
|
"confirm.delete.desc": "Session \"{title}\" will be permanently deleted, including all its messages and files on disk. This cannot be undone.",
|
|
147
130
|
"confirm.delete.running": "This session is running; deleting will interrupt it immediately.",
|
|
148
131
|
"confirm.delete.confirm": "Delete",
|
|
132
|
+
"confirm.move.title": "Move session to workspace",
|
|
133
|
+
"confirm.move.desc": "Move session \"{title}\" to the target workspace.",
|
|
134
|
+
"confirm.move.running": "This session is running; moving will interrupt and close it; you can reopen it from the target workspace to continue.",
|
|
135
|
+
"confirm.move.empty": "No other workspaces to move to.",
|
|
136
|
+
"confirm.move.select": "Select target workspace",
|
|
137
|
+
"confirm.move.current": "Current workspace: {name}",
|
|
138
|
+
"confirm.move.confirm": "Move",
|
|
149
139
|
"confirm.cancel": "Cancel",
|
|
150
140
|
"busy.processing": "Working…",
|
|
151
|
-
"error.operation": "Operation failed: {message}"
|
|
141
|
+
"error.operation": "Operation failed: {message}",
|
|
142
|
+
"workspace.default": "Default workspace",
|
|
143
|
+
"workspace.sessions": "{count} sessions"
|
|
152
144
|
};
|
|
153
145
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
},
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
146
|
+
const css = [
|
|
147
|
+
".sm-header{display:flex;align-items:center;gap:6px}",
|
|
148
|
+
".sm-headerBtn{box-sizing:border-box;min-height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;align-items:center;gap:4px;padding:3px 10px;font-size:12px;line-height:18px;display:inline-flex;white-space:nowrap}",
|
|
149
|
+
".sm-headerBtn:hover:not(:disabled),.sm-headerBtn:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
150
|
+
".sm-headerBtn:disabled{opacity:.5;cursor:not-allowed}",
|
|
151
|
+
".sm-headerBtn svg{flex:none}",
|
|
152
|
+
".sm-busy{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:28px;margin-left:4px}",
|
|
153
|
+
".sm-footer{display:flex}",
|
|
154
|
+
".sm-footerBtn{min-height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:6px;align-items:center;gap:6px;padding:3px 8px;font-size:12px;line-height:18px;display:inline-flex}",
|
|
155
|
+
".sm-footerBtn:hover,.sm-footerBtn:focus-visible{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
156
|
+
".sm-footerBtn svg{flex:none}",
|
|
157
|
+
".sm-panel{display:flex;flex-direction:column;gap:10px;min-height:280px;max-height:min(70vh,640px);min-width:480px}",
|
|
158
|
+
".sm-filter{display:flex;gap:4px;flex-wrap:wrap}",
|
|
159
|
+
".sm-filterBtn{cursor:pointer;color:var(--dsw-alias-label-secondary);background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:3px 12px;font-size:12px;line-height:18px}",
|
|
160
|
+
".sm-filterBtn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
161
|
+
".sm-filterBtn.sm-on{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-fill-l2);border-color:transparent}",
|
|
162
|
+
".sm-list{display:flex;flex-direction:column;gap:4px;min-height:0;overflow:auto;flex:1}",
|
|
163
|
+
".sm-row{box-sizing:border-box;display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px}",
|
|
164
|
+
".sm-row:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
165
|
+
".sm-rowCurrent{border-color:var(--dsw-alias-accent-primary,var(--dsw-alias-label-secondary))}",
|
|
166
|
+
".sm-rowMain{min-width:0;display:flex;flex-direction:column;gap:2px;flex:1 1 280px}",
|
|
167
|
+
".sm-rowTitle{color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px;display:flex;align-items:center;gap:6px;min-width:0}",
|
|
168
|
+
".sm-rowTitle>span:first-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}",
|
|
169
|
+
".sm-rowMeta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}",
|
|
170
|
+
".sm-rowActions{display:flex;gap:4px;flex:none;flex-wrap:wrap;justify-content:flex-end}",
|
|
171
|
+
".sm-badge{flex:none;border-radius:999px;padding:0 6px;font-size:10px;line-height:16px}",
|
|
172
|
+
".sm-badgeArchived{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l2)}",
|
|
173
|
+
".sm-badgeRunning{color:var(--dsw-alias-state-warning-primary,#d97706);background:rgba(217,119,6,.12)}",
|
|
174
|
+
".sm-badgeCurrent{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-fill-l2)}",
|
|
175
|
+
".sm-danger{color:var(--dsw-alias-state-error-primary) !important;border-color:var(--dsw-alias-state-error-primary) !important}",
|
|
176
|
+
".sm-dangerText{color:var(--dsw-alias-state-error-primary) !important}",
|
|
177
|
+
".sm-warn{display:flex;align-items:flex-start;gap:8px;color:var(--dsw-alias-label-secondary);background:rgba(217,119,6,.08);border:1px solid rgba(217,119,6,.25);border-radius:8px;padding:8px 10px;margin-top:4px}",
|
|
178
|
+
".sm-warn svg{flex:none;margin-top:1px}",
|
|
179
|
+
".sm-error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;min-height:0;margin-top:4px}",
|
|
180
|
+
".sm-foot{display:flex;justify-content:flex-end;align-items:center;gap:8px;width:100%}",
|
|
181
|
+
".sm-footError{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;margin-right:auto}",
|
|
182
|
+
".sm-empty{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px;text-align:center;padding:24px 0}",
|
|
183
|
+
".sm-moveSection{display:flex;flex-direction:column;gap:8px}",
|
|
184
|
+
".sm-moveLabel{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}",
|
|
185
|
+
".sm-moveList{display:flex;flex-direction:column;gap:4px;max-height:200px;overflow:auto}",
|
|
186
|
+
".sm-moveItem{box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;transition:all .15s}",
|
|
187
|
+
".sm-moveItem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
188
|
+
".sm-moveItemSelected{border-color:var(--dsw-alias-accent-primary);background:var(--dsw-alias-accent-primary-bg)}",
|
|
189
|
+
".sm-moveItemName{color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}",
|
|
190
|
+
".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)}",
|
|
191
|
+
".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)}",
|
|
192
|
+
".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}",
|
|
193
|
+
].join("");
|
|
171
194
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
body: JSON.stringify(payload || {})
|
|
180
|
-
});
|
|
181
|
-
let data;
|
|
182
|
-
try {
|
|
183
|
-
data = await response.json();
|
|
184
|
-
} catch {
|
|
185
|
-
data = { ok: false, error: "HTTP " + response.status };
|
|
186
|
-
}
|
|
187
|
-
return data;
|
|
195
|
+
const CSS_ID = "dsh-session-manager/session-manager";
|
|
196
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(CSS_ID) + "]") === null) {
|
|
197
|
+
const tag = document.createElement("style");
|
|
198
|
+
tag.dataset.plugin = "dsh-session-manager";
|
|
199
|
+
tag.dataset.pluginCss = CSS_ID;
|
|
200
|
+
tag.textContent = css;
|
|
201
|
+
document.head.appendChild(tag);
|
|
188
202
|
}
|
|
189
203
|
|
|
190
|
-
// ---------------------------------------------------------------------
|
|
191
|
-
// Small helpers
|
|
192
|
-
// ---------------------------------------------------------------------
|
|
193
|
-
const h = (type, props, ...children) => React.createElement(type, props, ...children);
|
|
194
204
|
|
|
195
|
-
|
|
205
|
+
// Anchor the session manager panel to the sidebar right edge so it
|
|
206
|
+
// always fits within the sidebar area, regardless of viewport size.
|
|
207
|
+
const SM_PANEL_CSS_ID = "dsh-session-manager/session-manager-anchor";
|
|
208
|
+
const installAnchor = () => {
|
|
209
|
+
if (typeof document === "undefined") return;
|
|
210
|
+
if (document.querySelector("style[data-plugin-css=" + JSON.stringify(SM_PANEL_CSS_ID) + "]") !== null) return;
|
|
211
|
+
const tag = document.createElement("style");
|
|
212
|
+
tag.dataset.plugin = "dsh-session-manager";
|
|
213
|
+
tag.dataset.pluginCss = SM_PANEL_CSS_ID;
|
|
214
|
+
tag.textContent = ":root{--sm-panel-left:8px}";
|
|
215
|
+
document.head.appendChild(tag);
|
|
216
|
+
const updateLeft = () => {
|
|
217
|
+
let right = 8;
|
|
218
|
+
const candidates = [
|
|
219
|
+
"[data-dsh-sidebar]",
|
|
220
|
+
"[data-testid=sidebar]",
|
|
221
|
+
"[aria-label=会话][role=navigation]",
|
|
222
|
+
"aside",
|
|
223
|
+
".dsw-sidebar"
|
|
224
|
+
];
|
|
225
|
+
for (const sel of candidates) {
|
|
226
|
+
const el = document.querySelector(sel);
|
|
227
|
+
if (el !== null) {
|
|
228
|
+
const rect = el.getBoundingClientRect();
|
|
229
|
+
if (rect.right > 100 && rect.right < window.innerWidth - 40) {
|
|
230
|
+
right = Math.round(rect.right) + 8;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
document.documentElement.style.setProperty("--sm-panel-left", right + "px");
|
|
236
|
+
};
|
|
237
|
+
updateLeft();
|
|
238
|
+
window.addEventListener("resize", updateLeft);
|
|
239
|
+
};
|
|
240
|
+
try { installAnchor(); } catch (_) {}
|
|
241
|
+
|
|
242
|
+
const h = (type, props, ...children) => React.createElement(type, props, ...children);
|
|
243
|
+
const callApi = (path, payload) => fetch(API + path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload || {}) }).then((r) => r.json());
|
|
244
|
+
const fetchApi = (path) => fetch(API + path).then((r) => r.json());
|
|
245
|
+
const formatRelative = (ts, now) => {
|
|
196
246
|
const diff = Math.max(0, now - ts);
|
|
197
247
|
const s = Math.floor(diff / 1000);
|
|
198
248
|
if (s < 60) return s + "s";
|
|
@@ -205,147 +255,361 @@ window.__ModuleLoader__.load({
|
|
|
205
255
|
const mo = Math.floor(d / 30);
|
|
206
256
|
if (mo < 12) return mo + "mo";
|
|
207
257
|
return Math.floor(mo / 12) + "y";
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function shortPath(cwd) {
|
|
258
|
+
};
|
|
259
|
+
const shortPath = (cwd) => {
|
|
211
260
|
if (!cwd) return "";
|
|
212
|
-
const parts = cwd.replace(/[
|
|
261
|
+
const parts = cwd.replace(/[\\\/]+$/, "").split(/[\\\/]/);
|
|
213
262
|
const last = parts[parts.length - 1] || "";
|
|
214
|
-
return last.length > 26 ? last.slice(0, 24) + "
|
|
215
|
-
}
|
|
263
|
+
return last.length > 26 ? last.slice(0, 24) + "\u2026" : last;
|
|
264
|
+
};
|
|
216
265
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
266
|
+
function ConfirmDialog(props) {
|
|
267
|
+
const open = props.open === true;
|
|
268
|
+
const onCancel = typeof props.onCancel === "function" ? props.onCancel : () => {};
|
|
269
|
+
const onConfirm = typeof props.onConfirm === "function" ? props.onConfirm : () => {};
|
|
270
|
+
const busy = props.busy === true;
|
|
271
|
+
const title = props.title || "";
|
|
272
|
+
const description = props.description || "";
|
|
273
|
+
const warning = props.warning || "";
|
|
274
|
+
const confirmLabel = props.confirmLabel || "确认";
|
|
275
|
+
const cancelLabel = props.cancelLabel || "取消";
|
|
276
|
+
const R = (typeof window !== "undefined") ? window.__smReact : null;
|
|
277
|
+
const createPortal = (R && typeof R.createPortal === "function") ? R.createPortal : null;
|
|
278
|
+
const body = (typeof document === "undefined") ? null : document.body;
|
|
279
|
+
if (!open) return null;
|
|
280
|
+
if (!body) return tree || null;
|
|
281
|
+
const tree = h("div", {
|
|
282
|
+
className: "sm-nativeDialogLayer sm-confirmDialog",
|
|
283
|
+
style: props.anchor && typeof props.anchor.top === "number"
|
|
284
|
+
? { position: "fixed", top: props.anchor.top + "px", left: props.anchor.left + "px", zIndex: 10001 }
|
|
285
|
+
: null,
|
|
286
|
+
role: "presentation",
|
|
287
|
+
onKeyDown: (e) => {
|
|
288
|
+
if (e.key === "Escape" && !busy) { e.preventDefault(); onCancel(); }
|
|
289
|
+
else if (e.key === "Enter" && !busy) { e.preventDefault(); onConfirm(); }
|
|
225
290
|
},
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
291
|
+
tabIndex: -1
|
|
292
|
+
},
|
|
293
|
+
h("div", {
|
|
294
|
+
className: "sm-nativeDialogBackdrop",
|
|
295
|
+
onMouseDown: (e) => { if (e.target === e.currentTarget && !busy) onCancel(); }
|
|
296
|
+
}),
|
|
297
|
+
h("section", {
|
|
298
|
+
className: "sm-nativeDialog",
|
|
299
|
+
role: "dialog",
|
|
300
|
+
"aria-modal": "true",
|
|
301
|
+
"aria-labelledby": "sm-confirm-dialog-title",
|
|
302
|
+
ref: (el) => { if (el && typeof el.focus === "function") el.focus(); }
|
|
303
|
+
},
|
|
304
|
+
h("div", { className: "sm-nativeDialogHeader" },
|
|
305
|
+
h("h2", { id: "sm-confirm-dialog-title", className: "sm-nativeDialogTitle" }, title)
|
|
306
|
+
),
|
|
307
|
+
h("div", { className: "sm-nativeDialogBody" },
|
|
308
|
+
description ? h("div", { className: "sm-nativeDialogDescription" }, description) : null,
|
|
309
|
+
warning ? h("div", { className: "sm-warn" }, h("span", null, warning)) : null
|
|
310
|
+
),
|
|
311
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
312
|
+
h("button", {
|
|
313
|
+
type: "button",
|
|
314
|
+
className: "sm-nativeDialogButton sm-nativeDialogCancel",
|
|
315
|
+
disabled: busy,
|
|
316
|
+
onClick: () => { if (!busy) onCancel(); }
|
|
317
|
+
}, cancelLabel),
|
|
318
|
+
h("button", {
|
|
319
|
+
type: "button",
|
|
320
|
+
className: "sm-nativeDialogButton sm-nativeDialogConfirm",
|
|
321
|
+
disabled: busy,
|
|
322
|
+
onClick: () => { if (!busy) onConfirm(); }
|
|
323
|
+
}, confirmLabel)
|
|
324
|
+
)
|
|
239
325
|
)
|
|
240
|
-
|
|
326
|
+
);
|
|
327
|
+
if (createPortal) return createPortal(tree, body);
|
|
328
|
+
// Default: render inline. position:fixed escapes the sidebar
|
|
329
|
+
// overflow:hidden ancestor at the DOM/CSS level, so the dialog
|
|
330
|
+
// appears next to the panel. Keeping the dialog in the same
|
|
331
|
+
// React tree as the panel preserves the onClick wiring (cancel /
|
|
332
|
+
// confirm call setConfirmFor, which only works when the click
|
|
333
|
+
// handler lives in the same root).
|
|
334
|
+
return tree;
|
|
241
335
|
}
|
|
242
336
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
const [
|
|
256
|
-
|
|
257
|
-
const [error, setError] = useState(null);
|
|
337
|
+
function MoveDialog(props) {
|
|
338
|
+
const t = typeof props.t === "function" ? props.t : (key) => key;
|
|
339
|
+
const open = props.open === true;
|
|
340
|
+
const onCancel = typeof props.onCancel === "function" ? props.onCancel : () => {};
|
|
341
|
+
const onConfirm = typeof props.onConfirm === "function" ? props.onConfirm : () => {};
|
|
342
|
+
const busy = props.busy === true;
|
|
343
|
+
const title = props.title || t("confirm.move.title");
|
|
344
|
+
const description = props.description || "";
|
|
345
|
+
const currentWorkspaceId = props.currentWorkspaceId || "";
|
|
346
|
+
const confirmLabel = props.confirmLabel || t("confirm.move.confirm");
|
|
347
|
+
const cancelLabel = props.cancelLabel || t("confirm.cancel");
|
|
348
|
+
const error = props.error || "";
|
|
349
|
+
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState("");
|
|
350
|
+
if (!open) return null;
|
|
258
351
|
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
352
|
+
const list = (Array.isArray(props.workspaces) ? props.workspaces : [])
|
|
353
|
+
.filter((ws) => ws && typeof ws === "object" && typeof ws.id === "string" && ws.id !== currentWorkspaceId);
|
|
354
|
+
const body = h("div", { className: "sm-nativeDialogBody" },
|
|
355
|
+
h("div", { className: "sm-nativeDialogDescription" }, description),
|
|
356
|
+
list.length === 0
|
|
357
|
+
? h("div", { className: "sm-moveLabel" }, t("confirm.move.empty"))
|
|
358
|
+
: h("div", { className: "sm-moveSection" },
|
|
359
|
+
h("div", { className: "sm-moveLabel" }, t("confirm.move.select")),
|
|
360
|
+
h("div", { className: "sm-moveList", role: "listbox", "aria-label": t("confirm.move.select") },
|
|
361
|
+
list.map((ws) => h("button", {
|
|
362
|
+
key: ws.id,
|
|
363
|
+
type: "button",
|
|
364
|
+
role: "option",
|
|
365
|
+
"aria-selected": selectedWorkspaceId === ws.id,
|
|
366
|
+
className: "sm-moveItem" + (selectedWorkspaceId === ws.id ? " sm-moveItemSelected" : ""),
|
|
367
|
+
disabled: busy,
|
|
368
|
+
onClick: () => setSelectedWorkspaceId(ws.id)
|
|
369
|
+
},
|
|
370
|
+
h("span", { className: "sm-moveItemName" }, String(ws.title || ws.name || ws.id)),
|
|
371
|
+
h("span", { className: "sm-moveItemMeta" }, ws.path ? String(ws.path) : "")
|
|
372
|
+
))
|
|
373
|
+
)
|
|
374
|
+
),
|
|
375
|
+
error ? h("div", { className: "sm-error", role: "alert" }, String(error)) : null
|
|
376
|
+
);
|
|
377
|
+
// Single cancel button (footer) + ESC handler. No "x" close button so the
|
|
378
|
+
// user only sees one cancel entry point.
|
|
379
|
+
const onKeyDown = (e) => {
|
|
380
|
+
if (e.key === "Escape" && !busy) {
|
|
381
|
+
e.preventDefault();
|
|
382
|
+
onCancel();
|
|
383
|
+
} else if (e.key === "Enter" && !busy && selectedWorkspaceId && list.length > 0) {
|
|
384
|
+
e.preventDefault();
|
|
385
|
+
onConfirm(selectedWorkspaceId);
|
|
269
386
|
}
|
|
270
387
|
};
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
388
|
+
return h("div", {
|
|
389
|
+
className: "sm-nativeDialogLayer sm-confirmDialog",
|
|
390
|
+
style: props.anchor && typeof props.anchor.top === "number"
|
|
391
|
+
? { position: "fixed", top: props.anchor.top + "px", left: props.anchor.left + "px", zIndex: 10001 }
|
|
392
|
+
: null,
|
|
393
|
+
role: "presentation",
|
|
394
|
+
onKeyDown,
|
|
395
|
+
tabIndex: -1
|
|
396
|
+
},
|
|
397
|
+
h("div", {
|
|
398
|
+
className: "sm-nativeDialogBackdrop",
|
|
399
|
+
onMouseDown: (e) => { if (e.target === e.currentTarget && !busy) onCancel(); }
|
|
400
|
+
}),
|
|
401
|
+
h("section", {
|
|
402
|
+
className: "sm-nativeDialog",
|
|
403
|
+
role: "dialog",
|
|
404
|
+
"aria-modal": "true",
|
|
405
|
+
"aria-labelledby": "sm-move-dialog-title",
|
|
406
|
+
ref: (el) => { if (el && typeof el.focus === "function") el.focus(); }
|
|
282
407
|
},
|
|
283
|
-
h(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
408
|
+
h("div", { className: "sm-nativeDialogHeader" },
|
|
409
|
+
h("h2", { id: "sm-move-dialog-title", className: "sm-nativeDialogTitle" }, title)
|
|
410
|
+
),
|
|
411
|
+
body,
|
|
412
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
413
|
+
h("button", {
|
|
414
|
+
type: "button",
|
|
415
|
+
className: "sm-nativeDialogButton sm-nativeDialogCancel",
|
|
416
|
+
disabled: busy,
|
|
417
|
+
onClick: onCancel
|
|
418
|
+
}, cancelLabel),
|
|
419
|
+
h("button", {
|
|
420
|
+
type: "button",
|
|
421
|
+
className: "sm-nativeDialogButton sm-nativeDialogConfirm",
|
|
422
|
+
disabled: busy || !selectedWorkspaceId || list.length === 0,
|
|
423
|
+
onClick: () => { if (!busy && selectedWorkspaceId) onConfirm(selectedWorkspaceId); }
|
|
424
|
+
}, confirmLabel)
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function MigratePresetDialog(props) {
|
|
431
|
+
const t = typeof props.t === "function" ? props.t : (key) => key;
|
|
432
|
+
const open = props.open === true;
|
|
433
|
+
const onCancel = typeof props.onCancel === "function" ? props.onCancel : () => {};
|
|
434
|
+
const onRun = typeof props.onRun === "function" ? props.onRun : () => {};
|
|
435
|
+
const busy = props.busy === true;
|
|
436
|
+
const sessionId = props.sessionId || "";
|
|
437
|
+
const fallbackPreset = props.currentPreset || "";
|
|
438
|
+
const [scan, setScan] = useState(null);
|
|
439
|
+
const [target, setTarget] = useState("");
|
|
440
|
+
useEffect(() => {
|
|
441
|
+
if (!open || !sessionId) return;
|
|
442
|
+
let alive = true;
|
|
443
|
+
setScan(null);
|
|
444
|
+
setTarget("");
|
|
445
|
+
fetchApi("/preset-scan?sessionId=" + encodeURIComponent(sessionId)).then((d) => {
|
|
446
|
+
if (!alive) return;
|
|
447
|
+
if (d && d.ok) {
|
|
448
|
+
const result = d.result || {};
|
|
449
|
+
const rows = Array.isArray(result.rows)
|
|
450
|
+
? result.rows
|
|
451
|
+
: (result.rows && Array.isArray(result.rows.rows) ? result.rows.rows : []);
|
|
452
|
+
const availablePresets = Array.isArray(result.availablePresets)
|
|
453
|
+
? result.availablePresets
|
|
454
|
+
: (result.rows && Array.isArray(result.rows.availablePresets) ? result.rows.availablePresets : []);
|
|
455
|
+
const row = rows.find((item) => item && item.sessionId === sessionId) || null;
|
|
456
|
+
setScan({ ok: true, row, availablePresets });
|
|
457
|
+
} else {
|
|
458
|
+
setScan({ ok: false, error: (d && d.error) || "scan failed" });
|
|
294
459
|
}
|
|
460
|
+
}).catch((error) => {
|
|
461
|
+
if (alive) setScan({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
462
|
+
});
|
|
463
|
+
return () => { alive = false; };
|
|
464
|
+
}, [open, sessionId]);
|
|
465
|
+
if (!open) return null;
|
|
466
|
+
|
|
467
|
+
const currentPreset = (scan && scan.ok && scan.row && (scan.row.finalPreset || scan.row.headerPreset)) || fallbackPreset || "—";
|
|
468
|
+
const presets = scan && scan.ok
|
|
469
|
+
? Array.from(new Set((scan.availablePresets || []).filter((preset) => typeof preset === "string" && preset.length > 0))).sort()
|
|
470
|
+
: [];
|
|
471
|
+
const targets = presets.filter((preset) => preset !== currentPreset);
|
|
472
|
+
const ready = target !== "" && target !== currentPreset;
|
|
473
|
+
const body = h("div", { className: "sm-migratePanel" },
|
|
474
|
+
scan === null
|
|
475
|
+
? h("div", { className: "sm-empty" }, t("busy.processing"))
|
|
476
|
+
: !scan.ok
|
|
477
|
+
? h("div", { className: "sm-error", role: "alert" }, scan.error || "scan failed")
|
|
478
|
+
: h(React.Fragment, null,
|
|
479
|
+
h("div", { className: "sm-migrateRow" },
|
|
480
|
+
h("label", null, t("migrate.current")),
|
|
481
|
+
h("div", { className: "sm-migrateCurrent" }, currentPreset)
|
|
482
|
+
),
|
|
483
|
+
h("div", { className: "sm-migrateRow" },
|
|
484
|
+
h("label", null, t("migrate.target")),
|
|
485
|
+
targets.length === 0
|
|
486
|
+
? h("div", { className: "sm-migrateEmpty" }, t("migrate.noPresets"))
|
|
487
|
+
: h("div", { className: "sm-migrateChips" }, targets.map((preset) => h("button", {
|
|
488
|
+
key: preset,
|
|
489
|
+
type: "button",
|
|
490
|
+
className: "sm-migrateChip" + (target === preset ? " sm-migrateChipOn" : ""),
|
|
491
|
+
disabled: busy,
|
|
492
|
+
onClick: () => setTarget(preset)
|
|
493
|
+
}, preset)))
|
|
494
|
+
),
|
|
495
|
+
target === currentPreset ? h("div", { className: "sm-migrateWarn" }, t("migrate.same")) : null
|
|
496
|
+
)
|
|
497
|
+
);
|
|
498
|
+
return h("div", {
|
|
499
|
+
className: "sm-nativeDialogLayer sm-confirmDialog sm-migrateConfirmLayer",
|
|
500
|
+
role: "presentation",
|
|
501
|
+
onKeyDown: (event) => { if (event.key === "Escape" && !busy) { event.preventDefault(); onCancel(); } }
|
|
502
|
+
},
|
|
503
|
+
h("div", {
|
|
504
|
+
className: "sm-nativeDialogBackdrop",
|
|
505
|
+
onMouseDown: (event) => { if (event.target === event.currentTarget && !busy) onCancel(); }
|
|
506
|
+
}),
|
|
507
|
+
h("section", {
|
|
508
|
+
className: "sm-nativeDialog sm-migrateDialog",
|
|
509
|
+
role: "dialog",
|
|
510
|
+
"aria-modal": "true",
|
|
511
|
+
"aria-labelledby": "sm-migrate-dialog-title"
|
|
295
512
|
},
|
|
296
|
-
h("
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
513
|
+
h("div", { className: "sm-nativeDialogHeader" },
|
|
514
|
+
h("h2", { id: "sm-migrate-dialog-title", className: "sm-nativeDialogTitle" }, t("migrate.title"))
|
|
515
|
+
),
|
|
516
|
+
body,
|
|
517
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
518
|
+
h("button", {
|
|
519
|
+
type: "button",
|
|
520
|
+
className: "sm-nativeDialogButton sm-nativeDialogCancel",
|
|
521
|
+
disabled: busy,
|
|
522
|
+
onClick: onCancel
|
|
523
|
+
}, t("confirm.cancel")),
|
|
524
|
+
h("button", {
|
|
525
|
+
type: "button",
|
|
526
|
+
className: "sm-nativeDialogButton sm-nativeDialogConfirm",
|
|
527
|
+
disabled: busy || !ready,
|
|
528
|
+
onClick: () => { if (ready) onRun(target); }
|
|
529
|
+
}, busy ? t("busy.processing") : t("migrate.confirm"))
|
|
530
|
+
)
|
|
531
|
+
)
|
|
314
532
|
);
|
|
315
533
|
}
|
|
316
534
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
535
|
+
const panelStore = {
|
|
536
|
+
open: false,
|
|
537
|
+
listeners: new Set(),
|
|
538
|
+
getSnapshot() { return this.open; },
|
|
539
|
+
subscribe(cb) { this.listeners.add(cb); return () => this.listeners.delete(cb); },
|
|
540
|
+
set(v) { if (this.open === v) return; this.open = v; for (const fn of [...this.listeners]) fn(); },
|
|
541
|
+
// Pending action queued by the title-bar buttons (e.g. 移动至工作区).
|
|
542
|
+
// The SessionManagerPanel consumes this on mount so it can pre-open
|
|
543
|
+
// the matching inner dialog (move / delete / migrate) next to itself.
|
|
544
|
+
pendingMove: null,
|
|
545
|
+
pendingListeners: new Set(),
|
|
546
|
+
_emitPending() { for (const fn of [...this.pendingListeners]) fn(); },
|
|
547
|
+
subscribePending(cb) { this.pendingListeners.add(cb); return () => this.pendingListeners.delete(cb); },
|
|
548
|
+
setPendingMove(v) { this.pendingMove = v; this._emitPending(); },
|
|
549
|
+
consumePendingMove() { const v = this.pendingMove; this.pendingMove = null; return v; }
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
function SessionManagerPanel(props) {
|
|
553
|
+
const t = props.t;
|
|
554
|
+
const open = props.open;
|
|
555
|
+
const onClose = props.onClose;
|
|
556
|
+
const useSessions = props.useSessions;
|
|
557
|
+
const useWorkspaces = props.useWorkspaces;
|
|
558
|
+
const onOpen = props.onOpen;
|
|
559
|
+
const onArchive = props.onArchive;
|
|
560
|
+
const onUnarchive = props.onUnarchive;
|
|
561
|
+
const onRemove = props.onRemove;
|
|
562
|
+
const onMove = props.onMove;
|
|
563
|
+
const onMigrate = props.onMigrate;
|
|
321
564
|
const list = useSessions((s) => s);
|
|
322
565
|
const archivedIds = useWorkspaces((s) => s.archivedSessionIds);
|
|
323
566
|
const [filter, setFilter] = useState("all");
|
|
324
|
-
const [confirm, setConfirm] = useState(null);
|
|
325
567
|
const [busyId, setBusyId] = useState(null);
|
|
326
568
|
const [error, setError] = useState(null);
|
|
327
569
|
const [now, setNow] = useState(() => Date.now());
|
|
328
|
-
|
|
570
|
+
const [moveFor, setMoveFor] = useState(null);
|
|
571
|
+
const [confirmFor, setConfirmFor] = useState(null);
|
|
572
|
+
const [workspaces, setWorkspaces] = useState([]);
|
|
573
|
+
const [migrating, setMigrating] = useState(false);
|
|
574
|
+
const [migrateFor, setMigrateFor] = useState(null);
|
|
329
575
|
useEffect(() => {
|
|
330
576
|
const timer = setInterval(() => setNow(Date.now()), 30000);
|
|
331
577
|
return () => clearInterval(timer);
|
|
332
578
|
}, []);
|
|
333
|
-
|
|
579
|
+
useEffect(() => {
|
|
580
|
+
if (!open) return;
|
|
581
|
+
let alive = true;
|
|
582
|
+
fetchApi("/workspaces").then((data) => {
|
|
583
|
+
if (alive && data.ok) setWorkspaces(data.result.workspaces || []);
|
|
584
|
+
});
|
|
585
|
+
return () => { alive = false; };
|
|
586
|
+
}, [open]);
|
|
587
|
+
// Drain any pending action queued by a header button. The panel is
|
|
588
|
+
// the single owner of every inner dialog, so opening it with a
|
|
589
|
+
// pending action pre-populates the corresponding dialog state.
|
|
590
|
+
useEffect(() => {
|
|
591
|
+
if (!open) return;
|
|
592
|
+
const pending = panelStore.consumePendingMove();
|
|
593
|
+
if (pending && pending.sessionId) {
|
|
594
|
+
setMoveFor({ id: pending.sessionId, displayTitle: pending.displayTitle || pending.sessionId, workspaceId: pending.workspaceId || "" });
|
|
595
|
+
}
|
|
596
|
+
}, [open]);
|
|
597
|
+
useEffect(() => {
|
|
598
|
+
if (moveFor) {
|
|
599
|
+
fetchApi("/workspaces").then((data) => {
|
|
600
|
+
if (data.ok) setWorkspaces(data.result.workspaces || []);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}, [moveFor]);
|
|
334
604
|
const archived = useMemo(() => new Set(archivedIds), [archivedIds]);
|
|
335
|
-
|
|
336
605
|
const rows = useMemo(() => {
|
|
337
|
-
// Hide subagent sessions and blank workspace placeholders (the
|
|
338
|
-
// "New Session" seat DSH regenerates per workspace on reconnect) —
|
|
339
|
-
// mirror the sidebar's sessionVisible rule: blank rows show only
|
|
340
|
-
// when they are the current session.
|
|
341
606
|
const all = list.ids
|
|
342
607
|
.map((id) => list.byId[id])
|
|
343
|
-
.filter((s) => s !==
|
|
608
|
+
.filter((s) => s !== undefined && s.origin !== "subagent" && (!s.blank || s.id === list.current));
|
|
344
609
|
if (filter === "active") return all.filter((s) => !archived.has(s.id));
|
|
345
610
|
if (filter === "archived") return all.filter((s) => archived.has(s.id));
|
|
346
611
|
return all;
|
|
347
612
|
}, [list, archived, filter]);
|
|
348
|
-
|
|
349
613
|
const runRow = async (sessionId, fn) => {
|
|
350
614
|
if (busyId !== null) return;
|
|
351
615
|
setBusyId(sessionId);
|
|
@@ -358,139 +622,344 @@ window.__ModuleLoader__.load({
|
|
|
358
622
|
setBusyId(null);
|
|
359
623
|
}
|
|
360
624
|
};
|
|
361
|
-
|
|
362
625
|
const filterBtn = (id, label) => h("button", {
|
|
363
626
|
type: "button",
|
|
364
627
|
key: id,
|
|
365
628
|
className: "sm-filterBtn" + (filter === id ? " sm-on" : ""),
|
|
366
629
|
onClick: () => setFilter(id)
|
|
367
630
|
}, label);
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
h("div", { className: "sm-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
631
|
+
const renderRow = (s) => {
|
|
632
|
+
const isArchived = archived.has(s.id);
|
|
633
|
+
const isCurrent = s.id === list.current;
|
|
634
|
+
const busy = busyId === s.id;
|
|
635
|
+
return h("div", {
|
|
636
|
+
key: s.id,
|
|
637
|
+
className: "sm-row" + (isCurrent ? " sm-rowCurrent" : "")
|
|
638
|
+
},
|
|
639
|
+
h("div", { className: "sm-rowMain" },
|
|
640
|
+
h("div", { className: "sm-rowTitle" },
|
|
641
|
+
h("span", null, s.blank ? t("panel.session.new") : (s.displayTitle || s.id)),
|
|
642
|
+
isArchived ? h("span", { className: "sm-badge sm-badgeArchived" }, t("panel.archived")) : null,
|
|
643
|
+
s.running ? h("span", { className: "sm-badge sm-badgeRunning" }, t("panel.running")) : null,
|
|
644
|
+
isCurrent ? h("span", { className: "sm-badge sm-badgeCurrent" }, t("panel.current")) : null
|
|
645
|
+
),
|
|
646
|
+
h("div", { className: "sm-rowMeta" }, shortPath(s.cwd) + (s.cwd ? " · " : "") + formatRelative(s.updatedAt, now))
|
|
380
647
|
),
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
648
|
+
h("div", { className: "sm-rowActions" },
|
|
649
|
+
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => onOpen(s.id) }, t("row.open")),
|
|
650
|
+
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")),
|
|
651
|
+
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => setMoveFor(s) }, t("row.move")),
|
|
652
|
+
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => setMigrateFor(s) }, t("row.migrate")),
|
|
653
|
+
h("button", { type: "button", className: "sm-rowBtn sm-rowBtnDanger", disabled: busy, onClick: () => setConfirmFor(s) }, t("row.delete"))
|
|
654
|
+
)
|
|
655
|
+
);
|
|
656
|
+
};
|
|
657
|
+
const rowsList = rows.length === 0
|
|
658
|
+
? h("div", { className: "sm-empty" }, filter === "archived" ? t("panel.empty.archived") : t("panel.empty"))
|
|
659
|
+
: h("div", { className: "sm-list" }, rows.map(renderRow));
|
|
660
|
+
const onPanelClose = () => {
|
|
661
|
+
if (busyId !== null) return;
|
|
662
|
+
setConfirmFor(null);
|
|
663
|
+
setMoveFor(null);
|
|
664
|
+
setMigrateFor(null);
|
|
665
|
+
onClose();
|
|
666
|
+
};
|
|
667
|
+
// Single cancel button in the panel footer (no native "x").
|
|
668
|
+
const onPanelKey = (e) => {
|
|
669
|
+
if (e.key === "Escape" && busyId === null) {
|
|
670
|
+
if (confirmFor || moveFor || migrateFor) return;
|
|
671
|
+
e.preventDefault();
|
|
672
|
+
onPanelClose();
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
const panelModal = h("div", {
|
|
676
|
+
className: "sm-nativeDialogLayer",
|
|
677
|
+
role: "presentation",
|
|
678
|
+
onKeyDown: onPanelKey,
|
|
679
|
+
tabIndex: -1
|
|
680
|
+
},
|
|
681
|
+
h("div", {
|
|
682
|
+
className: "sm-nativeDialogBackdrop",
|
|
683
|
+
onMouseDown: (e) => { if (e.target === e.currentTarget && busyId === null) onPanelClose(); }
|
|
684
|
+
}),
|
|
685
|
+
h("section", {
|
|
686
|
+
className: "sm-nativeDialog sm-panelDialog",
|
|
687
|
+
role: "dialog",
|
|
688
|
+
"aria-modal": "true",
|
|
689
|
+
"aria-labelledby": "sm-panel-dialog-title",
|
|
690
|
+
ref: (el) => { if (el && typeof el.focus === "function") el.focus(); }
|
|
691
|
+
},
|
|
692
|
+
h("div", { className: "sm-nativeDialogHeader" },
|
|
693
|
+
h("h2", { id: "sm-panel-dialog-title", className: "sm-nativeDialogTitle" }, t("panel.title")),
|
|
694
|
+
h("button", {
|
|
695
|
+
type: "button",
|
|
696
|
+
className: "sm-panelHeaderClose",
|
|
697
|
+
disabled: busyId !== null || migrating,
|
|
698
|
+
onClick: onPanelClose
|
|
699
|
+
}, t("panel.close"))
|
|
700
|
+
),
|
|
701
|
+
h("div", { className: "sm-panel" },
|
|
702
|
+
h("div", { className: "sm-filter" },
|
|
703
|
+
filterBtn("all", t("panel.filter.all")),
|
|
704
|
+
filterBtn("active", t("panel.filter.active")),
|
|
705
|
+
filterBtn("archived", t("panel.filter.archived"))
|
|
706
|
+
),
|
|
707
|
+
error ? h("div", { className: "sm-error", role: "alert" }, t("error.operation", { message: error })) : null,
|
|
708
|
+
rowsList
|
|
709
|
+
)
|
|
710
|
+
)
|
|
711
|
+
);
|
|
712
|
+
const deleteDialog = confirmFor === null ? null : h(ConfirmDialog, {
|
|
713
|
+
open: true,
|
|
714
|
+
onClose: () => { if (busyId === null) setConfirmFor(null); },
|
|
715
|
+
title: t("confirm.delete.title"),
|
|
716
|
+
description: t("confirm.delete.desc", { title: confirmFor.displayTitle || confirmFor.id }),
|
|
717
|
+
warning: confirmFor.running ? t("confirm.delete.running") : null,
|
|
718
|
+
confirmLabel: t("confirm.delete.confirm"),
|
|
719
|
+
cancelLabel: t("confirm.cancel"),
|
|
720
|
+
busy: busyId !== null,
|
|
721
|
+
onCancel: () => { if (busyId === null) setConfirmFor(null); },
|
|
722
|
+
onConfirm: () => {
|
|
723
|
+
const target = confirmFor;
|
|
724
|
+
setConfirmFor(null);
|
|
725
|
+
runRow(target.id, () => onRemove(target.id));
|
|
726
|
+
}
|
|
443
727
|
});
|
|
728
|
+
const moveDialog = moveFor === null ? null : h(MoveDialog, {
|
|
729
|
+
open: true,
|
|
730
|
+
onClose: () => { if (busyId === null) setMoveFor(null); },
|
|
731
|
+
title: t("confirm.move.title"),
|
|
732
|
+
description: t("confirm.move.desc", { title: moveFor.displayTitle || moveFor.id }),
|
|
733
|
+
sessionTitle: moveFor.displayTitle || moveFor.id,
|
|
734
|
+
currentWorkspaceId: moveFor.workspaceId,
|
|
735
|
+
workspaces: workspaces,
|
|
736
|
+
confirmLabel: t("confirm.move.confirm"),
|
|
737
|
+
cancelLabel: t("confirm.cancel"),
|
|
738
|
+
busy: busyId !== null,
|
|
739
|
+
onCancel: () => { if (busyId === null) setMoveFor(null); },
|
|
740
|
+
onConfirm: (targetWorkspaceId) => {
|
|
741
|
+
const target = moveFor;
|
|
742
|
+
setMoveFor(null);
|
|
743
|
+
runRow(target.id, () => onMove(target.id, targetWorkspaceId));
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
const migrateDialog = migrateFor === null ? null : h(MigratePresetDialog, {
|
|
747
|
+
open: true,
|
|
748
|
+
t: t,
|
|
749
|
+
sessionId: migrateFor.id,
|
|
750
|
+
currentPreset: migrateFor.agentPreset || "",
|
|
751
|
+
onCancel: () => { if (!migrating) setMigrateFor(null); },
|
|
752
|
+
busy: migrating,
|
|
753
|
+
onRun: async (targetPreset) => {
|
|
754
|
+
const target = migrateFor;
|
|
755
|
+
setMigrating(true);
|
|
756
|
+
setError(null);
|
|
757
|
+
try {
|
|
758
|
+
await onMigrate(target.id, targetPreset);
|
|
759
|
+
setMigrateFor(null);
|
|
760
|
+
try { window.alert(t("migrate.completed", { preset: targetPreset })); } catch (_) {}
|
|
761
|
+
} catch (error) {
|
|
762
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
763
|
+
setMigrateFor(null);
|
|
764
|
+
} finally {
|
|
765
|
+
setMigrating(false);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
// Render the panel AND the inner dialog as siblings so React keeps
|
|
770
|
+
// both mounted. CSS z-index keeps the right ordering: the inner
|
|
771
|
+
// dialog (.sm-confirmDialog / .sm-moveDialog / .sm-migrateDialog)
|
|
772
|
+
// carries z-index 10000 and therefore sits ON TOP OF the panel
|
|
773
|
+
// (z-index 9999). Clicking inside the panel that the inner dialog
|
|
774
|
+
// blocks is not possible because the inner backdrop covers the
|
|
775
|
+
// panel area entirely (the inner modal is centered).
|
|
776
|
+
const primary = moveDialog || deleteDialog || migrateDialog;
|
|
777
|
+
return h(React.Fragment, null, panelModal, primary);
|
|
444
778
|
}
|
|
445
779
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
780
|
+
function HeaderAction(props) {
|
|
781
|
+
const t = props.t;
|
|
782
|
+
const sessionId = props.sessionId;
|
|
783
|
+
// Slot framework spreads inject-factory return into top-level props,
|
|
784
|
+
// so the action callbacks live directly on props (not under .actions).
|
|
785
|
+
const onArchive = props.onArchive, onUnarchive = props.onUnarchive;
|
|
786
|
+
const onMove = props.onMove, onRemove = props.onRemove;
|
|
787
|
+
// Archive state lives in the workspace registry's archivedSessionIds
|
|
788
|
+
// (exposed through the global standard kit's useWorkspaces selector).
|
|
789
|
+
const useWorkspaces = props.useWorkspaces;
|
|
790
|
+
const archivedIds = (useWorkspaces ? useWorkspaces((s) => s.archivedSessionIds) : null) || [];
|
|
791
|
+
const isArchived = archivedIds.includes(sessionId);
|
|
792
|
+
|
|
793
|
+
const [confirmFor, setConfirmFor] = useState(null);
|
|
794
|
+
const [confirmAnchor, setConfirmAnchor] = useState(null);
|
|
795
|
+
const [moveFor, setMoveFor] = useState(null);
|
|
796
|
+
const [moveAnchor, setMoveAnchor] = useState(null);
|
|
797
|
+
const [workspaces, setWorkspaces] = useState([]);
|
|
798
|
+
const runWithAlert = (label, fn) => {
|
|
799
|
+
return Promise.resolve()
|
|
800
|
+
.then(() => fn())
|
|
801
|
+
.catch((e) => {
|
|
802
|
+
try {
|
|
803
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
804
|
+
window.alert(((t && t("error.operation")) || "操作失败:{message}").replace("{message}", msg));
|
|
805
|
+
} catch (_) {}
|
|
806
|
+
});
|
|
807
|
+
};
|
|
808
|
+
useEffect(() => {
|
|
809
|
+
if (!moveFor) { setWorkspaces([]); return; }
|
|
810
|
+
let alive = true;
|
|
811
|
+
fetchApi("/workspaces").then((d) => {
|
|
812
|
+
if (alive && d && d.ok) setWorkspaces(d.result.workspaces || []);
|
|
813
|
+
});
|
|
814
|
+
return () => { alive = false; };
|
|
815
|
+
}, [moveFor ? (moveFor.id || "x") : null]);
|
|
816
|
+
const anchorFor = (el) => {
|
|
817
|
+
if (!el || typeof el.getBoundingClientRect !== "function") return null;
|
|
818
|
+
const r = el.getBoundingClientRect();
|
|
819
|
+
return {
|
|
820
|
+
top: Math.round(r.top + r.height + 6),
|
|
821
|
+
left: Math.max(8, Math.round(r.right - 320))
|
|
822
|
+
};
|
|
823
|
+
};
|
|
824
|
+
const archiveLabelKey = isArchived ? "row.unarchive" : "row.archive";
|
|
825
|
+
return h(React.Fragment, null,
|
|
826
|
+
h("div", { className: "sm-header" },
|
|
827
|
+
h("button", {
|
|
828
|
+
type: "button",
|
|
829
|
+
className: "sm-headerBtn" + (isArchived ? " sm-headerBtnActive" : ""),
|
|
830
|
+
"aria-label": t ? t(archiveLabelKey) : (isArchived ? "移出归档" : "归档会话"),
|
|
831
|
+
onClick: () => runWithAlert("archive", () =>
|
|
832
|
+
isArchived ? onUnarchive(sessionId) : onArchive(sessionId)
|
|
833
|
+
)
|
|
834
|
+
}, isArchived ? (t ? t("row.unarchive") : "移出归档") : (t ? t("row.archive") : "归档")),
|
|
835
|
+
h("button", {
|
|
836
|
+
type: "button",
|
|
837
|
+
className: "sm-headerBtn" + (moveFor ? " sm-headerBtnActive" : ""),
|
|
838
|
+
"aria-label": t ? t("row.move") : "移动到工作区",
|
|
839
|
+
onClick: (e) => {
|
|
840
|
+
e.stopPropagation();
|
|
841
|
+
setConfirmFor(null);
|
|
842
|
+
setConfirmAnchor(null);
|
|
843
|
+
if (moveFor && moveFor.id === sessionId) {
|
|
844
|
+
setMoveFor(null);
|
|
845
|
+
setMoveAnchor(null);
|
|
846
|
+
} else {
|
|
847
|
+
setMoveAnchor(anchorFor(e.currentTarget));
|
|
848
|
+
setMoveFor({ id: sessionId, displayTitle: sessionId, workspaceId: "" });
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}, t ? t("row.move") : "移动"),
|
|
852
|
+
h("button", {
|
|
853
|
+
type: "button",
|
|
854
|
+
className: "sm-headerBtn sm-headerBtnDanger" + (confirmFor ? " sm-headerBtnActive" : ""),
|
|
855
|
+
"aria-label": t ? t("row.delete") : "删除会话",
|
|
856
|
+
onClick: (e) => {
|
|
857
|
+
e.stopPropagation();
|
|
858
|
+
setMoveFor(null);
|
|
859
|
+
setMoveAnchor(null);
|
|
860
|
+
if (confirmFor && confirmFor.id === sessionId) {
|
|
861
|
+
setConfirmFor(null);
|
|
862
|
+
setConfirmAnchor(null);
|
|
863
|
+
} else {
|
|
864
|
+
setConfirmAnchor(anchorFor(e.currentTarget));
|
|
865
|
+
setConfirmFor({ id: sessionId, displayTitle: sessionId });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}, t ? t("row.delete") : "删除")
|
|
460
869
|
),
|
|
461
|
-
h(
|
|
462
|
-
open,
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
870
|
+
confirmFor === null ? null : h(ConfirmDialog, {
|
|
871
|
+
open: true,
|
|
872
|
+
anchor: confirmAnchor,
|
|
873
|
+
onClose: () => setConfirmFor(null),
|
|
874
|
+
onCancel: () => setConfirmFor(null),
|
|
875
|
+
onConfirm: () => {
|
|
876
|
+
const target = confirmFor;
|
|
877
|
+
setConfirmFor(null);
|
|
878
|
+
runWithAlert("delete", () => onRemove(target.id));
|
|
879
|
+
},
|
|
880
|
+
title: t ? t("confirm.delete.title") : "删除会话",
|
|
881
|
+
description: (confirmFor && (t ? t("confirm.delete.desc", { title: confirmFor.displayTitle || confirmFor.id }) : ("将删除 " + (confirmFor.displayTitle || confirmFor.id)))) || "",
|
|
882
|
+
confirmLabel: t ? t("confirm.delete.confirm") : "确认删除",
|
|
883
|
+
cancelLabel: t ? t("confirm.cancel") : "取消",
|
|
884
|
+
busy: false
|
|
885
|
+
}),
|
|
886
|
+
moveFor === null ? null : h(MoveDialog, {
|
|
887
|
+
open: true,
|
|
888
|
+
anchor: moveAnchor,
|
|
889
|
+
t: t,
|
|
890
|
+
workspaces: workspaces,
|
|
891
|
+
currentWorkspaceId: moveFor.workspaceId || "",
|
|
892
|
+
onClose: () => { setMoveFor(null); setMoveAnchor(null); },
|
|
893
|
+
onCancel: () => { setMoveFor(null); setMoveAnchor(null); },
|
|
894
|
+
title: t ? t("confirm.move.title") : "移动会话到工作区",
|
|
895
|
+
description: (moveFor && (t ? t("confirm.move.desc", { title: moveFor.displayTitle || moveFor.id }) : "")) || "",
|
|
896
|
+
sessionTitle: moveFor.displayTitle || moveFor.id,
|
|
897
|
+
confirmLabel: t ? t("confirm.move.confirm") : "确认移动",
|
|
898
|
+
cancelLabel: t ? t("confirm.cancel") : "取消",
|
|
899
|
+
busy: false,
|
|
900
|
+
onConfirm: (targetWorkspaceId) => {
|
|
901
|
+
const target = moveFor;
|
|
902
|
+
setMoveFor(null);
|
|
903
|
+
setMoveAnchor(null);
|
|
904
|
+
runWithAlert("move", () => onMove(target.id, targetWorkspaceId));
|
|
905
|
+
}
|
|
471
906
|
})
|
|
472
907
|
);
|
|
473
908
|
}
|
|
474
909
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
910
|
+
class SafePanel extends React.Component {
|
|
911
|
+
constructor(p) { super(p); this.state = { failed: false }; }
|
|
912
|
+
getDerivedStateFromError() { return { failed: true }; }
|
|
913
|
+
componentDidCatch(error) { try { console.error("[dsh-session-manager] SessionManagerPanel crashed:", error); } catch (_) {} }
|
|
914
|
+
render() {
|
|
915
|
+
if (this.state.failed) return null;
|
|
916
|
+
return h(SessionManagerPanel, {
|
|
917
|
+
open: true,
|
|
918
|
+
onClose: () => { try { this.props.panelStore.set(false); } catch (_) {} },
|
|
919
|
+
useSessions: this.props.useSessions,
|
|
920
|
+
useWorkspaces: this.props.useWorkspaces,
|
|
921
|
+
t: this.props.t,
|
|
922
|
+
onOpen: (id) => { try { this.props.panelStore.set(false); this.props.props.onOpen(id); } catch (_) {} },
|
|
923
|
+
onArchive: this.props.props.onArchive,
|
|
924
|
+
onUnarchive: this.props.props.onUnarchive,
|
|
925
|
+
onRemove: this.props.props.onRemove,
|
|
926
|
+
onMove: this.props.props.onMove,
|
|
927
|
+
onMigrate: this.props.props.onMigrate
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function FooterAction(props) {
|
|
933
|
+
const t = props.t;
|
|
934
|
+
const onOpenPanel = props.onOpenPanel;
|
|
935
|
+
const open = useSyncExternalStore(
|
|
936
|
+
(cb) => panelStore.subscribe(cb),
|
|
937
|
+
() => panelStore.getSnapshot(),
|
|
938
|
+
() => panelStore.getSnapshot()
|
|
939
|
+
);
|
|
940
|
+
return h("div", { className: "sm-footer" },
|
|
941
|
+
h("button", { type: "button", className: "sm-footerBtn", "aria-label": t("footer.aria"), onClick: () => onOpenPanel() },
|
|
942
|
+
h(P.IconArchiveOutline20, { size: 14 }),
|
|
943
|
+
h("span", null, t("footer.label"))
|
|
944
|
+
),
|
|
945
|
+
// The panel is mounted ONLY while the user has the modal open. When
|
|
946
|
+
// The wrap guards against an in-panel crash by returning null on its
|
|
947
|
+
// own error boundary rather than letting it bubble up to the slot.
|
|
948
|
+
open ? h(SafePanel, {
|
|
949
|
+
panelStore: panelStore,
|
|
950
|
+
props: props,
|
|
951
|
+
t: t,
|
|
952
|
+
useSessions: props.useSessions,
|
|
953
|
+
useWorkspaces: props.useWorkspaces,
|
|
954
|
+
}) : null
|
|
955
|
+
);
|
|
956
|
+
}
|
|
479
957
|
|
|
480
958
|
function apply(ctx) {
|
|
481
959
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "session-manager: dictionaries");
|
|
482
|
-
|
|
483
|
-
// NOTE: the slot renderer SPREADS the inject factory's return value into
|
|
484
|
-
// the component's props as flat members (see renderEntry: ...injected).
|
|
485
|
-
// So these are delivered as onOpen/onArchive/... props, not under an
|
|
486
|
-
// `actions` namespace.
|
|
487
960
|
const actions = () => ({
|
|
488
|
-
onOpen: (sessionId) => {
|
|
489
|
-
|
|
490
|
-
},
|
|
491
|
-
onArchive: async (sessionId) => {
|
|
492
|
-
await ctx.workspaces.archiveSession(sessionId);
|
|
493
|
-
},
|
|
961
|
+
onOpen: (sessionId) => { ctx.sessions.open(sessionId); },
|
|
962
|
+
onArchive: async (sessionId) => { await ctx.workspaces.archiveSession(sessionId); },
|
|
494
963
|
onUnarchive: async (sessionId) => {
|
|
495
964
|
const response = await callApi("/unarchive", { sessionId });
|
|
496
965
|
if (!response.ok) throw new Error(response.error || "unarchive failed");
|
|
@@ -501,11 +970,46 @@ window.__ModuleLoader__.load({
|
|
|
501
970
|
const current = ctx.sessions.list.getSnapshot().current;
|
|
502
971
|
if (current === sessionId) ctx.sessions.clear();
|
|
503
972
|
},
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
973
|
+
onMove: async (sessionId, targetWorkspaceId) => {
|
|
974
|
+
const wasCurrent = ctx.sessions.list.getSnapshot().current === sessionId;
|
|
975
|
+
const response = await callApi("/move", { sessionId, targetWorkspaceId });
|
|
976
|
+
if (!response.ok) throw new Error(response.error || "move failed");
|
|
977
|
+
const refreshMovedSession = async () => {
|
|
978
|
+
await Promise.allSettled([
|
|
979
|
+
typeof ctx.workspaces?.refresh === "function" ? ctx.workspaces.refresh() : Promise.resolve(),
|
|
980
|
+
ctx.sessions.refresh()
|
|
981
|
+
]);
|
|
982
|
+
if (wasCurrent && ctx.sessions.list.getSnapshot().byId[sessionId] !== undefined) {
|
|
983
|
+
ctx.sessions.open(sessionId);
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
// /move retires a live session, so host/session-removed can arrive on
|
|
987
|
+
// the streaming channel slightly after the HTTP success response.
|
|
988
|
+
// Pull both projections now and once more after that frame to make the
|
|
989
|
+
// destination row appear without a browser refresh.
|
|
990
|
+
await refreshMovedSession();
|
|
991
|
+
setTimeout(() => { void refreshMovedSession(); }, 250);
|
|
992
|
+
setTimeout(() => { void refreshMovedSession(); }, 900);
|
|
993
|
+
},
|
|
994
|
+
onMigrate: async (sessionId, toPreset) => {
|
|
995
|
+
const wasCurrent = ctx.sessions.list.getSnapshot().current === sessionId;
|
|
996
|
+
const response = await callApi("/preset-migrate", { sessionId, toPreset });
|
|
997
|
+
if (!response.ok) throw new Error(response.error || "preset migration failed");
|
|
998
|
+
ctx.sessions.noteAgentPreset(sessionId, toPreset);
|
|
999
|
+
await ctx.sessions.refresh();
|
|
1000
|
+
if (wasCurrent) ctx.sessions.open(sessionId);
|
|
1001
|
+
// Host/session-removed and the HTTP response travel on different
|
|
1002
|
+
// streams. One delayed baseline closes that ordering race without a
|
|
1003
|
+
// page refresh and keeps the migrated conversation selectable.
|
|
1004
|
+
setTimeout(() => {
|
|
1005
|
+
void ctx.sessions.refresh().then(() => {
|
|
1006
|
+
ctx.sessions.noteAgentPreset(sessionId, toPreset);
|
|
1007
|
+
if (wasCurrent) ctx.sessions.open(sessionId);
|
|
1008
|
+
}).catch(() => {});
|
|
1009
|
+
}, 250);
|
|
1010
|
+
},
|
|
1011
|
+
onOpenPanel: () => panelStore.set(true)
|
|
507
1012
|
});
|
|
508
|
-
|
|
509
1013
|
ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
|
|
510
1014
|
name: "conversation.session.header.actions",
|
|
511
1015
|
id: "session-manager-header",
|
|
@@ -513,7 +1017,6 @@ window.__ModuleLoader__.load({
|
|
|
513
1017
|
locale: NS,
|
|
514
1018
|
inject: () => actions()
|
|
515
1019
|
}, HeaderAction));
|
|
516
|
-
|
|
517
1020
|
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
518
1021
|
name: "sidebar.footer.action",
|
|
519
1022
|
id: "session-manager-footer",
|
|
@@ -523,8 +1026,10 @@ window.__ModuleLoader__.load({
|
|
|
523
1026
|
}, FooterAction));
|
|
524
1027
|
}
|
|
525
1028
|
|
|
1029
|
+
const inject = ["slots", "sessions", "workspaces", "locale"];
|
|
1030
|
+
|
|
526
1031
|
exports.apply = apply;
|
|
527
1032
|
exports.inject = inject;
|
|
528
1033
|
return module.exports;
|
|
529
1034
|
}
|
|
530
|
-
});
|
|
1035
|
+
});
|