@huaqiu/dsh-tool-kicad-schematic-gen 0.6.4 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -331,10 +331,13 @@
331
331
  * SSE agents were removed with the package). The chip identity, the `@`
332
332
  * trigger, the clipboard projection and the menu row are preserved verbatim so
333
333
  * existing drafts and the composer UX keep working. The one deliberate change:
334
- * the chip's model text now pins the bundled `kicad-schematic-design` skill
335
- * instead of the deleted online tools (`generate_schematic_from_description` /
336
- * `generate_system_module_graph`), because the design skill is the new local
337
- * entry point the agent must follow.
334
+ * the chip's model text is now the bare clipboard token — the semantics that
335
+ * used to ride in the payload (pin the `kicad-schematic-design` skill instead
336
+ * of the deleted online tools `generate_schematic_from_description` /
337
+ * `generate_system_module_graph`, forbid hand-written KiCad files, defer to
338
+ * the user's words) moved to the node half's `huaqiu:schematic-gen`
339
+ * system-prompt section; see {@link SCHEMATIC_GEN_SERIALIZED} and
340
+ * `src/prompt.ts`.
338
341
  *
339
342
  * One module owns the identity of the chip, its menu row, and the model text
340
343
  * the chip expands to at submit time. Registering the source is MANDATORY, not
@@ -344,6 +347,12 @@
344
347
  * source"). A chip inserted without a registered source would therefore block
345
348
  * the user's message instead of degrading quietly.
346
349
  *
350
+ * The registration is also ORDER-sensitive: the trigger service is provided by
351
+ * a plugin whose own dependency chain is longer than ours, so reading it once
352
+ * at `apply()` time can find nothing. The entry therefore declares
353
+ * `inputTriggers` in its `inject` (cordis parks the plugin until the provider
354
+ * exists) and retries briefly if it still arrives late.
355
+ *
347
356
  * Registering it also buys the typed path for free: the user can type `@` and
348
357
  * pick *Generate schematic* (生成原理图 under a Chinese UI) from a group beside
349
358
  * *Files & folders* / *Sessions*.
@@ -407,20 +416,27 @@
407
416
  }
408
417
  /**
409
418
  * Model form of the chip: what the agent actually reads at the chip's position
410
- * in the user message.
419
+ * in the user message — the clipboard token itself, nothing more.
420
+ *
421
+ * This is the idiomatic mention pattern (DSH's own draft tests serialize a
422
+ * reference to its ref), and it is deliberate on two counts:
411
423
  *
412
- * Deliberately an *intent*, never a fabricated design description — the user's
413
- * own words stay the only source of requirements, and the plugin being loaded
414
- * is never by itself a reason to generate.
424
+ * 1. **Whatever `serialize` returns lands in the USER message** — the submit
425
+ * plane splices it over the chip's clipboard range — so a long payload is
426
+ * noise the user reads back in their own transcript on every send.
427
+ * 2. **The token has no semantics of its own**, so the semantics cannot live
428
+ * here; they live in the node half's `huaqiu:schematic-gen` system-prompt
429
+ * section (see `src/prompt.ts`), which defines the token, pins the
430
+ * `kicad-schematic-design` skill, and carries the guardrails.
415
431
  *
416
- * The retired plugin pinned the online tools here
417
- * (`generate_schematic_from_description` / `generate_system_module_graph`).
418
- * The migrated chip pins the bundled `kicad-schematic-design` skill instead:
419
- * the skill owns the module search / part import / editable-project flow, and
420
- * starts from the hardware design brief when no plan exists yet. Hand-written
421
- * KiCad files stay forbidden in both worlds.
432
+ * History: the chip first shipped a 6-line `<schematic-gen intent=…>` block,
433
+ * then a 3-line trimmed form, and now delegates entirely to the section. The
434
+ * retired plugin pinned deleted online tools here
435
+ * (`generate_schematic_from_description` / `generate_system_module_graph`);
436
+ * nothing in this constant may ever name a tool again — the skill pin is the
437
+ * section's job.
422
438
  */
423
- const SCHEMATIC_GEN_SERIALIZED = "<schematic-gen intent=\"kicad-schematic-design\">\nThe user explicitly chose Schematic Gen for this request. Produce the design with the `kicad-schematic-design` skill; when the hardware design brief is missing or incomplete, the skill starts by preparing it before the schematic work.\nNever hand-write .kicad_sch files or KiCad project files yourself — always go through the skill and its tools.\nUse the user's own words as the design description. If they have not described the circuit yet, ask for the requirements instead of assuming them.\n</schematic-gen>";
439
+ const SCHEMATIC_GEN_SERIALIZED = SCHEMATIC_GEN_CLIPBOARD;
424
440
  /**
425
441
  * The `ReferenceInsert` this source produces on pick (and on button click).
426
442
  *
@@ -476,23 +492,125 @@
476
492
  };
477
493
  }
478
494
  /**
479
- * Register the source and return its disposer.
495
+ * One registration attempt, reported instead of swallowed.
496
+ *
497
+ * {@link registerSchematicGenSource} is the disposer-only view for callers that
498
+ * do not care; this is the same attempt with the outcome visible, which is what
499
+ * a caller needs to decide whether a chip may be inserted at all — an
500
+ * unregistered source makes every send fail (see the module header).
480
501
  *
481
- * Never throws: a missing service, a duplicate registration (a second live
482
- * copy of this plugin) or a throwing registry must not take the whole client
483
- * half down. Without a registered source the sidebar button degrades to
484
- * inserting nothing rather than producing a chip that would block submission.
485
502
  * @param registry - `ctx.get('inputTriggers')`, or undefined.
486
- * @returns disposer (a no-op when registration did not happen).
503
+ * @returns whether the registration landed, plus its disposer.
487
504
  */
