dsh-workspace-plus 0.4.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -446
- package/cordis.patch.yml +45 -8
- package/lib/client.js +1040 -231
- package/lib/notice/click.ps1 +67 -0
- package/lib/notice/format.js +313 -0
- package/lib/notice/icon.svg +16 -0
- package/lib/notice/index.js +951 -0
- package/lib/notice/locale/en.json +6 -0
- package/lib/notice/locale/zh.json +6 -0
- package/lib/notice/package.json +6 -0
- package/lib/notice/settings.js +336 -0
- package/lib/notice/toast-host.ps1 +616 -0
- package/lib/notice/toast.js +517 -0
- package/lib/notice/win32.js +206 -0
- package/package.json +12 -4
package/lib/client.js
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* plugin is first needed, so every side effect here — the injected stylesheet
|
|
9
9
|
* included — happens at materialization time, never at script-parse time.
|
|
10
10
|
*
|
|
11
|
-
* The feature — workspace directories, as a DIALOG rather than a page.
|
|
11
|
+
* The workspace+ feature — workspace directories, as a DIALOG rather than a page.
|
|
12
12
|
* A DSH workspace keeps its one root directory; this registers N more project
|
|
13
13
|
* folders against it, each with a short label, and the host injects that table
|
|
14
14
|
* into the agent's context on every request — so "改 backend" in conversation
|
|
15
15
|
* resolves to an absolute path instead of a guess.
|
|
16
16
|
*
|
|
17
|
-
* Five seats cooperate:
|
|
17
|
+
* Five workspace+ seats cooperate:
|
|
18
18
|
*
|
|
19
19
|
* `settings.section` the `workspace+` settings page. It owns one preference:
|
|
20
20
|
* whether the feature runs for the default (first-use)
|
|
@@ -24,18 +24,24 @@
|
|
|
24
24
|
* `shell.overlay` the dialog itself, the same frame-wide layer the
|
|
25
25
|
* shell's own workspace Rename/Delete dialogs use (the
|
|
26
26
|
* Modal portals to <body>, so the entry owns only the
|
|
27
|
-
* open state, not the stack position).
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* open state, not the stack position). It reads as one
|
|
28
|
+
* column of bordered field rows — the workspace it is
|
|
29
|
+
* locked to, the 「目录」 list, an add row that IS the
|
|
30
|
+
* shell's own directory picker, and a footer split
|
|
31
|
+
* secondary-left / primary-right. There is no path input
|
|
32
|
+
* to type into: `ctx.uiWorkspace.pickDirectory()` is the
|
|
33
|
+
* only way a directory gets in, and the label/note row
|
|
34
|
+
* only appears once one has been chosen.
|
|
31
35
|
*
|
|
32
36
|
* workspace row button the primary trigger. The shell's workspace row menu is
|
|
33
37
|
* hardcoded to Rename/Delete with no plugin slot, so the
|
|
34
38
|
* trigger is a button this half appends to the row's
|
|
35
39
|
* action cluster, located through shell-emitted
|
|
36
40
|
* `data-row-key="workspace:<id>"` anchors and kept in
|
|
37
|
-
* place by a MutationObserver.
|
|
38
|
-
*
|
|
41
|
+
* place by a MutationObserver. Every real workspace row
|
|
42
|
+
* carries it; a workspace whose switch is off only draws
|
|
43
|
+
* it dimmed, because the dialog is where its labels are
|
|
44
|
+
* read and the switch itself lives on the settings page.
|
|
39
45
|
*
|
|
40
46
|
* `sidebar.panellist` the fallback trigger, registered only while no row
|
|
41
47
|
* button could be installed (a shell whose row DOM
|
|
@@ -52,6 +58,19 @@
|
|
|
52
58
|
* absolute path, so the model receives the path even when
|
|
53
59
|
* the injected table is not in context.
|
|
54
60
|
*
|
|
61
|
+
* The 通知 (notice) sub-feature — ported from the dsh-notice plugin, which now
|
|
62
|
+
* ships as the `dsh-workspace-plus/notice` loader row of this same bundle — adds
|
|
63
|
+
* two more seats here, described in detail at its section below:
|
|
64
|
+
*
|
|
65
|
+
* `settings.section` the 通知 settings page (same id, label and order the
|
|
66
|
+
* standalone plugin used), writing through the same
|
|
67
|
+
* `/dsh-notice/api` routes. When the notice row is off
|
|
68
|
+
* the host answers 404 and the page explains the state.
|
|
69
|
+
*
|
|
70
|
+
* the pending poll the last centimetre of the click-to-open path: toast
|
|
71
|
+
* click → `dsh-notice:` protocol → host route → pending
|
|
72
|
+
* intent → THIS half → `ctx.uiWorkspace.openSession()`.
|
|
73
|
+
*
|
|
55
74
|
* Only the shell's frozen platform module table may be `require`d here (react,
|
|
56
75
|
* react-dom, react/jsx-runtime, @deepseek-ai/cordis,
|
|
57
76
|
* @deepseek-ai/dsh-client-store, @deepseek-ai/dsh-client-ui-slots,
|
|
@@ -113,6 +132,506 @@ window.__ModuleLoader__.load({
|
|
|
113
132
|
/** Host JSON endpoint, resolved against the page so it survives any mount path. */
|
|
114
133
|
const API_PATH = 'workspaceplus/api'
|
|
115
134
|
|
|
135
|
+
// -----------------------------------------------------------------------
|
|
136
|
+
// The 通知 (notice) sub-feature — client side
|
|
137
|
+
//
|
|
138
|
+
// The notice host half (`lib/notice/index.js`, the `dsh-workspace-plus/notice`
|
|
139
|
+
// loader row) owns the Windows transport and three HTTP routes. This half
|
|
140
|
+
// contributes the two browser-side pieces the dsh-notice plugin used to ship
|
|
141
|
+
// in its own client bundle:
|
|
142
|
+
//
|
|
143
|
+
// the 通知 settings page — same slot (`settings.section`), same id, same
|
|
144
|
+
// order, writing through the SAME routes as before (`/dsh-notice/api`).
|
|
145
|
+
// When the notice row is disabled the host answers 404, the page renders
|
|
146
|
+
// that state, and nothing else in this bundle is affected.
|
|
147
|
+
//
|
|
148
|
+
// the pending-open poll — the last centimetre of the click-to-open path:
|
|
149
|
+
// toast click → protocol → host route → pending intent → THIS half →
|
|
150
|
+
// ctx.uiWorkspace.openSession(). The poll is unconditionally cheap (one
|
|
151
|
+
// 404-tolerant GET every 4 s) and is what makes clicking a notification
|
|
152
|
+
// open the conversation; it stays on even when the settings page cannot
|
|
153
|
+
// load, because a notification may outlive a row toggle.
|
|
154
|
+
//
|
|
155
|
+
// Route paths and the settings-page identity are kept byte-identical to
|
|
156
|
+
// dsh-notice 0.2.1, so an in-place upgrade preserves the protocol scheme,
|
|
157
|
+
// the registry command and every stored override.
|
|
158
|
+
// -----------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
/** Settings section identity of the 通知 page (unchanged from dsh-notice). */
|
|
161
|
+
const NOTICE_SETTINGS_SECTION_ID = 'notice'
|
|
162
|
+
const NOTICE_SETTINGS_SECTION_LABEL = '通知'
|
|
163
|
+
const NOTICE_SETTINGS_SECTION_ORDER = 26
|
|
164
|
+
|
|
165
|
+
/** Host routes the notice half talks to; must match lib/notice/index.js. */
|
|
166
|
+
const NOTICE_API_PATH = '/dsh-notice/api'
|
|
167
|
+
const NOTICE_PENDING_PATH = '/dsh-notice/api/pending'
|
|
168
|
+
|
|
169
|
+
/** Backstop poll interval, and the retry interval after a failed poll. */
|
|
170
|
+
const NOTICE_POLL_INTERVAL_MS = 4000
|
|
171
|
+
const NOTICE_POLL_RETRY_MS = 2000
|
|
172
|
+
|
|
173
|
+
/** Stylesheet identity for the 通知 settings page (one tag, rewritten on reload). */
|
|
174
|
+
const NOTICE_STYLE_ID = 'dsh-workspace-plus/notice.css'
|
|
175
|
+
|
|
176
|
+
/** Prefix for every class the notice page injects, so it can never collide. */
|
|
177
|
+
const NOTICE_CSS = `
|
|
178
|
+
.dsh-notice-section { display: flex; flex-direction: column; gap: 14px; }
|
|
179
|
+
.dsh-notice-group { display: flex; flex-direction: column; gap: 10px; }
|
|
180
|
+
.dsh-notice-group-title { color: var(--dsw-alias-label-secondary); font-size: 12px; font-weight: 600; letter-spacing: .04em; }
|
|
181
|
+
.dsh-notice-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
|
182
|
+
.dsh-notice-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
|
183
|
+
.dsh-notice-title { color: var(--dsw-alias-label-primary); font-size: 14px; font-weight: 600; }
|
|
184
|
+
.dsh-notice-desc { color: var(--dsw-alias-label-secondary); font-size: 12.5px; line-height: 1.5; }
|
|
185
|
+
.dsh-notice-switch { position: relative; flex: none; width: 38px; height: 22px; margin-top: 2px; padding: 0; cursor: pointer; border: 1px solid var(--dsw-alias-border-l2); border-radius: 999px; background: var(--dsw-alias-bg-layer-2); transition: background .15s ease, border-color .15s ease; }
|
|
186
|
+
.dsh-notice-switch[aria-checked="true"] { background: var(--dsw-alias-brand-primary); border-color: var(--dsw-alias-brand-primary); }
|
|
187
|
+
.dsh-notice-switch:disabled { cursor: default; opacity: .5; }
|
|
188
|
+
.dsh-notice-knob { position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: var(--dsw-alias-bg-base); transition: transform .15s ease; }
|
|
189
|
+
.dsh-notice-switch[aria-checked="true"] .dsh-notice-knob { transform: translateX(16px); }
|
|
190
|
+
.dsh-notice-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
191
|
+
.dsh-notice-button { cursor: pointer; padding: 5px 12px; font-size: 13px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
|
|
192
|
+
.dsh-notice-button:hover:not(:disabled) { border-color: var(--dsw-alias-border-l1); }
|
|
193
|
+
.dsh-notice-button:disabled { cursor: default; opacity: .5; }
|
|
194
|
+
.dsh-notice-status { display: flex; flex-direction: column; gap: 4px; padding: 10px 12px; border-radius: 8px; background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary); font-size: 12.5px; line-height: 1.6; }
|
|
195
|
+
.dsh-notice-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; word-break: break-all; }
|
|
196
|
+
.dsh-notice-error { color: var(--dsw-alias-state-error-primary); }
|
|
197
|
+
.dsh-notice-ok { color: var(--dsw-alias-state-success-primary); }
|
|
198
|
+
`
|
|
199
|
+
|
|
200
|
+
/** Install the notice stylesheet, exactly one tag per document (rewritten on reload). */
|
|
201
|
+
function ensureNoticeStyles() {
|
|
202
|
+
if (typeof document === 'undefined') return
|
|
203
|
+
const existing = document.querySelector('style[data-plugin-css=' + JSON.stringify(NOTICE_STYLE_ID) + ']')
|
|
204
|
+
if (existing !== null) {
|
|
205
|
+
existing.textContent = NOTICE_CSS
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
const tag = document.createElement('style')
|
|
209
|
+
tag.dataset.plugin = 'dsh-workspace-plus'
|
|
210
|
+
tag.dataset.pluginCss = NOTICE_STYLE_ID
|
|
211
|
+
tag.textContent = NOTICE_CSS
|
|
212
|
+
document.head.appendChild(tag)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Fetch the notice host's snapshot.
|
|
217
|
+
*
|
|
218
|
+
* @param origin - the origin this page was served from, reported once so the
|
|
219
|
+
* host can learn its own listen port and register the protocol against it.
|
|
220
|
+
* @param signal - caller cancellation.
|
|
221
|
+
* @returns the parsed payload.
|
|
222
|
+
*/
|
|
223
|
+
async function fetchNoticeSnapshot(origin, signal) {
|
|
224
|
+
const query = origin === null ? '' : `?origin=${encodeURIComponent(origin)}`
|
|
225
|
+
const response = await fetch(`${NOTICE_API_PATH}${query}`, { cache: 'no-store', signal })
|
|
226
|
+
if (!response.ok) throw new Error(`host responded ${response.status}`)
|
|
227
|
+
return await response.json()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Post one notice control operation.
|
|
232
|
+
*
|
|
233
|
+
* @param body - operation payload.
|
|
234
|
+
* @returns the parsed payload.
|
|
235
|
+
*/
|
|
236
|
+
async function postNoticeOperation(body) {
|
|
237
|
+
const response = await fetch(NOTICE_API_PATH, {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
cache: 'no-store',
|
|
240
|
+
headers: { 'content-type': 'application/json' },
|
|
241
|
+
body: JSON.stringify(body),
|
|
242
|
+
})
|
|
243
|
+
if (!response.ok) throw new Error(`host responded ${response.status}`)
|
|
244
|
+
return await response.json()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* One labelled switch row of the 通知 page.
|
|
249
|
+
*
|
|
250
|
+
* @param props - `{ title, description, checked, disabled, onChange }`.
|
|
251
|
+
* @returns the row element.
|
|
252
|
+
*/
|
|
253
|
+
function NoticeSwitchRow(props) {
|
|
254
|
+
return React.createElement(
|
|
255
|
+
'div',
|
|
256
|
+
{ className: 'dsh-notice-row' },
|
|
257
|
+
React.createElement(
|
|
258
|
+
'div',
|
|
259
|
+
{ className: 'dsh-notice-text' },
|
|
260
|
+
React.createElement('div', { className: 'dsh-notice-title' }, props.title),
|
|
261
|
+
props.description ? React.createElement('div', { className: 'dsh-notice-desc' }, props.description) : null,
|
|
262
|
+
),
|
|
263
|
+
React.createElement(
|
|
264
|
+
'button',
|
|
265
|
+
{
|
|
266
|
+
type: 'button',
|
|
267
|
+
role: 'switch',
|
|
268
|
+
'aria-checked': props.checked === true,
|
|
269
|
+
'aria-label': props.title,
|
|
270
|
+
disabled: props.disabled === true,
|
|
271
|
+
className: 'dsh-notice-switch',
|
|
272
|
+
onClick: () => props.onChange(props.checked !== true),
|
|
273
|
+
},
|
|
274
|
+
React.createElement('span', { className: 'dsh-notice-knob' }),
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The 通知 settings page (the same page the dsh-notice plugin registered,
|
|
281
|
+
* now riding in this bundle). Every switch writes immediately; the host
|
|
282
|
+
* answers with a fresh snapshot, so the page never predicts what was
|
|
283
|
+
* accepted. A 404 — the notice row being disabled — renders as an
|
|
284
|
+
* explanation instead of an error, because that state is a choice, not a
|
|
285
|
+
* breakage.
|
|
286
|
+
*
|
|
287
|
+
* @returns the settings section element.
|
|
288
|
+
*/
|
|
289
|
+
function NoticeSettings() {
|
|
290
|
+
const [snapshot, setSnapshot] = React.useState(null)
|
|
291
|
+
const [status, setStatus] = React.useState({ kind: 'loading', message: '' })
|
|
292
|
+
const [busy, setBusy] = React.useState(false)
|
|
293
|
+
|
|
294
|
+
React.useEffect(() => {
|
|
295
|
+
let cancelled = false
|
|
296
|
+
const controller = typeof AbortController === 'undefined' ? undefined : new AbortController()
|
|
297
|
+
;(async () => {
|
|
298
|
+
try {
|
|
299
|
+
const payload = await fetchNoticeSnapshot(null, controller?.signal)
|
|
300
|
+
if (cancelled) return
|
|
301
|
+
setSnapshot(payload)
|
|
302
|
+
setStatus({ kind: 'ready', message: '' })
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (cancelled) return
|
|
305
|
+
setStatus({ kind: 'error', message: String((error && error.message) || error) })
|
|
306
|
+
}
|
|
307
|
+
})()
|
|
308
|
+
return () => {
|
|
309
|
+
cancelled = true
|
|
310
|
+
controller?.abort()
|
|
311
|
+
}
|
|
312
|
+
}, [])
|
|
313
|
+
|
|
314
|
+
const send = React.useCallback(async (body) => {
|
|
315
|
+
setBusy(true)
|
|
316
|
+
try {
|
|
317
|
+
const payload = await postNoticeOperation(body)
|
|
318
|
+
// A rejected operation answers with a message and no snapshot (an
|
|
319
|
+
// unknown op, a body that is not JSON). Adopting that as state would
|
|
320
|
+
// blank the page, so it only updates the status line.
|
|
321
|
+
if (payload !== null && typeof payload === 'object' && payload.settings !== undefined) {
|
|
322
|
+
setSnapshot(payload)
|
|
323
|
+
}
|
|
324
|
+
setStatus({
|
|
325
|
+
kind: payload !== null && payload.ok === true ? 'ready' : 'error',
|
|
326
|
+
message: String(payload?.message ?? ''),
|
|
327
|
+
})
|
|
328
|
+
} catch (error) {
|
|
329
|
+
setStatus({ kind: 'error', message: String((error && error.message) || error) })
|
|
330
|
+
} finally {
|
|
331
|
+
setBusy(false)
|
|
332
|
+
}
|
|
333
|
+
}, [])
|
|
334
|
+
|
|
335
|
+
if (snapshot === null) {
|
|
336
|
+
const message = status.kind === 'error' ? status.message : ''
|
|
337
|
+
// `host responded 404` is the notice ROW being switched off on the
|
|
338
|
+
// plugin page — a deliberate state, worded as one rather than as a
|
|
339
|
+
// failure. Any other message is a genuine transport error.
|
|
340
|
+
if (status.kind === 'error' && message.includes('404')) {
|
|
341
|
+
return React.createElement(
|
|
342
|
+
'div',
|
|
343
|
+
{ className: 'dsh-notice-section' },
|
|
344
|
+
React.createElement(
|
|
345
|
+
'div',
|
|
346
|
+
{ className: 'dsh-notice-desc' },
|
|
347
|
+
'通知子功能当前未启用:在侧栏「插件」页的 dsh-workspace-plus 卡片里打开 notice 这一行即可开启。',
|
|
348
|
+
),
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
return React.createElement(
|
|
352
|
+
'div',
|
|
353
|
+
{ className: 'dsh-notice-section' },
|
|
354
|
+
React.createElement(
|
|
355
|
+
'div',
|
|
356
|
+
{ className: 'dsh-notice-desc' },
|
|
357
|
+
status.kind === 'error' ? `读取设置失败:${message}` : '正在读取设置…',
|
|
358
|
+
),
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const settings = snapshot.settings ?? {}
|
|
363
|
+
const kinds = Array.isArray(snapshot.kinds) ? snapshot.kinds : []
|
|
364
|
+
const toast = snapshot.toast ?? {}
|
|
365
|
+
const protocol = snapshot.protocol ?? {}
|
|
366
|
+
const disabled = busy || settings.enabled !== true
|
|
367
|
+
|
|
368
|
+
const kindRows = kinds.map((kind) =>
|
|
369
|
+
React.createElement(NoticeSwitchRow, {
|
|
370
|
+
key: kind.id,
|
|
371
|
+
title: kind.label,
|
|
372
|
+
checked: kind.enabled === true,
|
|
373
|
+
disabled,
|
|
374
|
+
onChange: (next) => void send({ op: 'patch', patch: { kinds: { [kind.id]: next } } }),
|
|
375
|
+
}),
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
const transportLine = (() => {
|
|
379
|
+
if (snapshot.platformSupported !== true) return '当前平台不是 Windows,通知不会显示。'
|
|
380
|
+
// The daemon starts with the first notification, so "not running yet" is
|
|
381
|
+
// the ordinary state right after loading the plugin, not a fault.
|
|
382
|
+
if (toast.running !== true && toast.started !== true) {
|
|
383
|
+
return 'toast 守护进程尚未启动,将在第一条通知到来时自动启动。'
|
|
384
|
+
}
|
|
385
|
+
if (toast.running !== true) return 'toast 守护进程未运行,下一条通知会自动重新启动它。'
|
|
386
|
+
return `toast 守护进程运行中,以 ${
|
|
387
|
+
toast.appIdConfigured === true ? '指定' : '自动'
|
|
388
|
+
} AUMID 发言:${toast.appId ?? '(未知)'}`
|
|
389
|
+
})()
|
|
390
|
+
|
|
391
|
+
const protocolLine = (() => {
|
|
392
|
+
if (protocol.registered === true) return `点击通知可打开对应对话(已注册 ${protocol.scheme}:)。`
|
|
393
|
+
if (settings.registerProtocol === false) return '已关闭协议注册:点击通知只会把 Harness 窗口切到前台。'
|
|
394
|
+
return `尚未注册 ${protocol.scheme ?? 'dsh-notice'}:,点击通知只会把 Harness 窗口切到前台。`
|
|
395
|
+
})()
|
|
396
|
+
|
|
397
|
+
return React.createElement(
|
|
398
|
+
'div',
|
|
399
|
+
{ className: 'dsh-notice-section' },
|
|
400
|
+
React.createElement(
|
|
401
|
+
'div',
|
|
402
|
+
{ className: 'dsh-notice-group' },
|
|
403
|
+
React.createElement(NoticeSwitchRow, {
|
|
404
|
+
title: '启用桌面通知',
|
|
405
|
+
description: '对话需要你时,弹出 Windows 原生通知;点击通知可打开对应的对话。',
|
|
406
|
+
checked: settings.enabled === true,
|
|
407
|
+
disabled: busy,
|
|
408
|
+
onChange: (next) => void send({ op: 'patch', patch: { enabled: next } }),
|
|
409
|
+
}),
|
|
410
|
+
),
|
|
411
|
+
React.createElement(
|
|
412
|
+
'div',
|
|
413
|
+
{ className: 'dsh-notice-group' },
|
|
414
|
+
React.createElement('div', { className: 'dsh-notice-group-title' }, '通知类型'),
|
|
415
|
+
...kindRows,
|
|
416
|
+
),
|
|
417
|
+
React.createElement(
|
|
418
|
+
'div',
|
|
419
|
+
{ className: 'dsh-notice-group' },
|
|
420
|
+
React.createElement('div', { className: 'dsh-notice-group-title' }, '行为'),
|
|
421
|
+
React.createElement(NoticeSwitchRow, {
|
|
422
|
+
title: '只通知主对话',
|
|
423
|
+
description: '关闭后,子代理与团队成员的每一轮结束也会通知(通常非常密集)。',
|
|
424
|
+
checked: settings.scope === 'roots',
|
|
425
|
+
disabled,
|
|
426
|
+
onChange: (next) => void send({ op: 'patch', patch: { scope: next ? 'roots' : 'all' } }),
|
|
427
|
+
}),
|
|
428
|
+
React.createElement(NoticeSwitchRow, {
|
|
429
|
+
title: '通知声音',
|
|
430
|
+
description: '关闭后通知静音出现,仍然进入 Windows 通知中心。',
|
|
431
|
+
checked: settings.sound === true,
|
|
432
|
+
disabled,
|
|
433
|
+
onChange: (next) => void send({ op: 'patch', patch: { sound: next } }),
|
|
434
|
+
}),
|
|
435
|
+
React.createElement(NoticeSwitchRow, {
|
|
436
|
+
title: '点击时把 Harness 窗口切到前台',
|
|
437
|
+
description: '关闭后只切换对话,不改变窗口的前后顺序。',
|
|
438
|
+
checked: settings.focusOnClick === true,
|
|
439
|
+
disabled,
|
|
440
|
+
onChange: (next) => void send({ op: 'patch', patch: { focusOnClick: next } }),
|
|
441
|
+
}),
|
|
442
|
+
React.createElement(NoticeSwitchRow, {
|
|
443
|
+
title: '注册 dsh-notice: 协议',
|
|
444
|
+
description:
|
|
445
|
+
'在当前用户注册表下注册协议,让点击通知能定位到具体对话。关闭则退化为只切前台,不写注册表。',
|
|
446
|
+
checked: settings.registerProtocol === true,
|
|
447
|
+
disabled,
|
|
448
|
+
onChange: (next) => void send({ op: 'patch', patch: { registerProtocol: next } }),
|
|
449
|
+
}),
|
|
450
|
+
React.createElement(
|
|
451
|
+
'div',
|
|
452
|
+
{ className: 'dsh-notice-row' },
|
|
453
|
+
React.createElement(
|
|
454
|
+
'div',
|
|
455
|
+
{ className: 'dsh-notice-text' },
|
|
456
|
+
React.createElement('div', { className: 'dsh-notice-title' }, '通知显示时长'),
|
|
457
|
+
React.createElement(
|
|
458
|
+
'div',
|
|
459
|
+
{ className: 'dsh-notice-desc' },
|
|
460
|
+
settings.duration === 'long' ? '当前:长(约 25 秒)' : '当前:短(约 5 秒)',
|
|
461
|
+
),
|
|
462
|
+
),
|
|
463
|
+
React.createElement(
|
|
464
|
+
'button',
|
|
465
|
+
{
|
|
466
|
+
type: 'button',
|
|
467
|
+
className: 'dsh-notice-button',
|
|
468
|
+
disabled,
|
|
469
|
+
onClick: () => void send({ op: 'patch', patch: { duration: settings.duration === 'long' ? 'short' : 'long' } }),
|
|
470
|
+
},
|
|
471
|
+
settings.duration === 'long' ? '改为短' : '改为长',
|
|
472
|
+
),
|
|
473
|
+
),
|
|
474
|
+
),
|
|
475
|
+
React.createElement(
|
|
476
|
+
'div',
|
|
477
|
+
{ className: 'dsh-notice-actions' },
|
|
478
|
+
React.createElement(
|
|
479
|
+
'button',
|
|
480
|
+
{
|
|
481
|
+
type: 'button',
|
|
482
|
+
className: 'dsh-notice-button',
|
|
483
|
+
disabled: busy,
|
|
484
|
+
onClick: () => void send({ op: 'test' }),
|
|
485
|
+
},
|
|
486
|
+
'发送测试通知',
|
|
487
|
+
),
|
|
488
|
+
React.createElement(
|
|
489
|
+
'button',
|
|
490
|
+
{
|
|
491
|
+
type: 'button',
|
|
492
|
+
className: 'dsh-notice-button',
|
|
493
|
+
disabled: busy || Object.keys(snapshot.overrides ?? {}).length === 0,
|
|
494
|
+
onClick: () => void send({ op: 'clear' }),
|
|
495
|
+
},
|
|
496
|
+
'恢复默认',
|
|
497
|
+
),
|
|
498
|
+
),
|
|
499
|
+
React.createElement(
|
|
500
|
+
'div',
|
|
501
|
+
{ className: 'dsh-notice-status' },
|
|
502
|
+
React.createElement('div', null, transportLine),
|
|
503
|
+
React.createElement('div', null, protocolLine),
|
|
504
|
+
toast.lastError
|
|
505
|
+
? React.createElement('div', { className: 'dsh-notice-mono dsh-notice-error' }, String(toast.lastError))
|
|
506
|
+
: null,
|
|
507
|
+
status.message
|
|
508
|
+
? React.createElement(
|
|
509
|
+
'div',
|
|
510
|
+
{ className: status.kind === 'error' ? 'dsh-notice-error' : 'dsh-notice-ok' },
|
|
511
|
+
status.message,
|
|
512
|
+
)
|
|
513
|
+
: null,
|
|
514
|
+
),
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Poll the notice host for a pending "open this conversation" intent.
|
|
520
|
+
*
|
|
521
|
+
* A click on a notification focuses the desktop window; the focus event
|
|
522
|
+
* triggers an immediate poll, so the conversation usually opens on the same
|
|
523
|
+
* frame the window appears. The timer is only the backstop for a click that
|
|
524
|
+
* arrives while the window already has focus. A 404 (notice row disabled)
|
|
525
|
+
* and a network failure are both silent: this poll must never become
|
|
526
|
+
* noticeable, and it must never take the workspace+ features down.
|
|
527
|
+
*
|
|
528
|
+
* @param ctx - the plugin's client context.
|
|
529
|
+
* @returns a disposer stopping every timer, listener and request in flight.
|
|
530
|
+
*/
|
|
531
|
+
function startNoticePendingPoll(ctx) {
|
|
532
|
+
let stopped = false
|
|
533
|
+
let timer = null
|
|
534
|
+
let inFlight = false
|
|
535
|
+
const controller = typeof AbortController === 'undefined' ? undefined : new AbortController()
|
|
536
|
+
|
|
537
|
+
const openSession = (sessionId) => {
|
|
538
|
+
if (typeof sessionId !== 'string' || sessionId === '') return
|
|
539
|
+
let workspace = null
|
|
540
|
+
try {
|
|
541
|
+
workspace = ctx.get('uiWorkspace')
|
|
542
|
+
} catch {
|
|
543
|
+
return
|
|
544
|
+
}
|
|
545
|
+
if (workspace === undefined || workspace === null || typeof workspace.openSession !== 'function') return
|
|
546
|
+
try {
|
|
547
|
+
workspace.openSession(sessionId)
|
|
548
|
+
} catch {
|
|
549
|
+
// An archived or unknown conversation cannot be selected; the click
|
|
550
|
+
// still brought the window forward, which is the useful half.
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const schedule = (delay) => {
|
|
555
|
+
if (stopped) return
|
|
556
|
+
if (timer !== null) clearTimeout(timer)
|
|
557
|
+
timer = setTimeout(() => void tick(), delay)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const tick = async () => {
|
|
561
|
+
if (stopped || inFlight) return
|
|
562
|
+
inFlight = true
|
|
563
|
+
let delay = NOTICE_POLL_INTERVAL_MS
|
|
564
|
+
try {
|
|
565
|
+
const response = await fetch(`${NOTICE_PENDING_PATH}?_=${Date.now()}`, {
|
|
566
|
+
cache: 'no-store',
|
|
567
|
+
signal: controller?.signal,
|
|
568
|
+
})
|
|
569
|
+
if (response.ok) {
|
|
570
|
+
const payload = await response.json()
|
|
571
|
+
if (payload !== null && typeof payload === 'object' && typeof payload.sessionId === 'string') {
|
|
572
|
+
openSession(payload.sessionId)
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
delay = NOTICE_POLL_RETRY_MS
|
|
576
|
+
}
|
|
577
|
+
} catch {
|
|
578
|
+
delay = NOTICE_POLL_RETRY_MS
|
|
579
|
+
} finally {
|
|
580
|
+
inFlight = false
|
|
581
|
+
}
|
|
582
|
+
schedule(delay)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// A click on a notification focuses the desktop window, and that focus
|
|
586
|
+
// event is what makes the conversation open immediately rather than up to
|
|
587
|
+
// one poll interval later.
|
|
588
|
+
const onFocus = () => schedule(0)
|
|
589
|
+
window.addEventListener('focus', onFocus)
|
|
590
|
+
document.addEventListener('visibilitychange', onFocus)
|
|
591
|
+
|
|
592
|
+
// Report the origin once, so the host learns the port it is listening on
|
|
593
|
+
// and can point the protocol registration at it. The settings page loads
|
|
594
|
+
// lazily, so this one early GET is also the only thing that wakes the
|
|
595
|
+
// host's protocol sync when the user never opens 设置 → 通知.
|
|
596
|
+
void fetchNoticeSnapshot(window.location.origin, controller?.signal).catch(() => {})
|
|
597
|
+
void tick()
|
|
598
|
+
|
|
599
|
+
return () => {
|
|
600
|
+
stopped = true
|
|
601
|
+
if (timer !== null) clearTimeout(timer)
|
|
602
|
+
window.removeEventListener('focus', onFocus)
|
|
603
|
+
document.removeEventListener('visibilitychange', onFocus)
|
|
604
|
+
controller?.abort()
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Run one notice registration, reporting a failure instead of throwing it.
|
|
610
|
+
*
|
|
611
|
+
* The shell treats a non-ACTIVE client entry as a fatal boot error, so an
|
|
612
|
+
* exception here would take the whole application down with it. The failure
|
|
613
|
+
* is still surfaced — through the shell's logger — but the fiber stays
|
|
614
|
+
* active and the workspace+ features keep working.
|
|
615
|
+
*
|
|
616
|
+
* @param ctx - the plugin's client context.
|
|
617
|
+
* @param subject - what was being registered, named in the message.
|
|
618
|
+
* @param setup - the registration body.
|
|
619
|
+
* @returns nothing.
|
|
620
|
+
*/
|
|
621
|
+
function registerNotice(ctx, subject, setup) {
|
|
622
|
+
try {
|
|
623
|
+
setup()
|
|
624
|
+
} catch (error) {
|
|
625
|
+
const message = String((error && error.message) || error)
|
|
626
|
+
try {
|
|
627
|
+
ctx.logger?.warn?.(`[dsh-workspace-plus/notice] ${subject}注册失败,已跳过:${message}`)
|
|
628
|
+
} catch {
|
|
629
|
+
// A logger that cannot log must not turn a skipped registration back
|
|
630
|
+
// into the boot failure this boundary exists to prevent.
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
116
635
|
/** Identity of the injected stylesheet, used to keep exactly one copy. */
|
|
117
636
|
const STYLE_ID = 'dsh-workspace-plus/panel.css'
|
|
118
637
|
|
|
@@ -128,9 +647,19 @@ window.__ModuleLoader__.load({
|
|
|
128
647
|
/** Class of the button this half appends to a workspace row. */
|
|
129
648
|
const ROW_BUTTON_CLASS = 'dsh-workspace-plus-rowbtn'
|
|
130
649
|
|
|
650
|
+
/**
|
|
651
|
+
* Class marking that same button on a workspace the settings page switched
|
|
652
|
+
* off. The button stays — the dialog is the only place a workspace's labels
|
|
653
|
+
* can be read — but it draws dimmed and says why in its tooltip.
|
|
654
|
+
*/
|
|
655
|
+
const ROW_BUTTON_OFF_CLASS = 'dsh-workspace-plus-rowbtn-off'
|
|
656
|
+
|
|
131
657
|
/** Label of the injected row button. */
|
|
132
658
|
const ROW_BUTTON_LABEL = '工作区目录设置'
|
|
133
659
|
|
|
660
|
+
/** Label of that button where workspace+ is switched off for the workspace. */
|
|
661
|
+
const ROW_BUTTON_LABEL_OFF = '工作区目录设置(这个工作区已在 workspace+ 里停用)'
|
|
662
|
+
|
|
134
663
|
/**
|
|
135
664
|
* The shell's own folder-open artwork, copied verbatim from
|
|
136
665
|
* `@deepseek-ai/dsh-client-ui-primitives` (`IconFolderOpenArtwork`). It is
|
|
@@ -172,6 +701,25 @@ window.__ModuleLoader__.load({
|
|
|
172
701
|
'M6.80132 2.14853C7.70663 1.80917 8.70422 1.80919 9.60952 2.14859L14.1296 3.84317V7.11961C14.1296 11.6089 ' +
|
|
173
702
|
'10.7615 13.5975 8.20543 14.5779C5.64931 13.5975 2.28052 11.6089 2.28052 7.11961V3.84317L6.80132 2.14853Z'
|
|
174
703
|
|
|
704
|
+
/**
|
|
705
|
+
* Two more official glyphs, same provenance as the folder artwork above and
|
|
706
|
+
* for the same reason: the dialog's row actions and its add row should read as
|
|
707
|
+
* the shell's own icons without depending on an export name.
|
|
708
|
+
*
|
|
709
|
+
* CLOSE_ARTWORK `IconCloseOutline16` (`ic_ds_close_outline_16`) — the row's
|
|
710
|
+
* remove control, the same cross the shell puts in a dialog's
|
|
711
|
+
* top-right corner.
|
|
712
|
+
* PLUS_ARTWORK `IconPlusOutline16` (`ic_ds_plus_outline_16`) — the add row.
|
|
713
|
+
* It is deliberately not a folder: the row's folder glyphs
|
|
714
|
+
* belong to registered directories, and a second folder would
|
|
715
|
+
* make the add row read as one of them.
|
|
716
|
+
*/
|
|
717
|
+
const CLOSE_ARTWORK = [
|
|
718
|
+
'M14.1168 13.197L13.197 14.1167L1.8833 2.80303L2.80309 1.88324L14.1168 13.197Z',
|
|
719
|
+
'M13.197 1.88326L14.1168 2.80305L2.80309 14.1168L1.8833 13.197L13.197 1.88326Z',
|
|
720
|
+
]
|
|
721
|
+
const PLUS_ARTWORK = ['M8.64453 1.5V7.34961H14.5V8.65039H8.64453V14.5H7.34473V8.65039H1.5V7.34961H7.34473V1.5H8.64453Z']
|
|
722
|
+
|
|
175
723
|
/**
|
|
176
724
|
* How long the fallback entry waits before deciding that no workspace-row
|
|
177
725
|
* button can exist, so a healthy shell never flashes it.
|
|
@@ -202,36 +750,54 @@ window.__ModuleLoader__.load({
|
|
|
202
750
|
.dsh-workspace-plus-settings-ok{color:var(--dsw-alias-state-success-primary);margin-top:8px;font-size:12px;line-height:18px}
|
|
203
751
|
.dsh-workspace-plus-rowbtn{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;padding:0;border:none;flex:none;cursor:pointer;border-radius:var(--dsw-radius-xs,4px);background:0 0;color:var(--dsw-alias-label-tertiary)}
|
|
204
752
|
.dsh-workspace-plus-rowbtn:hover{color:var(--dsw-alias-label-primary)}
|
|
753
|
+
.dsh-workspace-plus-rowbtn-off{opacity:.55}
|
|
754
|
+
.dsh-workspace-plus-rowbtn-off:hover{opacity:1;color:var(--dsw-alias-label-primary)}
|
|
205
755
|
.dsh-workspace-plus-rowbtn:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary));outline-offset:-2px}
|
|
206
|
-
.dsh-workspace-plus-dialog{box-sizing:border-box;display:flex;flex-direction:column;gap:
|
|
207
|
-
.dsh-workspace-plus-dialog-card.dsh-workspace-plus-dialog-card{width:min(
|
|
208
|
-
.dsh-workspace-plus-
|
|
209
|
-
.dsh-workspace-plus-
|
|
210
|
-
.dsh-workspace-plus-
|
|
211
|
-
.dsh-workspace-plus-
|
|
212
|
-
.dsh-workspace-plus-
|
|
213
|
-
.dsh-workspace-plus-
|
|
214
|
-
.dsh-workspace-plus-
|
|
215
|
-
.dsh-workspace-plus-
|
|
216
|
-
.dsh-workspace-plus-
|
|
217
|
-
.dsh-workspace-plus-path:
|
|
218
|
-
.dsh-workspace-plus-
|
|
219
|
-
.dsh-workspace-plus-path:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary));outline-offset:1px}
|
|
220
|
-
.dsh-workspace-plus-path-glyph{display:inline-flex;flex:none;align-items:center;color:var(--dsw-alias-label-secondary)}
|
|
221
|
-
.dsh-workspace-plus-path-empty{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary)}
|
|
222
|
-
.dsh-workspace-plus-list{box-sizing:border-box;width:100%;max-height:34vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-md,8px)}
|
|
223
|
-
.dsh-workspace-plus-row{display:flex;align-items:center;gap:12px;padding:9px 12px;background:var(--dsw-alias-bg-layer-1);border-top:1px solid var(--dsw-alias-border-l1)}
|
|
224
|
-
.dsh-workspace-plus-row:first-child{border-top:none}
|
|
225
|
-
.dsh-workspace-plus-row-tag{flex:none;min-width:76px;padding:1px 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-brand-primary);font-weight:600;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
226
|
-
.dsh-workspace-plus-row-path{min-width:0;flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,monospace)}
|
|
227
|
-
.dsh-workspace-plus-row-note{flex:0 1 auto;color:var(--dsw-alias-label-secondary);max-width:28ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
756
|
+
.dsh-workspace-plus-dialog{box-sizing:border-box;display:flex;flex-direction:column;gap:16px;color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px}
|
|
757
|
+
.dsh-workspace-plus-dialog-card.dsh-workspace-plus-dialog-card{width:min(640px,100%)}
|
|
758
|
+
.dsh-workspace-plus-field{box-sizing:border-box;display:flex;align-items:center;gap:10px;min-height:44px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-l1));border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1)}
|
|
759
|
+
.dsh-workspace-plus-field-glyph{display:inline-flex;flex:none;align-items:center;color:var(--dsw-alias-label-tertiary)}
|
|
760
|
+
.dsh-workspace-plus-field-title{flex:none;min-width:0;max-width:40%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}
|
|
761
|
+
.dsh-workspace-plus-field-root{min-width:0;flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right;font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:13px;color:var(--dsw-alias-label-tertiary)}
|
|
762
|
+
.dsh-workspace-plus-section{margin:0 2px -6px;color:var(--dsw-alias-label-secondary)}
|
|
763
|
+
.dsh-workspace-plus-list{box-sizing:border-box;width:100%;max-height:34vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-l1));border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1)}
|
|
764
|
+
.dsh-workspace-plus-row,.dsh-workspace-plus-field-row{box-sizing:border-box;display:flex;align-items:center;gap:10px;min-height:44px;padding:0 12px;border-top:1px solid var(--dsw-alias-border-l1)}
|
|
765
|
+
.dsh-workspace-plus-row:first-child,.dsh-workspace-plus-field-row:first-child{border-top:none}
|
|
766
|
+
.dsh-workspace-plus-row-tag{flex:none;max-width:38%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}
|
|
767
|
+
.dsh-workspace-plus-row-path{min-width:0;flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:13px;color:var(--dsw-alias-label-tertiary)}
|
|
768
|
+
.dsh-workspace-plus-row-note{flex:0 1 auto;max-width:16ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;color:var(--dsw-alias-label-secondary)}
|
|
228
769
|
.dsh-workspace-plus-row-outside{flex:none;display:inline-flex;align-items:center;color:var(--dsw-alias-state-warn-primary)}
|
|
229
770
|
.dsh-workspace-plus-row-outside:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary));outline-offset:1px;border-radius:var(--dsw-radius-xs,4px)}
|
|
230
|
-
.dsh-workspace-plus-
|
|
231
|
-
.dsh-workspace-plus-
|
|
771
|
+
.dsh-workspace-plus-remove{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:none;border-radius:var(--dsw-radius-sm,8px);background:0 0;color:var(--dsw-alias-label-tertiary);cursor:pointer}
|
|
772
|
+
.dsh-workspace-plus-remove:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
773
|
+
.dsh-workspace-plus-remove:disabled{cursor:not-allowed;opacity:.4}
|
|
774
|
+
.dsh-workspace-plus-remove:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary));outline-offset:-2px}
|
|
775
|
+
.dsh-workspace-plus-add{width:100%;font:inherit;text-align:left;color:var(--dsw-alias-label-secondary);background:0 0;border-right:none;border-bottom:none;border-left:none;cursor:pointer}
|
|
776
|
+
.dsh-workspace-plus-add:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
777
|
+
.dsh-workspace-plus-add:disabled{cursor:not-allowed;color:var(--dsw-alias-label-tertiary)}
|
|
778
|
+
.dsh-workspace-plus-add:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary));outline-offset:-2px}
|
|
779
|
+
.dsh-workspace-plus-add-text{min-width:0;flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
780
|
+
.dsh-workspace-plus-add-path{font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:13px}
|
|
781
|
+
.dsh-workspace-plus-select{min-width:0;flex:1 1 auto;font:inherit;color:var(--dsw-alias-label-primary);background:0 0;border:none;padding:0}
|
|
782
|
+
/* The light theme resolves bg-layer-1/2/3 to the same white, so a borderless
|
|
783
|
+
input filled with a layer token disappears inside its group there. The
|
|
784
|
+
interactive fills are the one neutral surface pair that reads in both themes:
|
|
785
|
+
6%/10% of the label colour in light, 8%/14% of it in dark. */
|
|
786
|
+
.dsh-workspace-plus-input{box-sizing:border-box;min-width:0;height:34px;padding:0 10px;font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);border:none;border-radius:var(--dsw-radius-sm,8px)}
|
|
787
|
+
.dsh-workspace-plus-input:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}
|
|
788
|
+
.dsh-workspace-plus-input:disabled{background:0 0;color:var(--dsw-alias-label-tertiary)}
|
|
789
|
+
.dsh-workspace-plus-select:focus-visible,.dsh-workspace-plus-input:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary));outline-offset:-1px}
|
|
790
|
+
.dsh-workspace-plus-input-label{flex:0 1 180px}
|
|
791
|
+
.dsh-workspace-plus-input-note{flex:1 1 auto}
|
|
232
792
|
.dsh-workspace-plus-submit{flex:none}
|
|
233
|
-
.dsh-workspace-plus-empty{
|
|
234
|
-
.dsh-workspace-plus-
|
|
793
|
+
.dsh-workspace-plus-empty{color:var(--dsw-alias-label-secondary)}
|
|
794
|
+
.dsh-workspace-plus-notice{box-sizing:border-box;display:flex;flex-direction:column;gap:4px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-l1));border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1)}
|
|
795
|
+
.dsh-workspace-plus-notice-error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,var(--dsw-alias-bg-layer-1))}
|
|
796
|
+
.dsh-workspace-plus-notice-warn{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 10%,var(--dsw-alias-bg-layer-1))}
|
|
797
|
+
.dsh-workspace-plus-status{font-size:14px;line-height:22px}
|
|
798
|
+
.dsh-workspace-plus-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%}
|
|
799
|
+
.dsh-workspace-plus-note{color:var(--dsw-alias-label-secondary)}
|
|
800
|
+
.dsh-workspace-plus-off{color:var(--dsw-alias-state-warn-primary)}
|
|
235
801
|
.dsh-workspace-plus-error{color:var(--dsw-alias-state-error-primary)}
|
|
236
802
|
.dsh-workspace-plus-ok{color:var(--dsw-alias-state-success-primary)}
|
|
237
803
|
.dsh-workspace-plus-mono{font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,monospace);word-break:break-all}
|
|
@@ -263,20 +829,25 @@ window.__ModuleLoader__.load({
|
|
|
263
829
|
|
|
264
830
|
ensureStyles()
|
|
265
831
|
|
|
832
|
+
// The notice page's stylesheet follows the same one-tag rule; calling it in
|
|
833
|
+
// the factory body (like the panel sheet above) means the 通知 page needs no
|
|
834
|
+
// `<style>` element of its own and reloads stay duplicate-free.
|
|
835
|
+
ensureNoticeStyles()
|
|
836
|
+
|
|
266
837
|
// -----------------------------------------------------------------------
|
|
267
838
|
// The workspace-directory feature
|
|
268
839
|
// -----------------------------------------------------------------------
|
|
269
840
|
|
|
270
841
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
842
|
+
* One glyph built from inlined official artwork. Entries are either a bare
|
|
843
|
+
* path string or `{ d, opacity }`, so the folder's translucent underlay and
|
|
844
|
+
* the flat single-path icons share one renderer.
|
|
274
845
|
*
|
|
275
|
-
* @param props -
|
|
276
|
-
* glyph is itself a control.
|
|
846
|
+
* @param props - the artwork to draw, its size, an optional class, and the
|
|
847
|
+
* click handler when the glyph is itself a control.
|
|
277
848
|
* @returns the icon element.
|
|
278
849
|
*/
|
|
279
|
-
function
|
|
850
|
+
function ArtworkGlyph({ artwork, size = 16, className = 'dsh-workspace-plus-glyph', onClick }) {
|
|
280
851
|
return React.createElement(
|
|
281
852
|
'svg',
|
|
282
853
|
{
|
|
@@ -290,17 +861,42 @@ window.__ModuleLoader__.load({
|
|
|
290
861
|
focusable: 'false',
|
|
291
862
|
...(onClick === undefined ? {} : { onClick }),
|
|
292
863
|
},
|
|
293
|
-
|
|
294
|
-
|
|
864
|
+
artwork.map((entry, index) => {
|
|
865
|
+
const path = typeof entry === 'string' ? { d: entry } : entry
|
|
866
|
+
return React.createElement('path', {
|
|
295
867
|
key: index,
|
|
296
868
|
d: path.d,
|
|
297
869
|
fill: 'currentColor',
|
|
298
870
|
...(path.opacity === undefined ? {} : { opacity: path.opacity }),
|
|
299
|
-
})
|
|
300
|
-
),
|
|
871
|
+
})
|
|
872
|
+
}),
|
|
301
873
|
)
|
|
302
874
|
}
|
|
303
875
|
|
|
876
|
+
/**
|
|
877
|
+
* The folder-open artwork as an element, so the fallback sidebar entry, the
|
|
878
|
+
* hint panel, and the dialog's directory rows all draw the same icon the
|
|
879
|
+
* shell uses on its workspace row.
|
|
880
|
+
*
|
|
881
|
+
* @param props - optional size, free-form class, and the click handler when
|
|
882
|
+
* the glyph is itself a control.
|
|
883
|
+
* @returns the icon element.
|
|
884
|
+
*/
|
|
885
|
+
function FolderGlyph({ size = 16, className, onClick }) {
|
|
886
|
+
return React.createElement(ArtworkGlyph, { artwork: FOLDER_ARTWORK, size, className, onClick })
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** The row's remove control: the shell's cross. */
|
|
890
|
+
function CloseGlyph({ size = 16 }) {
|
|
891
|
+
return React.createElement(ArtworkGlyph, { artwork: CLOSE_ARTWORK, size })
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** The add row's leading glyph: the shell's plus. */
|
|
895
|
+
function PlusGlyph({ size = 16 }) {
|
|
896
|
+
return React.createElement(ArtworkGlyph, { artwork: PLUS_ARTWORK, size })
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
|
|
304
900
|
/**
|
|
305
901
|
* The directory glyph, shared by the fallback sidebar entry and the hint
|
|
306
902
|
* panel. It draws the same official artwork as the injected row button, so
|
|
@@ -511,12 +1107,18 @@ window.__ModuleLoader__.load({
|
|
|
511
1107
|
}
|
|
512
1108
|
|
|
513
1109
|
/**
|
|
514
|
-
* Whether
|
|
515
|
-
*
|
|
516
|
-
* the
|
|
1110
|
+
* Whether workspace+ runs for one workspace id.
|
|
1111
|
+
*
|
|
1112
|
+
* This is the host's own switch (`settings.workspaceEnabled`, with the
|
|
1113
|
+
* default Workspace falling back to `defaultWorkspaceSetting`); the host
|
|
1114
|
+
* gates prompt-context injection and the `workspace_dirs` tool on it. On the
|
|
1115
|
+
* browser side it only chooses how the row button is presented — normal or
|
|
1116
|
+
* switched-off — because the button itself is present everywhere. A
|
|
1117
|
+
* workspace the snapshot does not know yet counts as enabled: guessing
|
|
1118
|
+
* "off" would mislabel a perfectly ordinary row.
|
|
517
1119
|
*
|
|
518
1120
|
* @param workspaceId - the id taken from the row's `data-row-key`.
|
|
519
|
-
* @returns true when
|
|
1121
|
+
* @returns true when workspace+ is on for that workspace.
|
|
520
1122
|
*/
|
|
521
1123
|
function isEnabledWorkspace(workspaceId) {
|
|
522
1124
|
const workspace = hostState.workspaces.find((candidate) => candidate.id === workspaceId)
|
|
@@ -548,6 +1150,23 @@ window.__ModuleLoader__.load({
|
|
|
548
1150
|
*/
|
|
549
1151
|
const SOURCE_NAME = 'workspace-plus'
|
|
550
1152
|
|
|
1153
|
+
/**
|
|
1154
|
+
* Where this group sits in the `@` menu.
|
|
1155
|
+
*
|
|
1156
|
+
* The pipeline sorts the roster for one trigger by `InputTriggerSource.order`
|
|
1157
|
+
* — lower first, `?? 0` when absent, and `Array.prototype.sort` is stable, so
|
|
1158
|
+
* every source that omits the field ties at 0 and keeps registration order.
|
|
1159
|
+
* That is exactly why this plugin used to land last: it is materialized after
|
|
1160
|
+
* the shell's own plugins, and the product's `@` groups are `reference` (0)
|
|
1161
|
+
* and `subagent` (0), with `cordis` at 1. Declaring a value below zero is the
|
|
1162
|
+
* only way an add-on can precede them, and `-10` leaves the product room for a
|
|
1163
|
+
* group or two ahead of 0 without this one having to be re-tuned.
|
|
1164
|
+
*
|
|
1165
|
+
* The labels are the ones this workspace is actually navigated by, so they
|
|
1166
|
+
* belong at the top of the menu rather than under the file tree.
|
|
1167
|
+
*/
|
|
1168
|
+
const SOURCE_ORDER = -10
|
|
1169
|
+
|
|
551
1170
|
/** Section heading of the group's rows, standing in for the pipeline's group title. */
|
|
552
1171
|
const SOURCE_SECTION = '工作区目录'
|
|
553
1172
|
|
|
@@ -642,6 +1261,8 @@ window.__ModuleLoader__.load({
|
|
|
642
1261
|
return {
|
|
643
1262
|
trigger: '@',
|
|
644
1263
|
name: SOURCE_NAME,
|
|
1264
|
+
// Sorting key of the `@` roster; see SOURCE_ORDER for why it is negative.
|
|
1265
|
+
order: SOURCE_ORDER,
|
|
645
1266
|
// The pipeline's group title would be an untranslated key; each row
|
|
646
1267
|
// carries its own section heading instead.
|
|
647
1268
|
showGroupTitle: false,
|
|
@@ -875,7 +1496,13 @@ window.__ModuleLoader__.load({
|
|
|
875
1496
|
}
|
|
876
1497
|
|
|
877
1498
|
/**
|
|
878
|
-
* One registered directory row
|
|
1499
|
+
* One registered directory row: glyph, label, absolute path, optional note,
|
|
1500
|
+
* the outside-the-root badge, and the remove control.
|
|
1501
|
+
*
|
|
1502
|
+
* The remove control is the shell's own cross rather than a labelled button:
|
|
1503
|
+
* every row carries one, and a word repeated down the list is what made the
|
|
1504
|
+
* earlier table read as a form instead of a list. Its accessible name still
|
|
1505
|
+
* says exactly what it removes.
|
|
879
1506
|
*
|
|
880
1507
|
* @param props - the row's directory, its workspace root, and the remove handler.
|
|
881
1508
|
* @returns the row element.
|
|
@@ -885,14 +1512,28 @@ window.__ModuleLoader__.load({
|
|
|
885
1512
|
return React.createElement(
|
|
886
1513
|
'div',
|
|
887
1514
|
{ className: 'dsh-workspace-plus-row' },
|
|
888
|
-
React.createElement(
|
|
1515
|
+
React.createElement(
|
|
1516
|
+
'span',
|
|
1517
|
+
{ className: 'dsh-workspace-plus-field-glyph' },
|
|
1518
|
+
React.createElement(FolderGlyph, { size: 16 }),
|
|
1519
|
+
),
|
|
1520
|
+
React.createElement('span', { className: 'dsh-workspace-plus-row-tag', title: dir.label }, dir.label),
|
|
889
1521
|
React.createElement('span', { className: 'dsh-workspace-plus-row-path', title: dir.path }, dir.path),
|
|
890
|
-
dir.note === undefined
|
|
1522
|
+
dir.note === undefined
|
|
1523
|
+
? null
|
|
1524
|
+
: React.createElement('span', { className: 'dsh-workspace-plus-row-note', title: dir.note }, dir.note),
|
|
891
1525
|
outside ? React.createElement(OutsideBadge) : null,
|
|
892
1526
|
React.createElement(
|
|
893
|
-
|
|
894
|
-
{
|
|
895
|
-
|
|
1527
|
+
'button',
|
|
1528
|
+
{
|
|
1529
|
+
type: 'button',
|
|
1530
|
+
className: 'dsh-workspace-plus-remove',
|
|
1531
|
+
disabled,
|
|
1532
|
+
'aria-label': '移除 ' + dir.label,
|
|
1533
|
+
title: '移除 ' + dir.label,
|
|
1534
|
+
onClick: () => onRemove(dir.label),
|
|
1535
|
+
},
|
|
1536
|
+
React.createElement(CloseGlyph, { size: 16 }),
|
|
896
1537
|
),
|
|
897
1538
|
)
|
|
898
1539
|
}
|
|
@@ -1053,44 +1694,181 @@ window.__ModuleLoader__.load({
|
|
|
1053
1694
|
}
|
|
1054
1695
|
}
|
|
1055
1696
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1697
|
+
/**
|
|
1698
|
+
* The dialog's blocks, in reading order. Every one of them is a bordered
|
|
1699
|
+
* surface with the row grid inside, so the dialog reads as one column of
|
|
1700
|
+
* like things rather than a toolbar, a table and a form stacked on top of
|
|
1701
|
+
* each other.
|
|
1702
|
+
*/
|
|
1703
|
+
const identity =
|
|
1704
|
+
selected === null
|
|
1705
|
+
? null
|
|
1062
1706
|
: React.createElement(
|
|
1063
|
-
'
|
|
1064
|
-
{
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1707
|
+
'div',
|
|
1708
|
+
{ className: 'dsh-workspace-plus-field' },
|
|
1709
|
+
React.createElement(
|
|
1710
|
+
'span',
|
|
1711
|
+
{ className: 'dsh-workspace-plus-field-glyph' },
|
|
1712
|
+
React.createElement(FolderGlyph, { size: 16 }),
|
|
1713
|
+
),
|
|
1714
|
+
state.locked
|
|
1715
|
+
? React.createElement(
|
|
1716
|
+
'span',
|
|
1717
|
+
{ className: 'dsh-workspace-plus-field-title', title: selected.title || selected.path },
|
|
1718
|
+
selected.title || selected.path,
|
|
1719
|
+
)
|
|
1720
|
+
: React.createElement(
|
|
1721
|
+
'select',
|
|
1722
|
+
{
|
|
1723
|
+
className: 'dsh-workspace-plus-select',
|
|
1724
|
+
value: state.selected ?? '',
|
|
1725
|
+
disabled: state.workspaces.length === 0,
|
|
1726
|
+
'aria-label': '选择工作区',
|
|
1727
|
+
onChange: (event) => patch({ selected: event.target.value === '' ? null : event.target.value }),
|
|
1728
|
+
},
|
|
1729
|
+
state.workspaces.length === 0
|
|
1730
|
+
? React.createElement('option', { value: '' }, '(没有可用工作区)')
|
|
1731
|
+
: state.workspaces.map((workspace) =>
|
|
1732
|
+
React.createElement(
|
|
1733
|
+
'option',
|
|
1734
|
+
{ key: workspace.id, value: workspace.path },
|
|
1735
|
+
(workspace.title || workspace.path) + ' — ' + workspace.path,
|
|
1736
|
+
),
|
|
1737
|
+
),
|
|
1079
1738
|
),
|
|
1739
|
+
// The trigger names the workspace, so the locked row also says which
|
|
1740
|
+
// root its labels resolve against.
|
|
1741
|
+
state.locked
|
|
1742
|
+
? React.createElement(
|
|
1743
|
+
'span',
|
|
1744
|
+
{ className: 'dsh-workspace-plus-field-root', title: selected.path },
|
|
1745
|
+
selected.path,
|
|
1746
|
+
)
|
|
1747
|
+
: null,
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
const sectionLabel = React.createElement('div', { className: 'dsh-workspace-plus-section' }, '目录')
|
|
1751
|
+
|
|
1752
|
+
const listBlock = (...children) =>
|
|
1753
|
+
React.createElement('div', { className: 'dsh-workspace-plus-list' }, ...children)
|
|
1754
|
+
|
|
1755
|
+
const emptyRow = (text) =>
|
|
1756
|
+
React.createElement('div', { className: 'dsh-workspace-plus-field-row dsh-workspace-plus-empty' }, text)
|
|
1757
|
+
|
|
1758
|
+
const noticeBlock = (variant, ...children) =>
|
|
1759
|
+
React.createElement(
|
|
1760
|
+
'div',
|
|
1761
|
+
{
|
|
1762
|
+
className:
|
|
1763
|
+
'dsh-workspace-plus-notice' +
|
|
1764
|
+
(variant === '' || variant === undefined ? '' : ' dsh-workspace-plus-notice-' + variant),
|
|
1765
|
+
},
|
|
1766
|
+
...children,
|
|
1767
|
+
)
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* The add group. Its row IS the picker — there is no path to type into — and
|
|
1771
|
+
* the label/note row appears only once a directory has been chosen, so an
|
|
1772
|
+
* idle dialog is a list plus one row, not a form with two empty fields.
|
|
1773
|
+
*/
|
|
1774
|
+
const addGroup = React.createElement(
|
|
1775
|
+
'div',
|
|
1776
|
+
{ className: 'dsh-workspace-plus-list' },
|
|
1777
|
+
React.createElement(
|
|
1778
|
+
'button',
|
|
1779
|
+
{
|
|
1780
|
+
type: 'button',
|
|
1781
|
+
className: 'dsh-workspace-plus-field-row dsh-workspace-plus-add',
|
|
1782
|
+
disabled: selected === null || state.busy || picking || !pickerAvailable,
|
|
1783
|
+
'aria-label': '选择目录',
|
|
1784
|
+
title: pickerAvailable
|
|
1785
|
+
? '打开目录选择器'
|
|
1786
|
+
: '当前页面没有挂载 ui-workspace,目录选择器不可用;可在对话里用 workspace_dirs 登记',
|
|
1787
|
+
onClick: () => void browse(),
|
|
1788
|
+
},
|
|
1789
|
+
React.createElement(
|
|
1790
|
+
'span',
|
|
1791
|
+
{ className: 'dsh-workspace-plus-field-glyph' },
|
|
1792
|
+
React.createElement(PlusGlyph, { size: 16 }),
|
|
1793
|
+
),
|
|
1794
|
+
React.createElement(
|
|
1795
|
+
'span',
|
|
1796
|
+
{
|
|
1797
|
+
className:
|
|
1798
|
+
draft.path === ''
|
|
1799
|
+
? 'dsh-workspace-plus-add-text'
|
|
1800
|
+
: 'dsh-workspace-plus-add-text dsh-workspace-plus-add-path',
|
|
1801
|
+
...(draft.path === '' ? {} : { title: draft.path }),
|
|
1802
|
+
},
|
|
1803
|
+
picking ? '选择中…' : draft.path === '' ? '添加目录' : draft.path,
|
|
1804
|
+
),
|
|
1805
|
+
),
|
|
1806
|
+
draft.path === ''
|
|
1807
|
+
? null
|
|
1808
|
+
: React.createElement(
|
|
1809
|
+
'div',
|
|
1810
|
+
{ className: 'dsh-workspace-plus-field-row dsh-workspace-plus-draft' },
|
|
1811
|
+
React.createElement('input', {
|
|
1812
|
+
className: 'dsh-workspace-plus-input dsh-workspace-plus-input-label',
|
|
1813
|
+
placeholder: '标签,如 backend',
|
|
1814
|
+
'aria-label': '标签',
|
|
1815
|
+
value: draft.label,
|
|
1816
|
+
disabled: state.busy,
|
|
1817
|
+
onChange: (event) => setDraft({ ...draft, label: event.target.value }),
|
|
1818
|
+
}),
|
|
1819
|
+
React.createElement('input', {
|
|
1820
|
+
className: 'dsh-workspace-plus-input dsh-workspace-plus-input-note',
|
|
1821
|
+
placeholder: '说明(可选)',
|
|
1822
|
+
'aria-label': '说明',
|
|
1823
|
+
value: draft.note,
|
|
1824
|
+
disabled: state.busy,
|
|
1825
|
+
onChange: (event) => setDraft({ ...draft, note: event.target.value }),
|
|
1826
|
+
}),
|
|
1827
|
+
React.createElement(
|
|
1828
|
+
Button,
|
|
1829
|
+
{
|
|
1830
|
+
variant: 'primary',
|
|
1831
|
+
className: 'dsh-workspace-plus-submit',
|
|
1832
|
+
disabled: state.busy || draft.path === '',
|
|
1833
|
+
onClick: () => void addDirectory(),
|
|
1834
|
+
},
|
|
1835
|
+
'添加',
|
|
1836
|
+
),
|
|
1080
1837
|
),
|
|
1081
|
-
state.locked
|
|
1082
|
-
? React.createElement('span', { className: 'dsh-workspace-plus-note dsh-workspace-plus-mono' }, selected?.path ?? '')
|
|
1083
|
-
: null,
|
|
1084
1838
|
)
|
|
1085
1839
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1840
|
+
// The switch that turned this workspace off is on the settings page, not
|
|
1841
|
+
// here, so the state is stated where the user is already looking.
|
|
1842
|
+
const disabledNotice =
|
|
1843
|
+
selected === null || selected.enabled !== false
|
|
1844
|
+
? null
|
|
1845
|
+
: noticeBlock(
|
|
1846
|
+
'warn',
|
|
1847
|
+
React.createElement(
|
|
1848
|
+
'div',
|
|
1849
|
+
{ className: 'dsh-workspace-plus-off' },
|
|
1850
|
+
'这个工作区已在 workspace+ 里停用:标签不会注入对话,workspace_dirs 工具也会拒绝操作。' +
|
|
1851
|
+
'到 设置 → workspace+ 打开它的开关即可恢复。',
|
|
1852
|
+
),
|
|
1853
|
+
)
|
|
1854
|
+
|
|
1855
|
+
// The host's own sentence about what it just did — the only status copy the
|
|
1856
|
+
// dialog shows, and only while it is true. A failed read is not repeated
|
|
1857
|
+
// here: its notice above already says it, at length.
|
|
1858
|
+
const statusLine =
|
|
1859
|
+
state.status === 'error' || state.message === ''
|
|
1860
|
+
? null
|
|
1861
|
+
: React.createElement(
|
|
1862
|
+
'div',
|
|
1863
|
+
{
|
|
1864
|
+
className:
|
|
1865
|
+
'dsh-workspace-plus-status ' +
|
|
1866
|
+
(state.ok === false ? 'dsh-workspace-plus-error' : 'dsh-workspace-plus-ok'),
|
|
1867
|
+
},
|
|
1868
|
+
state.message,
|
|
1869
|
+
)
|
|
1870
|
+
|
|
1871
|
+
const bodyBlocks = (() => {
|
|
1094
1872
|
if (state.status === 'error') {
|
|
1095
1873
|
const unauthorized = state.message.includes('401')
|
|
1096
1874
|
const unrouted = state.message.includes('404')
|
|
@@ -1113,12 +1891,9 @@ window.__ModuleLoader__.load({
|
|
|
1113
1891
|
: '这个面板通过 ' +
|
|
1114
1892
|
API_PATH +
|
|
1115
1893
|
' 与宿主进程通信,而当前页面没有可用的 HTTP 通道(例如页面不是 dsh 提供的、或宿主进程还没起来)。'
|
|
1116
|
-
return
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
React.createElement(
|
|
1120
|
-
'div',
|
|
1121
|
-
{ className: 'dsh-workspace-plus-empty' },
|
|
1894
|
+
return [
|
|
1895
|
+
noticeBlock(
|
|
1896
|
+
'error',
|
|
1122
1897
|
React.createElement('div', { className: 'dsh-workspace-plus-error' }, '无法连接 host 接口:' + state.message),
|
|
1123
1898
|
React.createElement('div', { className: 'dsh-workspace-plus-note' }, diagnosis),
|
|
1124
1899
|
React.createElement(
|
|
@@ -1127,128 +1902,53 @@ window.__ModuleLoader__.load({
|
|
|
1127
1902
|
'宿主半加载之后,映射也可以在对话里用 workspace_dirs 工具管理。',
|
|
1128
1903
|
),
|
|
1129
1904
|
),
|
|
1130
|
-
|
|
1905
|
+
]
|
|
1131
1906
|
}
|
|
1132
1907
|
if (!state.registryAvailable) {
|
|
1133
|
-
return
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1908
|
+
return [
|
|
1909
|
+
noticeBlock(
|
|
1910
|
+
'',
|
|
1911
|
+
React.createElement(
|
|
1912
|
+
'div',
|
|
1913
|
+
{ className: 'dsh-workspace-plus-note' },
|
|
1914
|
+
'宿主进程没有加载 workspace 注册表,无法列出工作区。请在对话里用 workspace_dirs 工具按绝对路径管理标签。',
|
|
1915
|
+
),
|
|
1140
1916
|
),
|
|
1141
|
-
|
|
1917
|
+
]
|
|
1142
1918
|
}
|
|
1143
|
-
if (
|
|
1144
|
-
return
|
|
1145
|
-
'div',
|
|
1146
|
-
{ className: 'dsh-workspace-plus-list' },
|
|
1147
|
-
React.createElement('div', { className: 'dsh-workspace-plus-empty' }, '还没有任何工作区。先在左侧边栏新建一个工作区,再回到这里登记目录。'),
|
|
1148
|
-
)
|
|
1919
|
+
if (state.status === 'loading') {
|
|
1920
|
+
return [sectionLabel, listBlock(emptyRow('正在读取…'))]
|
|
1149
1921
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
dir,
|
|
1159
|
-
workspacePath: selected.path,
|
|
1160
|
-
disabled: state.busy,
|
|
1161
|
-
onRemove: (label) => void removeDirectory(label),
|
|
1162
|
-
}),
|
|
1922
|
+
if (selected === null) {
|
|
1923
|
+
return [
|
|
1924
|
+
noticeBlock(
|
|
1925
|
+
'',
|
|
1926
|
+
React.createElement(
|
|
1927
|
+
'div',
|
|
1928
|
+
{ className: 'dsh-workspace-plus-note' },
|
|
1929
|
+
'还没有任何工作区。先在左侧边栏新建一个工作区,再回到这里登记目录。',
|
|
1163
1930
|
),
|
|
1931
|
+
),
|
|
1932
|
+
]
|
|
1933
|
+
}
|
|
1934
|
+
const rows = dirs.map((dir) =>
|
|
1935
|
+
React.createElement(DirectoryRow, {
|
|
1936
|
+
key: dir.label,
|
|
1937
|
+
dir,
|
|
1938
|
+
workspacePath: selected.path,
|
|
1939
|
+
disabled: state.busy,
|
|
1940
|
+
onRemove: (label) => void removeDirectory(label),
|
|
1941
|
+
}),
|
|
1164
1942
|
)
|
|
1943
|
+
return [
|
|
1944
|
+
sectionLabel,
|
|
1945
|
+
listBlock(...(rows.length === 0 ? [emptyRow('还没有登记目录')] : rows)),
|
|
1946
|
+
addGroup,
|
|
1947
|
+
]
|
|
1165
1948
|
})()
|
|
1166
1949
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
* the mapping, only the shell's directory picker. Clicking the field IS the
|
|
1170
|
-
* picker, the chosen directory is shown in it, and the label row below
|
|
1171
|
-
* collects the name that the conversation will use.
|
|
1172
|
-
*/
|
|
1173
|
-
const pathField = React.createElement(
|
|
1174
|
-
'button',
|
|
1175
|
-
{
|
|
1176
|
-
type: 'button',
|
|
1177
|
-
className: 'dsh-workspace-plus-path',
|
|
1178
|
-
disabled: selected === null || state.busy || picking || !pickerAvailable,
|
|
1179
|
-
'aria-label': '选择目录',
|
|
1180
|
-
title: pickerAvailable
|
|
1181
|
-
? '打开目录选择器'
|
|
1182
|
-
: '当前页面没有挂载 ui-workspace,目录选择器不可用;可在对话里用 workspace_dirs 登记',
|
|
1183
|
-
onClick: () => void browse(),
|
|
1184
|
-
},
|
|
1185
|
-
React.createElement(
|
|
1186
|
-
'span',
|
|
1187
|
-
{ className: 'dsh-workspace-plus-path-glyph' },
|
|
1188
|
-
React.createElement(FolderGlyph, { size: 16 }),
|
|
1189
|
-
),
|
|
1190
|
-
React.createElement(
|
|
1191
|
-
'span',
|
|
1192
|
-
{ className: draft.path === '' ? 'dsh-workspace-plus-path-empty' : 'dsh-workspace-plus-row-path' },
|
|
1193
|
-
picking ? '选择中…' : draft.path === '' ? '选择目录…' : draft.path,
|
|
1194
|
-
),
|
|
1195
|
-
)
|
|
1196
|
-
|
|
1197
|
-
const form = React.createElement(
|
|
1198
|
-
'div',
|
|
1199
|
-
{ className: 'dsh-workspace-plus-form' },
|
|
1200
|
-
pathField,
|
|
1201
|
-
React.createElement(
|
|
1202
|
-
'div',
|
|
1203
|
-
{ className: 'dsh-workspace-plus-form-row' },
|
|
1204
|
-
React.createElement('input', {
|
|
1205
|
-
className: 'dsh-workspace-plus-input dsh-workspace-plus-input-label',
|
|
1206
|
-
placeholder: '标签,如 backend',
|
|
1207
|
-
'aria-label': '标签',
|
|
1208
|
-
value: draft.label,
|
|
1209
|
-
disabled: selected === null || state.busy,
|
|
1210
|
-
onChange: (event) => setDraft({ ...draft, label: event.target.value }),
|
|
1211
|
-
}),
|
|
1212
|
-
React.createElement('input', {
|
|
1213
|
-
className: 'dsh-workspace-plus-input dsh-workspace-plus-input-note',
|
|
1214
|
-
placeholder: '说明(可选)',
|
|
1215
|
-
'aria-label': '说明',
|
|
1216
|
-
value: draft.note,
|
|
1217
|
-
disabled: selected === null || state.busy,
|
|
1218
|
-
onChange: (event) => setDraft({ ...draft, note: event.target.value }),
|
|
1219
|
-
}),
|
|
1220
|
-
React.createElement(
|
|
1221
|
-
Button,
|
|
1222
|
-
{
|
|
1223
|
-
variant: 'primary',
|
|
1224
|
-
className: 'dsh-workspace-plus-submit',
|
|
1225
|
-
disabled: selected === null || state.busy || draft.path === '',
|
|
1226
|
-
onClick: () => void addDirectory(),
|
|
1227
|
-
},
|
|
1228
|
-
'添加',
|
|
1229
|
-
),
|
|
1230
|
-
),
|
|
1231
|
-
)
|
|
1232
|
-
|
|
1233
|
-
const status = React.createElement(
|
|
1234
|
-
'div',
|
|
1235
|
-
{ className: 'dsh-workspace-plus-toolbar' },
|
|
1236
|
-
state.message === ''
|
|
1237
|
-
? null
|
|
1238
|
-
: React.createElement(
|
|
1239
|
-
'span',
|
|
1240
|
-
{ className: state.ok === false ? 'dsh-workspace-plus-error' : 'dsh-workspace-plus-ok' },
|
|
1241
|
-
state.message,
|
|
1242
|
-
),
|
|
1243
|
-
// The locked header already names the root; only the unlocked picker needs it repeated.
|
|
1244
|
-
state.locked || selected === null
|
|
1245
|
-
? null
|
|
1246
|
-
: React.createElement(
|
|
1247
|
-
'span',
|
|
1248
|
-
{ className: 'dsh-workspace-plus-note' },
|
|
1249
|
-
'根目录:',
|
|
1250
|
-
React.createElement('span', { className: 'dsh-workspace-plus-mono' }, selected.path),
|
|
1251
|
-
),
|
|
1950
|
+
const blocks = [identity, ...bodyBlocks, disabledNotice, statusLine].filter(
|
|
1951
|
+
(block) => block !== null && block !== undefined,
|
|
1252
1952
|
)
|
|
1253
1953
|
|
|
1254
1954
|
return React.createElement(
|
|
@@ -1259,21 +1959,22 @@ window.__ModuleLoader__.load({
|
|
|
1259
1959
|
title: '工作区目录设置',
|
|
1260
1960
|
closeLabel: '关闭',
|
|
1261
1961
|
// The card is wider than the shell's 380px default: a row carries a
|
|
1262
|
-
// label, an absolute path, a note and a badge
|
|
1263
|
-
// two fields plus its action on one line.
|
|
1962
|
+
// label, an absolute path, a note and a badge.
|
|
1264
1963
|
className: 'dsh-workspace-plus-dialog-card',
|
|
1964
|
+
// Secondary action left, primary right — the same split the shell's own
|
|
1965
|
+
// product dialogs use, so the footer reads without a second look.
|
|
1265
1966
|
footer: React.createElement(
|
|
1266
|
-
|
|
1267
|
-
|
|
1967
|
+
'div',
|
|
1968
|
+
{ className: 'dsh-workspace-plus-footer' },
|
|
1268
1969
|
React.createElement(
|
|
1269
1970
|
Button,
|
|
1270
|
-
{ variant: '
|
|
1971
|
+
{ variant: 'ghost', disabled: state.busy, onClick: () => void load(requestedId) },
|
|
1271
1972
|
'刷新',
|
|
1272
1973
|
),
|
|
1273
1974
|
React.createElement(Button, { variant: 'primary', onClick: closeWorkdirsDialog }, '完成'),
|
|
1274
1975
|
),
|
|
1275
1976
|
},
|
|
1276
|
-
React.createElement('div', { className: 'dsh-workspace-plus-dialog' },
|
|
1977
|
+
React.createElement('div', { className: 'dsh-workspace-plus-dialog' }, ...blocks),
|
|
1277
1978
|
)
|
|
1278
1979
|
}
|
|
1279
1980
|
|
|
@@ -1281,9 +1982,11 @@ window.__ModuleLoader__.load({
|
|
|
1281
1982
|
* The `workspace+` settings page, registered into `settings.section`.
|
|
1282
1983
|
*
|
|
1283
1984
|
* It owns one preference per workspace: whether the feature runs there. That
|
|
1284
|
-
* single switch covers
|
|
1285
|
-
*
|
|
1286
|
-
*
|
|
1985
|
+
* single switch covers the two surfaces the feature itself drives — the label
|
|
1986
|
+
* table injected into that workspace's conversations and the `workspace_dirs`
|
|
1987
|
+
* tool. It deliberately does NOT cover the workspace row button: every row
|
|
1988
|
+
* keeps its management entry point, so a switched-off workspace can still be
|
|
1989
|
+
* inspected (and understood) instead of silently losing its only way in.
|
|
1287
1990
|
*
|
|
1288
1991
|
* Every workspace defaults to on except the default (first-use) one, which
|
|
1289
1992
|
* belongs to the product rather than to this plugin and therefore starts off.
|
|
@@ -1467,9 +2170,9 @@ window.__ModuleLoader__.load({
|
|
|
1467
2170
|
React.createElement(
|
|
1468
2171
|
'div',
|
|
1469
2172
|
{ className: 'dsh-workspace-plus-settings-description' },
|
|
1470
|
-
'
|
|
2173
|
+
'按工作区启用:开关控制注入这个工作区对话上下文的标签表,以及 ',
|
|
1471
2174
|
React.createElement('span', { className: 'dsh-workspace-plus-mono' }, 'workspace_dirs'),
|
|
1472
|
-
'
|
|
2175
|
+
' 工具。关掉不会删除已登记的标签,也不会收走工作区行上的目录按钮——那个按钮在任何工作区都能打开弹窗。',
|
|
1473
2176
|
),
|
|
1474
2177
|
workspaceList,
|
|
1475
2178
|
atLine,
|
|
@@ -1528,6 +2231,14 @@ window.__ModuleLoader__.load({
|
|
|
1528
2231
|
* re-insert its own children around ours — so the position is re-asserted on
|
|
1529
2232
|
* every reconcile pass rather than set once at creation.
|
|
1530
2233
|
*
|
|
2234
|
+
* Re-assert only when the seat actually moved: `insertBefore` on a node that
|
|
2235
|
+
* is already exactly where it belongs still detaches and re-attaches it, and
|
|
2236
|
+
* a real pointer press that spans such a re-seat never becomes a click — the
|
|
2237
|
+
* browser drops the click because the press target was not connected when the
|
|
2238
|
+
* pointer was released. Reconcile runs on every DOM mutation in the frame, so
|
|
2239
|
+
* that detach used to land inside almost every press and the button was
|
|
2240
|
+
* unclickable while still looking (and hit-testing) perfectly fine.
|
|
2241
|
+
*
|
|
1531
2242
|
* @param actions - the row's action cluster.
|
|
1532
2243
|
* @param button - this plugin's button.
|
|
1533
2244
|
*/
|
|
@@ -1535,9 +2246,32 @@ window.__ModuleLoader__.load({
|
|
|
1535
2246
|
const last = actions.lastElementChild
|
|
1536
2247
|
if (last === button) return
|
|
1537
2248
|
const anchor = actions.children.length > 1 ? last : null
|
|
2249
|
+
if (button.parentElement === actions && button.nextElementSibling === anchor) return
|
|
1538
2250
|
actions.insertBefore(button, anchor)
|
|
1539
2251
|
}
|
|
1540
2252
|
|
|
2253
|
+
/**
|
|
2254
|
+
* Paint one row button with the switch state of its workspace.
|
|
2255
|
+
*
|
|
2256
|
+
* The button exists for every workspace, so this is also the only place the
|
|
2257
|
+
* switched-off state is visible on the row: the marker class dims the glyph
|
|
2258
|
+
* and the label says why, while the click still opens the dialog — which is
|
|
2259
|
+
* where those labels live and where the user decides what to do next.
|
|
2260
|
+
*
|
|
2261
|
+
* Idempotent, so the reconcile pass can call it on every button it already
|
|
2262
|
+
* owns without touching the DOM when nothing changed.
|
|
2263
|
+
*
|
|
2264
|
+
* @param button - this plugin's button on that row.
|
|
2265
|
+
* @param workspaceId - the id taken from the row's `data-row-key`.
|
|
2266
|
+
*/
|
|
2267
|
+
function markRowButton(button, workspaceId) {
|
|
2268
|
+
const enabled = isEnabledWorkspace(workspaceId)
|
|
2269
|
+
const label = enabled ? ROW_BUTTON_LABEL : ROW_BUTTON_LABEL_OFF
|
|
2270
|
+
button.classList.toggle(ROW_BUTTON_OFF_CLASS, !enabled)
|
|
2271
|
+
if (button.title !== label) button.title = label
|
|
2272
|
+
if (button.getAttribute('aria-label') !== label) button.setAttribute('aria-label', label)
|
|
2273
|
+
}
|
|
2274
|
+
|
|
1541
2275
|
/**
|
|
1542
2276
|
* Insert the directory button into every workspace row and keep it there.
|
|
1543
2277
|
*
|
|
@@ -1551,9 +2285,16 @@ window.__ModuleLoader__.load({
|
|
|
1551
2285
|
* A MutationObserver reconciles additions, removals, and position, because
|
|
1552
2286
|
* React re-renders the row without knowing this node exists.
|
|
1553
2287
|
*
|
|
1554
|
-
*
|
|
1555
|
-
*
|
|
1556
|
-
*
|
|
2288
|
+
* Every real workspace row carries the button, including one whose `enabled`
|
|
2289
|
+
* flag is false (the default Workspace before the user turns its switch on,
|
|
2290
|
+
* or a workspace the user switched off). The switch governs whether the
|
|
2291
|
+
* label table runs for that workspace — context injection and the
|
|
2292
|
+
* `workspace_dirs` tool — never whether its management UI can be reached:
|
|
2293
|
+
* the dialog is the only place those labels can be read, and the settings
|
|
2294
|
+
* page is the only place the switch can be flipped, so hiding the trigger
|
|
2295
|
+
* would strand both. A switched-off workspace only changes the button's
|
|
2296
|
+
* tooltip and draws it dimmed, and `refresh()` re-reads the state after a
|
|
2297
|
+
* settings change.
|
|
1557
2298
|
*
|
|
1558
2299
|
* @param options - the availability callback, fired when the injected count changes.
|
|
1559
2300
|
* @returns `{ dispose, available, refresh }`.
|
|
@@ -1584,7 +2325,7 @@ window.__ModuleLoader__.load({
|
|
|
1584
2325
|
const live = new Set(rows)
|
|
1585
2326
|
for (const [row, button] of [...injected]) {
|
|
1586
2327
|
const workspaceId = (row.getAttribute('data-row-key') ?? '').slice(ROW_KEY_PREFIX.length)
|
|
1587
|
-
const stillWanted = live.has(row) && row.isConnected && workspaceId !== ''
|
|
2328
|
+
const stillWanted = live.has(row) && row.isConnected && workspaceId !== ''
|
|
1588
2329
|
if (stillWanted) continue
|
|
1589
2330
|
button.remove()
|
|
1590
2331
|
injected.delete(row)
|
|
@@ -1593,14 +2334,13 @@ window.__ModuleLoader__.load({
|
|
|
1593
2334
|
const workspaceId = (row.getAttribute('data-row-key') ?? '').slice(ROW_KEY_PREFIX.length)
|
|
1594
2335
|
// The synthetic "ungrouped" row carries the empty key and owns no workspace.
|
|
1595
2336
|
if (workspaceId === '') continue
|
|
1596
|
-
// A switched-off workspace gets no entry point at all.
|
|
1597
|
-
if (!isEnabledWorkspace(workspaceId)) continue
|
|
1598
2337
|
const actions = row.lastElementChild
|
|
1599
2338
|
if (actions === null || actions === undefined) continue
|
|
1600
2339
|
const existing = injected.get(row)
|
|
1601
2340
|
if (existing !== undefined && existing.isConnected) {
|
|
1602
|
-
// Already there: only its seat may have moved.
|
|
2341
|
+
// Already there: only its seat and its switch state may have moved.
|
|
1603
2342
|
seatRowButton(actions, existing)
|
|
2343
|
+
markRowButton(existing, workspaceId)
|
|
1604
2344
|
continue
|
|
1605
2345
|
}
|
|
1606
2346
|
const button = document.createElement('button')
|
|
@@ -1609,12 +2349,35 @@ window.__ModuleLoader__.load({
|
|
|
1609
2349
|
button.title = ROW_BUTTON_LABEL
|
|
1610
2350
|
button.setAttribute('aria-label', ROW_BUTTON_LABEL)
|
|
1611
2351
|
button.innerHTML = ROW_BUTTON_GLYPH
|
|
1612
|
-
|
|
1613
|
-
|
|
2352
|
+
/**
|
|
2353
|
+
* Open the dialog for the row the button currently sits on. The key is
|
|
2354
|
+
* read here rather than captured, because the button outlives a row
|
|
2355
|
+
* whose key changes.
|
|
2356
|
+
*/
|
|
2357
|
+
const activate = (event) => {
|
|
2358
|
+
// Keep the activation off the row, whose own handler toggles the group.
|
|
1614
2359
|
event.preventDefault()
|
|
1615
2360
|
event.stopPropagation()
|
|
1616
|
-
|
|
2361
|
+
const current = (row.getAttribute('data-row-key') ?? '').slice(ROW_KEY_PREFIX.length)
|
|
2362
|
+
openWorkdirsDialog(current === '' ? null : current)
|
|
2363
|
+
}
|
|
2364
|
+
button.addEventListener('click', activate)
|
|
2365
|
+
/**
|
|
2366
|
+
* A press can still lose its click when the shell's React replaces part
|
|
2367
|
+
* of the row mid-press (the browser then drops the click, because the
|
|
2368
|
+
* press target is no longer connected at release). The release itself
|
|
2369
|
+
* always arrives, so it opens the same dialog; a following click is a
|
|
2370
|
+
* no-op because the store ignores a repeated identical open.
|
|
2371
|
+
*
|
|
2372
|
+
* Touch is excluded: a scroll-drag that happens to start on this 16px
|
|
2373
|
+
* button delivers its release here through implicit pointer capture,
|
|
2374
|
+
* and opening the dialog then would be wrong. Taps still open it
|
|
2375
|
+
* through the click that follows.
|
|
2376
|
+
*/
|
|
2377
|
+
button.addEventListener('pointerup', (event) => {
|
|
2378
|
+
if (event.button === 0 && event.pointerType !== 'touch') activate(event)
|
|
1617
2379
|
})
|
|
2380
|
+
markRowButton(button, workspaceId)
|
|
1618
2381
|
seatRowButton(actions, button)
|
|
1619
2382
|
injected.set(row, button)
|
|
1620
2383
|
}
|
|
@@ -1660,7 +2423,9 @@ window.__ModuleLoader__.load({
|
|
|
1660
2423
|
|
|
1661
2424
|
/**
|
|
1662
2425
|
* Contribute the `workspace+` settings page, the directory dialog, its
|
|
1663
|
-
* fallback entry, and the workspace-row buttons that open it
|
|
2426
|
+
* fallback entry, and the workspace-row buttons that open it — plus the two
|
|
2427
|
+
* client faces of the 通知 sub-feature (its settings page and the
|
|
2428
|
+
* pending-open poll behind notification clicks).
|
|
1664
2429
|
*
|
|
1665
2430
|
* Every registration goes through `ctx.slots.inject(key, callback)`: the
|
|
1666
2431
|
* callback runs as soon as the shell declares that slot (immediately when it
|
|
@@ -1687,6 +2452,33 @@ window.__ModuleLoader__.load({
|
|
|
1687
2452
|
'dsh-workspace-plus: settings section',
|
|
1688
2453
|
)
|
|
1689
2454
|
|
|
2455
|
+
// The 通知 sub-feature. Both faces are guarded by `registerNotice`: a
|
|
2456
|
+
// failure is reported and skipped instead of failing the fiber, because
|
|
2457
|
+
// the shell's boot audit treats every non-ACTIVE entry as fatal and the
|
|
2458
|
+
// workspace+ features must survive a notice-side mistake. The poll runs
|
|
2459
|
+
// unconditionally — clicking an older notification must still open the
|
|
2460
|
+
// conversation even while the notice ROW is off (its host half answers
|
|
2461
|
+
// 404, the poll backs off quietly). The settings page lives in the same
|
|
2462
|
+
// slot with the id/order dsh-notice used, so it reads as the same page.
|
|
2463
|
+
registerNotice(ctx, '通知轮询', () => ctx.effect(() => startNoticePendingPoll(ctx), 'dsh-workspace-plus/notice: pending open poll'))
|
|
2464
|
+
registerNotice(ctx, '通知设置页', () =>
|
|
2465
|
+
ctx.effect(
|
|
2466
|
+
() =>
|
|
2467
|
+
ctx.slots.inject('settings.section', () =>
|
|
2468
|
+
ctx.slots.register(
|
|
2469
|
+
{
|
|
2470
|
+
name: 'settings.section',
|
|
2471
|
+
id: NOTICE_SETTINGS_SECTION_ID,
|
|
2472
|
+
order: NOTICE_SETTINGS_SECTION_ORDER,
|
|
2473
|
+
label: NOTICE_SETTINGS_SECTION_LABEL,
|
|
2474
|
+
},
|
|
2475
|
+
NoticeSettings,
|
|
2476
|
+
),
|
|
2477
|
+
),
|
|
2478
|
+
'dsh-workspace-plus/notice: settings section',
|
|
2479
|
+
),
|
|
2480
|
+
)
|
|
2481
|
+
|
|
1690
2482
|
// The dialog lives in the frame-wide overlay: the same seat the shell's own
|
|
1691
2483
|
// workspace Rename/Delete dialogs occupy.
|
|
1692
2484
|
ctx.effect(
|
|
@@ -1802,9 +2594,9 @@ window.__ModuleLoader__.load({
|
|
|
1802
2594
|
},
|
|
1803
2595
|
})
|
|
1804
2596
|
|
|
1805
|
-
// Warm the shared snapshot at boot and keep
|
|
1806
|
-
// settings: a switch flipped in the settings page
|
|
1807
|
-
// a reload.
|
|
2597
|
+
// Warm the shared snapshot at boot and keep every row button in step with
|
|
2598
|
+
// the settings: a switch flipped in the settings page re-marks its button
|
|
2599
|
+
// (dimmed, with the reason in its tooltip) without a reload.
|
|
1808
2600
|
const offHostState = subscribeHostState(() => rowButtons.refresh())
|
|
1809
2601
|
void refreshHostState()
|
|
1810
2602
|
|
|
@@ -1823,11 +2615,27 @@ window.__ModuleLoader__.load({
|
|
|
1823
2615
|
exports.SETTINGS_SECTION_ID = SETTINGS_SECTION_ID
|
|
1824
2616
|
exports.SETTINGS_SECTION_LABEL = SETTINGS_SECTION_LABEL
|
|
1825
2617
|
exports.SETTINGS_SECTION_ORDER = SETTINGS_SECTION_ORDER
|
|
2618
|
+
exports.NOTICE_SETTINGS_SECTION_ID = NOTICE_SETTINGS_SECTION_ID
|
|
2619
|
+
exports.NOTICE_SETTINGS_SECTION_LABEL = NOTICE_SETTINGS_SECTION_LABEL
|
|
2620
|
+
exports.NOTICE_SETTINGS_SECTION_ORDER = NOTICE_SETTINGS_SECTION_ORDER
|
|
2621
|
+
exports.NOTICE_API_PATH = NOTICE_API_PATH
|
|
2622
|
+
exports.NOTICE_PENDING_PATH = NOTICE_PENDING_PATH
|
|
2623
|
+
exports.NOTICE_POLL_INTERVAL_MS = NOTICE_POLL_INTERVAL_MS
|
|
2624
|
+
exports.NOTICE_POLL_RETRY_MS = NOTICE_POLL_RETRY_MS
|
|
2625
|
+
exports.fetchNoticeSnapshot = fetchNoticeSnapshot
|
|
2626
|
+
exports.postNoticeOperation = postNoticeOperation
|
|
2627
|
+
exports.NoticeSwitchRow = NoticeSwitchRow
|
|
2628
|
+
exports.NoticeSettings = NoticeSettings
|
|
2629
|
+
exports.startNoticePendingPoll = startNoticePendingPoll
|
|
2630
|
+
exports.registerNotice = registerNotice
|
|
2631
|
+
exports.ensureNoticeStyles = ensureNoticeStyles
|
|
1826
2632
|
exports.DEFAULT_WORKSPACE_SETTING = DEFAULT_WORKSPACE_SETTING
|
|
1827
2633
|
exports.WORKSPACE_ENABLED_SETTING = WORKSPACE_ENABLED_SETTING
|
|
1828
2634
|
exports.API_PATH = API_PATH
|
|
1829
2635
|
exports.ROW_BUTTON_CLASS = ROW_BUTTON_CLASS
|
|
2636
|
+
exports.ROW_BUTTON_OFF_CLASS = ROW_BUTTON_OFF_CLASS
|
|
1830
2637
|
exports.ROW_BUTTON_LABEL = ROW_BUTTON_LABEL
|
|
2638
|
+
exports.ROW_BUTTON_LABEL_OFF = ROW_BUTTON_LABEL_OFF
|
|
1831
2639
|
exports.SIDEBAR_SLOT_SELECTOR = SIDEBAR_SLOT_SELECTOR
|
|
1832
2640
|
exports.WORKSPACE_ROW_SELECTOR = WORKSPACE_ROW_SELECTOR
|
|
1833
2641
|
exports.FALLBACK_SETTLE_MS = FALLBACK_SETTLE_MS
|
|
@@ -1855,6 +2663,7 @@ window.__ModuleLoader__.load({
|
|
|
1855
2663
|
exports.pickErrorMessage = pickErrorMessage
|
|
1856
2664
|
exports.installWorkspaceRowButtons = installWorkspaceRowButtons
|
|
1857
2665
|
exports.seatRowButton = seatRowButton
|
|
2666
|
+
exports.markRowButton = markRowButton
|
|
1858
2667
|
exports.WorkdirsPanelIcon = WorkdirsPanelIcon
|
|
1859
2668
|
exports.WorkdirsEntryIcon = WorkdirsEntryIcon
|
|
1860
2669
|
exports.WorkdirsDialog = WorkdirsDialog
|