dsh-plugin 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.en.md +5 -5
  2. package/README.md +5 -5
  3. package/client/client.js +4772 -1659
  4. package/client/client.js.map +1 -1
  5. package/lib/http/routes.js +451 -28
  6. package/lib/index.js +5 -1
  7. package/lib/services/{install-types.js → install/install-types.js} +8 -2
  8. package/lib/services/install/install.js +4 -0
  9. package/lib/services/install/npm-check.js +55 -0
  10. package/lib/services/{npm-resolve.js → install/npm-resolve.js} +11 -5
  11. package/lib/services/{preflight.js → install/preflight.js} +23 -7
  12. package/lib/services/{task-queue.js → install/task-queue.js} +166 -71
  13. package/lib/services/loader.js +60 -1
  14. package/lib/services/log.js +138 -0
  15. package/lib/services/probe.js +157 -0
  16. package/lib/services/{installed-versions.js → profile/installed-versions.js} +5 -1
  17. package/lib/services/{pending-restart.js → profile/pending-restart.js} +1 -1
  18. package/lib/services/{profile.js → profile/profile.js} +59 -4
  19. package/lib/services/{progress.js → profile/progress.js} +4 -0
  20. package/lib/services/settings.js +60 -0
  21. package/lib/types/http/routes.d.ts +1 -1
  22. package/lib/types/index.d.ts +5 -1
  23. package/lib/types/services/{install-types.d.ts → install/install-types.d.ts} +13 -4
  24. package/lib/types/services/{install.d.ts → install/install.d.ts} +9 -5
  25. package/lib/types/services/install/npm-check.d.ts +12 -0
  26. package/lib/types/services/{npm-resolve.d.ts → install/npm-resolve.d.ts} +2 -1
  27. package/lib/types/services/{task-queue.d.ts → install/task-queue.d.ts} +5 -1
  28. package/lib/types/services/loader.d.ts +14 -4
  29. package/lib/types/services/log.d.ts +49 -0
  30. package/lib/types/services/probe.d.ts +30 -0
  31. package/lib/types/services/{pending-restart.d.ts → profile/pending-restart.d.ts} +9 -3
  32. package/lib/types/services/{profile.d.ts → profile/profile.d.ts} +6 -2
  33. package/lib/types/services/{progress.d.ts → profile/progress.d.ts} +4 -0
  34. package/lib/types/services/settings.d.ts +24 -0
  35. package/package.json +1 -1
  36. package/src/client/components/PluginHubSection.tsx +346 -121
  37. package/src/client/components/{CatalogList.tsx → catalog/CatalogList.tsx} +15 -7
  38. package/src/client/components/{PluginCard.tsx → catalog/PluginCard.tsx} +10 -5
  39. package/src/client/components/layout/CatalogControls.tsx +80 -0
  40. package/src/client/components/{CatalogHeader.tsx → layout/CatalogHeader.tsx} +8 -4
  41. package/src/client/components/{CategoryTabs.tsx → layout/CategoryTabs.tsx} +7 -3
  42. package/src/client/components/layout/SectionTabs.tsx +60 -0
  43. package/src/client/components/{AboutModal.tsx → modals/AboutModal.tsx} +11 -7
  44. package/src/client/components/{HubUpdateModal.tsx → modals/HubUpdateModal.tsx} +9 -5
  45. package/src/client/components/modals/InstalledDetailModal.tsx +193 -0
  46. package/src/client/components/modals/LogsModal.tsx +301 -0
  47. package/src/client/components/modals/LogsPathModal.tsx +161 -0
  48. package/src/client/components/{NotificationsModal.tsx → modals/NotificationsModal.tsx} +51 -20
  49. package/src/client/components/{ProgressView.tsx → modals/ProgressView.tsx} +6 -2
  50. package/src/client/components/{modals.tsx → modals/modals.tsx} +121 -40
  51. package/src/client/components/{Dropdown.tsx → ui/Dropdown.tsx} +18 -6
  52. package/src/client/components/ui/Toggle.tsx +28 -0
  53. package/src/client/components/ui/icons.tsx +343 -0
  54. package/src/client/components/views/CustomInstallView.tsx +250 -0
  55. package/src/client/components/views/DiagnosticsView.tsx +172 -0
  56. package/src/client/components/views/InstalledView.tsx +328 -0
  57. package/src/client/components/views/LogsView.tsx +150 -0
  58. package/src/client/components/views/MarketView.tsx +66 -0
  59. package/src/client/components/views/SettingsView.tsx +273 -0
  60. package/src/client/data/catalog.ts +38 -0
  61. package/src/client/data/host.ts +107 -0
  62. package/src/client/data/hub.ts +41 -0
  63. package/src/client/hooks/useCatalog.ts +77 -117
  64. package/src/client/hooks/useIncrementalList.ts +46 -0
  65. package/src/client/hooks/useLanguage.ts +40 -0
  66. package/src/client/hooks/useSettings.ts +90 -0
  67. package/src/client/hooks/useTaskQueue.ts +168 -47
  68. package/src/client/index.tsx +4 -0
  69. package/src/client/locales.ts +365 -39
  70. package/src/client/logic/constants.ts +87 -0
  71. package/src/client/{lib → logic}/failures.ts +99 -74
  72. package/src/client/{lib → logic}/format.ts +4 -0
  73. package/src/client/logic/install-command.ts +56 -0
  74. package/src/client/logic/installed.ts +207 -0
  75. package/src/client/logic/normalize.ts +52 -0
  76. package/src/client/{lib → logic}/renderMarkdown.ts +4 -0
  77. package/src/client/logic/urls.ts +125 -0
  78. package/src/client/styles/CustomInstallView.module.css +191 -0
  79. package/src/client/styles/DiagnosticsView.module.css +231 -0
  80. package/src/client/styles/Dropdown.module.css +46 -41
  81. package/src/client/styles/Header.module.css +105 -133
  82. package/src/client/styles/InstalledView.module.css +447 -0
  83. package/src/client/styles/List.module.css +11 -1
  84. package/src/client/styles/LogsView.module.css +503 -0
  85. package/src/client/styles/Modal.module.css +357 -71
  86. package/src/client/styles/SectionTabs.module.css +142 -0
  87. package/src/client/styles/SettingsView.module.css +232 -0
  88. package/src/client/styles/Toggle.module.css +60 -0
  89. package/src/client/styles/tokens.module.css +6 -1
  90. package/src/client/types.ts +28 -4
  91. package/src/server/http/routes.ts +426 -28
  92. package/src/server/index.ts +5 -1
  93. package/src/server/services/{install-types.ts → install/install-types.ts} +13 -5
  94. package/src/server/services/{install.ts → install/install.ts} +9 -5
  95. package/src/server/services/install/npm-check.ts +53 -0
  96. package/src/server/services/{npm-resolve.ts → install/npm-resolve.ts} +11 -5
  97. package/src/server/services/{preflight.ts → install/preflight.ts} +24 -7
  98. package/src/server/services/{task-queue.ts → install/task-queue.ts} +168 -68
  99. package/src/server/services/loader.ts +59 -1
  100. package/src/server/services/log.ts +167 -0
  101. package/src/server/services/probe.ts +157 -0
  102. package/src/server/services/{installed-versions.ts → profile/installed-versions.ts} +5 -1
  103. package/src/server/services/{pending-restart.ts → profile/pending-restart.ts} +9 -3
  104. package/src/server/services/{profile.ts → profile/profile.ts} +58 -4
  105. package/src/server/services/{progress.ts → profile/progress.ts} +4 -0
  106. package/src/server/services/settings.ts +79 -0
  107. package/lib/services/install.js +0 -4
  108. package/src/client/components/CatalogControls.tsx +0 -104
  109. package/src/client/components/icons.tsx +0 -125
  110. package/src/client/lib/catalog.ts +0 -272
  111. package/src/client/lib/env.ts +0 -16
  112. /package/lib/types/services/{preflight.d.ts → install/preflight.d.ts} +0 -0
  113. /package/lib/types/services/{installed-versions.d.ts → profile/installed-versions.d.ts} +0 -0
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
2
6
  * Dialog layer for the Plugin Hub: the install-confirm dialog, the uninstall
