@dsh-workbench/core 0.2.0 → 0.2.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.
Files changed (59) hide show
  1. package/client.js +1429 -607
  2. package/dist/client/client.body.js +1429 -607
  3. package/dist/client/index.js +1430 -608
  4. package/dist/host/index.js +712 -158
  5. package/dist/types/app/client.d.ts +168 -81
  6. package/dist/types/app/layouts/__tests__/async-feedback.test.d.ts +1 -0
  7. package/dist/types/app/layouts/market/use-add-module.d.ts +1 -0
  8. package/dist/types/app/layouts/rightbar/MarketView.d.ts +3 -1
  9. package/dist/types/app/layouts/rightbar/{PanelPluginRows.d.ts → PanelGroupRows.d.ts} +3 -2
  10. package/dist/types/app/layouts/rightbar/PanelRowControls.d.ts +0 -1
  11. package/dist/types/app/layouts/rightbar/__tests__/market-catalog.test.d.ts +1 -0
  12. package/dist/types/app/layouts/rightbar/__tests__/market-dots.test.d.ts +1 -0
  13. package/dist/types/app/layouts/rightbar/__tests__/market-state-rules.test.d.ts +1 -0
  14. package/dist/types/app/layouts/rightbar/__tests__/panel-dismiss.test.d.ts +1 -0
  15. package/dist/types/app/layouts/rightbar/__tests__/panel-drag-target.test.d.ts +1 -0
  16. package/dist/types/app/layouts/rightbar/__tests__/panel-write.test.d.ts +1 -0
  17. package/dist/types/app/layouts/rightbar/board-selection.d.ts +9 -0
  18. package/dist/types/app/layouts/rightbar/dsh-borrow.d.ts +15 -0
  19. package/dist/types/app/layouts/rightbar/dsh-borrow.test.d.ts +1 -0
  20. package/dist/types/app/layouts/rightbar/dsh-surface.d.ts +11 -0
  21. package/dist/types/app/layouts/rightbar/dsh-surface.test.d.ts +1 -0
  22. package/dist/types/app/layouts/rightbar/market-installed-mock.d.ts +9 -0
  23. package/dist/types/app/layouts/rightbar/market-mock-types.d.ts +58 -0
  24. package/dist/types/app/layouts/rightbar/market-mock.d.ts +2 -48
  25. package/dist/types/app/layouts/rightbar/market-row-types.d.ts +4 -2
  26. package/dist/types/app/layouts/rightbar/market-rows.d.ts +9 -22
  27. package/dist/types/app/layouts/rightbar/market-section-icon.d.ts +1 -1
  28. package/dist/types/app/layouts/rightbar/market-shared-rows.d.ts +21 -0
  29. package/dist/types/app/layouts/rightbar/panel-dismiss.d.ts +2 -0
  30. package/dist/types/app/layouts/rightbar/panel-mock.d.ts +1 -1
  31. package/dist/types/app/layouts/rightbar/panel-write.d.ts +15 -0
  32. package/dist/types/app/layouts/rightbar/use-drag-listeners.d.ts +6 -0
  33. package/dist/types/app/layouts/sidebar/__tests__/session-row-ui.test.d.ts +1 -0
  34. package/dist/types/app/units/registry.d.ts +4 -1
  35. package/dist/types/modules/panels/__tests__/diy-harness.d.ts +19 -0
  36. package/dist/types/modules/panels/__tests__/diy-report.test.d.ts +1 -0
  37. package/dist/types/modules/panels/__tests__/diy-tools-health.test.d.ts +1 -0
  38. package/dist/types/modules/panels/__tests__/diy-tools-panel.test.d.ts +1 -0
  39. package/dist/types/modules/panels/__tests__/market-tools-harness.d.ts +42 -0
  40. package/dist/types/modules/panels/__tests__/market-workbench-update.test.d.ts +1 -0
  41. package/dist/types/modules/panels/model/config.d.ts +4 -3
  42. package/dist/types/modules/panels/model/diy-report.d.ts +63 -0
  43. package/dist/types/shared/api/host/__tests__/approval-note-injection.test.d.ts +1 -0
  44. package/dist/types/shared/api/host/__tests__/panel-state-route.test.d.ts +1 -0
  45. package/dist/types/shared/api/host/bundle-routes.d.ts +1 -0
  46. package/dist/types/shared/api/host/installed-module.d.ts +2 -0
  47. package/dist/types/shared/api/host/mount-units.d.ts +1 -0
  48. package/dist/types/shared/api/host/release.d.ts +6 -0
  49. package/dist/types/shared/api/host/unit-registry.d.ts +6 -0
  50. package/dist/types/shared/api/unit-manifests.d.ts +1 -0
  51. package/dist/types/shared/components/PanelFoot.d.ts +1 -1
  52. package/dist/types/shared/hooks/panel-state-report.d.ts +13 -0
  53. package/dist/types/shared/hooks/panel-state-report.test.d.ts +1 -0
  54. package/docs/HANDOFF.md +11 -0
  55. package/package.json +2 -2
  56. package/preset/diy.patch.yml +5 -26
  57. package/skills/workbench-module-diy/REVIEW.md +22 -0
  58. package/skills/workbench-module-diy/SKILL.md +3 -1
  59. package/skills/workbench-module-diy/TOOLING.md +2 -0
@@ -6,14 +6,17 @@ import { spawn } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  //#region ../shared/dist/config/constants.js
8
8
  var PLUGIN_ID = "@dsh-workbench/core";
9
+ var RELEASE_SIBLINGS = ["@dsh-workbench/shared", "@dsh-workbench/module-tools"];
9
10
  var API_PREFIX = "/workbench/api";
10
11
  var PREFS_PREFIX = `${API_PREFIX}/prefs`;
11
12
  var EVENTS_PATH = "/workbench/events";
12
13
  var ACTIONS_PREFIX = `${API_PREFIX}/actions`;
13
14
  var PROVISION_PREFIX = `${API_PREFIX}/provision`;
15
+ var PANEL_STATE_PREFIX = `${API_PREFIX}/panel-state`;
14
16
  var OPENCLI_PIN = "1.8.8";
15
17
  var PANELS_SERVICE = "workbenchPanels";
16
18
  var UNITS_SERVICE = "workbenchUnits";
19
+ var PANEL_STATE_SERVICE = "workbenchPanelState";
17
20
  var OPENCLI_TIMEOUT_MS = 12e4;
18
21
  var WARM_TIMEOUT_MS = 2e4;
19
22
  var INSTALL_TIMEOUT_MS = 3e5;
@@ -50,6 +53,11 @@ function parseMarkers(text) {
50
53
  }
51
54
  return refs;
52
55
  }
56
+ //#endregion
57
+ //#region ../shared/dist/api/unit.js
58
+ function hasEnvSupply(manifest) {
59
+ return (manifest.env?.plugins ?? []).length > 0 || (manifest.env?.libs ?? []).length > 0;
60
+ }
53
61
  var SUPPORTED_API_VERSIONS = [1];
54
62
  var DECLARATION_KEYS = [
55
63
  "apiVersion",
@@ -212,6 +220,202 @@ function parseJsonLoose(text) {
212
220
  }
213
221
  }
214
222
  }
