@dsh-workbench/shared 1.1.8 → 1.1.9

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 (224) hide show
  1. package/dist/api/blocker-table.d.ts +1 -0
  2. package/dist/api/blocker-table.js +1 -1
  3. package/dist/components/SelectField.css +0 -6
  4. package/dist/components/SelectField.d.ts +2 -3
  5. package/dist/components/SelectField.js +9 -30
  6. package/dist/components/SelectFieldNodes.d.ts +3 -1
  7. package/dist/components/SelectFieldNodes.js +2 -1
  8. package/dist/components/SelectFieldWheel.d.ts +9 -0
  9. package/dist/components/SelectFieldWheel.js +35 -0
  10. package/dist/config/constants.d.ts +4 -4
  11. package/dist/config/constants.js +4 -4
  12. package/dist/contract/capabilities.d.ts +29 -0
  13. package/dist/contract/capabilities.js +1 -0
  14. package/dist/contract/needs.d.ts +3 -3
  15. package/dist/contract/needs.js +1 -1
  16. package/dist/contract/plugin-source.d.ts +19 -0
  17. package/dist/contract/protocol/bundle-install.d.ts +3 -1
  18. package/dist/contract/protocol/bundle-install.js +3 -0
  19. package/dist/contract/protocol/platform.d.ts +1 -1
  20. package/dist/contract/runner.d.ts +6 -0
  21. package/dist/contract/runner.js +1 -0
  22. package/dist/contract/unit.d.ts +4 -4
  23. package/dist/host/adapter-contract.d.ts +3 -0
  24. package/dist/host/adapter-contract.js +93 -0
  25. package/dist/host/adapter-set.js +6 -4
  26. package/dist/host/adapter-types.d.ts +1 -1
  27. package/dist/host/bundle-manager.d.ts +3 -0
  28. package/dist/host/bundle-manager.js +181 -0
  29. package/dist/host/cli.js +3 -1
  30. package/dist/host/config-sync.d.ts +48 -0
  31. package/dist/host/config-sync.js +342 -0
  32. package/dist/host/dsh-plugins.d.ts +5 -0
  33. package/dist/host/dsh-plugins.js +51 -0
  34. package/dist/host/extension.js +1 -1
  35. package/dist/host/flow/check.d.ts +9 -0
  36. package/dist/host/flow/check.js +179 -0
  37. package/dist/host/flow/expr.d.ts +30 -0
  38. package/dist/host/flow/expr.js +99 -0
  39. package/dist/host/flow/kit.d.ts +14 -0
  40. package/dist/host/flow/kit.js +13 -0
  41. package/dist/host/flow/run.d.ts +13 -0
  42. package/dist/host/flow/run.js +287 -0
  43. package/dist/host/flow/types.d.ts +188 -0
  44. package/dist/host/flow/types.js +1 -0
  45. package/dist/host/host-actions.d.ts +2 -0
  46. package/dist/host/host-actions.js +5 -0
  47. package/dist/host/host-kit.d.ts +19 -0
  48. package/dist/host/host-kit.js +14 -0
  49. package/dist/host/http.d.ts +2 -6
  50. package/dist/host/http.js +2 -2
  51. package/dist/host/installed-module.d.ts +23 -0
  52. package/dist/host/installed-module.js +264 -0
  53. package/dist/host/lib-install.d.ts +7 -9
  54. package/dist/host/lib-install.js +9 -12
  55. package/dist/host/lib-runners.d.ts +3 -5
  56. package/dist/host/lib-runners.js +2 -2
  57. package/dist/host/local-declaration.d.ts +7 -0
  58. package/dist/host/local-declaration.js +33 -0
  59. package/dist/host/module-root.d.ts +1 -0
  60. package/dist/host/module-root.js +5 -0
  61. package/dist/host/npm-registry.d.ts +10 -0
  62. package/dist/host/npm-registry.js +16 -0
  63. package/dist/host/opencli-runner.d.ts +2 -1
  64. package/dist/host/package-content.d.ts +7 -0
  65. package/dist/host/package-content.js +69 -0
  66. package/dist/host/package-kind.d.ts +13 -0
  67. package/dist/host/package-kind.js +51 -0
  68. package/dist/host/package-route.d.ts +31 -0
  69. package/dist/host/package-route.js +57 -0
  70. package/dist/host/paths.d.ts +11 -19
  71. package/dist/host/paths.js +16 -43
  72. package/dist/host/platform.js +1 -1
  73. package/dist/host/plugin-ledger.d.ts +8 -8
  74. package/dist/host/plugin-ledger.js +23 -16
  75. package/dist/host/plugin.d.ts +1 -2
  76. package/dist/host/plugin.js +4 -5
  77. package/dist/host/port-types.d.ts +42 -9
  78. package/dist/host/preflight.d.ts +3 -2
  79. package/dist/host/profile-create.d.ts +18 -0
  80. package/dist/host/profile-create.js +118 -0
  81. package/dist/host/profile-registries.d.ts +10 -0
  82. package/dist/host/profile-registries.js +150 -0
  83. package/dist/host/registry-declaration.d.ts +21 -0
  84. package/dist/host/registry-declaration.js +33 -0
  85. package/dist/host/release-probe.d.ts +34 -0
  86. package/dist/host/release-probe.js +88 -0
  87. package/dist/host/runner.d.ts +26 -0
  88. package/dist/host/runner.js +50 -0
  89. package/dist/host/shell.d.ts +0 -2
  90. package/dist/host/shell.js +0 -7
  91. package/dist/host/spec.js +1 -1
  92. package/dist/host/supply-index.d.ts +1 -0
  93. package/dist/host/supply-index.js +8 -7
  94. package/dist/host/supply-port.js +13 -3
  95. package/dist/host/unit-face.d.ts +2 -1
  96. package/dist/host/version.js +11 -6
  97. package/dist/host.d.ts +24 -6
  98. package/dist/host.js +23 -6
  99. package/dist/i18n/en.js +16 -19
  100. package/dist/i18n/index.d.ts +17 -20
  101. package/dist/i18n/runtime-terms.d.ts +15 -0
  102. package/dist/i18n/runtime-terms.js +22 -0
  103. package/dist/i18n/zh.d.ts +16 -19
  104. package/dist/i18n/zh.js +16 -19
  105. package/dist/index.d.ts +4 -0
  106. package/dist/index.js +4 -0
  107. package/dist/testing/fake-adapter-set.d.ts +5 -0
  108. package/dist/testing/fake-adapter-set.js +150 -0
  109. package/dist/testing/needs.js +1 -1
  110. package/dist/testing.d.ts +1 -0
  111. package/dist/testing.js +1 -0
  112. package/package.json +1 -1
  113. package/src/api/__tests__/blocker-table.test.ts +25 -0
  114. package/src/api/{workbench-client.test.ts → __tests__/workbench-client.test.ts} +2 -2
  115. package/src/api/blocker-table.ts +6 -2
  116. package/src/components/SelectField.css +0 -6
  117. package/src/components/SelectField.tsx +7 -36
  118. package/src/components/SelectFieldNodes.tsx +4 -1
  119. package/src/components/{Card.test.tsx → __tests__/Card.test.tsx} +2 -2
  120. package/src/components/{CardAnchor.test.tsx → __tests__/CardAnchor.test.tsx} +3 -3
  121. package/src/components/{CardHead.test.tsx → __tests__/CardHead.test.tsx} +1 -1
  122. package/src/components/{ChoiceGroup.test.tsx → __tests__/ChoiceGroup.test.tsx} +2 -2
  123. package/src/components/{ConfirmBar.test.tsx → __tests__/ConfirmBar.test.tsx} +5 -5
  124. package/src/components/{Field.test.tsx → __tests__/Field.test.tsx} +1 -1
  125. package/src/components/{Form.test.tsx → __tests__/Form.test.tsx} +5 -5
  126. package/src/components/{FormActions.test.tsx → __tests__/FormActions.test.tsx} +3 -3
  127. package/src/components/{FormRows.test.tsx → __tests__/FormRows.test.tsx} +2 -2
  128. package/src/components/{FormTypes.test.ts → __tests__/FormTypes.test.ts} +1 -1
  129. package/src/components/{FormWiring.test.tsx → __tests__/FormWiring.test.tsx} +4 -4
  130. package/src/components/{Link.test.tsx → __tests__/Link.test.tsx} +1 -1
  131. package/src/components/{Rows.test.tsx → __tests__/Rows.test.tsx} +2 -2
  132. package/src/components/{SelectField.test.tsx → __tests__/SelectField.test.tsx} +7 -37
  133. package/src/components/{Spinner.test.tsx → __tests__/Spinner.test.tsx} +4 -4
  134. package/src/config/{constants.test.ts → __tests__/constants.test.ts} +1 -1
  135. package/src/config/{storage.test.ts → __tests__/storage.test.ts} +1 -1
  136. package/src/config/constants.ts +4 -4
  137. package/src/contract/capabilities.ts +30 -0
  138. package/src/contract/needs.ts +4 -4
  139. package/src/contract/plugin-source.ts +24 -0
  140. package/src/contract/protocol/bundle-install.ts +7 -1
  141. package/src/contract/protocol/platform.ts +1 -1
  142. package/src/contract/runner.ts +6 -0
  143. package/src/contract/unit.ts +4 -4
  144. package/src/host/{extension.test.ts → __tests__/extension.test.ts} +4 -4
  145. package/src/host/{install-failure.test.ts → __tests__/install-failure.test.ts} +1 -1
  146. package/src/host/{installed-sources.test.ts → __tests__/installed-sources.test.ts} +1 -1
  147. package/src/host/__tests__/lib-runners.test.ts +11 -2
  148. package/src/host/__tests__/plugin-ledger.test.ts +19 -6
  149. package/src/host/{recipe.test.ts → __tests__/recipe.test.ts} +3 -3
  150. package/src/host/__tests__/runtime-layout.test.ts +14 -0
  151. package/src/host/__tests__/supply-index.test.ts +2 -2
  152. package/src/host/__tests__/supply-port.test.ts +32 -0
  153. package/src/host/adapter-contract.ts +93 -0
  154. package/src/host/adapter-set.ts +6 -4
  155. package/src/host/adapter-types.ts +1 -1
  156. package/src/host/bundle-manager.ts +195 -0
  157. package/src/host/cli.ts +3 -1
  158. package/src/host/config-sync.ts +386 -0
  159. package/src/host/dsh-plugins.ts +60 -0
  160. package/src/host/extension.ts +1 -1
  161. package/src/host/flow/__tests__/advanced.test.ts +302 -0
  162. package/src/host/flow/__tests__/check.test.ts +169 -0
  163. package/src/host/flow/__tests__/run.test.ts +301 -0
  164. package/src/host/flow/check.ts +216 -0
  165. package/src/host/flow/expr.ts +94 -0
  166. package/src/host/flow/kit.ts +30 -0
  167. package/src/host/flow/run.ts +325 -0
  168. package/src/host/flow/types.ts +199 -0
  169. package/src/host/host-actions.ts +7 -0
  170. package/src/host/host-kit.ts +26 -0
  171. package/src/host/http.ts +6 -8
  172. package/src/host/installed-module.ts +272 -0
  173. package/src/host/lib-install.ts +14 -12
  174. package/src/host/lib-runners.ts +6 -8
  175. package/src/host/local-declaration.ts +37 -0
  176. package/src/host/module-root.ts +6 -0
  177. package/src/host/npm-registry.ts +21 -0
  178. package/src/host/package-content.ts +76 -0
  179. package/src/host/package-kind.ts +63 -0
  180. package/src/host/package-route.ts +93 -0
  181. package/src/host/paths.ts +27 -52
  182. package/src/host/platform.ts +1 -1
  183. package/src/host/plugin-ledger.ts +25 -18
  184. package/src/host/plugin.ts +4 -5
  185. package/src/host/port-types.ts +43 -9
  186. package/src/host/preflight.ts +3 -2
  187. package/src/host/profile-create.ts +133 -0
  188. package/src/host/profile-registries.ts +145 -0
  189. package/src/host/registry-declaration.ts +70 -0
  190. package/src/host/release-probe.ts +202 -0
  191. package/src/host/shell.ts +0 -9
  192. package/src/host/spec.ts +1 -1
  193. package/src/host/supply-index.ts +9 -7
  194. package/src/host/supply-port.ts +13 -3
  195. package/src/host/unit-face.ts +2 -1
  196. package/src/host/version.ts +12 -7
  197. package/src/host.ts +34 -6
  198. package/src/i18n/{index.test.ts → __tests__/index.test.ts} +3 -3
  199. package/src/i18n/{merge.test.ts → __tests__/merge.test.ts} +1 -1
  200. package/src/i18n/{zh.test.ts → __tests__/zh.test.ts} +2 -2
  201. package/src/i18n/en.ts +16 -20
  202. package/src/i18n/runtime-terms.ts +26 -0
  203. package/src/i18n/zh.ts +16 -20
  204. package/src/index.ts +4 -0
  205. package/src/lib/{board-fact.test.ts → __tests__/board-fact.test.ts} +1 -1
  206. package/src/lib/{busy.test.ts → __tests__/busy.test.ts} +1 -1
  207. package/src/lib/{form-problem.test.ts → __tests__/form-problem.test.ts} +1 -1
  208. package/src/lib/{git-address.test.ts → __tests__/git-address.test.ts} +1 -1
  209. package/src/lib/{react.test.ts → __tests__/react.test.ts} +1 -1
  210. package/src/lib/{text.test.ts → __tests__/text.test.ts} +1 -1
  211. package/src/lib/{ui.test.ts → __tests__/ui.test.ts} +1 -1
  212. package/src/testing/__tests__/fake-adapter-set.test.ts +28 -0
  213. package/src/testing/fake-adapter-set.ts +173 -0
  214. package/src/testing/needs.ts +1 -1
  215. package/src/testing.ts +1 -0
  216. package/src/host/__tests__/command-name.test.ts +0 -16
  217. package/src/host/__tests__/opencli-runner.test.ts +0 -120
  218. package/src/host/channel-plugin.ts +0 -55
  219. package/src/host/command-name.ts +0 -15
  220. package/src/host/opencli-runner.ts +0 -76
  221. package/src/host/paths.test.ts +0 -26
  222. package/src/host/plugin-source.ts +0 -137
  223. package/src/host/runtime-links.test.ts +0 -31
  224. package/src/host/runtime-links.ts +0 -23