3
7
  * dialog and the global toast. Both dialogs lock themselves while a mutation
4
8
  * or restart is running, then switch to a result view offering an immediate
@@ -6,12 +10,13 @@
6
10
  */
7
11
  import { createElement as h } from 'react'
8
12
  import type { MouseEvent } from 'react'
9
- import styles from '../styles/Modal.module.css'
10
- import type { EnvInfo, HubPlugin, TaskState, ToastState, Translate } from '../types.ts'
11
- import { CloseIcon, CopyIcon, LinkIcon } from './icons.tsx'
13
+ import styles from '../../styles/Modal.module.css'
14
+ import type { EnvInfo, HubPlugin, TaskState, ToastState, Translate } from '../../types.ts'
15
+ import { CloseIcon, CopyIcon, LinkIcon } from '../ui/icons.tsx'
12
16
  import { ProgressView } from './ProgressView.tsx'
13
- import { installCommandOf, pluginDetailUrl, pluginIssueUrl, pluginSiteUrl } from '../lib/catalog.ts'
14
- import { classifyFailure } from '../lib/failures.ts'
17
+ import { installCommandOf } from '../../logic/install-command.ts'
18
+ import { pluginDetailUrl, pluginIssueUrl, pluginSiteUrl } from '../../logic/urls.ts'
19
+ import { classifyFailure, npmTooLowVersion } from '../../logic/failures.ts'
15
20
 