223
+ var PANEL_STATE_MAX_TEXT = 2e3;
224
+ var PHASES = [
225
+ "idle",
226
+ "loading",
227
+ "ready",
228
+ "error"
229
+ ];
230
+ function asRecord$10(value) {
231
+ return typeof value === "object" && value !== null ? value : void 0;
232
+ }
233
+ function textOf$6(value, max) {
234
+ if (typeof value !== "string") return "";
235
+ const trimmed = value.trim();
236
+ return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
237
+ }
238
+ function atOf(value) {
239
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
240
+ if (value < 0) return null;
241
+ return Math.trunc(value);
242
+ }
243
+ function errorOf(raw) {
244
+ const record = asRecord$10(raw);
245
+ if (record === void 0) return void 0;
246
+ const code = textOf$6(record["code"], 120);
247
+ const message = textOf$6(record["message"], PANEL_STATE_MAX_TEXT);
248
+ if (code === "" && message === "") return void 0;
249
+ return {
250
+ code: code === "" ? "unknown" : code,
251
+ message
252
+ };
253
+ }
254
+ function parseCardsReport(payload) {
255
+ const record = asRecord$10(payload);
256
+ if (record === void 0) return {
257
+ ok: false,
258
+ message: "请求体要是一个对象"
259
+ };
260
+ const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
261
+ if (workspace === "") return {
262
+ ok: false,
263
+ message: "workspace 不能是空的 —— 面板按工作区存"
264
+ };
265
+ const raw = record["cards"];
266
+ if (!Array.isArray(raw)) return {
267
+ ok: false,
268
+ message: "cards 要是一个数组"
269
+ };
270
+ const cards = [];
271
+ for (const item of raw.slice(0, 32)) {
272
+ const card = asRecord$10(item);
273
+ if (card === void 0) continue;
274
+ const unit = textOf$6(card["unit"], 200);
275
+ const route = textOf$6(card["route"], 400);
276
+ const phase = textOf$6(card["phase"], 20);
277
+ const at = atOf(card["at"]);
278
+ if (unit === "" || route === "" || at === null || !PHASES.includes(phase)) continue;
279
+ const error = errorOf(card["error"]);
280
+ cards.push({
281
+ unit,
282
+ route,
283
+ phase,
284
+ at,
285
+ ...error === void 0 ? {} : { error }
286
+ });
287
+ }
288
+ if (cards.length === 0) return {
289
+ ok: false,
290
+ message: "cards 里没有一条能认的(unit / route / phase / at 都要有)"
291
+ };
292
+ return {
293
+ ok: true,
294
+ value: {
295
+ workspace,
296
+ cards
297
+ }
298
+ };
299
+ }
300
+ function parseCrashReport(payload) {
301
+ const record = asRecord$10(payload);
302
+ if (record === void 0) return {
303
+ ok: false,
304
+ message: "请求体要是一个对象"
305
+ };
306
+ const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
307
+ const unit = textOf$6(record["unit"], 200);
308
+ const reason = textOf$6(record["reason"], PANEL_STATE_MAX_TEXT);
309
+ if (workspace === "") return {
310
+ ok: false,
311
+ message: "workspace 不能是空的"
312
+ };
313
+ if (unit === "") return {
314
+ ok: false,
315
+ message: "unit 不能是空的"
316
+ };
317
+ if (reason === "") return {
318
+ ok: false,
319
+ message: "reason 不能是空的(没有原因的崩溃没法查)"
320
+ };
321
+ return {
322
+ ok: true,
323
+ value: {
324
+ workspace,
325
+ unit,
326
+ reason,
327
+ at: atOf(record["at"]) ?? Date.now()
328
+ }
329
+ };
330
+ }
331
+ function createPanelStateStore() {
332
+ const workspaces = /* @__PURE__ */ new Map();
333
+ function trim(entry, max) {
334
+ while (entry.size > max) {
335
+ const oldest = entry.keys().next();
336
+ if (oldest.done === true) return;
337
+ entry.delete(oldest.value);
338
+ }
339
+ }
340
+ function workspaceEntryOf(workspace) {
341
+ const existing = workspaces.get(workspace);
342
+ if (existing !== void 0) return existing;
343
+ const created = {
344
+ at: 0,
345
+ units: /* @__PURE__ */ new Map()
346
+ };
347
+ workspaces.set(workspace, created);
348
+ trim(workspaces, 64);
349
+ return created;
350
+ }
351
+ function unitEntryOf(entry, unit) {
352
+ const existing = entry.units.get(unit);
353
+ if (existing !== void 0) return existing;
354
+ const created = {
355
+ at: 0,
356
+ cards: /* @__PURE__ */ new Map()
357
+ };
358
+ entry.units.set(unit, created);
359
+ return created;
360
+ }
361
+ return {
362
+ cards(report) {
363
+ const entry = workspaceEntryOf(report.workspace);
364
+ for (const card of report.cards) {
365
+ const unit = unitEntryOf(entry, card.unit);
366
+ unit.cards.delete(card.route);
367
+ unit.cards.set(card.route, card);
368
+ trim(unit.cards, 12);
369
+ if (card.at > unit.at) unit.at = card.at;
370
+ if (card.at > entry.at) entry.at = card.at;
371
+ }
372
+ },
373
+ crash(report) {
374
+ const entry = workspaceEntryOf(report.workspace);
375
+ const unit = unitEntryOf(entry, report.unit);
376
+ const previous = unit.crash;
377
+ const counted = previous !== void 0 && previous.reason === report.reason && report.at >= previous.at && report.at - previous.at <= 6e5;
378
+ unit.crash = {
379
+ unit: report.unit,
380
+ reason: report.reason,
381
+ at: report.at,
382
+ count: counted ? previous.count + 1 : 1
383
+ };
384
+ if (report.at > unit.at) unit.at = report.at;
385
+ if (report.at > entry.at) entry.at = report.at;
386
+ },
387
+ snapshot(workspace) {
388
+ if (workspace === null) return {
389
+ workspace: null,
390
+ at: 0,
391
+ units: []
392
+ };
393
+ const entry = workspaces.get(workspace);
394
+ if (entry === void 0) return {
395
+ workspace,
396
+ at: 0,
397
+ units: []
398
+ };
399
+ const units = [...entry.units.entries()].map(([unit, one]) => {
400
+ const cards = [...one.cards.values()].sort((a, b) => a.route.localeCompare(b.route));
401
+ const okAt = cards.reduce((best, card) => card.phase === "ready" && (best === null || card.at > best) ? card.at : best, null);
402
+ return {
403
+ unit,
404
+ at: one.at,
405
+ okAt,
406
+ cards,
407
+ ...one.crash === void 0 ? {} : { crash: one.crash }
408
+ };
409
+ });
410
+ units.sort((a, b) => b.at - a.at || a.unit.localeCompare(b.unit));
411
+ return {
412
+ workspace,
413
+ at: entry.at,
414
+ units
415
+ };
416
+ }
417
+ };
418
+ }
215
419
  //#endregion
216
420
  //#region ../shared/dist/host/cli-error.js
217
421
  function unquote(raw) {
@@ -1403,6 +1607,42 @@ function registerActions(ctx, resources, wrote) {
1403
1607
  ...wrote === void 0 ? {} : { wrote }
1404
1608
  });
1405
1609
  }
1610
+ function registerPanelState(ctx, store) {
1611
+ return registerPost(ctx, {
1612
+ prefix: PANEL_STATE_PREFIX,
1613
+ what: "面板运行态",
1614
+ refusedCode: "report-refused",
1615
+ resources: [{
1616
+ resource: "cards",
1617
+ handle(payload) {
1618
+ const parsed = parseCardsReport(payload);
1619
+ if (!parsed.ok) return {
1620
+ ok: false,
1621
+ text: parsed.message
1622
+ };
1623
+ store.cards(parsed.value);
1624
+ return {
1625
+ ok: true,
1626
+ at: Date.now()
1627
+ };
1628
+ }
1629
+ }, {
1630
+ resource: "crash",
1631
+ handle(payload) {
1632
+ const parsed = parseCrashReport(payload);
1633
+ if (!parsed.ok) return {
1634
+ ok: false,
1635
+ text: parsed.message
1636
+ };
1637
+ store.crash(parsed.value);
1638
+ return {
1639
+ ok: true,
1640
+ at: Date.now()
1641
+ };
1642
+ }
1643
+ }]
1644
+ });
1645
+ }
1406
1646
  //#endregion
1407
1647
  //#region ../shared/dist/host/long-task.js
1408
1648
  var LIVE = /* @__PURE__ */ new Set(["running", "needs-login"]);
@@ -1796,15 +2036,48 @@ function resolvePluginSource(source, env, options = {}) {
1796
2036
  }
1797
2037
  //#endregion
1798
2038
  //#region ../shared/dist/host/tool-approval.js
2039
+ var APPROVAL_NOTE_ARG = "approval_note";
2040
+ var NOTE_SCHEMA = {
2041
+ type: "string",
2042
+ description: "一句话说明你这次想拿它干什么(要点头的那几通会显示在审批屏上)。"
2043
+ };
2044
+ function withApprovalNote(parameters, options = {}) {
2045
+ const properties = parameters["properties"];
2046
+ if (typeof properties !== "object" || properties === null) return parameters;
2047
+ const map = properties;
2048
+ if ("approval_note" in map) return parameters;
2049
+ const kept = (Array.isArray(parameters["required"]) ? parameters["required"] : []).filter((name) => name !== APPROVAL_NOTE_ARG);
2050
+ const required = options.required === true ? [...kept, APPROVAL_NOTE_ARG] : kept;
2051
+ return {
2052
+ ...parameters,
2053
+ properties: {
2054
+ ...map,
2055
+ [APPROVAL_NOTE_ARG]: NOTE_SCHEMA
2056
+ },
2057
+ ...required.length === 0 ? {} : { required }
2058
+ };
2059
+ }
2060
+ function approvalNoteOf(call) {
2061
+ const raw = call.arguments;
2062
+ const value = typeof raw === "object" && raw !== null ? raw[APPROVAL_NOTE_ARG] : void 0;
2063
+ if (typeof value !== "string") return void 0;
2064
+ const line = value.replace(/\s+/g, " ").trim();
2065
+ if (line === "") return void 0;
2066
+ return line.length > 160 ? `${line.slice(0, 160)}…` : line;
2067
+ }
1799
2068
  function messageOf$1(error) {
1800
2069
  return error instanceof Error ? error.message : String(error);
1801
2070
  }