@@ -0,0 +1,150 @@
1
+ import { ADAPTER_SET_API_VERSION } from '../host/adapters.js';
2
+ import { readBundleManager } from '../host/bundle-manager.js';
3
+ import { declarationGaps, moduleDeclarationOf, profileDirOf, profileModules, readInstalledModule, readProfilePackage, } from '../host/installed-module.js';
4
+ import { isNewerVersion } from '../host/version.js';
5
+ import { refusalReason } from '../host/spec.js';
6
+ import { declarationRefusalOf, pathDeclarationOf } from '../host/local-declaration.js';
7
+ import { createRegistryLookup } from '../host/registry-declaration.js';
8
+ import { createPackageRouter } from '../host/package-route.js';
9
+ import { createReleaseProbe } from '../host/release-probe.js';
10
+ export const FAKE_ADAPTER_MARKER = { adapterSet: 'fake' };
11
+ function result() {
12
+ return { ok: true, data: FAKE_ADAPTER_MARKER };
13
+ }
14
+ const data = {
15
+ async read(_request) {
16
+ return result();
17
+ },
18
+ async request(_recipe) {
19
+ return result();
20
+ },
21
+ async requestMany(_recipe, _calls) {
22
+ return {};
23
+ },
24
+ async submit(_recipe) {
25
+ return result();
26
+ },
27
+ };
28
+ const supply = {
29
+ resolvePluginSource: () => ({ ok: false, problem: 'not-declared' }),
30
+ libInstallCommand: () => '',
31
+ libUninstallCommand: () => '',
32
+ pluginInstallPlan: () => ({ ok: false, problem: 'not-declared' }),
33
+ latestVersion: async () => undefined,
34
+ missingReleaseParts: async () => [],
35
+ probeRelease: async () => undefined,
36
+ splitNpmSpec: () => undefined,
37
+ };
38
+ const modules = {
39
+ isolatedRoot: () => '/tmp/dsh-workbench-fake',
40
+ isolatedHome: () => '/tmp/dsh-workbench-fake/home',
41
+ pluginPackagePrefix: () => 'fake-plugin-',
42
+ pluginManifestFile: () => 'fake-plugin.json',
43
+ cliCatalogPath: () => '/tmp/dsh-workbench-fake/catalog.json',
44
+ vendorPluginsDir: () => '/tmp/dsh-workbench-fake/plugins',
45
+ runtimeInstallCommand: () => 'npm i -g fake-runtime',
46
+ afterLibInstall: () => undefined,
47
+ profileDirOf: (ctx) => profileDirOf(ctx),
48
+ profileModules,
49
+ readInstalledModule,
50
+ readProfilePackage,
51
+ readBundleManager,
52
+ createRegistryNotebook: () => ({}),
53
+ declarationGaps,
54
+ moduleDeclarationOf,
55
+ registryDeclarationOf: async () => undefined,
56
+ pathDeclarationOf,
57
+ declarationRefusalOf,
58
+ refusalReason,
59
+ packageRouteOf: async () => undefined,
60
+ isNewerVersion,
61
+ startSelfUpdate: () => ({}),
62
+ };
63
+ const profile = {
64
+ dirOf: () => null,
65
+ dshPlugins: () => () => ({ ok: true, plugins: [] }),
66
+ selfUpdateView: () => undefined,
67
+ };
68
+ const probe = {
69
+ probeUnits: async () => [],
70
+ registerPreflight: () => undefined,
71
+ declaredPluginCommands: () => new Map(),
72
+ };
73
+ const runtimePreset = (capabilities) => ({
74
+ packageRoot: () => '/tmp/dsh-workbench-fake',
75
+ capabilities: () => capabilities,
76
+ profile: () => ({
77
+ label: 'fake',
78
+ terms: {
79
+ plugin: { zh: '插件', en: 'plugin' },
80
+ pluginOf: { zh: '的插件', en: 'a plugin of' },
81
+ pluginThing: { zh: '插件', en: 'a plugin' },
82
+ pluginArea: { zh: '插件区', en: 'the plugin area' },
83
+ },
84
+ installHint: { zh: '无', en: 'nothing to install' },
85
+ diagnose: {},
86
+ }),
87
+ commandSurface: () => ({
88
+ name: 'fake',
89
+ pluginPrefix: 'fake-plugin-',
90
+ invoke: (site, command) => (command === undefined ? `fake ${site}` : `fake ${site} ${command}`),
91
+ help: (site) => `fake ${site} --help`,
92
+ browser: (verb) => `fake browser ${verb.join(' ')}`,
93
+ }),
94
+ commands: () => ({}),
95
+ runners: () => ({}),
96
+ health: () => ({}),
97
+ seed: () => undefined,
98
+ warnUnwritableRoot: () => undefined,
99
+ });
100
+ const plugins = {
101
+ declarationLookup: () => () => undefined,
102
+ createUnits: () => ({
103
+ refresh: async () => ({ units: [] }),
104
+ touch: () => undefined,
105
+ report: () => ({ units: [] }),
106
+ dispose: () => undefined,
107
+ }),
108
+ };
109
+ const actions = {
110
+ configSyncHandler: () => (async () => ({ ok: true })),
111
+ profileCreateHandler: () => (async () => ({ ok: true })),
112
+ configSyncAction: () => ({}),
113
+ profileCreateAction: () => ({}),
114
+ };
115
+ export function fakeAdapterSet(overrides = {}) {
116
+ const release = createReleaseProbe();
117
+ const registry = createRegistryLookup();
118
+ const router = createPackageRouter({ manifestFile: 'fake-plugin.json', dshLookup: registry.registryDshOf });
119
+ const withRelease = {
120
+ ...supply,
121
+ latestVersion: release.latestVersion,
122
+ missingReleaseParts: release.missingReleaseParts,
123
+ probeRelease: release.probeRelease,
124
+ };
125
+ return {
126
+ apiVersion: ADAPTER_SET_API_VERSION,
127
+ id: 'fake-adapter-set',
128
+ version: '0.0.0',
129
+ data,
130
+ supply: {
131
+ ...withRelease,
132
+ ...modules,
133
+ env: withRelease,
134
+ modules,
135
+ registryDeclarationOf: registry.registryDeclarationOf,
136
+ packageRouteOf: router.packageRouteOf,
137
+ },
138
+ modules,
139
+ profile,
140
+ probe,
141
+ runtime: runtimePreset({
142
+ console: false,
143
+ pluginMarket: false,
144
+ browserBridge: false,
145
+ }),
146
+ plugins,
147
+ actions,
148
+ ...overrides,
149
+ };
150
+ }
@@ -7,7 +7,7 @@ export const fakeNpmSource = {
7
7
  export function extensionNeedOf(name, extra = {}) {
8
8
  return {
9
9
  kind: 'extension',
10
- runtime: 'opencli',
10
+ runtime: 'fake-runtime',
11
11
  name,
12
12
  commands: [`${name}-read`],
13
13
  purpose: '测试',
package/dist/testing.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from './testing/host-fakes.js';
2
2
  export * from './testing/declaration.js';
3
3
  export * from './testing/needs.js';
4
4
  export * from './testing/data.js';
5
+ export * from './testing/fake-adapter-set.js';
package/dist/testing.js CHANGED
@@ -2,3 +2,4 @@ export * from './testing/host-fakes.js';
2
2
  export * from './testing/declaration.js';
3
3
  export * from './testing/needs.js';
4
4
  export * from './testing/data.js';
5
+ export * from './testing/fake-adapter-set.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/shared",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
4
4
  "description": "工作台与模块共享的纯代码与契约类型:类型、纯逻辑、哑组件、平台解析。宿主侧的东西从 /host 进(不影响浏览器产物)。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -313,3 +313,28 @@ describe('blockerOfCapability', () => {
313
313
  expect(blocker?.actions.map((action) => action.kind)).toEqual(['retry']);
314
314
  });
315
315
  });