16
21
  /** 完成结果视图:绿色对勾 + 标题/描述;
17
22
  * needsRestart=true(插件需重启才生效)→ 「稍后重启 / 立即重启」按钮对,点稍后重启后
@@ -52,7 +57,7 @@ function ResultView({
52
57
  h('div', { className: styles.resultRestarting }, restarting ? t('restarting') : t('restartHint')),
53
58
  h('div', { className: styles.modalActions },
54
59
  h('button', { className: styles.restartLater, onClick: onClose, disabled: restarting }, t('restartLater')),
55
- h('button', { className: styles.restartNow, onClick: onRestart, disabled: restarting },
60
+ h('button', { className: styles.restartNowWarning, onClick: onRestart, disabled: restarting },
56
61
  restarting ? t('restarting') : t('restartNow')),
57
62
  ),
58
63
  ])
@@ -62,7 +67,13 @@ function ResultView({
62
67
  }
63
68
 
64
69
  export interface InstallModalProps {
65
- plugin: HubPlugin
70
+ /** 目录插件(有完整元数据);命令行安装(custom)传 null 走 customTarget 模式 */
71
+ plugin: HubPlugin | null
72
+ /** 命令行安装目标(无目录元数据):提供时按裸目标展示确认/进度/结果,命令条显示 pnpm add <target> */
73
+ customTarget?: string
74
+ /** 全局 npm 安装(官方 README 的 `npm install -g <pkgs>`):提供时按包列表展示确认/进度/结果,
75
+ * 命令条显示 npm install -g <pkgs> —— 装的是系统级 CLI 工具,不进插件列表、无需重启 */
76
+ globalNpm?: string[]
66
77
  done: boolean
67
78
  task: TaskState | null
68
79
  t: Translate
