dsh-plugin-panel 0.1.2 → 0.1.3

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 CHANGED
@@ -6,7 +6,7 @@ window.__ModuleLoader__.load({
6
6
  const PANEL_ID = 'dsh-plugin-panel'
7
7
  const TAB_KIND = 'plugin-canvas'
8
8
  const NS = 'dsh-plugin-panel'
9
- const VERSION = 'v0.1.2'
9
+ const VERSION = 'v0.1.3'
10
10
  const PREF_KEY = 'dsh.plugin-list.pref.v2'
11
11
 
12
12
  const COPY = {
@@ -16,7 +16,6 @@ window.__ModuleLoader__.load({
16
16
  'guide.description': '查看并打开右侧栏插件。',
17
17
  'list.installed': '用户安装',
18
18
  'list.builtin': '系统内置',
19
- 'list.all': '全部',
20
19
  'list.refresh': '刷新',
21
20
  'list.market': '市场',
22
21
  'list.back': '返回',
@@ -28,6 +27,34 @@ window.__ModuleLoader__.load({
28
27
  'list.countFiltered': '已过滤',
29
28
  'list.perDay': '日均新增',
30
29
  'list.spanDays': '覆盖天数',
30
+ 'list.marketFirstNote': '首日已含此前录入的存量,不是从 0 开始',
31
+ 'list.install': '安装',
32
+ 'list.uninstall': '卸载',
33
+ 'list.uninstallConfirm': '确认卸载',
34
+ 'list.installing': '安装中…',
35
+ 'list.removing': '卸载中…',
36
+ 'list.hotDelta': '7 天增长',
37
+ 'list.hotStars': '仓库 star',
38
+ 'list.hotCategory': '分类',
39
+ 'list.hotRepo': '仓库',
40
+ 'list.installedState': '已安装',
41
+ 'list.notInstalled': '未安装',
42
+ 'list.chartTitle': '插件市场的增长趋势',
43
+ 'list.chartCumulative': '累计',
44
+ 'list.chartDaily': '每日增量',
45
+ 'list.chartDailyHint': '每天新增多少,比累计更能看出快慢',
46
+ 'list.marketSearch': '搜索整个市场的插件…',
47
+ 'list.marketSearching': '搜索中…',
48
+ 'list.searchError': '搜索暂时不可用。如果刚更新过插件,在插件管理里停用再启用一次即可。',
49
+ 'list.marketNoMatch': '没有找到匹配的插件',
50
+ 'list.marketHits': '找到 {n} 个',
51
+ 'list.hotSearch': '搜索插件名、说明或分类…',
52
+ 'list.hotNoMatch': '没有匹配的插件',
53
+ 'list.hotTitle': '最近 7 天增长最快',
54
+ 'list.hotHint': '按 GitHub 仓库 star 的 7 天增量排序,同一仓库只算一次。',
55
+ 'list.hotLoading': '正在算增长…',
56
+ 'list.hotError': '取不到增长数据。如果刚更新插件,需要重启一次 DeepSeek Harness 才会启用这个榜单。',
57
+ 'list.hotEmpty': '这 7 天没有增长记录。',
31
58
  'list.viewList': '列表',
32
59
  'list.viewCard': '卡片',
33
60
  'list.open': '打开',
@@ -89,7 +116,6 @@ window.__ModuleLoader__.load({
89
116
  'guide.description': 'Browse and open right-sidebar plugins.',
90
117
  'list.installed': 'Installed',
91
118
  'list.builtin': 'Built-in',
92
- 'list.all': 'All',
93
119
  'list.refresh': 'Refresh',
94
120
  'list.market': 'Market',
95
121
  'list.back': 'Back',
@@ -101,6 +127,34 @@ window.__ModuleLoader__.load({
101
127
  'list.countFiltered': 'Filtered',
102
128
  'list.perDay': 'Per day',
103
129
  'list.spanDays': 'Days',
130
+ 'list.marketFirstNote': 'First point already includes earlier stock; not growth from zero',
131
+ 'list.install': 'Install',
132
+ 'list.uninstall': 'Remove',
133
+ 'list.uninstallConfirm': 'Confirm',
134
+ 'list.installing': 'Installing…',
135
+ 'list.removing': 'Removing…',
136
+ 'list.hotDelta': '7-day growth',
137
+ 'list.hotStars': 'Repo stars',
138
+ 'list.hotCategory': 'Category',
139
+ 'list.hotRepo': 'Repository',
140
+ 'list.installedState': 'Installed',
141
+ 'list.notInstalled': 'Not installed',
142
+ 'list.chartTitle': 'Plugin market growth',
143
+ 'list.chartCumulative': 'Total',
144
+ 'list.chartDaily': 'Per day',
145
+ 'list.chartDailyHint': 'New plugins per day, which shows pace better than a total',
146
+ 'list.marketSearch': 'Search the whole market…',
147
+ 'list.marketSearching': 'Searching…',
148
+ 'list.searchError': 'Search is unavailable. If you just updated the plugin, disable and re-enable it once.',
149
+ 'list.marketNoMatch': 'No matching plugin',
150
+ 'list.marketHits': '{n} found',
151
+ 'list.hotSearch': 'Search by name, description or category…',
152
+ 'list.hotNoMatch': 'No matching plugin',
153
+ 'list.hotTitle': 'Fastest growing, last 7 days',
154
+ 'list.hotHint': 'Ranked by 7-day GitHub repository star delta; one row per repository.',
155
+ 'list.hotLoading': 'Crunching growth…',
156
+ 'list.hotError': 'Could not load growth data. Restart DeepSeek Harness once to enable this ranking.',
157
+ 'list.hotEmpty': 'No growth recorded in the last 7 days.',
104
158
  'list.viewList': 'List',
105
159
  'list.viewCard': 'Cards',
106
160
  'list.open': 'Open',
@@ -171,7 +225,7 @@ window.__ModuleLoader__.load({
171
225
  try {
172
226
  const parsed = JSON.parse(window.localStorage.getItem(PREF_KEY) || '{}')
173
227
  return {
174
- filter: ['installed', 'builtin', 'all'].includes(parsed.filter) ? parsed.filter : 'installed',
228
+ filter: parsed.filter === 'builtin' ? 'builtin' : 'installed',
175
229
  pins: Array.isArray(parsed.pins) ? parsed.pins.filter((id) => typeof id === 'string') : [],
176
230
  layout: parsed.layout === 'card' ? 'card' : 'list',
177
231
  }
@@ -273,15 +327,97 @@ window.__ModuleLoader__.load({
273
327
  return ''
274
328
  }
275
329
 
276
- function trendChart(days) {
330
+ /** Bars for the day-over-day change; the first day has no previous snapshot. */
331
+ function barChart(days, hoverIndex, onHover) {
277
332
  const W = 1000
278
- const H = 340
279
- const L = 76
280
- const R = 28
281
- const T = 24
282
- const B = 52
333
+ const H = 250
334
+ const L = 68
335
+ const R = 24
336
+ const T = 18
337
+ const B = 42
338
+ const deltas = days.map((day, index) => (index === 0 ? null : day.count - days[index - 1].count))
339
+ const usable = deltas.filter((value) => typeof value === 'number')
340
+ const peak = Math.max(...usable, 1) * 1.15
341
+ const n = days.length
342
+ const slot = (W - L - R) / Math.max(n, 1)
343
+ const barW = Math.max(2, Math.min(slot * 0.62, 14))
344
+ const x = (i) => L + slot * i + slot / 2
345
+ const zeroY = T + (H - T - B)
346
+ const y = (v) => zeroY - ((H - T - B) * v) / Math.max(peak, 1)
347
+ const ticks = [0, 0.5, 1].map((r) => Math.round((peak * r) / 500) * 500)
348
+ const labelIndexes = n <= 1 ? [0] : [0, Math.floor((n - 1) / 2), n - 1]
349
+
350
+ const pick = (event) => {
351
+ const rect = event.currentTarget.getBoundingClientRect()
352
+ if (!rect.width) return
353
+ const px = ((event.clientX - rect.left) / rect.width) * W
354
+ const index = Math.round((px - L - slot / 2) / Math.max(slot, 1))
355
+ onHover(Math.max(0, Math.min(n - 1, index)))
356
+ }
357
+
358
+ const hovered = hoverIndex >= 0 && hoverIndex < n ? hoverIndex : -1
359
+ const hx = hovered >= 0 ? x(hovered) : 0
360
+ const value = hovered >= 0 ? deltas[hovered] : null
361
+ const boxW = 200
362
+ const boxH = 64
363
+ const boxX = hovered >= 0 ? Math.min(Math.max(hx - boxW / 2, L), W - R - boxW) : 0
364
+ const boxY = hovered >= 0 ? Math.max(T + 8, y(typeof value === 'number' ? value : 0) - boxH - 14) : 0
365
+ const shown = typeof value === 'number' ? `${value >= 0 ? '+' : ''}${value.toLocaleString()}` : '—'
366
+
367
+ return h('svg', {
368
+ className: 'dsh-pl-chart',
369
+ viewBox: `0 0 ${W} ${H}`,
370
+ role: 'img',
371
+ onMouseMove: pick,
372
+ onMouseLeave: () => onHover(-1),
373
+ },
374
+ ticks.map((tick, i) => h('line', {
375
+ key: `g${i}`, x1: L, x2: W - R, y1: y(tick), y2: y(tick),
376
+ stroke: 'var(--dsw-alias-border-l1)', strokeWidth: 1,
377
+ })),
378
+ ticks.map((tick, i) => h('text', {
379
+ key: `t${i}`, x: L - 12, y: y(tick) + 6, textAnchor: 'end',
380
+ fill: 'var(--dsw-alias-label-secondary)', fontSize: 16,
381
+ }, `+${Math.round(tick / 1000)}k`)),
382
+ deltas.map((delta, i) => {
383
+ if (typeof delta !== 'number') return null
384
+ const top = Math.min(y(delta), zeroY)
385
+ const height = Math.max(1, Math.abs(zeroY - y(delta)))
386
+ return h('rect', {
387
+ key: `b${i}`, x: x(i) - barW / 2, y: top, width: barW, height,
388
+ rx: Math.min(2, barW / 2),
389
+ fill: 'var(--dsw-alias-brand-primary)',
390
+ opacity: hovered === i ? 0.95 : 0.55,
391
+ })
392
+ }),
393
+ labelIndexes.map((i) => h('text', {
394
+ key: `d${i}`, x: x(i), y: H - B + 34, textAnchor: i === 0 ? 'start' : i === n - 1 ? 'end' : 'middle',
395
+ fill: 'var(--dsw-alias-label-secondary)', fontSize: 16,
396
+ }, days[i].day.slice(5))),
397
+ hovered >= 0 ? h('g', null,
398
+ h('line', {
399
+ x1: hx, x2: hx, y1: T, y2: H - B,
400
+ stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 1, strokeDasharray: '4 4', opacity: 0.5,
401
+ }),
402
+ h('rect', {
403
+ x: boxX, y: boxY, width: boxW, height: boxH, rx: 10,
404
+ fill: 'var(--dsw-alias-bg-overlay)', stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 1,
405
+ }),
406
+ h('text', { x: boxX + 14, y: boxY + 24, fill: 'var(--dsw-alias-label-secondary)', fontSize: 16 }, days[hovered].day),
407
+ h('text', { x: boxX + 14, y: boxY + 50, fill: 'var(--dsw-alias-label-primary)', fontSize: 22, fontWeight: 600 }, shown),
408
+ ) : null,
409
+ )
410
+ }
411
+
412
+ function trendChart(days, hoverIndex, onHover, firstNote) {
413
+ const W = 1000
414
+ const H = 250
415
+ const L = 68
416
+ const R = 24
417
+ const T = 18
418
+ const B = 42
283
419
  const values = days.map((d) => d.count)
284
- const max = Math.max(...values, 1) * 1.04
420
+ const max = Math.max(...values, 1) * 1.2
285
421
  const n = days.length
286
422
  const x = (i) => L + (W - L - R) * (n <= 1 ? 0.5 : i / (n - 1))
287
423
  const y = (v) => T + (H - T - B) * (1 - v / max)
@@ -289,25 +425,68 @@ window.__ModuleLoader__.load({
289
425
  const area = `${line} L ${x(n - 1).toFixed(1)} ${H - B} L ${x(0).toFixed(1)} ${H - B} Z`
290
426
  const ticks = [0, 0.25, 0.5, 0.75, 1].map((r) => Math.round((max * r) / 1000) * 1000)
291
427
  const labelIndexes = n <= 1 ? [0] : [0, Math.floor((n - 1) / 2), n - 1]
292
- return h('svg', { className: 'dsh-pl-chart', viewBox: `0 0 ${W} ${H}`, role: 'img' },
428
+
429
+ const pick = (event) => {
430
+ const rect = event.currentTarget.getBoundingClientRect()
431
+ if (!rect.width) return
432
+ const px = ((event.clientX - rect.left) / rect.width) * W
433
+ const ratio = (px - L) / Math.max(W - L - R, 1)
434
+ const index = Math.round(ratio * Math.max(n - 1, 1))
435
+ onHover(Math.max(0, Math.min(n - 1, index)))
436
+ }
437
+
438
+ const hovered = hoverIndex >= 0 && hoverIndex < n ? hoverIndex : -1
439
+ const hx = hovered >= 0 ? x(hovered) : 0
440
+ const hy = hovered >= 0 ? y(values[hovered]) : 0
441
+ const isFirst = hovered === 0
442
+ const boxW = isFirst ? 246 : 196
443
+ const boxH = isFirst ? 96 : 64
444
+ const boxX = hovered >= 0 ? Math.min(Math.max(hx - boxW / 2, L), W - R - boxW) : 0
445
+ const boxY = hovered >= 0 ? Math.max(hy - boxH - 16, T) : 0
446
+
447
+ return h('svg', {
448
+ className: 'dsh-pl-chart',
449
+ viewBox: `0 0 ${W} ${H}`,
450
+ role: 'img',
451
+ onMouseMove: pick,
452
+ onMouseLeave: () => onHover(-1),
453
+ },
293
454
  ticks.map((tick, i) => h('line', {
294
455
  key: `g${i}`, x1: L, x2: W - R, y1: y(tick), y2: y(tick),
295
456
  stroke: 'var(--dsw-alias-border-l1)', strokeWidth: 1,
296
457
  })),
297
458
  ticks.map((tick, i) => h('text', {
298
459
  key: `t${i}`, x: L - 12, y: y(tick) + 6, textAnchor: 'end',
299
- fill: 'var(--dsw-alias-label-secondary)', fontSize: 20,
460
+ fill: 'var(--dsw-alias-label-secondary)', fontSize: 16,
300
461
  }, `${Math.round(tick / 1000)}k`)),
301
- h('path', { d: area, fill: 'var(--dsw-alias-brand-primary)', opacity: 0.12 }),
302
- h('path', { d: line, fill: 'none', stroke: 'var(--dsw-alias-brand-primary)', strokeWidth: 4, strokeLinejoin: 'round' }),
462
+ h('path', { d: area, fill: 'var(--dsw-alias-brand-primary)', opacity: 0.08 }),
463
+ h('path', { d: line, fill: 'none', stroke: 'var(--dsw-alias-brand-primary)', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }),
303
464
  labelIndexes.map((i) => h('text', {
304
465
  key: `d${i}`, x: x(i), y: H - B + 34, textAnchor: i === 0 ? 'start' : i === n - 1 ? 'end' : 'middle',
305
- fill: 'var(--dsw-alias-label-secondary)', fontSize: 20,
466
+ fill: 'var(--dsw-alias-label-secondary)', fontSize: 16,
306
467
  }, days[i].day.slice(5))),
307
468
  h('text', {
308
469
  x: x(n - 1), y: y(values[n - 1]) - 16, textAnchor: 'end',
309
- fill: 'var(--dsw-alias-label-primary)', fontSize: 26, fontWeight: 600,
470
+ fill: 'var(--dsw-alias-label-primary)', fontSize: 22, fontWeight: 600,
310
471
  }, String(values[n - 1])),
472
+ hovered >= 0 ? h('g', null,
473
+ h('line', {
474
+ x1: hx, x2: hx, y1: T, y2: H - B,
475
+ stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 1, strokeDasharray: '4 4', opacity: 0.5,
476
+ }),
477
+ h('circle', { cx: hx, cy: hy, r: 4.5, fill: 'var(--dsw-alias-brand-primary)', stroke: '#fff', strokeWidth: 2 }),
478
+ h('rect', {
479
+ x: boxX, y: boxY, width: boxW, height: boxH, rx: 10,
480
+ fill: 'var(--dsw-alias-bg-overlay)', stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 1,
481
+ }),
482
+ h('text', { x: boxX + 14, y: boxY + 24, fill: 'var(--dsw-alias-label-secondary)', fontSize: 16 },
483
+ days[hovered].day),
484
+ h('text', { x: boxX + 14, y: boxY + 50, fill: 'var(--dsw-alias-label-primary)', fontSize: 22, fontWeight: 600 },
485
+ `${values[hovered].toLocaleString()} 个`),
486
+ isFirst && firstNote
487
+ ? h('text', { x: boxX + 14, y: boxY + 78, fill: 'var(--dsw-alias-label-secondary)', fontSize: 14 }, firstNote)
488
+ : null,
489
+ ) : null,
311
490
  )
312
491
  }
313
492
 
@@ -468,6 +647,49 @@ window.__ModuleLoader__.load({
468
647
  }) || null
469
648
  }
470
649
 
650
+ /** One card shape, shared by the plugin list and the market ranking. */
651
+ function PluginCard(options) {
652
+ const { icon, title, subtitle, actions, className, onClick, badge } = options
653
+ const classes = ['dsh-pl-item']
654
+ if (className) classes.push(className)
655
+ return h('div', {
656
+ className: classes.join(' '),
657
+ role: 'button',
658
+ tabIndex: 0,
659
+ onClick,
660
+ onKeyDown: (event) => {
661
+ if (event.key === 'Enter' || event.key === ' ') {
662
+ event.preventDefault()
663
+ if (onClick) onClick()
664
+ }
665
+ },
666
+ },
667
+ icon || h(DefaultPluginIcon),
668
+ h('span', { className: 'dsh-pl-copy' },
669
+ h('strong', null, title),
670
+ subtitle ? h('span', { className: 'dsh-pl-where' }, subtitle) : null),
671
+ badge || null,
672
+ actions && actions.length ? h('div', { className: 'dsh-pl-acts' }, actions) : null)
673
+ }
674
+
675
+ /** One detail shape, shared by the plugin detail and the market detail. */
676
+ function DetailPanel(options) {
677
+ const { icon, title, facts, onClose, closeLabel, actions, note, children } = options
678
+ const rows = (facts || []).filter((entry) => entry && entry.value)
679
+ return h('aside', { className: 'dsh-pl-detail' },
680
+ h('div', { className: 'dsh-pl-detail-head' },
681
+ icon || h(DefaultPluginIcon),
682
+ h('strong', null, title),
683
+ h('button', { type: 'button', onClick: onClose }, closeLabel)),
684
+ h('div', { className: 'dsh-pl-detail-body' },
685
+ rows.map((entry, index) => h('div', { className: 'dsh-pl-fact', key: index },
686
+ h('span', { className: 'dsh-pl-fact-label' }, entry.label),
687
+ h('span', { className: 'dsh-pl-fact-value' }, entry.value))),
688
+ note ? h('p', { className: 'dsh-pl-note' }, note) : null,
689
+ children || null,
690
+ actions && actions.length ? h('div', { className: 'dsh-pl-actions' }, actions) : null))
691
+ }
692
+
471
693
  function DefaultPluginIcon() {
472
694
  return h('svg', {
473
695
  className: 'dsh-pl-fallback',
@@ -510,6 +732,19 @@ window.__ModuleLoader__.load({
510
732
  const [surface, setSurface] = React.useState('plugins')
511
733
  const [trend, setTrend] = React.useState(null)
512
734
  const [trendStatus, setTrendStatus] = React.useState('')
735
+ const [hoverIndex, setHoverIndex] = React.useState(-1)
736
+ const [hot, setHot] = React.useState(null)
737
+ const [hotError, setHotError] = React.useState('')
738
+ const [busyKey, setBusyKey] = React.useState('')
739
+ const [confirmKey, setConfirmKey] = React.useState('')
740
+ const [selectedMarket, setSelectedMarket] = React.useState(null)
741
+ const [chartMode, setChartMode] = React.useState('cumulative')
742
+ const [hotQuery, setHotQuery] = React.useState('')
743
+ const [marketQuery, setMarketQuery] = React.useState('')
744
+ const [searchResult, setSearchResult] = React.useState(null)
745
+ const [searchStatus, setSearchStatus] = React.useState('')
746
+ const [trendError, setTrendError] = React.useState('')
747
+ const [hotStatus, setHotStatus] = React.useState('')
513
748
  const [plugins, setPlugins] = React.useState([])
514
749
  const [bundles, setBundles] = React.useState([])
515
750
  const [status, setStatus] = React.useState(listPlugins ? 'loading' : 'unavailable')
@@ -635,33 +870,100 @@ window.__ModuleLoader__.load({
635
870
  return () => { cancelled = true }
636
871
  }, [selectedId, plugins, bundles, loadFacts])
637
872
 
873
+ const MARKET_CACHE_KEY = 'dsh.plugin-list.market.v1'
874
+
875
+ function todayKey() {
876
+ return new Date().toISOString().slice(0, 10)
877
+ }
878
+
879
+ function readMarketCache() {
880
+ try {
881
+ const raw = JSON.parse(window.localStorage.getItem(MARKET_CACHE_KEY) || '{}')
882
+ return raw && raw.day === todayKey() ? raw : null
883
+ } catch {
884
+ return null
885
+ }
886
+ }
887
+
888
+ function writeMarketCache(patch) {
889
+ try {
890
+ const raw = readMarketCache() || { day: todayKey() }
891
+ window.localStorage.setItem(MARKET_CACHE_KEY, JSON.stringify({ ...raw, ...patch }))
892
+ } catch { /* caching is best effort */ }
893
+ }
894
+
638
895
  const loadTrend = React.useCallback(async () => {
639
- if (typeof loadMarketTrend !== 'function') return
896
+ const cached = readMarketCache()
897
+ if (cached && cached.trend) {
898
+ setTrend(cached.trend)
899
+ setTrendStatus('ready')
900
+ return
901
+ }
902
+ const call = props.loadMarketTrend
903
+ if (typeof call !== 'function') return
640
904
  setTrendStatus('loading')
641
905
  try {
642
- const result = await loadMarketTrend()
906
+ const result = await call()
907
+ if (result && result.ok === false) {
908
+ setTrendError((result.error && result.error.message) || '')
909
+ setTrendStatus('error')
910
+ return
911
+ }
643
912
  const value = result && result.ok === true ? result.value : result
644
913
  if (value && Array.isArray(value.days) && value.days.length) {
645
914
  setTrend(value)
915
+ writeMarketCache({ trend: value })
646
916
  setTrendStatus('ready')
647
917
  } else {
648
918
  setTrendStatus('error')
649
919
  }
650
- } catch {
920
+ } catch (failure) {
921
+ setTrendError(failure instanceof Error ? failure.message : '')
651
922
  setTrendStatus('error')
652
923
  }
653
- }, [loadMarketTrend])
924
+ }, [props.loadMarketTrend])
925
+
926
+ const loadHot = React.useCallback(async () => {
927
+ const cached = readMarketCache()
928
+ if (cached && cached.hot) {
929
+ setHot(cached.hot)
930
+ setHotStatus('ready')
931
+ return
932
+ }
933
+ const call = props.loadMarketHot
934
+ if (typeof call !== 'function') return
935
+ setHotStatus('loading')
936
+ try {
937
+ const result = await call()
938
+ if (result && result.ok === false) {
939
+ setHotError((result.error && result.error.message) || '')
940
+ setHotStatus('error')
941
+ return
942
+ }
943
+ const value = result && result.ok === true ? result.value : result
944
+ if (value && Array.isArray(value.top) && value.top.length) {
945
+ setHot(value)
946
+ writeMarketCache({ hot: value })
947
+ setHotStatus('ready')
948
+ } else {
949
+ setHotStatus('empty')
950
+ }
951
+ } catch (failure) {
952
+ setHotError(failure instanceof Error ? failure.message : '')
953
+ setHotStatus('error')
954
+ }
955
+ }, [props.loadMarketHot])
654
956
 
655
957
  React.useEffect(() => {
656
- if (surface === 'market' && trendStatus === '') void loadTrend()
657
- }, [surface, trendStatus, loadTrend])
958
+ if (surface !== 'market') return
959
+ if (trendStatus === '') void loadTrend()
960
+ if (hotStatus === '') void loadHot()
961
+ }, [surface, trendStatus, hotStatus, loadTrend, loadHot])
658
962
 
659
963
  const visible = React.useMemo(() => {
660
964
  const rows = plugins.filter((plugin) => {
661
965
  const origin = originOf(plugin)
662
- if (filter === 'installed') return origin === 'installed'
663
- if (filter === 'builtin') return origin === 'builtin'
664
- return true
966
+ return filter === 'builtin' ? origin === 'builtin' : origin === 'installed'
665
967
  })
666
968
  const rank = (plugin) => {
667
969
  const places = placesFor(plugin, guide, seats, localeId)
@@ -681,13 +983,6 @@ window.__ModuleLoader__.load({
681
983
  const selected = plugins.find((item) => item.entryId === selectedId) || null
682
984
  const selectedBundle = selected ? matchBundle(selected, bundles) : null
683
985
 
684
- function fact(label, value) {
685
- if (!value) return null
686
- return h('div', { className: 'dsh-pl-fact' },
687
- h('span', { className: 'dsh-pl-fact-label' }, label),
688
- h('span', { className: 'dsh-pl-fact-value' }, value))
689
- }
690
-
691
986
  function revealHint(text) {
692
987
  const raw = String(text || '')
693
988
  if (/Unknown detail method|invalid server-response|unaddressable|not found/i.test(raw)) return textOf(t, 'list.revealRestart')
@@ -722,64 +1017,73 @@ window.__ModuleLoader__.load({
722
1017
  const componentText = rows.length
723
1018
  ? `${rows.length}${siblings.length ? ` · ${siblings.join('、')}${rows.length > 7 ? '…' : ''}` : ''}`
724
1019
  : ''
725
- return h('aside', { className: 'dsh-pl-detail' },
726
- h('div', { className: 'dsh-pl-detail-head' },
727
- icon ? h('img', { src: icon, alt: '' }) : h(DefaultPluginIcon),
728
- h('strong', null, title),
729
- h('button', { type: 'button', onClick: () => setSelectedId('') }, textOf(t, 'list.close'))),
730
- h('div', { className: 'dsh-pl-detail-body' },
731
- fact(textOf(t, 'list.intro'), intro || textOf(t, 'list.unknown')),
732
- fact(textOf(t, 'list.module'), shortModule(plugin.moduleName)),
733
- fact(textOf(t, 'list.entry'), plugin.entryId),
734
- fact(textOf(t, 'list.status'), textOf(t, phaseKey(plugin))),
735
- fact(textOf(t, 'list.package'), (selectedBundle && selectedBundle.name) || (manifest && manifest.name) || shortModule(plugin.moduleName)),
736
- fact(textOf(t, 'list.version'), (selectedBundle && selectedBundle.version) || (manifest && manifest.version)),
737
- fact(textOf(t, 'list.place'), place),
738
- fact(textOf(t, 'list.source'), source),
739
- fact(textOf(t, 'list.flags'), flags),
740
- fact(textOf(t, 'list.components'), componentText),
741
- fact(textOf(t, 'list.license'), manifest && manifest.license),
742
- fact(textOf(t, 'list.repo'), manifest && manifest.repository),
743
- fact(textOf(t, 'list.engines'), manifest && manifest.engines),
744
- fact(textOf(t, 'list.keywords'), manifest && Array.isArray(manifest.keywords) ? manifest.keywords.join('、') : ''),
745
- fact(textOf(t, 'list.inject'), manifest && Array.isArray(manifest.clientInject) ? manifest.clientInject.join('、') : ''),
746
- h('section', { className: 'dsh-pl-doc-block' },
747
- h('div', { className: 'dsh-pl-fact-label' }, textOf(t, 'list.doc')),
748
- readmeStatus === 'loading'
749
- ? h('p', null, textOf(t, 'list.docLoading'))
750
- : (readme || (facts && facts.document))
751
- ? markdownView(readme || facts.document)
752
- : h('p', null, textOf(t, 'list.docEmpty'))),
753
- h('div', { className: 'dsh-pl-actions' },
754
- tabKindFor(plugin, guide, localeId) && typeof openPanel === 'function'
755
- ? h('button', {
756
- type: 'button',
757
- onClick: () => openPanel(tabKindFor(plugin, guide, localeId)),
758
- }, textOf(t, 'list.open'))
759
- : null,
760
- typeof props.revealPackage === 'function'
761
- ? h('button', {
762
- type: 'button',
763
- onClick: () => {
764
- setRevealNote('')
765
- const source = selectedBundle && selectedBundle.source
766
- Promise.resolve(props.revealPackage({ moduleName: plugin.moduleName || '', source: source || '' })).then((result) => {
767
- if (result && result.ok === false) setRevealNote(revealHint(result.error && result.error.message))
768
- }).catch((failure) => {
769
- setRevealNote(revealHint(failure instanceof Error ? failure.message : ''))
770
- })
771
- },
772
- }, textOf(t, 'list.reveal'))
773
- : null,
774
- plugin.patchId && plugin.readOnlyReason == null && setPluginEnabled
775
- ? h('button', {
776
- type: 'button',
777
- onClick: () => { void setPluginEnabled(plugin.entryId, plugin.enabled !== true).then(() => load()) },
778
- }, plugin.enabled ? textOf(t, 'list.disable') : textOf(t, 'list.enable'))
779
- : plugin.readOnlyReason
780
- ? h('span', { className: 'dsh-pl-note' }, String(plugin.readOnlyReason))
781
- : h('span', { className: 'dsh-pl-note' }, textOf(t, 'list.locked')),
782
- revealNote ? h('span', { className: 'dsh-pl-note' }, revealNote) : null)))
1020
+ const detailFacts = [
1021
+ { label: textOf(t, 'list.intro'), value: intro || textOf(t, 'list.unknown') },
1022
+ { label: textOf(t, 'list.module'), value: shortModule(plugin.moduleName) },
1023
+ { label: textOf(t, 'list.entry'), value: plugin.entryId },
1024
+ { label: textOf(t, 'list.status'), value: textOf(t, phaseKey(plugin)) },
1025
+ { label: textOf(t, 'list.package'), value: (selectedBundle && selectedBundle.name) || (manifest && manifest.name) || shortModule(plugin.moduleName) },
1026
+ { label: textOf(t, 'list.version'), value: (selectedBundle && selectedBundle.version) || (manifest && manifest.version) },
1027
+ { label: textOf(t, 'list.place'), value: place },
1028
+ { label: textOf(t, 'list.source'), value: source },
1029
+ { label: textOf(t, 'list.flags'), value: flags },
1030
+ { label: textOf(t, 'list.components'), value: componentText },
1031
+ { label: textOf(t, 'list.license'), value: manifest && manifest.license },
1032
+ { label: textOf(t, 'list.repo'), value: manifest && manifest.repository },
1033
+ { label: textOf(t, 'list.engines'), value: manifest && manifest.engines },
1034
+ { label: textOf(t, 'list.keywords'), value: manifest && Array.isArray(manifest.keywords) ? manifest.keywords.join('、') : '' },
1035
+ { label: textOf(t, 'list.inject'), value: manifest && Array.isArray(manifest.clientInject) ? manifest.clientInject.join('、') : '' },
1036
+ ]
1037
+
1038
+ const detailActions = []
1039
+ const openKind = tabKindFor(plugin, guide, localeId)
1040
+ if (openKind && typeof openPanel === 'function') {
1041
+ detailActions.push(h('button', {
1042
+ key: 'open',
1043
+ type: 'button',
1044
+ onClick: () => openPanel(openKind),
1045
+ }, textOf(t, 'list.open')))
1046
+ }
1047
+ if (typeof props.revealPackage === 'function') {
1048
+ detailActions.push(h('button', {
1049
+ key: 'reveal',
1050
+ type: 'button',
1051
+ onClick: () => {
1052
+ setRevealNote('')
1053
+ const source = selectedBundle && selectedBundle.source
1054
+ Promise.resolve(props.revealPackage({ moduleName: plugin.moduleName || '', source: source || '' })).then((result) => {
1055
+ if (result && result.ok === false) setRevealNote(revealHint(result.error && result.error.message))
1056
+ }).catch((failure) => {
1057
+ setRevealNote(revealHint(failure instanceof Error ? failure.message : ''))
1058
+ })
1059
+ },
1060
+ }, textOf(t, 'list.reveal')))
1061
+ }
1062
+ if (plugin.patchId && plugin.readOnlyReason == null && setPluginEnabled) {
1063
+ detailActions.push(h('button', {
1064
+ key: 'toggle',
1065
+ type: 'button',
1066
+ onClick: () => { void setPluginEnabled(plugin.entryId, plugin.enabled !== true).then(() => load()) },
1067
+ }, plugin.enabled ? textOf(t, 'list.disable') : textOf(t, 'list.enable')))
1068
+ }
1069
+ if (revealNote) detailActions.push(h('span', { key: 'note', className: 'dsh-pl-note' }, revealNote))
1070
+ if (plugin.readOnlyReason) detailActions.push(h('span', { key: 'locked', className: 'dsh-pl-note' }, String(plugin.readOnlyReason)))
1071
+
1072
+ return h(DetailPanel, {
1073
+ icon: icon ? h('img', { src: icon, alt: '' }) : null,
1074
+ title,
1075
+ closeLabel: textOf(t, 'list.close'),
1076
+ onClose: () => setSelectedId(''),
1077
+ facts: detailFacts,
1078
+ actions: detailActions,
1079
+ children: h('section', { className: 'dsh-pl-doc-block' },
1080
+ h('div', { className: 'dsh-pl-fact-label' }, textOf(t, 'list.doc')),
1081
+ readmeStatus === 'loading'
1082
+ ? h('p', null, textOf(t, 'list.docLoading'))
1083
+ : (readme || (facts && facts.document))
1084
+ ? markdownView(readme || facts.document)
1085
+ : h('p', null, textOf(t, 'list.docEmpty'))),
1086
+ })
783
1087
  }
784
1088
 
785
1089
  function togglePin(entryId) {
@@ -801,6 +1105,168 @@ window.__ModuleLoader__.load({
801
1105
  }, label)
802
1106
  }
803
1107
 
1108
+ function isInstalled(name) {
1109
+ const target = String(name || '')
1110
+ return plugins.some((plugin) => {
1111
+ const moduleName = String(plugin.moduleName || '')
1112
+ return moduleName === target || plugin.patchId === target || plugin.entryId === `include:${target}`
1113
+ })
1114
+ }
1115
+
1116
+ async function installFromMarket(name) {
1117
+ if (typeof props.installBundle !== 'function' || busyKey) return
1118
+ setBusyKey(name)
1119
+ try {
1120
+ const result = await props.installBundle(String(name))
1121
+ const value = result && result.ok === true ? result.value : result
1122
+ if (value && value.application === 'failed') {
1123
+ setHotError((value.error && (value.error.diagnostic || value.error.code)) || '')
1124
+ } else if (value && Array.isArray(value.pendingBuilds) && value.pendingBuilds.length) {
1125
+ setHotError(`需要批准构建脚本:${value.pendingBuilds.join(', ')}`)
1126
+ }
1127
+ await load()
1128
+ } catch (failure) {
1129
+ setHotError(failure instanceof Error ? failure.message : '')
1130
+ } finally {
1131
+ setBusyKey('')
1132
+ }
1133
+ }
1134
+
1135
+ async function removeFromMarket(name) {
1136
+ if (typeof props.removeBundle !== 'function' || busyKey) return
1137
+ setBusyKey(name)
1138
+ setConfirmKey('')
1139
+ try {
1140
+ const result = await props.removeBundle(String(name))
1141
+ const value = result && result.ok === true ? result.value : result
1142
+ if (value && value.application === 'failed') {
1143
+ setHotError((value.error && (value.error.diagnostic || value.error.code)) || '')
1144
+ }
1145
+ await load()
1146
+ } catch (failure) {
1147
+ setHotError(failure instanceof Error ? failure.message : '')
1148
+ } finally {
1149
+ setBusyKey('')
1150
+ }
1151
+ }
1152
+
1153
+ function hotAction(row) {
1154
+ const name = row.name
1155
+ if (busyKey === name) {
1156
+ return h('span', { className: 'dsh-pl-busy' }, isInstalled(name) ? textOf(t, 'list.removing') : textOf(t, 'list.installing'))
1157
+ }
1158
+ if (isInstalled(name)) {
1159
+ return confirmKey === name
1160
+ ? h('button', {
1161
+ type: 'button',
1162
+ className: 'dsh-pl-card-btn',
1163
+ onClick: (event) => { event.stopPropagation(); void removeFromMarket(name) },
1164
+ }, textOf(t, 'list.uninstallConfirm'))
1165
+ : h('button', {
1166
+ type: 'button',
1167
+ className: 'dsh-pl-card-btn',
1168
+ onClick: (event) => { event.stopPropagation(); setConfirmKey(name) },
1169
+ }, textOf(t, 'list.uninstall'))
1170
+ }
1171
+ return h('button', {
1172
+ type: 'button',
1173
+ className: 'dsh-pl-card-btn',
1174
+ onClick: (event) => { event.stopPropagation(); void installFromMarket(name) },
1175
+ }, textOf(t, 'list.install'))
1176
+ }
1177
+
1178
+ function hotView() {
1179
+ const rows = hot && Array.isArray(hot.top) ? hot.top : []
1180
+ const query = hotQuery.trim().toLowerCase()
1181
+ const shown = query
1182
+ ? rows.filter((row) => [row.name, row.summary, row.category]
1183
+ .filter(Boolean)
1184
+ .some((field) => String(field).toLowerCase().includes(query)))
1185
+ : rows
1186
+ return h('section', { className: 'dsh-pl-hot' },
1187
+ h('div', { className: 'dsh-pl-block-head' },
1188
+ h('strong', null, textOf(t, 'list.hotTitle')),
1189
+ hot && hot.fromDay
1190
+ ? h('span', null, rows.length === shown.length ? `${hot.fromDay} → ${hot.toDay}` : `${shown.length} / ${rows.length}`)
1191
+ : null),
1192
+ h('p', { className: 'dsh-pl-note' }, textOf(t, 'list.hotHint')),
1193
+ hotStatus === 'loading'
1194
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.hotLoading'))
1195
+ : hotStatus === 'error'
1196
+ ? h('p', { className: 'dsh-pl-state' }, hotError ? `${textOf(t, 'list.hotError')}(${hotError})` : textOf(t, 'list.hotError'))
1197
+ : rows.length === 0
1198
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.hotEmpty'))
1199
+ : shown.length === 0
1200
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.hotNoMatch'))
1201
+ : h('div', { className: `dsh-pl-grid${view === 'card' ? ' is-card' : ''}` },
1202
+ shown.map((row, index) => h(PluginCard, {
1203
+ key: row.repo || row.name || index,
1204
+ icon: row.icon ? h('img', { src: row.icon, alt: '', draggable: false }) : null,
1205
+ title: row.name,
1206
+ subtitle: row.summary || row.category || '',
1207
+ className: 'is-market',
1208
+ badge: h('span', { className: 'dsh-pl-delta' }, `+${row.delta.toLocaleString()}`),
1209
+ onClick: () => setSelectedMarket(row),
1210
+ actions: [hotAction(row)],
1211
+ }))))
1212
+ }
1213
+
1214
+ const searchTimer = React.useRef(null)
1215
+
1216
+ const runMarketSearch = React.useCallback((query) => {
1217
+ const call = props.searchCatalog
1218
+ if (typeof call !== 'function') return
1219
+ if (!query.trim()) {
1220
+ setSearchResult(null)
1221
+ setSearchStatus('')
1222
+ return
1223
+ }
1224
+ setSearchStatus('loading')
1225
+ Promise.resolve(call({ query })).then((result) => {
1226
+ if (result && result.ok === false) {
1227
+ setSearchResult(null)
1228
+ setSearchStatus('error')
1229
+ return
1230
+ }
1231
+ const value = result && result.ok === true ? result.value : result
1232
+ setSearchResult(value && Array.isArray(value.items) ? value.items : [])
1233
+ setSearchStatus('ready')
1234
+ }).catch(() => {
1235
+ setSearchResult(null)
1236
+ setSearchStatus('error')
1237
+ })
1238
+ }, [props.searchCatalog])
1239
+
1240
+ function onMarketQuery(value) {
1241
+ setMarketQuery(value)
1242
+ if (searchTimer.current) clearTimeout(searchTimer.current)
1243
+ searchTimer.current = setTimeout(() => runMarketSearch(value), 300)
1244
+ }
1245
+
1246
+ function marketDetail(row) {
1247
+ const installed = isInstalled(row.name)
1248
+ const facts = [
1249
+ typeof row.delta === 'number'
1250
+ ? { label: textOf(t, 'list.hotDelta'), value: `+${row.delta.toLocaleString()}` }
1251
+ : null,
1252
+ typeof row.stars === 'number'
1253
+ ? { label: textOf(t, 'list.hotStars'), value: row.stars.toLocaleString() }
1254
+ : null,
1255
+ row.version ? { label: textOf(t, 'list.version'), value: row.version } : null,
1256
+ row.category ? { label: textOf(t, 'list.hotCategory'), value: row.category } : null,
1257
+ row.repo ? { label: textOf(t, 'list.hotRepo'), value: row.repo } : null,
1258
+ { label: textOf(t, 'list.status'), value: installed ? textOf(t, 'list.installedState') : textOf(t, 'list.notInstalled') },
1259
+ ].filter(Boolean)
1260
+ return h(DetailPanel, {
1261
+ title: row.name,
1262
+ closeLabel: textOf(t, 'list.close'),
1263
+ onClose: () => setSelectedMarket(null),
1264
+ facts,
1265
+ note: row.summary || '',
1266
+ actions: [hotAction(row)],
1267
+ })
1268
+ }
1269
+
804
1270
  function marketView() {
805
1271
  const days = trend && Array.isArray(trend.days) ? trend.days : []
806
1272
  const first = days[0]
@@ -809,22 +1275,63 @@ window.__ModuleLoader__.load({
809
1275
  ? Math.max(1, Math.round((Date.parse(last.day) - Date.parse(first.day)) / 86400000))
810
1276
  : 0
811
1277
  const perDay = first && last ? Math.round((last.count - first.count) / Math.max(span, 1)) : 0
1278
+ const query = marketQuery.trim()
1279
+ const searchBox = h('input', {
1280
+ type: 'search',
1281
+ className: 'dsh-pl-search',
1282
+ placeholder: textOf(t, 'list.marketSearch'),
1283
+ value: marketQuery,
1284
+ onChange: (event) => onMarketQuery(event.target.value),
1285
+ })
1286
+ if (query) {
1287
+ const items = Array.isArray(searchResult) ? searchResult : []
1288
+ return h('div', { className: 'dsh-pl-market' },
1289
+ searchBox,
1290
+ searchStatus === 'loading'
1291
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.marketSearching'))
1292
+ : searchStatus === 'error'
1293
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.searchError'))
1294
+ : items.length === 0
1295
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.marketNoMatch'))
1296
+ : h('div', null,
1297
+ h('div', { className: 'dsh-pl-block-head' },
1298
+ h('strong', null, textOf(t, 'list.marketHits').replace('{n}', String(items.length)))),
1299
+ h('div', { className: `dsh-pl-grid${view === 'card' ? ' is-card' : ''}` },
1300
+ items.map((row, index) => h(PluginCard, {
1301
+ key: row.name || index,
1302
+ icon: null,
1303
+ title: row.name,
1304
+ subtitle: row.summary || row.category || '',
1305
+ className: 'is-market',
1306
+ onClick: () => setSelectedMarket(row),
1307
+ actions: [hotAction(row)],
1308
+ })))))
1309
+ }
812
1310
  return h('div', { className: 'dsh-pl-market' },
813
- h('div', { className: 'dsh-pl-market-head' },
814
- h('strong', null, textOf(t, 'list.marketTitle')),
815
- h('p', { className: 'dsh-pl-note' }, textOf(t, 'list.marketHint'))),
1311
+ searchBox,
816
1312
  trendStatus === 'loading'
817
1313
  ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.marketLoading'))
818
1314
  : trendStatus === 'error'
819
1315
  ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.marketError'))
820
1316
  : days.length
821
1317
  ? [
822
- h('div', { className: 'dsh-pl-market-stats' },
823
- stat(textOf(t, 'list.countNow'), last.count.toLocaleString()),
824
- stat(textOf(t, 'list.countFiltered'), (last.rejected || 0).toLocaleString()),
825
- stat(textOf(t, 'list.perDay'), `+${perDay.toLocaleString()}`),
826
- stat(textOf(t, 'list.spanDays'), String(days.length))),
827
- trendChart(days),
1318
+ h('section', { className: 'dsh-pl-block', key: 'trend' },
1319
+ h('div', { className: 'dsh-pl-block-head' },
1320
+ h('strong', null, textOf(t, 'list.chartTitle')),
1321
+ h('div', { className: 'dsh-pl-seg', role: 'group', 'aria-label': 'chart' },
1322
+ chip('cumulative', textOf(t, 'list.chartCumulative'), chartMode, setChartMode),
1323
+ chip('daily', textOf(t, 'list.chartDaily'), chartMode, setChartMode))),
1324
+ h('div', { className: 'dsh-pl-market-stats' },
1325
+ stat(textOf(t, 'list.countNow'), last.count.toLocaleString()),
1326
+ stat(textOf(t, 'list.countFiltered'), (last.rejected || 0).toLocaleString()),
1327
+ stat(textOf(t, 'list.perDay'), `+${perDay.toLocaleString()}`),
1328
+ stat(textOf(t, 'list.spanDays'), String(days.length))),
1329
+ h('div', { className: 'dsh-pl-chart-wrap' },
1330
+ chartMode === 'daily'
1331
+ ? barChart(days, hoverIndex, setHoverIndex)
1332
+ : trendChart(days, hoverIndex, setHoverIndex, textOf(t, 'list.marketFirstNote'))),
1333
+ h('p', { className: 'dsh-pl-note' }, chartMode === 'daily' ? textOf(t, 'list.chartDailyHint') : textOf(t, 'list.marketHint'))),
1334
+ hotView(),
828
1335
  ]
829
1336
  : null)
830
1337
  }
@@ -840,21 +1347,28 @@ window.__ModuleLoader__.load({
840
1347
  h('header', { className: 'dsh-pl-bar' },
841
1348
  h('div', { className: 'dsh-pl-tools' },
842
1349
  h('div', { className: 'dsh-pl-seg', role: 'group', 'aria-label': 'filter' },
843
- chip('installed', textOf(t, 'list.installed'), filter, (id) => { setFilter(id); remember({ filter: id }) }),
844
- chip('builtin', textOf(t, 'list.builtin'), filter, (id) => { setFilter(id); remember({ filter: id }) }),
845
- chip('all', textOf(t, 'list.all'), filter, (id) => { setFilter(id); remember({ filter: id }) })),
846
- h('button', { type: 'button', onClick: () => { void load() } }, textOf(t, 'list.refresh')),
1350
+ chip('installed', textOf(t, 'list.installed'), surface === 'market' ? '' : filter, (id) => { setSurface('plugins'); setFilter(id); remember({ filter: id }) }),
1351
+ chip('builtin', textOf(t, 'list.builtin'), surface === 'market' ? '' : filter, (id) => { setSurface('plugins'); setFilter(id); remember({ filter: id }) }),
1352
+ chip('market', textOf(t, 'list.market'), surface === 'market' ? 'market' : '', () => setSurface('market'))),
847
1353
  h('button', {
848
1354
  type: 'button',
849
- className: `dsh-pl-market-btn${surface === 'market' ? ' is-on' : ''}`,
850
- onClick: () => setSurface(surface === 'market' ? 'plugins' : 'market'),
851
- }, surface === 'market' ? textOf(t, 'list.back') : textOf(t, 'list.market')),
1355
+ className: 'dsh-pl-refresh',
1356
+ onClick: () => {
1357
+ if (surface === 'market') {
1358
+ try { window.localStorage.removeItem('dsh.plugin-list.market.v1') } catch { /* ignore */ }
1359
+ setTrendStatus('')
1360
+ setHotStatus('')
1361
+ } else {
1362
+ void load()
1363
+ }
1364
+ },
1365
+ }, textOf(t, 'list.refresh')),
852
1366
  h('div', { className: 'dsh-pl-view', role: 'group', 'aria-label': 'view' },
853
1367
  chip('list', textOf(t, 'list.viewList'), view, (id) => { setView(id); remember({ layout: id }) }),
854
1368
  chip('card', textOf(t, 'list.viewCard'), view, (id) => { setView(id); remember({ layout: id }) })),
855
1369
  h('span', { className: 'dsh-pl-version' }, VERSION))),
856
1370
  surface === 'market'
857
- ? marketView()
1371
+ ? selectedMarket ? marketDetail(selectedMarket) : marketView()
858
1372
  : status === 'loading' && plugins.length === 0
859
1373
  ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.loading'))
860
1374
  : status === 'unavailable'
@@ -871,66 +1385,80 @@ window.__ModuleLoader__.load({
871
1385
  ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.empty'))
872
1386
  : visible.map((plugin) => {
873
1387
  const title = pluginTitle(plugin, localeId)
874
- const icon = safeIcon(plugin.meta && plugin.meta.icon)
1388
+ const iconUrl = safeIcon(plugin.meta && plugin.meta.icon)
875
1389
  const kind = tabKindFor(plugin, guide, localeId)
876
1390
  const pinned = pins.includes(plugin.entryId)
877
- return h('div', {
1391
+ const places = placesFor(plugin, guide, seats, localeId)
1392
+ .map((place) => textOf(t, place === 'right' ? 'list.placeRight' : place === 'settings' ? 'list.placeSettings' : 'list.placeLeft'))
1393
+ .join(' · ') || textOf(t, 'list.placeNone')
1394
+ const classes = [
1395
+ selectedId === plugin.entryId ? 'is-on' : '',
1396
+ pinned ? 'is-pinned' : '',
1397
+ plugin.fiberPhase === 'failed' ? 'is-failed' : '',
1398
+ ].filter(Boolean).join(' ')
1399
+ return h(PluginCard, {
878
1400
  key: plugin.entryId,
879
- className: `dsh-pl-item${selectedId === plugin.entryId ? ' is-on' : ''}${pinned ? ' is-pinned' : ''}${plugin.fiberPhase === 'failed' ? ' is-failed' : ''}`,
880
- role: 'button',
881
- tabIndex: 0,
1401
+ icon: iconUrl ? h('img', { src: iconUrl, alt: '', draggable: false }) : null,
1402
+ title,
1403
+ subtitle: places,
1404
+ className: classes,
882
1405
  onClick: () => setSelectedId(plugin.entryId),
883
- onKeyDown: (event) => {
884
- if (event.key === 'Enter' || event.key === ' ') {
885
- event.preventDefault()
886
- setSelectedId(plugin.entryId)
887
- }
888
- },
889
- },
890
- icon
891
- ? h('img', { src: icon, alt: '', draggable: false })
892
- : h(DefaultPluginIcon),
893
- h('span', { className: 'dsh-pl-copy' },
894
- h('strong', null, title),
895
- h('span', { className: 'dsh-pl-where' },
896
- (placesFor(plugin, guide, seats, localeId).map((place) => textOf(t, place === 'right' ? 'list.placeRight' : place === 'settings' ? 'list.placeSettings' : 'list.placeLeft')).join(' · ')
897
- || textOf(t, 'list.placeNone')))),
898
- h('div', { className: 'dsh-pl-acts' },
899
- h('button', {
900
- type: 'button',
901
- className: 'dsh-pl-pin',
902
- 'aria-pressed': pinned,
903
- onClick: (event) => {
904
- event.stopPropagation()
905
- togglePin(plugin.entryId)
906
- },
907
- }, pinned ? textOf(t, 'list.unpin') : textOf(t, 'list.pin')),
908
- kind ? h('button', {
909
- type: 'button',
910
- className: 'dsh-pl-open',
911
- onClick: (event) => {
912
- event.stopPropagation()
913
- if (typeof openPanel === 'function') openPanel(kind)
914
- },
915
- }, textOf(t, 'list.open')) : null))
1406
+ actions: [
1407
+ h('button', {
1408
+ key: 'pin',
1409
+ type: 'button',
1410
+ className: 'dsh-pl-card-btn',
1411
+ 'aria-pressed': pinned,
1412
+ onClick: (event) => {
1413
+ event.stopPropagation()
1414
+ togglePin(plugin.entryId)
1415
+ },
1416
+ }, pinned ? textOf(t, 'list.unpin') : textOf(t, 'list.pin')),
1417
+ kind ? h('button', {
1418
+ key: 'open',
1419
+ type: 'button',
1420
+ className: 'dsh-pl-card-btn',
1421
+ onClick: (event) => {
1422
+ event.stopPropagation()
1423
+ if (typeof openPanel === 'function') openPanel(kind)
1424
+ },
1425
+ }, textOf(t, 'list.open')) : null,
1426
+ ],
1427
+ })
916
1428
  }))))
917
1429
  }
918
1430
 
919
1431
  const CSS = `
920
1432
  .dsh-pl, .dsh-pl * { box-sizing: border-box; }
921
- .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; }
1433
+ .dsh-pl {
1434
+ --pl-fs-xs: 11px;
1435
+ --pl-fs-sm: 12px;
1436
+ --pl-fs-md: 13px;
1437
+ --pl-fs-lg: 14px;
1438
+ --pl-fs-title: 16px;
1439
+ --pl-fs-num: 18px;
1440
+ --pl-gap-1: 4px;
1441
+ --pl-gap-2: 8px;
1442
+ --pl-gap-3: 12px;
1443
+ --pl-radius-card: 16px;
1444
+ --pl-radius-inner: 12px;
1445
+ --pl-radius-chip: 10px;
1446
+ --pl-radius-pill: 7px;
1447
+ }
1448
+ .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-family: inherit; font-size: var(--pl-fs-md); line-height: 1.4; }
922
1449
  .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; }
923
1450
  .dsh-pl-top { display: flex; align-items: center; justify-content: space-between; }
924
- .dsh-pl-version, .dsh-pl-hint, .dsh-pl-item span { color: var(--dsw-alias-label-secondary); font-size: 11px; }
1451
+ .dsh-pl-version, .dsh-pl-hint, .dsh-pl-item span { color: var(--dsw-alias-label-secondary); font-size: var(--pl-fs-xs); }
925
1452
  .dsh-pl-hint { margin: 0; }
926
1453
  .dsh-pl-tools { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; width: 100%; }
927
- .dsh-pl-version { margin-left: 8px; }
928
- .dsh-pl-view { display: flex; gap: 2px; margin-left: auto; padding: 2px; border-radius: 8px; background: var(--dsw-alias-bg-layer-2); }
929
- .dsh-pl-view button { border: 0; border-radius: 6px; padding: 3px 8px; font-size: 11px; background: transparent; color: var(--dsw-alias-label-secondary); }
1454
+ .dsh-pl-refresh { margin-left: auto; }
1455
+ .dsh-pl-version { margin-left: 10px; }
1456
+ .dsh-pl-view { display: flex; gap: 2px; padding: 2px; border-radius: var(--pl-radius-inner); background: var(--dsw-alias-bg-layer-2); }
1457
+ .dsh-pl-view button { border: 0; border-radius: var(--pl-radius-pill); padding: 3px 8px; font-size: var(--pl-fs-xs); background: transparent; color: var(--dsw-alias-label-secondary); }
930
1458
  .dsh-pl-view button.is-on { background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-weight: 600; }
931
- .dsh-pl-seg { display: flex; gap: 2px; padding: 3px; border: 0; border-radius: 10px; background: var(--dsw-alias-bg-layer-2); }
932
- .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; }
933
- .dsh-pl-seg button { border: 0; border-radius: 7px; background: transparent; color: var(--dsw-alias-label-secondary); padding: 5px 12px; }
1459
+ .dsh-pl-seg { display: flex; gap: 2px; padding: 3px; border: 0; border-radius: var(--pl-radius-inner); background: var(--dsw-alias-bg-layer-2); }
1460
+ .dsh-pl button { font: inherit; font-size: var(--pl-fs-sm); 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: var(--pl-radius-chip); padding: 6px 12px; cursor: pointer; }
1461
+ .dsh-pl-seg button { border: 0; border-radius: var(--pl-radius-pill); background: transparent; color: var(--dsw-alias-label-secondary); padding: 5px 12px; }
934
1462
  .dsh-pl-seg button.is-on { background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-weight: 600; box-shadow: none; }
935
1463
  .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); }
936
1464
  .dsh-pl-body { display: flex; flex: 1 1 auto; min-height: 0; min-width: 0; overflow: hidden; }
@@ -942,54 +1470,67 @@ window.__ModuleLoader__.load({
942
1470
  .dsh-pl-acts { display: flex; gap: 6px; align-items: center; margin-left: auto; flex: none; }
943
1471
  .dsh-pl-item { position: relative; }
944
1472
  .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; }
945
- .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); }
1473
+ .dsh-pl-grid.is-card .dsh-pl-acts .dsh-pl-card-btn { padding: 4px 8px; font-size: var(--pl-fs-xs); border: 0; background: var(--dsw-alias-bg-layer-2); }
946
1474
  .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; }
947
- .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; }
948
- .dsh-pl-open, .dsh-pl-pin { margin-left: 0; flex: none; position: static; }
1475
+ .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: var(--pl-radius-card); background: var(--dsw-alias-bg-layer-1); overflow: hidden; }
1476
+ .dsh-pl-card-btn { margin-left: 0; flex: none; position: static; }
949
1477
  .dsh-pl-item.is-pinned { background: color-mix(in srgb, var(--dsw-alias-label-primary) 7%, var(--dsw-alias-bg-layer-1)); }
950
- .dsh-pl-item img, .dsh-pl-fallback { width: 22px; height: 22px; border-radius: 6px; object-fit: cover; flex: none; }
1478
+ .dsh-pl-item img, .dsh-pl-fallback { width: 22px; height: 22px; border-radius: var(--pl-radius-pill); object-fit: cover; flex: none; }
951
1479
  .dsh-pl-copy { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 2px; }
952
- .dsh-pl-copy strong { font-size: 14px; font-weight: 560; color: var(--dsw-alias-label-primary); white-space: normal; overflow-wrap: anywhere; }
953
- .dsh-pl-where { color: var(--dsw-alias-label-secondary); font-size: 11px; }
1480
+ .dsh-pl-copy strong { font-size: var(--pl-fs-lg); font-weight: 560; color: var(--dsw-alias-label-primary); white-space: normal; overflow-wrap: anywhere; }
1481
+ .dsh-pl-where { color: var(--dsw-alias-label-secondary); font-size: var(--pl-fs-xs); }
1482
+ .dsh-pl-item.is-market .dsh-pl-where { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1483
+ .dsh-pl-item.is-market .dsh-pl-copy strong { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
954
1484
  .dsh-pl-item.is-failed { border-color: var(--dsw-alias-state-error-primary); }
955
1485
  .dsh-pl-state { margin: 16px; color: var(--dsw-alias-label-secondary); }
956
1486
  .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; }
957
1487
  .dsh-pl-detail-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
958
- .dsh-pl-detail-head strong { flex: 1 1 auto; min-width: 0; font-size: 16px; line-height: 1.35; overflow-wrap: anywhere; }
1488
+ .dsh-pl-detail-head strong { flex: 1 1 auto; min-width: 0; font-size: var(--pl-fs-title); line-height: 1.35; overflow-wrap: anywhere; }
959
1489
  .dsh-pl-detail-head button { flex: none; white-space: nowrap; align-self: flex-start; }
960
1490
  .dsh-pl-detail-head img, .dsh-pl-detail-head .dsh-pl-fallback { width: 32px; height: 32px; }
961
1491
  .dsh-pl-fact { display: grid; grid-template-columns: 4.5em minmax(0, 1fr); column-gap: 12px; align-items: start; margin: 0 0 10px; }
962
- .dsh-pl-fact-label { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.45; }
963
- .dsh-pl-fact-value { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; }
964
- .dsh-pl-detail h3 { margin: 14px 0 6px; font-size: 12px; color: var(--dsw-alias-label-secondary); }
965
- .dsh-pl-detail p, .dsh-pl-doc { margin: 0 0 8px; font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; }
966
- .dsh-pl-note { color: var(--dsw-alias-label-secondary); }
1492
+ .dsh-pl-fact-label { color: var(--dsw-alias-label-secondary); font-size: var(--pl-fs-sm); line-height: 1.45; }
1493
+ .dsh-pl-fact-value { color: var(--dsw-alias-label-primary); font-size: var(--pl-fs-md); line-height: 1.45; overflow-wrap: anywhere; }
1494
+ .dsh-pl-detail h3 { margin: 14px 0 6px; font-size: var(--pl-fs-sm); color: var(--dsw-alias-label-secondary); }
1495
+ .dsh-pl-detail p, .dsh-pl-doc { margin: 0 0 8px; font-size: var(--pl-fs-md); line-height: 1.45; overflow-wrap: anywhere; }
1496
+ .dsh-pl-note { color: var(--dsw-alias-label-secondary); font-size: var(--pl-fs-sm); line-height: 1.5; }
967
1497
  .dsh-pl-doc-block { margin-top: 12px; }
968
- .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); }
969
- .dsh-pl-md h4 { margin: 10px 0 4px; font-size: 13px; font-weight: 650; }
970
- .dsh-pl-md p, .dsh-pl-md li { margin: 0 0 8px; font-size: 13px; line-height: 1.55; }
1498
+ .dsh-pl-md h3, .dsh-pl-md-h { margin: 14px 0 6px; font-size: var(--pl-fs-lg); line-height: 1.4; font-weight: 650; color: var(--dsw-alias-label-primary); }
1499
+ .dsh-pl-md h4 { margin: 10px 0 4px; font-size: var(--pl-fs-md); font-weight: 650; }
1500
+ .dsh-pl-md p, .dsh-pl-md li { margin: 0 0 8px; font-size: var(--pl-fs-md); line-height: 1.55; }
971
1501
  .dsh-pl-md ul { margin: 0 0 10px; padding-left: 18px; }
972
- .dsh-pl-md code { font-size: 12px; padding: 0 4px; border-radius: 4px; background: var(--dsw-alias-bg-layer-2); }
1502
+ .dsh-pl-md code { font-size: var(--pl-fs-sm); padding: 0 4px; border-radius: 4px; background: var(--dsw-alias-bg-layer-2); }
973
1503
  .dsh-pl-md a { color: var(--dsw-alias-brand-primary); }
974
- .dsh-pl-table-wrap { margin: 8px 0 14px; overflow-x: auto; border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; }
975
- .dsh-pl-md table { width: 100%; border-collapse: collapse; font-size: 13px; }
1504
+ .dsh-pl-table-wrap { margin: 8px 0 14px; overflow-x: auto; border: 1px solid var(--dsw-alias-border-l1); border-radius: var(--pl-radius-chip); }
1505
+ .dsh-pl-md table { width: 100%; border-collapse: collapse; font-size: var(--pl-fs-md); }
976
1506
  .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); }
977
1507
  .dsh-pl-md th { color: var(--dsw-alias-label-secondary); font-weight: 650; background: var(--dsw-alias-bg-layer-1); white-space: nowrap; }
978
1508
  .dsh-pl-md td:first-child { white-space: nowrap; }
979
1509
  .dsh-pl-md tr:last-child td { border-bottom: 0; }
980
- .dsh-pl-market-btn.is-on { background: var(--dsw-alias-bg-layer-2); }
981
- .dsh-pl-market { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 16px 18px 28px; }
982
- .dsh-pl-market-head strong { display: block; font-size: 15px; margin-bottom: 6px; }
983
- .dsh-pl-market-stats { display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0 8px; }
984
- .dsh-pl-stat { display: flex; flex-direction: column; gap: 4px; min-width: 118px; padding: 10px 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); }
985
- .dsh-pl-stat span { font-size: 11px; color: var(--dsw-alias-label-secondary); }
986
- .dsh-pl-stat strong { font-size: 20px; font-weight: 600; }
987
- .dsh-pl-chart { display: block; width: 100%; height: auto; margin-top: 12px; }
1510
+ .dsh-pl-market { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 12px; }
1511
+ .dsh-pl-market-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-bottom: 12px; }
1512
+ .dsh-pl-stat { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--pl-radius-card); background: var(--dsw-alias-bg-layer-1); }
1513
+ .dsh-pl-stat span { font-size: var(--pl-fs-xs); color: var(--dsw-alias-label-secondary); }
1514
+ .dsh-pl-stat strong { font-size: var(--pl-fs-num); font-weight: 600; line-height: 1.2; }
1515
+ .dsh-pl-chart-wrap { padding: 10px 12px 4px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--pl-radius-card); background: var(--dsw-alias-bg-layer-1); }
1516
+ .dsh-pl-chart { display: block; width: 100%; height: auto; }
1517
+ .dsh-pl-block-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
1518
+ .dsh-pl-block-head strong { font-size: var(--pl-fs-lg); }
1519
+ .dsh-pl-block-head span { font-size: var(--pl-fs-xs); color: var(--dsw-alias-label-secondary); }
1520
+ .dsh-pl-block { margin-bottom: 22px; }
1521
+ .dsh-pl-market .dsh-pl-note { margin: 10px 2px 0; }
1522
+ .dsh-pl-hot { margin-top: 4px; }
1523
+ .dsh-pl-search { width: 100%; margin: 2px 0 8px; padding: 8px 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--pl-radius-inner); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-family: inherit; font-size: var(--pl-fs-sm); }
1524
+ .dsh-pl-search::placeholder { color: var(--dsw-alias-label-secondary); }
1525
+ .dsh-pl-search:focus { outline: none; border-color: var(--dsw-alias-brand-primary); }
1526
+ .dsh-pl-delta { flex: none; margin-left: auto; font-size: var(--pl-fs-sm); font-weight: 600; color: var(--dsw-alias-brand-primary); }
1527
+ .dsh-pl-busy { flex: none; font-size: var(--pl-fs-xs); color: var(--dsw-alias-label-secondary); }
1528
+ .dsh-pl-hot-busy { flex: none; font-size: var(--pl-fs-xs); color: var(--dsw-alias-label-secondary); }
988
1529
  .dsh-pl-actions { display: flex; gap: 8px; align-items: center; margin-top: 16px; }
989
1530
  `
990
1531
 
991
1532
  return {
992
- name: 'dsh-plugins-ui',
1533
+ name: 'dsh-plugin-panel',
993
1534
  inject: ['slots', 'sidebarRightTabs', 'sidebarRight', 'layout', 'locale', 'remote', 'remote.pluginManager', 'remote.session', 'connection'],
994
1535
  apply(ctx) {
995
1536
  ctx.effect(() => ctx.locale.register(NS, COPY), 'plugin-list: dictionaries')
@@ -1002,6 +1543,10 @@ window.__ModuleLoader__.load({
1002
1543
  const rpcCall = ctx.connection.rpc.call.bind(ctx.connection.rpc)
1003
1544
  const loadFacts = (payload) => rpcCall('/dsh-plugins-ui-rpc', 'facts', payload || {})
1004
1545
  const loadMarketTrend = (payload) => rpcCall('/dsh-plugins-ui-rpc', 'trend', payload || {})
1546
+ const loadMarketHot = (payload) => rpcCall('/dsh-plugins-ui-rpc', 'hot', payload || {})
1547
+ const searchCatalog = (payload) => rpcCall('/dsh-plugins-ui-rpc', 'search', payload || {})
1548
+ const installBundle = pluginManager.installBundle.bind(pluginManager)
1549
+ const removeBundle = pluginManager.removeBundle.bind(pluginManager)
1005
1550
  const revealPackage = async (payload) => {
1006
1551
  const input = payload && typeof payload === 'object' ? payload : {}
1007
1552
  const dir = directoryFromSource(input.source)
@@ -1085,7 +1630,7 @@ window.__ModuleLoader__.load({
1085
1630
  name: 'sidebar.right.pane.tab',
1086
1631
  key: PANEL_ID,
1087
1632
  locale: NS,
1088
- inject: () => ({ listPlugins, listBundles, readBundleDetail, setPluginEnabled, loadFacts, revealPackage, loadMarketTrend, listGuide, subscribeGuide, listSeats, subscribeSeats, openPanel, t, readLocale, subscribeLocale }),
1633
+ inject: () => ({ listPlugins, listBundles, readBundleDetail, setPluginEnabled, installBundle, removeBundle, loadFacts, revealPackage, loadMarketTrend, loadMarketHot, searchCatalog, listGuide, subscribeGuide, listSeats, subscribeSeats, openPanel, t, readLocale, subscribeLocale }),
1089
1634
  }, ListView)), 'plugin-list: tab body')
1090
1635
  },
1091
1636
  }