488
- function registerSchematicGenSource(registry) {
489
- if (registry === void 0 || registry === null || typeof registry.registerSource !== "function") return () => {};
505
+ function registerSchematicGenSourceTracked(registry) {
506
+ if (registry === void 0 || registry === null || typeof registry.registerSource !== "function") return {
507
+ registered: false,
508
+ dispose: () => {}
509
+ };
490
510
  try {
491
511
  const dispose = registry.registerSource(createSchematicGenSource());
492
- return typeof dispose === "function" ? dispose : () => {};
512
+ return {
513
+ registered: true,
514
+ dispose: typeof dispose === "function" ? dispose : () => {}
515
+ };
493
516
  } catch {
494
- return () => {};
517
+ return {
518
+ registered: true,
519
+ dispose: () => {}
520
+ };
521
+ }
522
+ }
523
+ /**
524
+ * Host timer, or undefined where there is none.
525
+ *
526
+ * The retry is browser-only machinery, and the built bundle is also evaluated
527
+ * in bare hosts (the vm-based card test, a non-DOM worker) where `setInterval`
528
+ * does not exist. Those must degrade to "not registered yet" — a throw here
529
+ * would take the whole client half down, which is the failure mode this module
530
+ * exists to prevent.
531
+ */
532
+ function hostTimer() {
533
+ if (typeof setInterval !== "function" || typeof clearInterval !== "function") return void 0;
534
+ return {
535
+ set: (fn, ms) => setInterval(fn, ms),
536
+ clear: (handle) => clearInterval(handle)
537
+ };
538
+ }
539
+ /**
540
+ * Register the source, polling while the trigger service has not activated yet.
541
+ *
542
+ * Why this exists: activation is service-availability driven, and
543
+ * `inputTriggers` is provided by a plugin (`dsh-client-ui-input-trigger`) whose
544
+ * own dependency chain is far longer than ours. A plugin that reads the service
545
+ * exactly once at `apply()` time and finds it absent registers NOTHING —
546
+ * silently. The chip then lands in the composer (insertion only needs
547
+ * `conversation`) and the send dies on
548
+ * `slash: no serializer for reference source "schematic-gen"`, because the
549
+ * controller routes every chip to its owner's codec. Declaring `inputTriggers`
550
+ * in the plugin's `inject` is the primary fix (cordis then parks this plugin
551
+ * until the provider exists); this poll is the belt-and-braces path for a
552
+ * provider that arrives later than that declaration could promise.
553
+ *
554
+ * @param getRegistry - returns the current `ctx.get('inputTriggers')` value.
555
+ * @param hooks - scheduler/trace seams.
556
+ * @returns a live binding whose `registered()` the caller may consult.
557
+ */
558
+ function registerSchematicGenSourceWhenReady(getRegistry, hooks = {}) {
559
+ const intervalMs = hooks.intervalMs ?? 250;
560
+ const maxAttempts = hooks.maxAttempts ?? 80;
561
+ const traceFn = hooks.trace ?? trace$1;
562
+ const timer = hooks.setTimer !== void 0 && hooks.clearTimer !== void 0 ? {
563
+ set: hooks.setTimer,
564
+ clear: hooks.clearTimer
565
+ } : hostTimer();
566
+ let current = registerSchematicGenSourceTracked(getRegistry());
567
+ let handle;
568
+ let attempts = 0;
569
+ let stopped = false;
570
+ const stopTimer = () => {
571
+ if (handle === void 0 || timer === void 0) return;
572
+ timer.clear(handle);
573
+ handle = void 0;
574
+ };
575
+ const dispose = () => {
576
+ stopped = true;
577
+ stopTimer();
578
+ current.dispose();
579
+ };
580
+ if (current.registered) return {
581
+ registered: () => true,
582
+ dispose
583
+ };
584
+ if (timer === void 0) {
585
+ traceFn("no timer host — source registration not retried");
586
+ return {
587
+ registered: () => false,
588
+ dispose
589
+ };
495
590
  }
591
+ traceFn("waiting for inputTriggers", {
592
+ intervalMs,
593
+ maxAttempts
594
+ });
595
+ handle = timer.set(() => {
596
+ if (stopped) return;
597
+ attempts += 1;
598
+ const next = registerSchematicGenSourceTracked(getRegistry());
599
+ if (next.registered) {
600
+ current = next;
601
+ stopTimer();
602
+ traceFn("registered (late)", { attempts });
603
+ return;
604
+ }
605
+ if (attempts >= maxAttempts) {
606
+ stopTimer();
607
+ traceFn("source registration gave up", { attempts });
608
+ }
609
+ }, intervalMs);
610
+ return {
611
+ registered: () => current.registered,
612
+ dispose
613
+ };
496
614
  }
497
615
  //#endregion
498
616
  //#region src/client/sidebar-entry.ts
@@ -1126,24 +1244,33 @@
1126
1244
  }, busy ? "处理中…" : "放置到编辑器") : null, h("span", { style: { fontSize: 12 } }, `${result.fileCount} 个工程和库文件`)), result.status !== "ok" ? h("p", null, "存在未渲染页面或预览警告,请结合工具结果检查。") : null, message ? h("p", { role: "alert" }, message) : null);
1127
1245
  }
