@huaqiu/dsh-tool-plugin-update 0.6.11 → 0.6.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huaqiu/dsh-tool-plugin-update",
3
- "version": "0.6.11",
3
+ "version": "0.6.13",
4
4
  "type": "module",
5
5
  "main": "./lib/index.mjs",
6
6
  "types": "./lib/index.d.mts",
@@ -31,8 +31,8 @@
31
31
  "@deepseek-ai/dsh-host-webserver": "^0.2.1-rc.2"
32
32
  },
33
33
  "dependencies": {
34
- "@huaqiu/dsh-sidebar-entry": "0.6.11",
35
- "@huaqiu/dsh-plugin-log": "0.6.11"
34
+ "@huaqiu/dsh-plugin-log": "0.6.13",
35
+ "@huaqiu/dsh-sidebar-entry": "0.6.13"
36
36
  },
37
37
  "files": [
38
38
  "lib",
package/src/client/api.ts CHANGED
@@ -420,13 +420,22 @@ export interface InstallResult {
420
420
  }
421
421
 
422
422
  /** POST the install transaction for the confirmed set version. */
423
- export function installSet(setVersion: string): Promise<InstallResult> {
423
+ export function installSet(setVersion: string, pluginNames?: string[]): Promise<InstallResult> {
424
424
  return request<InstallResult>('/install', {
425
425
  method: 'POST',
426
- body: JSON.stringify({ setVersion }),
426
+ body: JSON.stringify({ setVersion, ...(pluginNames ? { pluginNames } : {}) }),
427
427
  })
428
428
  }
429
429
 
430
+ /** Persist a per-plugin decision for this release set. */
431
+ export function ignorePlugin(setVersion: string, name: string): Promise<unknown> {
432
+ return request<unknown>('/plugin-ignore', { method: 'POST', body: JSON.stringify({ setVersion, name }) })
433
+ }
434
+
435
+ export function unignorePlugin(setVersion: string, name: string): Promise<unknown> {
436
+ return request<unknown>('/plugin-unignore', { method: 'POST', body: JSON.stringify({ setVersion, name }) })
437
+ }
438
+
430
439
  /** POST ignore for the current offer (set-scoped, persists across restarts). */
431
440
  export function ignoreOffer(setVersion: string): Promise<unknown> {
432
441
  return request<unknown>('/ignore', { method: 'POST', body: JSON.stringify({ setVersion }) })
@@ -14,7 +14,7 @@ const zh = {
14
14
  entryTooltip: '查看可用的插件更新',
15
15
  viewTitle: '插件更新',
16
16
  viewIntro: '检查并安装可用的插件更新',
17
- viewInfo: '在这里查看插件集的更新。标注为「被阻止」的插件因主机或 HQ Edge 版本不兼容而无法安装;已是最新的插件无需操作。',
17
+ viewInfo: '在这里查看插件集的更新;已是最新的插件无需操作。',
18
18
  refresh: '刷新',
19
19
  close: '关闭',
20
20
  loading: '正在读取插件更新…',
@@ -24,6 +24,7 @@ const zh = {
24
24
  // it honestly rather than per-plugin.
25
25
  installAll: (count: number) => `安装整套更新(${count} 项)`,
26
26
  installOne: '安装整套',
27
+ installOnePlugin: '安装此插件',
27
28
  installOneTip: '将安装整套 N 项更新并自动重启 DSH,而不仅是此插件',
28
29
  installing: '正在安装…(DSH 将自动重启)',
29
30
  retry: '重试',
@@ -37,22 +38,9 @@ const zh = {
37
38
  groupCount: (count: number) => `${count} 个插件`,
38
39
  group: {
39
40
  actionable: '待处理',
40
- blocked: '无法安装',
41
41
  ignored: '已忽略',
42
42
  current: '已是最新',
43
43
  },
44
- state: {
45
- current: '当前',
46
- update: '可更新',
47
- install: '待安装',
48
- ignored: '已忽略',
49
- blocked: '被阻止',
50
- },
51
- blockedReason: {
52
- host: '不适用于当前 EDA 主机',
53
- 'hq-edge-version': '当前 HQ Edge 版本不兼容',
54
- 'runtime-unknown': 'HQ Edge 版本未知,无法校验',
55
- },
56
44
  runtime: {
57
45
  title: 'HQ Edge 运行时',
58
46
  current: '当前版本',
@@ -197,11 +185,10 @@ const zh = {
197
185
  allChangelog: '展开全部更新日志',
198
186
  changelogBtn: '日志',
199
187
  changelogTitle: '更新内容:',
200
- blockedBadge: '无法安装',
201
188
  // Footer
202
189
  footerCount: (n: number) => `准备安装 ${n} 项兼容插件`,
203
190
  footerSet: (v: string) => `版本集: v${v}`,
204
- installAllSet: (v: string) => `一键更新全套插件 (v${v})`,
191
+ installAllSet: (v: string) => `更新插件集 (v${v})`,
205
192
  ignoreBtn: '忽略',
206
193
  footnote: '更新将自动应用并重启 DSH 视图,不影响 EDA 图纸数据',
207
194
  },
@@ -222,7 +209,7 @@ const en: UpdateCopy = {
222
209
  entryTooltip: 'Review available plugin updates',
223
210
  viewTitle: 'Plugin Updates',
224
211
  viewIntro: 'Check for and install available plugin updates',
225
- viewInfo: 'Review the plugin set updates here. A plugin marked Blocked cannot be installed because its host or HQ Edge version is incompatible; a plugin marked Current needs no action.',
212
+ viewInfo: 'Review the plugin set updates here. Plugins that are up to date need no action.',
226
213
  refresh: 'Refresh',
227
214
  close: 'Close',
228
215
  loading: 'Reading plugin updates…',
@@ -230,6 +217,7 @@ const en: UpdateCopy = {
230
217
  checkFailed: 'Failed to check for updates — please retry',
231
218
  installAll: (count: number) => `Install whole set (${count})`,
232
219
  installOne: 'Install set',
220
+ installOnePlugin: 'Install this plugin',
233
221
  installOneTip: 'Installs the whole set of N updates and restarts DSH — not just this plugin',
234
222
  installing: 'Installing… (DSH will restart)',
235
223
  retry: 'Retry',
@@ -243,22 +231,9 @@ const en: UpdateCopy = {
243
231
  groupCount: (count: number) => `${count} plugin${count === 1 ? '' : 's'}`,
244
232
  group: {
245
233
  actionable: 'Actionable',
246
- blocked: 'Blocked',
247
234
  ignored: 'Ignored',
248
235
  current: 'Up to date',
249
236
  },
250
- state: {
251
- current: 'Current',
252
- update: 'Update',
253
- install: 'Install',
254
- ignored: 'Ignored',
255
- blocked: 'Blocked',
256
- },
257
- blockedReason: {
258
- host: 'Not applicable to the current EDA host',
259
- 'hq-edge-version': 'Incompatible with the current HQ Edge version',
260
- 'runtime-unknown': 'HQ Edge version unknown — cannot verify',
261
- },
262
237
  runtime: {
263
238
  title: 'HQ Edge Runtime',
264
239
  current: 'Current version',
@@ -397,7 +372,6 @@ const en: UpdateCopy = {
397
372
  allChangelog: 'Expand all changelogs',
398
373
  changelogBtn: 'Log',
399
374
  changelogTitle: 'What changed:',
400
- blockedBadge: 'Cannot install',
401
375
  footerCount: (n: number) => `Ready to install ${n} compatible plugin${n === 1 ? '' : 's'}`,
402
376
  footerSet: (v: string) => `Set: v${v}`,
403
377
  installAllSet: (v: string) => `Update all plugins (v${v})`,
@@ -1379,6 +1379,7 @@ const VIEW_CSS_TEXT = `
1379
1379
  align-items: start;
1380
1380
  }
1381
1381
  .hqpu-card {
1382
+ position: relative;
1382
1383
  margin: 0;
1383
1384
  padding: 10px;
1384
1385
  border-radius: 8px;
@@ -1444,6 +1445,30 @@ const VIEW_CSS_TEXT = `
1444
1445
  .hqpu-log-toggle:hover { color: var(--hq-text); }
1445
1446
  .hqpu-log-toggle svg { width: 12px; height: 12px; transition: transform 0.15s ease; }
1446
1447
  .hqpu-log-toggle[data-open='true'] svg { transform: rotate(180deg); }
1448
+ .hqpu-changelog-actions {
1449
+ display: flex;
1450
+ gap: 8px;
1451
+ margin-top: 10px;
1452
+ padding-top: 8px;
1453
+ border-top: 0.5px solid var(--hq-border);
1454
+ }
1455
+ .hqpu-card-action {
1456
+ height: 28px;
1457
+ padding: 0 10px;
1458
+ border: 0.5px solid var(--hq-border-strong);
1459
+ border-radius: 6px;
1460
+ background: var(--hq-card);
1461
+ color: var(--hq-text);
1462
+ font-size: 10px;
1463
+ cursor: pointer;
1464
+ }
1465
+ .hqpu-card-action:hover { border-color: var(--hq-brand-soft); color: var(--hq-brand-soft); }
1466
+ .hqpu-card-action--install {
1467
+ border-color: var(--hq-brand);
1468
+ background: var(--hq-brand);
1469
+ color: var(--hq-brand-fg);
1470
+ }
1471
+ .hqpu-card-action--install:hover { border-color: var(--hq-brand-soft); background: var(--hq-brand-soft); color: var(--hq-brand-fg); }
1447
1472
  .hqpu-changelog-drawer {
1448
1473
  margin-top: 8px;
1449
1474
  padding: 8px;
@@ -1466,40 +1491,6 @@ const VIEW_CSS_TEXT = `
1466
1491
  line-height: 16px;
1467
1492
  }
1468
1493
 
1469
- /* Blocked / incompatible card: dimmed, rose badge, no install button */
1470
- .hqpu-card--blocked {
1471
- opacity: 0.8;
1472
- background: color-mix(in srgb, var(--hq-card) 60%, transparent);
1473
- border-color: var(--hq-border);
1474
- }
1475
- .hqpu-card--blocked .hqpu-avatar {
1476
- background: color-mix(in srgb, var(--hq-card) 50%, transparent);
1477
- border-color: var(--hq-border);
1478
- color: var(--hq-faint);
1479
- }
1480
- .hqpu-card--blocked .hqpu-card-title { color: var(--hq-muted); }
1481
- .hqpu-blocked-badge {
1482
- font-size: 9px;
1483
- padding: 2px 6px;
1484
- border-radius: 4px;
1485
- color: var(--hq-rose);
1486
- background: color-mix(in srgb, var(--hq-rose) 18%, transparent);
1487
- border: 0.5px solid color-mix(in srgb, var(--hq-rose) 40%, transparent);
1488
- }
1489
- .hqpu-reason-line {
1490
- display: flex;
1491
- align-items: flex-start;
1492
- gap: 4px;
1493
- margin: 8px 0 0;
1494
- padding: 6px;
1495
- border-radius: 5px;
1496
- border: 0.5px solid color-mix(in srgb, var(--hq-rose) 20%, transparent);
1497
- background: color-mix(in srgb, var(--hq-rose) 8%, transparent);
1498
- font-size: 10px;
1499
- color: var(--hq-rose);
1500
- }
1501
- .hqpu-reason-line svg { width: 12px; height: 12px; flex: none; margin-top: 1px; }
1502
-
1503
1494
  /* Sticky footer */
1504
1495
  .hqpu-footer {
1505
1496
  flex: none;
@@ -1755,7 +1746,11 @@ class UpdateStore implements UpdateStoreLike {
1755
1746
  if (report.runtime?.updateAvailable) count += 1
1756
1747
  if (count === 0) {
1757
1748
  const latest = this.recent[0]
1758
- if (latest !== undefined && (latest.phase === 'failed' || latest.phase === 'rolled-back')) {
1749
+ // Keep a visible attention dot for every terminal outcome, including
1750
+ // success. Without this, a background update could finish while the
1751
+ // panel was closed and the user would have no indication that a result
1752
+ // is waiting in the update centre.
1753
+ if (latest !== undefined && TERMINAL_PHASES.has(latest.phase)) {
1759
1754
  count = 1
1760
1755
  }
1761
1756
  }
@@ -20,12 +20,14 @@
20
20
  import {
21
21
  checkUpdates,
22
22
  ignoreOffer,
23
+ ignorePlugin,
23
24
  installSet,
24
25
  requestRuntimeUpdate,
25
26
  runtimeLatestVersion,
26
27
  setPluginUpdateSource,
27
28
  setRuntimeUpdateSource,
28
29
  unignoreOffer,
30
+ unignorePlugin,
29
31
  TERMINAL_PHASES,
30
32
  type PluginUpdateItem,
31
33
  type PluginUpdateReport,
@@ -101,44 +103,6 @@ export function shortName(name: string): string {
101
103
  return unscoped.replace(/^dsh-(?:host-|client-)?/, '')
102
104
  }
103
105
 
104
- /** Normalize the server's block reason to a list (it may arrive as a string). */
105
- function blockedByList(item: PluginUpdateItem): string[] {
106
- const by = item.blockedBy
107
- if (Array.isArray(by)) return by.filter((entry): entry is string => typeof entry === 'string')
108
- return typeof by === 'string' && by.length > 0 ? [by] : []
109
- }
110
-
111
- /** The tag tone a state reads as, matching the shell's Tag palette. */
112
- function stateTone(item: PluginUpdateItem): 'success' | 'info' | 'warning' | 'danger' | 'neutral' {
113
- switch (item.state) {
114
- case 'update':
115
- return 'info'
116
- case 'install':
117
- return 'warning'
118
- case 'blocked':
119
- return 'danger'
120
- case 'ignored':
121
- return 'neutral'
122
- case 'current':
123
- return 'success'
124
- }
125
- }
126
-
127
- function stateLabel(item: PluginUpdateItem, copy: UpdateCopy): string {
128
- switch (item.state) {
129
- case 'current':
130
- return copy.state.current
131
- case 'update':
132
- return copy.state.update
133
- case 'install':
134
- return copy.state.install
135
- case 'ignored':
136
- return copy.state.ignored
137
- case 'blocked':
138
- return copy.state.blocked
139
- }
140
- }
141
-
142
106
  /** The one-line version transition under a card's title. */
143
107
  function versionLine(item: PluginUpdateItem): string {
144
108
  switch (item.state) {
@@ -154,14 +118,6 @@ function versionLine(item: PluginUpdateItem): string {
154
118
  }
155
119
  }
156
120
 
157
- function blockedReason(item: PluginUpdateItem, copy: UpdateCopy): string | undefined {
158
- const by = blockedByList(item)
159
- if (by.includes('host')) return copy.blockedReason.host
160
- if (by.includes('hq-edge-version')) return copy.blockedReason['hq-edge-version']
161
- if (by.includes('runtime-unknown')) return copy.blockedReason['runtime-unknown']
162
- return by.length > 0 ? by.join(', ') : undefined
163
- }
164
-
165
121
  /** Two-letter avatar initials for a card (CJK-safe). */
166
122
  function avatarInitials(item: PluginUpdateItem): string {
167
123
  const src =
@@ -175,7 +131,13 @@ function avatarInitials(item: PluginUpdateItem): string {
175
131
  }
176
132
 
177
133
  /** Shared per-card changelog toggle drawer (the per-plugin changes[] list). */
178
- function changelogDrawer(item: PluginUpdateItem, copy: UpdateCopy, open: boolean): HTMLElement {
134
+ function changelogDrawer(
135
+ item: PluginUpdateItem,
136
+ copy: UpdateCopy,
137
+ open: boolean,
138
+ onInstall: () => void,
139
+ onIgnore: () => void,
140
+ ): HTMLElement {
179
141
  const drawer = el('div', 'hqpu-changelog-drawer')
180
142
  drawer.style.display = open ? '' : 'none'
181
143
  drawer.append(el('div', 'hqpu-changelog-drawer-title', copy.ui.changelogTitle))
@@ -183,22 +145,39 @@ function changelogDrawer(item: PluginUpdateItem, copy: UpdateCopy, open: boolean
183
145
  for (const change of item.changes ?? []) {
184
146
  if (typeof change === 'string' && change.length > 0) ul.append(el('li', '', change))
185
147
  }
186
- drawer.append(ul)
148
+ if (ul.childElementCount > 0) drawer.append(ul)
149
+ const actions = el('div', 'hqpu-changelog-actions')
150
+ if (item.state === 'update' || item.state === 'install') {
151
+ const install = el('button', 'hqpu-card-action hqpu-card-action--install', copy.installOnePlugin)
152
+ install.type = 'button'
153
+ install.title = copy.installOneTip
154
+ install.addEventListener('click', onInstall)
155
+ const ignore = el('button', 'hqpu-card-action', copy.ignore)
156
+ ignore.type = 'button'
157
+ ignore.addEventListener('click', onIgnore)
158
+ actions.append(install, ignore)
159
+ } else if (item.state === 'ignored') {
160
+ const show = el('button', 'hqpu-card-action hqpu-card-action--install', copy.unignore)
161
+ show.type = 'button'
162
+ show.addEventListener('click', onIgnore)
163
+ actions.append(show)
164
+ }
165
+ if (actions.childElementCount > 0) drawer.append(actions)
187
166
  return drawer
188
167
  }
189
168
 
190
169
  /** One package card per the mockup: avatar initials, display name + package
191
170
  * subtitle, old→new version row (new in emerald), a 日志 toggle expanding the
192
- * inline changelog. Blocked/incompatible cards are dimmed with a rose
193
- * 无法安装 badge and a reason line, and carry NO install button. */
171
+ * inline changelog. */
194
172
  function packageCard(
195
173
  item: PluginUpdateItem,
196
174
  copy: UpdateCopy,
197
175
  open: boolean,
198
176
  onToggle: () => void,
177
+ onInstall: () => void,
178
+ onIgnore: () => void,
199
179
  ): HTMLLIElement {
200
- const blocked = item.state === 'blocked'
201
- const card = el('li', blocked ? 'hqpu-card hqpu-card--blocked' : 'hqpu-card')
180
+ const card = el('li', 'hqpu-card')
202
181
  card.dataset.pluginName = item.name
203
182
  card.dataset.state = item.state
204
183
 
@@ -229,9 +208,9 @@ function packageCard(
229
208
  left.append(avatar, main)
230
209
 
231
210
  const right = el('div', 'hqpu-card-end')
232
- if (blocked) {
233
- right.append(el('span', 'hqpu-blocked-badge', copy.ui.blockedBadge))
234
- } else if (Array.isArray(item.changes) && item.changes.length > 0) {
211
+ const hasDetails = (Array.isArray(item.changes) && item.changes.length > 0) ||
212
+ item.state === 'update' || item.state === 'install' || item.state === 'ignored'
213
+ if (hasDetails) {
235
214
  const toggle = el('button', 'hqpu-log-toggle')
236
215
  toggle.type = 'button'
237
216
  toggle.dataset.open = open ? 'true' : 'false'
@@ -242,26 +221,10 @@ function packageCard(
242
221
  head.append(left, right)
243
222
  card.append(head)
244
223
 
245
- // Inline changelog drawer (actionable items only).
246
- if (!blocked && Array.isArray(item.changes) && item.changes.length > 0) {
247
- card.append(changelogDrawer(item, copy, open))
248
- }
249
-
250
- // Blocked reason line(s): mapped copy plus the server's full reasons/notes.
251
- if (blocked) {
252
- const reason = blockedReason(item, copy)
253
- const entries = [
254
- ...(reason !== undefined ? [reason] : []),
255
- ...[...(item.reasons ?? []), ...(item.notes ?? [])].filter(
256
- (e): e is string => typeof e === 'string' && e.length > 0,
257
- ),
258
- ]
259
- for (const entry of entries) {
260
- const line = el('p', 'hqpu-reason-line')
261
- line.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 5V8.5M8 11.25h.01M14.5 8a6.5 6.5 0 11-13 0 6.5 6.5 0 0113 0Z"/></svg>`
262
- line.append(document.createTextNode(entry))
263
- card.append(line)
264
- }
224
+ // The actions live in the expanded changelog/details drawer so cards keep
225
+ // their compact layout until a person asks for more information.
226
+ if (hasDetails) {
227
+ card.append(changelogDrawer(item, copy, open, onInstall, onIgnore))
265
228
  }
266
229
  return card
267
230
  }
@@ -275,6 +238,8 @@ function group(
275
238
  allOpen: boolean,
276
239
  onToggle: (name: string) => void,
277
240
  onToggleAll: () => void,
241
+ onInstall: (name: string) => void,
242
+ onIgnore: (item: PluginUpdateItem) => void,
278
243
  ): HTMLElement | null {
279
244
  if (items.length === 0) return null
280
245
  const section = el('section', 'hqpu-group')
@@ -286,7 +251,7 @@ function group(
286
251
  titleWrap.append(el('span', 'hqpu-count-pill', String(items.length)))
287
252
  head.append(titleWrap)
288
253
  // Expand-all only makes sense over actionable, changelog-bearing cards.
289
- const actionable = items.some((i) => i.state !== 'blocked' && Array.isArray(i.changes) && i.changes.length > 0)
254
+ const actionable = items.some((i) => Array.isArray(i.changes) && i.changes.length > 0)
290
255
  if (actionable) {
291
256
  const all = el('button', 'hqpu-all-changelog', copy.ui.allChangelog)
292
257
  all.type = 'button'
@@ -296,7 +261,7 @@ function group(
296
261
 
297
262
  const list = el('ul', 'hqpu-cards')
298
263
  for (const item of items) {
299
- list.append(packageCard(item, copy, allOpen || changelogOpen.has(item.name), () => onToggle(item.name)))
264
+ list.append(packageCard(item, copy, allOpen || changelogOpen.has(item.name), () => onToggle(item.name), () => onInstall(item.name), () => onIgnore(item)))
300
265
  }
301
266
 
302
267
  section.append(head, list)
@@ -1119,6 +1084,8 @@ function renderList(
1119
1084
  onToggleCard: (name: string) => void,
1120
1085
  onToggleAll: () => void,
1121
1086
  onRetryCheck: () => void,
1087
+ onInstall: (name: string) => void,
1088
+ onIgnore: (item: PluginUpdateItem) => void,
1122
1089
  ): void {
1123
1090
  section.textContent = ''
1124
1091
  if (report === null) {
@@ -1144,16 +1111,17 @@ function renderList(
1144
1111
  const byState = (state: PluginUpdateItem['state']): PluginUpdateItem[] =>
1145
1112
  report.items.filter((item) => item.state === state)
1146
1113
 
1147
- // Most important first: what can be acted on, then what is refused, then
1148
- // what was muted, then what is already settled.
1114
+ // Most important first: what can be acted on, then what was muted, then what
1115
+ // is already settled. Blocked plugins are intentionally skipped — the server
1116
+ // still reports them, but they carry no user-actionable offer, so surfacing
1117
+ // them only adds noise without a path forward.
1149
1118
  const order: Array<[string, PluginUpdateItem[]]> = [
1150
1119
  [copy.group.actionable, [...byState('update'), ...byState('install')]],
1151
- [copy.group.blocked, byState('blocked')],
1152
1120
  [copy.group.ignored, byState('ignored')],
1153
1121
  [copy.group.current, byState('current')],
1154
1122
  ]
1155
1123
  for (const [heading, items] of order) {
1156
- const node = group(heading, items, copy, changelogOpen, allOpen, onToggleCard, onToggleAll)
1124
+ const node = group(heading, items, copy, changelogOpen, allOpen, onToggleCard, onToggleAll, onInstall, onIgnore)
1157
1125
  if (node !== null) section.append(node)
1158
1126
  }
1159
1127
  }
@@ -1190,7 +1158,7 @@ function renderPluginSource(
1190
1158
  }
1191
1159
 
1192
1160
  /** Run the install transaction (a new durable job), then let the store poll it. */
1193
- async function runInstall(store: UpdateStoreLike, copy: UpdateCopy): Promise<void> {
1161
+ async function runInstall(store: UpdateStoreLike, copy: UpdateCopy, pluginNames?: string[]): Promise<void> {
1194
1162
  const report = store.report()
1195
1163
  if (report === null) return
1196
1164
  // Without an evaluated set there is nothing to install: hq-edge has not read
@@ -1204,7 +1172,7 @@ async function runInstall(store: UpdateStoreLike, copy: UpdateCopy): Promise<voi
1204
1172
  // dead code — runInstall never called it).
1205
1173
  store.setInstalling(true)
1206
1174
  try {
1207
- const result = await installSet(report.manifestVersion)
1175
+ const result = await installSet(report.manifestVersion, pluginNames)
1208
1176
  if (result.error !== undefined) {
1209
1177
  store.setInstalling(false)
1210
1178
  showToast(`${copy.installError}: ${result.error}`, 'error')
@@ -1287,7 +1255,7 @@ function renderSettingsPanel(
1287
1255
  * The sticky footer: left meta ("准备安装 N 项兼容插件" / "版本集: vX"), a
1288
1256
  * full-width primary "一键更新全套插件 (vX)" (install = one durable job),
1289
1257
  * a secondary 忽略 wired to the existing ignore/unignore affordance, and the
1290
- * footnote. The count EXCLUDES blocked / ignored / already-current entries.
1258
+ * footnote. The count EXCLUDES ignored / already-current entries.
1291
1259
  */
1292
1260
  function renderFooter(
1293
1261
  footer: HTMLElement,
@@ -1685,6 +1653,17 @@ export function openUpdateView(store: UpdateStoreLike, copy: UpdateCopy): void {
1685
1653
  () => {
1686
1654
  void store.check()
1687
1655
  },
1656
+ (name) => {
1657
+ void runInstall(store, copy, [name])
1658
+ },
1659
+ (item) => {
1660
+ const report = store.report()
1661
+ if (report?.manifestVersion === null || report?.manifestVersion === undefined) return
1662
+ const action = item.state === 'ignored'
1663
+ ? unignorePlugin(report.manifestVersion, item.name)
1664
+ : ignorePlugin(report.manifestVersion, item.name)
1665
+ void action.then(() => store.check()).catch(() => undefined)
1666
+ },
1688
1667
  )
1689
1668
  }
1690
1669
  renderRuntime(runtimeSection, report, copy, runtimeActions, dismissedAlert, () => {
@@ -120,8 +120,8 @@ export function isVersionUpgrade(currentVersion: string | null, availableVersion
120
120
  * to `current`: the plugin is at (or ahead of) the offered release, there is
121
121
  * nothing to install, and no install control may be offered for it. Every
122
122
  * other state passes through untouched — `install` (plugin absent locally)
123
- * has no local version to compare, and `blocked`/`ignored`/`current` are
124
- * already non-actionable. Unknown server fields are preserved verbatim: the
123
+ * has no local version to compare, and `ignored`/`current` are already
124
+ * non-actionable. Unknown server fields are preserved verbatim: the
125
125
  * client stays a thin reader of the hq-edge report shape.
126
126
  */
127
127
  export function normalizePluginUpdateItem<T extends PluginUpdateItem>(item: T): T {