@huaqiu/dsh-tool-kicad-schematic-gen 0.6.3 → 0.6.5
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 +878 -37
- package/lib/index.d.ts +1 -1
- package/lib/index.js +139 -8
- package/package.json +8 -4
- package/src/client/composer-chip.ts +328 -0
- package/src/client/index.ts +74 -6
- package/src/client/schematic-ref.ts +183 -28
- package/src/client/sidebar-entry.ts +230 -0
- package/src/index.ts +40 -6
- package/src/prompt.ts +131 -0
- package/src/schematic/preview.ts +42 -2
package/lib/client.js
CHANGED
|
@@ -84,6 +84,190 @@
|
|
|
84
84
|
}
|
|
85
85
|
//#endregion
|
|
86
86
|
//#region ../dsh-sidebar-entry/lib/index.mjs
|
|
87
|
+
/** Find the sidebar shell root element, or undefined while not yet mounted. */
|
|
88
|
+
function sidebarRoot() {
|
|
89
|
+
const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
|
|
90
|
+
if (column === null) return void 0;
|
|
91
|
+
return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
|
|
92
|
+
}
|
|
93
|
+
/** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */
|
|
94
|
+
function newSessionButton(root) {
|
|
95
|
+
const nested = root.querySelector("button[class*=\"newSession\"]");
|
|
96
|
+
if (nested !== null) return nested;
|
|
97
|
+
for (const child of root.children) if (child.tagName === "BUTTON") return child;
|
|
98
|
+
}
|
|
99
|
+
/** Build the entry row (a detached button; insert once the shell is up). */
|
|
100
|
+
function createEntry(options) {
|
|
101
|
+
const entry = document.createElement("button");
|
|
102
|
+
entry.type = "button";
|
|
103
|
+
entry.setAttribute(options.rowAttribute, "");
|
|
104
|
+
if (options.plugin !== void 0) {
|
|
105
|
+
entry.setAttribute("data-dsh-plugin", options.plugin);
|
|
106
|
+
entry.setAttribute("data-dsh-part", "sidebar-entry");
|
|
107
|
+
}
|
|
108
|
+
entry.className = options.css["entry"] ?? "";
|
|
109
|
+
const labelSpan = document.createElement("span");
|
|
110
|
+
labelSpan.className = options.css["entryLabel"] ?? "";
|
|
111
|
+
const iconSpan = document.createElement("span");
|
|
112
|
+
iconSpan.className = options.css["entryIcon"] ?? "";
|
|
113
|
+
iconSpan.innerHTML = options.icon;
|
|
114
|
+
entry.append(iconSpan, labelSpan);
|
|
115
|
+
let badgeSpan = null;
|
|
116
|
+
const applyBadge = () => {
|
|
117
|
+
if (options.badge === void 0) return;
|
|
118
|
+
const count = options.badge.count();
|
|
119
|
+
if (count <= 0) {
|
|
120
|
+
badgeSpan?.remove();
|
|
121
|
+
badgeSpan = null;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (badgeSpan === null) {
|
|
125
|
+
badgeSpan = document.createElement("span");
|
|
126
|
+
badgeSpan.className = options.css["entryBadge"] ?? "";
|
|
127
|
+
entry.append(badgeSpan);
|
|
128
|
+
}
|
|
129
|
+
badgeSpan.textContent = String(count);
|
|
130
|
+
entry.setAttribute("data-has-badge", "");
|
|
131
|
+
};
|
|
132
|
+
const applyLabel = () => {
|
|
133
|
+
entry.setAttribute("aria-label", options.label());
|
|
134
|
+
if (options.tooltip !== void 0) entry.setAttribute("title", options.tooltip());
|
|
135
|
+
labelSpan.textContent = options.label();
|
|
136
|
+
};
|
|
137
|
+
applyLabel();
|
|
138
|
+
applyBadge();
|
|
139
|
+
entry.addEventListener("click", options.onToggle);
|
|
140
|
+
return {
|
|
141
|
+
entry,
|
|
142
|
+
applyLabel,
|
|
143
|
+
applyBadge
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Re-insert the entry after the New Session row, in the family's intended
|
|
148
|
+
* order.
|
|
149
|
+
*
|
|
150
|
+
* The `familySelectors` array order is the single source of truth for visual
|
|
151
|
+
* order, so the rendered order is stable regardless of which plugin mounts
|
|
152
|
+
* first or of observer callback order — a re-render can never swap two rows.
|
|
153
|
+
* (Earlier builds used the `position: 'before' | 'after'` convention, which
|
|
154
|
+
* clustered rows by mount order and let a late-mounting `before` row steal the
|
|
155
|
+
* lead. The array order supersedes it; `position` remains only as a fallback
|
|
156
|
+
* for a row whose selector is absent from the family list.)
|
|
157
|
+
*/
|
|
158
|
+
function placeEntry(root, entry, options) {
|
|
159
|
+
const button = newSessionButton(root);
|
|
160
|
+
if (button === void 0) return false;
|
|
161
|
+
if (entry.parentElement !== root) {
|
|
162
|
+
const row = button.closest("[class*=\"logoRow\"]");
|
|
163
|
+
const base = row !== null && row.parentElement === root ? row : button;
|
|
164
|
+
const order = options.familySelectors;
|
|
165
|
+
const present = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(order.join(", "))).map((el) => ({
|
|
166
|
+
el,
|
|
167
|
+
idx: order.findIndex((sel) => el.matches(sel))
|
|
168
|
+
}));
|
|
169
|
+
const selfIndex = order.indexOf(options.rowSelector);
|
|
170
|
+
let anchor;
|
|
171
|
+
if (selfIndex === -1) anchor = options.position === "before" ? present.length > 0 ? present[0].el : base.nextElementSibling : present.length > 0 ? present[present.length - 1].el.nextElementSibling : base.nextElementSibling;
|
|
172
|
+
else {
|
|
173
|
+
const later = present.find((member) => member.idx > selfIndex);
|
|
174
|
+
if (later !== void 0) anchor = later.el;
|
|
175
|
+
else {
|
|
176
|
+
const earlier = [...present].reverse().find((member) => member.idx < selfIndex);
|
|
177
|
+
anchor = earlier !== void 0 ? earlier.el.nextElementSibling : base.nextElementSibling;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
root.insertBefore(entry, anchor ?? null);
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Mount the sidebar entry, waiting for the shell to render and self-healing
|
|
186
|
+
* on later React re-renders.
|
|
187
|
+
* @param options - the row's attribute/icon/copy/action/ordering configuration.
|
|
188
|
+
* @returns disposer removing the entry and its observers.
|
|
189
|
+
*/
|
|
190
|
+
function mountSidebarEntry(options) {
|
|
191
|
+
if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {};
|
|
192
|
+
const { entry, applyLabel, applyBadge } = createEntry(options);
|
|
193
|
+
let root;
|
|
194
|
+
let placed = false;
|
|
195
|
+
let unsubscribeRefresh;
|
|
196
|
+
if (options.refresh !== void 0) try {
|
|
197
|
+
unsubscribeRefresh = options.refresh.subscribe(applyLabel);
|
|
198
|
+
} catch {}
|
|
199
|
+
let unsubscribeBadge;
|
|
200
|
+
if (options.badge !== void 0) try {
|
|
201
|
+
unsubscribeBadge = options.badge.subscribe(applyBadge);
|
|
202
|
+
} catch {}
|
|
203
|
+
const tryPlace = () => {
|
|
204
|
+
if (root !== void 0 && !root.isConnected) {
|
|
205
|
+
rootObserver.disconnect();
|
|
206
|
+
root = void 0;
|
|
207
|
+
placed = false;
|
|
208
|
+
}
|
|
209
|
+
if (placed) {
|
|
210
|
+
if (document.body.contains(entry)) return;
|
|
211
|
+
rootObserver.disconnect();
|
|
212
|
+
root = void 0;
|
|
213
|
+
placed = false;
|
|
214
|
+
}
|
|
215
|
+
root ??= sidebarRoot();
|
|
216
|
+
if (root === void 0) return;
|
|
217
|
+
placed = placeEntry(root, entry, options);
|
|
218
|
+
if (placed) rootObserver.observe(root, {
|
|
219
|
+
childList: true,
|
|
220
|
+
subtree: true
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
const waitObserver = new MutationObserver(() => {
|
|
224
|
+
tryPlace();
|
|
225
|
+
});
|
|
226
|
+
waitObserver.observe(document.body, {
|
|
227
|
+
childList: true,
|
|
228
|
+
subtree: true
|
|
229
|
+
});
|
|
230
|
+
const rootObserver = new MutationObserver(() => {
|
|
231
|
+
if (root === void 0 || !root.isConnected) {
|
|
232
|
+
placed = false;
|
|
233
|
+
tryPlace();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (!root.contains(entry)) placed = placeEntry(root, entry, options);
|
|
237
|
+
});
|
|
238
|
+
options.active === void 0 || (() => {
|
|
239
|
+
const syncActive = () => {
|
|
240
|
+
if (options.active.isOpen()) entry.dataset.active = "true";
|
|
241
|
+
else delete entry.dataset.active;
|
|
242
|
+
};
|
|
243
|
+
const unsubscribe = options.active.subscribe(syncActive);
|
|
244
|
+
syncActive();
|
|
245
|
+
return unsubscribe;
|
|
246
|
+
})();
|
|
247
|
+
tryPlace();
|
|
248
|
+
return () => {
|
|
249
|
+
waitObserver.disconnect();
|
|
250
|
+
rootObserver.disconnect();
|
|
251
|
+
unsubscribeRefresh?.();
|
|
252
|
+
unsubscribeBadge?.();
|
|
253
|
+
entry.remove();
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Every row selector in the Huaqiu sidebar family, in intended visual order.
|
|
258
|
+
*
|
|
259
|
+
* Family ordering is only stable when every participating plugin passes the
|
|
260
|
+
* SAME list to {@link mountSidebarEntry} — the core positions a row relative to
|
|
261
|
+
* the family block, never relative to transient shell geometry. Adding a new
|
|
262
|
+
* family row means adding its selector here (and to no plugin-local list).
|
|
263
|
+
*/
|
|
264
|
+
const HUAQIU_SIDEBAR_ENTRY_FAMILY = [
|
|
265
|
+
"[data-hq-plugin-update-entry]",
|
|
266
|
+
"[data-hqcg-footprint-entry]",
|
|
267
|
+
"[data-hqcg-symbol-entry]",
|
|
268
|
+
"[data-hqcg-schematic-entry]",
|
|
269
|
+
"[data-hq-dummy-entry]"
|
|
270
|
+
];
|
|
87
271
|
/**
|
|
88
272
|
* Read the host UI language from `<html lang>`.
|
|
89
273
|
*
|
|
@@ -100,6 +284,44 @@
|
|
|
100
284
|
const primary = tag.toLowerCase().split("-")[0];
|
|
101
285
|
return primary === "zh" || primary === "en" ? primary : void 0;
|
|
102
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Watch `<html lang>` and report every resolved change.
|
|
289
|
+
*
|
|
290
|
+
* Idempotent-safe: repeated calls share one observer and each caller gets its
|
|
291
|
+
* own disposer; the observer disconnects when the last listener leaves.
|
|
292
|
+
* @param listener - called with the newly resolved language.
|
|
293
|
+
* @returns disposer removing this listener.
|
|
294
|
+
*/
|
|
295
|
+
/** Shared observer/listener set backing {@link observeHostLocale}. */
|
|
296
|
+
const localeListeners = /* @__PURE__ */ new Set();
|
|
297
|
+
let localeObserver = null;
|
|
298
|
+
function observeHostLocale(listener) {
|
|
299
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined" || document.documentElement === null) return () => {};
|
|
300
|
+
let current = detectHostLocale();
|
|
301
|
+
const listeners = localeListeners;
|
|
302
|
+
listeners.add(listener);
|
|
303
|
+
if (localeObserver === null) {
|
|
304
|
+
localeObserver = new MutationObserver(() => {
|
|
305
|
+
const next = detectHostLocale();
|
|
306
|
+
if (next === current) return;
|
|
307
|
+
current = next;
|
|
308
|
+
for (const fn of [...listeners]) try {
|
|
309
|
+
fn(next);
|
|
310
|
+
} catch {}
|
|
311
|
+
});
|
|
312
|
+
localeObserver.observe(document.documentElement, {
|
|
313
|
+
attributes: true,
|
|
314
|
+
attributeFilter: ["lang"]
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return () => {
|
|
318
|
+
listeners.delete(listener);
|
|
319
|
+
if (listeners.size === 0 && localeObserver !== null) {
|
|
320
|
+
localeObserver.disconnect();
|
|
321
|
+
localeObserver = null;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
103
325
|
//#endregion
|
|
104
326
|
//#region src/client/schematic-ref.ts
|
|
105
327
|
/**
|
|
@@ -109,10 +331,13 @@
|
|
|
109
331
|
* SSE agents were removed with the package). The chip identity, the `@`
|
|
110
332
|
* trigger, the clipboard projection and the menu row are preserved verbatim so
|
|
111
333
|
* existing drafts and the composer UX keep working. The one deliberate change:
|
|
112
|
-
* the chip's model text now
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
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`.
|
|
116
341
|
*
|
|
117
342
|
* One module owns the identity of the chip, its menu row, and the model text
|
|
118
343
|
* the chip expands to at submit time. Registering the source is MANDATORY, not
|
|
@@ -122,6 +347,12 @@
|
|
|
122
347
|
* source"). A chip inserted without a registered source would therefore block
|
|
123
348
|
* the user's message instead of degrading quietly.
|
|
124
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
|
+
*
|
|
125
356
|
* Registering it also buys the typed path for free: the user can type `@` and
|
|
126
357
|
* pick *Generate schematic* (生成原理图 under a Chinese UI) from a group beside
|
|
127
358
|
* *Files & folders* / *Sessions*.
|
|
@@ -145,11 +376,11 @@
|
|
|
145
376
|
* Every line is `console.debug` and every helper is wrapped, so a host without
|
|
146
377
|
* a console (or one that rejects the payload) can never break the plugin.
|
|
147
378
|
*/
|
|
148
|
-
const TRACE = "[schematic-gen:ref]";
|
|
379
|
+
const TRACE$1 = "[schematic-gen:ref]";
|
|
149
380
|
/** Emit one trace line. Never throws. */
|
|
150
|
-
function trace(event, payload) {
|
|
381
|
+
function trace$1(event, payload) {
|
|
151
382
|
try {
|
|
152
|
-
console.debug(TRACE, event, payload ?? "");
|
|
383
|
+
console.debug(TRACE$1, event, payload ?? "");
|
|
153
384
|
} catch {}
|
|
154
385
|
}
|
|
155
386
|
/**
|
|
@@ -162,7 +393,7 @@
|
|
|
162
393
|
*/
|
|
163
394
|
function schematicGenLabel(lang = detectHostLocale()) {
|
|
164
395
|
const label = lang === "en" ? "Generate schematic" : "生成原理图";
|
|
165
|
-
trace("label", {
|
|
396
|
+
trace$1("label", {
|
|
166
397
|
hostLang: lang ?? null,
|
|
167
398
|
htmlLang: readHtmlLang(),
|
|
168
399
|
label
|
|
@@ -185,20 +416,27 @@
|
|
|
185
416
|
}
|
|
186
417
|
/**
|
|
187
418
|
* Model form of the chip: what the agent actually reads at the chip's position
|
|
188
|
-
* in the user message.
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* The
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
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:
|
|
423
|
+
*
|
|
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.
|
|
431
|
+
*
|
|
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.
|
|
200
438
|
*/
|
|
201
|
-
const SCHEMATIC_GEN_SERIALIZED =
|
|
439
|
+
const SCHEMATIC_GEN_SERIALIZED = SCHEMATIC_GEN_CLIPBOARD;
|
|
202
440
|
/**
|
|
203
441
|
* The `ReferenceInsert` this source produces on pick (and on button click).
|
|
204
442
|
*
|
|
@@ -212,7 +450,7 @@
|
|
|
212
450
|
label: schematicGenLabel(lang),
|
|
213
451
|
clipboardText: SCHEMATIC_GEN_CLIPBOARD
|
|
214
452
|
};
|
|
215
|
-
trace("insert", insert);
|
|
453
|
+
trace$1("insert", insert);
|
|
216
454
|
return insert;
|
|
217
455
|
}
|
|
218
456
|
/**
|
|
@@ -239,12 +477,12 @@
|
|
|
239
477
|
name: schematicGenLabel(lang),
|
|
240
478
|
description: schematicGenDescription(lang)
|
|
241
479
|
}];
|
|
242
|
-
trace("candidates", rows);
|
|
480
|
+
trace$1("candidates", rows);
|
|
243
481
|
return Promise.resolve(rows);
|
|
244
482
|
},
|
|
245
483
|
onPick: () => {
|
|
246
484
|
const insert = schematicGenInsert(getLang());
|
|
247
|
-
trace("onPick", insert);
|
|
485
|
+
trace$1("onPick", insert);
|
|
248
486
|
return { insert };
|
|
249
487
|
},
|
|
250
488
|
codec: {
|
|
@@ -254,24 +492,569 @@
|
|
|
254
492
|
};
|
|
255
493
|
}
|
|
256
494
|
/**
|
|
257
|
-
*
|
|
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).
|
|
258
501
|
*
|
|
259
|
-
* Never throws: a missing service, a duplicate registration (a second live
|
|
260
|
-
* copy of this plugin) or a throwing registry must not take the whole client
|
|
261
|
-
* half down. Without a registered source the sidebar button degrades to
|
|
262
|
-
* inserting nothing rather than producing a chip that would block submission.
|
|
263
502
|
* @param registry - `ctx.get('inputTriggers')`, or undefined.
|
|
264
|
-
* @returns
|
|
503
|
+
* @returns whether the registration landed, plus its disposer.
|
|
265
504
|
*/
|
|
266
|
-
function
|
|
267
|
-
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
|
+
};
|
|
268
510
|
try {
|
|
269
511
|
const dispose = registry.registerSource(createSchematicGenSource());
|
|
270
|
-
return
|
|
512
|
+
return {
|
|
513
|
+
registered: true,
|
|
514
|
+
dispose: typeof dispose === "function" ? dispose : () => {}
|
|
515
|
+
};
|
|
271
516
|
} catch {
|
|
272
|
-
return
|
|
517
|
+
return {
|
|
518
|
+
registered: true,
|
|
519
|
+
dispose: () => {}
|
|
520
|
+
};
|
|
273
521
|
}
|
|
274
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
|
+
};
|
|
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
|
+
};
|
|
614
|
+
}
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region src/client/sidebar-entry.ts
|
|
617
|
+
/**
|
|
618
|
+
* `@huaqiu/dsh-tool-kicad-schematic-gen` — the `Schematic Gen` sidebar toolbutton.
|
|
619
|
+
*
|
|
620
|
+
* Migrated from the retired `@huaqiu/dsh-tool-schematic-gen` plugin: the
|
|
621
|
+
* migration deleted the whole old package and this row went with it, which is
|
|
622
|
+
* why the sidebar showed only Plugin Updates / Footprint Gen / Symbol Gen while
|
|
623
|
+
* the plugin itself stayed installed and registered. The row, its ordering, its
|
|
624
|
+
* icon and its localization are restored verbatim.
|
|
625
|
+
*
|
|
626
|
+
* Mounts one row through the shared family helper
|
|
627
|
+
* (`@huaqiu/dsh-sidebar-entry`), in the same block and with the same look as
|
|
628
|
+
* the component-gen rows (封装生成 / Symbol 生成). Unlike those, clicking this
|
|
629
|
+
* row does NOT open a workspace: it inserts a `Generate schematic` reference
|
|
630
|
+
* chip into the current session's composer.
|
|
631
|
+
*
|
|
632
|
+
* The row is plain DOM; the chip it inserts is real DSH composer state.
|
|
633
|
+
*
|
|
634
|
+
* @module
|
|
635
|
+
*/
|
|
636
|
+
/** Stable data attribute identifying the injected row. */
|
|
637
|
+
const SCHEMATIC_ENTRY_SELECTOR = "[data-hqcg-schematic-entry]";
|
|
638
|
+
/** Full attribute name (the helper's idempotency key). */
|
|
639
|
+
const SCHEMATIC_ENTRY_ATTRIBUTE = "data-hqcg-schematic-entry";
|
|
640
|
+
/** L2 semantic plugin id: rows carry data-dsh-plugin + data-dsh-part. */
|
|
641
|
+
const PLUGIN_ID = "huaqiu-schematic-gen";
|
|
642
|
+
/** Row class names (defined by {@link ENTRY_CSS_TEXT}). */
|
|
643
|
+
const ENTRY_CSS = {
|
|
644
|
+
entry: "hqcg-schematic-sidebar-entry",
|
|
645
|
+
entryIcon: "hqcg-schematic-sidebar-entry__icon",
|
|
646
|
+
entryLabel: "hqcg-schematic-sidebar-entry__label"
|
|
647
|
+
};
|
|
648
|
+
/** Localized row copy: zh is the fallback, matching the HIT card. */
|
|
649
|
+
const COPY = {
|
|
650
|
+
zh: {
|
|
651
|
+
label: "原理图生成",
|
|
652
|
+
tooltip: "在输入框插入「生成原理图」引用"
|
|
653
|
+
},
|
|
654
|
+
en: {
|
|
655
|
+
label: "Schematic Gen",
|
|
656
|
+
tooltip: "Insert a \"Generate schematic\" reference into the composer"
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
/**
|
|
660
|
+
* Resolve the row copy for one language.
|
|
661
|
+
* @param lang - resolved host language; undefined falls back to zh.
|
|
662
|
+
* @returns the label/tooltip pair.
|
|
663
|
+
*/
|
|
664
|
+
function schematicEntryCopy(lang) {
|
|
665
|
+
return lang === "en" ? COPY.en : COPY.zh;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Inline schematic glyph — cropped to the artwork's ink, `currentColor` so it
|
|
669
|
+
* inherits the sidebar text/theme colour, 16×16 like the shell's nav glyphs
|
|
670
|
+
* (18×18 on the collapsed rail).
|
|
671
|
+
*/
|
|
672
|
+
const SCHEMATIC_ICON = `
|
|
673
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
674
|
+
<!-- Schematic Sheet Frame -->
|
|
675
|
+
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
676
|
+
|
|
677
|
+
<!-- Circuit Wires & Connection Nodes -->
|
|
678
|
+
<path d="M7 8H10M14 8H17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
679
|
+
<path d="M10 16H14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
680
|
+
<path d="M10 8V16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
681
|
+
|
|
682
|
+
<!-- Component Symbol (Resistor / Block representation) -->
|
|
683
|
+
<path d="M10 10L11 11L12 9L13 11L14 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
684
|
+
|
|
685
|
+
<!-- Junction Nodes / Pins -->
|
|
686
|
+
<circle cx="7" cy="8" r="1.5" fill="currentColor"/>
|
|
687
|
+
<circle cx="17" cy="8" r="1.5" fill="currentColor"/>
|
|
688
|
+
<circle cx="10" cy="16" r="1.5" fill="currentColor"/>
|
|
689
|
+
<circle cx="14" cy="16" r="1.5" fill="currentColor"/>
|
|
690
|
+
</svg>
|
|
691
|
+
`;
|
|
692
|
+
/**
|
|
693
|
+
* Row styles — matched to the shipped `sidebar.panellist` rows and to the
|
|
694
|
+
* component-gen rows, so the family block sits flush with the shell's own
|
|
695
|
+
* entries.
|
|
696
|
+
*/
|
|
697
|
+
const ENTRY_CSS_TEXT = `
|
|
698
|
+
.hqcg-schematic-sidebar-entry {
|
|
699
|
+
box-sizing: border-box;
|
|
700
|
+
display: flex;
|
|
701
|
+
align-items: center;
|
|
702
|
+
gap: 8px;
|
|
703
|
+
width: 100%;
|
|
704
|
+
margin: 0 2px;
|
|
705
|
+
min-height: 36px;
|
|
706
|
+
padding: 7px 8px;
|
|
707
|
+
background: transparent;
|
|
708
|
+
border: none;
|
|
709
|
+
border-radius: 12px;
|
|
710
|
+
color: var(--dsw-alias-label-primary);
|
|
711
|
+
cursor: pointer;
|
|
712
|
+
font: inherit;
|
|
713
|
+
line-height: 22px;
|
|
714
|
+
text-align: left;
|
|
715
|
+
white-space: nowrap;
|
|
716
|
+
}
|
|
717
|
+
.hqcg-schematic-sidebar-entry:hover {
|
|
718
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
719
|
+
}
|
|
720
|
+
.hqcg-schematic-sidebar-entry[data-active] {
|
|
721
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
722
|
+
color: var(--dsw-alias-label-primary);
|
|
723
|
+
}
|
|
724
|
+
.hqcg-schematic-sidebar-entry__icon {
|
|
725
|
+
display: inline-flex;
|
|
726
|
+
align-items: center;
|
|
727
|
+
justify-content: center;
|
|
728
|
+
flex: none;
|
|
729
|
+
}
|
|
730
|
+
.hqcg-schematic-sidebar-entry__icon svg {
|
|
731
|
+
display: block;
|
|
732
|
+
width: 16px;
|
|
733
|
+
height: 16px;
|
|
734
|
+
}
|
|
735
|
+
.hqcg-schematic-sidebar-entry__label {
|
|
736
|
+
min-width: 0;
|
|
737
|
+
overflow: hidden;
|
|
738
|
+
text-overflow: ellipsis;
|
|
739
|
+
}
|
|
740
|
+
/* Collapsed rail: icon-only, centered — the same 36px square the shell uses. */
|
|
741
|
+
[data-dsh-frame][data-sidebar-collapsed] .hqcg-schematic-sidebar-entry,
|
|
742
|
+
[data-sidebar-collapsed] .hqcg-schematic-sidebar-entry {
|
|
743
|
+
justify-content: center;
|
|
744
|
+
width: 36px;
|
|
745
|
+
height: 36px;
|
|
746
|
+
margin: 0;
|
|
747
|
+
padding: 0;
|
|
748
|
+
}
|
|
749
|
+
[data-dsh-frame][data-sidebar-collapsed] .hqcg-schematic-sidebar-entry__icon svg,
|
|
750
|
+
[data-sidebar-collapsed] .hqcg-schematic-sidebar-entry__icon svg {
|
|
751
|
+
width: 18px;
|
|
752
|
+
height: 18px;
|
|
753
|
+
}
|
|
754
|
+
[data-dsh-frame][data-sidebar-collapsed] .hqcg-schematic-sidebar-entry__label,
|
|
755
|
+
[data-sidebar-collapsed] .hqcg-schematic-sidebar-entry__label {
|
|
756
|
+
display: none;
|
|
757
|
+
}
|
|
758
|
+
`;
|
|
759
|
+
/** Style tag id used for idempotent style injection. */
|
|
760
|
+
const STYLE_TAG_ID = "data-plugin-css=\"huaqiu-schematic-gen-sidebar\"";
|
|
761
|
+
/** Inject the row stylesheet once (idempotent). */
|
|
762
|
+
function injectSchematicEntryStyles() {
|
|
763
|
+
if (typeof document === "undefined") return;
|
|
764
|
+
if (document.querySelector(`style[${STYLE_TAG_ID}]`) !== null) return;
|
|
765
|
+
const tag = document.createElement("style");
|
|
766
|
+
tag.dataset.pluginCss = "huaqiu-schematic-gen-sidebar";
|
|
767
|
+
tag.textContent = ENTRY_CSS_TEXT;
|
|
768
|
+
document.head.appendChild(tag);
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Mount the `Schematic Gen` sidebar row.
|
|
772
|
+
*
|
|
773
|
+
* Localization follows the host UI language through `<html lang>`
|
|
774
|
+
* (`dsh-client-locale` rewrites it on every language change and HQ Edge mirrors
|
|
775
|
+
* it), so the row re-labels itself without a `locale` service dependency.
|
|
776
|
+
*
|
|
777
|
+
* @param options - click action and family-block position.
|
|
778
|
+
* @returns disposer removing the row, its observers and its locale listener.
|
|
779
|
+
*/
|
|
780
|
+
function mountSchematicGenSidebarEntry(options) {
|
|
781
|
+
if (typeof document === "undefined") return () => {};
|
|
782
|
+
injectSchematicEntryStyles();
|
|
783
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
784
|
+
const announce = () => {
|
|
785
|
+
for (const fn of [...listeners]) try {
|
|
786
|
+
fn();
|
|
787
|
+
} catch {}
|
|
788
|
+
};
|
|
789
|
+
let lang = detectHostLocale();
|
|
790
|
+
const stopLocale = observeHostLocale((next) => {
|
|
791
|
+
lang = next;
|
|
792
|
+
announce();
|
|
793
|
+
});
|
|
794
|
+
const dispose = mountSidebarEntry({
|
|
795
|
+
rowAttribute: SCHEMATIC_ENTRY_ATTRIBUTE,
|
|
796
|
+
rowSelector: SCHEMATIC_ENTRY_SELECTOR,
|
|
797
|
+
plugin: PLUGIN_ID,
|
|
798
|
+
icon: SCHEMATIC_ICON,
|
|
799
|
+
css: ENTRY_CSS,
|
|
800
|
+
label: () => schematicEntryCopy(lang).label,
|
|
801
|
+
tooltip: () => schematicEntryCopy(lang).tooltip,
|
|
802
|
+
refresh: { subscribe: (fn) => {
|
|
803
|
+
listeners.add(fn);
|
|
804
|
+
return () => {
|
|
805
|
+
listeners.delete(fn);
|
|
806
|
+
};
|
|
807
|
+
} },
|
|
808
|
+
onToggle: options.onToggle,
|
|
809
|
+
position: options.position ?? "after",
|
|
810
|
+
familySelectors: HUAQIU_SIDEBAR_ENTRY_FAMILY
|
|
811
|
+
});
|
|
812
|
+
return () => {
|
|
813
|
+
dispose();
|
|
814
|
+
stopLocale();
|
|
815
|
+
listeners.clear();
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
//#endregion
|
|
819
|
+
//#region src/client/composer-chip.ts
|
|
820
|
+
/**
|
|
821
|
+
* `@huaqiu/dsh-tool-kicad-schematic-gen` — programmatic composer chip insertion.
|
|
822
|
+
*
|
|
823
|
+
* Migrated from the retired `@huaqiu/dsh-tool-schematic-gen` plugin, whose
|
|
824
|
+
* sidebar row was lost with the package. The row is restored together with the
|
|
825
|
+
* path that makes it do something: inserting the `@ Generate schematic` chip
|
|
826
|
+
* into the live composer.
|
|
827
|
+
*
|
|
828
|
+
* The public path is `ctx.conversation.input.for(actx).insertReference(ref,
|
|
829
|
+
* span)` — a verb on DSH's frozen `SessionInput` face, reached through two
|
|
830
|
+
* public services (`conversation`, `sessions`). No DOM forgery, no new public
|
|
831
|
+
* API, no DSH source change.
|
|
832
|
+
*
|
|
833
|
+
* Two coordinate systems are in play and mixing them silently misplaces the
|
|
834
|
+
* chip:
|
|
835
|
+
*
|
|
836
|
+
* - `InputState.draft` and `Occurrence.offset|length` are CLIPBOARD
|
|
837
|
+
* coordinates — a chip occupies its whole `clipboardText`.
|
|
838
|
+
* - `TokenSpan` is DETECT coordinates — a chip is exactly one `U+FFFC`.
|
|
839
|
+
*
|
|
840
|
+
* `insertReference` is span-CAS'd (`span.draftRev !== this.rev → false`) and
|
|
841
|
+
* refuses busy phases, so every call reads a fresh snapshot and retries once.
|
|
842
|
+
*
|
|
843
|
+
* @module
|
|
844
|
+
*/
|
|
845
|
+
/** Browser-console tag for the insertion path (see schematic-ref.ts). */
|
|
846
|
+
const TRACE = "[schematic-gen:chip]";
|
|
847
|
+
/** Emit one trace line. Never throws. */
|
|
848
|
+
function trace(event, payload) {
|
|
849
|
+
try {
|
|
850
|
+
console.debug(TRACE, event, payload ?? "");
|
|
851
|
+
} catch {}
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* The Session whose composer the user is looking at.
|
|
855
|
+
*
|
|
856
|
+
* `SessionListState` has no `current` field — reading one silently yields
|
|
857
|
+
* undefined and the insertion aborts before touching the draft. The main view
|
|
858
|
+
* is whichever row `retainedBy.mainView` counts, so that is the primary
|
|
859
|
+
* answer; host list order and then any known row are the fallbacks.
|
|
860
|
+
*
|
|
861
|
+
* @param sessions - `ctx.get('sessions')`, or undefined.
|
|
862
|
+
* @returns the Session id, or undefined when the host has none yet.
|
|
863
|
+
*/
|
|
864
|
+
function currentSessionId(sessions) {
|
|
865
|
+
let list;
|
|
866
|
+
try {
|
|
867
|
+
list = sessions?.list?.getSnapshot();
|
|
868
|
+
} catch {
|
|
869
|
+
list = void 0;
|
|
870
|
+
}
|
|
871
|
+
if (list === void 0 || list === null) return void 0;
|
|
872
|
+
const ids = Array.isArray(list.ids) ? list.ids : [];
|
|
873
|
+
for (const id of ids) if ((list.byId?.[id]?.retainedBy?.mainView ?? 0) > 0) return id;
|
|
874
|
+
if (ids.length > 0) return ids[0];
|
|
875
|
+
return Object.keys(list.byId ?? {})[0];
|
|
876
|
+
}
|
|
877
|
+
/** How many Sessions the host knows about — the only context a `no-session` trace needs. */
|
|
878
|
+
function countSessions(sessions) {
|
|
879
|
+
try {
|
|
880
|
+
const list = sessions?.list?.getSnapshot();
|
|
881
|
+
const ids = Array.isArray(list?.ids) ? list?.ids.length ?? 0 : 0;
|
|
882
|
+
return ids > 0 ? ids : Object.keys(list?.byId ?? {}).length;
|
|
883
|
+
} catch {
|
|
884
|
+
return 0;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Resolve one Session's scope context — the value `conversation.input.for()`
|
|
889
|
+
* needs.
|
|
890
|
+
*
|
|
891
|
+
* `binding(id).ctx` is the ONLY guard-safe path: DSH's dynamic-plugin facade
|
|
892
|
+
* rejects every service call whose result is a cordis Context
|
|
893
|
+
* (`sessions.scope()` → "service \"sessions\" returned a cordis Context, which
|
|
894
|
+
* the dynamic facade does not expose"), and that rejection THROWS. `binding()`
|
|
895
|
+
* returns a plain object, so its `ctx` field is reachable without going back
|
|
896
|
+
* through the facade. `scope()` stays as a last resort for a host whose
|
|
897
|
+
* binding carries no ctx.
|
|
898
|
+
*
|
|
899
|
+
* @param sessions - `ctx.get('sessions')`, or undefined.
|
|
900
|
+
* @param sessionId - target Session.
|
|
901
|
+
* @returns the scope context, or undefined.
|
|
902
|
+
*/
|
|
903
|
+
function resolveSessionScope(sessions, sessionId) {
|
|
904
|
+
let binding;
|
|
905
|
+
try {
|
|
906
|
+
binding = sessions?.binding?.(sessionId);
|
|
907
|
+
} catch {
|
|
908
|
+
binding = void 0;
|
|
909
|
+
}
|
|
910
|
+
const ctx = binding?.ctx;
|
|
911
|
+
if (ctx !== void 0 && ctx !== null) return ctx;
|
|
912
|
+
try {
|
|
913
|
+
return sessions?.scope?.(sessionId);
|
|
914
|
+
} catch {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Translate a clipboard-projected draft length into DETECT coordinates.
|
|
920
|
+
*
|
|
921
|
+
* Every existing chip shrinks from `length` clipboard characters to one detect
|
|
922
|
+
* character, so the detect length is the clipboard length minus the sum of
|
|
923
|
+
* `length - 1` over all occurrences. A draft with no chips is unchanged.
|
|
924
|
+
*
|
|
925
|
+
* @param state - the fresh input snapshot.
|
|
926
|
+
* @returns the detect-coordinate length of the whole draft.
|
|
927
|
+
*/
|
|
928
|
+
function detectLengthOf(state) {
|
|
929
|
+
const draft = typeof state.draft === "string" ? state.draft : "";
|
|
930
|
+
const occurrences = Array.isArray(state.occurrences) ? state.occurrences : [];
|
|
931
|
+
let shrink = 0;
|
|
932
|
+
for (const occurrence of occurrences) {
|
|
933
|
+
const length = typeof occurrence.length === "number" ? occurrence.length : 0;
|
|
934
|
+
if (length > 1) shrink += length - 1;
|
|
935
|
+
}
|
|
936
|
+
return Math.max(0, draft.length - shrink);
|
|
937
|
+
}
|
|
938
|
+
/** Whether the draft already carries our chip. */
|
|
939
|
+
function hasSchematicChip(state) {
|
|
940
|
+
return (Array.isArray(state.occurrences) ? state.occurrences : []).some((o) => o.source === "schematic-gen" && o.ref === "generate");
|
|
941
|
+
}
|
|
942
|
+
/** Phases in which `insertReference` refuses to edit. */
|
|
943
|
+
const BUSY_PHASES = ["adjudicating", "submitting"];
|
|
944
|
+
/**
|
|
945
|
+
* Insert the `Generate schematic` chip at the end of the current draft.
|
|
946
|
+
*
|
|
947
|
+
* Never throws and never partially edits: every failure leaves the draft
|
|
948
|
+
* exactly as it was.
|
|
949
|
+
*
|
|
950
|
+
* @param deps - lazy service accessors (resolved per call, never captured).
|
|
951
|
+
* @param insert - the reference to insert (defaults to the schematic-gen one,
|
|
952
|
+
* whose label is resolved against the host language at call
|
|
953
|
+
* time — so the chip matches the UI language, not a frozen
|
|
954
|
+
* English literal).
|
|
955
|
+
* @returns whether a chip landed, and why not when it did not.
|
|
956
|
+
*/
|
|
957
|
+
function insertSchematicChip(deps, insert = schematicGenInsert()) {
|
|
958
|
+
const sessions = deps.getSessions();
|
|
959
|
+
const conversation = deps.getConversation();
|
|
960
|
+
if (sessions === void 0 || conversation === void 0 || conversation.input === void 0) {
|
|
961
|
+
trace("unavailable", {
|
|
962
|
+
hasSessions: sessions !== void 0,
|
|
963
|
+
hasConversation: conversation !== void 0
|
|
964
|
+
});
|
|
965
|
+
return {
|
|
966
|
+
ok: false,
|
|
967
|
+
reason: "unavailable"
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const sessionId = currentSessionId(sessions);
|
|
971
|
+
if (sessionId === void 0 || sessionId === null || sessionId === "") {
|
|
972
|
+
trace("no-session", { known: countSessions(sessions) });
|
|
973
|
+
return {
|
|
974
|
+
ok: false,
|
|
975
|
+
reason: "no-session"
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
const actx = resolveSessionScope(sessions, sessionId);
|
|
979
|
+
if (actx === void 0 || actx === null) {
|
|
980
|
+
trace("no-scope", { sessionId });
|
|
981
|
+
return {
|
|
982
|
+
ok: false,
|
|
983
|
+
reason: "no-session"
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
let input;
|
|
987
|
+
try {
|
|
988
|
+
input = conversation.input.for(actx);
|
|
989
|
+
} catch (err) {
|
|
990
|
+
trace("for() threw", err instanceof Error ? err.message : err);
|
|
991
|
+
input = void 0;
|
|
992
|
+
}
|
|
993
|
+
if (input === void 0 || input === null || typeof input.insertReference !== "function") {
|
|
994
|
+
trace("unavailable", { hasInput: input !== void 0 && input !== null });
|
|
995
|
+
return {
|
|
996
|
+
ok: false,
|
|
997
|
+
reason: "unavailable"
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
trace("inserting", {
|
|
1001
|
+
label: insert.label,
|
|
1002
|
+
clipboardText: insert.clipboardText
|
|
1003
|
+
});
|
|
1004
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1005
|
+
const state = input.state?.getSnapshot() ?? {};
|
|
1006
|
+
if (hasSchematicChip(state)) {
|
|
1007
|
+
trace("already-present");
|
|
1008
|
+
return {
|
|
1009
|
+
ok: false,
|
|
1010
|
+
reason: "already-present"
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
const phase = state.phase ?? "plain";
|
|
1014
|
+
if (BUSY_PHASES.includes(phase)) {
|
|
1015
|
+
trace("busy", { phase });
|
|
1016
|
+
return {
|
|
1017
|
+
ok: false,
|
|
1018
|
+
reason: "busy"
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
const draftRev = typeof state.draftRev === "number" ? state.draftRev : 0;
|
|
1022
|
+
const end = detectLengthOf(state);
|
|
1023
|
+
let applied = false;
|
|
1024
|
+
try {
|
|
1025
|
+
applied = input.insertReference(insert, {
|
|
1026
|
+
start: end,
|
|
1027
|
+
end,
|
|
1028
|
+
draftRev
|
|
1029
|
+
});
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
trace("insertReference threw", err instanceof Error ? err.message : err);
|
|
1032
|
+
applied = false;
|
|
1033
|
+
}
|
|
1034
|
+
if (applied) {
|
|
1035
|
+
trace("inserted", {
|
|
1036
|
+
label: insert.label,
|
|
1037
|
+
span: {
|
|
1038
|
+
start: end,
|
|
1039
|
+
end,
|
|
1040
|
+
draftRev
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
try {
|
|
1044
|
+
input.focus?.();
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
trace("focus threw", err instanceof Error ? err.message : err);
|
|
1047
|
+
}
|
|
1048
|
+
return { ok: true };
|
|
1049
|
+
}
|
|
1050
|
+
trace("cas-miss", { attempt });
|
|
1051
|
+
}
|
|
1052
|
+
trace("conflict");
|
|
1053
|
+
return {
|
|
1054
|
+
ok: false,
|
|
1055
|
+
reason: "conflict"
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
275
1058
|
//#endregion
|
|
276
1059
|
//#region src/client/index.ts
|
|
277
1060
|
/** Harness compacts finished tool calls; keep only our delivery card visible. */
|
|
@@ -461,22 +1244,80 @@
|
|
|
461
1244
|
}, busy ? "处理中…" : "放置到编辑器") : null, h("span", { style: { fontSize: 12 } }, `${result.fileCount} 个工程和库文件`)), result.status !== "ok" ? h("p", null, "存在未渲染页面或预览警告,请结合工具结果检查。") : null, message ? h("p", { role: "alert" }, message) : null);
|
|
462
1245
|
}
|
|
463
1246
|
return {
|
|
464
|
-
inject: ["slots"],
|
|
1247
|
+
inject: ["slots", "inputTriggers"],
|
|
465
1248
|
apply: (ctx) => {
|
|
466
1249
|
getHost = () => ctx.get?.("hqEdge");
|
|
467
1250
|
const stopVisibility = keepDeliveryVisible();
|
|
468
|
-
const
|
|
1251
|
+
const source = registerSchematicGenSourceWhenReady(() => ctx.get?.("inputTriggers"));
|
|
1252
|
+
try {
|
|
1253
|
+
console.debug("[schematic-gen:client] apply", {
|
|
1254
|
+
inputTriggers: source.registered(),
|
|
1255
|
+
slots: typeof ctx.slots?.inject === "function"
|
|
1256
|
+
});
|
|
1257
|
+
} catch {}
|
|
1258
|
+
const chipDeps = {
|
|
1259
|
+
getConversation: () => ctx.get?.("conversation"),
|
|
1260
|
+
getSessions: () => ctx.get?.("sessions")
|
|
1261
|
+
};
|
|
1262
|
+
const stopEntry = mountSchematicGenSidebarEntry({
|
|
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
|
+
}
|
|
1274
|
+
const result = insertSchematicChip(chipDeps);
|
|
1275
|
+
try {
|
|
1276
|
+
console.debug("[schematic-gen:chip] click", result);
|
|
1277
|
+
} catch {}
|
|
1278
|
+
if (result.ok || result.reason === "already-present") return;
|
|
1279
|
+
notifyComposer(ctx, result.reason);
|
|
1280
|
+
},
|
|
1281
|
+
position: "after"
|
|
1282
|
+
});
|
|
1283
|
+
try {
|
|
1284
|
+
console.debug("[schematic-gen:client] sidebar row", { rows: document.querySelectorAll("[data-hqcg-schematic-entry]").length });
|
|
1285
|
+
} catch {}
|
|
469
1286
|
const dispose = ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
|
|
470
1287
|
name: "tool.call.toolview",
|
|
471
1288
|
key: "render_kicad_module_preview"
|
|
472
1289
|
}, Card));
|
|
473
1290
|
ctx.effect?.(() => () => {
|
|
474
1291
|
stopVisibility();
|
|
475
|
-
|
|
1292
|
+
source.dispose();
|
|
1293
|
+
stopEntry();
|
|
476
1294
|
if (typeof dispose === "function") dispose();
|
|
477
1295
|
});
|
|
478
1296
|
}
|
|
479
1297
|
};
|
|
1298
|
+
/** Surface why a chip insertion did not happen, through the composer's own notice channel. Best-effort by design. */
|
|
1299
|
+
function notifyComposer(context, reason) {
|
|
1300
|
+
try {
|
|
1301
|
+
const sessions = context.get?.("sessions");
|
|
1302
|
+
const conversation = context.get?.("conversation");
|
|
1303
|
+
if (sessions === void 0 || conversation?.input === void 0) return;
|
|
1304
|
+
const sessionId = currentSessionId(sessions);
|
|
1305
|
+
if (!sessionId) return;
|
|
1306
|
+
const actx = resolveSessionScope(sessions, sessionId);
|
|
1307
|
+
if (actx === void 0 || actx === null) return;
|
|
1308
|
+
conversation.input.for(actx)?.notify?.("info", noticeFor(reason));
|
|
1309
|
+
} catch {}
|
|
1310
|
+
}
|
|
1311
|
+
/** Human-readable notice text for one insertion failure reason. */
|
|
1312
|
+
function noticeFor(reason) {
|
|
1313
|
+
switch (reason) {
|
|
1314
|
+
case "no-session": return "Open a conversation first, then click Schematic Gen again.";
|
|
1315
|
+
case "busy": return "The composer is busy — try again once the current message settles.";
|
|
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.";
|
|
1318
|
+
default: return "Schematic Gen is unavailable in this session.";
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
480
1321
|
}
|
|
481
1322
|
});
|
|
482
1323
|
//#endregion
|