@tobycaimf/dsh-archived-sessions 1.0.1 → 1.0.2
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.en.md +13 -11
- package/README.md +13 -11
- package/lib/client.js +309 -286
- package/lib/client.js.map +2 -2
- package/lib/index.js +43 -0
- package/lib/index.js.map +2 -2
- package/package.json +2 -2
- package/src/client/index.jsx +314 -324
- package/src/index.js +48 -0
package/src/client/index.jsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-archived-sessions — browser half: renders
|
|
3
|
-
* section (settings.section list slot)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* dsh-archived-sessions — browser half: renders a single "会话管理" settings
|
|
3
|
+
* section (settings.section list slot) that unifies archived-session
|
|
4
|
+
* management and cross-workspace moving. It talks to the host half's
|
|
5
|
+
* /archived-sessions/* JSON routes by fetch, showing every conversation with
|
|
6
|
+
* its archive state and offering archive / restore / delete / move (and batch)
|
|
7
|
+
* actions. All DOM/runtime wiring failures are logged, never thrown — a thrown
|
|
8
|
+
* plugin apply takes down the whole web-shell boot.
|
|
8
9
|
*
|
|
9
10
|
* @module dsh-archived-sessions/client
|
|
10
11
|
*/
|
|
@@ -21,25 +22,36 @@ const CSS = `
|
|
|
21
22
|
.archv-sub{font-size:12px;line-height:1.55;color:var(--dsw-alias-label-tertiary);margin:0 0 12px;max-width:64ch}
|
|
22
23
|
.archv-err{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 12px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);border-radius:10px;color:var(--dsw-alias-state-error-primary);font-size:12px;margin-bottom:10px}
|
|
23
24
|
.archv-errretry{appearance:none;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 45%,transparent);background:transparent;color:inherit;border-radius:8px;padding:4px 10px;font-size:11px;cursor:pointer;flex:none}
|
|
24
|
-
.
|
|
25
|
+
.sess-filter{display:flex;align-items:center;gap:6px;margin:0 0 12px}
|
|
26
|
+
.sess-fbtn{appearance:none;min-height:30px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-fill-subtle);color:var(--dsw-alias-label-secondary);border-radius:999px;font-size:12px;font-weight:500;cursor:pointer}
|
|
27
|
+
.sess-fbtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
28
|
+
.sess-fbtn-on{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}
|
|
29
|
+
.sess-batch{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 2px 4px;margin-bottom:4px}
|
|
30
|
+
.sess-btntext{font-size:12px;color:var(--dsw-alias-label-tertiary);flex:none}
|
|
25
31
|
.archv-list{display:flex;flex-direction:column;gap:8px}
|
|
26
|
-
.archv-card{display:flex;
|
|
32
|
+
.archv-card{display:flex;flex-direction:column;align-items:stretch;gap:0;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-fill-elevated);transition:border-color .15s ease,background-color .15s ease}
|
|
27
33
|
.archv-card:hover{border-color:var(--dsw-alias-border-l4)}
|
|
34
|
+
.archv-card-exp{border-color:var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1)}
|
|
35
|
+
.archv-row{display:flex;align-items:center;gap:14px;width:100%;min-width:0}
|
|
28
36
|
.archv-main{min-width:0;display:flex;flex-direction:column;gap:4px}
|
|
29
37
|
.archv-name{font-size:13px;font-weight:550;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
30
38
|
.archv-meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap;min-width:0}
|
|
31
39
|
.archv-wtag{display:inline-flex;align-items:center;gap:4px;max-width:100%;font-size:11px;font-weight:500;line-height:1;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-subtle);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:3px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
32
40
|
.archv-wgone{border-style:dashed;color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 7%,transparent)}
|
|
41
|
+
.archv-active{color:var(--dsw-alias-state-success-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 45%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent)}
|
|
33
42
|
.archv-date{font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;flex:none}
|
|
34
43
|
.archv-id{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10.5px;color:var(--dsw-alias-label-tertiary);flex:none}
|
|
35
44
|
.archv-dot{color:var(--dsw-alias-border-l3);flex:none}
|
|
36
|
-
.archv-
|
|
45
|
+
.archv-check{width:15px;height:15px;accent-color:var(--dsw-alias-state-business-primary);flex:none;cursor:pointer}
|
|
46
|
+
.archv-body{flex:1;min-width:0;display:flex;align-items:center;gap:12px}
|
|
47
|
+
.archv-actions{display:flex;gap:8px;flex:none;flex-wrap:wrap;justify-content:flex-end}
|
|
37
48
|
.archv-btn{appearance:none;min-height:32px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-fill-subtle);color:var(--dsw-alias-label-secondary);border-radius:9px;font-size:12px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:background-color .15s ease,border-color .15s ease,color .15s ease}
|
|
38
49
|
.archv-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
39
50
|
.archv-btn:disabled{opacity:.5;cursor:default}
|
|
40
|
-
.archv-restore{min-width:62px;justify-content:center}
|
|
41
51
|
.archv-del{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 45%,transparent)}
|
|
42
52
|
.archv-del:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
|
|
53
|
+
.archv-go{color:var(--dsw-alias-state-business-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 45%,transparent)}
|
|
54
|
+
.archv-go:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);color:var(--dsw-alias-state-business-primary)}
|
|
43
55
|
.archv-empty{display:flex;align-items:center;gap:10px;padding:20px 14px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
|
44
56
|
.archv-skel{display:flex;flex-direction:column;gap:8px}
|
|
45
57
|
.archv-skel-card{height:58px;border-radius:12px;background:var(--dsw-alias-fill-subtle);position:relative;overflow:hidden}
|
|
@@ -50,7 +62,24 @@ const CSS = `
|
|
|
50
62
|
@keyframes archv-rot{to{transform:rotate(360deg)}}
|
|
51
63
|
@keyframes archv-pop{from{opacity:0;transform:translateX(-50%) translateY(10px)}}
|
|
52
64
|
@media (prefers-reduced-motion:reduce){.archv-skel-card::after{animation:none}.archv-card,.archv-btn{transition:none}.archv-status,.archv-spin{animation:none}}
|
|
53
|
-
@media (max-width:
|
|
65
|
+
@media (max-width:640px){.archv-card{flex-direction:column;align-items:stretch;gap:10px}.archv-actions{justify-content:flex-end}}
|
|
66
|
+
.mv-sheet{width:100%;box-sizing:border-box;display:flex;flex-direction:column;gap:12px;margin-top:12px;padding:14px;border:1px solid var(--dsw-alias-border-l2);border-left:3px solid var(--dsw-alias-state-business-primary);border-radius:10px;background:var(--dsw-alias-fill-subtle)}
|
|
67
|
+
.mv-sheet-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
68
|
+
.mv-sheet-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}
|
|
69
|
+
.mv-sheet-close{appearance:none;width:26px;height:26px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:7px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:16px;line-height:1}
|
|
70
|
+
.mv-sheet-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
71
|
+
.mv-seg{display:flex;gap:2px;padding:2px;background:var(--dsw-alias-fill-elevated);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;width:100%}
|
|
72
|
+
.mv-segbtn{appearance:none;flex:1;min-height:30px;padding:0 12px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);border-radius:8px;font-size:12px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:6px}
|
|
73
|
+
.mv-segbtn:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
|
|
74
|
+
.mv-segbtn-on{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px rgb(0 0 0/.08)}
|
|
75
|
+
.mv-field{display:flex;flex-direction:column;gap:6px}
|
|
76
|
+
.mv-field label.mv-field-label{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary)}
|
|
77
|
+
.mv-field select,.mv-field input[type=text]{box-sizing:border-box;appearance:none;width:100%;min-height:34px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-fill-elevated);color:var(--dsw-alias-label-primary);font-size:12px;font-family:inherit}
|
|
78
|
+
.mv-field select:focus-visible,.mv-field input[type=text]:focus-visible,.mv-sheet-close:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
|
|
79
|
+
.mv-browse-row{display:flex;align-items:center;gap:8px}
|
|
80
|
+
.mv-browse-row input[type=text]{flex:1;min-width:0}
|
|
81
|
+
.mv-foot{display:flex;justify-content:flex-end;align-items:center;gap:8px;margin-top:2px}
|
|
82
|
+
@media (max-width:640px){.archv-row{flex-wrap:wrap}.mv-sheet{padding:12px}}
|
|
54
83
|
`
|
|
55
84
|
|
|
56
85
|
function fmtDate(iso) {
|
|
@@ -81,172 +110,69 @@ async function postJSON(path, body) {
|
|
|
81
110
|
return data
|
|
82
111
|
}
|
|
83
112
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const refresh = () => {
|
|
99
|
-
setError(null)
|
|
100
|
-
postJSON('/archived-sessions/list', {})
|
|
101
|
-
.then((r) => setItems(r.items || []))
|
|
102
|
-
.catch((e) => setError(String((e && e.message) || e)))
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
refresh()
|
|
107
|
-
return () => { if (timer.current) clearTimeout(timer.current) }
|
|
108
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
109
|
-
}, [])
|
|
110
|
-
|
|
111
|
-
const act = (action, it) => {
|
|
112
|
-
if (busy) return
|
|
113
|
-
if (action === 'delete') {
|
|
114
|
-
if (confirmId !== it.sessionId) { setConfirmId(it.sessionId); return }
|
|
115
|
-
setConfirmId(null)
|
|
116
|
-
} else { setConfirmId(null); setError(null) }
|
|
117
|
-
setBusy(it.sessionId)
|
|
118
|
-
postJSON('/archived-sessions/' + action, { sessionId: it.sessionId })
|
|
119
|
-
.then(() => {
|
|
120
|
-
setBusy(null)
|
|
121
|
-
showToast(action === 'restore' ? `已恢复「${it.title || it.sessionId}」` : `已删除 ${it.title || it.sessionId}`)
|
|
122
|
-
refresh()
|
|
123
|
-
})
|
|
124
|
-
.catch((e) => { setBusy(null); setConfirmId(null); setError(String((e && e.message) || e)) })
|
|
125
|
-
}
|
|
113
|
+
// Left-nav icon swap: DSH's settings shell renders a shared fallback gear for
|
|
114
|
+
// every custom section and settings.section has no per-section icon field.
|
|
115
|
+
// Like dsh-better-sidebar, we mark our own row by matching its visible label
|
|
116
|
+
// text, then CSS swaps the gear for an archive-box glyph. The marker owns no
|
|
117
|
+
// shell structure and is removed on fiber disposal (HMR-safe, ships with the
|
|
118
|
+
// plugin).
|
|
119
|
+
const NAV_CSS = `
|
|
120
|
+
[data-dsh-nav-sessions] > svg:first-child { display: none; }
|
|
121
|
+
[data-dsh-nav-sessions]::before {
|
|
122
|
+
content: ''; flex: none; width: 16px; height: 16px; background: currentColor;
|
|
123
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http://www.w3.org/2000/svg'%20width%3D'24'%20height%3D'24'%20viewBox%3D'0%200%2024%2024'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%3E%3Cpath%20d%3D'M21%208v13H3V8'/%3E%3Cpath%20d%3D'M1%203h22v5H1z'/%3E%3Cpath%20d%3D'M10%2012h4'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
124
|
+
mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http://www.w3.org/2000/svg'%20width%3D'24'%20height%3D'24'%20viewBox%3D'0%200%2024%2024'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%3E%3Cpath%20d%3D'M21%208v13H3V8'/%3E%3Cpath%20d%3D'M1%203h22v5H1z'/%3E%3Cpath%20d%3D'M10%2012h4'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
125
|
+
}
|
|
126
|
+
`
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
function markSettingsNav() {
|
|
129
|
+
const LABEL = '会话管理'
|
|
130
|
+
let disposed = false
|
|
131
|
+
const sync = () => {
|
|
132
|
+
if (disposed) return
|
|
133
|
+
const buttons = document.querySelectorAll('[role="dialog"] nav button')
|
|
134
|
+
for (const b of buttons) {
|
|
135
|
+
const t = (b.textContent || '').trim()
|
|
136
|
+
if (t === LABEL) b.setAttribute('data-dsh-nav-sessions', '')
|
|
137
|
+
else b.removeAttribute('data-dsh-nav-sessions')
|
|
134
138
|
}
|
|
135
|
-
const wName = it.workspaceTitle || pathName(it.workspacePath)
|
|
136
|
-
if (wName) return <span className="archv-wtag" title={it.workspacePath || ''}>{wName}</span>
|
|
137
|
-
return null
|
|
138
139
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<p className="archv-sub">
|
|
148
|
-
被归档(从侧栏隐藏)的会话都在这里。恢复会把会话放回原分组;删除会彻底移除日志文件,不可恢复。
|
|
149
|
-
</p>
|
|
150
|
-
{error && (
|
|
151
|
-
<div className="archv-err" role="alert">
|
|
152
|
-
<span>{error}</span>
|
|
153
|
-
<button type="button" className="archv-errretry" onClick={refresh}>重试</button>
|
|
154
|
-
</div>
|
|
155
|
-
)}
|
|
156
|
-
{items === null ? (
|
|
157
|
-
<div className="archv-skel" aria-label="加载中">
|
|
158
|
-
{[0, 1, 2].map((i) => <div key={i} className="archv-skel-card" />)}
|
|
159
|
-
</div>
|
|
160
|
-
) : items.length === 0 ? (
|
|
161
|
-
<div className="archv-empty">目前没有归档会话。在侧栏任一会话的菜单里选择「归档」即可收纳进来。</div>
|
|
162
|
-
) : (
|
|
163
|
-
<div className="archv-list" role="list">
|
|
164
|
-
{items.map((it) => {
|
|
165
|
-
const isConfirm = confirmId === it.sessionId
|
|
166
|
-
const date = fmtDate(it.createdAt)
|
|
167
|
-
const wtag = workspaceTag(it)
|
|
168
|
-
return (
|
|
169
|
-
<div key={it.sessionId} className="archv-card" role="listitem">
|
|
170
|
-
<div className="archv-main">
|
|
171
|
-
<div className="archv-name" title={it.title || ''}>{it.title || '(无标题)'}</div>
|
|
172
|
-
<div className="archv-meta">
|
|
173
|
-
{date && <span className="archv-date">{date}</span>}
|
|
174
|
-
{date && wtag && <span className="archv-dot">·</span>}
|
|
175
|
-
{wtag}
|
|
176
|
-
<span className="archv-id">{it.sessionId}</span>
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
179
|
-
<div className="archv-actions">
|
|
180
|
-
<button type="button" className="archv-btn archv-restore" disabled={busy !== null} aria-label={'恢复 ' + (it.title || it.sessionId)} onClick={() => act('restore', it)}>
|
|
181
|
-
{busy === it.sessionId && <span className="archv-spin" aria-hidden="true" />}恢复
|
|
182
|
-
</button>
|
|
183
|
-
<button type="button" className="archv-btn archv-del" disabled={busy !== null} aria-label={(isConfirm ? '确认删除 ' : '删除 ') + (it.title || it.sessionId)} onClick={() => act('delete', it)}>
|
|
184
|
-
{isConfirm ? '确认删除?' : '删除'}
|
|
185
|
-
</button>
|
|
186
|
-
</div>
|
|
187
|
-
</div>
|
|
188
|
-
)
|
|
189
|
-
})}
|
|
190
|
-
</div>
|
|
191
|
-
)}
|
|
192
|
-
{toast && <div className="archv-status" role="status">{toast}</div>}
|
|
193
|
-
</div>
|
|
194
|
-
)
|
|
140
|
+
sync()
|
|
141
|
+
const obs = new MutationObserver(sync)
|
|
142
|
+
obs.observe(document.body, { childList: true, subtree: true, characterData: true })
|
|
143
|
+
return () => {
|
|
144
|
+
disposed = true
|
|
145
|
+
obs.disconnect()
|
|
146
|
+
document.querySelectorAll('[data-dsh-nav-sessions]').forEach((e) => e.removeAttribute('data-dsh-nav-sessions'))
|
|
147
|
+
}
|
|
195
148
|
}
|
|
196
149
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
.
|
|
200
|
-
.
|
|
201
|
-
|
|
202
|
-
.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
.mv-card:hover{border-color:var(--dsw-alias-border-l4)}
|
|
208
|
-
.mv-main{min-width:0;display:flex;flex-direction:column;gap:4px}
|
|
209
|
-
.mv-name{font-size:13px;font-weight:550;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
210
|
-
.mv-meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap;min-width:0}
|
|
211
|
-
.mv-wtag{display:inline-flex;align-items:center;gap:4px;max-width:100%;font-size:11px;font-weight:500;line-height:1;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-subtle);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:3px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
212
|
-
.mv-archived{color:var(--dsw-alias-state-warning-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 50%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 8%,transparent)}
|
|
213
|
-
.mv-date{font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;flex:none}
|
|
214
|
-
.mv-id{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10.5px;color:var(--dsw-alias-label-tertiary);flex:none}
|
|
215
|
-
.mv-dot{color:var(--dsw-alias-border-l3);flex:none}
|
|
216
|
-
.mv-actions{display:flex;gap:8px;flex:none}
|
|
217
|
-
.mv-btn{appearance:none;min-height:32px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-fill-subtle);color:var(--dsw-alias-label-secondary);border-radius:9px;font-size:12px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:background-color .15s ease,border-color .15s ease,color .15s ease}
|
|
218
|
-
.mv-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
219
|
-
.mv-btn:disabled{opacity:.5;cursor:default}
|
|
220
|
-
.mv-go{color:var(--dsw-alias-state-business-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 45%,transparent)}
|
|
221
|
-
.mv-go:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);color:var(--dsw-alias-state-business-primary)}
|
|
222
|
-
.mv-sheet{display:flex;flex-direction:column;gap:10px;margin-top:10px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-fill-subtle)}
|
|
223
|
-
.mv-sheet-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
224
|
-
.mv-sheet-row input[type=radio]{accent-color:var(--dsw-alias-state-business-primary)}
|
|
225
|
-
.mv-sheet select,.mv-sheet input[type=text]{appearance:none;min-height:32px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-fill-elevated);color:var(--dsw-alias-label-primary);font-size:12px;font-family:inherit}
|
|
226
|
-
.mv-sheet input[type=text]{flex:1;min-width:220px}
|
|
227
|
-
.mv-sheet select:focus-visible,.mv-sheet input[type=text]:focus-visible,.mv-go:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
228
|
-
.mv-empty{display:flex;align-items:center;gap:10px;padding:20px 14px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
|
229
|
-
.mv-skel{display:flex;flex-direction:column;gap:8px}
|
|
230
|
-
.mv-skel-card{height:58px;border-radius:12px;background:var(--dsw-alias-fill-subtle);position:relative;overflow:hidden}
|
|
231
|
-
.mv-skel-card::after{content:'';position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--dsw-alias-fill-elevated) 75%,transparent),transparent);animation:mv-shimmer 1.4s infinite}
|
|
232
|
-
@keyframes mv-shimmer{100%{transform:translateX(100%)}}
|
|
233
|
-
@keyframes mv-pop{from{opacity:0;transform:translateX(-50%) translateY(10px)}}
|
|
234
|
-
.mv-status{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:60;background:var(--dsw-alias-fill-elevated);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);padding:9px 16px;border-radius:999px;font-size:12px;box-shadow:0 8px 24px rgb(0 0 0/.18);display:flex;align-items:center;gap:8px;animation:mv-pop .18s ease-out;max-width:min(90vw,420px)}
|
|
235
|
-
.mv-spin{width:12px;height:12px;border:2px solid color-mix(in srgb,var(--dsw-alias-label-secondary) 35%,transparent);border-top-color:var(--dsw-alias-label-secondary);border-radius:50%;animation:archv-rot .8s linear infinite;flex:none}
|
|
236
|
-
@media (prefers-reduced-motion:reduce){.mv-skel-card::after{animation:none}.mv-card,.mv-btn{transition:none}.mv-status,.mv-spin{animation:none}}
|
|
237
|
-
@media (max-width:560px){.mv-card{flex-direction:column;align-items:stretch;gap:10px}.mv-actions{justify-content:flex-end}.mv-sheet input[type=text]{min-width:0}}
|
|
238
|
-
`
|
|
150
|
+
function installSettingsNavIcons(ctx) {
|
|
151
|
+
const styleEl = document.createElement('style')
|
|
152
|
+
styleEl.textContent = NAV_CSS
|
|
153
|
+
document.head.appendChild(styleEl)
|
|
154
|
+
const dispose = markSettingsNav()
|
|
155
|
+
ctx.effect(() => () => {
|
|
156
|
+
dispose()
|
|
157
|
+
if (styleEl.parentNode) styleEl.parentNode.removeChild(styleEl)
|
|
158
|
+
})
|
|
159
|
+
}
|
|
239
160
|
|
|
240
|
-
function
|
|
161
|
+
function SessionPanel({ workspacesSvc }) {
|
|
241
162
|
const [sessions, setSessions] = useState(null)
|
|
242
163
|
const [workspaces, setWorkspaces] = useState([])
|
|
164
|
+
const [filter, setFilter] = useState('all') // 'all' | 'archived'
|
|
165
|
+
const [selected, setSelected] = useState({})
|
|
166
|
+
const [confirmDel, setConfirmDel] = useState(null)
|
|
167
|
+
const [confirmBatch, setConfirmBatch] = useState(false)
|
|
243
168
|
const [busy, setBusy] = useState(null)
|
|
244
|
-
const [
|
|
245
|
-
const [
|
|
169
|
+
const [openMove, setOpenMove] = useState(null)
|
|
170
|
+
const [moveMode, setMoveMode] = useState('existing')
|
|
246
171
|
const [targetWs, setTargetWs] = useState('')
|
|
247
172
|
const [newPath, setNewPath] = useState('')
|
|
248
173
|
const [error, setError] = useState(null)
|
|
249
174
|
const [toast, setToast] = useState(null)
|
|
175
|
+
const [picking, setPicking] = useState(false)
|
|
250
176
|
const timer = useRef(null)
|
|
251
177
|
|
|
252
178
|
const showToast = (msg) => {
|
|
@@ -264,6 +190,9 @@ function MovePanel() {
|
|
|
264
190
|
.then(([s, works]) => {
|
|
265
191
|
setSessions(s.items || [])
|
|
266
192
|
setWorkspaces(works.items || [])
|
|
193
|
+
setSelected({})
|
|
194
|
+
setConfirmDel(null)
|
|
195
|
+
setConfirmBatch(false)
|
|
267
196
|
if (!targetWs && works.items && works.items.length) setTargetWs(works.items[0].workspaceId)
|
|
268
197
|
})
|
|
269
198
|
.catch((e) => setError(String((e && e.message) || e)))
|
|
@@ -280,16 +209,46 @@ function MovePanel() {
|
|
|
280
209
|
return w ? w.path : ''
|
|
281
210
|
}
|
|
282
211
|
|
|
212
|
+
const archivedList = sessions ? sessions.filter((x) => x.archived) : []
|
|
213
|
+
const activeList = sessions ? sessions.filter((x) => !x.archived) : []
|
|
214
|
+
const list = filter === 'archived' ? archivedList : sessions || []
|
|
215
|
+
const selIds = Object.keys(selected).filter((k) => selected[k])
|
|
216
|
+
|
|
217
|
+
const toggle = (id) => setSelected((s) => ({ ...s, [id]: !s[id] }))
|
|
218
|
+
const clearSel = () => setSelected({})
|
|
219
|
+
const selectAll = () => {
|
|
220
|
+
const o = {}
|
|
221
|
+
list.forEach((x) => { o[x.sessionId] = true })
|
|
222
|
+
setSelected(o)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const act = (action, it) => {
|
|
226
|
+
if (busy) return
|
|
227
|
+
if (action === 'delete') {
|
|
228
|
+
if (confirmDel !== it.sessionId) { setConfirmDel(it.sessionId); return }
|
|
229
|
+
setConfirmDel(null)
|
|
230
|
+
}
|
|
231
|
+
setBusy(it.sessionId)
|
|
232
|
+
postJSON('/archived-sessions/' + action, { sessionId: it.sessionId })
|
|
233
|
+
.then(() => {
|
|
234
|
+
setBusy(null)
|
|
235
|
+
const n = it.title || it.sessionId
|
|
236
|
+
showToast(action === 'archive' ? `已归档「${n}」` : action === 'restore' ? `已恢复「${n}」` : `已删除 ${n}`)
|
|
237
|
+
refresh()
|
|
238
|
+
})
|
|
239
|
+
.catch((e) => { setBusy(null); setConfirmDel(null); setError(String((e && e.message) || e)) })
|
|
240
|
+
}
|
|
241
|
+
|
|
283
242
|
const doMove = (it) => {
|
|
284
|
-
const targetPath =
|
|
243
|
+
const targetPath = moveMode === 'new' ? newPath.trim() : wsPath(targetWs)
|
|
285
244
|
if (!targetPath) { setError('请选择已有工作区或输入新的目标目录路径'); return }
|
|
286
245
|
setBusy(it.sessionId)
|
|
287
246
|
setError(null)
|
|
288
247
|
postJSON('/archived-sessions/move', { sessionId: it.sessionId, targetPath })
|
|
289
248
|
.then((r) => {
|
|
290
249
|
setBusy(null)
|
|
291
|
-
|
|
292
|
-
|
|
250
|
+
setOpenMove(null)
|
|
251
|
+
setMoveMode('existing')
|
|
293
252
|
setNewPath('')
|
|
294
253
|
showToast(`已把「${it.title || it.sessionId}」移到 ${r.workspaceTitle || targetPath}`)
|
|
295
254
|
refresh()
|
|
@@ -297,188 +256,219 @@ function MovePanel() {
|
|
|
297
256
|
.catch((e) => { setBusy(null); setError(String((e && e.message) || e)) })
|
|
298
257
|
}
|
|
299
258
|
|
|
300
|
-
const
|
|
301
|
-
if (
|
|
302
|
-
|
|
259
|
+
const doBatch = (action) => {
|
|
260
|
+
if (!selIds.length || busy) return
|
|
261
|
+
if (action === 'delete-many') {
|
|
262
|
+
if (!confirmBatch) { setConfirmBatch(true); return }
|
|
263
|
+
setConfirmBatch(false)
|
|
264
|
+
}
|
|
265
|
+
setBusy('__batch__')
|
|
266
|
+
postJSON('/archived-sessions/' + action, { sessionIds: selIds })
|
|
267
|
+
.then((r) => {
|
|
268
|
+
setBusy(null)
|
|
269
|
+
const n = (r && (r.archived || r.restored || r.deleted)) || selIds.length
|
|
270
|
+
showToast(`已处理 ${n} 个会话`)
|
|
271
|
+
refresh()
|
|
272
|
+
})
|
|
273
|
+
.catch((e) => { setBusy(null); setConfirmBatch(false); setError(String((e && e.message) || e)) })
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const openMoveFor = (it) => {
|
|
277
|
+
if (openMove === it.sessionId) { setOpenMove(null); return }
|
|
278
|
+
setTargetWs(workspaces.length ? (targetWs || workspaces[0].workspaceId) : '')
|
|
279
|
+
setMoveMode('existing')
|
|
280
|
+
setNewPath('')
|
|
281
|
+
setOpenMove(it.sessionId)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const pickDirectory = async () => {
|
|
285
|
+
if (!workspacesSvc || picking) return
|
|
286
|
+
setPicking(true)
|
|
287
|
+
try {
|
|
288
|
+
const p = await workspacesSvc.pickDirectory()
|
|
289
|
+
if (p) { setNewPath(p); setMoveMode('new') }
|
|
290
|
+
} catch (e) {
|
|
291
|
+
setError(String((e && e.message) || e))
|
|
292
|
+
} finally {
|
|
293
|
+
setPicking(false)
|
|
303
294
|
}
|
|
304
|
-
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const workspaceTag = (it) => {
|
|
298
|
+
if (it.hasWorkspace && it.workspaceGone) {
|
|
299
|
+
return <span className="archv-wtag archv-wgone" title={(it.workspacePath || '') + '(原工作区已删除)'}>工作区已删 · {pathName(it.workspacePath) || '?'}</span>
|
|
300
|
+
}
|
|
301
|
+
const wName = it.workspaceTitle || pathName(it.workspacePath)
|
|
302
|
+
if (wName) return <span className="archv-wtag" title={it.workspacePath || ''}>{wName}</span>
|
|
303
|
+
return <span className="archv-wtag">未分组</span>
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const rowActions = (it) => {
|
|
307
|
+
if (it.archived) {
|
|
308
|
+
return (
|
|
309
|
+
<div className="archv-actions">
|
|
310
|
+
<button type="button" className="archv-btn archv-go" disabled={busy !== null} onClick={() => act('restore', it)}>恢复</button>
|
|
311
|
+
<button type="button" className="archv-btn archv-go" disabled={busy !== null} onClick={() => openMoveFor(it)}>{openMove === it.sessionId ? '收起' : '移动…'}</button>
|
|
312
|
+
<button type="button" className="archv-btn archv-del" disabled={busy !== null} onClick={() => act('delete', it)}>{confirmDel === it.sessionId ? '确认删除?' : '删除'}</button>
|
|
313
|
+
</div>
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
return (
|
|
317
|
+
<div className="archv-actions">
|
|
318
|
+
<button type="button" className="archv-btn" disabled={busy !== null} onClick={() => act('archive', it)}>归档</button>
|
|
319
|
+
<button type="button" className="archv-btn archv-go" disabled={busy !== null} onClick={() => openMoveFor(it)}>{openMove === it.sessionId ? '收起' : '移动…'}</button>
|
|
320
|
+
</div>
|
|
321
|
+
)
|
|
305
322
|
}
|
|
306
323
|
|
|
307
324
|
return (
|
|
308
|
-
<div className="
|
|
309
|
-
<style>{
|
|
310
|
-
<div className="
|
|
311
|
-
<h2 className="
|
|
312
|
-
{sessions !== null && <span className="
|
|
325
|
+
<div className="archv" role="region" aria-label="会话管理">
|
|
326
|
+
<style>{CSS}</style>
|
|
327
|
+
<div className="archv-head">
|
|
328
|
+
<h2 className="archv-title">会话管理</h2>
|
|
329
|
+
{sessions !== null && <span className="archv-count" aria-label={`${sessions.length} 个会话`}>{sessions.length}</span>}
|
|
313
330
|
</div>
|
|
314
|
-
<p className="
|
|
315
|
-
|
|
316
|
-
会话当前处于打开状态时需先在侧栏切走才能移动。
|
|
331
|
+
<p className="archv-sub">
|
|
332
|
+
统一管理全部会话:归档 / 恢复 / 彻底删除 / 移动到其他工作区,支持批量。归档会从侧栏隐藏;删除会彻底移除日志,不可恢复。
|
|
317
333
|
</p>
|
|
318
334
|
{error && (
|
|
319
|
-
<div className="
|
|
335
|
+
<div className="archv-err" role="alert">
|
|
320
336
|
<span>{error}</span>
|
|
321
|
-
<button type="button" className="
|
|
337
|
+
<button type="button" className="archv-errretry" onClick={refresh}>重试</button>
|
|
322
338
|
</div>
|
|
323
339
|
)}
|
|
324
340
|
{sessions === null ? (
|
|
325
|
-
<div className="
|
|
326
|
-
{[0, 1, 2].map((i) => <div key={i} className="
|
|
341
|
+
<div className="archv-skel" aria-label="加载中">
|
|
342
|
+
{[0, 1, 2].map((i) => <div key={i} className="archv-skel-card" />)}
|
|
327
343
|
</div>
|
|
328
|
-
) : sessions.length === 0 ? (
|
|
329
|
-
<div className="mv-empty">暂无可移动的会话。</div>
|
|
330
344
|
) : (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
345
|
+
<>
|
|
346
|
+
<div className="sess-filter" role="tablist" aria-label="会话筛选">
|
|
347
|
+
<button type="button" className={'sess-fbtn' + (filter === 'all' ? ' sess-fbtn-on' : '')} onClick={() => { setFilter('all'); clearSel(); setConfirmBatch(false) }}>全部 ({sessions.length})</button>
|
|
348
|
+
<button type="button" className={'sess-fbtn' + (filter === 'archived' ? ' sess-fbtn-on' : '')} onClick={() => { setFilter('archived'); clearSel(); setConfirmBatch(false) }}>已归档 ({archivedList.length})</button>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
{list.length > 0 && (
|
|
352
|
+
<div className="sess-batch">
|
|
353
|
+
<span className="sess-btntext">{selIds.length ? `已选 ${selIds.length} 项` : (filter === 'archived' ? `共 ${archivedList.length} 个归档会话` : `共 ${sessions.length} 个会话(活动 ${activeList.length} / 已归档 ${archivedList.length})`)}</span>
|
|
354
|
+
<button type="button" className="archv-btn" disabled={list.length === 0} onClick={selectAll}>全选</button>
|
|
355
|
+
{selIds.length > 0 && (
|
|
356
|
+
<>
|
|
357
|
+
{filter === 'archived' && (
|
|
358
|
+
<>
|
|
359
|
+
<button type="button" className="archv-btn" disabled={busy !== null} onClick={() => doBatch('restore-many')}>恢复所选</button>
|
|
360
|
+
<button type="button" className="archv-btn archv-del" disabled={busy !== null} onClick={() => doBatch('delete-many')}>{confirmBatch ? '确认删除所选?' : '删除所选'}</button>
|
|
361
|
+
</>
|
|
362
|
+
)}
|
|
363
|
+
{filter === 'all' && (
|
|
364
|
+
<button type="button" className="archv-btn" disabled={busy !== null} onClick={() => doBatch('archive-many')}>归档所选</button>
|
|
365
|
+
)}
|
|
366
|
+
<button type="button" className="archv-btn" onClick={clearSel}>取消选择</button>
|
|
367
|
+
</>
|
|
368
|
+
)}
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
371
|
+
|
|
372
|
+
{list.length === 0 ? (
|
|
373
|
+
<div className="archv-empty">{filter === 'archived' ? '目前没有归档会话。在“全部”里选中会话点“归档”即可收纳进来。' : '暂无可管理的会话。'}</div>
|
|
374
|
+
) : (
|
|
375
|
+
<div className="archv-list" role="list">
|
|
376
|
+
{list.map((it) => {
|
|
377
|
+
const date = fmtDate(it.createdAt)
|
|
378
|
+
const expanded = openMove === it.sessionId
|
|
379
|
+
return (
|
|
380
|
+
<div key={it.sessionId} className={'archv-card' + (expanded ? ' archv-card-exp' : '')} role="listitem">
|
|
381
|
+
<div className="archv-row">
|
|
382
|
+
<input
|
|
383
|
+
type="checkbox"
|
|
384
|
+
className="archv-check"
|
|
385
|
+
checked={!!selected[it.sessionId]}
|
|
386
|
+
onChange={() => toggle(it.sessionId)}
|
|
387
|
+
aria-label={'选择 ' + (it.title || it.sessionId)}
|
|
388
|
+
/>
|
|
389
|
+
<div className="archv-body">
|
|
390
|
+
<div className="archv-main">
|
|
391
|
+
<div className="archv-name" title={it.title || ''}>{it.title || '(无标题)'}</div>
|
|
392
|
+
<div className="archv-meta">
|
|
393
|
+
{it.archived ? <span className="archv-wtag archv-wgone">已归档</span> : <span className="archv-wtag archv-active">活动</span>}
|
|
394
|
+
{workspaceTag(it)}
|
|
395
|
+
{date && <><span className="archv-dot">·</span><span className="archv-date">{date}</span></>}
|
|
396
|
+
<span className="archv-id">{it.sessionId}</span>
|
|
397
|
+
</div>
|
|
374
398
|
</div>
|
|
375
|
-
|
|
376
|
-
<div className="mv-sheet-row">
|
|
377
|
-
<button type="button" className="mv-btn mv-go" disabled={busy !== null} onClick={() => doMove(it)}>
|
|
378
|
-
{busy === it.sessionId && <span className="mv-spin" aria-hidden="true" />}确认移动
|
|
379
|
-
</button>
|
|
380
|
-
<button type="button" className="mv-btn" disabled={busy !== null} onClick={() => { setOpen(null); setMode('existing'); setNewPath(''); }}>取消</button>
|
|
399
|
+
{rowActions(it)}
|
|
381
400
|
</div>
|
|
382
401
|
</div>
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
402
|
+
{expanded && (
|
|
403
|
+
<div className="mv-sheet" role="region" aria-label="移动到工作区">
|
|
404
|
+
<div className="mv-sheet-head">
|
|
405
|
+
<h3 className="mv-sheet-title">移动到工作区</h3>
|
|
406
|
+
<button type="button" className="mv-sheet-close" aria-label="关闭" onClick={() => setOpenMove(null)}>×</button>
|
|
407
|
+
</div>
|
|
408
|
+
<div className="mv-seg" role="tablist">
|
|
409
|
+
<button type="button" role="tab" aria-selected={moveMode === 'existing'} className={'mv-segbtn' + (moveMode === 'existing' ? ' mv-segbtn-on' : '')} onClick={() => setMoveMode('existing')}>已有工作区</button>
|
|
410
|
+
<button type="button" role="tab" aria-selected={moveMode === 'new'} className={'mv-segbtn' + (moveMode === 'new' ? ' mv-segbtn-on' : '')} onClick={() => setMoveMode('new')}>新建目录</button>
|
|
411
|
+
</div>
|
|
412
|
+
{moveMode === 'existing' ? (
|
|
413
|
+
<div className="mv-field">
|
|
414
|
+
<label className="mv-field-label" htmlFor="mv-target-ws">目标工作区</label>
|
|
415
|
+
<select id="mv-target-ws" value={targetWs} onChange={(e) => setTargetWs(e.target.value)}>
|
|
416
|
+
{workspaces.length === 0 && <option value="">(暂无工作区)</option>}
|
|
417
|
+
{workspaces.map((w) => (
|
|
418
|
+
<option key={w.workspaceId} value={w.workspaceId}>{w.title} · {w.path}</option>
|
|
419
|
+
))}
|
|
420
|
+
</select>
|
|
421
|
+
</div>
|
|
422
|
+
) : (
|
|
423
|
+
<div className="mv-field">
|
|
424
|
+
<label className="mv-field-label" htmlFor="mv-new-path">新工作区目录路径</label>
|
|
425
|
+
<div className="mv-browse-row">
|
|
426
|
+
<input
|
|
427
|
+
id="mv-new-path"
|
|
428
|
+
type="text"
|
|
429
|
+
value={newPath}
|
|
430
|
+
onChange={(e) => setNewPath(e.target.value)}
|
|
431
|
+
placeholder="例如 /Users/you/Projects/demo 或 ~/demo"
|
|
432
|
+
/>
|
|
433
|
+
<button
|
|
434
|
+
type="button"
|
|
435
|
+
className="archv-btn"
|
|
436
|
+
onClick={pickDirectory}
|
|
437
|
+
disabled={busy !== null || picking || !workspacesSvc}
|
|
438
|
+
title={!workspacesSvc ? '当前运行环境不支持系统目录选择' : '打开系统目录选择窗口'}
|
|
439
|
+
>
|
|
440
|
+
{picking ? '选择中…' : '浏览…'}
|
|
441
|
+
</button>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
)}
|
|
445
|
+
<div className="mv-foot">
|
|
446
|
+
<button type="button" className="archv-btn" onClick={() => setOpenMove(null)}>取消</button>
|
|
447
|
+
<button type="button" className="archv-btn archv-go" disabled={busy !== null} onClick={() => doMove(it)}>
|
|
448
|
+
{busy === it.sessionId && <span className="archv-spin" aria-hidden="true" />}确认移动
|
|
449
|
+
</button>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
)
|
|
455
|
+
})}
|
|
456
|
+
</div>
|
|
457
|
+
)}
|
|
458
|
+
</>
|
|
400
459
|
)}
|
|
401
|
-
{toast && <div className="
|
|
460
|
+
{toast && <div className="archv-status" role="status">{toast}</div>}
|
|
402
461
|
</div>
|
|
403
462
|
)
|
|
404
463
|
}
|
|
405
464
|
|
|
406
|
-
// Left-nav icon swap for the two plugin settings sections.
|
|
407
|
-
//
|
|
408
|
-
// DSH's settings shell renders a *shared* fallback gear for every custom
|
|
409
|
-
// section (its internal navIcon() only differentiates 模型/插件/Agent预设), and
|
|
410
|
-
// settings.section has no per-section icon field. So — exactly like
|
|
411
|
-
// dsh-better-sidebar — we mark our own rows by matching their visible label
|
|
412
|
-
// text against the shell DOM, then let CSS swap the gear for a distinct glyph.
|
|
413
|
-
// The marker owns no shell structure and is removed on fiber disposal, so the
|
|
414
|
-
// adaptation stays HMR-safe and ships with the plugin.
|
|
415
|
-
const NAV_CSS = `
|
|
416
|
-
[data-dsh-nav-archived] > svg:first-child,
|
|
417
|
-
[data-dsh-nav-move] > svg:first-child { display: none; }
|
|
418
|
-
[data-dsh-nav-archived]::before,
|
|
419
|
-
[data-dsh-nav-move]::before {
|
|
420
|
-
content: ''; flex: none; width: 16px; height: 16px; background: currentColor;
|
|
421
|
-
-webkit-mask: center / contain no-repeat; mask: center / contain no-repeat;
|
|
422
|
-
}
|
|
423
|
-
[data-dsh-nav-archived]::before {
|
|
424
|
-
-webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http://www.w3.org/2000/svg'%20width%3D'24'%20height%3D'24'%20viewBox%3D'0%200%2024%2024'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%3E%3Cpath%20d%3D'M21%208v13H3V8'/%3E%3Cpath%20d%3D'M1%203h22v5H1z'/%3E%3Cpath%20d%3D'M10%2012h4'/%3E%3C/svg%3E");
|
|
425
|
-
mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http://www.w3.org/2000/svg'%20width%3D'24'%20height%3D'24'%20viewBox%3D'0%200%2024%2024'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%3E%3Cpath%20d%3D'M21%208v13H3V8'/%3E%3Cpath%20d%3D'M1%203h22v5H1z'/%3E%3Cpath%20d%3D'M10%2012h4'/%3E%3C/svg%3E");
|
|
426
|
-
}
|
|
427
|
-
[data-dsh-nav-move]::before {
|
|
428
|
-
-webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http://www.w3.org/2000/svg'%20width%3D'24'%20height%3D'24'%20viewBox%3D'0%200%2024%2024'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%3E%3Cpath%20d%3D'M22%2019a2%202%200%200%201-2%202H4a2%202%200%200%201-2-2V5a2%202%200%200%201%202-2h5l2%203h9a2%202%200%200%201%202%202z'/%3E%3Cpath%20d%3D'M12%2010v6'/%3E%3Cpath%20d%3D'm15%2013-3%203-3-3'/%3E%3C/svg%3E");
|
|
429
|
-
mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http://www.w3.org/2000/svg'%20width%3D'24'%20height%3D'24'%20viewBox%3D'0%200%2024%2024'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%3E%3Cpath%20d%3D'M22%2019a2%202%200%200%201-2%202H4a2%202%200%200%201-2-2V5a2%202%200%200%201%202-2h5l2%203h9a2%202%200%200%201%202%202z'/%3E%3Cpath%20d%3D'M12%2010v6'/%3E%3Cpath%20d%3D'm15%2013-3%203-3-3'/%3E%3C/svg%3E");
|
|
430
|
-
}
|
|
431
|
-
`
|
|
432
|
-
|
|
433
|
-
function markSettingsNav() {
|
|
434
|
-
const ARCHIVED_LABEL = '归档会话'
|
|
435
|
-
const MOVE_LABEL = '移动会话'
|
|
436
|
-
let disposed = false
|
|
437
|
-
const sync = () => {
|
|
438
|
-
if (disposed) return
|
|
439
|
-
const buttons = document.querySelectorAll('[role="dialog"] nav button')
|
|
440
|
-
for (const b of buttons) {
|
|
441
|
-
const t = (b.textContent || '').trim()
|
|
442
|
-
if (t === ARCHIVED_LABEL) b.setAttribute('data-dsh-nav-archived', '')
|
|
443
|
-
else b.removeAttribute('data-dsh-nav-archived')
|
|
444
|
-
if (t === MOVE_LABEL) b.setAttribute('data-dsh-nav-move', '')
|
|
445
|
-
else b.removeAttribute('data-dsh-nav-move')
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
sync()
|
|
449
|
-
const obs = new MutationObserver(sync)
|
|
450
|
-
obs.observe(document.body, { childList: true, subtree: true, characterData: true })
|
|
451
|
-
return () => {
|
|
452
|
-
disposed = true
|
|
453
|
-
obs.disconnect()
|
|
454
|
-
document.querySelectorAll('[data-dsh-nav-archived]').forEach((e) => e.removeAttribute('data-dsh-nav-archived'))
|
|
455
|
-
document.querySelectorAll('[data-dsh-nav-move]').forEach((e) => e.removeAttribute('data-dsh-nav-move'))
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function installSettingsNavIcons(ctx) {
|
|
460
|
-
const styleEl = document.createElement('style')
|
|
461
|
-
styleEl.textContent = NAV_CSS
|
|
462
|
-
document.head.appendChild(styleEl)
|
|
463
|
-
const dispose = markSettingsNav()
|
|
464
|
-
ctx.effect(() => () => {
|
|
465
|
-
dispose()
|
|
466
|
-
if (styleEl.parentNode) styleEl.parentNode.removeChild(styleEl)
|
|
467
|
-
})
|
|
468
|
-
}
|
|
469
|
-
|
|
470
465
|
export function apply(ctx) {
|
|
471
466
|
installSettingsNavIcons(ctx)
|
|
467
|
+
const workspacesSvc = ctx.get('workspaces')
|
|
472
468
|
ctx.slots.inject('settings.section', () =>
|
|
473
469
|
ctx.slots.register(
|
|
474
|
-
{ name: 'settings.section', id: '
|
|
475
|
-
(props) => <
|
|
476
|
-
),
|
|
477
|
-
)
|
|
478
|
-
ctx.slots.inject('settings.section', () =>
|
|
479
|
-
ctx.slots.register(
|
|
480
|
-
{ name: 'settings.section', id: 'move-sessions', order: 91, label: '移动会话' },
|
|
481
|
-
(props) => <MovePanel {...props} />,
|
|
470
|
+
{ name: 'settings.section', id: 'session-manager', order: 90, label: '会话管理' },
|
|
471
|
+
(props) => <SessionPanel {...props} workspacesSvc={workspacesSvc} />,
|
|
482
472
|
),
|
|
483
473
|
)
|
|
484
474
|
}
|