1802
- function compose(detail, verdict) {
1803
- if (detail === void 0 || typeof detail.zh !== "string" || detail.zh.trim() === "") return verdict.fallback;
1804
- return {
2071
+ function compose(detail, verdict, note) {
2072
+ const base = detail === void 0 || typeof detail.zh !== "string" || detail.zh.trim() === "" ? verdict.fallback : {
1805
2073
  en: `${detail.en} — ${verdict.question.en}`,
1806
2074
  zh: `${detail.zh} —— ${verdict.question.zh}`
1807
2075
  };
2076
+ if (note === void 0) return base;
2077
+ return {
2078
+ en: `${base.en} — ${note}`,
2079
+ zh: `${base.zh} —— ${note}`
2080
+ };
1808
2081
  }
1809
2082
  async function approvalFor(call, context = {}) {
1810
2083
  if (context.writes !== true) return void 0;
@@ -1838,7 +2111,7 @@ async function approvalFor(call, context = {}) {
1838
2111
  return {
1839
2112
  kind: "ask",
1840
2113
  reason: verdict.reason,
1841
- displayReason: compose(facts, verdict)
2114
+ displayReason: compose(facts, verdict, approvalNoteOf(call))
1842
2115
  };
1843
2116
  }
1844
2117
  //#endregion
@@ -2207,6 +2480,22 @@ function profileModules(profileDir) {
2207
2480
  }
2208
2481
  return found;
2209
2482
  }
2483
+ function createDeclarationLookup(profileDir) {
2484
+ let roster;
2485
+ const scan = () => new Map(profileModules(profileDir).map((row) => [row.unit, row.pkg]));
2486
+ return (unit) => {
2487
+ roster ??= scan();
2488
+ let pkg = roster.get(unit);
2489
+ if (pkg === void 0) {
2490
+ roster = scan();
2491
+ pkg = roster.get(unit);
2492
+ }
2493
+ return pkg === void 0 ? void 0 : {
2494
+ pkg,
2495
+ declaration: moduleDeclarationOf(profileDir, pkg)
2496
+ };
2497
+ };
2498
+ }
2210
2499
  function registryFromLockfile(profileDir, pkg) {
2211
2500
  let text;
2212
2501
  try {
@@ -2667,40 +2956,58 @@ function strings(raw) {
2667
2956
  function parseBoard(raw) {
2668
2957
  if (typeof raw !== "object" || raw === null) return EMPTY_BOARD;
2669
2958
  const value = raw;
2959
+ const order = strings(value.order);
2670
2960
  return {
2671
2961
  shown: strings(value.shown),
2672
- hidden: strings(value.hidden)
2962
+ hidden: strings(value.hidden),
2963
+ ...order.length === 0 ? {} : { order }
2673
2964
  };
2674
2965
  }
2675
2966
  function parseReleased(raw) {
2676
2967
  if (typeof raw !== "object" || raw === null) return [];
2677
2968
  return strings(raw.ids);
2678
2969
  }
2679
- function resolvePanels(manifests, boards) {
2680
- const ordered = [.../* @__PURE__ */ new Set([...boards.workspace?.shown ?? [], ...boards.machine?.shown ?? []])];
2681
- const rank = new Map(ordered.map((id, index) => [id, index]));
2682
- return manifests.filter((manifest) => manifest.card !== null).map((manifest) => {
2970
+ function resolvePanels(manifests, boards, owned = /* @__PURE__ */ new Set()) {
2971
+ const listed = manifests.filter((manifest) => manifest.card !== null || !owned.has(manifest.id));
2972
+ const carded = listed.filter((manifest) => manifest.card !== null);
2973
+ const declared = (manifest) => manifest.card?.order ?? 0;
2974
+ const byDeclared = (a, b) => declared(a) - declared(b) || a.id.localeCompare(b.id);
2975
+ const queued = [...new Set(boards.workspace?.order ?? [])].map((id) => carded.find((manifest) => manifest.id === id)).filter((manifest) => manifest !== void 0).filter((manifest) => resolveScope({
2976
+ id: manifest.id,
2977
+ boards
2978
+ }));
2979
+ const placed = new Set(queued.map((manifest) => manifest.id));
2980
+ const ordered = [...queued, ...carded.filter((manifest) => !placed.has(manifest.id) && resolveScope({
2981
+ id: manifest.id,
2982
+ boards
2983
+ })).sort(byDeclared)];
2984
+ const rank = new Map(ordered.map((manifest, index) => [manifest.id, index]));
2985
+ return listed.map((manifest) => {
2986
+ const hasCard = manifest.card !== null;
2683
2987
  const on = resolveScope({
2684
2988
  id: manifest.id,
2685
2989
  boards
2686
2990
  });
2687
- const order = rank.get(manifest.id) ?? ordered.length + (manifest.card?.order ?? 0);
2991
+ const order = hasCard ? rank.get(manifest.id) ?? ordered.length + declared(manifest) : 0;
2688
2992
  return {
2689
2993
  id: manifest.id,
2690
2994
  titleKey: manifest.card?.titleKey ?? manifest.id,
2691
2995
  order,
2692
2996
  shown: on,
2693
2997
  source: overrideOf(boards.workspace, manifest.id) === void 0 ? "default" : "user",
2694
- hasCard: true
2998
+ hasCard
2695
2999
  };
2696
- }).sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
3000
+ }).sort((a, b) => {
3001
+ if (a.hasCard !== b.hasCard) return a.hasCard ? -1 : 1;
3002
+ return a.order - b.order || a.id.localeCompare(b.id);
3003
+ });
2697
3004
  }
2698
3005
  var PLUGIN_PANEL_PREFIX = "plugin:";
2699
3006
  function pluginPanelId(pkg) {
2700
3007
  return `${PLUGIN_PANEL_PREFIX}${pkg}`;
2701
3008
  }
2702
3009
  function resolvePluginPanels(plugins, boards) {
2703
- const ordered = [.../* @__PURE__ */ new Set([...boards.workspace?.shown ?? [], ...boards.machine?.shown ?? []])];
3010
+ const ordered = [...new Set(boards.workspace?.order ?? [])];
2704
3011
  const rank = new Map(ordered.map((id, index) => [id, index]));
2705
3012
  const seen = /* @__PURE__ */ new Set();
2706
3013
  return plugins.map((plugin) => {
@@ -2735,16 +3042,22 @@ function added(list, id) {
2735
3042
  function dropped(list, id) {
2736
3043
  return list.filter((item) => item !== id);
2737
3044
  }
2738
- function withShown(board, id) {
2739
- return {
3045
+ function withShown(board, id, visible) {
3046
+ const next = {
2740
3047
  shown: added(board.shown, id),
2741
3048
  hidden: dropped(board.hidden, id)
2742
3049
  };
3050
+ return visible === void 0 ? next : {
3051
+ ...next,
3052
+ order: added(board.order ?? visible, id)
3053
+ };
2743
3054
  }
2744
3055
  function withHidden(board, id) {
3056
+ const order = dropped(board.order ?? [], id);
2745
3057
  return {
2746
3058
  shown: dropped(board.shown, id),
2747
- hidden: added(board.hidden, id)
3059
+ hidden: added(board.hidden, id),
3060
+ ...order.length === 0 ? {} : { order }
2748
3061
  };
2749
3062
  }
2750
3063
  function withoutPanel(board, id) {
@@ -2753,71 +3066,15 @@ function withoutPanel(board, id) {
2753
3066
  hidden: dropped(board.hidden, id)
2754
3067
  };
2755
3068
  }
2756
- function withMoved(board, id, index) {
2757
- const current = [...board.shown];
3069
+ function withMoved(board, id, index, visible) {
3070
+ const current = visible.includes(id) ? [...visible] : [...visible, id];
2758
3071
  const from = current.indexOf(id);
2759
- const max = from === -1 ? current.length : current.length - 1;
2760
- const to = Math.max(0, Math.min(max, Math.trunc(index)));
2761
- if (from === -1) current.splice(to, 0, id);
2762
- else if (from !== to) current.splice(to, 0, ...current.splice(from, 1));
2763
- return {
2764
- shown: current,
2765
- hidden: dropped(board.hidden, id)
2766
- };
2767
- }
2768
- //#endregion
2769
- //#region src/shared/api/host/diy-facts.ts
2770
- function asExported(dir) {
2771
- try {
2772
- const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
2773
- return (typeof manifest.exports === "object" && manifest.exports !== null ? Object.values(manifest.exports) : []).filter((one) => typeof one === "string");
2774
- } catch {
2775
- return [];
2776
- }
2777
- }
2778
- function mtimeOf(file) {
2779
- try {
2780
- return statSync(file).mtimeMs;
2781
- } catch {
2782
- return;
2783
- }
2784
- }
2785
- function newestMtime(dir) {
2786
- let newest;
2787
- const walk = (at) => {
2788
- let entries;
2789
- try {
2790
- entries = readdirSync(at, { withFileTypes: true });
2791
- } catch {
2792
- return;
2793
- }
2794
- for (const entry of entries) {
2795
- const full = join(at, entry.name);
2796
- if (entry.isDirectory()) walk(full);
2797
- else if (entry.isFile()) {
2798
- const stamp = mtimeOf(full);
2799
- if (stamp !== void 0 && (newest === void 0 || stamp > newest)) newest = stamp;
2800
- }
2801
- }
2802
- };
2803
- walk(dir);
2804
- return newest;
2805
- }
2806
- function moduleBuildFacts(dir) {
2807
- const artifacts = asExported(dir).map((rel) => ({
2808
- path: rel,
2809
- mtime: mtimeOf(join(dir, rel))
2810
- }));
2811
- const present = artifacts.filter((one) => one.mtime !== void 0);
2812
- const oldestBuilt = present.reduce((min, one) => min === void 0 || one.mtime < min ? one.mtime : min, void 0);
2813
- const newestSource = newestMtime(join(dir, "src"));
3072
+ const to = Math.max(0, Math.min(current.length - 1, Math.trunc(index)));
3073
+ if (from !== to) current.splice(to, 0, ...current.splice(from, 1));
2814
3074
  return {
2815
- artifacts: artifacts.map((one) => ({
2816
- path: one.path,
2817
- at: one.mtime === void 0 ? null : new Date(one.mtime).toISOString()
2818
- })),
2819
- builtAll: present.length === artifacts.length && artifacts.length > 0,
2820
- stale: newestSource !== void 0 && oldestBuilt !== void 0 && newestSource > oldestBuilt
3075
+ shown: added(board.shown, id),
3076
+ hidden: dropped(board.hidden, id),
3077
+ order: current
2821
3078
  };
2822
3079
  }
2823
3080
  //#endregion
@@ -3040,10 +3297,169 @@ function installedPluginsOf(dir = vendorPluginsDir()) {
3040
3297
  }));
3041
3298
  }
3042
3299
  //#endregion
3300
+ //#region src/shared/api/host/diy-facts.ts
3301
+ function asExported(dir) {
3302
+ try {
3303
+ const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
3304
+ return (typeof manifest.exports === "object" && manifest.exports !== null ? Object.values(manifest.exports) : []).filter((one) => typeof one === "string");
3305
+ } catch {
3306
+ return [];
3307
+ }
3308
+ }
3309
+ function mtimeOf(file) {
3310
+ try {
3311
+ return statSync(file).mtimeMs;
3312
+ } catch {
3313
+ return;
3314
+ }
3315
+ }
3316
+ function newestMtime(dir) {
3317
+ let newest;
3318
+ const walk = (at) => {
3319
+ let entries;
3320
+ try {
3321
+ entries = readdirSync(at, { withFileTypes: true });
3322
+ } catch {
3323
+ return;
3324
+ }
3325
+ for (const entry of entries) {
3326
+ const full = join(at, entry.name);
3327
+ if (entry.isDirectory()) walk(full);
3328
+ else if (entry.isFile()) {
3329
+ const stamp = mtimeOf(full);
3330
+ if (stamp !== void 0 && (newest === void 0 || stamp > newest)) newest = stamp;
3331
+ }
3332
+ }
3333
+ };
3334
+ walk(dir);
3335
+ return newest;
3336
+ }
3337
+ function moduleBuildFacts(dir) {
3338
+ const artifacts = asExported(dir).map((rel) => ({
3339
+ path: rel,
3340
+ mtime: mtimeOf(join(dir, rel))
3341
+ }));
3342
+ const present = artifacts.filter((one) => one.mtime !== void 0);
3343
+ const oldestBuilt = present.reduce((min, one) => min === void 0 || one.mtime < min ? one.mtime : min, void 0);
3344
+ const newestSource = newestMtime(join(dir, "src"));
3345
+ return {
3346
+ artifacts: artifacts.map((one) => ({
3347
+ path: one.path,
3348
+ at: one.mtime === void 0 ? null : new Date(one.mtime).toISOString()
3349
+ })),
3350
+ builtAll: present.length === artifacts.length && artifacts.length > 0,
3351
+ stale: newestSource !== void 0 && oldestBuilt !== void 0 && newestSource > oldestBuilt
3352
+ };
3353
+ }
3354
+ //#endregion
3355
+ //#region src/modules/panels/model/diy-report.ts
3356
+ function healthOf(options) {
3357
+ const { registry, unit, installed } = options;
3358
+ const refused = registry?.refusals().find((one) => one.unit === unit);
3359
+ if (refused !== void 0) return {
3360
+ state: "failed",
3361
+ reason: refused.reasons.join(";")
3362
+ };
3363
+ const failed = registry?.failureOf(unit);
3364
+ if (failed !== void 0) return {
3365
+ state: "failed",
3366
+ reason: failed
3367
+ };
3368
+ if (registry?.all().some((one) => one.unit === unit) === true) return { state: "ok" };
3369
+ return {
3370
+ state: "absent",
3371
+ reason: installed ? "工作台还没读到它的声明 —— 新装的模块要重启一次工作台" : "还没装"
3372
+ };
3373
+ }
3374
+ function healthText(health) {
3375
+ if (health.state === "ok") return "是";
3376
+ if (health.state === "failed") return `不是 —— ${health.reason ?? "(没给原因)"}`;
3377
+ return `还没有(${health.reason ?? "还不知道"})`;
3378
+ }
3379
+ function ageText(at, now) {
3380
+ const seconds = Math.max(0, Math.round((now - at) / 1e3));
3381
+ if (seconds < 10) return "刚刚";
3382
+ if (seconds < 60) return `${String(seconds)} 秒前`;
3383
+ const minutes = Math.round(seconds / 60);
3384
+ if (minutes < 60) return `${String(minutes)} 分钟前`;
3385
+ return `${String(Math.round(minutes / 60))} 小时前`;
3386
+ }
3387
+ function stampOf(at) {
3388
+ return `${new Date(at).toISOString().slice(11, 19)}Z`;
3389
+ }
3390
+ function readText$1(read, now) {
3391
+ const when = `${stampOf(read.at)} · ${ageText(read.at, now)}`;
3392
+ if (read.phase === "ready") return `${read.route} 读到了(${when})`;
3393
+ if (read.phase === "loading") return `${read.route} 正在读(${when})`;
3394
+ if (read.phase === "idle") return `${read.route} 没在取数(${when})`;
3395
+ const code = read.error?.code ?? "unknown";
3396
+ const message = read.error?.message ?? "";
3397
+ return `${read.route} 读不到(${code})${message === "" ? "" : `:${message}`}(${when})`;
3398
+ }
3399
+ function renderPanel(options) {
3400
+ const { packagesDir, workspace, rows, board, at, now } = options;
3401
+ const head = `工坊:${packagesDir}\n这个工作区:${workspace ?? "(会话不在任何工作区上)"}`;
3402
+ const note = at === 0 ? "\n还没有一条面板运行态上报 —— 面板上一个取数 / 渲染都没发生过:要么这个工作区的面板上没开卡片,要么用户那一页还是刷新前的旧版本(上报要页面刷一次才带上)。" : `\n面板上有 ${String(board.length)} 张卡|运行态来自用户那一页(最后一次上报 ${stampOf(at)} · ${ageText(at, now)})`;
3403
+ const body = rows.map((row) => {
3404
+ const title = `—— ${row.unit}${row.name === "" ? "" : `(${row.name})`} · ${healthText(row.health)}`;
3405
+ const report = row.report;
3406
+ if (report === void 0) return [title, ` 没上报:卡片没渲染出来,或者用户那一页还没刷新到带这份补丁的版本`].join("\n");
3407
+ const reads = report.cards.slice(0, 6).map((one) => ` 数据 · ${readText$1(one, now)}`);
3408
+ const more = report.cards.length > 6 ? [` 数据 · …还有 ${String(report.cards.length - 6)} 条`] : [];
3409
+ const crash = report.crash === void 0 ? " 崩过:没有" : ` 崩过:是 —— ${report.crash.reason}(${stampOf(report.crash.at)} · 共 ${String(report.crash.count)} 次)`;
3410
+ return [
3411
+ title,
3412
+ ...reads,
3413
+ ...more,
3414
+ crash
3415
+ ].join("\n");
3416
+ });
3417
+ return body.length === 0 ? `${head}${note}` : `${head}${note}\n${body.join("\n")}`;
3418
+ }
3419
+ function statusOf(options) {
3420
+ const { dir, unit, name, installed, known, open, health } = options;
3421
+ const facts = moduleBuildFacts(dir);
3422
+ const next = health.state === "failed" ? `它起不来:${health.reason ?? "(没给原因)"} —— 先把这一条修掉(改模块源码 → build;改了 package.json / cordis.patch.yml 要重启工作台),别去重装` : !installed ? "装:action=install(第一次,会让用户点头一次)" : !known ? "面板还不认得这个单元 —— 重启一次工作台(新装的模块要重启才注册进面板),重启后再 action=open" : !open ? "上板:action=open(只上板,不重装)" : !facts.builtAll ? "还没构建:pnpm exec dsh-wb-module build" : facts.stale ? "源码比产物新:pnpm exec dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新页面)" : "都在生效:改源码 → build(宿主侧秒级热换;改了 client.js 刷新页面),不要再装一遍";
3423
+ return {
3424
+ unit,
3425
+ name,
3426
+ dir,
3427
+ installed,
3428
+ known,
3429
+ open,
3430
+ artifacts: facts.artifacts,
3431
+ stale: facts.stale,
3432
+ health,
3433
+ next
3434
+ };
3435
+ }
3436
+ function renderStatus(packagesDir, lines) {
3437
+ if (lines.length === 0) return renderList(packagesDir, []);
3438
+ return `工坊:${packagesDir}\n${lines.map((one) => {
3439
+ const head = `—— ${one.unit}(${one.name})`;
3440
+ const facts = ` 装:${one.installed ? "是" : "没有"} · 面板认得:${one.known ? "是" : "不是"} · 这个项目上开着:${one.open ? "是" : "没有"}`;
3441
+ const artifacts = one.artifacts.length === 0 ? " 产物:没有" : ` 产物:${one.artifacts.map((item) => `${item.path}${item.at === null ? "(缺)" : ` ${item.at.slice(11, 19)}Z`}`).join(" · ")} · 源码比产物新:${one.stale ? "是" : "没有"}`;
3442
+ return [
3443
+ head,
3444
+ facts,
3445
+ ` 起得来:${healthText(one.health)}`,
3446
+ artifacts,
3447
+ ` 下一步:${one.next}`
3448
+ ].join("\n");
3449
+ }).join("\n")}`;
3450
+ }
3451
+ function renderList(packagesDir, modules) {
3452
+ if (modules.length === 0) return `工坊里还没有模块(${packagesDir})—— 在里面造一个:pnpm exec dsh-wb-module init <名字> --dir <工坊>/packages/module-<名字> --template diy`;
3453
+ const lines = modules.map((one) => `${one.unit} · ${one.installed ? "已装" : "没装"} · ${one.name}` + (one.description === "" ? "" : `\n${" ".repeat(one.unit.length)} ${one.description}`));
3454
+ const footer = modules.filter((one) => !one.installed).length === 0 ? "都已装过 —— **不要再装**:改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。" : "装:workbench_diy(action=install, unit=<上面那个 unit>)";
3455
+ return `工坊:${packagesDir}\n${lines.join("\n")}\n\n${footer}`;
3456
+ }
3457
+ //#endregion
3043
3458
  //#region src/modules/panels/model/diy-tools.ts
3044
3459
  var ACTIONS$2 = [
3045
3460
  "list",
3046
3461
  "status",
3462
+ "panel",
3047
3463
  "probe",
3048
3464
  "recon",
3049
3465
  "install",
@@ -3099,33 +3515,6 @@ function verdictOf$1(change) {
3099
3515
  const tail = (change.packageResult?.output ?? "").trim().split("\n").slice(-3).join(" | ");
3100
3516
  return `没装上(${change.errorCode ?? "failed"})${tail === "" ? "" : `:${tail}`}`;
3101
3517
  }
3102
- function statusOf(options) {
3103
- const { dir, unit, name, installed, known, open } = options;
3104
- const facts = moduleBuildFacts(dir);
3105
- const next = !installed ? "装:action=install(第一次,会让用户点头一次)" : !known ? "面板还不认得这个单元 —— 重启一次工作台(新装的模块要重启才注册进面板),重启后再 action=open" : !open ? "上板:action=open(只上板,不重装)" : !facts.builtAll ? "还没构建:pnpm exec dsh-wb-module build" : facts.stale ? "源码比产物新:pnpm exec dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新页面)" : "都在生效:改源码 → build(宿主侧秒级热换;改了 client.js 刷新页面),不要再装一遍";
3106
- return {
3107
- unit,
3108
- name,
3109
- dir,
3110
- installed,
3111
- known,
3112
- open,
3113
- artifacts: facts.artifacts,
3114
- stale: facts.stale,
3115
- next
3116
- };
3117
- }
3118
- function renderStatus(packagesDir, lines) {
3119
- if (lines.length === 0) return renderList(packagesDir, []);
3120
- return `工坊:${packagesDir}\n${lines.map((one) => {
3121
- return [
3122
- `—— ${one.unit}(${one.name})`,
3123
- ` 装:${one.installed ? "是" : "没有"} · 面板认得:${one.known ? "是" : "不是"} · 这个项目上开着:${one.open ? "是" : "没有"}`,
3124
- one.artifacts.length === 0 ? " 产物:没有" : ` 产物:${one.artifacts.map((item) => `${item.path}${item.at === null ? "(缺)" : ` ${item.at.slice(11, 19)}Z`}`).join(" · ")} · 源码比产物新:${one.stale ? "是" : "没有"}`,
3125
- ` 下一步:${one.next}`
3126
- ].join("\n");
3127
- }).join("\n")}`;
3128
- }
3129
3518
  function renderProbe(out) {
3130
3519
  if (out.ok) return `读到了 —— 这就是那条读的返回值,把它固化成模块宿主半侧的那一条:\n${(JSON.stringify(out.value, null, 2) ?? "undefined").slice(0, 2e3)}`;
3131
3520
  const lines = [`没读到(${out.code}):${out.message}`];
@@ -3146,12 +3535,6 @@ function renderFind(out) {
3146
3535
  const more = out.entries.length > 20 ? `\n… 还有 ${String(out.entries.length - 20)} 个` : "";
3147
3536
  return `找到了 ${String(out.count)} 个元素 —— 每行的单元格按换行拼在 text 里;把它固化成模块宿主半侧的那一条(host 的 browserFind + 切格 + 按列序取值):\n${head.join("\n")}${more}`;
3148
3537
  }
3149
- function renderList(packagesDir, modules) {
3150
- if (modules.length === 0) return `工坊里还没有模块(${packagesDir})—— 在里面造一个:pnpm exec dsh-wb-module init <名字> --dir <工坊>/packages/module-<名字> --template diy`;
3151
- const lines = modules.map((one) => `${one.unit} · ${one.installed ? "已装" : "没装"} · ${one.name}` + (one.description === "" ? "" : `\n${" ".repeat(one.unit.length)} ${one.description}`));
3152
- const footer = modules.filter((one) => !one.installed).length === 0 ? "都已装过 —— **不要再装**:改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。" : "装:workbench_diy(action=install, unit=<上面那个 unit>)";
3153
- return `工坊:${packagesDir}\n${lines.join("\n")}\n\n${footer}`;
3154
- }
3155
3538
  function showOrReason(panels, unit, workspace) {
3156
3539
  try {
3157
3540
  panels.show(unit, workspace);
@@ -3171,18 +3554,18 @@ function registerDiyTools(ctx, market, panels) {
3171
3554
  }
3172
3555
  const tool = {
3173
3556
  name: "workbench_diy",
3174
- description: "看 DIY 工坊里的模块现在走到哪一步,或者把它们装上 / 上板。只认工坊里的模块(不是市场 —— 装别的东西用 workbench_modules)。status 会把五件事一次说清(装了没 / 面板认得没 / 这个项目上开着没 / 产物齐不齐 / 源码是否比产物新)并给出下一步,所以别自己去拼这些状态、也别反复 install。recon 是**先看有没有现成的**:拿一个关键词(站点名 / 系统名 / 域名)去 opencli 自带的那份目录里找,回它有哪些命令、要不要用户的 Chrome 登录;都没有就老实说,并给出\"写 adapter\"或\"探一条读\"两条路。用户不知道 opencli 是什么 —— 这一步别问他,自己查。probe 是**探一条读**:用用户浏览器里现有的登录去读一次,把结果与失败码归一(两拍、重试、平台码映射都在里面),探明之后再把它固化成模块宿主半侧的那一条。两种读法二选一:script(页面里跑一段回 JSON 的脚本,读接口)或 css(给一个选择器,读 DOM 那一行 —— 页面里没有接口时用这个,别写 document.querySelectorAll)。装是这台机器上的持久变更,要用户点头;**一辈子只装一次**:之后改源码只需 dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面),不要再调它。",
3175
- parameters: {
3557
+ description: "看 DIY 工坊里的模块现在走到哪一步,或者把它们装上 / 上板。只认工坊里的模块(不是市场 —— 装别的东西用 workbench_modules)。status 会把六件事一次说清(装了没 / 面板认得没 / 这个项目上开着没 / **起得来没** / 产物齐不齐 / 源码是否比产物新)并给出下一步,所以别自己去拼这些状态、也别反复 install。panel 是**面板上现在究竟是什么样**:这个工作区开了哪几张卡、每张最后是读到了还是读不到(带宿主回的原文)、有没有崩过 —— 用户说\"卡片不对 / 一片空白 / 一直转圈\"就先问它,别靠猜、也别让他截图给你。recon 是**先看有没有现成的**:拿一个关键词(站点名 / 系统名 / 域名)去 opencli 自带的那份目录里找,回它有哪些命令、要不要用户的 Chrome 登录;都没有就老实说,并给出\"写 adapter\"或\"探一条读\"两条路。用户不知道 opencli 是什么 —— 这一步别问他,自己查。probe 是**探一条读**:用用户浏览器里现有的登录去读一次,把结果与失败码归一(两拍、重试、平台码映射都在里面),探明之后再把它固化成模块宿主半侧的那一条。两种读法二选一:script(页面里跑一段回 JSON 的脚本,读接口)或 css(给一个选择器,读 DOM 那一行 —— 页面里没有接口时用这个,别写 document.querySelectorAll)。装是这台机器上的持久变更,要用户点头;**一辈子只装一次**:之后改源码只需 dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面),不要再调它。",
3558
+ parameters: withApprovalNote({
3176
3559
  type: "object",
3177
3560
  properties: {
3178
3561
  action: {
3179
3562
  type: "string",
3180
- description: "list=工坊里有哪些模块(默认);status=某个模块现在走到哪一步、下一步做什么;probe=在用户浏览器里探一条读;recon=先查 opencli 现成的能力(自带 adapter / 本机插件);open=只把它在这个项目上板(不装);install=装上并上板(第一次、会让用户点头)。",
3563
+ description: "list=工坊里有哪些模块(默认);status=某个模块现在走到哪一步、下一步做什么;panel=这个工作区的面板上现在是什么样(谁读到了 / 谁读不到 / 谁崩过);probe=在用户浏览器里探一条读;recon=先查 opencli 现成的能力(自带 adapter / 本机插件);open=只把它在这个项目上板(不装);install=装上并上板(第一次、会让用户点头)。",
3181
3564
  enum: [...ACTIONS$2]
3182
3565
  },
3183
3566
  unit: {
3184
3567
  type: "string",
3185
- description: "status / open / install 填:工坊里那个模块的单元 id(list 会给)。"
3568
+ description: "status / panel / open / install 填:工坊里那个模块的单元 id(list 会给)。"
3186
3569
  },
3187
3570
  script: {
3188
3571
  type: "string",
@@ -3223,7 +3606,7 @@ function registerDiyTools(ctx, market, panels) {
3223
3606
  },
3224
3607
  required: ["action"],
3225
3608
  additionalProperties: false
3226
- },
3609
+ }, { required: true }),
3227
3610
  output: {
3228
3611
  schema: OUTPUT_SCHEMA$2,
3229
3612
  render: (_args, value) => {
@@ -3243,7 +3626,10 @@ function registerDiyTools(ctx, market, panels) {
3243
3626
  const session = sessionWorkspace(exec);
3244
3627
  if (!inWorkshopSession(ctx, session)) throw new Error(`这把工具只在 DIY 工坊里用:当前会话不在工坊里(这个会话的工作区 ${session ?? "(不知道)"})—— 普通项目里装模块用 workbench_modules,工坊的入口是左栏那一行「工作台DIY」`);
3245
3628
  const workspace = session;
3246
- const modules = workshopModules(packagesDir, profileModules(profileDirOf(ctx)).map((row) => row.unit));
3629
+ const installed = profileModules(profileDirOf(ctx)).map((row) => row.unit);
3630
+ const modules = workshopModules(packagesDir, installed);
3631
+ const registry = ctx.get(UNITS_SERVICE);
3632
+ const panelState = ctx.get(PANEL_STATE_SERVICE);
3247
3633
  if (action === "list") return plainJson({
3248
3634
  workshop: packagesDir,
3249
3635
  modules,
@@ -3262,7 +3648,12 @@ function registerDiyTools(ctx, market, panels) {
3262
3648
  name: one.name,
3263
3649
  installed: one.installed,
3264
3650
  known: panelEntries.some((entry) => entry.id === one.unit),
3265
- open: panels.isEnabled(one.unit, workspace)
3651
+ open: panels.isEnabled(one.unit, workspace),
3652
+ health: healthOf({
3653
+ registry,
3654
+ unit: one.unit,
3655
+ installed: one.installed
3656
+ })
3266
3657
  });
3267
3658
  if (action === "status") {
3268
3659
  const wanted = asText$1(args.unit);
@@ -3275,6 +3666,49 @@ function registerDiyTools(ctx, market, panels) {
3275
3666
  text: renderStatus(packagesDir, rows)
3276
3667
  });
3277
3668
  }
3669
+ if (action === "panel") {
3670
+ const snapshot = panelState?.snapshot(workspace) ?? {
3671
+ workspace,
3672
+ at: 0,
3673
+ units: []
3674
+ };
3675
+ const wanted = asText$1(args.unit);
3676
+ const board = panelEntries.filter((entry) => entry.shown && entry.hasCard).map((entry) => entry.id);
3677
+ const nameOf = new Map(modules.map((one) => [one.unit, one.name]));
3678
+ const reported = new Map(snapshot.units.map((one) => [one.unit, one]));
3679
+ const rows = [.../* @__PURE__ */ new Set([
3680
+ ...board,
3681
+ ...reported.keys(),
3682
+ ...nameOf.keys()
3683
+ ])].filter((unit) => wanted === "" || unit === wanted).map((unit) => {
3684
+ const report = reported.get(unit);
3685
+ return {
3686
+ unit,
3687
+ name: nameOf.get(unit) ?? "",
3688
+ health: healthOf({
3689
+ registry,
3690
+ unit,
3691
+ installed: installed.includes(unit)
3692
+ }),
3693
+ ...report === void 0 ? {} : { report }
3694
+ };
3695
+ });
3696
+ return plainJson({
3697
+ workshop: packagesDir,
3698
+ workspace,
3699
+ board,
3700
+ report: snapshot,
3701
+ panels: rows,
3702
+ text: renderPanel({
3703
+ packagesDir,
3704
+ workspace,
3705
+ rows,
3706
+ board,
3707
+ at: snapshot.at,
3708
+ now: Date.now()
3709
+ })
3710
+ });
3711
+ }
3278
3712
  if (action === "recon") {
3279
3713
  const keywords = keywordsOf(asText$1(args.keyword), asText$1(args.url));
3280
3714
  if (keywords.length === 0) throw new Error("recon 要一个关键词(站点名 / 系统名 / 域名都行):先拿它去 opencli 自带的那份目录里找有没有现成的能力,别一上来就自己写 adapter;用户不知道 opencli 是什么,这一步别问他。");
@@ -3429,6 +3863,32 @@ async function latestVersion(options) {
3429
3863
  });
3430
3864
  return value;
3431
3865
  }
3866
+ var existsCache = /* @__PURE__ */ new Map();
3867
+ async function versionExists(options) {
3868
+ const { shell, name, version, registry, force = false } = options;
3869
+ const key = `${registry}|${name}@${version}`;
3870
+ const hit = existsCache.get(key);
3871
+ if (!force && hit !== void 0 && (hit.value || Date.now() - hit.at < 6e5)) return hit.value;
3872
+ const result = await runShell(shell, `npm view ${quoteShellArg(`${name}@${version}`)} version --registry ${quoteShellArg(registry)} --cache ${quoteShellArg(npmCacheDir())}`, { timeoutMs: VERSION_CHECK_TIMEOUT_MS }).catch(() => void 0);
3873
+ const value = result?.exitCode === 0 && parseVersion(result.stdout) !== void 0;
3874
+ existsCache.set(key, {
3875
+ at: Date.now(),
3876
+ value
3877
+ });
3878
+ return value;
3879
+ }
3880
+ async function missingReleaseParts(options) {
3881
+ const { shell, registry, version, force = false } = options;
3882
+ const missing = [];
3883
+ for (const name of RELEASE_SIBLINGS) if (!await versionExists({
3884
+ shell,
3885
+ name,
3886
+ version,
3887
+ registry,
3888
+ force
3889
+ })) missing.push(name);
3890
+ return missing;
3891
+ }
3432
3892
  async function probeRelease(options) {
3433
3893
  const { shell, package: pkg, registry, force = false } = options;
3434
3894
  if (pkg === "") return void 0;
@@ -4206,7 +4666,7 @@ async function retryAfterRegistryFix(deps, manager, pkg, change) {
4206
4666
  function createMarketService(options) {
4207
4667
  const { manifests, manifestsNow, probe, shell, logger, onInstalled, onUninstalled, isReleased, bundles, installed, modules, workbench, registries, ledger, references } = options;
4208
4668
  const command = options.command ?? "opencli";
4209
- const catalog = () => (manifestsNow?.() ?? manifests).filter((manifest) => (manifest.env?.plugins ?? []).length > 0 || (manifest.env?.libs ?? []).length > 0);
4669
+ const catalog = () => manifestsNow?.() ?? manifests;
4210
4670
  function requireEntry(unit) {
4211
4671
  const manifest = catalog().find((item) => item.id === unit);
4212
4672
  if (manifest === void 0) throw new Error(`市场里没有这个模块:${unit}(可安装:${catalog().map((item) => item.id).join(", ") || "(空)"})`);
@@ -4553,6 +5013,15 @@ function createMarketService(options) {
4553
5013
  registry,
4554
5014
  force: true
4555
5015
  });
5016
+ if (shell !== void 0 && registry !== void 0 && latest !== void 0) {
5017
+ const missing = await missingReleaseParts({
5018
+ shell,
5019
+ registry,
5020
+ version: latest,
5021
+ force: true
5022
+ });
5023
+ if (missing.length > 0) throw new Error(`源上已经有 ${read.pkg}@${latest},但这一份发布还没铺齐:${missing.join("、")} 在这一版上还拉不到。装上会缺依赖,稍等再试 —— 市场页那颗「重新检查」会重新探一次。`);
5024
+ }
4556
5025
  const spec = latest === void 0 ? `${read.pkg}@latest` : `${read.pkg}@${latest}`;
4557
5026
  const outcome = await installDshBundle(routeDeps(requireBundles()), spec, { ...registry === void 0 ? {} : { registry } });
4558
5027
  logger?.info?.(`[dsh-workbench] 已更新工作台 ${read.version} → ${latest ?? "最新版"}`);
@@ -4749,8 +5218,8 @@ function registerMarketTools(ctx, market) {
4749
5218
  }
4750
5219
  const tool = {
4751
5220
  name: "workbench_modules",
4752
- description: "看 / 装 / 升 / 卸工作台的面板模块,以及用户自己装的 opencli 插件。install-bundle 按 spec(包名,可带版本;git / tarball / 本地路径也行)把模块包装进 profile —— 装完会连带装它声明的 opencli 插件;其余 action 按模块 id 动它要的外部供给;update-plugin / uninstall-plugin 按包名或插件名动用户装的那份插件;update-workbench 把工作台自己更新到源上的新版(客户端与宿主一般会自己换上;真要重启时结果里会说,重启由用户来,别自己重启)。卸载只动工作台运行时,不影响终端那套;共用依赖只有升级。",
4753
- parameters: {
5221
+ description: "看 / 装 / 升 / 卸工作台的面板模块,以及用户自己装的 opencli 插件。install-bundle 按 spec(包名,可带版本;git / tarball / 本地路径也行)把模块包装进 profile —— 装完会连带装它声明的 opencli 插件;其余 action 按模块 id 动它要的外部供给;update-workbench 把工作台自己更新到源上的新版(客户端与宿主一般会自己换上;真要重启时结果里会说,重启由用户来,别自己重启)。卸载只动工作台运行时,不影响终端那套;共用依赖只有升级。",
5222
+ parameters: withApprovalNote({
4754
5223
  type: "object",
4755
5224
  properties: {
4756
5225
  action: {
@@ -4777,7 +5246,7 @@ function registerMarketTools(ctx, market) {
4777
5246
  },
4778
5247
  required: ["action"],
4779
5248
  additionalProperties: false
4780
- },
5249
+ }, { required: true }),
4781
5250
  output: {
4782
5251
  schema: OUTPUT_SCHEMA$1,
4783
5252
  render: (_args, value) => {
@@ -4988,9 +5457,11 @@ function createPanelsService(options) {
4988
5457
  return readBoard(folder) ?? EMPTY_BOARD;
4989
5458
  }
4990
5459
  function writeBoard(folder, next) {
5460
+ const order = next.order ?? [];
4991
5461
  folder.write(PANELS_FILE, {
4992
5462
  shown: next.shown,
4993
- hidden: next.hidden
5463
+ hidden: next.hidden,
5464
+ ...order.length === 0 ? {} : { order }
4994
5465
  });
4995
5466
  }
4996
5467
  function boardsOf(workspace) {
@@ -5021,6 +5492,9 @@ function createPanelsService(options) {
5021
5492
  if (next.length === 0) machine.remove(RELEASED_FILE);
5022
5493
  else machine.write(RELEASED_FILE, { ids: next });
5023
5494
  }
5495
+ function ownedSet() {
5496
+ return new Set(owned?.() ?? []);
5497
+ }
5024
5498
  function notify() {
5025
5499
  for (const listener of [...listeners]) try {
5026
5500
  listener();
@@ -5031,7 +5505,7 @@ function createPanelsService(options) {
5031
5505
  function withPersisted(folder, next, key) {
5032
5506
  writeBoard(folder, next);
5033
5507
  notify();
5034
- return resolvePanels(manifests(), boardsOf(key));
5508
+ return resolvePanels(manifests(), boardsOf(key), ownedSet());
5035
5509
  }
5036
5510
  function takeBackReleased(id) {
5037
5511
  const current = readReleased();
@@ -5051,16 +5525,19 @@ function createPanelsService(options) {
5051
5525
  function requireSwitchable(id) {
5052
5526
  if (pluginIds().has(id)) return;
5053
5527
  requireKnown(id);
5054
- if (!manifests().some((manifest) => manifest.id === id && manifest.card !== null)) throw new Error(`${id} 没有卡片,不能收起(它是把别的套件放回来的入口)`);
5528
+ if (manifests().find((item) => item.id === id)?.card === null && ownedSet().has(id)) throw new Error(`${id} 没有卡片,不能收起(它是把别的套件放回来的入口)`);
5055
5529
  }
5056
5530
  function requireWorkspace(workspace) {
5057
5531
  const key = workspace?.trim() ?? "";
5058
5532
  if (key === "") throw new Error("这个会话不属于任何工作区:面板按工作区存,先打开或新建一个工作区");
5059
5533
  return key;
5060
5534
  }
5535
+ function visibleOrder(workspace) {
5536
+ return resolvePanels(manifests(), boardsOf(workspace), ownedSet()).filter((entry) => entry.shown && entry.hasCard).map((entry) => entry.id);
5537
+ }
5061
5538
  return {
5062
5539
  list(workspace, target) {
5063
- return resolvePanels(manifests(), target === "machine" ? boardsOf(null) : boardsOf(workspace));
5540
+ return resolvePanels(manifests(), target === "machine" ? boardsOf(null) : boardsOf(workspace), ownedSet());
5064
5541
  },
5065
5542
  listPlugins(workspace, target) {
5066
5543
  return resolvePluginPanels(pluginRefs(), target === "machine" ? boardsOf(null) : boardsOf(workspace));
@@ -5105,7 +5582,7 @@ function createPanelsService(options) {
5105
5582
  requireSwitchable(id);
5106
5583
  const { folder, key } = targetOf(target ?? "workspace", workspace);
5107
5584
  takeBackReleased(id);
5108
- return withPersisted(folder, withShown(boardIn(folder), id), key);
5585
+ return withPersisted(folder, withShown(boardIn(folder), id, key === null ? void 0 : visibleOrder(key)), key);
5109
5586
  },
5110
5587
  hide(id, workspace, target) {
5111
5588
  requireSwitchable(id);
@@ -5114,10 +5591,12 @@ function createPanelsService(options) {
5114
5591
  },
5115
5592
  move(id, index, workspace, target) {
5116
5593
  requireSwitchable(id);
5117
- const { folder, key } = targetOf(target ?? "workspace", workspace);
5594
+ const scope = target ?? "workspace";
5595
+ if (scope === "machine") throw new Error("机器级默认板没有顺序这一说:它只回答\"默认开哪几行\"。顺序是每个工作区自己的 —— 要排顺序就用默认作用域的那一次 move(或面板里拖动)。");
5596
+ const { folder, key } = targetOf(scope, workspace);
5118
5597
  const current = boardIn(folder);
5119
5598
  if (!current.shown.includes(id)) takeBackReleased(id);
5120
- return withPersisted(folder, withMoved(current, id, index), key);
5599
+ return withPersisted(folder, withMoved(current, id, index, visibleOrder(key)), key);
5121
5600
  },
5122
5601
  reset(workspace, target) {
5123
5602
  const { folder, key } = targetOf(target ?? "workspace", workspace);
@@ -5137,7 +5616,7 @@ function createPanelsService(options) {
5137
5616
  if (id === void 0) {
5138
5617
  folder.remove(PANELS_FILE);
5139
5618
  notify();
5140
- return resolvePanels(manifests(), boardsOf(key));
5619
+ return resolvePanels(manifests(), boardsOf(key), ownedSet());
5141
5620
  }
5142
5621
  return withPersisted(folder, withoutPanel(boardIn(folder), id), key);
5143
5622
  }
@@ -5318,13 +5797,13 @@ function registerPanelTools(ctx, panels, values) {
5318
5797
  }
5319
5798
  const tool = {
5320
5799
  name: "workbench_panels",
5321
- description: "看或调右侧面板:启用哪些套件、什么顺序。启用/收起 = 卡片与 agent 工具一起上下线(别的项目不受影响,不删数据)。收起是\"这个工作区明确不要它\",连那些在机器级默认板上开着的套件也照样在这个工作区收起来,之后 show 能放回来。默认只动当前工作区(scope=workspace);scope=machine 动的是\"机器级默认板\" —— 机器级默认板只是还没配过的工作区的默认值,已经自己配过的工作区保持自己的选择,不会被它改。在那份板上 hide = 明确写下\"不要它\",这一笔比名单更管用:名单里还列着它也算不要,没提过它的工作区因此看不到它;show 会把这笔划掉、把它放回去。本工作区那份板只记**改过的那几行**(逐行覆盖):没提到的行跟着机器级默认板走,所以那份板以后改了,没改过的工作区会跟着变。workbench_panels 默认只动当前工作区,动机器级默认板要显式给 scope=machine。",
5322
- parameters: {
5800
+ description: "看或调右侧面板:启用哪些套件、什么顺序。启用/收起 = 卡片与 agent 工具一起上下线(别的项目不受影响,不删数据)。收起是\"这个工作区明确不要它\",连那些在机器级默认板上开着的套件也照样在这个工作区收起来,之后 show 能放回来。顺序只属于工作区:没排过的按各模块自己声明的次序(开着的在前、关着的跟在后面);show 或 move 过就以那次为准。机器级默认板只回答\"默认开哪几行\",没有顺序这一说。scope=machine 动的是\"机器级默认板\" —— 它只是还没配过的工作区的默认值,已经自己配过的工作区保持自己的选择,不会被它改。在那份板上 hide = 明确写下\"不要它\",这一笔比名单更管用:名单里还列着它也算不要,没提过它的工作区因此看不到它;show 会把这笔划掉、把它放回去。本工作区那份板只记**改过的那几行**(逐行覆盖):没提到的行跟着机器级默认板走,所以那份板以后改了,没改过的工作区会跟着变。",
5801
+ parameters: withApprovalNote({
5323
5802
  type: "object",
5324
5803
  properties: {
5325
5804
  action: {
5326
5805
  type: "string",
5327
- description: "show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它(卡片与工具一起下线,默认在机器上也开着的套件同样收起,别的项目不受影响);move=调顺序;reset=清掉这个工作区改过的那些行(改完就全部回到跟着机器级默认板走);follow=把某一行放回跟随(带 id = 只放回那一行;不带 id = 全部放回)。",
5806
+ description: "show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它(卡片与工具一起下线,默认在机器上也开着的套件同样收起,别的项目不受影响);move=调这个工作区的顺序(新开一行时它排在最后);reset=清掉这个工作区改过的那些行(改完就全部回到跟着机器级默认板走);follow=把某一行放回跟随(带 id = 只放回那一行;不带 id = 全部放回)。",
5328
5807
  enum: [
5329
5808
  "list",
5330
5809
  "show",
@@ -5340,17 +5819,17 @@ function registerPanelTools(ctx, panels, values) {
5340
5819
  },
5341
5820
  index: {
5342
5821
  type: "integer",
5343
- description: "move 的位置,0 基(0=最上面)。"
5822
+ description: "move 的位置,0 基(数的是当前开着的那几行)。"
5344
5823
  },
5345
5824
  scope: {
5346
5825
  type: "string",
5347
- description: "workspace(默认)=只动当前工作区;machine=动机器级默认板,那是还没配过的工作区的默认值 —— 已经自己配过的工作区保持自己的选择,不受它影响。",
5826
+ description: "workspace(默认)=只动当前工作区;machine=动机器级默认板,那是还没配过的工作区的默认值 —— 已经自己配过的工作区保持自己的选择,不受它影响。机器级板没有顺序这一说(move 会报错)。",
5348
5827
  enum: ["workspace", "machine"]
5349
5828
  }
5350
5829
  },
5351
5830
  required: ["action"],
5352
5831
  additionalProperties: false
5353
- },
5832
+ }, { required: true }),
5354
5833
  output: {
5355
5834
  schema: OUTPUT_SCHEMA,
5356
5835
  render: (_args, value) => {
@@ -5430,7 +5909,7 @@ function registerPanelTools(ctx, panels, values) {
5430
5909
  var OWNED = [{
5431
5910
  unit: panelsUnit.id,
5432
5911
  manifest: panelsUnit,
5433
- mount({ ctx, logger, manifests, ownedUnits, opencliCommand, probe, store, values }) {
5912
+ mount({ ctx, logger, manifests, ownedUnits, installable, opencliCommand, probe, store, values }) {
5434
5913
  const ledger = createPluginLedger({
5435
5914
  store,
5436
5915
  home: isolatedHome()
@@ -5445,9 +5924,15 @@ var OWNED = [{
5445
5924
  ctx.provide(PANELS_SERVICE, panels);
5446
5925
  const profileDir = profileDirOf(ctx);
5447
5926
  const shell = ctx.get("shell");
5927
+ const moduleManifests = () => {
5928
+ const list = manifests();
5929
+ if (installable === void 0) return list.filter(hasEnvSupply);
5930
+ const ids = new Set(installable());
5931
+ return list.filter((manifest) => ids.has(manifest.id));
5932
+ };
5448
5933
  const market = createMarketService({
5449
- manifests: manifests(),
5450
- manifestsNow: manifests,
5934
+ manifests: moduleManifests(),
5935
+ manifestsNow: moduleManifests,
5451
5936
  ...probe === void 0 ? {} : { probe },
5452
5937
  shell,
5453
5938
  ...opencliCommand === void 0 ? {} : { command: opencliCommand },
@@ -5834,17 +6319,43 @@ function bundleRegistriesHandler(ctx) {
5834
6319
  };
5835
6320
  }
5836
6321
  var refusalLog = /* @__PURE__ */ new Map();
6322
+ var holdLog = /* @__PURE__ */ new Map();
6323
+ async function latestIfReleaseReady(input) {
6324
+ const missing = await missingReleaseParts({
6325
+ shell: input.shell,
6326
+ registry: input.registry,
6327
+ version: input.version,
6328
+ force: input.force
6329
+ });
6330
+ if (missing.length === 0) {
6331
+ holdLog.delete(input.pkg);
6332
+ return input.version;
6333
+ }
6334
+ const line = `${input.version}|${missing.join("、")}`;
6335
+ if (holdLog.get(input.pkg) !== line) {
6336
+ holdLog.set(input.pkg, line);
6337
+ input.logger?.warn?.(`[dsh-workbench] 源上已经有 ${input.pkg}@${input.version},但这一份发布还没铺齐(${missing.join("、")} 在这个版本上还拉不到)—— 先不提示更新;等铺齐或点「重新检查」。`);
6338
+ }
6339
+ }
5837
6340
  async function workbenchVersion(input) {
5838
6341
  const read = readProfilePackage(input.dir, PLUGIN_ID);
5839
6342
  if (read === void 0) return void 0;
5840
6343
  const registry = read.registry ?? input.fallback;
5841
- const queried = read.local === true || input.shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
5842
- shell: input.shell,
6344
+ const shell = input.shell;
6345
+ const queried = read.local === true || shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
6346
+ shell,
5843
6347
  name: read.pkg,
5844
6348
  registry,
5845
6349
  force: input.force
5846
6350
  });
5847
- const latest = queried !== void 0 && isNewerVersion(queried, read.version) ? queried : void 0;
6351
+ const latest = queried === void 0 || shell === void 0 || registry === void 0 || !isNewerVersion(queried, read.version) ? void 0 : await latestIfReleaseReady({
6352
+ pkg: read.pkg,
6353
+ version: queried,
6354
+ registry,
6355
+ shell,
6356
+ force: input.force,
6357
+ logger: input.logger
6358
+ });
5848
6359
  return {
5849
6360
  pkg: read.pkg,
5850
6361
  installed: read.version,
@@ -5896,7 +6407,8 @@ function moduleVersionsHandler(ctx, shell) {
5896
6407
  dir,
5897
6408
  shell,
5898
6409
  fallback,
5899
- force
6410
+ force,
6411
+ logger: ctx.logger
5900
6412
  });
5901
6413
  return {
5902
6414
  ok: true,
@@ -6687,6 +7199,28 @@ function withRunsRoute(result, unit) {
6687
7199
  }
6688
7200
  };
6689
7201
  }
7202
+ function notedRegistry(registry, writesOf) {
7203
+ return { register(definition) {
7204
+ if (!writesOf(definition.name)) return registry.register(definition);
7205
+ const noted = withApprovalNote(definition.parameters, { required: true });
7206
+ return registry.register(noted === definition.parameters ? definition : {
7207
+ ...definition,
7208
+ parameters: noted
7209
+ });
7210
+ } };
7211
+ }
7212
+ function scopedMountCtx(ctx, registry, writesOf) {
7213
+ const tools = notedRegistry(registry, writesOf);
7214
+ const get = (name) => {
7215
+ const value = ctx.get(name);
7216
+ return name === "tools" && value !== void 0 ? tools : value;
7217
+ };
7218
+ return new Proxy(ctx, { get(target, key) {
7219
+ if (key === "get") return get;
7220
+ const value = Reflect.get(target, key, target);
7221
+ return typeof value === "function" ? value.bind(target) : value;
7222
+ } });
7223
+ }
6690
7224
  function failureReasonOf(error) {
6691
7225
  if (error instanceof Error) return error.message;
6692
7226
  return typeof error === "string" ? error : String(error);
@@ -6830,6 +7364,9 @@ function mountUnits(units, deps) {
6830
7364
  continue;
6831
7365
  }
6832
7366
  const unit = host.manifest.id;
7367
+ const tools = deps.ctx.get("tools");
7368
+ const writes = toolUnits(deps.manifests());
7369
+ const mountCtx = tools === void 0 ? deps.ctx : scopedMountCtx(deps.ctx, tools, (name) => writes.get(name)?.writes === true);
6833
7370
  const longTasks = deps.longTasks === void 0 ? void 0 : createLongTaskHost({
6834
7371
  scope: deps.longTasks.scope,
6835
7372
  unit,
@@ -6839,10 +7376,11 @@ function mountUnits(units, deps) {
6839
7376
  try {
6840
7377
  result = host.face.mount({
6841
7378
  manifest: host.manifest,
6842
- ctx: deps.ctx,
7379
+ ctx: mountCtx,
6843
7380
  logger: deps.logger,
6844
7381
  manifests: deps.manifests,
6845
7382
  ownedUnits,
7383
+ ...deps.installable === void 0 ? {} : { installable: deps.installable },
6846
7384
  ...deps.opencliCommand === void 0 ? {} : { opencliCommand: deps.opencliCommand },
6847
7385
  ...deps.probe === void 0 ? {} : { probe: deps.probe },
6848
7386
  ...deps.values === void 0 ? {} : { values: deps.values },
@@ -6907,6 +7445,9 @@ function mountUnits(units, deps) {
6907
7445
  }
6908
7446
  //#endregion
6909
7447
  //#region src/shared/api/host/unit-registry.ts
7448
+ function installableUnitsOf(registry) {
7449
+ return registry.all().filter((host) => host.source.kind === "package").map((host) => host.unit);
7450
+ }
6910
7451
  function sourceLabel(source) {
6911
7452
  if (source.kind === "owned") return "外壳自己";
6912
7453
  if (source.kind === "plugin") return "按已装 opencli 插件合成";
@@ -6916,6 +7457,7 @@ function createUnitRegistry(options) {
6916
7457
  const refusals = [];
6917
7458
  const hosts = [];
6918
7459
  const byUnit = /* @__PURE__ */ new Map();
7460
+ const failures = /* @__PURE__ */ new Map();
6919
7461
  const listeners = /* @__PURE__ */ new Set();
6920
7462
  function notify() {
6921
7463
  for (const listener of listeners) listener();
@@ -6985,7 +7527,14 @@ function createUnitRegistry(options) {
6985
7527
  subscribe(listener) {
6986
7528
  listeners.add(listener);
6987
7529
  return () => listeners.delete(listener);
6988
- }
7530
+ },
7531
+ fail(unit, reason) {
7532
+ failures.set(unit, reason);
7533
+ },
7534
+ recovered(unit) {
7535
+ failures.delete(unit);
7536
+ },
7537
+ failureOf: (unit) => failures.get(unit)
6989
7538
  };
6990
7539
  }
6991
7540
  //#endregion
@@ -7270,12 +7819,13 @@ function pluginToolParameters(command) {
7270
7819
  const properties = {};
7271
7820
  for (const arg of command.args) properties[arg.name] = propertyOf(arg);
7272
7821
  const required = command.args.filter((arg) => arg.required).map((arg) => arg.name);
7273
- return {
7822
+ const schema = {
7274
7823
  type: "object",
7275
7824
  properties,
7276
7825
  ...required.length === 0 ? {} : { required },
7277
7826
  additionalProperties: false
7278
7827
  };
7828
+ return pluginToolWrites(command) ? withApprovalNote(schema, { required: true }) : schema;
7279
7829
  }
7280
7830
  function pluginToolDescription(command) {
7281
7831
  const described = command.description.trim();
@@ -7675,6 +8225,7 @@ function seedWorkbenchRuntime(ctx) {
7675
8225
  function apply(ctx) {
7676
8226
  const facts = createFactLog();
7677
8227
  const values = createValueHandoff();
8228
+ const panelState = createPanelStateStore();
7678
8229
  seedWorkbenchRuntime(ctx);
7679
8230
  const opencli = opencliCommandOf(ctx);
7680
8231
  const routes = {
@@ -7687,23 +8238,17 @@ function apply(ctx) {
7687
8238
  };
7688
8239
  const renderers = /* @__PURE__ */ new Map();
7689
8240
  const shell = ctx.get("shell");
7690
- let roster;
7691
8241
  const profileDir = profileDirOf(ctx);
7692
8242
  const units = createUnitRegistry({
7693
- declarationOf: (unit) => {
7694
- if (roster === void 0) roster = new Map(profileModules(profileDir).map((row) => [row.unit, row.pkg]));
7695
- const pkg = roster.get(unit);
7696
- return pkg === void 0 ? void 0 : {
7697
- pkg,
7698
- declaration: moduleDeclarationOf(profileDir, pkg)
7699
- };
7700
- },
8243
+ declarationOf: createDeclarationLookup(profileDir),
7701
8244
  logger: ctx.logger
7702
8245
  });
7703
8246
  ctx.provide(UNITS_SERVICE, units);
8247
+ ctx.provide(PANEL_STATE_SERVICE, panelState);
7704
8248
  routes[UNIT_MANIFESTS_ROUTE] = () => ({
7705
8249
  ok: true,
7706
- manifests: units.manifests()
8250
+ manifests: units.manifests(),
8251
+ installable: installableUnitsOf(units)
7707
8252
  });
7708
8253
  assembleHostUnits(units);
7709
8254
  ctx.inject(["workspaceRegistry"], (scoped) => {
@@ -7777,6 +8322,7 @@ function apply(ctx) {
7777
8322
  if (item === void 0) return;
7778
8323
  mounted.delete(id);
7779
8324
  mountFailures.delete(id);
8325
+ units.recovered(id);
7780
8326
  for (const dispose of item.toolDisposers) dispose?.();
7781
8327
  item.gateOff?.();
7782
8328
  item.plan.disposeLongTasks();
@@ -7795,6 +8341,7 @@ function apply(ctx) {
7795
8341
  logger: ctx.logger,
7796
8342
  manifests: liveManifests,
7797
8343
  ownedUnits: () => units.owned(),
8344
+ installable: () => installableUnitsOf(units),
7798
8345
  store,
7799
8346
  opencliCommand: opencli.command,
7800
8347
  probe: (unit, options) => probeOne(unit, options?.force ?? false),
@@ -7809,6 +8356,7 @@ function apply(ctx) {
7809
8356
  reason: failureReasonOf(error),
7810
8357
  host
7811
8358
  });
8359
+ units.fail(id, failureReasonOf(error));
7812
8360
  ctx.logger?.warn?.(`[dsh-workbench] ${id} 套件起不来(装配期):${failureReasonOf(error)}`);
7813
8361
  return;
7814
8362
  }
@@ -7841,6 +8389,7 @@ function apply(ctx) {
7841
8389
  gateOff: one.gate?.subscribe(() => toolScope.reapply())
7842
8390
  });
7843
8391
  mountFailures.delete(id);
8392
+ units.recovered(id);
7844
8393
  } catch (error) {
7845
8394
  for (const route of added) delete routes[route];
7846
8395
  for (const kind of one.contextRenderers.keys()) renderers.delete(kind);
@@ -7852,6 +8401,7 @@ function apply(ctx) {
7852
8401
  reason: failureReasonOf(error),
7853
8402
  host
7854
8403
  });
8404
+ units.fail(id, failureReasonOf(error));
7855
8405
  ctx.logger?.warn?.(`[dsh-workbench] ${id} 套件起不来(注册期):${failureReasonOf(error)}`);
7856
8406
  }
7857
8407
  }
@@ -7876,7 +8426,10 @@ function apply(ctx) {
7876
8426
  function sync() {
7877
8427
  const present = new Set(units.all().map((one) => one.unit));
7878
8428
  for (const id of [...mounted.keys()]) if (!present.has(id)) unmountUnit(id);
7879
- for (const id of [...mountFailures.keys()]) if (!present.has(id)) mountFailures.delete(id);
8429
+ for (const id of [...mountFailures.keys()]) if (!present.has(id)) {
8430
+ mountFailures.delete(id);
8431
+ units.recovered(id);
8432
+ }
7880
8433
  for (const host of units.all()) {
7881
8434
  if (mounted.has(host.unit)) continue;
7882
8435
  const already = mountFailures.get(host.unit);
@@ -8023,6 +8576,7 @@ function apply(ctx) {
8023
8576
  return () => off?.();
8024
8577
  }, `${PLUGIN_ID}: 对话引用注入`);
8025
8578
  ctx.effect(() => registerFactStream(ctx, facts), `${PLUGIN_ID}: 变更流`);
8579
+ ctx.effect(() => registerPanelState(ctx, panelState), `${PLUGIN_ID}: 面板运行态上报(浏览器半侧报自己的取数与渲染收尾)`);
8026
8580
  ctx.effect(() => registerApi(ctx, routes), `${PLUGIN_ID}: HTTP 投影`);
8027
8581
  }
8028
8582
  //#endregion