dsh-plugin-panel 0.0.0-stage → 0.1.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/client.js ADDED
@@ -0,0 +1,953 @@
1
+ window.__ModuleLoader__.load({
2
+ id: 'dsh-plugin-panel',
3
+ factory(require) {
4
+ const React = require('react')
5
+ const h = React.createElement
6
+ const PANEL_ID = 'dsh-plugins-ui'
7
+ const TAB_KIND = 'plugin-canvas'
8
+ const NS = 'dsh-plugins-ui'
9
+ const VERSION = 'v0.1.0'
10
+ const PREF_KEY = 'dsh.plugin-list.pref.v2'
11
+
12
+ const COPY = {
13
+ zh: {
14
+ 'tab.title': '插件管理',
15
+ 'guide.title': '插件管理',
16
+ 'guide.description': '查看并打开右侧栏插件。',
17
+ 'list.installed': '用户安装',
18
+ 'list.builtin': '系统内置',
19
+ 'list.all': '全部',
20
+ 'list.refresh': '刷新',
21
+ 'list.viewList': '列表',
22
+ 'list.viewCard': '卡片',
23
+ 'list.open': '打开',
24
+ 'list.placeRight': '右侧栏',
25
+ 'list.placeSettings': '设置',
26
+ 'list.placeLeft': '左侧菜单',
27
+ 'list.placeNone': '无界面入口',
28
+ 'list.pin': '置顶',
29
+ 'list.unpin': '取消置顶',
30
+ 'list.noPanel': '这个插件没有右侧栏页面。',
31
+ 'list.loading': '正在读取插件…',
32
+ 'list.empty': '没有匹配的插件。',
33
+ 'list.error': '读不到插件列表。',
34
+ 'list.retry': '重试',
35
+ 'list.unavailable': '这个配置档没有插件管理服务。',
36
+ 'list.running': '运行中',
37
+ 'list.stopped': '已停用',
38
+ 'list.failed': '失败',
39
+ 'list.install': '安装',
40
+ 'list.active': '活跃',
41
+ 'list.unknown': '未记录',
42
+
43
+ 'list.close': '关闭',
44
+ 'list.enable': '启用',
45
+ 'list.disable': '停用',
46
+ 'list.reveal': '打开所在位置',
47
+ 'list.revealFailed': '没有打开目录。',
48
+ 'list.revealRestart': '打开所在位置需要在重启 DeepSeek Harness 后可用。',
49
+ 'list.locked': '这项不能在这里开关。',
50
+ 'list.intro': '介绍',
51
+ 'list.package': '包名',
52
+ 'list.version': '版本',
53
+ 'list.source': '来源',
54
+ 'list.doc': '说明文档',
55
+
56
+ 'list.docLoading': '正在读取说明…',
57
+ 'list.docEmpty': '没有说明文档。',
58
+ 'list.module': '模块',
59
+ 'list.entry': '入口',
60
+ 'list.place': '位置',
61
+ 'list.placeInstallation': '系统安装',
62
+ 'list.placeProfile': '本机安装',
63
+ 'list.status': '状态',
64
+ 'list.flags': '标记',
65
+ 'list.official': '官方',
66
+ 'list.optional': '可选',
67
+ 'list.removable': '可卸载',
68
+ 'list.fixed': '不可卸载',
69
+ 'list.components': '同包组件',
70
+ 'list.license': '许可',
71
+ 'list.repo': '仓库',
72
+ 'list.engines': '引擎',
73
+ 'list.keywords': '关键词',
74
+ 'list.inject': '界面依赖',
75
+ },
76
+ en: {
77
+ 'tab.title': 'Plugin manager',
78
+ 'guide.title': 'Plugin manager',
79
+ 'guide.description': 'Browse and open right-sidebar plugins.',
80
+ 'list.installed': 'Installed',
81
+ 'list.builtin': 'Built-in',
82
+ 'list.all': 'All',
83
+ 'list.refresh': 'Refresh',
84
+ 'list.viewList': 'List',
85
+ 'list.viewCard': 'Cards',
86
+ 'list.open': 'Open',
87
+ 'list.placeRight': 'Right sidebar',
88
+ 'list.placeSettings': 'Settings',
89
+ 'list.placeLeft': 'Left menu',
90
+ 'list.placeNone': 'No UI entry',
91
+ 'list.pin': 'Pin',
92
+ 'list.unpin': 'Unpin',
93
+ 'list.noPanel': 'This plugin has no right-sidebar page.',
94
+ 'list.loading': 'Reading plugins…',
95
+ 'list.empty': 'No plugins match.',
96
+ 'list.error': 'Could not read the plugin list.',
97
+ 'list.retry': 'Retry',
98
+ 'list.unavailable': 'This profile has no plugin manager.',
99
+ 'list.running': 'Running',
100
+ 'list.stopped': 'Stopped',
101
+ 'list.failed': 'Failed',
102
+ 'list.install': 'Installed',
103
+ 'list.active': 'Active',
104
+ 'list.unknown': 'Unknown',
105
+
106
+ 'list.close': 'Close',
107
+ 'list.enable': 'Enable',
108
+ 'list.disable': 'Disable',
109
+ 'list.reveal': 'Show in Finder',
110
+ 'list.revealFailed': 'The folder did not open.',
111
+ 'list.revealRestart': 'Show in Finder becomes available after restarting DeepSeek Harness.',
112
+ 'list.locked': 'This row cannot be switched here.',
113
+ 'list.intro': 'Introduction',
114
+ 'list.package': 'Package',
115
+ 'list.version': 'Version',
116
+ 'list.source': 'Source',
117
+ 'list.doc': 'Notes',
118
+
119
+ 'list.docLoading': 'Reading notes…',
120
+ 'list.docEmpty': 'No notes.',
121
+ 'list.module': 'Module',
122
+ 'list.entry': 'Entry',
123
+ 'list.place': 'Location',
124
+ 'list.placeInstallation': 'Shipped with DSH',
125
+ 'list.placeProfile': 'Installed in this profile',
126
+ 'list.status': 'Status',
127
+ 'list.flags': 'Flags',
128
+ 'list.official': 'Official',
129
+ 'list.optional': 'Optional',
130
+ 'list.removable': 'Removable',
131
+ 'list.fixed': 'Not removable',
132
+ 'list.components': 'Components',
133
+ 'list.license': 'License',
134
+ 'list.repo': 'Repository',
135
+ 'list.engines': 'Engine',
136
+ 'list.keywords': 'Keywords',
137
+ 'list.inject': 'UI dependencies',
138
+ },
139
+ }
140
+
141
+ function textOf(t, key) {
142
+ try {
143
+ const value = typeof t === 'function' ? t(key) : key
144
+ return typeof value === 'string' && value ? value : key
145
+ } catch {
146
+ return key
147
+ }
148
+ }
149
+
150
+ function loadPref() {
151
+ try {
152
+ const parsed = JSON.parse(window.localStorage.getItem(PREF_KEY) || '{}')
153
+ return {
154
+ filter: ['installed', 'builtin', 'all'].includes(parsed.filter) ? parsed.filter : 'installed',
155
+ pins: Array.isArray(parsed.pins) ? parsed.pins.filter((id) => typeof id === 'string') : [],
156
+ layout: parsed.layout === 'card' ? 'card' : 'list',
157
+ }
158
+ } catch {
159
+ return { filter: 'installed', pins: [], layout: 'list' }
160
+ }
161
+ }
162
+
163
+ function savePref(pref) {
164
+ try { window.localStorage.setItem(PREF_KEY, JSON.stringify(pref)) } catch { /* ignore */ }
165
+ }
166
+
167
+ function resolveText(value, localeId) {
168
+ if (typeof value === 'string') return value
169
+ if (!value || typeof value !== 'object') return ''
170
+ const lang = String(localeId || 'zh').split('-')[0].toLowerCase()
171
+ return value[localeId] || value[lang] || value.zh || value.en || Object.values(value).find((item) => typeof item === 'string') || ''
172
+ }
173
+
174
+ function shortModule(name) {
175
+ const text = String(name || '')
176
+ if (!text) return ''
177
+ if (text.startsWith('file:') || text.includes('/node_modules/')) {
178
+ const parts = text.replace(/^file:\/\//, '').split('/')
179
+ const at = parts.lastIndexOf('node_modules')
180
+ if (at >= 0 && parts[at + 1]) {
181
+ return parts[at + 1].startsWith('@') && parts[at + 2] ? `${parts[at + 1]}/${parts[at + 2]}` : parts[at + 1]
182
+ }
183
+ }
184
+ return text
185
+ }
186
+
187
+ function pluginTitle(plugin, localeId) {
188
+ const title = resolveText(plugin.meta && plugin.meta.title, localeId)
189
+ if (title && !title.startsWith('file:') && !title.includes('/node_modules/')) return title
190
+ return shortModule(plugin.moduleName) || shortModule(plugin.entryId) || title || ''
191
+ }
192
+
193
+ // Keep aligned with catalog.js originOf. The browser bundle cannot import it.
194
+ function originOf(plugin) {
195
+ const name = String(plugin.moduleName || '')
196
+ if (name.startsWith('cordis:')) return 'system'
197
+ if (name.startsWith('@deepseek-ai/')) return 'builtin'
198
+ return 'installed'
199
+ }
200
+
201
+ function phaseKey(plugin) {
202
+ if (!plugin.enabled) return 'list.stopped'
203
+ if (plugin.fiberPhase === 'failed') return 'list.failed'
204
+ return 'list.running'
205
+ }
206
+
207
+ function safeIcon(src) {
208
+ if (typeof src !== 'string') return ''
209
+ const value = src.trim()
210
+ if (value.startsWith('data:image/') || value.startsWith('/') || value.startsWith('plugins/') || value.startsWith('https://') || value.startsWith('http://')) return value
211
+ return ''
212
+ }
213
+
214
+ function languageOf(id) {
215
+ const match = String(id || 'zh').match(/[a-z]{2,8}(?:-[a-z0-9]{1,8})*/i)
216
+ return match ? match[0] : 'zh'
217
+ }
218
+
219
+ function markdownOf(result) {
220
+ const value = result && result.ok === true ? result.value : result
221
+ const body = value && value.markdown ? value : (value && value.value) || value
222
+ const text = body && typeof body.markdown === 'string' ? body.markdown : ''
223
+ return text.replace(/^---[\s\S]*?---\s*/, '').trim().slice(0, 8000)
224
+ }
225
+
226
+ function inlineMarkdown(text) {
227
+ const nodes = []
228
+ const pattern = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\[[^\]]+\]\(https?:\/\/[^)\s]+\))/g
229
+ let last = 0
230
+ let match = pattern.exec(text)
231
+ while (match) {
232
+ if (match.index > last) nodes.push(text.slice(last, match.index))
233
+ const token = match[0]
234
+ if (token.startsWith('`')) nodes.push(h('code', null, token.slice(1, -1)))
235
+ else if (token.startsWith('**')) nodes.push(h('strong', null, token.slice(2, -2)))
236
+ else {
237
+ const link = token.match(/^\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)$/)
238
+ if (link) nodes.push(h('a', { href: link[2], target: '_blank', rel: 'noreferrer' }, link[1]))
239
+ }
240
+ last = match.index + token.length
241
+ match = pattern.exec(text)
242
+ }
243
+ if (last < text.length) nodes.push(text.slice(last))
244
+ return nodes
245
+ }
246
+
247
+ function directoryFromSource(source) {
248
+ const raw = String(source || '')
249
+ const at = raw.lastIndexOf('@')
250
+ const tail = at >= 0 ? raw.slice(at + 1) : raw
251
+ if (tail.startsWith('link:')) return tail.slice(5)
252
+ if (tail.startsWith('file:') && !/\.(tgz|tar\.gz)$/i.test(tail)) return tail.slice(5)
253
+ return ''
254
+ }
255
+
256
+ function detailNames(plugin, bundle) {
257
+ const names = []
258
+ const moduleName = String((plugin && plugin.moduleName) || '')
259
+ if (moduleName && !moduleName.includes('/') && !moduleName.startsWith('file:')) names.push(moduleName)
260
+ if (bundle && bundle.name && !names.includes(bundle.name)) names.push(bundle.name)
261
+ return names
262
+ }
263
+
264
+ function tableCells(line) {
265
+ return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((cell) => cell.trim())
266
+ }
267
+
268
+ function isTableRow(line) {
269
+ const trimmed = line.trim()
270
+ return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.includes('|')
271
+ }
272
+
273
+ function renderTable(rows) {
274
+ const parsed = rows.map(tableCells).filter((cells) => cells.some(Boolean))
275
+ const body = parsed.filter((cells) => !cells.every((cell) => /^:?-{3,}:?$/.test(cell)))
276
+ if (body.length < 2) return null
277
+ const head = body[0]
278
+ const rest = body.slice(1)
279
+ return h('div', { className: 'dsh-pl-table-wrap' },
280
+ h('table', null,
281
+ h('thead', null, h('tr', null, head.map((cell, index) => h('th', { key: index }, inlineMarkdown(cell))))),
282
+ h('tbody', null, rest.map((row, rowIndex) => h('tr', { key: rowIndex },
283
+ head.map((_, index) => h('td', { key: index }, inlineMarkdown(row[index] || ''))))))))
284
+ }
285
+
286
+ function markdownView(source) {
287
+ const lines = String(source || '').replace(/\r\n/g, '\n').split('\n')
288
+ const blocks = []
289
+ let list = null
290
+ let paragraph = []
291
+ let table = null
292
+ const flushParagraph = () => {
293
+ if (!paragraph.length) return
294
+ blocks.push(h('p', { key: `p-${blocks.length}` }, inlineMarkdown(paragraph.join(' '))))
295
+ paragraph = []
296
+ }
297
+ const flushList = () => {
298
+ if (!list) return
299
+ blocks.push(h('ul', { key: `ul-${blocks.length}` }, list.map((item, index) => h('li', { key: index }, inlineMarkdown(item)))))
300
+ list = null
301
+ }
302
+ const flushTable = () => {
303
+ if (!table) return
304
+ const node = renderTable(table)
305
+ if (node) blocks.push(h('div', { key: `table-${blocks.length}` }, node))
306
+ table = null
307
+ }
308
+ lines.forEach((line) => {
309
+ const trimmed = line.trim()
310
+ if (isTableRow(trimmed)) {
311
+ flushParagraph()
312
+ flushList()
313
+ table = table || []
314
+ table.push(trimmed)
315
+ return
316
+ }
317
+ flushTable()
318
+ if (!trimmed || trimmed.startsWith('![')) {
319
+ flushParagraph()
320
+ flushList()
321
+ return
322
+ }
323
+ const heading = /^(#{1,4})\s+(.*)$/.exec(trimmed)
324
+ if (heading) {
325
+ flushParagraph()
326
+ flushList()
327
+ blocks.push(h(heading[1].length <= 2 ? 'h3' : 'h4', { key: `h-${blocks.length}`, className: 'dsh-pl-md-h' }, inlineMarkdown(heading[2])))
328
+ return
329
+ }
330
+ const item = /^[-*]\s+(.*)$/.exec(trimmed)
331
+ if (item) {
332
+ flushParagraph()
333
+ list = list || []
334
+ list.push(item[1])
335
+ return
336
+ }
337
+ flushList()
338
+ paragraph.push(trimmed.replace(/^>\s?/, ''))
339
+ })
340
+ flushParagraph()
341
+ flushList()
342
+ flushTable()
343
+ return h('div', { className: 'dsh-pl-md' }, blocks)
344
+ }
345
+
346
+ function unwrapList(result) {
347
+ if (Array.isArray(result)) return result
348
+ if (result && result.ok === true && Array.isArray(result.value)) return result.value
349
+ throw new Error(result && result.error && result.error.message ? result.error.message : 'list failed')
350
+ }
351
+
352
+ function tabKindFor(plugin, entries, localeId) {
353
+ const names = new Set([
354
+ plugin.entryId,
355
+ plugin.moduleName,
356
+ shortModule(plugin.moduleName),
357
+ plugin.patchId,
358
+ String(plugin.entryId || '').replace(/^include:/, ''),
359
+ ].filter(Boolean))
360
+ const title = pluginTitle(plugin, localeId)
361
+ const hit = (entries || []).find((entry) => {
362
+ if (!entry || !entry.kind) return false
363
+ if (names.has(entry.id) || names.has(entry.providerId) || names.has(entry.kind)) return true
364
+ const label = typeof entry.title === 'function' ? entry.title() : entry.title
365
+ return Boolean(label && title && label === title)
366
+ })
367
+ return hit ? hit.kind : ''
368
+ }
369
+
370
+ function namesOf(plugin) {
371
+ return new Set([
372
+ plugin.entryId,
373
+ plugin.moduleName,
374
+ shortModule(plugin.moduleName),
375
+ plugin.patchId,
376
+ String(plugin.entryId || '').replace(/^include:/, ''),
377
+ ].filter(Boolean))
378
+ }
379
+
380
+ function placesFor(plugin, guide, seats, localeId) {
381
+ const names = namesOf(plugin)
382
+ const places = []
383
+ if (tabKindFor(plugin, guide, localeId)) places.push('right')
384
+ for (const seat of seats || []) {
385
+ const id = seat.id || seat.key
386
+ const owner = seat.registrant
387
+ if (names.has(id) || names.has(owner) || names.has(shortModule(owner))) {
388
+ if (!places.includes(seat.place)) places.push(seat.place)
389
+ }
390
+ }
391
+ return places
392
+ }
393
+
394
+ function matchBundle(plugin, bundles) {
395
+ const moduleName = String(plugin.moduleName || '')
396
+ const short = shortModule(moduleName)
397
+ return bundles.find((bundle) => {
398
+ if (!bundle) return false
399
+ if (bundle.name === moduleName || bundle.name === short) return true
400
+ const rows = Array.isArray(bundle.rows) ? bundle.rows : []
401
+ return rows.some((row) => row.moduleName === moduleName || row.entryId === plugin.entryId)
402
+ }) || null
403
+ }
404
+
405
+ function DefaultPluginIcon() {
406
+ return h('svg', {
407
+ className: 'dsh-pl-fallback',
408
+ width: 28,
409
+ height: 28,
410
+ viewBox: '0 0 32 32',
411
+ 'aria-hidden': true,
412
+ },
413
+ h('rect', { x: 1, y: 1, width: 30, height: 30, rx: 7, fill: 'var(--dsw-alias-bg-layer-2)', stroke: 'var(--dsw-alias-border-l2)' }),
414
+ h('rect', { x: 8, y: 8, width: 7, height: 7, rx: 1.6, fill: 'var(--dsw-alias-label-secondary)' }),
415
+ h('rect', { x: 17, y: 8, width: 7, height: 7, rx: 1.6, fill: 'var(--dsw-alias-label-secondary)', opacity: 0.45 }),
416
+ h('rect', { x: 8, y: 17, width: 7, height: 7, rx: 1.6, fill: 'var(--dsw-alias-label-secondary)', opacity: 0.45 }),
417
+ h('rect', { x: 17, y: 17, width: 7, height: 7, rx: 1.6, fill: 'var(--dsw-alias-label-secondary)' }))
418
+ }
419
+
420
+ function PluginIcon({ size }) {
421
+ const edge = size > 0 ? size : 16
422
+ return h('svg', {
423
+ width: edge, height: edge, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': true,
424
+ style: { display: 'block' },
425
+ },
426
+ h('rect', { x: 4, y: 5, width: 16, height: 3, rx: 1, fill: 'currentColor' }),
427
+ h('rect', { x: 4, y: 10.5, width: 16, height: 3, rx: 1, fill: 'currentColor' }),
428
+ h('rect', { x: 4, y: 16, width: 10, height: 3, rx: 1, fill: 'currentColor' }))
429
+ }
430
+
431
+ function ListView(props) {
432
+ const t = typeof props.t === 'function' ? props.t : (key) => key
433
+ const listPlugins = props.listPlugins
434
+ const listBundles = props.listBundles
435
+ const readBundleDetail = props.readBundleDetail
436
+ const loadFacts = props.loadFacts
437
+ const setPluginEnabled = props.setPluginEnabled
438
+ const listGuide = props.listGuide
439
+ const openPanel = props.openPanel
440
+ const pref = React.useRef(loadPref())
441
+ const [filter, setFilter] = React.useState(pref.current.filter)
442
+ const [pins, setPins] = React.useState(pref.current.pins || [])
443
+ const [view, setView] = React.useState(pref.current.layout === 'card' ? 'card' : 'list')
444
+ const [plugins, setPlugins] = React.useState([])
445
+ const [bundles, setBundles] = React.useState([])
446
+ const [status, setStatus] = React.useState(listPlugins ? 'loading' : 'unavailable')
447
+ const [error, setError] = React.useState('')
448
+ const [selectedId, setSelectedId] = React.useState('')
449
+ const [readme, setReadme] = React.useState('')
450
+ const [readmeStatus, setReadmeStatus] = React.useState('')
451
+ const [facts, setFacts] = React.useState(null)
452
+ const [revealNote, setRevealNote] = React.useState('')
453
+ const [guide, setGuide] = React.useState(() => typeof listGuide === 'function' ? listGuide() : [])
454
+ const [seats, setSeats] = React.useState(() => typeof props.listSeats === 'function' ? props.listSeats() : [])
455
+ const [localeId, setLocaleId] = React.useState('zh')
456
+
457
+ function remember(next) {
458
+ pref.current = { ...pref.current, ...next }
459
+ savePref(pref.current)
460
+ }
461
+
462
+ const load = React.useCallback(async () => {
463
+ if (!listPlugins) {
464
+ setStatus('unavailable')
465
+ return
466
+ }
467
+ setStatus((current) => (current === 'ready' ? 'ready' : 'loading'))
468
+ setError('')
469
+ try {
470
+ const list = unwrapList(await listPlugins()).filter((item) => item && item.entryId && originOf(item) !== 'system')
471
+ setPlugins(list)
472
+ setStatus('ready')
473
+ if (typeof listBundles === 'function') {
474
+ try {
475
+ const bundleResult = unwrapList(await listBundles())
476
+ setBundles(Array.isArray(bundleResult) ? bundleResult : [])
477
+ } catch { /* package facts stay empty; the list itself still renders */ }
478
+ }
479
+ } catch (failure) {
480
+ setError(failure instanceof Error ? failure.message : textOf(t, 'list.error'))
481
+ setStatus((current) => (current === 'ready' ? 'ready' : 'error'))
482
+ }
483
+ }, [listPlugins, listBundles, t])
484
+
485
+ React.useEffect(() => { void load() }, [load])
486
+ React.useEffect(() => {
487
+ if (typeof listGuide !== 'function') return undefined
488
+ const refresh = () => setGuide(listGuide() || [])
489
+ refresh()
490
+ return typeof props.subscribeGuide === 'function' ? props.subscribeGuide(refresh) : undefined
491
+ }, [listGuide, props.subscribeGuide])
492
+ React.useEffect(() => {
493
+ if (typeof props.listSeats !== 'function') return undefined
494
+ setSeats(props.listSeats() || [])
495
+ return typeof props.subscribeSeats === 'function' ? props.subscribeSeats(() => setSeats(props.listSeats() || [])) : undefined
496
+ }, [props.listSeats, props.subscribeSeats])
497
+ React.useEffect(() => {
498
+ if (typeof props.subscribeLocale !== 'function') return undefined
499
+ const read = () => {
500
+ try {
501
+ const snap = props.readLocale()
502
+ return snap && (snap.id || snap.locale) || 'zh'
503
+ } catch {
504
+ return 'zh'
505
+ }
506
+ }
507
+ setLocaleId(read())
508
+ return props.subscribeLocale(() => setLocaleId(read()))
509
+ }, [props.subscribeLocale, props.readLocale])
510
+
511
+ React.useEffect(() => {
512
+ if (!selectedId) {
513
+ setReadme('')
514
+ setReadmeStatus('')
515
+ return undefined
516
+ }
517
+ const plugin = plugins.find((item) => item.entryId === selectedId)
518
+ const bundle = plugin ? matchBundle(plugin, bundles) : null
519
+ const candidates = plugin ? detailNames(plugin, bundle) : []
520
+ if (!candidates.length) {
521
+ setReadme('')
522
+ setReadmeStatus('empty')
523
+ return undefined
524
+ }
525
+ let cancelled = false
526
+ setReadmeStatus('loading')
527
+ const run = async () => {
528
+ for (const name of candidates) {
529
+ if (cancelled) return
530
+ try {
531
+ const result = await readBundleDetail(name, languageOf(localeId))
532
+ if (cancelled) return
533
+ const markdown = markdownOf(result)
534
+ if (markdown) {
535
+ setReadme(markdown)
536
+ setReadmeStatus('ready')
537
+ return
538
+ }
539
+ } catch { /* the next candidate may still resolve */ }
540
+ }
541
+ if (!cancelled) {
542
+ setReadme('')
543
+ setReadmeStatus('empty')
544
+ }
545
+ }
546
+ void run()
547
+ return () => { cancelled = true }
548
+ }, [selectedId, plugins, bundles, localeId, readBundleDetail])
549
+
550
+ React.useEffect(() => {
551
+ if (!selectedId || typeof loadFacts !== 'function') {
552
+ setFacts(null)
553
+ return undefined
554
+ }
555
+ const plugin = plugins.find((item) => item.entryId === selectedId)
556
+ if (!plugin) return undefined
557
+ let cancelled = false
558
+ loadFacts({
559
+ moduleName: plugin.moduleName || '',
560
+ source: (matchBundle(plugin, bundles) || {}).source || '',
561
+ }).then((result) => {
562
+ if (cancelled) return
563
+ const value = result && result.ok === true ? result.value : result
564
+ setFacts(value && (value.document || value.manifest) ? value : null)
565
+ }).catch(() => { if (!cancelled) setFacts(null) })
566
+ return () => { cancelled = true }
567
+ }, [selectedId, plugins, bundles, loadFacts])
568
+
569
+ const visible = React.useMemo(() => {
570
+ const rows = plugins.filter((plugin) => {
571
+ const origin = originOf(plugin)
572
+ if (filter === 'installed') return origin === 'installed'
573
+ if (filter === 'builtin') return origin === 'builtin'
574
+ return true
575
+ })
576
+ const rank = (plugin) => {
577
+ const places = placesFor(plugin, guide, seats, localeId)
578
+ if (places.includes('right')) return 0
579
+ if (places.includes('settings')) return 1
580
+ if (places.includes('left')) return 2
581
+ return 3
582
+ }
583
+ const pinAt = (plugin) => {
584
+ const index = pins.indexOf(plugin.entryId)
585
+ return index < 0 ? Number.POSITIVE_INFINITY : index
586
+ }
587
+ rows.sort((a, b) => pinAt(a) - pinAt(b) || rank(a) - rank(b) || pluginTitle(a, localeId).localeCompare(pluginTitle(b, localeId), 'zh'))
588
+ return rows
589
+ }, [plugins, filter, localeId, guide, seats, pins])
590
+
591
+ const selected = plugins.find((item) => item.entryId === selectedId) || null
592
+ const selectedBundle = selected ? matchBundle(selected, bundles) : null
593
+
594
+ function fact(label, value) {
595
+ if (!value) return null
596
+ return h('div', { className: 'dsh-pl-fact' },
597
+ h('span', { className: 'dsh-pl-fact-label' }, label),
598
+ h('span', { className: 'dsh-pl-fact-value' }, value))
599
+ }
600
+
601
+ function revealHint(text) {
602
+ const raw = String(text || '')
603
+ if (/Unknown detail method|invalid server-response|unaddressable|not found/i.test(raw)) return textOf(t, 'list.revealRestart')
604
+ return raw || textOf(t, 'list.revealFailed')
605
+ }
606
+
607
+ function detailAside(plugin) {
608
+ const title = pluginTitle(plugin, localeId)
609
+ const icon = safeIcon(plugin.meta && plugin.meta.icon)
610
+ const manifest = facts && facts.manifest
611
+ const intro = resolveText(plugin.meta && plugin.meta.description, localeId)
612
+ || (manifest && manifest.summary)
613
+ || (selectedBundle && selectedBundle.description)
614
+ || ''
615
+ const place = selectedBundle && selectedBundle.availability === 'profile'
616
+ ? textOf(t, 'list.placeProfile')
617
+ : selectedBundle && selectedBundle.availability === 'installation'
618
+ ? textOf(t, 'list.placeInstallation')
619
+ : ''
620
+ const source = (selectedBundle && selectedBundle.source) || place
621
+ const flags = [
622
+ selectedBundle && selectedBundle.official ? textOf(t, 'list.official') : '',
623
+ selectedBundle && selectedBundle.optional ? textOf(t, 'list.optional') : '',
624
+ selectedBundle ? (selectedBundle.removable ? textOf(t, 'list.removable') : textOf(t, 'list.fixed')) : '',
625
+ ].filter(Boolean).join(' · ')
626
+ const rows = selectedBundle && Array.isArray(selectedBundle.rows) ? selectedBundle.rows : []
627
+ const siblings = rows
628
+ .filter((row) => row && row.entryId !== plugin.entryId)
629
+ .slice(0, 6)
630
+ .map((row) => shortModule(row.moduleName) || row.rowId)
631
+ .filter(Boolean)
632
+ const componentText = rows.length
633
+ ? `${rows.length}${siblings.length ? ` · ${siblings.join('、')}${rows.length > 7 ? '…' : ''}` : ''}`
634
+ : ''
635
+ return h('aside', { className: 'dsh-pl-detail' },
636
+ h('div', { className: 'dsh-pl-detail-head' },
637
+ icon ? h('img', { src: icon, alt: '' }) : h(DefaultPluginIcon),
638
+ h('strong', null, title),
639
+ h('button', { type: 'button', onClick: () => setSelectedId('') }, textOf(t, 'list.close'))),
640
+ h('div', { className: 'dsh-pl-detail-body' },
641
+ fact(textOf(t, 'list.intro'), intro || textOf(t, 'list.unknown')),
642
+ fact(textOf(t, 'list.module'), shortModule(plugin.moduleName)),
643
+ fact(textOf(t, 'list.entry'), plugin.entryId),
644
+ fact(textOf(t, 'list.status'), textOf(t, phaseKey(plugin))),
645
+ fact(textOf(t, 'list.package'), (selectedBundle && selectedBundle.name) || (manifest && manifest.name) || shortModule(plugin.moduleName)),
646
+ fact(textOf(t, 'list.version'), (selectedBundle && selectedBundle.version) || (manifest && manifest.version)),
647
+ fact(textOf(t, 'list.place'), place),
648
+ fact(textOf(t, 'list.source'), source),
649
+ fact(textOf(t, 'list.flags'), flags),
650
+ fact(textOf(t, 'list.components'), componentText),
651
+ fact(textOf(t, 'list.license'), manifest && manifest.license),
652
+ fact(textOf(t, 'list.repo'), manifest && manifest.repository),
653
+ fact(textOf(t, 'list.engines'), manifest && manifest.engines),
654
+ fact(textOf(t, 'list.keywords'), manifest && Array.isArray(manifest.keywords) ? manifest.keywords.join('、') : ''),
655
+ fact(textOf(t, 'list.inject'), manifest && Array.isArray(manifest.clientInject) ? manifest.clientInject.join('、') : ''),
656
+ h('section', { className: 'dsh-pl-doc-block' },
657
+ h('div', { className: 'dsh-pl-fact-label' }, textOf(t, 'list.doc')),
658
+ readmeStatus === 'loading'
659
+ ? h('p', null, textOf(t, 'list.docLoading'))
660
+ : (readme || (facts && facts.document))
661
+ ? markdownView(readme || facts.document)
662
+ : h('p', null, textOf(t, 'list.docEmpty'))),
663
+ h('div', { className: 'dsh-pl-actions' },
664
+ tabKindFor(plugin, guide, localeId) && typeof openPanel === 'function'
665
+ ? h('button', {
666
+ type: 'button',
667
+ onClick: () => openPanel(tabKindFor(plugin, guide, localeId)),
668
+ }, textOf(t, 'list.open'))
669
+ : null,
670
+ typeof props.revealPackage === 'function'
671
+ ? h('button', {
672
+ type: 'button',
673
+ onClick: () => {
674
+ setRevealNote('')
675
+ const source = selectedBundle && selectedBundle.source
676
+ Promise.resolve(props.revealPackage({ moduleName: plugin.moduleName || '', source: source || '' })).then((result) => {
677
+ if (result && result.ok === false) setRevealNote(revealHint(result.error && result.error.message))
678
+ }).catch((failure) => {
679
+ setRevealNote(revealHint(failure instanceof Error ? failure.message : ''))
680
+ })
681
+ },
682
+ }, textOf(t, 'list.reveal'))
683
+ : null,
684
+ plugin.patchId && plugin.readOnlyReason == null && setPluginEnabled
685
+ ? h('button', {
686
+ type: 'button',
687
+ onClick: () => { void setPluginEnabled(plugin.entryId, plugin.enabled !== true).then(() => load()) },
688
+ }, plugin.enabled ? textOf(t, 'list.disable') : textOf(t, 'list.enable'))
689
+ : plugin.readOnlyReason
690
+ ? h('span', { className: 'dsh-pl-note' }, String(plugin.readOnlyReason))
691
+ : h('span', { className: 'dsh-pl-note' }, textOf(t, 'list.locked')),
692
+ revealNote ? h('span', { className: 'dsh-pl-note' }, revealNote) : null)))
693
+ }
694
+
695
+ function togglePin(entryId) {
696
+ setPins((current) => {
697
+ const next = current.includes(entryId)
698
+ ? current.filter((id) => id !== entryId)
699
+ : [entryId, ...current]
700
+ remember({ pins: next })
701
+ return next
702
+ })
703
+ }
704
+
705
+ function chip(id, label, current, onPick) {
706
+ return h('button', {
707
+ type: 'button',
708
+ className: current === id ? 'is-on' : '',
709
+ 'aria-pressed': current === id,
710
+ onClick: () => onPick(id),
711
+ }, label)
712
+ }
713
+
714
+ return h('div', { className: 'dsh-pl' },
715
+ h('style', null, CSS),
716
+ h('header', { className: 'dsh-pl-bar' },
717
+ h('div', { className: 'dsh-pl-tools' },
718
+ h('div', { className: 'dsh-pl-seg', role: 'group', 'aria-label': 'filter' },
719
+ chip('installed', textOf(t, 'list.installed'), filter, (id) => { setFilter(id); remember({ filter: id }) }),
720
+ chip('builtin', textOf(t, 'list.builtin'), filter, (id) => { setFilter(id); remember({ filter: id }) }),
721
+ chip('all', textOf(t, 'list.all'), filter, (id) => { setFilter(id); remember({ filter: id }) })),
722
+ h('button', { type: 'button', onClick: () => { void load() } }, textOf(t, 'list.refresh')),
723
+ h('div', { className: 'dsh-pl-view', role: 'group', 'aria-label': 'view' },
724
+ chip('list', textOf(t, 'list.viewList'), view, (id) => { setView(id); remember({ layout: id }) }),
725
+ chip('card', textOf(t, 'list.viewCard'), view, (id) => { setView(id); remember({ layout: id }) })),
726
+ h('span', { className: 'dsh-pl-version' }, VERSION))),
727
+ status === 'loading' && plugins.length === 0
728
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.loading'))
729
+ : status === 'unavailable'
730
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.unavailable'))
731
+ : status === 'error' && plugins.length === 0
732
+ ? h('div', { className: 'dsh-pl-state' },
733
+ h('p', null, error || textOf(t, 'list.error')),
734
+ h('button', { type: 'button', onClick: () => { void load() } }, textOf(t, 'list.retry')))
735
+ : selected
736
+ ? detailAside(selected)
737
+ : h('div', { className: 'dsh-pl-body' },
738
+ h('div', { className: `dsh-pl-grid${view === 'card' ? ' is-card' : ''}` },
739
+ visible.length === 0
740
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.empty'))
741
+ : visible.map((plugin) => {
742
+ const title = pluginTitle(plugin, localeId)
743
+ const icon = safeIcon(plugin.meta && plugin.meta.icon)
744
+ const kind = tabKindFor(plugin, guide, localeId)
745
+ const pinned = pins.includes(plugin.entryId)
746
+ return h('div', {
747
+ key: plugin.entryId,
748
+ className: `dsh-pl-item${selectedId === plugin.entryId ? ' is-on' : ''}${pinned ? ' is-pinned' : ''}${plugin.fiberPhase === 'failed' ? ' is-failed' : ''}`,
749
+ role: 'button',
750
+ tabIndex: 0,
751
+ onClick: () => setSelectedId(plugin.entryId),
752
+ onKeyDown: (event) => {
753
+ if (event.key === 'Enter' || event.key === ' ') {
754
+ event.preventDefault()
755
+ setSelectedId(plugin.entryId)
756
+ }
757
+ },
758
+ },
759
+ icon
760
+ ? h('img', { src: icon, alt: '', draggable: false })
761
+ : h(DefaultPluginIcon),
762
+ h('span', { className: 'dsh-pl-copy' },
763
+ h('strong', null, title),
764
+ h('span', { className: 'dsh-pl-where' },
765
+ (placesFor(plugin, guide, seats, localeId).map((place) => textOf(t, place === 'right' ? 'list.placeRight' : place === 'settings' ? 'list.placeSettings' : 'list.placeLeft')).join(' · ')
766
+ || textOf(t, 'list.placeNone')))),
767
+ h('div', { className: 'dsh-pl-acts' },
768
+ h('button', {
769
+ type: 'button',
770
+ className: 'dsh-pl-pin',
771
+ 'aria-pressed': pinned,
772
+ onClick: (event) => {
773
+ event.stopPropagation()
774
+ togglePin(plugin.entryId)
775
+ },
776
+ }, pinned ? textOf(t, 'list.unpin') : textOf(t, 'list.pin')),
777
+ kind ? h('button', {
778
+ type: 'button',
779
+ className: 'dsh-pl-open',
780
+ onClick: (event) => {
781
+ event.stopPropagation()
782
+ if (typeof openPanel === 'function') openPanel(kind)
783
+ },
784
+ }, textOf(t, 'list.open')) : null))
785
+ }))))
786
+ }
787
+
788
+ const CSS = `
789
+ .dsh-pl, .dsh-pl * { box-sizing: border-box; }
790
+ .dsh-pl { display: flex; flex-direction: column; flex: 1 1 auto; height: 100%; min-height: 0; min-width: 0; padding-top: 8px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-base); font: 13px/1.4 inherit; }
791
+ .dsh-pl-bar { display: flex; flex: none; flex-direction: column; gap: 8px; margin: 0; padding: 12px 12px 10px; border-bottom: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-specific-sidebar-fill, var(--dsw-alias-bg-layer-1)); position: relative; z-index: 1; }
792
+ .dsh-pl-top { display: flex; align-items: center; justify-content: space-between; }
793
+ .dsh-pl-version, .dsh-pl-hint, .dsh-pl-item span { color: var(--dsw-alias-label-secondary); font-size: 11px; }
794
+ .dsh-pl-hint { margin: 0; }
795
+ .dsh-pl-tools { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; width: 100%; }
796
+ .dsh-pl-version { margin-left: 8px; }
797
+ .dsh-pl-view { display: flex; gap: 2px; margin-left: auto; padding: 2px; border-radius: 8px; background: var(--dsw-alias-bg-layer-2); }
798
+ .dsh-pl-view button { border: 0; border-radius: 6px; padding: 3px 8px; font-size: 11px; background: transparent; color: var(--dsw-alias-label-secondary); }
799
+ .dsh-pl-view button.is-on { background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-weight: 600; }
800
+ .dsh-pl-seg { display: flex; gap: 2px; padding: 3px; border: 0; border-radius: 10px; background: var(--dsw-alias-bg-layer-2); }
801
+ .dsh-pl button { font: inherit; font-size: 12px; line-height: 1.2; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-base); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; padding: 6px 12px; cursor: pointer; }
802
+ .dsh-pl-seg button { border: 0; border-radius: 7px; background: transparent; color: var(--dsw-alias-label-secondary); padding: 5px 12px; }
803
+ .dsh-pl-seg button.is-on { background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-weight: 600; box-shadow: none; }
804
+ .dsh-pl-item.is-on { border-color: var(--dsw-alias-brand-primary); background: var(--dsw-alias-bg-layer-2); box-shadow: inset 0 0 0 1px var(--dsw-alias-brand-primary); }
805
+ .dsh-pl-body { display: flex; flex: 1 1 auto; min-height: 0; min-width: 0; overflow: hidden; }
806
+ .dsh-pl-rows, .dsh-pl-grid { flex: 1 1 auto; min-width: 0; overflow: auto; padding: 8px; }
807
+ .dsh-pl-rows { display: flex; flex-direction: column; gap: 6px; }
808
+ .dsh-pl-grid { display: flex; flex-direction: column; gap: 10px; align-content: start; padding: 12px; }
809
+ .dsh-pl-grid.is-card { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
810
+ .dsh-pl-grid.is-card .dsh-pl-item { padding: 10px; gap: 8px; }
811
+ .dsh-pl-acts { display: flex; gap: 6px; align-items: center; margin-left: auto; flex: none; }
812
+ .dsh-pl-item { position: relative; }
813
+ .dsh-pl-grid.is-card .dsh-pl-acts { position: absolute; top: 8px; right: 8px; margin: 0; padding: 0; background: transparent; opacity: 0; pointer-events: none; transition: opacity .12s ease; }
814
+ .dsh-pl-grid.is-card .dsh-pl-acts .dsh-pl-pin, .dsh-pl-grid.is-card .dsh-pl-acts .dsh-pl-open { padding: 4px 8px; font-size: 11px; border: 0; background: var(--dsw-alias-bg-layer-2); }
815
+ .dsh-pl-grid.is-card .dsh-pl-item:hover .dsh-pl-acts, .dsh-pl-grid.is-card .dsh-pl-item:focus-within .dsh-pl-acts { opacity: 1; pointer-events: auto; }
816
+ .dsh-pl-item { display: flex; gap: 10px; align-items: center; width: 100%; min-width: 0; min-height: 64px; padding: 12px; text-align: left; border: 1px solid var(--dsw-alias-border-l2); border-radius: 16px; background: var(--dsw-alias-bg-layer-1); overflow: hidden; }
817
+ .dsh-pl-open, .dsh-pl-pin { margin-left: 0; flex: none; position: static; }
818
+ .dsh-pl-item.is-pinned { background: color-mix(in srgb, var(--dsw-alias-label-primary) 7%, var(--dsw-alias-bg-layer-1)); }
819
+ .dsh-pl-item img, .dsh-pl-fallback { width: 22px; height: 22px; border-radius: 6px; object-fit: cover; flex: none; }
820
+ .dsh-pl-copy { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 2px; }
821
+ .dsh-pl-copy strong { font-size: 14px; font-weight: 560; color: var(--dsw-alias-label-primary); white-space: normal; overflow-wrap: anywhere; }
822
+ .dsh-pl-where { color: var(--dsw-alias-label-secondary); font-size: 11px; }
823
+ .dsh-pl-item.is-failed { border-color: var(--dsw-alias-state-error-primary); }
824
+ .dsh-pl-state { margin: 16px; color: var(--dsw-alias-label-secondary); }
825
+ .dsh-pl-detail { flex: 1 1 auto; width: auto; max-width: none; min-width: 0; min-height: 0; overflow: auto; background: var(--dsw-alias-bg-base); padding: 12px 16px 20px; }
826
+ .dsh-pl-detail-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
827
+ .dsh-pl-detail-head strong { flex: 1 1 auto; min-width: 0; font-size: 16px; line-height: 1.35; overflow-wrap: anywhere; }
828
+ .dsh-pl-detail-head button { flex: none; white-space: nowrap; align-self: flex-start; }
829
+ .dsh-pl-detail-head img, .dsh-pl-detail-head .dsh-pl-fallback { width: 32px; height: 32px; }
830
+ .dsh-pl-fact { display: grid; grid-template-columns: 4.5em minmax(0, 1fr); column-gap: 12px; align-items: start; margin: 0 0 10px; }
831
+ .dsh-pl-fact-label { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.45; }
832
+ .dsh-pl-fact-value { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; }
833
+ .dsh-pl-detail h3 { margin: 14px 0 6px; font-size: 12px; color: var(--dsw-alias-label-secondary); }
834
+ .dsh-pl-detail p, .dsh-pl-doc { margin: 0 0 8px; font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; }
835
+ .dsh-pl-note { color: var(--dsw-alias-label-secondary); }
836
+ .dsh-pl-doc-block { margin-top: 12px; }
837
+ .dsh-pl-md h3, .dsh-pl-md-h { margin: 14px 0 6px; font-size: 14px; line-height: 1.4; font-weight: 650; color: var(--dsw-alias-label-primary); }
838
+ .dsh-pl-md h4 { margin: 10px 0 4px; font-size: 13px; font-weight: 650; }
839
+ .dsh-pl-md p, .dsh-pl-md li { margin: 0 0 8px; font-size: 13px; line-height: 1.55; }
840
+ .dsh-pl-md ul { margin: 0 0 10px; padding-left: 18px; }
841
+ .dsh-pl-md code { font-size: 12px; padding: 0 4px; border-radius: 4px; background: var(--dsw-alias-bg-layer-2); }
842
+ .dsh-pl-md a { color: var(--dsw-alias-brand-primary); }
843
+ .dsh-pl-table-wrap { margin: 8px 0 14px; overflow-x: auto; border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; }
844
+ .dsh-pl-md table { width: 100%; border-collapse: collapse; font-size: 13px; }
845
+ .dsh-pl-md th, .dsh-pl-md td { padding: 8px 10px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--dsw-alias-border-l1); }
846
+ .dsh-pl-md th { color: var(--dsw-alias-label-secondary); font-weight: 650; background: var(--dsw-alias-bg-layer-1); white-space: nowrap; }
847
+ .dsh-pl-md td:first-child { white-space: nowrap; }
848
+ .dsh-pl-md tr:last-child td { border-bottom: 0; }
849
+ .dsh-pl-actions { display: flex; gap: 8px; align-items: center; margin-top: 16px; }
850
+ `
851
+
852
+ return {
853
+ name: 'dsh-plugins-ui',
854
+ inject: ['slots', 'sidebarRightTabs', 'sidebarRight', 'layout', 'locale', 'remote', 'remote.pluginManager', 'remote.session', 'connection'],
855
+ apply(ctx) {
856
+ ctx.effect(() => ctx.locale.register(NS, COPY), 'plugin-list: dictionaries')
857
+ const locale = ctx.locale
858
+ const pluginManager = ctx.remote.pluginManager
859
+ const listPlugins = pluginManager.listPlugins.bind(pluginManager)
860
+ const listBundles = pluginManager.listBundles.bind(pluginManager)
861
+ const readBundleDetail = pluginManager.readBundleDetail.bind(pluginManager)
862
+ const setPluginEnabled = pluginManager.setPluginEnabled.bind(pluginManager)
863
+ const rpcCall = ctx.connection.rpc.call.bind(ctx.connection.rpc)
864
+ const loadFacts = (payload) => rpcCall('/dsh-plugins-ui-rpc', 'facts', payload || {})
865
+ const revealPackage = async (payload) => {
866
+ const input = payload && typeof payload === 'object' ? payload : {}
867
+ const dir = directoryFromSource(input.source)
868
+ const session = ctx.remote && ctx.remote.session
869
+ if (dir && session && typeof session.openWorkspacePath === 'function') {
870
+ try {
871
+ await session.openWorkspacePath({ path: dir, action: 'reveal' })
872
+ return { ok: true }
873
+ } catch { /* the host RPC below is the fallback */ }
874
+ }
875
+ try {
876
+ return await rpcCall('/dsh-plugins-ui-rpc', 'reveal', {
877
+ moduleName: input.moduleName || '',
878
+ source: input.source || '',
879
+ })
880
+ } catch {
881
+ return { ok: false, error: { message: dir ? '' : '找不到这个插件的目录。' } }
882
+ }
883
+ }
884
+ const listGuide = () => (typeof ctx.sidebarRightTabs.guide === 'function' ? ctx.sidebarRightTabs.guide() : [])
885
+ const subscribeGuide = (fn) => (typeof ctx.sidebarRightTabs.subscribe === 'function' ? ctx.sidebarRightTabs.subscribe(fn) : undefined)
886
+ const SEATS = [
887
+ ['sidebar.right.pane.tab', 'right'],
888
+ ['settings.section', 'settings'],
889
+ ['settings.plugins.tab', 'settings'],
890
+ ['settings.general.item', 'settings'],
891
+ ['sidebar.panellist', 'left'],
892
+ ['sidebar.footer.action', 'left'],
893
+ ]
894
+ const readSeat = (key, place) => {
895
+ if (!ctx.slots || typeof ctx.slots.entries !== 'function') return []
896
+ try {
897
+ return (ctx.slots.entries(key) || []).map((row) => ({
898
+ place,
899
+ registrant: row && (row.registrant || (row.options && row.options.registrant)) || '',
900
+ id: row && (row.id || row.key || (row.options && (row.options.id || row.options.key))) || '',
901
+ }))
902
+ } catch {
903
+ return []
904
+ }
905
+ }
906
+ const listSeats = () => SEATS.flatMap(([key, place]) => readSeat(key, place))
907
+ const subscribeSeats = (fn) => (ctx.slots && typeof ctx.slots.subscribe === 'function'
908
+ ? ctx.slots.subscribe('settings.section', fn)
909
+ : undefined)
910
+ const openPanel = (kind) => {
911
+ if (ctx.layout && typeof ctx.layout.openRightbar === 'function') ctx.layout.openRightbar(true, false)
912
+ ctx.sidebarRight.openTab(kind)
913
+ }
914
+ const t = locale.bind(NS)
915
+ const readLocale = () => (locale.getSnapshot ? locale.getSnapshot() : locale.getLocale())
916
+ const subscribeLocale = (fn) => locale.subscribe(fn)
917
+
918
+ ctx.effect(() => {
919
+ const definition = {
920
+ id: PANEL_ID,
921
+ priority: 'extension',
922
+ title: () => t('tab.title'),
923
+ icon: PluginIcon,
924
+ }
925
+ try {
926
+ return ctx.sidebarRightTabs.register({ ...definition, kind: 'guide' })
927
+ } catch {
928
+ return ctx.sidebarRightTabs.register({ ...definition, kind: TAB_KIND })
929
+ }
930
+ }, 'plugin-list: tab type')
931
+
932
+ function TabTitle() {
933
+ return t('tab.title')
934
+ }
935
+
936
+ ctx.effect(() => {
937
+ const stops = [PANEL_ID, TAB_KIND].map((key) => ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({
938
+ name: 'sidebar.right.pane.tab.title',
939
+ key,
940
+ }, TabTitle)))
941
+ return () => stops.forEach((stop) => stop())
942
+ }, 'plugin-list: tab title')
943
+
944
+ ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
945
+ name: 'sidebar.right.pane.tab',
946
+ key: PANEL_ID,
947
+ locale: NS,
948
+ inject: () => ({ listPlugins, listBundles, readBundleDetail, setPluginEnabled, loadFacts, revealPackage, listGuide, subscribeGuide, listSeats, subscribeSeats, openPanel, t, readLocale, subscribeLocale }),
949
+ }, ListView)), 'plugin-list: tab body')
950
+ },
951
+ }
952
+ },
953
+ })