1128
1246
  return {
1129
- inject: ["slots"],
1247
+ inject: ["slots", "inputTriggers"],
1130
1248
  apply: (ctx) => {
1131
1249
  getHost = () => ctx.get?.("hqEdge");
1132
1250
  const stopVisibility = keepDeliveryVisible();
1133
- const triggers = ctx.get?.("inputTriggers");
1251
+ const source = registerSchematicGenSourceWhenReady(() => ctx.get?.("inputTriggers"));
1134
1252
  try {
1135
1253
  console.debug("[schematic-gen:client] apply", {
1136
- inputTriggers: triggers !== void 0 && triggers !== null,
1254
+ inputTriggers: source.registered(),
1137
1255
  slots: typeof ctx.slots?.inject === "function"
1138
1256
  });
1139
1257
  } catch {}
1140
- const stopRefs = registerSchematicGenSource(triggers);
1141
1258
  const chipDeps = {
1142
1259
  getConversation: () => ctx.get?.("conversation"),
1143
1260
  getSessions: () => ctx.get?.("sessions")
1144
1261
  };
1145
1262
  const stopEntry = mountSchematicGenSidebarEntry({
1146
1263
  onToggle: () => {
1264
+ if (!source.registered()) {
1265
+ try {
1266
+ console.debug("[schematic-gen:chip] click", {
1267
+ ok: false,
1268
+ reason: "source-unregistered"
1269
+ });
1270
+ } catch {}
1271
+ notifyComposer(ctx, "source-unregistered");
1272
+ return;
1273
+ }
1147
1274
  const result = insertSchematicChip(chipDeps);
1148
1275
  try {
1149
1276
  console.debug("[schematic-gen:chip] click", result);
@@ -1162,7 +1289,7 @@
1162
1289
  }, Card));
1163
1290
  ctx.effect?.(() => () => {
1164
1291
  stopVisibility();
1165
- stopRefs();
1292
+ source.dispose();
1166
1293
  stopEntry();
1167
1294
  if (typeof dispose === "function") dispose();
1168
1295
  });
@@ -1187,6 +1314,7 @@
1187
1314
  case "no-session": return "Open a conversation first, then click Schematic Gen again.";
1188
1315
  case "busy": return "The composer is busy — try again once the current message settles.";
1189
1316
  case "conflict": return "The composer changed while inserting — click Schematic Gen again.";
1317
+ case "source-unregistered": return "Schematic Gen is still starting up — click again in a moment.";
1190
1318
  default: return "Schematic Gen is unavailable in this session.";
1191
1319
  }
1192
1320
  }
package/lib/index.d.ts CHANGED
@@ -415,7 +415,7 @@ declare function resolveConfig(input?: ConfigInput): Config;
415
415
  //#endregion
416
416
  //#region src/index.d.ts
417
417
  declare const name = "@huaqiu/dsh-tool-kicad-schematic-gen";
418
- declare const inject: readonly ["tools", "fs", "sandboxPolicy", "skills", "webServer"];
418
+ declare const inject: readonly ["tools", "fs", "sandboxPolicy", "skills", "webServer", "systemPrompt"];
419
419
  declare function apply(ctx: Context, input?: ConfigInput): void;
420
420
  //#endregion
421
421
  export { Config, apply, inject, name, resolveConfig };
package/lib/index.js CHANGED
@@ -7392,6 +7392,95 @@ function registerModuleAnalysis(ctx, config, analyze = createModuleAnalyzer(ctx,
7392
7392
  return analyze;
7393
7393
  }
7394
7394
  //#endregion
7395
+ //#region src/prompt.ts
7396
+ /**
7397
+ * The `huaqiu:schematic-gen` system-prompt section.
7398
+ *
7399
+ * Split out from `src/index.ts` so the text and the registration are
7400
+ * unit-testable without a cordis context, tools, or a skill registry — the
7401
+ * same split `@huaqiu/dsh-eda-selection` uses for its `eda:selection` section.
7402
+ *
7403
+ * ── Why a section at all ────────────────────────────────────────────────────
7404
+ * The composer chip's model form is the bare `@schematic-gen` token (see
7405
+ * `client/schematic-ref.ts`): the token is a UI affordance with no semantics
7406
+ * of its own, and whatever `codec.serialize` returns lands in the USER
7407
+ * message — repeating it on every send is noise the user reads back in their
7408
+ * own transcript. The section carries the semantics once, in the system
7409
+ * prompt, where policy text belongs.
7410
+ *
7411
+ * ── Why static, not a per-session provider ──────────────────────────────────
7412
+ * "Once the user chose Schematic Gen, the rest of the conversation continues
7413
+ * that work" is already true without any session state: the token sits in the
7414
+ * conversation history for every later turn, and the section is re-read on
7415
+ * every assembly. A dynamic provider would need a session-state source — the
7416
+ * only candidates are message-history I/O or a client→node signal, and prompt
7417
+ * assembly is on the agent's hot path (`dsh-eda-selection` documents the same
7418
+ * no-I/O rule). So the section is static text gated on the token, not a
7419
+ * per-session flag.
7420
+ *
7421
+ * @module
7422
+ */
7423
+ /**
7424
+ * Unique prompt-section name.
7425
+ *
7426
+ * Reuses the retired `huaqiu:schematic-gen` name: `dsh-eda-selection` records
7427
+ * that the old section "has moved to the bundled kicad-schematic-design
7428
+ * skill", and this is that section coming back for the chip era — same slot,
7429
+ * same adjacency, no other registration to collide with.
7430
+ */
7431
+ const HUAQIU_SCHEMATIC_GEN_SECTION = "huaqiu:schematic-gen";
7432
+ /** The chip token the section is gated on — the chip's clipboard projection. */
7433
+ const SCHEMATIC_GEN_TOKEN = "@schematic-gen";
7434
+ /**
7435
+ * The section text: what the `@schematic-gen` token MEANS.
7436
+ *
7437
+ * Static on purpose (see the module header). Four jobs, each mapping to a
7438
+ * failure mode observed with the earlier inline-payload chip:
7439
+ * 1. define the token as an explicit user choice — a bare token alone is an
7440
+ * opaque string the model has no trained semantics for;
7441
+ * 2. pin the `kicad-schematic-design` skill — the retired online tools are
7442
+ * gone, so an unpinned agent may reach for a dead entry point;
7443
+ * 3. forbid hand-written `.kicad_sch` files — asked for "an ESP32 mini", an
7444
+ * LLM will otherwise author KiCad files itself, which KiCad cannot open;
7445
+ * 4. defer requirements to the user's own words — a gesture is not a spec.
7446
+ *
7447
+ * It never claims the token carries content: it says what the token means,
7448
+ * nothing about what the user asked for.
7449
+ */
7450
+ function schematicGenSectionText() {
7451
+ return [
7452
+ `Schematic Gen: the ${SCHEMATIC_GEN_TOKEN} token in a user message is the Schematic Gen chip — the user explicitly chose it for that request. Produce the design with the \`kicad-schematic-design\` skill, which starts from the hardware design brief when none exists yet.`,
7453
+ "Never hand-write .kicad_sch files or KiCad project files yourself; go through the skill and its tools.",
7454
+ "Use the user's own words as the design description — if they have not described the circuit yet, ask for the requirements instead of assuming them.",
7455
+ "Follow-up messages in the same conversation continue that work without repeating the token."
7456
+ ].join("\n");
7457
+ }
7458
+ /**
7459
+ * Register the static Schematic Gen section.
7460
+ *
7461
+ * Never throws: a DSH deployment without the prompt service, or one where a
7462
+ * duplicate registration races (hot reload), must still load the tools and
7463
+ * the skill — the section is an enhancement, not a dependency of the rest of
7464
+ * the plugin. A duplicate is the expected hot-reload case, so it is swallowed
7465
+ * rather than surfaced; the first registration keeps serving.
7466
+ *
7467
+ * @param systemPrompt - `ctx.systemPrompt`, or undefined.
7468
+ * @returns the section's disposer, or `undefined` when nothing was registered.
7469
+ */
7470
+ function registerSchematicGenSection(systemPrompt) {
7471
+ if (systemPrompt === void 0 || systemPrompt === null || typeof systemPrompt.section !== "function") return;
7472
+ try {
7473
+ const dispose = systemPrompt.section({
7474
+ name: HUAQIU_SCHEMATIC_GEN_SECTION,
7475
+ order: 590,
7476
+ text: schematicGenSectionText()
7477
+ });
7478
+ return typeof dispose === "function" ? dispose : void 0;
7479
+ } catch {
7480
+ return;
7481
+ }
7482
+ }
7483
+ //#endregion
7395
7484
  //#region src/index.ts
7396
7485
  const name = "@huaqiu/dsh-tool-kicad-schematic-gen";
7397
7486
  const inject = [
@@ -7399,11 +7488,12 @@ const inject = [
7399
7488
  "fs",
7400
7489
  "sandboxPolicy",
7401
7490
  "skills",
7402
- "webServer"
7491
+ "webServer",
7492
+ "systemPrompt"
7403
7493
  ];
7404
7494
  /** Shared component name for the unified DSH-plugin log (`~/.hq-edge/logs/dsh-plugins`). */
7405
7495
  const log = getLogger("dsh-schematic-gen");
7406
- log.info("schematic-gen: module loaded (waiting for the tools, fs, sandboxPolicy, skills and webServer services)");
7496
+ log.info("schematic-gen: module loaded (waiting for the tools, fs, sandboxPolicy, skills, webServer and systemPrompt services)");
7407
7497
  function apply(ctx, input = {}) {
7408
7498
  const config = resolveConfig(input);
7409
7499
  log.info("applying schematic-gen node half", {
@@ -7416,6 +7506,9 @@ function apply(ctx, input = {}) {
7416
7506
  registerModules(ctx, config.modules, analyze);
7417
7507
  apply$2(ctx, config.schematic);
7418
7508
  apply$1(ctx, config.schematic.kicadCliPath);
7509
+ const disposeSection = registerSchematicGenSection(ctx.systemPrompt);
7510
+ if (disposeSection === void 0) log.warn("systemPrompt service unavailable — the huaqiu:schematic-gen section was not registered");
7511
+ else ctx.effect(() => disposeSection);
7419
7512
  } catch (error) {
7420
7513
  log.error("schematic-gen node half failed", { error: String(error?.message ?? error) });
7421
7514
  throw error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huaqiu/dsh-tool-kicad-schematic-gen",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "KiCad 原理图设计:电路模块搜索、器件模型与手册、符号导入、编辑预览、脚本构建及设计技能",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -32,8 +32,8 @@
32
32
  "pdfjs-dist": "6.3.289",
33
33
  "sharp": "0.35.4",
34
34
  "undici": "^8.10.0",
35
- "@huaqiu/dsh-plugin-log": "0.6.4",
36
- "@huaqiu/dsh-sidebar-entry": "0.6.4"
35
+ "@huaqiu/dsh-plugin-log": "0.6.6",
36
+ "@huaqiu/dsh-sidebar-entry": "0.6.6"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/cordis": "^4.0.2",
@@ -46,7 +46,8 @@
46
46
  "client": {
47
47
  "platform": "web",
48
48
  "inject": [
49
- "@deepseek-ai/dsh-client-runtime"
49
+ "@deepseek-ai/dsh-client-runtime",
50
+ "inputTriggers"
50
51
  ]
51
52
  }
52
53
  },
@@ -3,7 +3,7 @@ import type * as ReactTypes from 'react'
3
3
  import {parseCard} from './overview-result.ts'
4
4
  import {placementHost,placeOverview,type PlacementHost} from './place.ts'
5
5
  import {supersededOverviews} from './latest-overview.ts'
6
- import {registerSchematicGenSource} from './schematic-ref.ts'
6
+ import {registerSchematicGenSourceWhenReady} from './schematic-ref.ts'
7
7
  import {mountSchematicGenSidebarEntry} from './sidebar-entry.ts'
8
8
  import {insertSchematicChip,currentSessionId,resolveSessionScope,type ChipInserterDeps,type ConversationLike,type SessionsLike} from './composer-chip.ts'
9
9
 
@@ -71,17 +71,21 @@ loader.load({id:'@huaqiu/dsh-tool-kicad-schematic-gen',factory:(require:(name:st
71
71
  result.status!=='ok'?h('p',null,'存在未渲染页面或预览警告,请结合工具结果检查。'):null,
72
72
  message?h('p',{role:'alert'},message):null)
73
73
  }
74
- return {inject:['slots'],apply:(ctx:ClientContext)=>{
74
+ return {inject:['slots','inputTriggers'],apply:(ctx:ClientContext)=>{
75
75
  getHost=()=>ctx.get?.('hqEdge') as PlacementHost|undefined
76
76
  const stopVisibility=keepDeliveryVisible()
77
77
  // Trace the two things that decide whether the plugin is visible at all:
78
78
  // the trigger registry (the `@` menu) and the mounted sidebar row. When the
79
79
  // node half fails to import, DSH never loads this file — so silence here
80
80
  // means "the entry did not activate", not "the row failed to mount".
81
- const triggers=ctx.get?.('inputTriggers')
82
- try{console.debug('[schematic-gen:client] apply',{inputTriggers:triggers!==undefined&&triggers!==null,
81
+ // `inputTriggers` is declared in `inject`, so cordis parks this plugin until
82
+ // the provider exists; `registerSchematicGenSourceWhenReady` still polls, so
83
+ // a provider that arrives later than that promise still gets our source —
84
+ // an unregistered source makes every send fail with
85
+ // `slash: no serializer for reference source "schematic-gen"`.
86
+ const source=registerSchematicGenSourceWhenReady(()=>ctx.get?.('inputTriggers'))
87
+ try{console.debug('[schematic-gen:client] apply',{inputTriggers:source.registered(),
83
88
  slots:typeof ctx.slots?.inject==='function'})}catch{}
84
- const stopRefs=registerSchematicGenSource(triggers)
85
89
  // The sidebar toolbutton. Clicking it inserts the chip; it never opens a
86
90
  // workspace. Both services are read lazily — they may materialize after this
87
91
  // plugin, and a missing one must not take the client half down.
@@ -90,6 +94,15 @@ loader.load({id:'@huaqiu/dsh-tool-kicad-schematic-gen',factory:(require:(name:st
90
94
  getSessions:()=>ctx.get?.('sessions') as SessionsLike|undefined,
91
95
  }
92
96
  const stopEntry=mountSchematicGenSidebarEntry({onToggle:()=>{
97
+ // Never insert a chip DSH cannot submit: the controller routes every chip to
98
+ // its owner's codec, so a source that never registered blocks the whole send
99
+ // instead of degrading. While the trigger pipeline is still warming up, say
100
+ // so and leave the draft clean.
101
+ if(!source.registered()){
102
+ try{console.debug('[schematic-gen:chip] click',{ok:false,reason:'source-unregistered'})}catch{}
103
+ notifyComposer(ctx,'source-unregistered')
104
+ return
105
+ }
93
106
  const result=insertSchematicChip(chipDeps)
94
107
  // The one line that answers "I clicked and nothing happened": every failure
95
108
  // path in the inserter is traced, so the browser console names the reason.
@@ -101,7 +114,7 @@ loader.load({id:'@huaqiu/dsh-tool-kicad-schematic-gen',factory:(require:(name:st
101
114
  },position:'after'})
102
115
  try{console.debug('[schematic-gen:client] sidebar row',{rows:document.querySelectorAll('[data-hqcg-schematic-entry]').length})}catch{}
103
116
  const dispose=ctx.slots.inject('tool.call.toolview',()=>ctx.slots.register({name:'tool.call.toolview',key:'render_kicad_module_preview'},Card))
104
- ctx.effect?.(()=>()=>{stopVisibility();stopRefs();stopEntry();if(typeof dispose==='function')dispose()})
117
+ ctx.effect?.(()=>()=>{stopVisibility();source.dispose();stopEntry();if(typeof dispose==='function')dispose()})
105
118
  }}
106
119
  /** Surface why a chip insertion did not happen, through the composer's own notice channel. Best-effort by design. */
107
120
  function notifyComposer(context:ClientContext,reason:string){
@@ -119,13 +132,16 @@ loader.load({id:'@huaqiu/dsh-tool-kicad-schematic-gen',factory:(require:(name:st
119
132
  input?.notify?.('info',noticeFor(reason))
120
133
  }catch{/* notice delivery is strictly optional */}
121
134
  }
122
- /** Human-readable notice text for one insertion failure reason. */
123
- function noticeFor(reason:string):string{
124
- switch(reason){
125
- case 'no-session':return 'Open a conversation first, then click Schematic Gen again.'
126
- case 'busy':return 'The composer is busy — try again once the current message settles.'
127
- case 'conflict':return 'The composer changed while inserting — click Schematic Gen again.'
128
- default:return 'Schematic Gen is unavailable in this session.'
129
- }
135
+ /** Human-readable notice text for one insertion failure reason. */
136
+ function noticeFor(reason:string):string{
137
+ switch(reason){
138
+ case 'no-session':return 'Open a conversation first, then click Schematic Gen again.'
139
+ case 'busy':return 'The composer is busy — try again once the current message settles.'
140
+ case 'conflict':return 'The composer changed while inserting — click Schematic Gen again.'
141
+ // The trigger pipeline has not registered our source yet. Inserting anyway
142
+ // would produce a chip DSH refuses to submit, so the click is declined.
143
+ case 'source-unregistered':return 'Schematic Gen is still starting up — click again in a moment.'
144
+ default:return 'Schematic Gen is unavailable in this session.'
130
145
  }
146
+ }
131
147
  }})
@@ -5,10 +5,13 @@
5
5
  * SSE agents were removed with the package). The chip identity, the `@`
6
6
  * trigger, the clipboard projection and the menu row are preserved verbatim so
7
7
  * existing drafts and the composer UX keep working. The one deliberate change:
8
- * the chip's model text now pins the bundled `kicad-schematic-design` skill
9
- * instead of the deleted online tools (`generate_schematic_from_description` /
10
- * `generate_system_module_graph`), because the design skill is the new local
11
- * entry point the agent must follow.
8
+ * the chip's model text is now the bare clipboard token — the semantics that
9
+ * used to ride in the payload (pin the `kicad-schematic-design` skill instead
10
+ * of the deleted online tools `generate_schematic_from_description` /
11
+ * `generate_system_module_graph`, forbid hand-written KiCad files, defer to
12
+ * the user's words) moved to the node half's `huaqiu:schematic-gen`
13
+ * system-prompt section; see {@link SCHEMATIC_GEN_SERIALIZED} and
14
+ * `src/prompt.ts`.
12
15
  *
13
16
  * One module owns the identity of the chip, its menu row, and the model text
14
17
  * the chip expands to at submit time. Registering the source is MANDATORY, not
@@ -18,6 +21,12 @@
18
21
  * source"). A chip inserted without a registered source would therefore block
19
22
  * the user's message instead of degrading quietly.
20
23
  *
24
+ * The registration is also ORDER-sensitive: the trigger service is provided by
25
+ * a plugin whose own dependency chain is longer than ours, so reading it once
26
+ * at `apply()` time can find nothing. The entry therefore declares
27
+ * `inputTriggers` in its `inject` (cordis parks the plugin until the provider
28
+ * exists) and retries briefly if it still arrives late.
29
+ *
21
30
  * Registering it also buys the typed path for free: the user can type `@` and
22
31
  * pick *Generate schematic* (生成原理图 under a Chinese UI) from a group beside
23
32
  * *Files & folders* / *Sessions*.
@@ -115,30 +124,27 @@ export function schematicGenDescription(lang: HostLocale | undefined = detectHos
115
124
 
116
125
  /**
117
126
  * Model form of the chip: what the agent actually reads at the chip's position
118
- * in the user message.
127
+ * in the user message — the clipboard token itself, nothing more.
128
+ *
129
+ * This is the idiomatic mention pattern (DSH's own draft tests serialize a
130
+ * reference to its ref), and it is deliberate on two counts:
119
131
  *
120
- * Deliberately an *intent*, never a fabricated design description — the user's
121
- * own words stay the only source of requirements, and the plugin being loaded
122
- * is never by itself a reason to generate.
132
+ * 1. **Whatever `serialize` returns lands in the USER message** — the submit
133
+ * plane splices it over the chip's clipboard range — so a long payload is
134
+ * noise the user reads back in their own transcript on every send.
135
+ * 2. **The token has no semantics of its own**, so the semantics cannot live
136
+ * here; they live in the node half's `huaqiu:schematic-gen` system-prompt
137
+ * section (see `src/prompt.ts`), which defines the token, pins the
138
+ * `kicad-schematic-design` skill, and carries the guardrails.
123
139
  *
124
- * The retired plugin pinned the online tools here
125
- * (`generate_schematic_from_description` / `generate_system_module_graph`).
126
- * The migrated chip pins the bundled `kicad-schematic-design` skill instead:
127
- * the skill owns the module search / part import / editable-project flow, and
128
- * starts from the hardware design brief when no plan exists yet. Hand-written
129
- * KiCad files stay forbidden in both worlds.
140
+ * History: the chip first shipped a 6-line `<schematic-gen intent=…>` block,
141
+ * then a 3-line trimmed form, and now delegates entirely to the section. The
142
+ * retired plugin pinned deleted online tools here
143
+ * (`generate_schematic_from_description` / `generate_system_module_graph`);
144
+ * nothing in this constant may ever name a tool again — the skill pin is the
145
+ * section's job.
130
146
  */
131
- export const SCHEMATIC_GEN_SERIALIZED =
132
- '<schematic-gen intent="kicad-schematic-design">\n' +
133
- 'The user explicitly chose Schematic Gen for this request. Produce the design ' +
134
- 'with the `kicad-schematic-design` skill; when the hardware design brief is ' +
135
- 'missing or incomplete, the skill starts by preparing it before the schematic ' +
136
- 'work.\n' +
137
- 'Never hand-write .kicad_sch files or KiCad project files yourself — always go ' +
138
- 'through the skill and its tools.\n' +
139
- "Use the user's own words as the design description. If they have not described " +
140
- 'the circuit yet, ask for the requirements instead of assuming them.\n' +
141
- '</schematic-gen>'
147
+ export const SCHEMATIC_GEN_SERIALIZED = SCHEMATIC_GEN_CLIPBOARD
142
148
 
143
149
  /** Menu/group candidate description. */
144
150
  /**
@@ -255,18 +261,167 @@ export function createSchematicGenSource(
255
261
  * @returns disposer (a no-op when registration did not happen).
256
262
  */
257
263
  export function registerSchematicGenSource(registry: unknown): () => void {
264
+ return registerSchematicGenSourceTracked(registry).dispose
265
+ }
266
+
267
+ /** Outcome of one registration attempt. */
268
+ export interface SchematicGenRegistration {
269
+ /** Whether the source is now live in the trigger roster. */
270
+ readonly registered: boolean
271
+ /** Unregister (a no-op when the attempt did not register). */
272
+ readonly dispose: () => void
273
+ }
274
+
275
+ /**
276
+ * One registration attempt, reported instead of swallowed.
277
+ *
278
+ * {@link registerSchematicGenSource} is the disposer-only view for callers that
279
+ * do not care; this is the same attempt with the outcome visible, which is what
280
+ * a caller needs to decide whether a chip may be inserted at all — an
281
+ * unregistered source makes every send fail (see the module header).
282
+ *
283
+ * @param registry - `ctx.get('inputTriggers')`, or undefined.
284
+ * @returns whether the registration landed, plus its disposer.
285
+ */
286
+ export function registerSchematicGenSourceTracked(registry: unknown): SchematicGenRegistration {
258
287
  if (
259
288
  registry === undefined || registry === null ||
260
289
  typeof (registry as InputTriggerRegistryLike).registerSource !== 'function'
261
290
  ) {
262
- return () => {}
291
+ return { registered: false, dispose: () => {} }
263
292
  }
264
293
  try {
265
294
  const dispose = (registry as InputTriggerRegistryLike).registerSource(createSchematicGenSource())
266
- return typeof dispose === 'function' ? dispose : () => {}
295
+ return {
296
+ registered: true,
297
+ dispose: typeof dispose === 'function' ? dispose : () => {},
298
+ }
267
299
  } catch {
268
300
  // Already registered (duplicate trigger/name) — the existing owner keeps
269
301
  // serving our chips, which is the correct outcome.
270
- return () => {}
302
+ return { registered: true, dispose: () => {} }
303
+ }
304
+ }
305
+
306
+ /** Scheduler seam for {@link registerSchematicGenSourceWhenReady} (tests inject fakes). */
307
+ export interface RegistrationRetryHooks {
308
+ /** Poll interval while the trigger service is still absent. */
309
+ intervalMs?: number
310
+ /** Give up after this many polls; default 80 (≈20 s at 250 ms). */
311
+ maxAttempts?: number
312
+ /** Timer constructor; defaults to the host `setInterval`. */
313
+ setTimer?: (fn: () => void, ms: number) => unknown
314
+ /** Timer destructor; defaults to the host `clearInterval`. */
315
+ clearTimer?: (handle: unknown) => void
316
+ /** Trace sink; defaults to this module's {@link trace}. */
317
+ trace?: (event: string, payload?: unknown) => void
318
+ }
319
+
320
+ /** Live registration handle returned by {@link registerSchematicGenSourceWhenReady}. */
321
+ export interface SchematicGenSourceBinding {
322
+ /** Whether the source is registered RIGHT NOW (re-read on every call). */
323
+ registered(): boolean
324
+ /** Stop retrying and unregister. */
325
+ dispose(): void
326
+ }
327
+
328
+ /**
329
+ * Host timer, or undefined where there is none.
330
+ *
331
+ * The retry is browser-only machinery, and the built bundle is also evaluated
332
+ * in bare hosts (the vm-based card test, a non-DOM worker) where `setInterval`
333
+ * does not exist. Those must degrade to "not registered yet" — a throw here
334
+ * would take the whole client half down, which is the failure mode this module
335
+ * exists to prevent.
336
+ */
337
+ function hostTimer(): { set: (fn: () => void, ms: number) => unknown; clear: (handle: unknown) => void } | undefined {
338
+ if (typeof setInterval !== 'function' || typeof clearInterval !== 'function') return undefined
339
+ return {
340
+ set: (fn, ms) => setInterval(fn, ms),
341
+ clear: (handle) => clearInterval(handle as never),
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Register the source, polling while the trigger service has not activated yet.
347
+ *
348
+ * Why this exists: activation is service-availability driven, and
349
+ * `inputTriggers` is provided by a plugin (`dsh-client-ui-input-trigger`) whose
350
+ * own dependency chain is far longer than ours. A plugin that reads the service
351
+ * exactly once at `apply()` time and finds it absent registers NOTHING —
352
+ * silently. The chip then lands in the composer (insertion only needs
353
+ * `conversation`) and the send dies on
354
+ * `slash: no serializer for reference source "schematic-gen"`, because the
355
+ * controller routes every chip to its owner's codec. Declaring `inputTriggers`
356
+ * in the plugin's `inject` is the primary fix (cordis then parks this plugin
357
+ * until the provider exists); this poll is the belt-and-braces path for a
358
+ * provider that arrives later than that declaration could promise.
359
+ *
360
+ * @param getRegistry - returns the current `ctx.get('inputTriggers')` value.
361
+ * @param hooks - scheduler/trace seams.
362
+ * @returns a live binding whose `registered()` the caller may consult.
363
+ */
364
+ export function registerSchematicGenSourceWhenReady(
365
+ getRegistry: () => unknown,
366
+ hooks: RegistrationRetryHooks = {},
367
+ ): SchematicGenSourceBinding {
368
+ const intervalMs = hooks.intervalMs ?? 250
369
+ const maxAttempts = hooks.maxAttempts ?? 80
370
+ const traceFn = hooks.trace ?? trace
371
+ const timer = hooks.setTimer !== undefined && hooks.clearTimer !== undefined
372
+ ? { set: hooks.setTimer, clear: hooks.clearTimer }
373
+ : hostTimer()
374
+
375
+ let current: SchematicGenRegistration = registerSchematicGenSourceTracked(getRegistry())
376
+ let handle: unknown
377
+ let attempts = 0
378
+ let stopped = false
379
+
380
+ const stopTimer = (): void => {
381
+ if (handle === undefined || timer === undefined) return
382
+ timer.clear(handle)
383
+ handle = undefined
384
+ }
385
+
386
+ const dispose = (): void => {
387
+ stopped = true
388
+ stopTimer()
389
+ current.dispose()
390
+ }
391
+
392
+ if (current.registered) {
393
+ return { registered: () => true, dispose }
394
+ }
395
+
396
+ if (timer === undefined) {
397
+ // No timer host: the caller's `registered()` stays false and the sidebar
398
+ // click declines to insert, which is the honest outcome — a chip that
399
+ // cannot be submitted is worse than no chip.
400
+ traceFn('no timer host — source registration not retried')
401
+ return { registered: () => false, dispose }
402
+ }
403
+
404
+ traceFn('waiting for inputTriggers', { intervalMs, maxAttempts })
405
+ handle = timer.set(() => {
406
+ if (stopped) return
407
+ attempts += 1
408
+ const next = registerSchematicGenSourceTracked(getRegistry())
409
+ if (next.registered) {
410
+ // The earlier attempt registered nothing, so it carries no disposer of
411
+ // its own — only the winning registration needs unregistering.
412
+ current = next
413
+ stopTimer()
414
+ traceFn('registered (late)', { attempts })
415
+ return
416
+ }
417
+ if (attempts >= maxAttempts) {
418
+ stopTimer()
419
+ traceFn('source registration gave up', { attempts })
420
+ }
421
+ }, intervalMs)
422
+
423
+ return {
424
+ registered: () => current.registered,
425
+ dispose,
271
426
  }
272
427
  }
package/src/index.ts CHANGED
@@ -8,10 +8,11 @@ import {apply as registerSchematic} from './schematic/index.ts'
8
8
  import {apply as registerSkills} from './skills.ts'
9
9
  import {registerModuleAnalysis} from './schematic/analysis-tool.ts'
10
10
  import {getKiCadPaths} from './runtime-paths.ts'
11
+ import {registerSchematicGenSection} from './prompt.ts'
11
12
 
12
13
  export {Config,resolveConfig} from './config.ts'
13
14
  export const name='@huaqiu/dsh-tool-kicad-schematic-gen'
14
- export const inject=['tools','fs','sandboxPolicy','skills','webServer'] as const
15
+ export const inject=['tools','fs','sandboxPolicy','skills','webServer','systemPrompt'] as const
15
16
 
16
17
  /** Shared component name for the unified DSH-plugin log (`~/.hq-edge/logs/dsh-plugins`). */
17
18
  const log = getLogger('dsh-schematic-gen')
@@ -20,7 +21,7 @@ const log = getLogger('dsh-schematic-gen')
20
21
  // ready" means the entry is parked on an unprovided service (or that another
21
22
  // entry's failure stopped the boot), which is a different problem from a
22
23
  // failure inside apply().
23
- log.info('schematic-gen: module loaded (waiting for the tools, fs, sandboxPolicy, skills and webServer services)')
24
+ log.info('schematic-gen: module loaded (waiting for the tools, fs, sandboxPolicy, skills, webServer and systemPrompt services)')
24
25
 
25
26
  export function apply(ctx:Context, input:ConfigInput={}):void {
26
27
  const config=resolveConfig(input)
@@ -36,6 +37,14 @@ export function apply(ctx:Context, input:ConfigInput={}):void {
36
37
  registerModules(ctx,config.modules,analyze)
37
38
  registerSchematic(ctx,config.schematic)
38
39
  registerSkills(ctx,config.schematic.kicadCliPath)
40
+ // The `@schematic-gen` chip serializes to its bare token; this section is
41
+ // what gives that token meaning (see prompt.ts for why it is static).
42
+ const disposeSection = registerSchematicGenSection((ctx as {systemPrompt?:unknown}).systemPrompt)
43
+ if (disposeSection === undefined) {
44
+ log.warn('systemPrompt service unavailable — the huaqiu:schematic-gen section was not registered')
45
+ } else {
46
+ ctx.effect(() => disposeSection)
47
+ }
39
48
  } catch (error) {
40
49
  log.error('schematic-gen node half failed',{error:String((error as Error)?.message??error)})
41
50
  throw error
package/src/prompt.ts ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The `huaqiu:schematic-gen` system-prompt section.
3
+ *
4
+ * Split out from `src/index.ts` so the text and the registration are
5
+ * unit-testable without a cordis context, tools, or a skill registry — the
6
+ * same split `@huaqiu/dsh-eda-selection` uses for its `eda:selection` section.
7
+ *
8
+ * ── Why a section at all ────────────────────────────────────────────────────
9
+ * The composer chip's model form is the bare `@schematic-gen` token (see
10
+ * `client/schematic-ref.ts`): the token is a UI affordance with no semantics
11
+ * of its own, and whatever `codec.serialize` returns lands in the USER
12
+ * message — repeating it on every send is noise the user reads back in their
13
+ * own transcript. The section carries the semantics once, in the system
14
+ * prompt, where policy text belongs.
15
+ *
16
+ * ── Why static, not a per-session provider ──────────────────────────────────
17
+ * "Once the user chose Schematic Gen, the rest of the conversation continues
18
+ * that work" is already true without any session state: the token sits in the
19
+ * conversation history for every later turn, and the section is re-read on
20
+ * every assembly. A dynamic provider would need a session-state source — the
21
+ * only candidates are message-history I/O or a client→node signal, and prompt
22
+ * assembly is on the agent's hot path (`dsh-eda-selection` documents the same
23
+ * no-I/O rule). So the section is static text gated on the token, not a
24
+ * per-session flag.
25
+ *
26
+ * @module
27
+ */
28
+
29
+ /**
30
+ * Unique prompt-section name.
31
+ *
32
+ * Reuses the retired `huaqiu:schematic-gen` name: `dsh-eda-selection` records
33
+ * that the old section "has moved to the bundled kicad-schematic-design
34
+ * skill", and this is that section coming back for the chip era — same slot,
35
+ * same adjacency, no other registration to collide with.
36
+ */
37
+ export const HUAQIU_SCHEMATIC_GEN_SECTION = 'huaqiu:schematic-gen'
38
+
39
+ /**
40
+ * Centrally allocated placement: 590, between DSH's `PLAN_POLICY` (500) and
41
+ * `TEAM_POLICY` (600).
42
+ *
43
+ * That is where `dsh-eda-selection` pins its `eda:selection` section, so both
44
+ * Huaqiu policy blocks stay adjacent and both precede the tool-list sections
45
+ * (`TOOL_*` at 1000+) they refer to. `ctx.systemPrompt.getSectionOrder()`
46
+ * resolves only DSH's own named positions, so the number is pinned here and
47
+ * asserted by a test.
48
+ */
49
+ export const HUAQIU_SCHEMATIC_GEN_SECTION_ORDER = 590
50
+
51
+ /** The chip token the section is gated on — the chip's clipboard projection. */
52
+ export const SCHEMATIC_GEN_TOKEN = '@schematic-gen'
53
+
54
+ /**
55
+ * The section text: what the `@schematic-gen` token MEANS.
56
+ *
57
+ * Static on purpose (see the module header). Four jobs, each mapping to a
58
+ * failure mode observed with the earlier inline-payload chip:
59
+ * 1. define the token as an explicit user choice — a bare token alone is an
60
+ * opaque string the model has no trained semantics for;
61
+ * 2. pin the `kicad-schematic-design` skill — the retired online tools are
62
+ * gone, so an unpinned agent may reach for a dead entry point;
63
+ * 3. forbid hand-written `.kicad_sch` files — asked for "an ESP32 mini", an
64
+ * LLM will otherwise author KiCad files itself, which KiCad cannot open;
65
+ * 4. defer requirements to the user's own words — a gesture is not a spec.
66
+ *
67
+ * It never claims the token carries content: it says what the token means,
68
+ * nothing about what the user asked for.
69
+ */
70
+ export function schematicGenSectionText(): string {
71
+ return [
72
+ `Schematic Gen: the ${SCHEMATIC_GEN_TOKEN} token in a user message is the ` +
73
+ 'Schematic Gen chip — the user explicitly chose it for that request. ' +
74
+ 'Produce the design with the `kicad-schematic-design` skill, which ' +
75
+ 'starts from the hardware design brief when none exists yet.',
76
+ 'Never hand-write .kicad_sch files or KiCad project files yourself; go ' +
77
+ 'through the skill and its tools.',
78
+ "Use the user's own words as the design description — if they have not " +
79
+ 'described the circuit yet, ask for the requirements instead of ' +
80
+ 'assuming them.',
81
+ 'Follow-up messages in the same conversation continue that work without ' +
82
+ 'repeating the token.',
83
+ ].join('\n')
84
+ }
85
+
86
+ /**
87
+ * Structural view of `ctx.systemPrompt` — only the face we use.
88
+ *
89
+ * Structural so this module compiles without depending on
90
+ * `@deepseek-ai/dsh-system-prompt` (whose `Context` augmentation would
91
+ * otherwise have to be in the compilation for a typed `ctx.systemPrompt`).
92
+ */
93
+ export interface SystemPromptLike {
94
+ section(section: {
95
+ name: string
96
+ order: number
97
+ text: string | ((context: unknown) => string)
98
+ }): () => void
99
+ }
100
+
101
+ /**
102
+ * Register the static Schematic Gen section.
103
+ *
104
+ * Never throws: a DSH deployment without the prompt service, or one where a
105
+ * duplicate registration races (hot reload), must still load the tools and
106
+ * the skill — the section is an enhancement, not a dependency of the rest of
107
+ * the plugin. A duplicate is the expected hot-reload case, so it is swallowed
108
+ * rather than surfaced; the first registration keeps serving.
109
+ *
110
+ * @param systemPrompt - `ctx.systemPrompt`, or undefined.
111
+ * @returns the section's disposer, or `undefined` when nothing was registered.
112
+ */
113
+ export function registerSchematicGenSection(systemPrompt: unknown): (() => void) | undefined {
114
+ if (
115
+ systemPrompt === undefined || systemPrompt === null ||
116
+ typeof (systemPrompt as SystemPromptLike).section !== 'function'
117
+ ) {
118
+ return undefined
119
+ }
120
+ try {
121
+ const dispose = (systemPrompt as SystemPromptLike).section({
122
+ name: HUAQIU_SCHEMATIC_GEN_SECTION,
123
+ order: HUAQIU_SCHEMATIC_GEN_SECTION_ORDER,
124
+ text: schematicGenSectionText(),
125
+ })
126
+ return typeof dispose === 'function' ? dispose : undefined
127
+ } catch {
128
+ // Duplicate registration (hot reload) — the first one is still live.
129
+ return undefined
130
+ }
131
+ }