316
+
317
+ describe('执行类失败的话术由适配器给', () => {
318
+ it('适配器给了 diagnose.exec 就用它,不再说默认那句', () => {
319
+ const blocker = blockerOfError(
320
+ { code: 'cli-exec-failed', message: '跑不起来' },
321
+ t,
322
+ { diagnose: { exec: '确认 MCP server 起得来:npx -y chrome-devtools-mcp@latest' } },
323
+ );
324
+ expect(blocker.hint).toBe('确认 MCP server 起得来:npx -y chrome-devtools-mcp@latest');
325
+ });
326
+
327
+ it('适配器没给就回词表那句(opencli 那份)', () => {
328
+ const blocker = blockerOfError({ code: 'cli-exec-failed', message: '跑不起来' }, t);
329
+ expect(blocker.hint).toBe('blocker.exec.hint');
330
+ });
331
+
332
+ it('错误自己带的 hint 优先级最高', () => {
333
+ const blocker = blockerOfError(
334
+ { code: 'cli-exec-failed', message: '跑不起来', hint: '端口被占了' },
335
+ t,
336
+ { diagnose: { exec: '确认 MCP server 起得来' } },
337
+ );
338
+ expect(blocker.hint).toBe('端口被占了');
339
+ });
340
+ });
@@ -1,6 +1,6 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
- import { API_PREFIX } from '../config/constants.js';
3
- import { apiGet } from './workbench-client.js';
2
+ import { API_PREFIX } from '../../config/constants.js';
3
+ import { apiGet } from '../workbench-client.js';
4
4
 
