dsh-tool-adapt 0.2.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/lib/client.js ADDED
@@ -0,0 +1,841 @@
1
+ // dsh-tool-adapt — client half (official __ModuleLoader__ web bundle)
2
+ //
3
+ // Browser-only bundle: consumed by the DSH web loader (window.__ModuleLoader__).
4
+ // Not importable in Node. Renders the ADAPT status pill and an official-style
5
+ // expandable Settings Card under settings.plugin.item / key tool-adapt.
6
+ //
7
+ // The pill is hidden by DEFAULT: the ui.pill switch in the Settings Card
8
+ // (设置 → 插件 → ADAPT「显示状态胶囊」) owns visibility; the polled /status
9
+ // config is the hot source of truth the pill follows.
10
+ //
11
+ // Lifecycle contract: every client side effect — the pill mounted flag, root
12
+ // element, shadow DOM, plugin-card style tag, MutationObserver, ResizeObserver,
13
+ // both intervals, the pending animation frame, and every window / document /
14
+ // element / pointer / drag listener — is owned by the Cordis fiber through
15
+ // ctx.effect() and released by the returned disposer. Cleanup is idempotent,
16
+ // so stop / update / HMR can dispose the bundle and a later apply re-mounts
17
+ // the pill from scratch.
18
+
19
+ window.__ModuleLoader__.load({
20
+ id: 'dsh-tool-adapt',
21
+ factory: (require) => {
22
+ const module = { exports: {} }
23
+ const exports = module.exports
24
+ const React = require('react')
25
+
26
+ const NS = 'tool-adapt'
27
+ const API = '/api/tool-adapt'
28
+ const POLL_MS = 5000
29
+ const e = React.createElement
30
+
31
+ const FIELDS = [
32
+ { path: ['ui', 'pill'], kind: 'bool', label: '显示状态胶囊', hint: '默认关闭。开启后输入框旁显示 ADAPT 胶囊;关闭只隐藏 UI,守卫逻辑不受影响。' },
33
+ { path: ['guard', 'enabled'], kind: 'bool', label: '守卫启用', hint: '提权死状态下移除 schema 参数,并剥除仍携带的字段。' },
34
+ { path: ['l2', 'enabled'], kind: 'bool', label: 'L2 惯例预装', hint: '向非排除模型注入 DSH 工具调用惯例。' },
35
+ { path: ['l2', 'excludeModels'], kind: 'models', label: '排除模型', hint: '逗号分隔的 * 通配模式,默认 deepseek-*。' },
36
+ { path: ['l2', 'block'], kind: 'textarea', label: '惯例块' },
37
+ { path: ['l0', 'enabled'], kind: 'bool', label: 'L0 失败保险丝' },
38
+ { path: ['l0', 'remindAfter'], kind: 'num', label: '提醒阈值', min: 2, max: 20 },
39
+ { path: ['l0', 'vetoAfter'], kind: 'num', label: 'veto 阈值', min: 0, max: 20, hint: '0 表示关闭 veto。' },
40
+ { path: ['l0', 'reminderText'], kind: 'textarea', label: '提醒文案' },
41
+ { path: ['l0', 'vetoText'], kind: 'textarea', label: 'veto 文案' },
42
+ ]
43
+
44
+ function isPlainObject(v) {
45
+ return v !== null && typeof v === 'object' && !Array.isArray(v)
46
+ }
47
+
48
+ function getAt(obj, path) {
49
+ let cur = obj
50
+ for (const key of path) {
51
+ if (!isPlainObject(cur) || !(key in cur)) return undefined
52
+ cur = cur[key]
53
+ }
54
+ return cur
55
+ }
56
+
57
+ function formatValue(field, value) {
58
+ if (field.kind === 'bool') return value ? 'true' : 'false'
59
+ if (field.kind === 'models') return Array.isArray(value) ? value.join(',') : ''
60
+ if (value === undefined || value === null) return ''
61
+ return String(value)
62
+ }
63
+
64
+ function parseValue(field, text) {
65
+ if (field.kind === 'bool') return text === true || text === 'true'
66
+ if (field.kind === 'models') {
67
+ const list = String(text || '').split(',').map((s) => s.trim()).filter(Boolean)
68
+ if (list.length > 50) return undefined
69
+ for (const entry of list) {
70
+ if (!/^[A-Za-z0-9.*_-]{1,64}$/.test(entry)) return undefined
71
+ }
72
+ return list
73
+ }
74
+ if (field.kind === 'num') {
75
+ const n = Number.parseInt(String(text), 10)
76
+ if (!Number.isInteger(n)) return undefined
77
+ if (field.min !== undefined && n < field.min) return undefined
78
+ if (field.max !== undefined && n > field.max) return undefined
79
+ return n
80
+ }
81
+ const s = String(text || '')
82
+ if (!s) return undefined
83
+ if (field.path[1] === 'block' && s.length > 4000) return undefined
84
+ if ((field.path[1] === 'reminderText' || field.path[1] === 'vetoText') && s.length > 2000) return undefined
85
+ return s
86
+ }
87
+
88
+ function fieldKey(path) {
89
+ return path.join('.')
90
+ }
91
+
92
+ // Official PluginCard / field chrome cannot be imported by an out-of-repo
93
+ // plugin (bundle purity). Recreate the same disclosure card so ADAPT sits
94
+ // in the Plugins list as one expandable <li> beside Shell / Agent loop.
95
+ const CARD_CSS_ID = 'dsh-tool-adapt/plugin-card'
96
+ const CARD_CSS = [
97
+ '.dtaCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}',
98
+ '.dtaCard:hover{border-color:var(--dsw-alias-label-dimmed)}',
99
+ '.dtaCardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}',
100
+ '.dtaHeader{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}',
101
+ '.dtaHeader:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}',
102
+ '.dtaHeadText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}',
103
+ '.dtaName{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}',
104
+ '.dtaDescription{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}',
105
+ '.dtaChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}',
106
+ '.dtaChevronOpen{transform:rotate(180deg)}',
107
+ '.dtaBody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}',
108
+ '.dtaReadOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}',
109
+ '.dtaPending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}',
110
+ '.dtaFooter{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}',
111
+ '.dtaFailed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}',
112
+ '.dtaDiscard,.dtaSave{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}',
113
+ '.dtaDiscard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}',
114
+ '.dtaDiscard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}',
115
+ '.dtaSave{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}',
116
+ '.dtaDiscard:disabled,.dtaSave:disabled{opacity:.4;cursor:default}',
117
+ '.dtaDiscard:focus-visible,.dtaSave:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}',
118
+ '.dtaField{flex-direction:column;gap:6px;padding:12px 0;display:flex}',
119
+ '.dtaField+.dtaField{border-top:1px solid var(--dsw-alias-border-l2)}',
120
+ '.dtaFieldHead{align-items:center;gap:8px;display:flex}',
121
+ '.dtaLabel{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}',
122
+ '.dtaBadges{align-items:center;gap:8px;display:inline-flex}',
123
+ '.dtaBadge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}',
124
+ '.dtaReset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}',
125
+ '.dtaReset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}',
126
+ '.dtaReset:disabled{cursor:default}',
127
+ '.dtaInput,.dtaTextarea{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;font-size:13px;line-height:1.5;width:100%;box-sizing:border-box}',
128
+ '.dtaInput{height:34px;padding:0 12px}',
129
+ '.dtaTextarea{min-height:88px;padding:8px 12px;resize:vertical}',
130
+ '.dtaInput:focus-visible,.dtaTextarea:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}',
131
+ '.dtaInput:disabled,.dtaTextarea:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}',
132
+ '.dtaInputInvalid{border-color:var(--dsw-alias-label-error)}',
133
+ '.dtaInvalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}',
134
+ '.dtaHint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}',
135
+ '.dtaSwitch{appearance:none;width:36px;height:20px;margin:0;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-3);position:relative;cursor:pointer;flex:none}',
136
+ '.dtaSwitch::after{content:"";width:14px;height:14px;border-radius:50%;background:var(--dsw-alias-label-tertiary);position:absolute;top:2px;left:2px;transition:transform .16s,background .16s}',
137
+ '.dtaSwitch:checked{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}',
138
+ '.dtaSwitch:checked::after{background:var(--dsw-alias-bg-layer-3);transform:translateX(16px)}',
139
+ '.dtaSwitch:disabled{opacity:.4;cursor:default}',
140
+ '.dtaSwitch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}',
141
+ ].join('')
142
+
143
+ function ensureCardStyles() {
144
+ if (typeof document === 'undefined') return
145
+ if (document.querySelector('style[data-plugin-css=' + JSON.stringify(CARD_CSS_ID) + ']')) return
146
+ const tag = document.createElement('style')
147
+ tag.dataset.plugin = 'dsh-tool-adapt'
148
+ tag.dataset.pluginCss = CARD_CSS_ID
149
+ tag.textContent = CARD_CSS
150
+ document.head.appendChild(tag)
151
+ }
152
+
153
+ // Idempotent inverse of ensureCardStyles(): the plugin disposer removes the
154
+ // card style tag so stop / update / HMR leave no style behind. A later
155
+ // SettingsCard render re-creates it via ensureCardStyles().
156
+ function removeCardStyles() {
157
+ if (typeof document === 'undefined') return
158
+ const tag = document.querySelector('style[data-plugin-css=' + JSON.stringify(CARD_CSS_ID) + ']')
159
+ if (tag && tag.parentNode) tag.parentNode.removeChild(tag)
160
+ }
161
+
162
+ function Chevron(props) {
163
+ return e('svg', {
164
+ width: 14,
165
+ height: 14,
166
+ className: props.className,
167
+ viewBox: '0 0 14 14',
168
+ fill: 'none',
169
+ xmlns: 'http://www.w3.org/2000/svg',
170
+ 'aria-hidden': true,
171
+ }, e('path', {
172
+ d: 'M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z',
173
+ fill: 'currentColor',
174
+ }))
175
+ }
176
+
177
+ function FieldRow(props) {
178
+ const id = props.id
179
+ const head = [
180
+ e('label', { key: 'lab', className: 'dtaLabel', htmlFor: id }, props.label),
181
+ ]
182
+ if (props.overridden) {
183
+ head.push(e('span', { key: 'badges', className: 'dtaBadges' },
184
+ e('span', { className: 'dtaBadge' }, '已覆盖'),
185
+ e('button', {
186
+ type: 'button',
187
+ className: 'dtaReset',
188
+ disabled: props.disabled,
189
+ onClick: props.onReset,
190
+ }, '恢复默认'),
191
+ ))
192
+ }
193
+ const control = props.kind === 'bool'
194
+ ? e('input', {
195
+ id,
196
+ className: 'dtaSwitch',
197
+ type: 'checkbox',
198
+ checked: props.text === 'true',
199
+ disabled: props.disabled,
200
+ onChange: (ev) => props.onEdit(ev.target.checked ? 'true' : 'false'),
201
+ })
202
+ : props.kind === 'textarea'
203
+ ? e('textarea', {
204
+ id,
205
+ className: props.invalid ? 'dtaTextarea dtaInputInvalid' : 'dtaTextarea',
206
+ value: props.text,
207
+ disabled: props.disabled,
208
+ 'aria-invalid': props.invalid || undefined,
209
+ onChange: (ev) => props.onEdit(ev.target.value),
210
+ })
211
+ : e('input', {
212
+ id,
213
+ className: props.invalid ? 'dtaInput dtaInputInvalid' : 'dtaInput',
214
+ type: 'text',
215
+ inputMode: props.kind === 'num' ? 'numeric' : undefined,
216
+ value: props.text,
217
+ disabled: props.disabled,
218
+ 'aria-invalid': props.invalid || undefined,
219
+ onChange: (ev) => props.onEdit(ev.target.value),
220
+ })
221
+ return e('div', { className: 'dtaField' },
222
+ e('div', { className: 'dtaFieldHead' }, head),
223
+ control,
224
+ e('p', { className: props.invalid ? 'dtaInvalid' : 'dtaHint' },
225
+ props.invalid ? '当前草稿无法保存' : (props.hint || null),
226
+ ),
227
+ )
228
+ }
229
+
230
+ function SettingsCard(props) {
231
+ ensureCardStyles()
232
+ const scope = props.scope
233
+ const api = props.api
234
+ const [tick, setTick] = React.useState(0)
235
+ const [open, setOpen] = React.useState(false)
236
+ const [staged, setStaged] = React.useState({})
237
+ const [saving, setSaving] = React.useState(false)
238
+ const [failed, setFailed] = React.useState(false)
239
+
240
+ React.useEffect(() => {
241
+ if (!scope || typeof scope.subscribe !== 'function') return undefined
242
+ return scope.subscribe(() => setTick((n) => n + 1))
243
+ }, [scope])
244
+
245
+ const snap = scope && typeof scope.getSnapshot === 'function'
246
+ ? scope.getSnapshot()
247
+ : { status: 'unavailable', value: undefined, base: undefined, user: undefined, revision: undefined, writable: false }
248
+
249
+ const available = snap.status === 'ready'
250
+ const writable = !!snap.writable
251
+ const value = snap.value || {}
252
+ const base = snap.base || {}
253
+ const user = snap.user || {}
254
+
255
+ const plan = []
256
+ for (const field of FIELDS) {
257
+ const key = fieldKey(field.path)
258
+ const draft = staged[key]
259
+ if (!draft) continue
260
+ if (draft.clear) {
261
+ if (getAt(user, field.path) !== undefined) plan.push({ field, op: 'unset', path: field.path })
262
+ continue
263
+ }
264
+ const parsed = parseValue(field, draft.text)
265
+ if (parsed === undefined) {
266
+ plan.push({ field, invalid: true })
267
+ continue
268
+ }
269
+ if (formatValue(field, getAt(value, field.path)) === formatValue(field, parsed)) continue
270
+ plan.push({ field, op: 'set', path: field.path, value: parsed })
271
+ }
272
+
273
+ const dirty = plan.length > 0
274
+ const invalid = plan.some((item) => item.invalid)
275
+ const blocked = !dirty || invalid || saving
276
+
277
+ function stage(field, next) {
278
+ setFailed(false)
279
+ setStaged((prev) => Object.assign({}, prev, { [fieldKey(field.path)]: next }))
280
+ }
281
+
282
+ function discard() {
283
+ if (!dirty && !failed) return
284
+ setStaged({})
285
+ setFailed(false)
286
+ }
287
+
288
+ async function save() {
289
+ if (!api || !api.settings || saving || !dirty || invalid || !writable) return
290
+ setSaving(true)
291
+ setFailed(false)
292
+ try {
293
+ const ops = plan.filter((item) => !item.invalid).map((item) => (
294
+ item.op === 'unset'
295
+ ? { op: 'unset', path: item.path }
296
+ : { op: 'set', path: item.path, value: item.value }
297
+ ))
298
+ const payload = { ns: NS, ops }
299
+ if (snap.revision !== undefined) payload.expectedRevision = snap.revision
300
+ const response = await api.settings.mutate(payload)
301
+ const ok = !!(response && response.result && response.result.ok)
302
+ if (ok) setStaged({})
303
+ else setFailed(true)
304
+ } catch (_) {
305
+ setFailed(true)
306
+ }
307
+ setSaving(false)
308
+ }
309
+
310
+ void tick
311
+ if (!available) return null
312
+
313
+ const fields = FIELDS.map((field) => {
314
+ const key = fieldKey(field.path)
315
+ const draft = staged[key]
316
+ const current = getAt(value, field.path)
317
+ const stored = getAt(user, field.path) !== undefined
318
+ const overridden = draft
319
+ ? !draft.clear
320
+ : stored
321
+ const text = draft ? draft.text : formatValue(field, current)
322
+ const parsed = draft && !draft.clear ? parseValue(field, draft.text) : current
323
+ const invalidField = !!(draft && !draft.clear && parsed === undefined)
324
+ return e(FieldRow, {
325
+ key,
326
+ id: 'plugin-config-tool-adapt-' + key.replace(/\./g, '-'),
327
+ kind: field.kind,
328
+ label: field.label,
329
+ hint: field.hint,
330
+ text,
331
+ overridden,
332
+ invalid: invalidField,
333
+ disabled: !writable || saving,
334
+ onEdit: (next) => stage(field, { text: next, clear: false }),
335
+ onReset: () => stage(field, { text: formatValue(field, getAt(base, field.path)), clear: true }),
336
+ })
337
+ })
338
+
339
+ const body = open ? e('div', { className: 'dtaBody' },
340
+ writable ? null : e('p', { className: 'dtaReadOnly', role: 'status' }, '本部署的设置为只读。'),
341
+ fields,
342
+ e('div', { className: 'dtaFooter' },
343
+ failed ? e('p', { className: 'dtaFailed', role: 'status' }, '本部署没有接受这些值,已保留供你修改。') : null,
344
+ e('button', {
345
+ type: 'button',
346
+ className: 'dtaDiscard',
347
+ disabled: !dirty || saving,
348
+ onClick: discard,
349
+ }, '放弃修改'),
350
+ e('button', {
351
+ type: 'button',
352
+ className: 'dtaSave',
353
+ disabled: blocked || !writable,
354
+ onClick: save,
355
+ }, saving ? '保存中…' : '保存'),
356
+ ),
357
+ ) : null
358
+
359
+ return e('li', { className: open ? 'dtaCard dtaCardOpen' : 'dtaCard' },
360
+ e('button', {
361
+ type: 'button',
362
+ className: 'dtaHeader',
363
+ 'aria-expanded': open,
364
+ 'aria-label': (open ? '收起设置' : '展开设置') + ': ADAPT',
365
+ onClick: () => setOpen(!open),
366
+ },
367
+ e('span', { className: 'dtaHeadText' },
368
+ e('span', { className: 'dtaName' }, 'ADAPT'),
369
+ e('span', { className: 'dtaDescription' }, '模型适配守卫。配置热生效,作用于下一次工具调用与提示词组装。'),
370
+ ),
371
+ dirty ? e('span', { className: 'dtaPending' }, '未保存') : null,
372
+ e(Chevron, { className: open ? 'dtaChevron dtaChevronOpen' : 'dtaChevron' }),
373
+ ),
374
+ body,
375
+ )
376
+ }
377
+
378
+ // ── composer pill ───────────────────────────────────────────────────────
379
+
380
+ const PILL_ROOT_ID = 'dsh-tool-adapt-root'
381
+ const ANCHOR_KEY = 'dsh.toolAdapt.anchor'
382
+ const ANCHORS = ['tl', 'tr', 'bl', 'br']
383
+ const ENSURE_MS = 2000
384
+
385
+ // Mounted flag owned by the plugin lifecycle: set when the pill starts and
386
+ // reset by the idempotent disposer, so a later apply (update / HMR / re-run)
387
+ // mounts a fresh pill instead of being blocked by a stale flag.
388
+ let pillMounted = false
389
+ let pillDispose = null
390
+
391
+ function startPill(scope) {
392
+ if (typeof document === 'undefined') return
393
+ if (pillMounted) {
394
+ // Previous mount leaked (disposal was skipped). Dispose it first, then
395
+ // mount again: every apply must end with exactly one live pill.
396
+ const previous = pillDispose
397
+ pillDispose = null
398
+ if (typeof previous === 'function') previous()
399
+ return startPill()
400
+ }
401
+ pillMounted = true
402
+
403
+ // Idempotent re-mount: drop any root a previous bundle left behind
404
+ // before creating a fresh one.
405
+ const stale = document.getElementById(PILL_ROOT_ID)
406
+ if (stale && stale.parentNode) stale.parentNode.removeChild(stale)
407
+
408
+ const cleanups = []
409
+ const track = (fn) => cleanups.push(fn)
410
+
411
+ let disposed = false
412
+ let root = null
413
+ let dragCleanup = null
414
+
415
+ // Mount target is ONLY the composer seat. There is deliberately no
416
+ // document.body fallback (and no composer-card parent fallback): the pill
417
+ // must stay inside the composer stacking context so DSH overlays can
418
+ // cover it. When the seat has not appeared yet, the document observer
419
+ // below waits for [data-composer-seat] and mounts as soon as it exists.
420
+ function findSeat() {
421
+ return document.querySelector('[data-composer-seat]')
422
+ }
423
+ function ensureMounted() {
424
+ const seat = findSeat()
425
+ if (!seat) return false
426
+ if (root.parentNode !== seat || !root.isConnected) seat.appendChild(root)
427
+ return true
428
+ }
429
+
430
+ root = document.createElement('div')
431
+ root.id = PILL_ROOT_ID
432
+ // Hidden by default (ui.pill defaults to false): the visibility gate
433
+ // below flips this to '' only once the settings snapshot or a status
434
+ // poll reports ui.pill === true.
435
+ root.style.cssText = 'position:fixed;z-index:1;display:none;font-family:ui-sans-serif,system-ui,sans-serif;'
436
+
437
+ const shadow = root.attachShadow({ mode: 'open' })
438
+ shadow.innerHTML = '<style>' + [
439
+ ':host{all:initial}',
440
+ '.pill{display:flex;align-items:center;gap:7px;padding:7px 14px;border:1px solid var(--dsw-alias-border-l1,#444);border-radius:999px;background:var(--dsw-alias-bg-layer-1,#222);background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#222) 82%, transparent);backdrop-filter:blur(8px);color:var(--dsw-alias-label-primary,#eee);font-size:12px;font-weight:600;letter-spacing:.04em;cursor:grab;box-shadow:0 2px 10px rgba(0,0,0,.28);user-select:none;touch-action:none;transition:border-color .18s ease,box-shadow .18s ease,transform .18s ease}',
441
+ '.pill:active{cursor:grabbing}',
442
+ '.pill:hover{border-color:var(--dsw-alias-brand-primary,#4a9eff);box-shadow:0 4px 16px rgba(0,0,0,.38);transform:translateY(-1px)}',
443
+ '.pill:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4a9eff);outline-offset:2px}',
444
+ '.glyph{display:flex;width:14px;height:14px;color:var(--dsw-alias-brand-primary,#4a9eff)}',
445
+ '.glyph svg{width:14px;height:14px;fill:currentColor}',
446
+ '.dot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-secondary,#999);transition:background .3s ease}',
447
+ '.dot.on{background:var(--dsw-alias-state-success-primary,#3fb950);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary,#3fb950) 22%, transparent)}',
448
+ '.dot.off{background:var(--dsw-alias-label-secondary,#999)}',
449
+ '.panel{position:fixed;right:0;bottom:calc(100% + 10px);width:360px;max-height:50vh;overflow:auto;display:none;flex-direction:column;gap:10px;padding:14px;border:1px solid var(--dsw-alias-border-l1,#444);border-radius:16px;background:var(--dsw-alias-bg-layer-1,#222);background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#222) 92%, transparent);backdrop-filter:blur(14px);color:var(--dsw-alias-label-primary,#eee);font-size:12px;box-shadow:0 12px 40px rgba(0,0,0,.45)}',
450
+ '.panel.open{display:flex}',
451
+ '.head{display:flex;align-items:baseline;gap:8px}',
452
+ '.head .t{font-size:14px;font-weight:700}',
453
+ '.head .sub{color:var(--dsw-alias-label-secondary,#999);font-size:11px}',
454
+ '.close{margin-left:auto;border:none;background:transparent;color:var(--dsw-alias-label-secondary,#999);cursor:pointer;font-size:14px}',
455
+ '.hint{color:var(--dsw-alias-label-secondary,#999);font-size:11px;line-height:1.5}',
456
+ '.status{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-secondary,#999);font-size:11px}',
457
+ '.status .sdot{width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-state-success-primary,#3fb950)}',
458
+ '.status .sdot.off{background:var(--dsw-alias-label-secondary,#999)}',
459
+ '.err{color:var(--dsw-alias-state-error-primary,#f85149);font-size:11px;word-break:break-all}',
460
+ ].join('') + '</style>'
461
+
462
+ const SHIELD = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l8 3.5v5.2c0 5-3.4 9.3-8 11.3-4.6-2-8-6.3-8-11.3V5.5L12 2zm0 2.2L6 6.4v4.3c0 3.9 2.6 7.4 6 9.1 3.4-1.7 6-5.2 6-9.1V6.4l-6-2.2z"/><path d="M10.8 14.6l-2.3-2.3-1.4 1.4 3.7 3.7 6.1-6.1-1.4-1.4-4.7 4.7z" opacity=".9"/></svg>'
463
+ const pill = document.createElement('button')
464
+ pill.className = 'pill'
465
+ pill.title = 'ADAPT 适配守卫(点击查看状态;拖动可吸附到输入框四角)'
466
+ const glyph = document.createElement('span')
467
+ glyph.className = 'glyph'
468
+ glyph.innerHTML = SHIELD
469
+ const dot = document.createElement('span')
470
+ dot.className = 'dot'
471
+ const label = document.createElement('span')
472
+ label.textContent = 'ADAPT'
473
+ pill.append(glyph, label, dot)
474
+
475
+ const cluster = document.createElement('div')
476
+ cluster.style.cssText = 'display:flex;align-items:center'
477
+ cluster.append(pill)
478
+
479
+ const panel = document.createElement('div')
480
+ panel.className = 'panel'
481
+ const head = document.createElement('div')
482
+ head.className = 'head'
483
+ const title = document.createElement('span')
484
+ title.className = 't'
485
+ title.textContent = 'ADAPT 模型适配守卫'
486
+ const sub = document.createElement('span')
487
+ sub.className = 'sub'
488
+ sub.textContent = 'dsh-tool-adapt'
489
+ const closeBtn = document.createElement('button')
490
+ closeBtn.className = 'close'
491
+ closeBtn.textContent = '✕'
492
+ head.append(title, sub, closeBtn)
493
+ const statusEl = document.createElement('div')
494
+ statusEl.className = 'status'
495
+ const hint = document.createElement('div')
496
+ hint.className = 'hint'
497
+ hint.textContent = '完整配置请到 设置 → 插件 → ADAPT。这里只显示当前守卫状态。'
498
+ const errEl = document.createElement('div')
499
+ errEl.className = 'err'
500
+ panel.append(head, statusEl, hint, errEl)
501
+ shadow.append(cluster, panel)
502
+
503
+ let anchor = 'br'
504
+ try {
505
+ const saved = localStorage.getItem(ANCHOR_KEY)
506
+ if (saved === 'left') anchor = 'bl'
507
+ else if (saved === 'right') anchor = 'br'
508
+ else if (ANCHORS.indexOf(saved) !== -1) anchor = saved
509
+ } catch (_) {}
510
+
511
+ root.style.right = '16px'
512
+ root.style.bottom = '64px'
513
+ let lastX = null
514
+ let lastY = null
515
+ let dragging = false
516
+
517
+ function findComposer() {
518
+ return document.querySelector('[data-composer-card]') || findSeat() || null
519
+ }
520
+
521
+ function place() {
522
+ if (dragging || !root.isConnected) return
523
+ const seat = findComposer()
524
+ if (!seat) return
525
+ const r = seat.getBoundingClientRect()
526
+ const pr = cluster.getBoundingClientRect()
527
+ const gap = 8
528
+ const leftSide = anchor === 'tl' || anchor === 'bl'
529
+ const topSide = anchor === 'tl' || anchor === 'tr'
530
+ let x = leftSide ? r.left - pr.width - gap : r.right + gap
531
+ let y = topSide ? r.top - pr.height - gap : r.bottom + gap
532
+ x = Math.max(4, Math.min(window.innerWidth - pr.width - 4, x))
533
+ y = Math.max(4, Math.min(window.innerHeight - pr.height - 4, y))
534
+ if (x === lastX && y === lastY) return
535
+ lastX = x
536
+ lastY = y
537
+ root.style.left = x + 'px'
538
+ root.style.top = y + 'px'
539
+ root.style.right = 'auto'
540
+ root.style.bottom = 'auto'
541
+ if (panel.classList.contains('open')) placePanel()
542
+ }
543
+
544
+ let rafId = 0
545
+ let rafPending = false
546
+ function schedulePlace() {
547
+ if (disposed || rafPending) return
548
+ rafPending = true
549
+ rafId = requestAnimationFrame(function () {
550
+ rafPending = false
551
+ rafId = 0
552
+ place()
553
+ })
554
+ }
555
+
556
+ // Wait for [data-composer-seat]: observe the whole document so the pill
557
+ // mounts whenever the seat first appears and follows later re-inserts.
558
+ const domObserver = new MutationObserver(function () {
559
+ if (ensureMounted()) schedulePlace()
560
+ })
561
+ domObserver.observe(document.documentElement, { childList: true, subtree: true })
562
+ track(function () { domObserver.disconnect() })
563
+
564
+ window.addEventListener('scroll', schedulePlace, { capture: true, passive: true })
565
+ track(function () { window.removeEventListener('scroll', schedulePlace, { capture: true }) })
566
+
567
+ window.addEventListener('resize', schedulePlace)
568
+ track(function () { window.removeEventListener('resize', schedulePlace) })
569
+
570
+ const seatRo = new ResizeObserver(schedulePlace)
571
+ let seatObserved = null
572
+ function watchSeat() {
573
+ const seat = findComposer()
574
+ if (seat && seat !== seatObserved) {
575
+ if (seatObserved) seatRo.unobserve(seatObserved)
576
+ seatRo.observe(seat)
577
+ seatObserved = seat
578
+ }
579
+ }
580
+ watchSeat()
581
+ track(function () { seatRo.disconnect() })
582
+
583
+ const ensureTimer = setInterval(function () {
584
+ ensureMounted() // React may have re-rendered the composer seat away
585
+ watchSeat()
586
+ place()
587
+ }, ENSURE_MS)
588
+ track(function () { clearInterval(ensureTimer) })
589
+
590
+ let lastDragAt = 0
591
+ function onPointerDown(ev) {
592
+ if (ev.button !== 0 || disposed) return
593
+ ev.preventDefault()
594
+ dragging = true
595
+ panel.classList.remove('open')
596
+ const startX = ev.clientX
597
+ const startY = ev.clientY
598
+ const startLeft = root.getBoundingClientRect().left
599
+ const startTop = root.getBoundingClientRect().top
600
+ let moved = false
601
+ const prevSelect = document.body.style.userSelect
602
+ document.body.style.userSelect = 'none'
603
+ function onMove(mv) {
604
+ const dx = mv.clientX - startX
605
+ const dy = mv.clientY - startY
606
+ if (Math.abs(dx) + Math.abs(dy) > 3) moved = true
607
+ root.style.left = (startLeft + dx) + 'px'
608
+ root.style.top = (startTop + dy) + 'px'
609
+ root.style.right = 'auto'
610
+ root.style.bottom = 'auto'
611
+ }
612
+ function onUp() {
613
+ dragCleanup = null
614
+ document.removeEventListener('pointermove', onMove)
615
+ document.removeEventListener('pointerup', onUp)
616
+ document.body.style.userSelect = prevSelect
617
+ dragging = false
618
+ if (!moved) return
619
+ lastDragAt = Date.now()
620
+ const rect = root.getBoundingClientRect()
621
+ const cx = rect.left + rect.width / 2
622
+ const cy = rect.top + rect.height / 2
623
+ const seat = findComposer()
624
+ if (seat) {
625
+ const r = seat.getBoundingClientRect()
626
+ const leftSide = cx < r.left + r.width / 2
627
+ const topSide = cy < r.top + r.height / 2
628
+ anchor = (topSide ? 't' : 'b') + (leftSide ? 'l' : 'r')
629
+ try { localStorage.setItem(ANCHOR_KEY, anchor) } catch (_) {}
630
+ }
631
+ lastX = null
632
+ lastY = null
633
+ place()
634
+ }
635
+ // Disposal during an active drag must still release the document-level
636
+ // listeners and restore the body selection style.
637
+ dragCleanup = function () {
638
+ document.removeEventListener('pointermove', onMove)
639
+ document.removeEventListener('pointerup', onUp)
640
+ document.body.style.userSelect = prevSelect
641
+ dragging = false
642
+ }
643
+ document.addEventListener('pointermove', onMove)
644
+ document.addEventListener('pointerup', onUp)
645
+ }
646
+ cluster.addEventListener('pointerdown', onPointerDown)
647
+ track(function () { cluster.removeEventListener('pointerdown', onPointerDown) })
648
+
649
+ function placePanel() {
650
+ panel.style.position = 'fixed'
651
+ const r = cluster.getBoundingClientRect()
652
+ const pw = panel.offsetWidth || 360
653
+ const ph = panel.offsetHeight || 220
654
+ let left = r.left
655
+ const maxLeft = Math.max(4, window.innerWidth - pw - 4)
656
+ if (left > maxLeft) left = maxLeft
657
+ if (left < 4) left = 4
658
+ const below = window.innerHeight - r.bottom - 8 >= ph || r.top < ph + 8
659
+ if (below) {
660
+ panel.style.left = left + 'px'
661
+ panel.style.top = (r.bottom + 8) + 'px'
662
+ panel.style.right = 'auto'
663
+ panel.style.bottom = 'auto'
664
+ } else {
665
+ panel.style.left = left + 'px'
666
+ panel.style.top = 'auto'
667
+ panel.style.right = 'auto'
668
+ panel.style.bottom = (window.innerHeight - r.top + 8) + 'px'
669
+ }
670
+ }
671
+
672
+ function renderPill(cfg) {
673
+ const on = !!(cfg && cfg.guard && cfg.guard.enabled)
674
+ dot.className = 'dot ' + (on ? 'on' : 'off')
675
+ pill.title = on ? 'ADAPT 适配守卫:已启用' : 'ADAPT 适配守卫:已停用'
676
+ }
677
+
678
+ function renderStatus(data) {
679
+ statusEl.textContent = ''
680
+ const sdot = document.createElement('span')
681
+ sdot.className = data && data.ok ? 'sdot' : 'sdot off'
682
+ const txt = document.createElement('span')
683
+ if (data && data.ok) {
684
+ const via = data.settingsReady ? '官方 Settings(tool-adapt)' : '兼容 JSON 文件'
685
+ txt.textContent = '守卫 ' + (data.config && data.config.guard && data.config.guard.enabled ? '已启用' : '已停用') + ' · ' + via
686
+ } else {
687
+ txt.textContent = (data && data.error) || '状态获取失败'
688
+ }
689
+ statusEl.append(sdot, txt)
690
+ }
691
+
692
+ async function refresh() {
693
+ try {
694
+ const res = await fetch(API + '/status', { cache: 'no-store' })
695
+ const data = await res.json()
696
+ if (data && data.ok) {
697
+ renderPill(data.config)
698
+ renderStatus(data)
699
+ noteStatusConfig(data.config)
700
+ errEl.textContent = data.fileError ? '配置警告: ' + data.fileError : ''
701
+ } else {
702
+ renderStatus(data)
703
+ errEl.textContent = (data && data.error) || '状态获取失败'
704
+ }
705
+ } catch (err) {
706
+ renderStatus(null)
707
+ errEl.textContent = String((err && err.message) || err)
708
+ }
709
+ }
710
+
711
+ async function pollPill() {
712
+ try {
713
+ const res = await fetch(API + '/status', { cache: 'no-store' })
714
+ const data = await res.json()
715
+ if (data && data.ok) {
716
+ renderPill(data.config)
717
+ noteStatusConfig(data.config)
718
+ }
719
+ } catch (_) {}
720
+ }
721
+
722
+ function onPillClick() {
723
+ if (Date.now() - lastDragAt < 350) return
724
+ panel.classList.toggle('open')
725
+ if (panel.classList.contains('open')) {
726
+ placePanel()
727
+ refresh()
728
+ }
729
+ }
730
+ function onCloseClick() {
731
+ panel.classList.remove('open')
732
+ }
733
+ pill.addEventListener('click', onPillClick)
734
+ track(function () { pill.removeEventListener('click', onPillClick) })
735
+ closeBtn.addEventListener('click', onCloseClick)
736
+ track(function () { closeBtn.removeEventListener('click', onCloseClick) })
737
+
738
+ refresh()
739
+ pollPill()
740
+ const pollTimer = setInterval(pollPill, POLL_MS)
741
+ track(function () { clearInterval(pollTimer) })
742
+
743
+ // ── visibility gate: 设置 → 插件 → ADAPT「显示状态胶囊」(ui.pill) ─────
744
+ // Default OFF: the root is created display:none and only the polled
745
+ // /status config (hot, host-authoritative, also correct for the legacy
746
+ // JSON file mode) may reveal it. Placed after every binding above is
747
+ // initialized because applyVisibility() can call schedulePlace().
748
+ let pillVisible = false
749
+
750
+ function applyVisibility() {
751
+ if (!root) return
752
+ root.style.display = pillVisible ? '' : 'none'
753
+ if (!pillVisible) {
754
+ if (panel.classList.contains('open')) panel.classList.remove('open')
755
+ } else {
756
+ schedulePlace() // cluster rect was 0 while hidden — reposition now
757
+ }
758
+ }
759
+
760
+ function noteStatusConfig(cfg) {
761
+ const show = !!(cfg && isPlainObject(cfg.ui) && cfg.ui.pill === true)
762
+ if (show === pillVisible) return
763
+ pillVisible = show
764
+ applyVisibility()
765
+ }
766
+
767
+ if (scope && typeof scope.subscribe === 'function') {
768
+ // A settings save reaches the host config immediately; re-fetch right
769
+ // away so the pill flips without waiting up to POLL_MS. The 5s poll
770
+ // below remains the safety net (and covers other tabs).
771
+ const unsubscribe = scope.subscribe(function () { refresh() })
772
+ track(function () { unsubscribe() })
773
+ }
774
+
775
+ // Mount immediately when the seat already exists; otherwise the document
776
+ // observer mounts the pill as soon as [data-composer-seat] appears.
777
+ ensureMounted()
778
+ place()
779
+
780
+ function disposePill() {
781
+ if (disposed) return
782
+ disposed = true
783
+ pillMounted = false
784
+ if (rafPending && rafId !== 0) cancelAnimationFrame(rafId)
785
+ rafPending = false
786
+ rafId = 0
787
+ if (dragCleanup) {
788
+ const cleanupDrag = dragCleanup
789
+ dragCleanup = null
790
+ cleanupDrag()
791
+ }
792
+ for (let i = cleanups.length - 1; i >= 0; i--) {
793
+ try { cleanups[i]() } catch (_) {}
794
+ }
795
+ cleanups.length = 0
796
+ if (root && root.parentNode) root.parentNode.removeChild(root)
797
+ root = null
798
+ pillDispose = null
799
+ }
800
+ pillDispose = disposePill
801
+ return disposePill
802
+ }
803
+
804
+ function apply(ctx) {
805
+ // Settings card first: pill mounting waits for the composer seat
806
+ // asynchronously and must never delay or break the Settings Slot.
807
+ const scope = ctx.settingsScope.bind({ namespace: NS })
808
+ // Compatibility layer: prefer DSH 0.1.2 fine-grained remote settings;
809
+ // retain the legacy RC connection API for older hosts.
810
+ const remote = ctx.get('remote')
811
+ const api = remote && remote.settings
812
+ ? { settings: { mutate: (payload) => remote.settings.mutate(payload.ns, payload.ops, payload.expectedRevision).then((result) => ({ result })) } }
813
+ : (ctx.get('connection') && ctx.get('connection').api)
814
+ const disposeSlot = ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
815
+ name: 'settings.plugin.item',
816
+ key: NS,
817
+ label: 'ADAPT',
818
+ }, function ToolAdaptCard() {
819
+ return e(SettingsCard, { scope, api })
820
+ }))
821
+
822
+ // Pill: fully lifecycle-owned. The effect runs startPill() now and the
823
+ // returned disposer on stop / update / HMR, idempotently releasing the
824
+ // mounted flag, root, shadow DOM, observers, both intervals, the rAF,
825
+ // and every listener so the next apply re-mounts from scratch. The pill
826
+ // starts hidden: startPill gates visibility on ui.pill (default false)
827
+ // via the settings scope plus the /status poll fallback.
828
+ ctx.effect(() => startPill(scope), 'dsh-tool-adapt: composer pill')
829
+
830
+ // Plugin-card style tag: created lazily by SettingsCard renders and
831
+ // removed here so no style is left behind after stop / update.
832
+ ctx.effect(() => () => removeCardStyles(), 'dsh-tool-adapt: plugin card style')
833
+
834
+ return disposeSlot
835
+ }
836
+
837
+ exports.apply = apply
838
+ exports.inject = ['slots', 'settingsScope', 'connection']
839
+ return module.exports
840
+ },
841
+ })