@@ -85,12 +96,14 @@ export interface InstallModalProps {
85
96
  /**
86
97
  * 信任确认弹窗:安装进入后台队列后弹窗仍可关闭(任务继续),
87
98
  * 只在本任务执行中展示实时进度;完成后切换为结果视图,与卸载一致。
99
+ * 目录插件与命令行安装(customTarget 模式)共用同一套确认/进度/结果流程。
88
100
  */
89
101
  export function InstallModal(props: InstallModalProps) {
90
- const { plugin, done, task, t, langPath, restarting, submitting, update, cliOnly, needsRestart, onClose, onCopy, onInstall, onRestart } = props
102
+ const { plugin, customTarget, globalNpm, done, task, t, langPath, restarting, submitting, update, cliOnly, needsRestart, onClose, onCopy, onInstall, onRestart } = props
91
103
  const busy = submitting || (task !== null && (task.status === 'pending' || task.status === 'running'))
92
- // 进行中标题带上插件名(中文「XX 插件安装中」;英文状态词在前更自然),并用状态色区分
93
- const name = plugin.displayName ?? plugin.slug
104
+ // 进行中标题带上插件名(中文「XX 插件安装中」;英文状态词在前更自然),并用状态色区分;
105
+ // 命令行安装无插件元数据 → 直接用输入的目标展示;全局 npm 安装 → 包列表空格拼接
106
+ const name = customTarget ?? (globalNpm !== undefined && globalNpm.length > 0 ? globalNpm.join(' ') : undefined) ?? plugin?.displayName ?? plugin?.slug ?? ''
94
107
  const busyTitle = (label: string) => langPath === 'zh/' ? `${name} 插件${label}` : `${label} ${name}`
95
108
  const title = busy
96
109
  ? task && task.status === 'pending'
@@ -134,10 +147,9 @@ export function InstallModal(props: InstallModalProps) {
134
147
  h('div', { className: styles.trustHint }, t('confirmDesc')),
135
148
  h('div', { className: styles.modalRow },
136
149
  h('span', { className: styles.modalLabel }, t('confirmPlugin')),
137
- h('span', { className: styles.modalValue, title: plugin.displayName ?? plugin.slug },
138
- plugin.displayName ?? plugin.slug),
150
+ h('span', { className: styles.modalValue, title: name }, name),
139
151
  ),
140
- plugin.source?.repo ? h('div', { className: styles.modalRow },
152
+ plugin?.source?.repo ? h('div', { className: styles.modalRow },
141
153
  h('span', { className: styles.modalLabel }, t('confirmSource')),
142
154
  // 来源仓库:可点击跳转官网收录详情页(新窗口打开)
143
155
  h('a', {
@@ -148,7 +160,8 @@ export function InstallModal(props: InstallModalProps) {
148
160
  title: plugin.source.repo,
149
161
  }, h(LinkIcon), plugin.source.repo),
150
162
  ) : null,
151
- // 手动命令条:整条点击即复制,右侧再放一个显式复制按钮
163
+ // 手动命令条:整条点击即复制,右侧再放一个显式复制按钮;
164
+ // 命令行安装(无目录数据)直接展示 pnpm add <target>;全局 npm 安装展示 npm install -g <pkgs>
152
165
  h('div', {
153
166
  className: styles.modalCmd,
154
167
  onClick: onCopy,
@@ -156,9 +169,16 @@ export function InstallModal(props: InstallModalProps) {
156
169
  role: 'button',
157
170
  tabIndex: 0,
158
171
  },
159
- h('span', { className: styles.modalCmdText }, installCommandOf(plugin)),
172
+ h('span', { className: styles.modalCmdText },
173
+ globalNpm !== undefined && globalNpm.length > 0
174
+ ? `npm install -g ${globalNpm.join(' ')}`
175
+ : customTarget ? `pnpm add ${customTarget}` : installCommandOf(plugin!)),
160
176
  h('span', { className: styles.modalCmdCopy }, h(CopyIcon), t('copyCmdLabel')),
161
177
  ),
178
+ // 全局 npm 安装说明:装的是系统级 CLI 工具,不进插件列表、无需重启宿主
179
+ globalNpm !== undefined && globalNpm.length > 0
180
+ ? h('div', { className: styles.cliOnlyHint }, t('globalNpmHint'))
181
+ : null,
162
182
  // AI 识别可能需要命令行辅助的插件(webInstallable=false):提示但不拦截,仍允许尝试一键安装
163
183
  cliOnly ? h('div', { className: styles.cliOnlyHint }, t('cliOnlyHint')) : null,
164
184
  task && task.status === 'pending' ? h('div', { className: styles.queuedHint }, t('queuedHint')) : null,
@@ -268,21 +288,22 @@ export function UninstallModal(props: UninstallModalProps) {
268
288
  task && task.status === 'pending' ? h('div', { className: styles.queuedHint }, t('queuedHint')) : null,
269
289
  task ? h(ProgressView, { task }) : null,
270
290
  h('div', { className: styles.modalActions },
271
- h('button', {
272
- className: styles.modalCancel,
273
- onClick: onCancel,
274
- }, t('confirmCancel')),
275
291
  // 复制卸载命令:万一直接卸载失败,可去终端手动执行
276
292
  h('button', {
277
293
  className: styles.modalCopy,
278
294
  disabled: busy,
279
295
  onClick: onCopyCommand,
280
296
  }, t('copyUninstallCommand')),
297
+ // 确认框语义:取消动作需明确指向「卸载」,避免歧义
298
+ h('button', {
299
+ className: styles.modalCancel,
300
+ onClick: onCancel,
301
+ }, t('cancelUninstall')),
281
302
  h('button', {
282
303
  className: styles.uninstallConfirm,
283
304
  disabled: busy,
284
305
  onClick: onConfirm,
285
- }, busy ? (task && task.status === 'pending' ? t('queuedUninstallTitle') : t('uninstalling')) : t('uninstall')),
306
+ }, busy ? (task && task.status === 'pending' ? t('queuedUninstallTitle') : t('uninstalling')) : t('uninstallConfirm')),
286
307
  ),
287
308
  ),
288
309
  ),
@@ -305,6 +326,16 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
305
326
  }) {
306
327
  // 失败归类:当前安装通道(npm/git)装不上 = 插件分发/依赖的问题,一律引导提 Issue
307
328
  const failureKind = classifyFailure(message)
329
+ // 服务端 [npm-too-low] 标记里带的本机 npm 版本(无标记为 null,提示文案降级为「怀疑」)
330
+ const npmVersion = npmTooLowVersion(message)
331
+ // 复制完整错误:实际执行/尝试过的安装命令(如有)+ 完整错误正文。
332
+ // 只复制 message 会丢上下文 —— 命令行安装输入的目标、执行过的命令都在 command/attempts 里,
333
+ // 光拷一句「unsupported install target」出去没人看得懂
334
+ const copyText = [
335
+ ...(command ? [command] : []),
336
+ ...(attempts ?? []),
337
+ message,
338
+ ].join('\n')
308
339
  return h('div', {
309
340
  className: styles.overlay,
310
341
  onClick: (e: MouseEvent<HTMLDivElement>) => {
@@ -337,33 +368,37 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
337
368
  title: repo,
338
369
  }, repo) : null,
339
370
  // 复制按钮放最上面、浅灰隐蔽;报错正文不允许鼠标选择复制,只能点这里
340
- h('button', { className: styles.errorCopySoft, onClick: () => onCopy(message) }, t('errorCopy')),
371
+ h('button', { className: styles.errorCopySoft, onClick: () => onCopy(copyText) }, t('errorCopy')),
341
372
  ),
342
373
  // 报错信息完整展示(可滚动),比失败记录的预览看得更多
343
374
  h('pre', { className: styles.errorBox }, message),
344
- // 插件问题(prepare 构建失败 / 原生依赖构建被拦截 / git 分发缺产物):都是插件打包分发问题 —— 先说明原因,按钮在下方引导提 Issue
345
- failureKind === 'pluginPrepare' || failureKind === 'pnpmIgnoredBuild'
346
- ? h('div', null, [
347
- // [packaging](预检/装后校验拦截:git 分发缺产物)与
348
- // prepare 构建脚本实际执行失败 / 原生依赖构建被 pnpm 拦截:都是插件打包分发问题 —— 先说明原因,按钮在下方提交
349
- h('div', { className: styles.failPrepareHint }, failureKind === 'pnpmIgnoredBuild'
350
- ? t('failIgnoredBuild')
351
- : /\[packaging\]/i.test(message) ? t('failPackagingHint') : t('failPrepareHint')),
352
- repo ? h('a', {
375
+ // npm 内部崩溃(edgesOut)+ 本机 npm 版本过低:npm 自身已知缺陷,不是插件问题 ——
376
+ // 直接给升级指引,不引导提 Issue(提了也是 npm 的问题,插件作者无法修复)
377
+ failureKind === 'npmTooOld'
378
+ ? h('div', { className: styles.failPrepareHint },
379
+ t(npmVersion ? 'failNpmTooLowV' : 'failNpmTooLow', npmVersion ? { v: npmVersion } : undefined))
380
+ : failureKind === 'pluginPrepare' || failureKind === 'pnpmIgnoredBuild'
381
+ ? h('div', null, [
382
+ // [packaging](预检/装后校验拦截:git 分发缺产物)与
383
+ // prepare 构建脚本实际执行失败 / 原生依赖构建被 pnpm 拦截:都是插件打包分发问题 —— 先说明原因,按钮在下方提交
384
+ h('div', { className: styles.failPrepareHint }, failureKind === 'pnpmIgnoredBuild'
385
+ ? t('failIgnoredBuild')
386
+ : /\[packaging\]/i.test(message) ? t('failPackagingHint') : t('failPrepareHint')),
387
+ repo ? h('a', {
388
+ className: styles.failBigIssue,
389
+ href: pluginIssueUrl(repo, message, env, command, attempts),
390
+ target: '_blank',
391
+ rel: 'noopener noreferrer',
392
+ title: t('failIssueHint'),
393
+ }, t('failIssueBig')) : null,
394
+ ])
395
+ : repo ? h('a', {
353
396
  className: styles.failBigIssue,
354
397
  href: pluginIssueUrl(repo, message, env, command, attempts),
355
398
  target: '_blank',
356
399
  rel: 'noopener noreferrer',
357
400
  title: t('failIssueHint'),
358
401
  }, t('failIssueBig')) : null,
359
- ])
360
- : repo ? h('a', {
361
- className: styles.failBigIssue,
362
- href: pluginIssueUrl(repo, message, env, command, attempts),
363
- target: '_blank',
364
- rel: 'noopener noreferrer',
365
- title: t('failIssueHint'),
366
- }, t('failIssueBig')) : null,
367
402
  ),
368
403
  h('div', { className: styles.modalActions },
369
404
  h('button', { className: styles.restartLater, onClick: onClose }, t('errorClose')),
@@ -380,10 +415,56 @@ export function Toast({ toast, t }: { toast: ToastState; t: Translate }) {
380
415
  : toast.kind === 'done' ? t('installDone')
381
416
  : toast.kind === 'fail' ? t('installFail')
382
417
  : toast.kind === 'removed' ? t('uninstallDone')
383
- : t('uninstallFail')
384
- const fail = toast.kind === 'fail' || toast.kind === 'removeFail'
418
+ : toast.kind === 'revealFail' ? t('openFolderFail')
419
+ : toast.kind === 'updates' ? t('updatesFound', { n: toast.n ?? 0 })
420
+ : t('uninstallFail')
421
+ const fail = toast.kind === 'fail' || toast.kind === 'removeFail' || toast.kind === 'revealFail'
385
422
  return h('div', {
386
423
  key: toast.id,
387
424
  className: fail ? `${styles.toast} ${styles.toastFail}` : styles.toast,
388
425
  }, text)
389
426
  }
427
+
428
+ /** 待重启确认弹窗:已安装列表行内「重启」按钮点击后弹出,
429
+ * 与通知中心待重启条目同一交互(说明 + 稍后重启 / 立即重启),
430
+ * 避免行内按钮误触直接触发宿主重启。 */
431
+ export function RestartConfirmModal({ t, restarting, onClose, onRestartNow }: {
432
+ t: Translate
433
+ restarting: boolean
434
+ onClose: () => void
435
+ onRestartNow: () => void
436
+ }) {
437
+ return h('div', {
438
+ className: styles.overlay,
439
+ onClick: (e: MouseEvent<HTMLDivElement>) => { if (e.target === e.currentTarget) onClose() },
440
+ },
441
+ h('div', { className: styles.modal, role: 'dialog', 'aria-modal': 'true' },
442
+ h('div', { className: styles.modalHead },
443
+ h('div', { className: styles.modalTitle }, t('sectionPendingRestart')),
444
+ h('button', {
445
+ className: styles.modalClose,
446
+ type: 'button',
447
+ 'aria-label': t('errorClose'),
448
+ onClick: onClose,
449
+ }, h(CloseIcon)),
450
+ ),
451
+ h('div', { className: styles.modalBody },
452
+ h('div', { className: styles.failPrepareHint }, t('restartHint')),
453
+ h('div', { className: styles.modalActions },
454
+ h('button', {
455
+ className: styles.restartLater,
456
+ type: 'button',
457
+ disabled: restarting,
458
+ onClick: onClose,
459
+ }, t('restartLater')),
460
+ h('button', {
461
+ className: styles.restartNowWarning,
462
+ type: 'button',
463
+ disabled: restarting,
464
+ onClick: onRestartNow,
465
+ }, restarting ? t('restarting') : t('restartNow')),
466
+ ),
467
+ ),
468
+ ),
469
+ )
470
+ }
@@ -1,12 +1,19 @@
1
1
  /**
2
- * Custom dropdown (native <select> replacement).
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
3
5
  *
4
- * macOS/浏览器原生下拉无法跟随宿主主题,这里自绘:触发器按钮 + 绝对定位面板,
5
- * 点外部/Esc 关闭;选项 hover 加重底色、激活项深色实心高亮,全部走 --dsw-alias-* 主题变量。
6
+ * Button-style dropdown (native <select> replacement).
7
+ *
8
+ * The trigger is a compact toolbar button — hairline border + subtle grey
9
+ * fill + current value + chevron — so it sits naturally next to the other
10
+ * toolbar buttons and takes a single slot of horizontal space. Clicking
11
+ * outside or pressing Esc closes the panel; options get a hover fill and
12
+ * the active one a brand highlight, all driven by the --hub-* tokens.
6
13
  */
7
14
  import { createElement as h, useEffect, useRef, useState } from 'react'
8
15
  import type { JSX } from 'react'
9
- import styles from '../styles/Dropdown.module.css'
16
+ import styles from '../../styles/Dropdown.module.css'
10
17
  import { ChevronDownIcon } from './icons.tsx'
11
18
 
12
19
  export interface DropdownOption<T extends string> {
@@ -21,9 +28,11 @@ interface DropdownProps<T extends string> {
21
28
  options: DropdownOption<T>[]
22
29
  onChange: (value: T) => void
23
30
  title?: string
31
+ /** 额外挂到根节点上的类名(如 margin 微调) */
32
+ className?: string
24
33
  }
25
34
 
26
- export function Dropdown<T extends string>({ value, options, onChange, title }: DropdownProps<T>): JSX.Element {
35
+ export function Dropdown<T extends string>({ value, options, onChange, title, className }: DropdownProps<T>): JSX.Element {
27
36
  const [open, setOpen] = useState(false)
28
37
  const rootRef = useRef<HTMLDivElement | null>(null)
29
38
 
@@ -46,7 +55,10 @@ export function Dropdown<T extends string>({ value, options, onChange, title }:
46
55
 
47
56
  const current = options.find((o) => o.value === value)
48
57
 
49
- return h('div', { className: styles.dropdown, ref: rootRef },
58
+ return h('div', {
59
+ className: className ? `${styles.dropdown} ${className}` : styles.dropdown,
60
+ ref: rootRef,
61
+ },
50
62
  h('button', {
51
63
  className: styles.dropdownBtn,
52
64
  type: 'button',
@@ -0,0 +1,28 @@
1
+ /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * Compact switch used by the settings rows. Native-button based so it is
7
+ * keyboard focusable and announces state via aria-checked; styling driven
8
+ * by the --hub-* tokens (on = brand fill, off = neutral track).
9
+ */
10
+ import { createElement as h } from 'react'
11
+ import styles from '../../styles/Toggle.module.css'
12
+
13
+ export function Toggle({ checked, onChange, title, disabled }: {
14
+ checked: boolean
15
+ onChange: (next: boolean) => void
16
+ title?: string
17
+ disabled?: boolean
18
+ }) {
19
+ return h('button', {
20
+ type: 'button',
21
+ role: 'switch',
22
+ 'aria-checked': checked,
23
+ disabled,
24
+ title,
25
+ className: checked ? styles.toggleOn : styles.toggle,
26
+ onClick: () => onChange(!checked),
27
+ }, h('span', { className: styles.knob }))
28
+ }