dsh-session-manager 0.1.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -77
- package/README.zh.md +117 -0
- package/lib/client.js +855 -348
- package/lib/index.js +1043 -5
- package/package.json +10 -5
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,145 @@ 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-headerDanger{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}",
|
|
153
|
+
".sm-headerDanger:hover:not(:disabled){background:rgba(217,78,68,.1)}",
|
|
154
|
+
".sm-busy{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:28px;margin-left:4px}",
|
|
155
|
+
".sm-footer{display:flex}",
|
|
156
|
+
".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}",
|
|
157
|
+
".sm-footerBtn:hover,.sm-footerBtn:focus-visible{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
158
|
+
".sm-footerBtn svg{flex:none}",
|
|
159
|
+
".sm-panel{display:flex;flex-direction:column;gap:10px;min-height:280px;max-height:min(70vh,640px);min-width:480px}",
|
|
160
|
+
".sm-filter{display:flex;gap:4px;flex-wrap:wrap}",
|
|
161
|
+
".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}",
|
|
162
|
+
".sm-filterBtn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
163
|
+
".sm-filterBtn.sm-on{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-fill-l2);border-color:transparent}",
|
|
164
|
+
".sm-list{display:flex;flex-direction:column;gap:4px;min-height:0;overflow:auto;flex:1}",
|
|
165
|
+
".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}",
|
|
166
|
+
".sm-row:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
167
|
+
".sm-rowCurrent{border-color:var(--dsw-alias-accent-primary,var(--dsw-alias-label-secondary))}",
|
|
168
|
+
".sm-rowMain{min-width:0;display:flex;flex-direction:column;gap:2px;flex:1 1 280px}",
|
|
169
|
+
".sm-rowTitle{color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px;display:flex;align-items:center;gap:6px;min-width:0}",
|
|
170
|
+
".sm-rowTitle>span:first-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}",
|
|
171
|
+
".sm-rowMeta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}",
|
|
172
|
+
".sm-rowActions{display:flex;gap:4px;flex:none;flex-wrap:wrap;justify-content:flex-end}",
|
|
173
|
+
".sm-badge{flex:none;border-radius:999px;padding:0 6px;font-size:10px;line-height:16px}",
|
|
174
|
+
".sm-badgeArchived{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l2)}",
|
|
175
|
+
".sm-badgeRunning{color:var(--dsw-alias-state-warning-primary,#d97706);background:rgba(217,119,6,.12)}",
|
|
176
|
+
".sm-badgeCurrent{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-fill-l2)}",
|
|
177
|
+
".sm-danger{color:var(--dsw-alias-state-error-primary) !important;border-color:var(--dsw-alias-state-error-primary) !important}",
|
|
178
|
+
".sm-dangerText{color:var(--dsw-alias-state-error-primary) !important}",
|
|
179
|
+
".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}",
|
|
180
|
+
".sm-warn svg{flex:none;margin-top:1px}",
|
|
181
|
+
".sm-error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;min-height:0;margin-top:4px}",
|
|
182
|
+
".sm-foot{display:flex;justify-content:flex-end;align-items:center;gap:8px;width:100%}",
|
|
183
|
+
".sm-footError{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;margin-right:auto}",
|
|
184
|
+
".sm-empty{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px;text-align:center;padding:24px 0}",
|
|
185
|
+
".sm-moveSection{display:flex;flex-direction:column;gap:8px}",
|
|
186
|
+
".sm-moveLabel{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}",
|
|
187
|
+
".sm-moveList{display:flex;flex-direction:column;gap:4px;max-height:200px;overflow:auto}",
|
|
188
|
+
".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}",
|
|
189
|
+
".sm-moveItem:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
190
|
+
".sm-moveItemSelected{border-color:var(--dsw-alias-accent-primary);background:var(--dsw-alias-accent-primary-bg)}",
|
|
191
|
+
".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{background:var(--dsw-alias-state-error-secondary);color:#fff;border-color:var(--dsw-alias-state-error-primary)}.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:center;gap:8px;min-width:0;flex:1 1 auto;overflow:hidden}.sm-panelDialog .sm-rowTitle{min-width:0;flex:1 1 auto;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;overflow:hidden}.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{text-overflow:ellipsis;white-space:nowrap;overflow:hidden;flex:1 1 auto}.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
|
+
".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
|
+
].join("");
|
|
171
196
|
|
|
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;
|
|
197
|
+
const CSS_ID = "dsh-session-manager/session-manager";
|
|
198
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(CSS_ID) + "]") === null) {
|
|
199
|
+
const tag = document.createElement("style");
|
|
200
|
+
tag.dataset.plugin = "dsh-session-manager";
|
|
201
|
+
tag.dataset.pluginCss = CSS_ID;
|
|
202
|
+
tag.textContent = css;
|
|
203
|
+
document.head.appendChild(tag);
|
|
188
204
|
}
|
|
189
205
|
|
|
190
|
-
// ---------------------------------------------------------------------
|
|
191
|
-
// Small helpers
|
|
192
|
-
// ---------------------------------------------------------------------
|
|
193
|
-
const h = (type, props, ...children) => React.createElement(type, props, ...children);
|
|
194
206
|
|
|
195
|
-
|
|
207
|
+
// Anchor the session manager panel to the sidebar right edge so it
|
|
208
|
+
// always fits within the sidebar area, regardless of viewport size.
|
|
209
|
+
const SM_PANEL_CSS_ID = "dsh-session-manager/session-manager-anchor";
|
|
210
|
+
const installAnchor = () => {
|
|
211
|
+
if (typeof document === "undefined") return;
|
|
212
|
+
if (document.querySelector("style[data-plugin-css=" + JSON.stringify(SM_PANEL_CSS_ID) + "]") !== null) return;
|
|
213
|
+
const tag = document.createElement("style");
|
|
214
|
+
tag.dataset.plugin = "dsh-session-manager";
|
|
215
|
+
tag.dataset.pluginCss = SM_PANEL_CSS_ID;
|
|
216
|
+
tag.textContent = ":root{--sm-panel-left:8px}";
|
|
217
|
+
document.head.appendChild(tag);
|
|
218
|
+
const updateLeft = () => {
|
|
219
|
+
let right = 8;
|
|
220
|
+
const candidates = [
|
|
221
|
+
"[data-dsh-sidebar]",
|
|
222
|
+
"[data-testid=sidebar]",
|
|
223
|
+
"[aria-label=会话][role=navigation]",
|
|
224
|
+
"aside",
|
|
225
|
+
".dsw-sidebar"
|
|
226
|
+
];
|
|
227
|
+
for (const sel of candidates) {
|
|
228
|
+
const el = document.querySelector(sel);
|
|
229
|
+
if (el !== null) {
|
|
230
|
+
const rect = el.getBoundingClientRect();
|
|
231
|
+
if (rect.right > 100 && rect.right < window.innerWidth - 40) {
|
|
232
|
+
right = Math.round(rect.right) + 8;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
document.documentElement.style.setProperty("--sm-panel-left", right + "px");
|
|
238
|
+
};
|
|
239
|
+
updateLeft();
|
|
240
|
+
window.addEventListener("resize", updateLeft);
|
|
241
|
+
};
|
|
242
|
+
try { installAnchor(); } catch (_) {}
|
|
243
|
+
|
|
244
|
+
const h = (type, props, ...children) => React.createElement(type, props, ...children);
|
|
245
|
+
const callApi = (path, payload) => fetch(API + path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload || {}) }).then((r) => r.json());
|
|
246
|
+
const fetchApi = (path) => fetch(API + path).then((r) => r.json());
|
|
247
|
+
const formatRelative = (ts, now) => {
|
|
196
248
|
const diff = Math.max(0, now - ts);
|
|
197
249
|
const s = Math.floor(diff / 1000);
|
|
198
250
|
if (s < 60) return s + "s";
|
|
@@ -205,147 +257,361 @@ window.__ModuleLoader__.load({
|
|
|
205
257
|
const mo = Math.floor(d / 30);
|
|
206
258
|
if (mo < 12) return mo + "mo";
|
|
207
259
|
return Math.floor(mo / 12) + "y";
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function shortPath(cwd) {
|
|
260
|
+
};
|
|
261
|
+
const shortPath = (cwd) => {
|
|
211
262
|
if (!cwd) return "";
|
|
212
|
-
const parts = cwd.replace(/[
|
|
263
|
+
const parts = cwd.replace(/[\\\/]+$/, "").split(/[\\\/]/);
|
|
213
264
|
const last = parts[parts.length - 1] || "";
|
|
214
|
-
return last.length > 26 ? last.slice(0, 24) + "
|
|
215
|
-
}
|
|
265
|
+
return last.length > 26 ? last.slice(0, 24) + "\u2026" : last;
|
|
266
|
+
};
|
|
216
267
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
268
|
+
function ConfirmDialog(props) {
|
|
269
|
+
const open = props.open === true;
|
|
270
|
+
const onCancel = typeof props.onCancel === "function" ? props.onCancel : () => {};
|
|
271
|
+
const onConfirm = typeof props.onConfirm === "function" ? props.onConfirm : () => {};
|
|
272
|
+
const busy = props.busy === true;
|
|
273
|
+
const title = props.title || "";
|
|
274
|
+
const description = props.description || "";
|
|
275
|
+
const warning = props.warning || "";
|
|
276
|
+
const confirmLabel = props.confirmLabel || "确认";
|
|
277
|
+
const cancelLabel = props.cancelLabel || "取消";
|
|
278
|
+
const R = (typeof window !== "undefined") ? window.__smReact : null;
|
|
279
|
+
const createPortal = (R && typeof R.createPortal === "function") ? R.createPortal : null;
|
|
280
|
+
const body = (typeof document === "undefined") ? null : document.body;
|
|
281
|
+
if (!open) return null;
|
|
282
|
+
if (!body) return tree || null;
|
|
283
|
+
const tree = h("div", {
|
|
284
|
+
className: "sm-nativeDialogLayer sm-confirmDialog",
|
|
285
|
+
style: props.anchor && typeof props.anchor.top === "number"
|
|
286
|
+
? { position: "fixed", top: props.anchor.top + "px", left: props.anchor.left + "px", zIndex: 10001 }
|
|
287
|
+
: null,
|
|
288
|
+
role: "presentation",
|
|
289
|
+
onKeyDown: (e) => {
|
|
290
|
+
if (e.key === "Escape" && !busy) { e.preventDefault(); onCancel(); }
|
|
291
|
+
else if (e.key === "Enter" && !busy) { e.preventDefault(); onConfirm(); }
|
|
225
292
|
},
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
293
|
+
tabIndex: -1
|
|
294
|
+
},
|
|
295
|
+
h("div", {
|
|
296
|
+
className: "sm-nativeDialogBackdrop",
|
|
297
|
+
onMouseDown: (e) => { if (e.target === e.currentTarget && !busy) onCancel(); }
|
|
298
|
+
}),
|
|
299
|
+
h("section", {
|
|
300
|
+
className: "sm-nativeDialog",
|
|
301
|
+
role: "dialog",
|
|
302
|
+
"aria-modal": "true",
|
|
303
|
+
"aria-labelledby": "sm-confirm-dialog-title",
|
|
304
|
+
ref: (el) => { if (el && typeof el.focus === "function") el.focus(); }
|
|
305
|
+
},
|
|
306
|
+
h("div", { className: "sm-nativeDialogHeader" },
|
|
307
|
+
h("h2", { id: "sm-confirm-dialog-title", className: "sm-nativeDialogTitle" }, title)
|
|
308
|
+
),
|
|
309
|
+
h("div", { className: "sm-nativeDialogBody" },
|
|
310
|
+
description ? h("div", { className: "sm-nativeDialogDescription" }, description) : null,
|
|
311
|
+
warning ? h("div", { className: "sm-warn" }, h("span", null, warning)) : null
|
|
312
|
+
),
|
|
313
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
314
|
+
h("button", {
|
|
315
|
+
type: "button",
|
|
316
|
+
className: "sm-nativeDialogButton sm-nativeDialogCancel",
|
|
317
|
+
disabled: busy,
|
|
318
|
+
onClick: () => { if (!busy) onCancel(); }
|
|
319
|
+
}, cancelLabel),
|
|
320
|
+
h("button", {
|
|
321
|
+
type: "button",
|
|
322
|
+
className: "sm-nativeDialogButton sm-nativeDialogConfirm",
|
|
323
|
+
disabled: busy,
|
|
324
|
+
onClick: () => { if (!busy) onConfirm(); }
|
|
325
|
+
}, confirmLabel)
|
|
326
|
+
)
|
|
239
327
|
)
|
|
240
|
-
|
|
328
|
+
);
|
|
329
|
+
if (createPortal) return createPortal(tree, body);
|
|
330
|
+
// Default: render inline. position:fixed escapes the sidebar
|
|
331
|
+
// overflow:hidden ancestor at the DOM/CSS level, so the dialog
|
|
332
|
+
// appears next to the panel. Keeping the dialog in the same
|
|
333
|
+
// React tree as the panel preserves the onClick wiring (cancel /
|
|
334
|
+
// confirm call setConfirmFor, which only works when the click
|
|
335
|
+
// handler lives in the same root).
|
|
336
|
+
return tree;
|
|
241
337
|
}
|
|
242
338
|
|
|
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);
|
|
339
|
+
function MoveDialog(props) {
|
|
340
|
+
const t = typeof props.t === "function" ? props.t : (key) => key;
|
|
341
|
+
const open = props.open === true;
|
|
342
|
+
const onCancel = typeof props.onCancel === "function" ? props.onCancel : () => {};
|
|
343
|
+
const onConfirm = typeof props.onConfirm === "function" ? props.onConfirm : () => {};
|
|
344
|
+
const busy = props.busy === true;
|
|
345
|
+
const title = props.title || t("confirm.move.title");
|
|
346
|
+
const description = props.description || "";
|
|
347
|
+
const currentWorkspaceId = props.currentWorkspaceId || "";
|
|
348
|
+
const confirmLabel = props.confirmLabel || t("confirm.move.confirm");
|
|
349
|
+
const cancelLabel = props.cancelLabel || t("confirm.cancel");
|
|
350
|
+
const error = props.error || "";
|
|
351
|
+
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState("");
|
|
352
|
+
if (!open) return null;
|
|
258
353
|
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
354
|
+
const list = (Array.isArray(props.workspaces) ? props.workspaces : [])
|
|
355
|
+
.filter((ws) => ws && typeof ws === "object" && typeof ws.id === "string" && ws.id !== currentWorkspaceId);
|
|
356
|
+
const body = h("div", { className: "sm-nativeDialogBody" },
|
|
357
|
+
h("div", { className: "sm-nativeDialogDescription" }, description),
|
|
358
|
+
list.length === 0
|
|
359
|
+
? h("div", { className: "sm-moveLabel" }, t("confirm.move.empty"))
|
|
360
|
+
: h("div", { className: "sm-moveSection" },
|
|
361
|
+
h("div", { className: "sm-moveLabel" }, t("confirm.move.select")),
|
|
362
|
+
h("div", { className: "sm-moveList", role: "listbox", "aria-label": t("confirm.move.select") },
|
|
363
|
+
list.map((ws) => h("button", {
|
|
364
|
+
key: ws.id,
|
|
365
|
+
type: "button",
|
|
366
|
+
role: "option",
|
|
367
|
+
"aria-selected": selectedWorkspaceId === ws.id,
|
|
368
|
+
className: "sm-moveItem" + (selectedWorkspaceId === ws.id ? " sm-moveItemSelected" : ""),
|
|
369
|
+
disabled: busy,
|
|
370
|
+
onClick: () => setSelectedWorkspaceId(ws.id)
|
|
371
|
+
},
|
|
372
|
+
h("span", { className: "sm-moveItemName" }, String(ws.title || ws.name || ws.id)),
|
|
373
|
+
h("span", { className: "sm-moveItemMeta" }, ws.path ? String(ws.path) : "")
|
|
374
|
+
))
|
|
375
|
+
)
|
|
376
|
+
),
|
|
377
|
+
error ? h("div", { className: "sm-error", role: "alert" }, String(error)) : null
|
|
378
|
+
);
|
|
379
|
+
// Single cancel button (footer) + ESC handler. No "x" close button so the
|
|
380
|
+
// user only sees one cancel entry point.
|
|
381
|
+
const onKeyDown = (e) => {
|
|
382
|
+
if (e.key === "Escape" && !busy) {
|
|
383
|
+
e.preventDefault();
|
|
384
|
+
onCancel();
|
|
385
|
+
} else if (e.key === "Enter" && !busy && selectedWorkspaceId && list.length > 0) {
|
|
386
|
+
e.preventDefault();
|
|
387
|
+
onConfirm(selectedWorkspaceId);
|
|
269
388
|
}
|
|
270
389
|
};
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
390
|
+
return h("div", {
|
|
391
|
+
className: "sm-nativeDialogLayer sm-confirmDialog",
|
|
392
|
+
style: props.anchor && typeof props.anchor.top === "number"
|
|
393
|
+
? { position: "fixed", top: props.anchor.top + "px", left: props.anchor.left + "px", zIndex: 10001 }
|
|
394
|
+
: null,
|
|
395
|
+
role: "presentation",
|
|
396
|
+
onKeyDown,
|
|
397
|
+
tabIndex: -1
|
|
398
|
+
},
|
|
399
|
+
h("div", {
|
|
400
|
+
className: "sm-nativeDialogBackdrop",
|
|
401
|
+
onMouseDown: (e) => { if (e.target === e.currentTarget && !busy) onCancel(); }
|
|
402
|
+
}),
|
|
403
|
+
h("section", {
|
|
404
|
+
className: "sm-nativeDialog",
|
|
405
|
+
role: "dialog",
|
|
406
|
+
"aria-modal": "true",
|
|
407
|
+
"aria-labelledby": "sm-move-dialog-title",
|
|
408
|
+
ref: (el) => { if (el && typeof el.focus === "function") el.focus(); }
|
|
282
409
|
},
|
|
283
|
-
h(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
410
|
+
h("div", { className: "sm-nativeDialogHeader" },
|
|
411
|
+
h("h2", { id: "sm-move-dialog-title", className: "sm-nativeDialogTitle" }, title)
|
|
412
|
+
),
|
|
413
|
+
body,
|
|
414
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
415
|
+
h("button", {
|
|
416
|
+
type: "button",
|
|
417
|
+
className: "sm-nativeDialogButton sm-nativeDialogCancel",
|
|
418
|
+
disabled: busy,
|
|
419
|
+
onClick: onCancel
|
|
420
|
+
}, cancelLabel),
|
|
421
|
+
h("button", {
|
|
422
|
+
type: "button",
|
|
423
|
+
className: "sm-nativeDialogButton sm-nativeDialogConfirm",
|
|
424
|
+
disabled: busy || !selectedWorkspaceId || list.length === 0,
|
|
425
|
+
onClick: () => { if (!busy && selectedWorkspaceId) onConfirm(selectedWorkspaceId); }
|
|
426
|
+
}, confirmLabel)
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function MigratePresetDialog(props) {
|
|
433
|
+
const t = typeof props.t === "function" ? props.t : (key) => key;
|
|
434
|
+
const open = props.open === true;
|
|
435
|
+
const onCancel = typeof props.onCancel === "function" ? props.onCancel : () => {};
|
|
436
|
+
const onRun = typeof props.onRun === "function" ? props.onRun : () => {};
|
|
437
|
+
const busy = props.busy === true;
|
|
438
|
+
const sessionId = props.sessionId || "";
|
|
439
|
+
const fallbackPreset = props.currentPreset || "";
|
|
440
|
+
const [scan, setScan] = useState(null);
|
|
441
|
+
const [target, setTarget] = useState("");
|
|
442
|
+
useEffect(() => {
|
|
443
|
+
if (!open || !sessionId) return;
|
|
444
|
+
let alive = true;
|
|
445
|
+
setScan(null);
|
|
446
|
+
setTarget("");
|
|
447
|
+
fetchApi("/preset-scan?sessionId=" + encodeURIComponent(sessionId)).then((d) => {
|
|
448
|
+
if (!alive) return;
|
|
449
|
+
if (d && d.ok) {
|
|
450
|
+
const result = d.result || {};
|
|
451
|
+
const rows = Array.isArray(result.rows)
|
|
452
|
+
? result.rows
|
|
453
|
+
: (result.rows && Array.isArray(result.rows.rows) ? result.rows.rows : []);
|
|
454
|
+
const availablePresets = Array.isArray(result.availablePresets)
|
|
455
|
+
? result.availablePresets
|
|
456
|
+
: (result.rows && Array.isArray(result.rows.availablePresets) ? result.rows.availablePresets : []);
|
|
457
|
+
const row = rows.find((item) => item && item.sessionId === sessionId) || null;
|
|
458
|
+
setScan({ ok: true, row, availablePresets });
|
|
459
|
+
} else {
|
|
460
|
+
setScan({ ok: false, error: (d && d.error) || "scan failed" });
|
|
294
461
|
}
|
|
462
|
+
}).catch((error) => {
|
|
463
|
+
if (alive) setScan({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
464
|
+
});
|
|
465
|
+
return () => { alive = false; };
|
|
466
|
+
}, [open, sessionId]);
|
|
467
|
+
if (!open) return null;
|
|
468
|
+
|
|
469
|
+
const currentPreset = (scan && scan.ok && scan.row && (scan.row.finalPreset || scan.row.headerPreset)) || fallbackPreset || "—";
|
|
470
|
+
const presets = scan && scan.ok
|
|
471
|
+
? Array.from(new Set((scan.availablePresets || []).filter((preset) => typeof preset === "string" && preset.length > 0))).sort()
|
|
472
|
+
: [];
|
|
473
|
+
const targets = presets.filter((preset) => preset !== currentPreset);
|
|
474
|
+
const ready = target !== "" && target !== currentPreset;
|
|
475
|
+
const body = h("div", { className: "sm-migratePanel" },
|
|
476
|
+
scan === null
|
|
477
|
+
? h("div", { className: "sm-empty" }, t("busy.processing"))
|
|
478
|
+
: !scan.ok
|
|
479
|
+
? h("div", { className: "sm-error", role: "alert" }, scan.error || "scan failed")
|
|
480
|
+
: h(React.Fragment, null,
|
|
481
|
+
h("div", { className: "sm-migrateRow" },
|
|
482
|
+
h("label", null, t("migrate.current")),
|
|
483
|
+
h("div", { className: "sm-migrateCurrent" }, currentPreset)
|
|
484
|
+
),
|
|
485
|
+
h("div", { className: "sm-migrateRow" },
|
|
486
|
+
h("label", null, t("migrate.target")),
|
|
487
|
+
targets.length === 0
|
|
488
|
+
? h("div", { className: "sm-migrateEmpty" }, t("migrate.noPresets"))
|
|
489
|
+
: h("div", { className: "sm-migrateChips" }, targets.map((preset) => h("button", {
|
|
490
|
+
key: preset,
|
|
491
|
+
type: "button",
|
|
492
|
+
className: "sm-migrateChip" + (target === preset ? " sm-migrateChipOn" : ""),
|
|
493
|
+
disabled: busy,
|
|
494
|
+
onClick: () => setTarget(preset)
|
|
495
|
+
}, preset)))
|
|
496
|
+
),
|
|
497
|
+
target === currentPreset ? h("div", { className: "sm-migrateWarn" }, t("migrate.same")) : null
|
|
498
|
+
)
|
|
499
|
+
);
|
|
500
|
+
return h("div", {
|
|
501
|
+
className: "sm-nativeDialogLayer sm-confirmDialog sm-migrateConfirmLayer",
|
|
502
|
+
role: "presentation",
|
|
503
|
+
onKeyDown: (event) => { if (event.key === "Escape" && !busy) { event.preventDefault(); onCancel(); } }
|
|
504
|
+
},
|
|
505
|
+
h("div", {
|
|
506
|
+
className: "sm-nativeDialogBackdrop",
|
|
507
|
+
onMouseDown: (event) => { if (event.target === event.currentTarget && !busy) onCancel(); }
|
|
508
|
+
}),
|
|
509
|
+
h("section", {
|
|
510
|
+
className: "sm-nativeDialog sm-migrateDialog",
|
|
511
|
+
role: "dialog",
|
|
512
|
+
"aria-modal": "true",
|
|
513
|
+
"aria-labelledby": "sm-migrate-dialog-title"
|
|
295
514
|
},
|
|
296
|
-
h("
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
515
|
+
h("div", { className: "sm-nativeDialogHeader" },
|
|
516
|
+
h("h2", { id: "sm-migrate-dialog-title", className: "sm-nativeDialogTitle" }, t("migrate.title"))
|
|
517
|
+
),
|
|
518
|
+
body,
|
|
519
|
+
h("div", { className: "sm-nativeDialogFooter" },
|
|
520
|
+
h("button", {
|
|
521
|
+
type: "button",
|
|
522
|
+
className: "sm-nativeDialogButton sm-nativeDialogCancel",
|
|
523
|
+
disabled: busy,
|
|
524
|
+
onClick: onCancel
|
|
525
|
+
}, t("confirm.cancel")),
|
|
526
|
+
h("button", {
|
|
527
|
+
type: "button",
|
|
528
|
+
className: "sm-nativeDialogButton sm-nativeDialogConfirm",
|
|
529
|
+
disabled: busy || !ready,
|
|
530
|
+
onClick: () => { if (ready) onRun(target); }
|
|
531
|
+
}, busy ? t("busy.processing") : t("migrate.confirm"))
|
|
532
|
+
)
|
|
533
|
+
)
|
|
314
534
|
);
|
|
315
535
|
}
|
|
316
536
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
537
|
+
const panelStore = {
|
|
538
|
+
open: false,
|
|
539
|
+
listeners: new Set(),
|
|
540
|
+
getSnapshot() { return this.open; },
|
|
541
|
+
subscribe(cb) { this.listeners.add(cb); return () => this.listeners.delete(cb); },
|
|
542
|
+
set(v) { if (this.open === v) return; this.open = v; for (const fn of [...this.listeners]) fn(); },
|
|
543
|
+
// Pending action queued by the title-bar buttons (e.g. 移动至工作区).
|
|
544
|
+
// The SessionManagerPanel consumes this on mount so it can pre-open
|
|
545
|
+
// the matching inner dialog (move / delete / migrate) next to itself.
|
|
546
|
+
pendingMove: null,
|
|
547
|
+
pendingListeners: new Set(),
|
|
548
|
+
_emitPending() { for (const fn of [...this.pendingListeners]) fn(); },
|
|
549
|
+
subscribePending(cb) { this.pendingListeners.add(cb); return () => this.pendingListeners.delete(cb); },
|
|
550
|
+
setPendingMove(v) { this.pendingMove = v; this._emitPending(); },
|
|
551
|
+
consumePendingMove() { const v = this.pendingMove; this.pendingMove = null; return v; }
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
function SessionManagerPanel(props) {
|
|
555
|
+
const t = props.t;
|
|
556
|
+
const open = props.open;
|
|
557
|
+
const onClose = props.onClose;
|
|
558
|
+
const useSessions = props.useSessions;
|
|
559
|
+
const useWorkspaces = props.useWorkspaces;
|
|
560
|
+
const onOpen = props.onOpen;
|
|
561
|
+
const onArchive = props.onArchive;
|
|
562
|
+
const onUnarchive = props.onUnarchive;
|
|
563
|
+
const onRemove = props.onRemove;
|
|
564
|
+
const onMove = props.onMove;
|
|
565
|
+
const onMigrate = props.onMigrate;
|
|
321
566
|
const list = useSessions((s) => s);
|
|
322
567
|
const archivedIds = useWorkspaces((s) => s.archivedSessionIds);
|
|
323
568
|
const [filter, setFilter] = useState("all");
|
|
324
|
-
const [confirm, setConfirm] = useState(null);
|
|
325
569
|
const [busyId, setBusyId] = useState(null);
|
|
326
570
|
const [error, setError] = useState(null);
|
|
327
571
|
const [now, setNow] = useState(() => Date.now());
|
|
328
|
-
|
|
572
|
+
const [moveFor, setMoveFor] = useState(null);
|
|
573
|
+
const [confirmFor, setConfirmFor] = useState(null);
|
|
574
|
+
const [workspaces, setWorkspaces] = useState([]);
|
|
575
|
+
const [migrating, setMigrating] = useState(false);
|
|
576
|
+
const [migrateFor, setMigrateFor] = useState(null);
|
|
329
577
|
useEffect(() => {
|
|
330
578
|
const timer = setInterval(() => setNow(Date.now()), 30000);
|
|
331
579
|
return () => clearInterval(timer);
|
|
332
580
|
}, []);
|
|
333
|
-
|
|
581
|
+
useEffect(() => {
|
|
582
|
+
if (!open) return;
|
|
583
|
+
let alive = true;
|
|
584
|
+
fetchApi("/workspaces").then((data) => {
|
|
585
|
+
if (alive && data.ok) setWorkspaces(data.result.workspaces || []);
|
|
586
|
+
});
|
|
587
|
+
return () => { alive = false; };
|
|
588
|
+
}, [open]);
|
|
589
|
+
// Drain any pending action queued by a header button. The panel is
|
|
590
|
+
// the single owner of every inner dialog, so opening it with a
|
|
591
|
+
// pending action pre-populates the corresponding dialog state.
|
|
592
|
+
useEffect(() => {
|
|
593
|
+
if (!open) return;
|
|
594
|
+
const pending = panelStore.consumePendingMove();
|
|
595
|
+
if (pending && pending.sessionId) {
|
|
596
|
+
setMoveFor({ id: pending.sessionId, displayTitle: pending.displayTitle || pending.sessionId, workspaceId: pending.workspaceId || "" });
|
|
597
|
+
}
|
|
598
|
+
}, [open]);
|
|
599
|
+
useEffect(() => {
|
|
600
|
+
if (moveFor) {
|
|
601
|
+
fetchApi("/workspaces").then((data) => {
|
|
602
|
+
if (data.ok) setWorkspaces(data.result.workspaces || []);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}, [moveFor]);
|
|
334
606
|
const archived = useMemo(() => new Set(archivedIds), [archivedIds]);
|
|
335
|
-
|
|
336
607
|
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
608
|
const all = list.ids
|
|
342
609
|
.map((id) => list.byId[id])
|
|
343
|
-
.filter((s) => s !==
|
|
610
|
+
.filter((s) => s !== undefined && s.origin !== "subagent" && (!s.blank || s.id === list.current));
|
|
344
611
|
if (filter === "active") return all.filter((s) => !archived.has(s.id));
|
|
345
612
|
if (filter === "archived") return all.filter((s) => archived.has(s.id));
|
|
346
613
|
return all;
|
|
347
614
|
}, [list, archived, filter]);
|
|
348
|
-
|
|
349
615
|
const runRow = async (sessionId, fn) => {
|
|
350
616
|
if (busyId !== null) return;
|
|
351
617
|
setBusyId(sessionId);
|
|
@@ -358,139 +624,344 @@ window.__ModuleLoader__.load({
|
|
|
358
624
|
setBusyId(null);
|
|
359
625
|
}
|
|
360
626
|
};
|
|
361
|
-
|
|
362
627
|
const filterBtn = (id, label) => h("button", {
|
|
363
628
|
type: "button",
|
|
364
629
|
key: id,
|
|
365
630
|
className: "sm-filterBtn" + (filter === id ? " sm-on" : ""),
|
|
366
631
|
onClick: () => setFilter(id)
|
|
367
632
|
}, label);
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
h("div", { className: "sm-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
633
|
+
const renderRow = (s) => {
|
|
634
|
+
const isArchived = archived.has(s.id);
|
|
635
|
+
const isCurrent = s.id === list.current;
|
|
636
|
+
const busy = busyId === s.id;
|
|
637
|
+
return h("div", {
|
|
638
|
+
key: s.id,
|
|
639
|
+
className: "sm-row" + (isCurrent ? " sm-rowCurrent" : "")
|
|
640
|
+
},
|
|
641
|
+
h("div", { className: "sm-rowMain" },
|
|
642
|
+
h("div", { className: "sm-rowTitle" },
|
|
643
|
+
h("span", null, s.blank ? t("panel.session.new") : (s.displayTitle || s.id)),
|
|
644
|
+
isArchived ? h("span", { className: "sm-badge sm-badgeArchived" }, t("panel.archived")) : null,
|
|
645
|
+
s.running ? h("span", { className: "sm-badge sm-badgeRunning" }, t("panel.running")) : null,
|
|
646
|
+
isCurrent ? h("span", { className: "sm-badge sm-badgeCurrent" }, t("panel.current")) : null
|
|
647
|
+
),
|
|
648
|
+
h("div", { className: "sm-rowMeta" }, shortPath(s.cwd) + (s.cwd ? " · " : "") + formatRelative(s.updatedAt, now))
|
|
380
649
|
),
|
|
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
|
-
|
|
650
|
+
h("div", { className: "sm-rowActions" },
|
|
651
|
+
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => onOpen(s.id) }, t("row.open")),
|
|
652
|
+
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")),
|
|
653
|
+
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => setMoveFor(s) }, t("row.move")),
|
|
654
|
+
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => setMigrateFor(s) }, t("row.migrate")),
|
|
655
|
+
h("button", { type: "button", className: "sm-rowBtn sm-rowBtnDanger", disabled: busy, onClick: () => setConfirmFor(s) }, t("row.delete"))
|
|
656
|
+
)
|
|
657
|
+
);
|
|
658
|
+
};
|
|
659
|
+
const rowsList = rows.length === 0
|
|
660
|
+
? h("div", { className: "sm-empty" }, filter === "archived" ? t("panel.empty.archived") : t("panel.empty"))
|
|
661
|
+
: h("div", { className: "sm-list" }, rows.map(renderRow));
|
|
662
|
+
const onPanelClose = () => {
|
|
663
|
+
if (busyId !== null) return;
|
|
664
|
+
setConfirmFor(null);
|
|
665
|
+
setMoveFor(null);
|
|
666
|
+
setMigrateFor(null);
|
|
667
|
+
onClose();
|
|
668
|
+
};
|
|
669
|
+
// Single cancel button in the panel footer (no native "x").
|
|
670
|
+
const onPanelKey = (e) => {
|
|
671
|
+
if (e.key === "Escape" && busyId === null) {
|
|
672
|
+
if (confirmFor || moveFor || migrateFor) return;
|
|
673
|
+
e.preventDefault();
|
|
674
|
+
onPanelClose();
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
const panelModal = h("div", {
|
|
678
|
+
className: "sm-nativeDialogLayer",
|
|
679
|
+
role: "presentation",
|
|
680
|
+
onKeyDown: onPanelKey,
|
|
681
|
+
tabIndex: -1
|
|
682
|
+
},
|
|
683
|
+
h("div", {
|
|
684
|
+
className: "sm-nativeDialogBackdrop",
|
|
685
|
+
onMouseDown: (e) => { if (e.target === e.currentTarget && busyId === null) onPanelClose(); }
|
|
686
|
+
}),
|
|
687
|
+
h("section", {
|
|
688
|
+
className: "sm-nativeDialog sm-panelDialog",
|
|
689
|
+
role: "dialog",
|
|
690
|
+
"aria-modal": "true",
|
|
691
|
+
"aria-labelledby": "sm-panel-dialog-title",
|
|
692
|
+
ref: (el) => { if (el && typeof el.focus === "function") el.focus(); }
|
|
693
|
+
},
|
|
694
|
+
h("div", { className: "sm-nativeDialogHeader" },
|
|
695
|
+
h("h2", { id: "sm-panel-dialog-title", className: "sm-nativeDialogTitle" }, t("panel.title")),
|
|
696
|
+
h("button", {
|
|
697
|
+
type: "button",
|
|
698
|
+
className: "sm-panelHeaderClose",
|
|
699
|
+
disabled: busyId !== null || migrating,
|
|
700
|
+
onClick: onPanelClose
|
|
701
|
+
}, t("panel.close"))
|
|
702
|
+
),
|
|
703
|
+
h("div", { className: "sm-panel" },
|
|
704
|
+
h("div", { className: "sm-filter" },
|
|
705
|
+
filterBtn("all", t("panel.filter.all")),
|
|
706
|
+
filterBtn("active", t("panel.filter.active")),
|
|
707
|
+
filterBtn("archived", t("panel.filter.archived"))
|
|
708
|
+
),
|
|
709
|
+
error ? h("div", { className: "sm-error", role: "alert" }, t("error.operation", { message: error })) : null,
|
|
710
|
+
rowsList
|
|
711
|
+
)
|
|
712
|
+
)
|
|
713
|
+
);
|
|
714
|
+
const deleteDialog = confirmFor === null ? null : h(ConfirmDialog, {
|
|
715
|
+
open: true,
|
|
716
|
+
onClose: () => { if (busyId === null) setConfirmFor(null); },
|
|
717
|
+
title: t("confirm.delete.title"),
|
|
718
|
+
description: t("confirm.delete.desc", { title: confirmFor.displayTitle || confirmFor.id }),
|
|
719
|
+
warning: confirmFor.running ? t("confirm.delete.running") : null,
|
|
720
|
+
confirmLabel: t("confirm.delete.confirm"),
|
|
721
|
+
cancelLabel: t("confirm.cancel"),
|
|
722
|
+
busy: busyId !== null,
|
|
723
|
+
onCancel: () => { if (busyId === null) setConfirmFor(null); },
|
|
724
|
+
onConfirm: () => {
|
|
725
|
+
const target = confirmFor;
|
|
726
|
+
setConfirmFor(null);
|
|
727
|
+
runRow(target.id, () => onRemove(target.id));
|
|
728
|
+
}
|
|
443
729
|
});
|
|
730
|
+
const moveDialog = moveFor === null ? null : h(MoveDialog, {
|
|
731
|
+
open: true,
|
|
732
|
+
onClose: () => { if (busyId === null) setMoveFor(null); },
|
|
733
|
+
title: t("confirm.move.title"),
|
|
734
|
+
description: t("confirm.move.desc", { title: moveFor.displayTitle || moveFor.id }),
|
|
735
|
+
sessionTitle: moveFor.displayTitle || moveFor.id,
|
|
736
|
+
currentWorkspaceId: moveFor.workspaceId,
|
|
737
|
+
workspaces: workspaces,
|
|
738
|
+
confirmLabel: t("confirm.move.confirm"),
|
|
739
|
+
cancelLabel: t("confirm.cancel"),
|
|
740
|
+
busy: busyId !== null,
|
|
741
|
+
onCancel: () => { if (busyId === null) setMoveFor(null); },
|
|
742
|
+
onConfirm: (targetWorkspaceId) => {
|
|
743
|
+
const target = moveFor;
|
|
744
|
+
setMoveFor(null);
|
|
745
|
+
runRow(target.id, () => onMove(target.id, targetWorkspaceId));
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
const migrateDialog = migrateFor === null ? null : h(MigratePresetDialog, {
|
|
749
|
+
open: true,
|
|
750
|
+
t: t,
|
|
751
|
+
sessionId: migrateFor.id,
|
|
752
|
+
currentPreset: migrateFor.agentPreset || "",
|
|
753
|
+
onCancel: () => { if (!migrating) setMigrateFor(null); },
|
|
754
|
+
busy: migrating,
|
|
755
|
+
onRun: async (targetPreset) => {
|
|
756
|
+
const target = migrateFor;
|
|
757
|
+
setMigrating(true);
|
|
758
|
+
setError(null);
|
|
759
|
+
try {
|
|
760
|
+
await onMigrate(target.id, targetPreset);
|
|
761
|
+
setMigrateFor(null);
|
|
762
|
+
try { window.alert(t("migrate.completed", { preset: targetPreset })); } catch (_) {}
|
|
763
|
+
} catch (error) {
|
|
764
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
765
|
+
setMigrateFor(null);
|
|
766
|
+
} finally {
|
|
767
|
+
setMigrating(false);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
// Render the panel AND the inner dialog as siblings so React keeps
|
|
772
|
+
// both mounted. CSS z-index keeps the right ordering: the inner
|
|
773
|
+
// dialog (.sm-confirmDialog / .sm-moveDialog / .sm-migrateDialog)
|
|
774
|
+
// carries z-index 10000 and therefore sits ON TOP OF the panel
|
|
775
|
+
// (z-index 9999). Clicking inside the panel that the inner dialog
|
|
776
|
+
// blocks is not possible because the inner backdrop covers the
|
|
777
|
+
// panel area entirely (the inner modal is centered).
|
|
778
|
+
const primary = moveDialog || deleteDialog || migrateDialog;
|
|
779
|
+
return h(React.Fragment, null, panelModal, primary);
|
|
444
780
|
}
|
|
445
781
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
782
|
+
function HeaderAction(props) {
|
|
783
|
+
const t = props.t;
|
|
784
|
+
const sessionId = props.sessionId;
|
|
785
|
+
// Slot framework spreads inject-factory return into top-level props,
|
|
786
|
+
// so the action callbacks live directly on props (not under .actions).
|
|
787
|
+
const onArchive = props.onArchive, onUnarchive = props.onUnarchive;
|
|
788
|
+
const onMove = props.onMove, onRemove = props.onRemove;
|
|
789
|
+
// Archive state lives in the workspace registry's archivedSessionIds
|
|
790
|
+
// (exposed through the global standard kit's useWorkspaces selector).
|
|
791
|
+
const useWorkspaces = props.useWorkspaces;
|
|
792
|
+
const archivedIds = (useWorkspaces ? useWorkspaces((s) => s.archivedSessionIds) : null) || [];
|
|
793
|
+
const isArchived = archivedIds.includes(sessionId);
|
|
794
|
+
|
|
795
|
+
const [confirmFor, setConfirmFor] = useState(null);
|
|
796
|
+
const [confirmAnchor, setConfirmAnchor] = useState(null);
|
|
797
|
+
const [moveFor, setMoveFor] = useState(null);
|
|
798
|
+
const [moveAnchor, setMoveAnchor] = useState(null);
|
|
799
|
+
const [workspaces, setWorkspaces] = useState([]);
|
|
800
|
+
const runWithAlert = (label, fn) => {
|
|
801
|
+
return Promise.resolve()
|
|
802
|
+
.then(() => fn())
|
|
803
|
+
.catch((e) => {
|
|
804
|
+
try {
|
|
805
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
806
|
+
window.alert(((t && t("error.operation")) || "操作失败:{message}").replace("{message}", msg));
|
|
807
|
+
} catch (_) {}
|
|
808
|
+
});
|
|
809
|
+
};
|
|
810
|
+
useEffect(() => {
|
|
811
|
+
if (!moveFor) { setWorkspaces([]); return; }
|
|
812
|
+
let alive = true;
|
|
813
|
+
fetchApi("/workspaces").then((d) => {
|
|
814
|
+
if (alive && d && d.ok) setWorkspaces(d.result.workspaces || []);
|
|
815
|
+
});
|
|
816
|
+
return () => { alive = false; };
|
|
817
|
+
}, [moveFor ? (moveFor.id || "x") : null]);
|
|
818
|
+
const anchorFor = (el) => {
|
|
819
|
+
if (!el || typeof el.getBoundingClientRect !== "function") return null;
|
|
820
|
+
const r = el.getBoundingClientRect();
|
|
821
|
+
return {
|
|
822
|
+
top: Math.round(r.top + r.height + 6),
|
|
823
|
+
left: Math.max(8, Math.round(r.right - 320))
|
|
824
|
+
};
|
|
825
|
+
};
|
|
826
|
+
const archiveLabelKey = isArchived ? "row.unarchive" : "row.archive";
|
|
827
|
+
return h(React.Fragment, null,
|
|
828
|
+
h("div", { className: "sm-header" },
|
|
829
|
+
h("button", {
|
|
830
|
+
type: "button",
|
|
831
|
+
className: "sm-headerBtn" + (isArchived ? " sm-headerBtnActive" : ""),
|
|
832
|
+
"aria-label": t ? t(archiveLabelKey) : (isArchived ? "移出归档" : "归档会话"),
|
|
833
|
+
onClick: () => runWithAlert("archive", () =>
|
|
834
|
+
isArchived ? onUnarchive(sessionId) : onArchive(sessionId)
|
|
835
|
+
)
|
|
836
|
+
}, isArchived ? (t ? t("row.unarchive") : "移出归档") : (t ? t("row.archive") : "归档")),
|
|
837
|
+
h("button", {
|
|
838
|
+
type: "button",
|
|
839
|
+
className: "sm-headerBtn" + (moveFor ? " sm-headerBtnActive" : ""),
|
|
840
|
+
"aria-label": t ? t("row.move") : "移动到工作区",
|
|
841
|
+
onClick: (e) => {
|
|
842
|
+
e.stopPropagation();
|
|
843
|
+
setConfirmFor(null);
|
|
844
|
+
setConfirmAnchor(null);
|
|
845
|
+
if (moveFor && moveFor.id === sessionId) {
|
|
846
|
+
setMoveFor(null);
|
|
847
|
+
setMoveAnchor(null);
|
|
848
|
+
} else {
|
|
849
|
+
setMoveAnchor(anchorFor(e.currentTarget));
|
|
850
|
+
setMoveFor({ id: sessionId, displayTitle: sessionId, workspaceId: "" });
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}, t ? t("row.move") : "移动"),
|
|
854
|
+
h("button", {
|
|
855
|
+
type: "button",
|
|
856
|
+
className: "sm-headerBtn sm-headerBtnDanger" + (confirmFor ? " sm-headerBtnActive" : ""),
|
|
857
|
+
"aria-label": t ? t("row.delete") : "删除会话",
|
|
858
|
+
onClick: (e) => {
|
|
859
|
+
e.stopPropagation();
|
|
860
|
+
setMoveFor(null);
|
|
861
|
+
setMoveAnchor(null);
|
|
862
|
+
if (confirmFor && confirmFor.id === sessionId) {
|
|
863
|
+
setConfirmFor(null);
|
|
864
|
+
setConfirmAnchor(null);
|
|
865
|
+
} else {
|
|
866
|
+
setConfirmAnchor(anchorFor(e.currentTarget));
|
|
867
|
+
setConfirmFor({ id: sessionId, displayTitle: sessionId });
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}, t ? t("row.delete") : "删除")
|
|
460
871
|
),
|
|
461
|
-
h(
|
|
462
|
-
open,
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
872
|
+
confirmFor === null ? null : h(ConfirmDialog, {
|
|
873
|
+
open: true,
|
|
874
|
+
anchor: confirmAnchor,
|
|
875
|
+
onClose: () => setConfirmFor(null),
|
|
876
|
+
onCancel: () => setConfirmFor(null),
|
|
877
|
+
onConfirm: () => {
|
|
878
|
+
const target = confirmFor;
|
|
879
|
+
setConfirmFor(null);
|
|
880
|
+
runWithAlert("delete", () => onRemove(target.id));
|
|
881
|
+
},
|
|
882
|
+
title: t ? t("confirm.delete.title") : "删除会话",
|
|
883
|
+
description: (confirmFor && (t ? t("confirm.delete.desc", { title: confirmFor.displayTitle || confirmFor.id }) : ("将删除 " + (confirmFor.displayTitle || confirmFor.id)))) || "",
|
|
884
|
+
confirmLabel: t ? t("confirm.delete.confirm") : "确认删除",
|
|
885
|
+
cancelLabel: t ? t("confirm.cancel") : "取消",
|
|
886
|
+
busy: false
|
|
887
|
+
}),
|
|
888
|
+
moveFor === null ? null : h(MoveDialog, {
|
|
889
|
+
open: true,
|
|
890
|
+
anchor: moveAnchor,
|
|
891
|
+
t: t,
|
|
892
|
+
workspaces: workspaces,
|
|
893
|
+
currentWorkspaceId: moveFor.workspaceId || "",
|
|
894
|
+
onClose: () => { setMoveFor(null); setMoveAnchor(null); },
|
|
895
|
+
onCancel: () => { setMoveFor(null); setMoveAnchor(null); },
|
|
896
|
+
title: t ? t("confirm.move.title") : "移动会话到工作区",
|
|
897
|
+
description: (moveFor && (t ? t("confirm.move.desc", { title: moveFor.displayTitle || moveFor.id }) : "")) || "",
|
|
898
|
+
sessionTitle: moveFor.displayTitle || moveFor.id,
|
|
899
|
+
confirmLabel: t ? t("confirm.move.confirm") : "确认移动",
|
|
900
|
+
cancelLabel: t ? t("confirm.cancel") : "取消",
|
|
901
|
+
busy: false,
|
|
902
|
+
onConfirm: (targetWorkspaceId) => {
|
|
903
|
+
const target = moveFor;
|
|
904
|
+
setMoveFor(null);
|
|
905
|
+
setMoveAnchor(null);
|
|
906
|
+
runWithAlert("move", () => onMove(target.id, targetWorkspaceId));
|
|
907
|
+
}
|
|
471
908
|
})
|
|
472
909
|
);
|
|
473
910
|
}
|
|
474
911
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
912
|
+
class SafePanel extends React.Component {
|
|
913
|
+
constructor(p) { super(p); this.state = { failed: false }; }
|
|
914
|
+
getDerivedStateFromError() { return { failed: true }; }
|
|
915
|
+
componentDidCatch(error) { try { console.error("[dsh-session-manager] SessionManagerPanel crashed:", error); } catch (_) {} }
|
|
916
|
+
render() {
|
|
917
|
+
if (this.state.failed) return null;
|
|
918
|
+
return h(SessionManagerPanel, {
|
|
919
|
+
open: true,
|
|
920
|
+
onClose: () => { try { this.props.panelStore.set(false); } catch (_) {} },
|
|
921
|
+
useSessions: this.props.useSessions,
|
|
922
|
+
useWorkspaces: this.props.useWorkspaces,
|
|
923
|
+
t: this.props.t,
|
|
924
|
+
onOpen: (id) => { try { this.props.panelStore.set(false); this.props.props.onOpen(id); } catch (_) {} },
|
|
925
|
+
onArchive: this.props.props.onArchive,
|
|
926
|
+
onUnarchive: this.props.props.onUnarchive,
|
|
927
|
+
onRemove: this.props.props.onRemove,
|
|
928
|
+
onMove: this.props.props.onMove,
|
|
929
|
+
onMigrate: this.props.props.onMigrate
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function FooterAction(props) {
|
|
935
|
+
const t = props.t;
|
|
936
|
+
const onOpenPanel = props.onOpenPanel;
|
|
937
|
+
const open = useSyncExternalStore(
|
|
938
|
+
(cb) => panelStore.subscribe(cb),
|
|
939
|
+
() => panelStore.getSnapshot(),
|
|
940
|
+
() => panelStore.getSnapshot()
|
|
941
|
+
);
|
|
942
|
+
return h("div", { className: "sm-footer" },
|
|
943
|
+
h("button", { type: "button", className: "sm-footerBtn", "aria-label": t("footer.aria"), onClick: () => onOpenPanel() },
|
|
944
|
+
h(P.IconArchiveOutline20, { size: 14 }),
|
|
945
|
+
h("span", null, t("footer.label"))
|
|
946
|
+
),
|
|
947
|
+
// The panel is mounted ONLY while the user has the modal open. When
|
|
948
|
+
// The wrap guards against an in-panel crash by returning null on its
|
|
949
|
+
// own error boundary rather than letting it bubble up to the slot.
|
|
950
|
+
open ? h(SafePanel, {
|
|
951
|
+
panelStore: panelStore,
|
|
952
|
+
props: props,
|
|
953
|
+
t: t,
|
|
954
|
+
useSessions: props.useSessions,
|
|
955
|
+
useWorkspaces: props.useWorkspaces,
|
|
956
|
+
}) : null
|
|
957
|
+
);
|
|
958
|
+
}
|
|
479
959
|
|
|
480
960
|
function apply(ctx) {
|
|
481
961
|
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
962
|
const actions = () => ({
|
|
488
|
-
onOpen: (sessionId) => {
|
|
489
|
-
|
|
490
|
-
},
|
|
491
|
-
onArchive: async (sessionId) => {
|
|
492
|
-
await ctx.workspaces.archiveSession(sessionId);
|
|
493
|
-
},
|
|
963
|
+
onOpen: (sessionId) => { ctx.sessions.open(sessionId); },
|
|
964
|
+
onArchive: async (sessionId) => { await ctx.workspaces.archiveSession(sessionId); },
|
|
494
965
|
onUnarchive: async (sessionId) => {
|
|
495
966
|
const response = await callApi("/unarchive", { sessionId });
|
|
496
967
|
if (!response.ok) throw new Error(response.error || "unarchive failed");
|
|
@@ -501,11 +972,46 @@ window.__ModuleLoader__.load({
|
|
|
501
972
|
const current = ctx.sessions.list.getSnapshot().current;
|
|
502
973
|
if (current === sessionId) ctx.sessions.clear();
|
|
503
974
|
},
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
975
|
+
onMove: async (sessionId, targetWorkspaceId) => {
|
|
976
|
+
const wasCurrent = ctx.sessions.list.getSnapshot().current === sessionId;
|
|
977
|
+
const response = await callApi("/move", { sessionId, targetWorkspaceId });
|
|
978
|
+
if (!response.ok) throw new Error(response.error || "move failed");
|
|
979
|
+
const refreshMovedSession = async () => {
|
|
980
|
+
await Promise.allSettled([
|
|
981
|
+
typeof ctx.workspaces?.refresh === "function" ? ctx.workspaces.refresh() : Promise.resolve(),
|
|
982
|
+
ctx.sessions.refresh()
|
|
983
|
+
]);
|
|
984
|
+
if (wasCurrent && ctx.sessions.list.getSnapshot().byId[sessionId] !== undefined) {
|
|
985
|
+
ctx.sessions.open(sessionId);
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
// /move retires a live session, so host/session-removed can arrive on
|
|
989
|
+
// the streaming channel slightly after the HTTP success response.
|
|
990
|
+
// Pull both projections now and once more after that frame to make the
|
|
991
|
+
// destination row appear without a browser refresh.
|
|
992
|
+
await refreshMovedSession();
|
|
993
|
+
setTimeout(() => { void refreshMovedSession(); }, 250);
|
|
994
|
+
setTimeout(() => { void refreshMovedSession(); }, 900);
|
|
995
|
+
},
|
|
996
|
+
onMigrate: async (sessionId, toPreset) => {
|
|
997
|
+
const wasCurrent = ctx.sessions.list.getSnapshot().current === sessionId;
|
|
998
|
+
const response = await callApi("/preset-migrate", { sessionId, toPreset });
|
|
999
|
+
if (!response.ok) throw new Error(response.error || "preset migration failed");
|
|
1000
|
+
ctx.sessions.noteAgentPreset(sessionId, toPreset);
|
|
1001
|
+
await ctx.sessions.refresh();
|
|
1002
|
+
if (wasCurrent) ctx.sessions.open(sessionId);
|
|
1003
|
+
// Host/session-removed and the HTTP response travel on different
|
|
1004
|
+
// streams. One delayed baseline closes that ordering race without a
|
|
1005
|
+
// page refresh and keeps the migrated conversation selectable.
|
|
1006
|
+
setTimeout(() => {
|
|
1007
|
+
void ctx.sessions.refresh().then(() => {
|
|
1008
|
+
ctx.sessions.noteAgentPreset(sessionId, toPreset);
|
|
1009
|
+
if (wasCurrent) ctx.sessions.open(sessionId);
|
|
1010
|
+
}).catch(() => {});
|
|
1011
|
+
}, 250);
|
|
1012
|
+
},
|
|
1013
|
+
onOpenPanel: () => panelStore.set(true)
|
|
507
1014
|
});
|
|
508
|
-
|
|
509
1015
|
ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
|
|
510
1016
|
name: "conversation.session.header.actions",
|
|
511
1017
|
id: "session-manager-header",
|
|
@@ -513,7 +1019,6 @@ window.__ModuleLoader__.load({
|
|
|
513
1019
|
locale: NS,
|
|
514
1020
|
inject: () => actions()
|
|
515
1021
|
}, HeaderAction));
|
|
516
|
-
|
|
517
1022
|
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
518
1023
|
name: "sidebar.footer.action",
|
|
519
1024
|
id: "session-manager-footer",
|
|
@@ -523,8 +1028,10 @@ window.__ModuleLoader__.load({
|
|
|
523
1028
|
}, FooterAction));
|
|
524
1029
|
}
|
|
525
1030
|
|
|
1031
|
+
const inject = ["slots", "sessions", "workspaces", "locale"];
|
|
1032
|
+
|
|
526
1033
|
exports.apply = apply;
|
|
527
1034
|
exports.inject = inject;
|
|
528
1035
|
return module.exports;
|
|
529
1036
|
}
|
|
530
|
-
});
|
|
1037
|
+
});
|