@stolyarovmn/dsh-ui-registry-aggregator 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -52
- package/client.js +360 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
# Registry Aggregator for DeepSeek Harness 0.2.x
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Registry Aggregator adds federated plugin discovery and update discovery around the native DeepSeek Harness Plugin Manager.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
This is the clean DSH `0.2.0` implementation line. It does **not** carry DSH `0.1.x` compatibility code.
|
|
6
|
+
|
|
7
|
+
Current stable version: `0.5.2`
|
|
8
|
+
Current validation baseline: DSH `v0.2.0-rc.2`
|
|
9
|
+
Declared DSH compatibility: `>=0.2.0-rc.2 <0.3.0`
|
|
10
|
+
|
|
11
|
+
Compatibility badges are evaluated against the DSH version that is actually running rather than a hardcoded RC value.
|
|
12
|
+
|
|
13
|
+
## Screenshot
|
|
14
|
+
|
|
15
|
+
Real Harness UI, manually verified on DSH `0.2.0-rc.2`:
|
|
16
|
+
|
|
17
|
+

|
|
7
18
|
|
|
8
19
|
## Install from npm
|
|
9
20
|
|
|
@@ -12,62 +23,84 @@ $DSH_VERSION = "0.2.0-rc.2"
|
|
|
12
23
|
$PROFILE = "dsh-020-test"
|
|
13
24
|
$env:DSH_HOME = "$env:USERPROFILE\.dsh-020-test"
|
|
14
25
|
|
|
15
|
-
pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" plugin --profile $PROFILE add "@stolyarovmn/dsh-ui-registry-aggregator@0.5.
|
|
26
|
+
pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" plugin --profile $PROFILE add "@stolyarovmn/dsh-ui-registry-aggregator@0.5.2"
|
|
16
27
|
pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" --profile $PROFILE
|
|
17
28
|
```
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
## What it adds
|
|
31
|
+
|
|
32
|
+
The native Plugin Manager remains responsible for installation, enable/disable, uninstall, and bundle lifecycle. Registry Aggregator adds discovery, compatibility evidence, metadata, and update orchestration.
|
|
33
|
+
|
|
34
|
+
### Main Plugins page
|
|
35
|
+
|
|
36
|
+
On DSH `0.2.0-rc.2`, Registry Aggregator extends the main Plugins list with:
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
- `Installed | Sources | Browse | Updates` navigation using the native Installed heading/count visual language;
|
|
39
|
+
- the original native Installed cards as the **Installed** view;
|
|
40
|
+
- **Sources**, **Browse**, and **Updates** directly on the main Plugins page;
|
|
41
|
+
- a numeric update count beside **Updates**;
|
|
42
|
+
- `Update → vX.Y.Z` directly on an installed package when a newer eligible version is available;
|
|
43
|
+
- **Update all** for eligible updates, processed sequentially with Registry Aggregator updating itself last;
|
|
44
|
+
- automatic teardown of the main-page surface when the Plugin Manager opens a bundle/detail page.
|
|
22
45
|
|
|
23
|
-
The
|
|
46
|
+
The supported `plugins.bundle.config` detail view remains available as a fallback inside **Installed → Registry Aggregator**.
|
|
24
47
|
|
|
25
|
-
|
|
48
|
+
> **DSH 0.2.0-rc.2 integration note:** stock rc.2 does not expose a list-level Plugin Manager slot. The main-page integration is therefore an explicit rc.2 compatibility bridge. It waits until the native Plugin Manager inventory has finished loading, does not use a `MutationObserver`, and removes itself on detail pages. When DSH exposes an appropriate list-level slot, this bridge should be replaced by the supported slot API.
|
|
49
|
+
|
|
50
|
+
### Sources
|
|
26
51
|
|
|
27
|
-
- native `plugins.bundle.config` integration;
|
|
28
|
-
- `Sources | Browse | Updates` navigation;
|
|
29
|
-
- live source configuration through `ctx.configForms.get('registry-aggregator')`; the bundle slot is presentation-only because DSH does not guarantee a single `form` for bundle-wide pages;
|
|
30
52
|
- built-in npm and GitHub sources;
|
|
31
53
|
- custom JSON and corporate catalog sources;
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
- compatibility
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
54
|
+
- enable/disable, add, remove, refresh, copy-address, package/repository count, health, and error state;
|
|
55
|
+
- live configuration through `ctx.configForms.get('registry-aggregator')`;
|
|
56
|
+
- authenticated Host RPC through the DSH Connection service;
|
|
57
|
+
- bounded responses, request timeouts, redirect validation, DNS pinning, and private-network blocking.
|
|
58
|
+
|
|
59
|
+
### Browse
|
|
60
|
+
|
|
61
|
+
- federated search across enabled npm, GitHub, custom JSON, and corporate sources;
|
|
62
|
+
- popular/default results when the query is empty;
|
|
63
|
+
- compact source, release, freshness, tag, compatibility, and page-size filters;
|
|
64
|
+
- combinable sorting by relevance, stars, downloads, freshness, and name;
|
|
65
|
+
- multi-criterion percentile-normalized composite ranking;
|
|
66
|
+
- 20 / 50 / 100 pagination;
|
|
67
|
+
- explicit package versions;
|
|
68
|
+
- GitHub stars and release/repository freshness;
|
|
69
|
+
- npm `30d | total` download evidence resolved lazily for visible npm rows;
|
|
70
|
+
- package artwork from the DSH manifest icon contract, with safe repository-local README artwork fallback for npm packages that expose GitHub metadata;
|
|
71
|
+
- `Compatible`, `Incompatible`, and `Not verified` evidence against the actual running DSH version;
|
|
72
|
+
- compatibility filtering;
|
|
73
|
+
- icon-only one-click installation through native `remote.pluginManager.inspect()` → `installBundle()`;
|
|
74
|
+
- known-incompatible packages are disabled before installation is attempted.
|
|
75
|
+
|
|
76
|
+
### Updates
|
|
77
|
+
|
|
78
|
+
- update discovery from native installed bundles plus latest npm manifests;
|
|
79
|
+
- per-package update through the native Plugin Manager;
|
|
80
|
+
- package Update uses a two-arrow glyph while content/list Refresh keeps the single-arrow glyph;
|
|
81
|
+
- native installed artwork is reused when available;
|
|
82
|
+
- sequential **Update all**;
|
|
83
|
+
- explicitly incompatible update targets remain disabled;
|
|
84
|
+
- Registry Aggregator updates itself last so it cannot interrupt the remaining queue.
|
|
60
85
|
|
|
61
86
|
## Architecture
|
|
62
87
|
|
|
63
88
|
```text
|
|
64
89
|
DSH Plugins
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
├─ Installed | Sources | Browse | Updates
|
|
91
|
+
│ ├─ Installed
|
|
92
|
+
│ │ ├─ native DSH package cards
|
|
93
|
+
│ │ └─ Update → vX.Y.Z
|
|
94
|
+
│ ├─ Sources
|
|
95
|
+
│ ├─ Browse
|
|
96
|
+
│ └─ Updates
|
|
97
|
+
│ └─ Update all
|
|
98
|
+
│
|
|
99
|
+
└─ Installed → Registry Aggregator
|
|
100
|
+
└─ plugins.bundle.config
|
|
101
|
+
├─ Sources
|
|
102
|
+
├─ Browse
|
|
103
|
+
└─ Updates
|
|
71
104
|
|
|
72
105
|
Sources UI
|
|
73
106
|
├─ ctx.configForms.get('registry-aggregator')
|
|
@@ -81,19 +114,33 @@ Sources UI
|
|
|
81
114
|
├─ custom-json
|
|
82
115
|
└─ corporate
|
|
83
116
|
|
|
84
|
-
|
|
117
|
+
Plugin lifecycle
|
|
85
118
|
└─ native DSH remote.pluginManager
|
|
86
119
|
├─ inspect(spec)
|
|
87
120
|
├─ installBundle(spec)
|
|
88
121
|
└─ listBundles() / plugin-manager/changed
|
|
89
|
-
|
|
90
|
-
Enable / disable / uninstall
|
|
91
|
-
└─ native DSH Plugin Manager
|
|
92
122
|
```
|
|
93
123
|
|
|
124
|
+
## Known limitations
|
|
125
|
+
|
|
126
|
+
- build-script approval remains in the native **Add plugin** dialog;
|
|
127
|
+
- **Update all** cancellation is not implemented; eligible updates run sequentially once started;
|
|
128
|
+
- automatic update discovery does not yet cover GitHub-only installed dependencies without npm package identity;
|
|
129
|
+
- the main Plugins-list surface is a version-pinned DSH `0.2.0-rc.2` compatibility bridge because rc.2 has no supported list-level Plugin Manager slot.
|
|
130
|
+
|
|
131
|
+
## Validation
|
|
132
|
+
|
|
133
|
+
`0.5.2` was promoted from the manually verified `0.5.2-rc.6` candidate after:
|
|
134
|
+
|
|
135
|
+
- JavaScript syntax checks;
|
|
136
|
+
- repository unit/client contract tests;
|
|
137
|
+
- `npm pack --dry-run` validation;
|
|
138
|
+
- isolated install smoke test against DSH `0.2.0-rc.2`;
|
|
139
|
+
- real Harness UI verification of the main Plugins-page Browse integration shown above.
|
|
140
|
+
|
|
94
141
|
## Test from GitHub
|
|
95
142
|
|
|
96
|
-
|
|
143
|
+
For unreleased development builds, use a commit SHA instead of publishing a test package:
|
|
97
144
|
|
|
98
145
|
```powershell
|
|
99
146
|
$DSH_VERSION = "0.2.0-rc.2"
|
|
@@ -105,8 +152,6 @@ pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" plugin --profile $PROFILE add "github:S
|
|
|
105
152
|
pnpm dlx "@deepseek-ai/dsh@$DSH_VERSION" --profile $PROFILE
|
|
106
153
|
```
|
|
107
154
|
|
|
108
|
-
Then open **Plugins → Installed → Registry Aggregator → Sources** and verify source state in the real Harness UI.
|
|
109
|
-
|
|
110
155
|
## Historical lines
|
|
111
156
|
|
|
112
157
|
- `dsh-0.1.7` — stable `v0.4.17` line for DSH `0.1.7-rc.2`.
|
package/client.js
CHANGED
|
@@ -86,6 +86,9 @@ window.__ModuleLoader__.load({
|
|
|
86
86
|
updateNone: 'Installed plugins are up to date',
|
|
87
87
|
updateNoneBody: 'No newer package versions were found in the connected registries.',
|
|
88
88
|
updateRefresh: 'Check for updates',
|
|
89
|
+
updatesAvailable: 'Updates: {count} available',
|
|
90
|
+
updateAll: 'Update all',
|
|
91
|
+
updatingAll: 'Updating all…',
|
|
89
92
|
downloads30d: 'npm downloads in the last 30 days',
|
|
90
93
|
downloadsTotal: 'npm total downloads',
|
|
91
94
|
filterSource: 'Source',
|
|
@@ -198,6 +201,9 @@ window.__ModuleLoader__.load({
|
|
|
198
201
|
updateNone: '已安装插件均为最新版本',
|
|
199
202
|
updateNoneBody: '在已连接的注册表中没有发现更高版本。',
|
|
200
203
|
updateRefresh: '检查更新',
|
|
204
|
+
updatesAvailable: '更新:{count} 个可用',
|
|
205
|
+
updateAll: '全部更新',
|
|
206
|
+
updatingAll: '正在全部更新…',
|
|
201
207
|
downloads30d: 'npm 最近 30 天下载量',
|
|
202
208
|
downloadsTotal: 'npm 总下载量',
|
|
203
209
|
filterSource: '来源',
|
|
@@ -241,6 +247,8 @@ window.__ModuleLoader__.load({
|
|
|
241
247
|
'.ra-root{box-sizing:border-box;min-width:0;max-width:100%;overflow-x:hidden;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;padding:2px 0 8px}',
|
|
242
248
|
'.ra-tabs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));width:min(100%,440px);padding:4px;margin:12px 0 24px;border-radius:12px;background:var(--dsw-alias-bg-module-platform,var(--dsw-alias-bg-layer-2))}',
|
|
243
249
|
'.ra-tab{height:34px;padding:0 14px;border:.5px solid transparent;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer}',
|
|
250
|
+
'.ra-tab-label{display:inline-flex;align-items:center;justify-content:center;gap:6px}',
|
|
251
|
+
'.ra-update-badge{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-foreground);font-size:10px;line-height:16px;font-weight:700}',
|
|
244
252
|
'.ra-tab:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
245
253
|
'.ra-tab:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}',
|
|
246
254
|
'.ra-tab[aria-selected=true]{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);font-weight:600}',
|
|
@@ -254,6 +262,7 @@ window.__ModuleLoader__.load({
|
|
|
254
262
|
'.ra-button:focus-visible,.ra-input:focus-visible,.ra-select:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}',
|
|
255
263
|
'.ra-button:disabled{opacity:.5;cursor:not-allowed}',
|
|
256
264
|
'.ra-button-primary{background:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-foreground)}',
|
|
265
|
+
'.ra-update-all{display:inline-flex;align-items:center;justify-content:center;gap:6px}',
|
|
257
266
|
'.ra-panel{border:.5px solid var(--dsw-alias-border-l4);border-radius:12px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}',
|
|
258
267
|
'.ra-add-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 13px}',
|
|
259
268
|
'.ra-add-form{display:grid;grid-template-columns:minmax(160px,1fr) minmax(130px,180px) minmax(220px,1.2fr) auto;gap:10px;padding:12px 13px;border-top:.5px solid var(--dsw-alias-border-l4);align-items:end}',
|
|
@@ -1678,12 +1687,12 @@ window.__ModuleLoader__.load({
|
|
|
1678
1687
|
)
|
|
1679
1688
|
}
|
|
1680
1689
|
|
|
1681
|
-
function
|
|
1690
|
+
function useUpdateDiscovery(enabled = true) {
|
|
1682
1691
|
const [revision, setRevision] = React.useState(0)
|
|
1683
|
-
const [state, setState] = React.useState({ loading:
|
|
1684
|
-
const [operations, setOperations] = React.useState({})
|
|
1692
|
+
const [state, setState] = React.useState({ loading: enabled, items: [], error: '' })
|
|
1685
1693
|
|
|
1686
1694
|
React.useEffect(() => {
|
|
1695
|
+
if (!enabled) return undefined
|
|
1687
1696
|
let active = true
|
|
1688
1697
|
const controller = new AbortController()
|
|
1689
1698
|
const load = async () => {
|
|
@@ -1725,36 +1734,76 @@ window.__ModuleLoader__.load({
|
|
|
1725
1734
|
controller.abort()
|
|
1726
1735
|
if (typeof dispose === 'function') dispose()
|
|
1727
1736
|
}
|
|
1728
|
-
}, [revision])
|
|
1737
|
+
}, [enabled, revision])
|
|
1738
|
+
|
|
1739
|
+
const refresh = React.useCallback(() => setRevision(value => value + 1), [])
|
|
1740
|
+
return { state, refresh }
|
|
1741
|
+
}
|
|
1729
1742
|
|
|
1730
|
-
|
|
1743
|
+
function UpdatesView({ t, discovery }) {
|
|
1744
|
+
const { state, refresh } = discovery
|
|
1745
|
+
const [operations, setOperations] = React.useState({})
|
|
1746
|
+
const [bulkUpdating, setBulkUpdating] = React.useState(false)
|
|
1747
|
+
|
|
1748
|
+
const runUpdate = async (item, refreshAfter = true) => {
|
|
1731
1749
|
const key = item.bundle.name
|
|
1732
1750
|
const current = operations[key]
|
|
1733
|
-
if (['starting', 'installing', 'applying'].includes(current?.phase)) return
|
|
1751
|
+
if (['starting', 'installing', 'applying'].includes(current?.phase)) return false
|
|
1734
1752
|
setOperations(value => ({ ...value, [key]: { phase: 'starting', error: '' } }))
|
|
1735
1753
|
try {
|
|
1736
1754
|
await updateInstalledPlugin(item.bundle, item.availableVersion, progress => {
|
|
1737
1755
|
setOperations(value => ({ ...value, [key]: { ...(value[key] ?? {}), ...progress, error: '' } }))
|
|
1738
1756
|
})
|
|
1739
1757
|
setOperations(value => ({ ...value, [key]: { phase: 'done', error: '' } }))
|
|
1740
|
-
|
|
1758
|
+
if (refreshAfter) refresh()
|
|
1759
|
+
return true
|
|
1741
1760
|
} catch (error) {
|
|
1742
1761
|
setOperations(value => ({ ...value, [key]: { phase: 'failed', error: String(error?.message ?? error) } }))
|
|
1762
|
+
return false
|
|
1743
1763
|
}
|
|
1744
1764
|
}
|
|
1745
1765
|
|
|
1766
|
+
const runUpdateAll = async () => {
|
|
1767
|
+
if (bulkUpdating || state.loading) return
|
|
1768
|
+
const queue = state.items
|
|
1769
|
+
.filter(item => item.metadata?.compatibility !== 'unsupported')
|
|
1770
|
+
.sort((left, right) => Number(left.bundle.name === PACKAGE) - Number(right.bundle.name === PACKAGE))
|
|
1771
|
+
if (!queue.length) return
|
|
1772
|
+
setBulkUpdating(true)
|
|
1773
|
+
try {
|
|
1774
|
+
for (const item of queue) await runUpdate(item, false)
|
|
1775
|
+
} finally {
|
|
1776
|
+
setBulkUpdating(false)
|
|
1777
|
+
refresh()
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const updatableCount = state.items.filter(item => item.metadata?.compatibility !== 'unsupported').length
|
|
1782
|
+
|
|
1746
1783
|
return h('section', { className: 'ra-section', 'aria-labelledby': 'ra-updates-title' },
|
|
1747
1784
|
h('div', { className: 'ra-section-head' },
|
|
1748
1785
|
h('div', null,
|
|
1749
1786
|
h('h3', { id: 'ra-updates-title', className: 'ra-heading' }, t('updatesTitle')),
|
|
1750
1787
|
h('p', { className: 'ra-lead' }, t('updatesLead')),
|
|
1751
1788
|
),
|
|
1752
|
-
h(
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1789
|
+
h('div', { className: 'ra-actions' },
|
|
1790
|
+
state.items.length > 1 ? h('button', {
|
|
1791
|
+
type: 'button',
|
|
1792
|
+
className: 'ra-button ra-update-all',
|
|
1793
|
+
title: bulkUpdating ? t('updatingAll') : t('updateAll'),
|
|
1794
|
+
disabled: state.loading || bulkUpdating || updatableCount === 0,
|
|
1795
|
+
onClick: () => { void runUpdateAll() },
|
|
1796
|
+
},
|
|
1797
|
+
h(IconUpdate, { size: 14 }),
|
|
1798
|
+
h('span', null, bulkUpdating ? t('updatingAll') : t('updateAll')),
|
|
1799
|
+
) : null,
|
|
1800
|
+
h(IconButton, {
|
|
1801
|
+
label: t('updateRefresh'),
|
|
1802
|
+
disabled: state.loading || bulkUpdating,
|
|
1803
|
+
onClick: refresh,
|
|
1804
|
+
icon: h(IconRefresh, { size: 16, className: state.loading ? 'ra-spin' : undefined }),
|
|
1805
|
+
}),
|
|
1806
|
+
),
|
|
1758
1807
|
),
|
|
1759
1808
|
state.loading
|
|
1760
1809
|
? h('div', { className: 'ra-plugin-meta', role: 'status' }, t('updateChecking'))
|
|
@@ -1799,7 +1848,7 @@ window.__ModuleLoader__.load({
|
|
|
1799
1848
|
h(IconButton, {
|
|
1800
1849
|
label,
|
|
1801
1850
|
state: done ? 'done' : failed ? 'error' : undefined,
|
|
1802
|
-
disabled: busy || done || incompatible,
|
|
1851
|
+
disabled: bulkUpdating || busy || done || incompatible,
|
|
1803
1852
|
onClick: () => { void runUpdate(item) },
|
|
1804
1853
|
icon: done
|
|
1805
1854
|
? h(IconCheck, { size: 16 })
|
|
@@ -1815,6 +1864,7 @@ window.__ModuleLoader__.load({
|
|
|
1815
1864
|
|
|
1816
1865
|
function RegistryAggregator({ t, view }) {
|
|
1817
1866
|
const [tab, setTab] = React.useState('sources')
|
|
1867
|
+
const updateDiscovery = useUpdateDiscovery(view === 'page')
|
|
1818
1868
|
const formState = React.useSyncExternalStore(
|
|
1819
1869
|
listener => sourceConfigForm.subscribe(listener),
|
|
1820
1870
|
() => sourceConfigForm.getSnapshot(),
|
|
@@ -1825,10 +1875,15 @@ window.__ModuleLoader__.load({
|
|
|
1825
1875
|
mutate: (operations, expectedRevision) => sourceConfigForm.mutate(operations, expectedRevision),
|
|
1826
1876
|
}), [formState])
|
|
1827
1877
|
if (view !== 'page') return null
|
|
1878
|
+
const updateCount = updateDiscovery.state.items.length
|
|
1879
|
+
const updateLabel = h('span', { className: 'ra-tab-label' },
|
|
1880
|
+
t('updates'),
|
|
1881
|
+
updateCount > 0 ? h('span', { className: 'ra-update-badge', 'aria-hidden': true }, updateCount > 99 ? '99+' : String(updateCount)) : null,
|
|
1882
|
+
)
|
|
1828
1883
|
const tabs = [
|
|
1829
|
-
['sources', t('sources')],
|
|
1830
|
-
['browse', t('browse')],
|
|
1831
|
-
['updates', t('updates')],
|
|
1884
|
+
['sources', t('sources'), t('sources')],
|
|
1885
|
+
['browse', t('browse'), t('browse')],
|
|
1886
|
+
['updates', updateLabel, updateCount > 0 ? format(t, 'updatesAvailable', { count: updateCount }) : t('updates')],
|
|
1832
1887
|
]
|
|
1833
1888
|
const selectRelative = delta => {
|
|
1834
1889
|
const index = tabs.findIndex(item => item[0] === tab)
|
|
@@ -1838,18 +1893,19 @@ window.__ModuleLoader__.load({
|
|
|
1838
1893
|
const body = tab === 'browse'
|
|
1839
1894
|
? h(BrowseView, { t })
|
|
1840
1895
|
: tab === 'updates'
|
|
1841
|
-
? h(UpdatesView, { t })
|
|
1896
|
+
? h(UpdatesView, { t, discovery: updateDiscovery })
|
|
1842
1897
|
: h(SourcesView, { t, form })
|
|
1843
1898
|
|
|
1844
1899
|
return h('div', { className: 'ra-root' },
|
|
1845
1900
|
h('style', null, css),
|
|
1846
1901
|
h('div', { className: 'ra-tabs', role: 'tablist', 'aria-label': 'Registry Aggregator' },
|
|
1847
|
-
...tabs.map(([id, label]) => h('button', {
|
|
1902
|
+
...tabs.map(([id, label, ariaLabel]) => h('button', {
|
|
1848
1903
|
key: id,
|
|
1849
1904
|
type: 'button',
|
|
1850
1905
|
role: 'tab',
|
|
1851
1906
|
className: 'ra-tab',
|
|
1852
1907
|
'aria-selected': tab === id,
|
|
1908
|
+
'aria-label': ariaLabel,
|
|
1853
1909
|
tabIndex: tab === id ? 0 : -1,
|
|
1854
1910
|
onClick: () => setTab(id),
|
|
1855
1911
|
onKeyDown: event => {
|
|
@@ -1867,6 +1923,290 @@ window.__ModuleLoader__.load({
|
|
|
1867
1923
|
)
|
|
1868
1924
|
}
|
|
1869
1925
|
|
|
1926
|
+
|
|
1927
|
+
// Temporary DSH 0.2.0-rc.2 compatibility surface.
|
|
1928
|
+
// Stock rc.2 has no supported list-level Plugin Manager slot, so this bridge
|
|
1929
|
+
// is deliberately version-pinned and mounts only after the native Installed
|
|
1930
|
+
// group exists. It never observes React DOM mutations and removes itself as
|
|
1931
|
+
// soon as the Plugin Manager navigates to a detail page.
|
|
1932
|
+
function PluginsPageBridgeSurface({ t, installed, cards }) {
|
|
1933
|
+
const [tab, setTab] = React.useState('installed')
|
|
1934
|
+
const discovery = useUpdateDiscovery(true)
|
|
1935
|
+
const [operations, setOperations] = React.useState({})
|
|
1936
|
+
const [bulkUpdating, setBulkUpdating] = React.useState(false)
|
|
1937
|
+
const formState = React.useSyncExternalStore(
|
|
1938
|
+
listener => sourceConfigForm.subscribe(listener),
|
|
1939
|
+
() => sourceConfigForm.getSnapshot(),
|
|
1940
|
+
() => sourceConfigForm.getSnapshot(),
|
|
1941
|
+
)
|
|
1942
|
+
const form = React.useMemo(() => ({
|
|
1943
|
+
state: formState,
|
|
1944
|
+
mutate: (operations, expectedRevision) => sourceConfigForm.mutate(operations, expectedRevision),
|
|
1945
|
+
}), [formState])
|
|
1946
|
+
|
|
1947
|
+
const installedCount = installed.querySelector('[data-plugin-count]')?.textContent
|
|
1948
|
+
?? String(installed.querySelectorAll('[data-plugin-package]').length)
|
|
1949
|
+
const updateCount = discovery.state.items.length
|
|
1950
|
+
const updatableCount = discovery.state.items.filter(item => item.metadata?.compatibility !== 'unsupported').length
|
|
1951
|
+
|
|
1952
|
+
const runUpdate = async (item, refreshAfter = true) => {
|
|
1953
|
+
const key = item.bundle.name
|
|
1954
|
+
const current = operations[key]
|
|
1955
|
+
if (['starting', 'installing', 'applying'].includes(current?.phase)) return false
|
|
1956
|
+
setOperations(value => ({ ...value, [key]: { phase: 'starting', error: '' } }))
|
|
1957
|
+
try {
|
|
1958
|
+
await updateInstalledPlugin(item.bundle, item.availableVersion, progress => {
|
|
1959
|
+
setOperations(value => ({ ...value, [key]: { ...(value[key] ?? {}), ...progress, error: '' } }))
|
|
1960
|
+
})
|
|
1961
|
+
setOperations(value => ({ ...value, [key]: { phase: 'done', error: '' } }))
|
|
1962
|
+
if (refreshAfter) discovery.refresh()
|
|
1963
|
+
return true
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
setOperations(value => ({ ...value, [key]: { phase: 'failed', error: String(error?.message ?? error) } }))
|
|
1966
|
+
return false
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
const runUpdateAll = async () => {
|
|
1971
|
+
if (bulkUpdating || discovery.state.loading) return
|
|
1972
|
+
const queue = discovery.state.items
|
|
1973
|
+
.filter(item => item.metadata?.compatibility !== 'unsupported')
|
|
1974
|
+
.sort((left, right) => Number(left.bundle.name === PACKAGE) - Number(right.bundle.name === PACKAGE))
|
|
1975
|
+
if (!queue.length) return
|
|
1976
|
+
setBulkUpdating(true)
|
|
1977
|
+
try {
|
|
1978
|
+
for (const item of queue) await runUpdate(item, false)
|
|
1979
|
+
} finally {
|
|
1980
|
+
setBulkUpdating(false)
|
|
1981
|
+
discovery.refresh()
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
React.useEffect(() => {
|
|
1986
|
+
cards.style.display = tab === 'installed' ? '' : 'none'
|
|
1987
|
+
return () => { cards.style.display = '' }
|
|
1988
|
+
}, [cards, tab])
|
|
1989
|
+
|
|
1990
|
+
const updateSignature = discovery.state.items
|
|
1991
|
+
.map(item => [item.bundle?.name, item.bundle?.version, item.availableVersion, item.metadata?.compatibility].join(':'))
|
|
1992
|
+
.join('|')
|
|
1993
|
+
const operationSignature = Object.entries(operations)
|
|
1994
|
+
.map(([key, value]) => [key, value?.phase, value?.error].join(':'))
|
|
1995
|
+
.join('|')
|
|
1996
|
+
|
|
1997
|
+
React.useEffect(() => {
|
|
1998
|
+
const updateByName = new Map(discovery.state.items.map(item => [item.bundle?.name, item]))
|
|
1999
|
+
const created = []
|
|
2000
|
+
for (const card of installed.querySelectorAll('[data-plugin-package]')) {
|
|
2001
|
+
const packageName = card.getAttribute('data-plugin-package')
|
|
2002
|
+
const item = updateByName.get(packageName)
|
|
2003
|
+
const old = card.querySelector('[data-ra-installed-update]')
|
|
2004
|
+
if (old) old.remove()
|
|
2005
|
+
if (!item?.availableVersion) continue
|
|
2006
|
+
|
|
2007
|
+
const toggle = card.querySelector('[role="switch"]')
|
|
2008
|
+
const actionHost = toggle?.parentElement
|
|
2009
|
+
if (!actionHost) continue
|
|
2010
|
+
|
|
2011
|
+
const operation = operations[packageName] ?? {}
|
|
2012
|
+
const busy = bulkUpdating || ['starting', 'installing', 'applying'].includes(operation.phase)
|
|
2013
|
+
const incompatible = item.metadata?.compatibility === 'unsupported'
|
|
2014
|
+
const button = document.createElement('button')
|
|
2015
|
+
button.type = 'button'
|
|
2016
|
+
button.className = 'ra-installed-update'
|
|
2017
|
+
button.setAttribute('data-ra-installed-update', '')
|
|
2018
|
+
button.setAttribute('data-state', operation.phase === 'failed' ? 'error' : busy ? 'busy' : 'ready')
|
|
2019
|
+
button.textContent = 'Update → v' + item.availableVersion
|
|
2020
|
+
button.title = operation.error || (incompatible
|
|
2021
|
+
? format(t, 'incompatibleTitle', { version: item.metadata?.runtimeVersion ?? '?' })
|
|
2022
|
+
: format(t, 'updateAction', { version: 'v' + item.availableVersion }))
|
|
2023
|
+
button.disabled = busy || incompatible
|
|
2024
|
+
button.addEventListener('click', event => {
|
|
2025
|
+
event.preventDefault()
|
|
2026
|
+
event.stopPropagation()
|
|
2027
|
+
void runUpdate(item)
|
|
2028
|
+
})
|
|
2029
|
+
actionHost.insertBefore(button, toggle)
|
|
2030
|
+
created.push(button)
|
|
2031
|
+
}
|
|
2032
|
+
return () => {
|
|
2033
|
+
for (const button of created) button.remove()
|
|
2034
|
+
}
|
|
2035
|
+
}, [installed, updateSignature, operationSignature, bulkUpdating])
|
|
2036
|
+
|
|
2037
|
+
const bridgeTabs = [
|
|
2038
|
+
['installed', t('installed'), installedCount],
|
|
2039
|
+
['sources', t('sources'), ''],
|
|
2040
|
+
['browse', t('browse'), ''],
|
|
2041
|
+
['updates', t('updates'), updateCount > 0 ? String(updateCount) : ''],
|
|
2042
|
+
]
|
|
2043
|
+
const selectRelative = delta => {
|
|
2044
|
+
const index = bridgeTabs.findIndex(item => item[0] === tab)
|
|
2045
|
+
setTab(bridgeTabs[(index + delta + bridgeTabs.length) % bridgeTabs.length][0])
|
|
2046
|
+
}
|
|
2047
|
+
const body = tab === 'sources'
|
|
2048
|
+
? h(SourcesView, { t, form })
|
|
2049
|
+
: tab === 'browse'
|
|
2050
|
+
? h(BrowseView, { t })
|
|
2051
|
+
: tab === 'updates'
|
|
2052
|
+
? h(UpdatesView, { t, discovery })
|
|
2053
|
+
: null
|
|
2054
|
+
|
|
2055
|
+
return h('div', { className: 'ra-native-surface' },
|
|
2056
|
+
h('div', { className: 'ra-native-nav-row' },
|
|
2057
|
+
h('div', { className: 'ra-native-tabs', role: 'tablist', 'aria-label': 'Plugin Manager views' },
|
|
2058
|
+
...bridgeTabs.flatMap(([id, label, count], index) => [
|
|
2059
|
+
index === 0 ? null : h('span', { key: id + '-separator', className: 'ra-native-separator', 'aria-hidden': true }, '|'),
|
|
2060
|
+
h('button', {
|
|
2061
|
+
key: id,
|
|
2062
|
+
type: 'button',
|
|
2063
|
+
role: 'tab',
|
|
2064
|
+
className: 'ra-native-tab',
|
|
2065
|
+
'aria-selected': tab === id,
|
|
2066
|
+
tabIndex: tab === id ? 0 : -1,
|
|
2067
|
+
onClick: () => setTab(id),
|
|
2068
|
+
onKeyDown: event => {
|
|
2069
|
+
if (event.key === 'ArrowRight') {
|
|
2070
|
+
event.preventDefault()
|
|
2071
|
+
selectRelative(1)
|
|
2072
|
+
} else if (event.key === 'ArrowLeft') {
|
|
2073
|
+
event.preventDefault()
|
|
2074
|
+
selectRelative(-1)
|
|
2075
|
+
}
|
|
2076
|
+
},
|
|
2077
|
+
},
|
|
2078
|
+
h('span', null, label),
|
|
2079
|
+
count ? h('span', { className: 'ra-native-count' }, count) : null,
|
|
2080
|
+
),
|
|
2081
|
+
].filter(Boolean)),
|
|
2082
|
+
),
|
|
2083
|
+
tab === 'installed' && updateCount > 0
|
|
2084
|
+
? h('button', {
|
|
2085
|
+
type: 'button',
|
|
2086
|
+
className: 'ra-native-update-all',
|
|
2087
|
+
disabled: discovery.state.loading || bulkUpdating || updatableCount === 0,
|
|
2088
|
+
onClick: () => { void runUpdateAll() },
|
|
2089
|
+
},
|
|
2090
|
+
h(IconUpdate, { size: 14 }),
|
|
2091
|
+
h('span', null, bulkUpdating ? t('updatingAll') : t('updateAll')),
|
|
2092
|
+
)
|
|
2093
|
+
: null,
|
|
2094
|
+
),
|
|
2095
|
+
body ? h('div', { className: 'ra-native-body' }, body) : null,
|
|
2096
|
+
)
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
function installPluginsPagePollingBridge(ctx) {
|
|
2100
|
+
const doc = window.document
|
|
2101
|
+
if (!doc?.querySelector) return () => {}
|
|
2102
|
+
|
|
2103
|
+
const HOST_ATTR = 'data-registry-aggregator-main-surface'
|
|
2104
|
+
const bridgeCss = [
|
|
2105
|
+
'[' + HOST_ATTR + ']{box-sizing:border-box;min-width:0;width:100%}',
|
|
2106
|
+
'[' + HOST_ATTR + '] .ra-native-surface{min-width:0;width:100%}',
|
|
2107
|
+
'[' + HOST_ATTR + '] .ra-native-nav-row{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:22px}',
|
|
2108
|
+
'[' + HOST_ATTR + '] .ra-native-tabs{display:flex;align-items:baseline;min-width:0;flex-wrap:wrap}',
|
|
2109
|
+
'[' + HOST_ATTR + '] .ra-native-tab{display:inline-flex;align-items:baseline;gap:8px;margin:0;padding:0;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:14px;line-height:22px;font-weight:500;cursor:pointer}',
|
|
2110
|
+
'[' + HOST_ATTR + '] .ra-native-tab[aria-selected=true]{color:var(--dsw-alias-label-primary)}',
|
|
2111
|
+
'[' + HOST_ATTR + '] .ra-native-tab:hover{color:var(--dsw-alias-label-primary)}',
|
|
2112
|
+
'[' + HOST_ATTR + '] .ra-native-tab:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px;border-radius:var(--dsw-radius-xs)}',
|
|
2113
|
+
'[' + HOST_ATTR + '] .ra-native-count{color:var(--dsw-alias-label-caption);font-weight:400;font-variant-numeric:tabular-nums}',
|
|
2114
|
+
'[' + HOST_ATTR + '] .ra-native-separator{padding:0 10px;color:var(--dsw-alias-label-caption);font-size:12px;line-height:22px}',
|
|
2115
|
+
'[' + HOST_ATTR + '] .ra-native-update-all{display:inline-flex;align-items:center;justify-content:center;gap:6px;flex:none;height:30px;padding:0 10px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:18px;cursor:pointer}',
|
|
2116
|
+
'[' + HOST_ATTR + '] .ra-native-update-all:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
2117
|
+
'[' + HOST_ATTR + '] .ra-native-update-all:disabled{opacity:.5;cursor:default}',
|
|
2118
|
+
'[' + HOST_ATTR + '] .ra-native-body{padding-top:16px}',
|
|
2119
|
+
'.ra-installed-update{position:relative;z-index:2;display:inline-flex;align-items:center;justify-content:center;height:28px;padding:0 9px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:18px;white-space:nowrap;cursor:pointer}',
|
|
2120
|
+
'.ra-installed-update:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
2121
|
+
'.ra-installed-update:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}',
|
|
2122
|
+
'.ra-installed-update:disabled{opacity:.5;cursor:default}',
|
|
2123
|
+
'.ra-installed-update[data-state=error]{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 30%,transparent)}',
|
|
2124
|
+
].join('')
|
|
2125
|
+
let host
|
|
2126
|
+
let root
|
|
2127
|
+
let createRoot
|
|
2128
|
+
let installedGroup
|
|
2129
|
+
let installedHead
|
|
2130
|
+
let cardsList
|
|
2131
|
+
let headDisplay = ''
|
|
2132
|
+
let cardsDisplay = ''
|
|
2133
|
+
let disposed = false
|
|
2134
|
+
|
|
2135
|
+
const disposeRoot = () => {
|
|
2136
|
+
if (root) {
|
|
2137
|
+
try { root.unmount() } catch {}
|
|
2138
|
+
root = undefined
|
|
2139
|
+
}
|
|
2140
|
+
if (installedHead?.isConnected) installedHead.style.display = headDisplay
|
|
2141
|
+
if (cardsList?.isConnected) cardsList.style.display = cardsDisplay
|
|
2142
|
+
if (installedGroup?.querySelectorAll) {
|
|
2143
|
+
for (const button of installedGroup.querySelectorAll('[data-ra-installed-update]')) button.remove()
|
|
2144
|
+
}
|
|
2145
|
+
if (host?.isConnected) host.remove()
|
|
2146
|
+
host = undefined
|
|
2147
|
+
installedGroup = undefined
|
|
2148
|
+
installedHead = undefined
|
|
2149
|
+
cardsList = undefined
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
const reconcile = () => {
|
|
2153
|
+
if (disposed) return
|
|
2154
|
+
const panel = doc.querySelector('section[data-plugin-panel]')
|
|
2155
|
+
if (!panel) {
|
|
2156
|
+
disposeRoot()
|
|
2157
|
+
return
|
|
2158
|
+
}
|
|
2159
|
+
if (panel.querySelector('[data-plugin-loading]')) return
|
|
2160
|
+
|
|
2161
|
+
const installed = panel.querySelector('section[data-plugin-group="bundles"]')
|
|
2162
|
+
if (!installed) {
|
|
2163
|
+
// Detail pages intentionally have no Installed group. Removing the
|
|
2164
|
+
// bridge here prevents the duplicate UI shown above bundle details.
|
|
2165
|
+
disposeRoot()
|
|
2166
|
+
return
|
|
2167
|
+
}
|
|
2168
|
+
const head = installed.firstElementChild
|
|
2169
|
+
const cards = Array.from(installed.children).find(element => element.tagName === 'UL')
|
|
2170
|
+
if (!head || !cards) return
|
|
2171
|
+
if (host?.isConnected && installedGroup === installed && host.parentElement === installed && head.nextElementSibling === host) return
|
|
2172
|
+
|
|
2173
|
+
disposeRoot()
|
|
2174
|
+
installedGroup = installed
|
|
2175
|
+
installedHead = head
|
|
2176
|
+
cardsList = cards
|
|
2177
|
+
headDisplay = head.style.display
|
|
2178
|
+
cardsDisplay = cards.style.display
|
|
2179
|
+
head.style.display = 'none'
|
|
2180
|
+
|
|
2181
|
+
host = doc.createElement('div')
|
|
2182
|
+
host.setAttribute(HOST_ATTR, '')
|
|
2183
|
+
host.setAttribute('aria-label', 'Registry Aggregator')
|
|
2184
|
+
head.insertAdjacentElement('afterend', host)
|
|
2185
|
+
|
|
2186
|
+
try {
|
|
2187
|
+
if (!createRoot) ({ createRoot } = require('react-dom/client'))
|
|
2188
|
+
if (typeof createRoot !== 'function') throw new Error('react-dom/client createRoot is unavailable')
|
|
2189
|
+
root = createRoot(host)
|
|
2190
|
+
root.render(h(React.Fragment, null,
|
|
2191
|
+
h('style', null, css + '\n' + bridgeCss),
|
|
2192
|
+
h(PluginsPageBridgeSurface, { t: ctx.locale.bind(NS), installed, cards }),
|
|
2193
|
+
))
|
|
2194
|
+
} catch {
|
|
2195
|
+
disposeRoot()
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
const timer = window.setInterval(reconcile, 750)
|
|
2200
|
+
const initial = window.setTimeout(reconcile, 900)
|
|
2201
|
+
|
|
2202
|
+
return () => {
|
|
2203
|
+
disposed = true
|
|
2204
|
+
window.clearInterval(timer)
|
|
2205
|
+
window.clearTimeout(initial)
|
|
2206
|
+
disposeRoot()
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
|
|
1870
2210
|
return {
|
|
1871
2211
|
inject: ['slots', 'locale', 'connection', 'configForms', 'remote', 'remote.pluginManager'],
|
|
1872
2212
|
apply(ctx) {
|
|
@@ -1874,6 +2214,7 @@ window.__ModuleLoader__.load({
|
|
|
1874
2214
|
remote = ctx.remote
|
|
1875
2215
|
sourceConfigForm = ctx.configForms.get(HOST_ENTRY)
|
|
1876
2216
|
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'registry-aggregator: locale')
|
|
2217
|
+
ctx.effect(() => ctx.configForms.whileServed([HOST_ENTRY], () => installPluginsPagePollingBridge(ctx)), 'registry-aggregator: DSH 0.2.0-rc.2 Plugins page polling bridge')
|
|
1877
2218
|
ctx.effect(() => ctx.configForms.whileServed([HOST_ENTRY], () =>
|
|
1878
2219
|
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
1879
2220
|
name: 'plugins.bundle.config',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stolyarovmn/dsh-ui-registry-aggregator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Federated plugin discovery and update UI for DeepSeek Harness 0.2.x, integrated with the native Plugin Manager.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stolyarovmn",
|