dsh-plugin-panel 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js ADDED
@@ -0,0 +1,961 @@
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-plugin-panel'
7
+ const TAB_KIND = 'plugin-canvas'
8
+ const NS = 'dsh-plugin-panel'
9
+ const VERSION = 'v0.1.1'
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
+ const SELF_NAMES = new Set(['dsh-plugin-panel', 'dsh-plugins-ui', 'dsh-plugin-canvas'])
353
+
354
+ function isSelf(plugin) {
355
+ const entry = String((plugin && plugin.entryId) || '').replace(/^include:/, '')
356
+ const moduleName = String((plugin && plugin.moduleName) || '')
357
+ return SELF_NAMES.has(entry) || SELF_NAMES.has(moduleName) || SELF_NAMES.has(shortModule(moduleName))
358
+ }
359
+
360
+ function tabKindFor(plugin, entries, localeId) {
361
+ const names = new Set([
362
+ plugin.entryId,
363
+ plugin.moduleName,
364
+ shortModule(plugin.moduleName),
365
+ plugin.patchId,
366
+ String(plugin.entryId || '').replace(/^include:/, ''),
367
+ ].filter(Boolean))
368
+ const title = pluginTitle(plugin, localeId)
369
+ const hit = (entries || []).find((entry) => {
370
+ if (!entry || !entry.kind) return false
371
+ if (names.has(entry.id) || names.has(entry.providerId) || names.has(entry.kind)) return true
372
+ const label = typeof entry.title === 'function' ? entry.title() : entry.title
373
+ return Boolean(label && title && label === title)
374
+ })
375
+ return hit ? hit.kind : ''
376
+ }
377
+
378
+ function namesOf(plugin) {
379
+ return new Set([
380
+ plugin.entryId,
381
+ plugin.moduleName,
382
+ shortModule(plugin.moduleName),
383
+ plugin.patchId,
384
+ String(plugin.entryId || '').replace(/^include:/, ''),
385
+ ].filter(Boolean))
386
+ }
387
+
388
+ function placesFor(plugin, guide, seats, localeId) {
389
+ const names = namesOf(plugin)
390
+ const places = []
391
+ if (tabKindFor(plugin, guide, localeId) || isSelf(plugin)) places.push('right')
392
+ for (const seat of seats || []) {
393
+ const id = seat.id || seat.key
394
+ const owner = seat.registrant
395
+ if (names.has(id) || names.has(owner) || names.has(shortModule(owner))) {
396
+ if (!places.includes(seat.place)) places.push(seat.place)
397
+ }
398
+ }
399
+ return places
400
+ }
401
+
402
+ function matchBundle(plugin, bundles) {
403
+ const moduleName = String(plugin.moduleName || '')
404
+ const short = shortModule(moduleName)
405
+ return bundles.find((bundle) => {
406
+ if (!bundle) return false
407
+ if (bundle.name === moduleName || bundle.name === short) return true
408
+ const rows = Array.isArray(bundle.rows) ? bundle.rows : []
409
+ return rows.some((row) => row.moduleName === moduleName || row.entryId === plugin.entryId)
410
+ }) || null
411
+ }
412
+
413
+ function DefaultPluginIcon() {
414
+ return h('svg', {
415
+ className: 'dsh-pl-fallback',
416
+ width: 28,
417
+ height: 28,
418
+ viewBox: '0 0 32 32',
419
+ 'aria-hidden': true,
420
+ },
421
+ 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)' }),
422
+ h('rect', { x: 8, y: 8, width: 7, height: 7, rx: 1.6, fill: 'var(--dsw-alias-label-secondary)' }),
423
+ h('rect', { x: 17, y: 8, width: 7, height: 7, rx: 1.6, fill: 'var(--dsw-alias-label-secondary)', opacity: 0.45 }),
424
+ h('rect', { x: 8, y: 17, width: 7, height: 7, rx: 1.6, fill: 'var(--dsw-alias-label-secondary)', opacity: 0.45 }),
425
+ h('rect', { x: 17, y: 17, width: 7, height: 7, rx: 1.6, fill: 'var(--dsw-alias-label-secondary)' }))
426
+ }
427
+
428
+ function PluginIcon({ size }) {
429
+ const edge = size > 0 ? size : 16
430
+ return h('svg', {
431
+ width: edge, height: edge, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': true,
432
+ style: { display: 'block' },
433
+ },
434
+ h('rect', { x: 4, y: 5, width: 16, height: 3, rx: 1, fill: 'currentColor' }),
435
+ h('rect', { x: 4, y: 10.5, width: 16, height: 3, rx: 1, fill: 'currentColor' }),
436
+ h('rect', { x: 4, y: 16, width: 10, height: 3, rx: 1, fill: 'currentColor' }))
437
+ }
438
+
439
+ function ListView(props) {
440
+ const t = typeof props.t === 'function' ? props.t : (key) => key
441
+ const listPlugins = props.listPlugins
442
+ const listBundles = props.listBundles
443
+ const readBundleDetail = props.readBundleDetail
444
+ const loadFacts = props.loadFacts
445
+ const setPluginEnabled = props.setPluginEnabled
446
+ const listGuide = props.listGuide
447
+ const openPanel = props.openPanel
448
+ const pref = React.useRef(loadPref())
449
+ const [filter, setFilter] = React.useState(pref.current.filter)
450
+ const [pins, setPins] = React.useState(pref.current.pins || [])
451
+ const [view, setView] = React.useState(pref.current.layout === 'card' ? 'card' : 'list')
452
+ const [plugins, setPlugins] = React.useState([])
453
+ const [bundles, setBundles] = React.useState([])
454
+ const [status, setStatus] = React.useState(listPlugins ? 'loading' : 'unavailable')
455
+ const [error, setError] = React.useState('')
456
+ const [selectedId, setSelectedId] = React.useState('')
457
+ const [readme, setReadme] = React.useState('')
458
+ const [readmeStatus, setReadmeStatus] = React.useState('')
459
+ const [facts, setFacts] = React.useState(null)
460
+ const [revealNote, setRevealNote] = React.useState('')
461
+ const [guide, setGuide] = React.useState(() => typeof listGuide === 'function' ? listGuide() : [])
462
+ const [seats, setSeats] = React.useState(() => typeof props.listSeats === 'function' ? props.listSeats() : [])
463
+ const [localeId, setLocaleId] = React.useState('zh')
464
+
465
+ function remember(next) {
466
+ pref.current = { ...pref.current, ...next }
467
+ savePref(pref.current)
468
+ }
469
+
470
+ const load = React.useCallback(async () => {
471
+ if (!listPlugins) {
472
+ setStatus('unavailable')
473
+ return
474
+ }
475
+ setStatus((current) => (current === 'ready' ? 'ready' : 'loading'))
476
+ setError('')
477
+ try {
478
+ const list = unwrapList(await listPlugins()).filter((item) => item && item.entryId && originOf(item) !== 'system')
479
+ setPlugins(list)
480
+ setStatus('ready')
481
+ if (typeof listBundles === 'function') {
482
+ try {
483
+ const bundleResult = unwrapList(await listBundles())
484
+ setBundles(Array.isArray(bundleResult) ? bundleResult : [])
485
+ } catch { /* package facts stay empty; the list itself still renders */ }
486
+ }
487
+ } catch (failure) {
488
+ setError(failure instanceof Error ? failure.message : textOf(t, 'list.error'))
489
+ setStatus((current) => (current === 'ready' ? 'ready' : 'error'))
490
+ }
491
+ }, [listPlugins, listBundles, t])
492
+
493
+ React.useEffect(() => { void load() }, [load])
494
+ React.useEffect(() => {
495
+ if (typeof listGuide !== 'function') return undefined
496
+ const refresh = () => setGuide(listGuide() || [])
497
+ refresh()
498
+ return typeof props.subscribeGuide === 'function' ? props.subscribeGuide(refresh) : undefined
499
+ }, [listGuide, props.subscribeGuide])
500
+ React.useEffect(() => {
501
+ if (typeof props.listSeats !== 'function') return undefined
502
+ setSeats(props.listSeats() || [])
503
+ return typeof props.subscribeSeats === 'function' ? props.subscribeSeats(() => setSeats(props.listSeats() || [])) : undefined
504
+ }, [props.listSeats, props.subscribeSeats])
505
+ React.useEffect(() => {
506
+ if (typeof props.subscribeLocale !== 'function') return undefined
507
+ const read = () => {
508
+ try {
509
+ const snap = props.readLocale()
510
+ return snap && (snap.id || snap.locale) || 'zh'
511
+ } catch {
512
+ return 'zh'
513
+ }
514
+ }
515
+ setLocaleId(read())
516
+ return props.subscribeLocale(() => setLocaleId(read()))
517
+ }, [props.subscribeLocale, props.readLocale])
518
+
519
+ React.useEffect(() => {
520
+ if (!selectedId) {
521
+ setReadme('')
522
+ setReadmeStatus('')
523
+ return undefined
524
+ }
525
+ const plugin = plugins.find((item) => item.entryId === selectedId)
526
+ const bundle = plugin ? matchBundle(plugin, bundles) : null
527
+ const candidates = plugin ? detailNames(plugin, bundle) : []
528
+ if (!candidates.length) {
529
+ setReadme('')
530
+ setReadmeStatus('empty')
531
+ return undefined
532
+ }
533
+ let cancelled = false
534
+ setReadmeStatus('loading')
535
+ const run = async () => {
536
+ for (const name of candidates) {
537
+ if (cancelled) return
538
+ try {
539
+ const result = await readBundleDetail(name, languageOf(localeId))
540
+ if (cancelled) return
541
+ const markdown = markdownOf(result)
542
+ if (markdown) {
543
+ setReadme(markdown)
544
+ setReadmeStatus('ready')
545
+ return
546
+ }
547
+ } catch { /* the next candidate may still resolve */ }
548
+ }
549
+ if (!cancelled) {
550
+ setReadme('')
551
+ setReadmeStatus('empty')
552
+ }
553
+ }
554
+ void run()
555
+ return () => { cancelled = true }
556
+ }, [selectedId, plugins, bundles, localeId, readBundleDetail])
557
+
558
+ React.useEffect(() => {
559
+ if (!selectedId || typeof loadFacts !== 'function') {
560
+ setFacts(null)
561
+ return undefined
562
+ }
563
+ const plugin = plugins.find((item) => item.entryId === selectedId)
564
+ if (!plugin) return undefined
565
+ let cancelled = false
566
+ loadFacts({
567
+ moduleName: plugin.moduleName || '',
568
+ source: (matchBundle(plugin, bundles) || {}).source || '',
569
+ }).then((result) => {
570
+ if (cancelled) return
571
+ const value = result && result.ok === true ? result.value : result
572
+ setFacts(value && (value.document || value.manifest) ? value : null)
573
+ }).catch(() => { if (!cancelled) setFacts(null) })
574
+ return () => { cancelled = true }
575
+ }, [selectedId, plugins, bundles, loadFacts])
576
+
577
+ const visible = React.useMemo(() => {
578
+ const rows = plugins.filter((plugin) => {
579
+ const origin = originOf(plugin)
580
+ if (filter === 'installed') return origin === 'installed'
581
+ if (filter === 'builtin') return origin === 'builtin'
582
+ return true
583
+ })
584
+ const rank = (plugin) => {
585
+ const places = placesFor(plugin, guide, seats, localeId)
586
+ if (places.includes('right')) return 0
587
+ if (places.includes('settings')) return 1
588
+ if (places.includes('left')) return 2
589
+ return 3
590
+ }
591
+ const pinAt = (plugin) => {
592
+ const index = pins.indexOf(plugin.entryId)
593
+ return index < 0 ? Number.POSITIVE_INFINITY : index
594
+ }
595
+ rows.sort((a, b) => pinAt(a) - pinAt(b) || rank(a) - rank(b) || pluginTitle(a, localeId).localeCompare(pluginTitle(b, localeId), 'zh'))
596
+ return rows
597
+ }, [plugins, filter, localeId, guide, seats, pins])
598
+
599
+ const selected = plugins.find((item) => item.entryId === selectedId) || null
600
+ const selectedBundle = selected ? matchBundle(selected, bundles) : null
601
+
602
+ function fact(label, value) {
603
+ if (!value) return null
604
+ return h('div', { className: 'dsh-pl-fact' },
605
+ h('span', { className: 'dsh-pl-fact-label' }, label),
606
+ h('span', { className: 'dsh-pl-fact-value' }, value))
607
+ }
608
+
609
+ function revealHint(text) {
610
+ const raw = String(text || '')
611
+ if (/Unknown detail method|invalid server-response|unaddressable|not found/i.test(raw)) return textOf(t, 'list.revealRestart')
612
+ return raw || textOf(t, 'list.revealFailed')
613
+ }
614
+
615
+ function detailAside(plugin) {
616
+ const title = pluginTitle(plugin, localeId)
617
+ const icon = safeIcon(plugin.meta && plugin.meta.icon)
618
+ const manifest = facts && facts.manifest
619
+ const intro = resolveText(plugin.meta && plugin.meta.description, localeId)
620
+ || (manifest && manifest.summary)
621
+ || (selectedBundle && selectedBundle.description)
622
+ || ''
623
+ const place = selectedBundle && selectedBundle.availability === 'profile'
624
+ ? textOf(t, 'list.placeProfile')
625
+ : selectedBundle && selectedBundle.availability === 'installation'
626
+ ? textOf(t, 'list.placeInstallation')
627
+ : ''
628
+ const source = (selectedBundle && selectedBundle.source) || place
629
+ const flags = [
630
+ selectedBundle && selectedBundle.official ? textOf(t, 'list.official') : '',
631
+ selectedBundle && selectedBundle.optional ? textOf(t, 'list.optional') : '',
632
+ selectedBundle ? (selectedBundle.removable ? textOf(t, 'list.removable') : textOf(t, 'list.fixed')) : '',
633
+ ].filter(Boolean).join(' · ')
634
+ const rows = selectedBundle && Array.isArray(selectedBundle.rows) ? selectedBundle.rows : []
635
+ const siblings = rows
636
+ .filter((row) => row && row.entryId !== plugin.entryId)
637
+ .slice(0, 6)
638
+ .map((row) => shortModule(row.moduleName) || row.rowId)
639
+ .filter(Boolean)
640
+ const componentText = rows.length
641
+ ? `${rows.length}${siblings.length ? ` · ${siblings.join('、')}${rows.length > 7 ? '…' : ''}` : ''}`
642
+ : ''
643
+ return h('aside', { className: 'dsh-pl-detail' },
644
+ h('div', { className: 'dsh-pl-detail-head' },
645
+ icon ? h('img', { src: icon, alt: '' }) : h(DefaultPluginIcon),
646
+ h('strong', null, title),
647
+ h('button', { type: 'button', onClick: () => setSelectedId('') }, textOf(t, 'list.close'))),
648
+ h('div', { className: 'dsh-pl-detail-body' },
649
+ fact(textOf(t, 'list.intro'), intro || textOf(t, 'list.unknown')),
650
+ fact(textOf(t, 'list.module'), shortModule(plugin.moduleName)),
651
+ fact(textOf(t, 'list.entry'), plugin.entryId),
652
+ fact(textOf(t, 'list.status'), textOf(t, phaseKey(plugin))),
653
+ fact(textOf(t, 'list.package'), (selectedBundle && selectedBundle.name) || (manifest && manifest.name) || shortModule(plugin.moduleName)),
654
+ fact(textOf(t, 'list.version'), (selectedBundle && selectedBundle.version) || (manifest && manifest.version)),
655
+ fact(textOf(t, 'list.place'), place),
656
+ fact(textOf(t, 'list.source'), source),
657
+ fact(textOf(t, 'list.flags'), flags),
658
+ fact(textOf(t, 'list.components'), componentText),
659
+ fact(textOf(t, 'list.license'), manifest && manifest.license),
660
+ fact(textOf(t, 'list.repo'), manifest && manifest.repository),
661
+ fact(textOf(t, 'list.engines'), manifest && manifest.engines),
662
+ fact(textOf(t, 'list.keywords'), manifest && Array.isArray(manifest.keywords) ? manifest.keywords.join('、') : ''),
663
+ fact(textOf(t, 'list.inject'), manifest && Array.isArray(manifest.clientInject) ? manifest.clientInject.join('、') : ''),
664
+ h('section', { className: 'dsh-pl-doc-block' },
665
+ h('div', { className: 'dsh-pl-fact-label' }, textOf(t, 'list.doc')),
666
+ readmeStatus === 'loading'
667
+ ? h('p', null, textOf(t, 'list.docLoading'))
668
+ : (readme || (facts && facts.document))
669
+ ? markdownView(readme || facts.document)
670
+ : h('p', null, textOf(t, 'list.docEmpty'))),
671
+ h('div', { className: 'dsh-pl-actions' },
672
+ tabKindFor(plugin, guide, localeId) && typeof openPanel === 'function'
673
+ ? h('button', {
674
+ type: 'button',
675
+ onClick: () => openPanel(tabKindFor(plugin, guide, localeId)),
676
+ }, textOf(t, 'list.open'))
677
+ : null,
678
+ typeof props.revealPackage === 'function'
679
+ ? h('button', {
680
+ type: 'button',
681
+ onClick: () => {
682
+ setRevealNote('')
683
+ const source = selectedBundle && selectedBundle.source
684
+ Promise.resolve(props.revealPackage({ moduleName: plugin.moduleName || '', source: source || '' })).then((result) => {
685
+ if (result && result.ok === false) setRevealNote(revealHint(result.error && result.error.message))
686
+ }).catch((failure) => {
687
+ setRevealNote(revealHint(failure instanceof Error ? failure.message : ''))
688
+ })
689
+ },
690
+ }, textOf(t, 'list.reveal'))
691
+ : null,
692
+ plugin.patchId && plugin.readOnlyReason == null && setPluginEnabled
693
+ ? h('button', {
694
+ type: 'button',
695
+ onClick: () => { void setPluginEnabled(plugin.entryId, plugin.enabled !== true).then(() => load()) },
696
+ }, plugin.enabled ? textOf(t, 'list.disable') : textOf(t, 'list.enable'))
697
+ : plugin.readOnlyReason
698
+ ? h('span', { className: 'dsh-pl-note' }, String(plugin.readOnlyReason))
699
+ : h('span', { className: 'dsh-pl-note' }, textOf(t, 'list.locked')),
700
+ revealNote ? h('span', { className: 'dsh-pl-note' }, revealNote) : null)))
701
+ }
702
+
703
+ function togglePin(entryId) {
704
+ setPins((current) => {
705
+ const next = current.includes(entryId)
706
+ ? current.filter((id) => id !== entryId)
707
+ : [entryId, ...current]
708
+ remember({ pins: next })
709
+ return next
710
+ })
711
+ }
712
+
713
+ function chip(id, label, current, onPick) {
714
+ return h('button', {
715
+ type: 'button',
716
+ className: current === id ? 'is-on' : '',
717
+ 'aria-pressed': current === id,
718
+ onClick: () => onPick(id),
719
+ }, label)
720
+ }
721
+
722
+ return h('div', { className: 'dsh-pl' },
723
+ h('style', null, CSS),
724
+ h('header', { className: 'dsh-pl-bar' },
725
+ h('div', { className: 'dsh-pl-tools' },
726
+ h('div', { className: 'dsh-pl-seg', role: 'group', 'aria-label': 'filter' },
727
+ chip('installed', textOf(t, 'list.installed'), filter, (id) => { setFilter(id); remember({ filter: id }) }),
728
+ chip('builtin', textOf(t, 'list.builtin'), filter, (id) => { setFilter(id); remember({ filter: id }) }),
729
+ chip('all', textOf(t, 'list.all'), filter, (id) => { setFilter(id); remember({ filter: id }) })),
730
+ h('button', { type: 'button', onClick: () => { void load() } }, textOf(t, 'list.refresh')),
731
+ h('div', { className: 'dsh-pl-view', role: 'group', 'aria-label': 'view' },
732
+ chip('list', textOf(t, 'list.viewList'), view, (id) => { setView(id); remember({ layout: id }) }),
733
+ chip('card', textOf(t, 'list.viewCard'), view, (id) => { setView(id); remember({ layout: id }) })),
734
+ h('span', { className: 'dsh-pl-version' }, VERSION))),
735
+ status === 'loading' && plugins.length === 0
736
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.loading'))
737
+ : status === 'unavailable'
738
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.unavailable'))
739
+ : status === 'error' && plugins.length === 0
740
+ ? h('div', { className: 'dsh-pl-state' },
741
+ h('p', null, error || textOf(t, 'list.error')),
742
+ h('button', { type: 'button', onClick: () => { void load() } }, textOf(t, 'list.retry')))
743
+ : selected
744
+ ? detailAside(selected)
745
+ : h('div', { className: 'dsh-pl-body' },
746
+ h('div', { className: `dsh-pl-grid${view === 'card' ? ' is-card' : ''}` },
747
+ visible.length === 0
748
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.empty'))
749
+ : visible.map((plugin) => {
750
+ const title = pluginTitle(plugin, localeId)
751
+ const icon = safeIcon(plugin.meta && plugin.meta.icon)
752
+ const kind = tabKindFor(plugin, guide, localeId)
753
+ const pinned = pins.includes(plugin.entryId)
754
+ return h('div', {
755
+ key: plugin.entryId,
756
+ className: `dsh-pl-item${selectedId === plugin.entryId ? ' is-on' : ''}${pinned ? ' is-pinned' : ''}${plugin.fiberPhase === 'failed' ? ' is-failed' : ''}`,
757
+ role: 'button',
758
+ tabIndex: 0,
759
+ onClick: () => setSelectedId(plugin.entryId),
760
+ onKeyDown: (event) => {
761
+ if (event.key === 'Enter' || event.key === ' ') {
762
+ event.preventDefault()
763
+ setSelectedId(plugin.entryId)
764
+ }
765
+ },
766
+ },
767
+ icon
768
+ ? h('img', { src: icon, alt: '', draggable: false })
769
+ : h(DefaultPluginIcon),
770
+ h('span', { className: 'dsh-pl-copy' },
771
+ h('strong', null, title),
772
+ h('span', { className: 'dsh-pl-where' },
773
+ (placesFor(plugin, guide, seats, localeId).map((place) => textOf(t, place === 'right' ? 'list.placeRight' : place === 'settings' ? 'list.placeSettings' : 'list.placeLeft')).join(' · ')
774
+ || textOf(t, 'list.placeNone')))),
775
+ h('div', { className: 'dsh-pl-acts' },
776
+ h('button', {
777
+ type: 'button',
778
+ className: 'dsh-pl-pin',
779
+ 'aria-pressed': pinned,
780
+ onClick: (event) => {
781
+ event.stopPropagation()
782
+ togglePin(plugin.entryId)
783
+ },
784
+ }, pinned ? textOf(t, 'list.unpin') : textOf(t, 'list.pin')),
785
+ kind ? h('button', {
786
+ type: 'button',
787
+ className: 'dsh-pl-open',
788
+ onClick: (event) => {
789
+ event.stopPropagation()
790
+ if (typeof openPanel === 'function') openPanel(kind)
791
+ },
792
+ }, textOf(t, 'list.open')) : null))
793
+ }))))
794
+ }
795
+
796
+ const CSS = `
797
+ .dsh-pl, .dsh-pl * { box-sizing: border-box; }
798
+ .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; }
799
+ .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; }
800
+ .dsh-pl-top { display: flex; align-items: center; justify-content: space-between; }
801
+ .dsh-pl-version, .dsh-pl-hint, .dsh-pl-item span { color: var(--dsw-alias-label-secondary); font-size: 11px; }
802
+ .dsh-pl-hint { margin: 0; }
803
+ .dsh-pl-tools { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; width: 100%; }
804
+ .dsh-pl-version { margin-left: 8px; }
805
+ .dsh-pl-view { display: flex; gap: 2px; margin-left: auto; padding: 2px; border-radius: 8px; background: var(--dsw-alias-bg-layer-2); }
806
+ .dsh-pl-view button { border: 0; border-radius: 6px; padding: 3px 8px; font-size: 11px; background: transparent; color: var(--dsw-alias-label-secondary); }
807
+ .dsh-pl-view button.is-on { background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-weight: 600; }
808
+ .dsh-pl-seg { display: flex; gap: 2px; padding: 3px; border: 0; border-radius: 10px; background: var(--dsw-alias-bg-layer-2); }
809
+ .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; }
810
+ .dsh-pl-seg button { border: 0; border-radius: 7px; background: transparent; color: var(--dsw-alias-label-secondary); padding: 5px 12px; }
811
+ .dsh-pl-seg button.is-on { background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-weight: 600; box-shadow: none; }
812
+ .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); }
813
+ .dsh-pl-body { display: flex; flex: 1 1 auto; min-height: 0; min-width: 0; overflow: hidden; }
814
+ .dsh-pl-rows, .dsh-pl-grid { flex: 1 1 auto; min-width: 0; overflow: auto; padding: 8px; }
815
+ .dsh-pl-rows { display: flex; flex-direction: column; gap: 6px; }
816
+ .dsh-pl-grid { display: flex; flex-direction: column; gap: 10px; align-content: start; padding: 12px; }
817
+ .dsh-pl-grid.is-card { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
818
+ .dsh-pl-grid.is-card .dsh-pl-item { padding: 10px; gap: 8px; }
819
+ .dsh-pl-acts { display: flex; gap: 6px; align-items: center; margin-left: auto; flex: none; }
820
+ .dsh-pl-item { position: relative; }
821
+ .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; }
822
+ .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); }
823
+ .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; }
824
+ .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; }
825
+ .dsh-pl-open, .dsh-pl-pin { margin-left: 0; flex: none; position: static; }
826
+ .dsh-pl-item.is-pinned { background: color-mix(in srgb, var(--dsw-alias-label-primary) 7%, var(--dsw-alias-bg-layer-1)); }
827
+ .dsh-pl-item img, .dsh-pl-fallback { width: 22px; height: 22px; border-radius: 6px; object-fit: cover; flex: none; }
828
+ .dsh-pl-copy { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 2px; }
829
+ .dsh-pl-copy strong { font-size: 14px; font-weight: 560; color: var(--dsw-alias-label-primary); white-space: normal; overflow-wrap: anywhere; }
830
+ .dsh-pl-where { color: var(--dsw-alias-label-secondary); font-size: 11px; }
831
+ .dsh-pl-item.is-failed { border-color: var(--dsw-alias-state-error-primary); }
832
+ .dsh-pl-state { margin: 16px; color: var(--dsw-alias-label-secondary); }
833
+ .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; }
834
+ .dsh-pl-detail-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
835
+ .dsh-pl-detail-head strong { flex: 1 1 auto; min-width: 0; font-size: 16px; line-height: 1.35; overflow-wrap: anywhere; }
836
+ .dsh-pl-detail-head button { flex: none; white-space: nowrap; align-self: flex-start; }
837
+ .dsh-pl-detail-head img, .dsh-pl-detail-head .dsh-pl-fallback { width: 32px; height: 32px; }
838
+ .dsh-pl-fact { display: grid; grid-template-columns: 4.5em minmax(0, 1fr); column-gap: 12px; align-items: start; margin: 0 0 10px; }
839
+ .dsh-pl-fact-label { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.45; }
840
+ .dsh-pl-fact-value { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; }
841
+ .dsh-pl-detail h3 { margin: 14px 0 6px; font-size: 12px; color: var(--dsw-alias-label-secondary); }
842
+ .dsh-pl-detail p, .dsh-pl-doc { margin: 0 0 8px; font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; }
843
+ .dsh-pl-note { color: var(--dsw-alias-label-secondary); }
844
+ .dsh-pl-doc-block { margin-top: 12px; }
845
+ .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); }
846
+ .dsh-pl-md h4 { margin: 10px 0 4px; font-size: 13px; font-weight: 650; }
847
+ .dsh-pl-md p, .dsh-pl-md li { margin: 0 0 8px; font-size: 13px; line-height: 1.55; }
848
+ .dsh-pl-md ul { margin: 0 0 10px; padding-left: 18px; }
849
+ .dsh-pl-md code { font-size: 12px; padding: 0 4px; border-radius: 4px; background: var(--dsw-alias-bg-layer-2); }
850
+ .dsh-pl-md a { color: var(--dsw-alias-brand-primary); }
851
+ .dsh-pl-table-wrap { margin: 8px 0 14px; overflow-x: auto; border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; }
852
+ .dsh-pl-md table { width: 100%; border-collapse: collapse; font-size: 13px; }
853
+ .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); }
854
+ .dsh-pl-md th { color: var(--dsw-alias-label-secondary); font-weight: 650; background: var(--dsw-alias-bg-layer-1); white-space: nowrap; }
855
+ .dsh-pl-md td:first-child { white-space: nowrap; }
856
+ .dsh-pl-md tr:last-child td { border-bottom: 0; }
857
+ .dsh-pl-actions { display: flex; gap: 8px; align-items: center; margin-top: 16px; }
858
+ `
859
+
860
+ return {
861
+ name: 'dsh-plugins-ui',
862
+ inject: ['slots', 'sidebarRightTabs', 'sidebarRight', 'layout', 'locale', 'remote', 'remote.pluginManager', 'remote.session', 'connection'],
863
+ apply(ctx) {
864
+ ctx.effect(() => ctx.locale.register(NS, COPY), 'plugin-list: dictionaries')
865
+ const locale = ctx.locale
866
+ const pluginManager = ctx.remote.pluginManager
867
+ const listPlugins = pluginManager.listPlugins.bind(pluginManager)
868
+ const listBundles = pluginManager.listBundles.bind(pluginManager)
869
+ const readBundleDetail = pluginManager.readBundleDetail.bind(pluginManager)
870
+ const setPluginEnabled = pluginManager.setPluginEnabled.bind(pluginManager)
871
+ const rpcCall = ctx.connection.rpc.call.bind(ctx.connection.rpc)
872
+ const loadFacts = (payload) => rpcCall('/dsh-plugins-ui-rpc', 'facts', payload || {})
873
+ const revealPackage = async (payload) => {
874
+ const input = payload && typeof payload === 'object' ? payload : {}
875
+ const dir = directoryFromSource(input.source)
876
+ const session = ctx.remote && ctx.remote.session
877
+ if (dir && session && typeof session.openWorkspacePath === 'function') {
878
+ try {
879
+ await session.openWorkspacePath({ path: dir, action: 'reveal' })
880
+ return { ok: true }
881
+ } catch { /* the host RPC below is the fallback */ }
882
+ }
883
+ try {
884
+ return await rpcCall('/dsh-plugins-ui-rpc', 'reveal', {
885
+ moduleName: input.moduleName || '',
886
+ source: input.source || '',
887
+ })
888
+ } catch {
889
+ return { ok: false, error: { message: dir ? '' : '找不到这个插件的目录。' } }
890
+ }
891
+ }
892
+ const listGuide = () => (typeof ctx.sidebarRightTabs.guide === 'function' ? ctx.sidebarRightTabs.guide() : [])
893
+ const subscribeGuide = (fn) => (typeof ctx.sidebarRightTabs.subscribe === 'function' ? ctx.sidebarRightTabs.subscribe(fn) : undefined)
894
+ const SEATS = [
895
+ ['sidebar.right.pane.tab', 'right'],
896
+ ['settings.section', 'settings'],
897
+ ['settings.plugins.tab', 'settings'],
898
+ ['settings.general.item', 'settings'],
899
+ ['sidebar.panellist', 'left'],
900
+ ['sidebar.footer.action', 'left'],
901
+ ]
902
+ const readSeat = (key, place) => {
903
+ if (!ctx.slots || typeof ctx.slots.entries !== 'function') return []
904
+ try {
905
+ return (ctx.slots.entries(key) || []).map((row) => ({
906
+ place,
907
+ registrant: row && (row.registrant || (row.options && row.options.registrant)) || '',
908
+ id: row && (row.id || row.key || (row.options && (row.options.id || row.options.key))) || '',
909
+ }))
910
+ } catch {
911
+ return []
912
+ }
913
+ }
914
+ const listSeats = () => SEATS.flatMap(([key, place]) => readSeat(key, place))
915
+ const subscribeSeats = (fn) => (ctx.slots && typeof ctx.slots.subscribe === 'function'
916
+ ? ctx.slots.subscribe('settings.section', fn)
917
+ : undefined)
918
+ const openPanel = (kind) => {
919
+ if (ctx.layout && typeof ctx.layout.openRightbar === 'function') ctx.layout.openRightbar(true, false)
920
+ ctx.sidebarRight.openTab(kind)
921
+ }
922
+ const t = locale.bind(NS)
923
+ const readLocale = () => (locale.getSnapshot ? locale.getSnapshot() : locale.getLocale())
924
+ const subscribeLocale = (fn) => locale.subscribe(fn)
925
+
926
+ ctx.effect(() => {
927
+ const definition = {
928
+ id: PANEL_ID,
929
+ priority: 'extension',
930
+ title: () => t('tab.title'),
931
+ icon: PluginIcon,
932
+ }
933
+ try {
934
+ return ctx.sidebarRightTabs.register({ ...definition, kind: 'guide' })
935
+ } catch {
936
+ return ctx.sidebarRightTabs.register({ ...definition, kind: TAB_KIND })
937
+ }
938
+ }, 'plugin-list: tab type')
939
+
940
+ function TabTitle() {
941
+ return t('tab.title')
942
+ }
943
+
944
+ ctx.effect(() => {
945
+ const stops = [PANEL_ID, TAB_KIND].map((key) => ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({
946
+ name: 'sidebar.right.pane.tab.title',
947
+ key,
948
+ }, TabTitle)))
949
+ return () => stops.forEach((stop) => stop())
950
+ }, 'plugin-list: tab title')
951
+
952
+ ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
953
+ name: 'sidebar.right.pane.tab',
954
+ key: PANEL_ID,
955
+ locale: NS,
956
+ inject: () => ({ listPlugins, listBundles, readBundleDetail, setPluginEnabled, loadFacts, revealPackage, listGuide, subscribeGuide, listSeats, subscribeSeats, openPanel, t, readLocale, subscribeLocale }),
957
+ }, ListView)), 'plugin-list: tab body')
958
+ },
959
+ }
960
+ },
961
+ })