@liustack/modlens 3.16.6 → 3.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +4 -3
- package/README.zh-CN.md +5 -4
- package/dist/main.js +262 -46
- package/docs/cli.md +4 -3
- package/docs/cli.zh-CN.md +4 -3
- package/docs/harness-setup.md +32 -2
- package/docs/harness-setup.zh-CN.md +14 -2
- package/docs/security.md +2 -2
- package/docs/security.zh-CN.md +2 -2
- package/docs/troubleshooting.md +2 -2
- package/docs/troubleshooting.zh-CN.md +2 -2
- package/dsh/client.js +634 -1
- package/dsh/index.js +426 -8
- package/package.json +1 -1
- package/skills/modlens/SKILL.md +4 -4
- package/skills/modlens/references/configure.md +34 -8
- package/skills/modlens/references/configure.zh-CN.md +25 -8
- package/skills/modlens/references/onboard.md +1 -1
- package/skills/modlens/references/runtime.md +1 -1
- package/skills/modlens/scripts/run.ps1 +1 -1
- package/skills/modlens/scripts/run.sh +1 -1
package/dsh/client.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// host half.
|
|
16
16
|
window.__ModuleLoader__.load({
|
|
17
17
|
id: '@liustack/modlens',
|
|
18
|
-
factory: () => {
|
|
18
|
+
factory: (require) => {
|
|
19
19
|
var module = { exports: {} }
|
|
20
20
|
var exports = module.exports
|
|
21
21
|
|
|
@@ -172,7 +172,637 @@ window.__ModuleLoader__.load({
|
|
|
172
172
|
})
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// The settings card (issue #39). dsh renders a fixed set of plugin cards
|
|
176
|
+
// and does not enumerate settings namespaces, so a card is contributed
|
|
177
|
+
// through the `settings.plugin.item` slot rather than by declaring a
|
|
178
|
+
// schema. It reads and writes the host route above, which owns
|
|
179
|
+
// ~/.modlens/config.json: the browser never sees an API key, and never
|
|
180
|
+
// sends a blank one back over a stored key.
|
|
181
|
+
var ENGINES = ['antigravity-cli', 'gemini-api', 'openai', 'anthropic', 'claude-cli']
|
|
182
|
+
var REUSE = ['claude', 'codex', 'opencode', 'pi', 'grok']
|
|
183
|
+
|
|
184
|
+
// Two short label sets rather than a locale bundle: the card has a dozen
|
|
185
|
+
// strings, and a bundle would be more machinery than the thing it labels.
|
|
186
|
+
var TEXT = {
|
|
187
|
+
en: {
|
|
188
|
+
title: 'Vision engine (ModLens)',
|
|
189
|
+
subtitle: 'Vision engine provider configuration.',
|
|
190
|
+
openConfig: 'Open config file',
|
|
191
|
+
automatic: 'Automatic (failover chain decides)',
|
|
192
|
+
pickToConfigure: 'Pick an engine above to configure its key and endpoint.',
|
|
193
|
+
engine: 'Engine',
|
|
194
|
+
apiKey: 'API key',
|
|
195
|
+
baseUrl: 'Base URL',
|
|
196
|
+
model: 'Model',
|
|
197
|
+
stored: 'stored, leave empty to keep it',
|
|
198
|
+
unset: 'not set',
|
|
199
|
+
fallback: 'provider default',
|
|
200
|
+
save: 'Save',
|
|
201
|
+
saving: 'saving...',
|
|
202
|
+
saved: 'saved',
|
|
203
|
+
loading: 'loading...',
|
|
204
|
+
discard: 'Discard',
|
|
205
|
+
cliNote: 'This engine signs in through its own CLI: no key, no endpoint.',
|
|
206
|
+
autoTitle: 'Auto mode',
|
|
207
|
+
autoHint: 'Reuse the vision engines already on this machine.',
|
|
208
|
+
found: 'found',
|
|
209
|
+
notLoggedIn: 'found, not signed in',
|
|
210
|
+
notFound: 'not on this machine',
|
|
211
|
+
envSourced:
|
|
212
|
+
'These come from environment variables. Saving copies them into the config file, which then becomes this engine’s only source.',
|
|
213
|
+
},
|
|
214
|
+
zh: {
|
|
215
|
+
title: '视觉引擎(ModLens)',
|
|
216
|
+
subtitle: '视觉引擎提供商配置。',
|
|
217
|
+
openConfig: '打开配置文件',
|
|
218
|
+
automatic: '自动(不固定,由故障转移链决定)',
|
|
219
|
+
pickToConfigure: '在上面选一个引擎,才能配置它的密钥和地址。',
|
|
220
|
+
engine: '引擎',
|
|
221
|
+
apiKey: 'API 密钥',
|
|
222
|
+
baseUrl: '接口地址',
|
|
223
|
+
model: '模型',
|
|
224
|
+
stored: '已保存,留空即不改动',
|
|
225
|
+
unset: '未设置',
|
|
226
|
+
fallback: '使用该引擎默认值',
|
|
227
|
+
save: '保存',
|
|
228
|
+
saving: '保存中…',
|
|
229
|
+
saved: '已保存',
|
|
230
|
+
loading: '加载中…',
|
|
231
|
+
discard: '放弃修改',
|
|
232
|
+
cliNote: '该引擎通过自己的 CLI 登录,无需密钥和接口地址。',
|
|
233
|
+
autoTitle: 'auto 模式',
|
|
234
|
+
autoHint: '自动复用本机已有视觉引擎。',
|
|
235
|
+
found: '已找到',
|
|
236
|
+
notLoggedIn: '已找到,未登录',
|
|
237
|
+
notFound: '本机没有',
|
|
238
|
+
envSourced: '这些值来自环境变量。保存会把它们写进配置文件,此后该引擎只认配置文件。',
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function labels() {
|
|
243
|
+
var lang = (document.documentElement.lang || navigator.language || 'en').toLowerCase()
|
|
244
|
+
return lang.indexOf('zh') === 0 ? TEXT.zh : TEXT.en
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// The next draft when the engine changes or a summary arrives. The three
|
|
248
|
+
// engine fields belong to the newly selected engine; the reuse grants are
|
|
249
|
+
// the user's pending answers and survive an engine switch, since granting
|
|
250
|
+
// codex has nothing to do with which engine reads the images.
|
|
251
|
+
function nextDraft(summary, provider, keepReuse) {
|
|
252
|
+
// provider '' is its own answer: not pinned, the failover chain
|
|
253
|
+
// decides. There is then no single engine whose key belongs in these
|
|
254
|
+
// fields, so they stay empty and the card says how to get them back.
|
|
255
|
+
var engine = summary.engines[provider] || { baseUrl: '', model: '' }
|
|
256
|
+
return {
|
|
257
|
+
provider: provider,
|
|
258
|
+
apiKey: '',
|
|
259
|
+
baseUrl: engine.baseUrl,
|
|
260
|
+
model: engine.model,
|
|
261
|
+
reuse: Object.assign({}, keepReuse || summary.reuse),
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// What one save is actually about. The pin travels only when the select
|
|
266
|
+
// moved; the engine fields only when they were edited. A save that always
|
|
267
|
+
// carried both pinned an engine nobody chose and wrote the values the
|
|
268
|
+
// card loaded back over whatever the file holds now.
|
|
269
|
+
function savePayload(summary, draft) {
|
|
270
|
+
var payload = { reuse: {} }
|
|
271
|
+
REUSE.forEach((name) => {
|
|
272
|
+
if (draft.reuse[name] !== summary.reuse[name]) {
|
|
273
|
+
payload.reuse[name] = draft.reuse[name]
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
if (draft.provider !== summary.provider) {
|
|
277
|
+
payload.provider = draft.provider
|
|
278
|
+
}
|
|
279
|
+
var pristine = nextDraft(summary, draft.provider, draft.reuse)
|
|
280
|
+
var engineEdited = draft.apiKey !== '' || draft.baseUrl !== pristine.baseUrl || draft.model !== pristine.model
|
|
281
|
+
if (draft.provider !== '' && engineEdited) {
|
|
282
|
+
payload.engine = draft.provider
|
|
283
|
+
payload.apiKey = draft.apiKey
|
|
284
|
+
payload.baseUrl = draft.baseUrl
|
|
285
|
+
payload.model = draft.model
|
|
286
|
+
}
|
|
287
|
+
return payload
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function ConfigCard(react, ui) {
|
|
291
|
+
var h = react.createElement
|
|
292
|
+
var Input = ui.Input
|
|
293
|
+
|
|
294
|
+
// The chrome is the native plugin card's, value for value (border,
|
|
295
|
+
// layer backgrounds, 12px radius, header row with a rotating chevron,
|
|
296
|
+
// footer with discard ghost + save primary), so this card reads as a
|
|
297
|
+
// sibling of the built-in three rather than a lodger.
|
|
298
|
+
var chevron = (open) =>
|
|
299
|
+
h(
|
|
300
|
+
'svg',
|
|
301
|
+
{
|
|
302
|
+
width: 16,
|
|
303
|
+
height: 16,
|
|
304
|
+
viewBox: '0 0 16 16',
|
|
305
|
+
style: {
|
|
306
|
+
color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))',
|
|
307
|
+
flex: 'none',
|
|
308
|
+
transition: 'transform .16s',
|
|
309
|
+
transform: open ? 'rotate(180deg)' : 'none',
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
h('path', {
|
|
313
|
+
d: 'M4 6l4 4 4-4',
|
|
314
|
+
fill: 'none',
|
|
315
|
+
stroke: 'currentColor',
|
|
316
|
+
strokeWidth: 1.5,
|
|
317
|
+
strokeLinecap: 'round',
|
|
318
|
+
strokeLinejoin: 'round',
|
|
319
|
+
}),
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
return function ModlensCard() {
|
|
323
|
+
var t = labels()
|
|
324
|
+
var openState = react.useState(false)
|
|
325
|
+
var summaryState = react.useState(null)
|
|
326
|
+
var draftState = react.useState(null)
|
|
327
|
+
var noteState = react.useState('')
|
|
328
|
+
var open = openState[0]
|
|
329
|
+
var summary = summaryState[0]
|
|
330
|
+
var draft = draftState[0]
|
|
331
|
+
var note = noteState[0]
|
|
332
|
+
|
|
333
|
+
var seed = (next, provider, keepReuse) => nextDraft(next, provider, keepReuse)
|
|
334
|
+
|
|
335
|
+
var load = react.useCallback(() => {
|
|
336
|
+
// discover: the self-check probing which local harnesses exist to
|
|
337
|
+
// be borrowed. Paid once per expand, cached host-side.
|
|
338
|
+
fetch('/modlens/config?discover=1')
|
|
339
|
+
.then((r) =>
|
|
340
|
+
r.json().then((body) => {
|
|
341
|
+
if (!r.ok) throw new Error(body.error || 'load failed')
|
|
342
|
+
return body
|
|
343
|
+
}),
|
|
344
|
+
)
|
|
345
|
+
.then((next) => {
|
|
346
|
+
summaryState[1](next)
|
|
347
|
+
draftState[1](seed(next, next.provider))
|
|
348
|
+
noteState[1]('')
|
|
349
|
+
})
|
|
350
|
+
.catch((error) => {
|
|
351
|
+
noteState[1](String(error.message ? error.message : error))
|
|
352
|
+
})
|
|
353
|
+
}, [])
|
|
354
|
+
|
|
355
|
+
react.useEffect(() => {
|
|
356
|
+
if (open && summary === null) load()
|
|
357
|
+
}, [open, summary, load])
|
|
358
|
+
|
|
359
|
+
// A row wrapping ONE control is a label, which names that control. A
|
|
360
|
+
// row wrapping a set of them must not be: the label becomes the first
|
|
361
|
+
// checkbox's accessible name and swallows the whole section's prose.
|
|
362
|
+
// Those rows are a named group instead.
|
|
363
|
+
var fieldRow = (label, control, key, groupName) =>
|
|
364
|
+
h(
|
|
365
|
+
groupName ? 'div' : 'label',
|
|
366
|
+
{
|
|
367
|
+
key: key,
|
|
368
|
+
role: groupName ? 'group' : undefined,
|
|
369
|
+
'aria-label': groupName || undefined,
|
|
370
|
+
style: {
|
|
371
|
+
display: 'flex',
|
|
372
|
+
flexDirection: 'column',
|
|
373
|
+
gap: '6px',
|
|
374
|
+
padding: '12px 0',
|
|
375
|
+
borderTop: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35))',
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
h('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary, inherit)' } }, label),
|
|
379
|
+
control,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
var body = null
|
|
383
|
+
if (open) {
|
|
384
|
+
if (summary === null || draft === null) {
|
|
385
|
+
body = h(
|
|
386
|
+
'div',
|
|
387
|
+
{
|
|
388
|
+
style: {
|
|
389
|
+
padding: '12px 0',
|
|
390
|
+
color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))',
|
|
391
|
+
fontSize: '13px',
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
note || t.loading,
|
|
395
|
+
)
|
|
396
|
+
} else {
|
|
397
|
+
var keyless = (summary.keyless || []).indexOf(draft.provider) >= 0
|
|
398
|
+
var current = summary.engines[draft.provider] || { hasKey: false }
|
|
399
|
+
var pristine = seed(summary, draft.provider)
|
|
400
|
+
var dirty =
|
|
401
|
+
draft.provider !== summary.provider ||
|
|
402
|
+
draft.apiKey !== '' ||
|
|
403
|
+
draft.baseUrl !== pristine.baseUrl ||
|
|
404
|
+
draft.model !== pristine.model ||
|
|
405
|
+
REUSE.some((name) => draft.reuse[name] !== summary.reuse[name])
|
|
406
|
+
|
|
407
|
+
var set = (key, value) => {
|
|
408
|
+
var next = Object.assign({}, draft)
|
|
409
|
+
next[key] = value
|
|
410
|
+
draftState[1](next)
|
|
411
|
+
noteState[1]('')
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
var textField = (label, key, type, placeholder) =>
|
|
415
|
+
fieldRow(
|
|
416
|
+
label,
|
|
417
|
+
h(Input, {
|
|
418
|
+
type: type,
|
|
419
|
+
value: draft[key],
|
|
420
|
+
placeholder: placeholder,
|
|
421
|
+
onChange: (event) => {
|
|
422
|
+
set(key, event.target.value)
|
|
423
|
+
},
|
|
424
|
+
}),
|
|
425
|
+
key,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
// Auto mode: the probes say which harnesses exist on this
|
|
429
|
+
// machine. Found ones get a checkbox with their status; missing
|
|
430
|
+
// ones are named as absent so the list explains itself.
|
|
431
|
+
var probes = Array.isArray(summary.discovery) ? summary.discovery : null
|
|
432
|
+
// Being listed means being found: an absent harness is simply
|
|
433
|
+
// not shown, and only "not signed in" earns a note.
|
|
434
|
+
var autoRows = REUSE.filter((name) => {
|
|
435
|
+
if (!probes) return true
|
|
436
|
+
var probe = probes.find((candidate) => candidate.harness === name)
|
|
437
|
+
return probe ? probe.cliFound : false
|
|
438
|
+
}).map((name) => {
|
|
439
|
+
var probe = probes && probes.find((candidate) => candidate.harness === name)
|
|
440
|
+
return h(
|
|
441
|
+
'label',
|
|
442
|
+
{
|
|
443
|
+
key: name,
|
|
444
|
+
style: {
|
|
445
|
+
display: 'flex',
|
|
446
|
+
alignItems: 'center',
|
|
447
|
+
gap: '8px',
|
|
448
|
+
fontSize: '13px',
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
h('input', {
|
|
452
|
+
type: 'checkbox',
|
|
453
|
+
checked: Boolean(draft.reuse[name]),
|
|
454
|
+
onChange: (event) => {
|
|
455
|
+
var next = Object.assign({}, draft.reuse)
|
|
456
|
+
next[name] = event.target.checked
|
|
457
|
+
set('reuse', next)
|
|
458
|
+
},
|
|
459
|
+
}),
|
|
460
|
+
h('span', null, name),
|
|
461
|
+
probe && probe.loggedIn === false
|
|
462
|
+
? h(
|
|
463
|
+
'span',
|
|
464
|
+
{
|
|
465
|
+
style: {
|
|
466
|
+
color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))',
|
|
467
|
+
fontSize: '12px',
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
t.notLoggedIn,
|
|
471
|
+
)
|
|
472
|
+
: null,
|
|
473
|
+
)
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
body = h(
|
|
477
|
+
'div',
|
|
478
|
+
null,
|
|
479
|
+
fieldRow(
|
|
480
|
+
t.engine,
|
|
481
|
+
h(
|
|
482
|
+
'select',
|
|
483
|
+
{
|
|
484
|
+
value: draft.provider,
|
|
485
|
+
onChange: (event) => {
|
|
486
|
+
draftState[1](seed(summary, event.target.value, draft.reuse))
|
|
487
|
+
noteState[1]('')
|
|
488
|
+
},
|
|
489
|
+
style: {
|
|
490
|
+
appearance: 'none',
|
|
491
|
+
width: '100%',
|
|
492
|
+
padding: '8px 12px',
|
|
493
|
+
borderRadius: '8px',
|
|
494
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35))',
|
|
495
|
+
background: 'transparent',
|
|
496
|
+
color: 'inherit',
|
|
497
|
+
font: 'inherit',
|
|
498
|
+
fontSize: '13px',
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
[h('option', { key: '', value: '' }, t.automatic)].concat(
|
|
502
|
+
ENGINES.map((name) => h('option', { key: name, value: name }, name)),
|
|
503
|
+
),
|
|
504
|
+
),
|
|
505
|
+
'engine',
|
|
506
|
+
),
|
|
507
|
+
draft.provider === ''
|
|
508
|
+
? fieldRow(
|
|
509
|
+
t.apiKey,
|
|
510
|
+
h(
|
|
511
|
+
'div',
|
|
512
|
+
{
|
|
513
|
+
style: {
|
|
514
|
+
fontSize: '13px',
|
|
515
|
+
color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))',
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
t.pickToConfigure,
|
|
519
|
+
),
|
|
520
|
+
'unpinned',
|
|
521
|
+
)
|
|
522
|
+
: keyless
|
|
523
|
+
? fieldRow(
|
|
524
|
+
t.apiKey,
|
|
525
|
+
h(
|
|
526
|
+
'div',
|
|
527
|
+
{
|
|
528
|
+
style: { fontSize: '13px', color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))' },
|
|
529
|
+
},
|
|
530
|
+
t.cliNote,
|
|
531
|
+
),
|
|
532
|
+
'clinote',
|
|
533
|
+
)
|
|
534
|
+
: textField(t.apiKey, 'apiKey', 'password', current.hasKey ? t.stored : t.unset),
|
|
535
|
+
draft.provider === '' || keyless ? null : textField(t.baseUrl, 'baseUrl', 'text', t.fallback),
|
|
536
|
+
draft.provider === '' ? null : textField(t.model, 'model', 'text', t.fallback),
|
|
537
|
+
// Where these values are coming from, said once, because the
|
|
538
|
+
// first save moves them: an engine the file names takes its
|
|
539
|
+
// settings from the file alone.
|
|
540
|
+
draft.provider === '' || current.source !== 'env'
|
|
541
|
+
? null
|
|
542
|
+
: fieldRow(
|
|
543
|
+
'',
|
|
544
|
+
h(
|
|
545
|
+
'div',
|
|
546
|
+
{
|
|
547
|
+
style: {
|
|
548
|
+
fontSize: '13px',
|
|
549
|
+
color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))',
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
t.envSourced,
|
|
553
|
+
),
|
|
554
|
+
'envsourced',
|
|
555
|
+
),
|
|
556
|
+
fieldRow(
|
|
557
|
+
h(
|
|
558
|
+
'span',
|
|
559
|
+
null,
|
|
560
|
+
t.autoTitle,
|
|
561
|
+
h(
|
|
562
|
+
'span',
|
|
563
|
+
{
|
|
564
|
+
style: {
|
|
565
|
+
color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))',
|
|
566
|
+
fontWeight: 400,
|
|
567
|
+
marginLeft: '8px',
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
t.autoHint,
|
|
571
|
+
),
|
|
572
|
+
),
|
|
573
|
+
h(
|
|
574
|
+
'div',
|
|
575
|
+
{ style: { display: 'flex', flexWrap: 'wrap', gap: '10px 18px', paddingTop: '2px' } },
|
|
576
|
+
autoRows,
|
|
577
|
+
),
|
|
578
|
+
'auto',
|
|
579
|
+
t.autoTitle,
|
|
580
|
+
),
|
|
581
|
+
h(
|
|
582
|
+
'div',
|
|
583
|
+
{
|
|
584
|
+
key: 'footer',
|
|
585
|
+
style: {
|
|
586
|
+
borderTop: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35))',
|
|
587
|
+
display: 'flex',
|
|
588
|
+
justifyContent: 'flex-end',
|
|
589
|
+
alignItems: 'center',
|
|
590
|
+
gap: '8px',
|
|
591
|
+
padding: '12px 0 4px',
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
h(
|
|
595
|
+
'a',
|
|
596
|
+
{
|
|
597
|
+
href: '#',
|
|
598
|
+
onClick: (event) => {
|
|
599
|
+
event.preventDefault()
|
|
600
|
+
fetch('/modlens/config', {
|
|
601
|
+
method: 'POST',
|
|
602
|
+
headers: { 'content-type': 'application/json' },
|
|
603
|
+
body: JSON.stringify({ open: true }),
|
|
604
|
+
}).catch(() => {})
|
|
605
|
+
},
|
|
606
|
+
style: {
|
|
607
|
+
fontSize: '12px',
|
|
608
|
+
color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))',
|
|
609
|
+
textDecoration: 'underline',
|
|
610
|
+
textUnderlineOffset: '2px',
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
t.openConfig,
|
|
614
|
+
),
|
|
615
|
+
h(
|
|
616
|
+
'span',
|
|
617
|
+
{
|
|
618
|
+
role: 'status',
|
|
619
|
+
style: {
|
|
620
|
+
marginRight: 'auto',
|
|
621
|
+
marginLeft: '10px',
|
|
622
|
+
fontSize: '12px',
|
|
623
|
+
color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))',
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
note,
|
|
627
|
+
),
|
|
628
|
+
h(
|
|
629
|
+
'button',
|
|
630
|
+
{
|
|
631
|
+
type: 'button',
|
|
632
|
+
disabled: !dirty || note === t.saving,
|
|
633
|
+
onClick: () => {
|
|
634
|
+
draftState[1](seed(summary, summary.provider))
|
|
635
|
+
noteState[1]('')
|
|
636
|
+
},
|
|
637
|
+
style: {
|
|
638
|
+
appearance: 'none',
|
|
639
|
+
font: 'inherit',
|
|
640
|
+
fontSize: '13px',
|
|
641
|
+
lineHeight: 1.5,
|
|
642
|
+
cursor: dirty ? 'pointer' : 'default',
|
|
643
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35))',
|
|
644
|
+
borderRadius: '8px',
|
|
645
|
+
padding: '5px 14px',
|
|
646
|
+
background: 'none',
|
|
647
|
+
color: 'var(--dsw-alias-label-secondary, inherit)',
|
|
648
|
+
opacity: dirty ? 1 : 0.4,
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
t.discard,
|
|
652
|
+
),
|
|
653
|
+
h(
|
|
654
|
+
'button',
|
|
655
|
+
{
|
|
656
|
+
type: 'button',
|
|
657
|
+
disabled: !dirty || note === t.saving,
|
|
658
|
+
onClick: () => {
|
|
659
|
+
noteState[1](t.saving)
|
|
660
|
+
var payload = savePayload(summary, draft)
|
|
661
|
+
fetch('/modlens/config', {
|
|
662
|
+
method: 'POST',
|
|
663
|
+
headers: { 'content-type': 'application/json' },
|
|
664
|
+
body: JSON.stringify(payload),
|
|
665
|
+
})
|
|
666
|
+
.then((r) =>
|
|
667
|
+
r.json().then((payload) => {
|
|
668
|
+
if (!r.ok) throw new Error(payload.error || 'save failed')
|
|
669
|
+
return payload
|
|
670
|
+
}),
|
|
671
|
+
)
|
|
672
|
+
.then((next) => {
|
|
673
|
+
// The save response carries no discovery; keep the
|
|
674
|
+
// probes already on screen.
|
|
675
|
+
next.discovery = summary.discovery
|
|
676
|
+
summaryState[1](next)
|
|
677
|
+
draftState[1](seed(next, next.provider))
|
|
678
|
+
noteState[1](t.saved)
|
|
679
|
+
})
|
|
680
|
+
.catch((error) => {
|
|
681
|
+
noteState[1](String(error.message ? error.message : error))
|
|
682
|
+
})
|
|
683
|
+
},
|
|
684
|
+
style: {
|
|
685
|
+
appearance: 'none',
|
|
686
|
+
font: 'inherit',
|
|
687
|
+
fontSize: '13px',
|
|
688
|
+
lineHeight: 1.5,
|
|
689
|
+
cursor: dirty ? 'pointer' : 'default',
|
|
690
|
+
border: '1px solid transparent',
|
|
691
|
+
borderRadius: '8px',
|
|
692
|
+
padding: '5px 14px',
|
|
693
|
+
background: 'var(--dsw-alias-label-primary, currentColor)',
|
|
694
|
+
color: 'var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.05))',
|
|
695
|
+
opacity: dirty ? 1 : 0.4,
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
t.save,
|
|
699
|
+
),
|
|
700
|
+
),
|
|
701
|
+
)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return h(
|
|
706
|
+
'div',
|
|
707
|
+
{
|
|
708
|
+
style: {
|
|
709
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35))',
|
|
710
|
+
background: open
|
|
711
|
+
? 'var(--dsw-alias-bg-layer-2, rgba(127,127,127,0.10))'
|
|
712
|
+
: 'var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.05))',
|
|
713
|
+
borderRadius: '12px',
|
|
714
|
+
transition: 'border-color .16s, background .16s',
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
h(
|
|
718
|
+
'button',
|
|
719
|
+
{
|
|
720
|
+
type: 'button',
|
|
721
|
+
'aria-expanded': open,
|
|
722
|
+
onClick: () => {
|
|
723
|
+
openState[1](!open)
|
|
724
|
+
},
|
|
725
|
+
style: {
|
|
726
|
+
appearance: 'none',
|
|
727
|
+
width: '100%',
|
|
728
|
+
font: 'inherit',
|
|
729
|
+
color: 'inherit',
|
|
730
|
+
textAlign: 'left',
|
|
731
|
+
cursor: 'pointer',
|
|
732
|
+
background: 'none',
|
|
733
|
+
border: 0,
|
|
734
|
+
borderRadius: '12px',
|
|
735
|
+
display: 'flex',
|
|
736
|
+
alignItems: 'center',
|
|
737
|
+
gap: '12px',
|
|
738
|
+
padding: '14px 16px',
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
h(
|
|
742
|
+
'div',
|
|
743
|
+
{ style: { flex: 1, minWidth: 0 } },
|
|
744
|
+
h('div', { style: { fontSize: '14px', fontWeight: 600 } }, t.title),
|
|
745
|
+
h(
|
|
746
|
+
'div',
|
|
747
|
+
{
|
|
748
|
+
style: {
|
|
749
|
+
color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))',
|
|
750
|
+
fontSize: '13px',
|
|
751
|
+
lineHeight: 1.5,
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
t.subtitle,
|
|
755
|
+
),
|
|
756
|
+
),
|
|
757
|
+
chevron(open),
|
|
758
|
+
),
|
|
759
|
+
open ? h('div', { style: { margin: '0 16px', paddingBottom: '8px' } }, body) : null,
|
|
760
|
+
)
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function registerCard(ctx) {
|
|
765
|
+
// Reaching for an undeclared service throws in cordis, so the optional
|
|
766
|
+
// dependency rides a scoped ctx.inject: the closure runs where slots
|
|
767
|
+
// exists and never runs where it does not, exactly as the host half
|
|
768
|
+
// takes webServer.
|
|
769
|
+
if (typeof ctx.inject !== 'function') return
|
|
770
|
+
ctx.inject(['slots'], (scope) => {
|
|
771
|
+
// The card and its route live and die together: with the host route
|
|
772
|
+
// off (settingsCard: false, or no web profile) a card would only
|
|
773
|
+
// render an error, which is not what turning a feature off means.
|
|
774
|
+
// Any response at all proves the route exists; only a 404 or a
|
|
775
|
+
// network failure reads as absent.
|
|
776
|
+
fetch('/modlens/config')
|
|
777
|
+
.then((response) => {
|
|
778
|
+
if (response.status === 404) return
|
|
779
|
+
try {
|
|
780
|
+
mountCard(scope)
|
|
781
|
+
} catch (error) {
|
|
782
|
+
console.error('[modlens] settings card skipped: ' + error)
|
|
783
|
+
}
|
|
784
|
+
})
|
|
785
|
+
.catch(() => {})
|
|
786
|
+
})
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function mountCard(ctx) {
|
|
790
|
+
var react
|
|
791
|
+
try {
|
|
792
|
+
react = require('react')
|
|
793
|
+
} catch (error) {
|
|
794
|
+
console.error('[modlens] settings card skipped: ' + error)
|
|
795
|
+
return
|
|
796
|
+
}
|
|
797
|
+
var ui = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
798
|
+
var Card = ConfigCard(react, ui)
|
|
799
|
+
ctx.slots.inject('settings.plugin.item', function* () {
|
|
800
|
+
yield ctx.slots.register({ name: 'settings.plugin.item', id: 'modlens', order: 30 }, Card)
|
|
801
|
+
})
|
|
802
|
+
}
|
|
803
|
+
|
|
175
804
|
function apply(ctx) {
|
|
805
|
+
registerCard(ctx)
|
|
176
806
|
document.addEventListener('paste', onPaste, true)
|
|
177
807
|
document.addEventListener('focusin', onFocusIn, true)
|
|
178
808
|
// cordis effect: unregister on plugin disposal (HMR, profile reload).
|
|
@@ -188,6 +818,9 @@ window.__ModuleLoader__.load({
|
|
|
188
818
|
}
|
|
189
819
|
|
|
190
820
|
exports.apply = apply
|
|
821
|
+
// Exposed for the repo's tests only; not part of the plugin contract.
|
|
822
|
+
exports.__card = { nextDraft: nextDraft, savePayload: savePayload }
|
|
823
|
+
// `slots` is optional, so it is not required here: registerCard checks.
|
|
191
824
|
exports.inject = []
|
|
192
825
|
return module.exports
|
|
193
826
|
},
|