5
5
  interface FetchRecord {
6
6
  url: string;
@@ -22,7 +22,11 @@ function copyAction(t: Translate, text: string): BlockerAction {
22
22
  export function blockerOfError(
23
23
  error: ApiError | undefined,
24
24
  t: Translate,
25
- context: { path?: string | null; platform?: string } = {},
25
+ context: {
26
+ path?: string | null;
27
+ platform?: string;
28
+ diagnose?: Readonly<Partial<Record<'exec' | 'login' | 'bridge', string>>>;
29
+ } = {},
26
30
  ): ModuleBlocker {
27
31
  if (error === undefined) {
28
32
  return {
@@ -93,7 +97,7 @@ export function blockerOfError(
93
97
  id: 'cli-exec-failed',
94
98
  severity: 'block',
95
99
  title: error.message,
96
- hint: error.hint ?? t('blocker.exec.hint'),
100
+ hint: error.hint ?? context.diagnose?.exec ?? t('blocker.exec.hint'),
97
101
  ...(error.detail === undefined ? {} : { detail: error.detail }),
98
102
  actions: [retryAction(t)],
99
103
  };
@@ -70,12 +70,6 @@
70
70
  justify-content: center;
71
71
  }
72
72
 
73
- .wb-selectReveal {
74
- display: inline-flex;
75
- align-items: center;
76
- justify-content: center;
77
- }
78
-
79
73
  .wb-selectIconOnly .wb-selectIcon {
80
74
  width: 100%;
81
75
  height: 100%;
@@ -56,7 +56,6 @@ export interface SelectFieldProps {
56
56
  readonly selection?: 'check' | 'fill' | undefined;
57
57
  readonly variant?: 'ghost' | 'boxed' | undefined;
58
58
  readonly iconOnly?: boolean | undefined;
59
- readonly revealId?: string | undefined;
60
59
  readonly ariaLabel?: string | undefined;
61
60
  readonly title?: string | undefined;
62
61
  readonly id?: string | undefined;
@@ -76,41 +75,15 @@ function triggerClassOf(variantClass: string | undefined, iconOnly: boolean): st
76
75
  return mergeClass('wb-selectTrigger', variantClass, iconOnly ? 'wb-selectIconOnly' : undefined);
77
76
  }
78
77
 
79
- function itemOf(option: SelectFieldOption, revealId: string | undefined): MenuItem {
80
- const mark = option.value === revealId;
81
- if (!mark) {
82
- return {
83
- id: option.value,
84
- label: option.label,
85
- ...(option.icon === undefined ? {} : { icon: option.icon }),
86
- ...(option.disabled === true ? { disabled: true } : {}),
87
- };
88
- }
89
- const revealed = (node: ReactNode): ReactNode => (
90
- <span className="wb-selectReveal" data-wb-select-reveal="">
91
- {node}
92
- </span>
93
- );
78
+ function itemOf(option: SelectFieldOption): MenuItem {
94
79
  return {
95
80
  id: option.value,
96
- label: option.icon === undefined ? revealed(option.label) : option.label,
97
- ...(option.icon === undefined ? {} : { icon: revealed(option.icon) }),
81
+ label: option.label,
82
+ ...(option.icon === undefined ? {} : { icon: option.icon }),
98
83
  ...(option.disabled === true ? { disabled: true } : {}),
99
84
  };
100
85
  }
101
86
 
102
- function revealNode(): HTMLElement | null {
103
- const found = document.querySelector('[data-wb-select-reveal]');
104
- return found instanceof HTMLElement ? found : null;
105
- }
106
-
107
- function useRevealOnOpen(open: boolean, revealId: string | undefined): void {
108
- useEffect(() => {
109
- if (!open || revealId === undefined) return;
110
- revealNode()?.scrollIntoView({ block: 'nearest' });
111
- }, [open, revealId]);
112
- }
113
-
114
87
  function selectValueNode(
115
88
  current: SelectFieldOption | undefined,
116
89
  renderValue: SelectFieldProps['renderValue'],
@@ -251,11 +224,10 @@ export function stepSelectCursor(
251
224
  return (cursor + step + shown.length) % shown.length;
252
225
  }
253
226
 
254
- /** 把候选排成 `Menu` 要的条目表:按 `group` 插分组标题;一项都没有时退回 `emptyText` 那一行(不给就是空表)。`revealId` 是开列表时要滚进可视区的那个 value。 */
227
+ /** 把候选排成 `Menu` 要的条目表:按 `group` 插分组标题;一项都没有时退回 `emptyText` 那一行(不给就是空表)。 */
255
228
  export function selectEntries(
256
229
  shown: readonly SelectFieldOption[],
257
230
  emptyText: string | undefined,
258
- revealId?: string | undefined,
259
231
  ): readonly MenuEntry[] {
260
232
  if (shown.length === 0) {
261
233
  return emptyText === undefined ? [] : [{ type: 'label', id: EMPTY_ID, text: emptyText }];
@@ -267,7 +239,7 @@ export function selectEntries(
267
239
  out.push({ type: 'label', id: `__wb-select-group-${String(out.length)}`, text: option.group });
268
240
  group = option.group;
269
241
  }
270
- out.push(itemOf(option, revealId));
242
+ out.push(itemOf(option));
271
243
  }
272
244
  return out;
273
245
  }
@@ -372,7 +344,6 @@ export function SelectField(props: SelectFieldProps) {
372
344
  selection = 'fill',
373
345
  variant = 'boxed',
374
346
  iconOnly = false,
375
- revealId,
376
347
  ariaLabel,
377
348
  title,
378
349
  id,
@@ -383,7 +354,6 @@ export function SelectField(props: SelectFieldProps) {
383
354
  const state = useSelectState(props);
384
355
  const current = options.find((option) => option.value === value);
385
356
 
386
- useRevealOnOpen(state.open, revealId);
387
357
  const variantClass = variant === 'ghost' ? 'wb-selectGhost' : undefined;
388
358
  const triggerClass = triggerClassOf(variantClass, iconOnly);
389
359
 
@@ -425,6 +395,7 @@ export function SelectField(props: SelectFieldProps) {
425
395
  value: selectValueNode(current, renderValue, placeholder),
426
396
  iconOnly,
427
397
  onOpen: () => state.setOpen(true),
398
+ ref: state.triggerRef,
428
399
  });
429
400
 
430
401
  const anchor =
@@ -470,7 +441,7 @@ export function SelectField(props: SelectFieldProps) {
470
441
  anchor,
471
442
  selection,
472
443
  selectedId: selectCursorValue(state.shown, state.cursor) ?? undefined,
473
- items: selectEntries(state.shown, state.empty, revealId),
444
+ items: selectEntries(state.shown, state.empty),
474
445
  footer:
475
446
  footer === undefined
476
447
  ? undefined
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import type { ReactNode, Ref } from 'react';
2
2
  import { Fragment, h } from '../lib/react.js';
3
3
  import {
4
4
  IconChevronDownOutlineRegular,
@@ -19,6 +19,7 @@ import { Spinner } from './Spinner.js';
19
19
  * @param args.value - 已渲染好的当前值节点(文本或占位)。
20
20
  * @param args.iconOnly - 只画图标(窄栏那颗切换钮),值与箭头都不画。
21
21
  * @param args.onOpen - 点它做什么,通常是把列表打开。
22
+ * @param args.ref - 挂到这颗 `<button>` 上,取消后回焦点、菜单开着头条滚轮要用它。
22
23
  * @returns 一颗 `<button>`。
23
24
  */
24
25
  export function selectTriggerNode(args: {
@@ -33,10 +34,12 @@ export function selectTriggerNode(args: {
33
34
  readonly value: ReactNode;
34
35
  readonly iconOnly: boolean;
35
36
  readonly onOpen: () => void;
37
+ readonly ref: Ref<HTMLButtonElement> | undefined;
36
38
  }): ReactNode {
37
39
  return (
38
40
  <button
39
41
  type="button"
42
+ ref={args.ref}
40
43
  id={args.id}
41
44
  className={args.className}
42
45
  disabled={args.disabled}
@@ -1,10 +1,10 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
 
3
- vi.mock('../lib/react.js', () => ({
3
+ vi.mock('../../lib/react.js', () => ({
4
4
  h: (type: unknown, props: unknown, ...children: unknown[]) => ({ type, props, children }),
5
5
  }));
6
6
 
7
- import { Card, type CardProps } from './Card.js';
7
+ import { Card, type CardProps } from '../Card.js';
8
8
 
9
9
  const t = (key: string): string => key;
10
10
 
@@ -1,12 +1,12 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
 
3
- vi.mock('../lib/react.js', () => ({
3
+ vi.mock('../../lib/react.js', () => ({
4
4
  h: (type: unknown, props: unknown, ...children: unknown[]) => ({ type, props, children }),
5
5
  }));
6
6
 
7
- vi.mock('./CardAnchor.module.css', () => ({ default: { anchor: 'wb-anchor' } }));
7
+ vi.mock('../CardAnchor.module.css', () => ({ default: { anchor: 'wb-anchor' } }));
8
8
 
9
- import { CardAnchor, cardIdOf } from './CardAnchor.js';
9
+ import { CardAnchor, cardIdOf } from '../CardAnchor.js';
10
10
 
11
11
  interface AnyElement {
12
12
  readonly type: unknown;
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { CardHead } from './CardHead.js';
2
+ import { CardHead } from '../CardHead.js';
3
3
 
4
4
  interface AnyElement {
5
5
  readonly type: unknown;
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { Fragment, h } from '../lib/react.js';
3
- import { ChoiceGroup } from './ChoiceGroup.js';
2
+ import { Fragment, h } from '../../lib/react.js';
3
+ import { ChoiceGroup } from '../ChoiceGroup.js';
4
4
 
5
5
  interface AnyElement {
6
6
  readonly type: unknown;
@@ -1,9 +1,9 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { describe, expect, it } from 'vitest';
3
- import { ConfirmBar, type ConfirmRow } from './ConfirmBar.js';
4
- import { FormActions, type FormAction } from './FormActions.js';
5
- import { Note } from './Note.js';
6
- import { Rows } from './Rows.js';
3
+ import { ConfirmBar, type ConfirmRow } from '../ConfirmBar.js';
4
+ import { FormActions, type FormAction } from '../FormActions.js';
5
+ import { Note } from '../Note.js';
6
+ import { Rows } from '../Rows.js';
7
7
 
8
8
  interface AnyElement {
9
9
  readonly type: unknown;
@@ -84,7 +84,7 @@ describe('确认条(契约类 wb-confirmBar)', () => {
84
84
  });
85
85
  });
86
86
 
87
- const CSS = readFileSync(new URL('./ConfirmBar.css', import.meta.url), 'utf8');
87
+ const CSS = readFileSync(new URL('../ConfirmBar.css', import.meta.url), 'utf8');
88
88
 
89
89
  function declarations(selector: string): string[] {
90
90
  for (const block of CSS.split('}')) {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { Field } from './Field.js';
2
+ import { Field } from '../Field.js';
3
3
 
4
4
  function texts(node: unknown): string[] {
5
5
  if (node === null || node === undefined || typeof node === 'boolean') return [];
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { describe, expect, it } from 'vitest';
3
- import type { FormField, FormValue } from './FormTypes.js';
3
+ import type { FormField, FormValue } from '../FormTypes.js';
4
4
  import {
5
5
  duplicateKeysOf,
6
6
  formActionsOf,
@@ -11,9 +11,9 @@ import {
11
11
  formValuesOf,
12
12
  reportDuplicateKeys,
13
13
  visibleFieldsOf,
14
- } from './FormNodes.js';
15
- import { FormRow } from './FormRows.js';
16
- import type { FormAction } from './FormActions.js';
14
+ } from '../FormNodes.js';
15
+ import { FormRow } from '../FormRows.js';
16
+ import type { FormAction } from '../FormActions.js';
17
17
 
18
18
  function field(key: string, value: FormValue, validate?: FormField['validate']): FormField {
19
19
  return {
@@ -444,7 +444,7 @@ describe('formRowsOf', () => {
444
444
  });
445
445
  });
446
446
 
447
- const CSS = readFileSync(new URL('./Form.css', import.meta.url), 'utf8');
447
+ const CSS = readFileSync(new URL('../Form.css', import.meta.url), 'utf8');
448
448
 
449
449
  function declarations(selector: string): string[] {
450
450
  for (const block of CSS.split('}')) {
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { describe, expect, it, vi } from 'vitest';
3
- import { FormActions, type FormAction, type FormActionKind } from './FormActions.js';
4
- import { SaveBar } from './SaveBar.js';
3
+ import { FormActions, type FormAction, type FormActionKind } from '../FormActions.js';
4
+ import { SaveBar } from '../SaveBar.js';
5
5
 
6
6
  function action(key: string, kind?: FormActionKind): FormAction {
7
7
  return { key, label: key, ...(kind === undefined ? {} : { kind }), onClick: () => {} };
@@ -90,7 +90,7 @@ describe('FormActions', () => {
90
90
  });
91
91
  });
92
92
 
93
- const CSS = readFileSync(new URL('./FormActions.css', import.meta.url), 'utf8');
93
+ const CSS = readFileSync(new URL('../FormActions.css', import.meta.url), 'utf8');
94
94
 
95
95
  function declarations(selector: string): string[] {
96
96
  for (const block of CSS.split('}')) {
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { describe, expect, it } from 'vitest';
3
- import { FormRow, FormRows } from './FormRows.js';
3
+ import { FormRow, FormRows } from '../FormRows.js';
4
4
 
5
5
  function texts(node: unknown): string[] {
6
6
  if (node === null || node === undefined || typeof node === 'boolean') return [];
@@ -111,7 +111,7 @@ describe('FormRow', () => {
111
111
  });
112
112
  });
113
113
 
114
- const FORM_CSS = readFileSync(new URL('./FormRows.css', import.meta.url), 'utf8');
114
+ const FORM_CSS = readFileSync(new URL('../FormRows.css', import.meta.url), 'utf8');
115
115
 
116
116
  function normalize(selector: string): string {
117
117
  return selector
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, expectTypeOf, it } from 'vitest';
2
- import type { FormFieldOf, FormProps } from './FormTypes.js';
2
+ import type { FormFieldOf, FormProps } from '../FormTypes.js';
3
3
 
4
4
  type Draft = { readonly env: string; readonly branch?: string };
5
5
 
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
- import type { FormField, FormProps, FormValue } from './FormTypes.js';
3
- import { Form } from './Form.js';
2
+ import type { FormField, FormProps, FormValue } from '../FormTypes.js';
3
+ import { Form } from '../Form.js';
4
4
 
5
5
  function field(key: string, value: FormValue, validate?: FormField['validate']): FormField {
6
6
  return {
@@ -13,8 +13,8 @@ function field(key: string, value: FormValue, validate?: FormField['validate']):
13
13
  };
14
14
  }
15
15
 
16
- vi.mock('../lib/react.js', async () => {
17
- const actual = await vi.importActual<Record<string, unknown>>('../lib/react.js');
16
+ vi.mock('../../lib/react.js', async () => {
17
+ const actual = await vi.importActual<Record<string, unknown>>('../../lib/react.js');
18
18
  return {
19
19
  ...actual,
20
20
  useState: (initial: unknown) => [typeof initial === 'function' ? (initial as () => unknown)() : initial, () => {}],
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { Link } from './Link.js';
2
+ import { Link } from '../Link.js';
3
3
 
4
4
  interface AnyElement {
5
5
  readonly type: unknown;
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { describe, expect, it } from 'vitest';
3
- import { Row, Rows } from './Rows.js';
3
+ import { Row, Rows } from '../Rows.js';
4
4
 
5
5
  function texts(node: unknown): string[] {
6
6
  if (node === null || node === undefined || typeof node === 'boolean') return [];
@@ -35,7 +35,7 @@ describe('Row', () => {
35
35
  });
36
36
  });
37
37
 
38
- const ROWS_CSS = readFileSync(new URL('./Rows.css', import.meta.url), 'utf8');
38
+ const ROWS_CSS = readFileSync(new URL('../Rows.css', import.meta.url), 'utf8');
39
39
  const FLEX_CONTAINER = `.wb-rows {
40
40
  display: flex;
41
41
  flex-direction: column;