@huaqiu/dsh-eda-selection 0.0.0-stage → 0.5.7

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 深圳华秋智联股份有限公司
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ # DSH bundle patch: inserts the ambient selection plugin.
2
+ #
3
+ # The browser half registers a `conversation.input.dock` entry and the node
4
+ # half registers a `systemPrompt` section, so the entry inject must hold the
5
+ # plugin until both surfaces exist.
6
+ - insert:
7
+ - id: huaqiu-eda-selection
8
+ name: '@huaqiu/dsh-eda-selection'
9
+ inject: ['hqEdge', 'systemPrompt']
package/lib/client.js ADDED
@@ -0,0 +1,357 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@huaqiu/dsh-eda-selection",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ //#region src/client/chip-text.ts
9
+ /** Fallback translator: the key itself, so a missing dictionary is never blank. */
10
+ const identityTranslate = (key) => key;
11
+ /** Editor display name for one editor identity. */
12
+ function editorLabel(editor, t = identityTranslate) {
13
+ switch (editor) {
14
+ case "PCB": return t("editor.pcb");
15
+ case "SCHEMATIC": return t("editor.schematic");
16
+ default: return t("editor.unspecified");
17
+ }
18
+ }
19
+ /**
20
+ * `"{count} items"` with the singular form honoured for one item.
21
+ *
22
+ * Both languages ship a dedicated singular key, so English reads "1 item"
23
+ * rather than "1 items".
24
+ */
25
+ function itemCountLabel(count, t = identityTranslate) {
26
+ return count === 1 ? t("chip.item") : t("chip.items", { count });
27
+ }
28
+ /**
29
+ * The bounded, comma-joined label suffix, or `''` when there is nothing to show.
30
+ *
31
+ * The separator is a plain comma plus a space rather than a locale-specific
32
+ * list separator: the chip is a compact ambient indicator, and the host's
33
+ * labels are identifiers (R1, U3, …) that read the same in both languages.
34
+ */
35
+ function labelSuffix(labels) {
36
+ return labels.slice(0, 3).join(", ");
37
+ }
38
+ /**
39
+ * The full chip text, e.g. `Current selection · 3 items (PCB): R1, R2, R3`.
40
+ *
41
+ * Returns `''` for anything that must NOT be rendered — an unknown state
42
+ * (`null`) or a known-empty selection. One gate for both, so the chip and the
43
+ * prompt section can never disagree about when the selection exists.
44
+ */
45
+ function formatSelectionChip(snapshot, t = identityTranslate) {
46
+ if (snapshot === null || !snapshot.hasSelection || snapshot.itemCount <= 0) return "";
47
+ const parts = [
48
+ t("chip.label"),
49
+ t("chip.sep"),
50
+ itemCountLabel(snapshot.itemCount, t)
51
+ ];
52
+ if (snapshot.editor !== "UNSPECIFIED") parts.push(`(${editorLabel(snapshot.editor, t)})`);
53
+ const labels = labelSuffix(snapshot.labels);
54
+ return labels.length > 0 ? `${parts.join(" ")}${t("chip.labels", { labels })}` : parts.join(" ");
55
+ }
56
+ /** Screen-reader label for the chip. */
57
+ function selectionChipAriaLabel(snapshot, t = identityTranslate) {
58
+ if (snapshot === null || !snapshot.hasSelection || snapshot.itemCount <= 0) return "";
59
+ return t("chip.aria", { count: snapshot.itemCount });
60
+ }
61
+ //#endregion
62
+ //#region src/client/SelectionChip.tsx
63
+ /**
64
+ * The ambient `Current selection` chip — the browser half's only visible
65
+ * surface.
66
+ *
67
+ * It is a passive indicator: it renders what the host published and nothing
68
+ * else. There is no click action (the agent reaches the real selection through
69
+ * the existing EDA tools), no expand affordance, and no per-item type claim.
70
+ *
71
+ * ── When it renders ─────────────────────────────────────────────────────────
72
+ * Only for a snapshot with `hasSelection && itemCount > 0`. Both UNKNOWN
73
+ * (`null` — nothing published yet, or the sync failed) and KNOWN-EMPTY
74
+ * (`hasSelection: false` with a real revision) render NOTHING. Rendering
75
+ * either of them would put a chip on screen that the host does not back.
76
+ *
77
+ * @module
78
+ */
79
+ const ROOT = {
80
+ display: "inline-flex",
81
+ alignItems: "center",
82
+ gap: 6,
83
+ alignSelf: "flex-start",
84
+ maxWidth: "100%",
85
+ padding: "3px 10px",
86
+ borderRadius: 999,
87
+ boxSizing: "border-box",
88
+ fontSize: 12,
89
+ lineHeight: "18px",
90
+ color: "var(--dsw-alias-label-secondary)",
91
+ background: "color-mix(in srgb, var(--dsw-alias-label-primary) 5%, transparent)",
92
+ border: "1px solid color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent)"
93
+ };
94
+ /** The leading dot — the only ornament, and it carries no meaning. */
95
+ const DOT = {
96
+ width: 6,
97
+ height: 6,
98
+ borderRadius: "50%",
99
+ flex: "0 0 auto",
100
+ background: "var(--dsw-alias-state-success, #34c759)"
101
+ };
102
+ const TEXT = {
103
+ overflow: "hidden",
104
+ textOverflow: "ellipsis",
105
+ whiteSpace: "nowrap"
106
+ };
107
+ /**
108
+ * Render the chip, or `null` when there is nothing grounded to show.
109
+ *
110
+ * Returning `null` (rather than an empty fragment) lets the dock skip the row
111
+ * entirely, so an absent selection costs no vertical space in the composer.
112
+ */
113
+ function SelectionChip({ snapshot, t }) {
114
+ const text = formatSelectionChip(snapshot, t);
115
+ if (text.length === 0) return null;
116
+ const aria = selectionChipAriaLabel(snapshot, t);
117
+ return (0, react.createElement)("div", {
118
+ style: ROOT,
119
+ "data-dsh-plugin": "huaqiu-eda-selection",
120
+ "data-dsh-part": "selection-chip",
121
+ "data-editor": snapshot?.editor ?? "UNSPECIFIED",
122
+ "data-revision": snapshot?.revision ?? "",
123
+ role: "status",
124
+ "aria-label": aria.length > 0 ? aria : void 0
125
+ }, (0, react.createElement)("span", {
126
+ style: DOT,
127
+ "aria-hidden": true
128
+ }), (0, react.createElement)("span", { style: TEXT }, text));
129
+ }
130
+ //#endregion
131
+ //#region src/client/locales.ts
132
+ /**
133
+ * zh / en copy for the ambient `Current selection` dock entry.
134
+ *
135
+ * Chinese is the source of truth: `EdaSelectionKey` is derived from it and
136
+ * `en` is typed against it, so a key added to one language without the other
137
+ * is a COMPILE error rather than a silent Chinese fallback at runtime.
138
+ *
139
+ * Kept in its own module (rather than inlined in the component) so the copy is
140
+ * reviewable on its own and the test file can assert bilingual balance
141
+ * directly, mirroring `@huaqiu/dsh-auth` / `@huaqiu/dsh-tool-schematic-gen`.
142
+ */
143
+ const zh = {
144
+ /** Dock-entry label shown wherever the owner projects one. */
145
+ "dock.label": "当前选中",
146
+ /** The chip itself: "Current selection · 3 items (PCB)". */
147
+ "chip.label": "当前选中",
148
+ "chip.sep": "·",
149
+ /** Editor names, spelled for a human reader. */
150
+ "editor.pcb": "PCB",
151
+ "editor.schematic": "原理图",
152
+ "editor.unspecified": "编辑器",
153
+ /** "{count} items" / "{count} 项". */
154
+ "chip.items": "{count} 项",
155
+ /** One item — English needs the singular form. */
156
+ "chip.item": "1 项",
157
+ /** Screen-reader label for the chip. */
158
+ "chip.aria": "当前编辑器选中 {count} 项",
159
+ /** Bounded label list, shown after the count when the host supplied any. */
160
+ "chip.labels": ":{labels}"
161
+ };
162
+ const en = {
163
+ "dock.label": "Current selection",
164
+ "chip.label": "Current selection",
165
+ "chip.sep": "·",
166
+ "editor.pcb": "PCB",
167
+ "editor.schematic": "Schematic",
168
+ "editor.unspecified": "editor",
169
+ "chip.items": "{count} items",
170
+ "chip.item": "1 item",
171
+ "chip.aria": "Current editor selection: {count} items",
172
+ "chip.labels": ": {labels}"
173
+ };
174
+ //#endregion
175
+ //#region src/client/hq-edge.ts
176
+ /**
177
+ * Normalize one raw summary into {@link SelectionSnapshot}, or `null` when it
178
+ * carries no usable state.
179
+ *
180
+ * The bridge already normalizes, but this plugin can be pointed at a host
181
+ * service that predates the selection face (or at a hand-rolled test double),
182
+ * so the same defensive rules are applied again here:
183
+ *
184
+ * - null / undefined / non-object → `null` (unknown)
185
+ * - `hasSelection` not a boolean → `null` (cannot tell selected from not)
186
+ * - unknown editor name → `UNSPECIFIED`
187
+ * - non-string labels are dropped; the array is copied
188
+ *
189
+ * A well-formed `hasSelection: false` is KEPT: that is a known-empty state,
190
+ * and dropping it would erase the distinction the whole design rests on.
191
+ *
192
+ * @param raw - value read from `hqEdge.selection.getCurrent()`.
193
+ * @returns the normalized snapshot, or `null` for unknown/malformed.
194
+ */
195
+ function normalizeSelectionSnapshot(raw) {
196
+ if (raw === null || raw === void 0) return null;
197
+ if (typeof raw !== "object" || Array.isArray(raw)) return null;
198
+ const record = raw;
199
+ if (typeof record.hasSelection !== "boolean") return null;
200
+ const itemCount = typeof record.itemCount === "number" && Number.isFinite(record.itemCount) && record.itemCount > 0 ? Math.floor(record.itemCount) : 0;
201
+ const editor = record.editor;
202
+ const knownEditor = editor === "SCHEMATIC" || editor === "PCB" ? editor : "UNSPECIFIED";
203
+ const labels = Array.isArray(record.labels) ? record.labels.filter((label) => typeof label === "string") : [];
204
+ const revision = typeof record.revision === "string" ? record.revision : typeof record.revision === "number" && Number.isFinite(record.revision) ? String(record.revision) : "";
205
+ return {
206
+ hasSelection: record.hasSelection,
207
+ itemCount,
208
+ editor: knownEditor,
209
+ labels,
210
+ revision
211
+ };
212
+ }
213
+ //#endregion
214
+ //#region src/client/index.ts
215
+ /**
216
+ * `@huaqiu/dsh-eda-selection` — browser half.
217
+ *
218
+ * Contributes ONE entry to the public `conversation.input.dock` slot: an
219
+ * ambient `Current selection · N items (editor)` chip that appears above the
220
+ * composer whenever the FOCUSED KiCad editor has a selection, and disappears
221
+ * the moment it does not.
222
+ *
223
+ * ── Where the state comes from ──────────────────────────────────────────────
224
+ * The `hqEdge` host service provided by `@hqedge/dsh-edge-bridge`. Its
225
+ * `selection` face is fed by the bridge's OWN `/runtime/dsh` poll, so this
226
+ * plugin adds NO second polling loop and never talks to HQ Edge itself.
227
+ *
228
+ * `inject` declares `hqEdge`, so DSH holds this plugin's activation until the
229
+ * bridge has provided it — `ctx.get('hqEdge')` inside `apply()` therefore
230
+ * resolves. The accessor is still built lazily (a closure, not a captured
231
+ * value) because the service object is created once but its `selection` face
232
+ * is read on every subscription.
233
+ *
234
+ * The bridge's `onChanged` replays the current state immediately when one is
235
+ * already known, so a chip mounted after the first summary does not wait for a
236
+ * change; and it returns a disposer, which is what the slot's own lifecycle
237
+ * expects.
238
+ *
239
+ * ── Unknown vs known-empty ──────────────────────────────────────────────────
240
+ * `hqEdge.selection.getCurrent()` returns `null` when the state is UNKNOWN
241
+ * (nothing published yet, or the sync failed). That is NOT a selection, and
242
+ * the chip renders nothing for it — a chip must never appear on screen that
243
+ * the host does not currently back.
244
+ *
245
+ * ── No second composer ──────────────────────────────────────────────────────
246
+ * This entry never touches the draft, the input state, or the reference
247
+ * pipeline. It is a read-only projection of host state; the agent reaches the
248
+ * real selection through the existing EDA selection tools.
249
+ *
250
+ * @module @huaqiu/dsh-eda-selection/client
251
+ */
252
+ const inject = [
253
+ "slots",
254
+ "locale",
255
+ "hqEdge"
256
+ ];
257
+ /** This plugin's dictionary namespace. */
258
+ const EDA_SELECTION_NS = "huaqiu:eda-selection";
259
+ /**
260
+ * Resolve the `hqEdge` service defensively.
261
+ *
262
+ * `inject` already guarantees it exists by `apply()` time; the try/catch and
263
+ * the shape checks keep a standalone DSH (no edge-bridge) from taking the
264
+ * plugin down. In that case the chip simply never renders.
265
+ */
266
+ function getHqEdge(ctx) {
267
+ try {
268
+ const edge = ctx.get("hqEdge");
269
+ if (!edge || !edge.selection || typeof edge.selection.getCurrent !== "function") return void 0;
270
+ return edge;
271
+ } catch {
272
+ return;
273
+ }
274
+ }
275
+ /**
276
+ * Read and normalize the current snapshot from the bridge.
277
+ *
278
+ * The bridge already normalizes; re-normalizing here keeps this plugin honest
279
+ * when pointed at an older bridge (or a test double) that does not.
280
+ */
281
+ function readSnapshot(edge) {
282
+ if (!edge) return null;
283
+ try {
284
+ return normalizeSelectionSnapshot(edge.selection?.getCurrent());
285
+ } catch {
286
+ return null;
287
+ }
288
+ }
289
+ /**
290
+ * The dock component: subscribes to the bridge and renders the chip.
291
+ *
292
+ * A hook rather than a plain read, because the snapshot arrives asynchronously
293
+ * (the bridge's first poll lands after this component mounts).
294
+ */
295
+ function SelectionDock({ t, getEdge }) {
296
+ const [snapshot, setSnapshot] = (0, react.useState)(null);
297
+ (0, react.useEffect)(() => {
298
+ const edge = getEdge();
299
+ if (!edge || typeof edge.selection?.onChanged !== "function") {
300
+ setSnapshot(null);
301
+ return;
302
+ }
303
+ setSnapshot(readSnapshot(edge));
304
+ let alive = true;
305
+ const dispose = edge.selection.onChanged((next) => {
306
+ if (!alive) return;
307
+ setSnapshot(normalizeSelectionSnapshot(next));
308
+ });
309
+ return () => {
310
+ alive = false;
311
+ dispose();
312
+ };
313
+ }, [getEdge]);
314
+ return (0, react.createElement)(SelectionChip, {
315
+ snapshot,
316
+ t
317
+ });
318
+ }
319
+ /**
320
+ * Install the dock entry on the running client context.
321
+ *
322
+ * Never throws: a deployment without the slot service, the locale service, or
323
+ * the `hqEdge` bridge must still load — it simply shows no chip.
324
+ */
325
+ function apply(ctx) {
326
+ const locale = ctx.locale;
327
+ if (locale && typeof locale.register === "function") try {
328
+ locale.register(EDA_SELECTION_NS, {
329
+ zh,
330
+ en
331
+ });
332
+ } catch {}
333
+ const t = locale && typeof locale.bind === "function" ? locale.bind(EDA_SELECTION_NS) : (key) => key;
334
+ const getEdge = () => getHqEdge(ctx);
335
+ const slots = ctx.slots;
336
+ if (!slots || typeof slots.inject !== "function" || typeof slots.register !== "function") return;
337
+ ctx.effect?.(() => slots.inject("conversation.input.dock", () => slots.register({
338
+ name: "conversation.input.dock",
339
+ id: "eda-selection",
340
+ order: 5,
341
+ label: () => t("dock.label"),
342
+ locale: EDA_SELECTION_NS
343
+ }, () => (0, react.createElement)(SelectionDock, {
344
+ t,
345
+ getEdge
346
+ }))), "huaqiu eda-selection: composer dock chip");
347
+ }
348
+ //#endregion
349
+ exports.EDA_SELECTION_NS = EDA_SELECTION_NS;
350
+ exports.SelectionDock = SelectionDock;
351
+ exports.apply = apply;
352
+ exports.inject = inject;
353
+ return module.exports;
354
+ }
355
+ });
356
+
357
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":["createElement","useState","createElement"],"sources":["../src/client/chip-text.ts","../src/client/SelectionChip.tsx","../src/client/locales.ts","../src/client/hq-edge.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Pure label formatting for the ambient `Current selection` chip.\n *\n * Deliberately free of React and of the cordis context so the text the user\n * reads is unit-testable on its own — the same call\n * `@huaqiu/dsh-tool-symbol-footprint` makes for its sidebar copy.\n *\n * ── Grounding rule ──────────────────────────────────────────────────────────\n * The chip may only state what the host actually reported:\n *\n * - the item COUNT (`item_count`) — never inferred from `labels.length`,\n * because the host truncates the label list to a small bound;\n * - the editor identity (`editor`), and only when it is one we model;\n * - a BOUNDED label list, explicitly introduced as a partial view.\n *\n * Anything else — \"the selected footprint\", \"net N$12\", a guessed type — would\n * be a claim the summary does not support, and the agent has the real entity\n * ids one tool call away.\n *\n * @module\n */\nimport type { SelectionEditorName, SelectionSnapshot } from './hq-edge.js'\n\n/** Translation function shape (the locale service's `bind` result). */\nexport type Translate = (key: string, params?: Record<string, unknown>) => string\n\n/** Fallback translator: the key itself, so a missing dictionary is never blank. */\nexport const identityTranslate: Translate = (key) => key\n\n/** Editor display name for one editor identity. */\nexport function editorLabel(\n editor: SelectionEditorName,\n t: Translate = identityTranslate,\n): string {\n switch (editor) {\n case 'PCB':\n return t('editor.pcb')\n case 'SCHEMATIC':\n return t('editor.schematic')\n default:\n return t('editor.unspecified')\n }\n}\n\n/**\n * `\"{count} items\"` with the singular form honoured for one item.\n *\n * Both languages ship a dedicated singular key, so English reads \"1 item\"\n * rather than \"1 items\".\n */\nexport function itemCountLabel(count: number, t: Translate = identityTranslate): string {\n return count === 1 ? t('chip.item') : t('chip.items', { count })\n}\n\n/**\n * How many labels the chip may append after the count.\n *\n * The host already bounds the list (8 entries × 64 chars), but the chip is one\n * line of ambient UI, so it shows fewer than the host is willing to send.\n */\nexport const MAX_CHIP_LABELS = 3\n\n/**\n * The bounded, comma-joined label suffix, or `''` when there is nothing to show.\n *\n * The separator is a plain comma plus a space rather than a locale-specific\n * list separator: the chip is a compact ambient indicator, and the host's\n * labels are identifiers (R1, U3, …) that read the same in both languages.\n */\nexport function labelSuffix(labels: readonly string[]): string {\n return labels.slice(0, MAX_CHIP_LABELS).join(', ')\n}\n\n/**\n * The full chip text, e.g. `Current selection · 3 items (PCB): R1, R2, R3`.\n *\n * Returns `''` for anything that must NOT be rendered — an unknown state\n * (`null`) or a known-empty selection. One gate for both, so the chip and the\n * prompt section can never disagree about when the selection exists.\n */\nexport function formatSelectionChip(\n snapshot: SelectionSnapshot | null,\n t: Translate = identityTranslate,\n): string {\n if (snapshot === null || !snapshot.hasSelection || snapshot.itemCount <= 0) return ''\n\n const parts: string[] = [t('chip.label'), t('chip.sep'), itemCountLabel(snapshot.itemCount, t)]\n\n // The editor is parenthesised only when it is one we model; UNSPECIFIED\n // means the host has not identified a focused editor, and \"(editor)\" would\n // be noise.\n if (snapshot.editor !== 'UNSPECIFIED') {\n parts.push(`(${editorLabel(snapshot.editor, t)})`)\n }\n\n const labels = labelSuffix(snapshot.labels)\n\n // Appended WITHOUT a joining space: the separator lives inside the localized\n // string (`': {labels}'`), and a space before a colon reads wrong in both\n // languages.\n return labels.length > 0 ? `${parts.join(' ')}${t('chip.labels', { labels })}` : parts.join(' ')\n}\n\n/** Screen-reader label for the chip. */\nexport function selectionChipAriaLabel(\n snapshot: SelectionSnapshot | null,\n t: Translate = identityTranslate,\n): string {\n if (snapshot === null || !snapshot.hasSelection || snapshot.itemCount <= 0) return ''\n return t('chip.aria', { count: snapshot.itemCount })\n}\n","/**\n * The ambient `Current selection` chip — the browser half's only visible\n * surface.\n *\n * It is a passive indicator: it renders what the host published and nothing\n * else. There is no click action (the agent reaches the real selection through\n * the existing EDA tools), no expand affordance, and no per-item type claim.\n *\n * ── When it renders ─────────────────────────────────────────────────────────\n * Only for a snapshot with `hasSelection && itemCount > 0`. Both UNKNOWN\n * (`null` — nothing published yet, or the sync failed) and KNOWN-EMPTY\n * (`hasSelection: false` with a real revision) render NOTHING. Rendering\n * either of them would put a chip on screen that the host does not back.\n *\n * @module\n */\nimport { createElement, type CSSProperties, type ReactElement } from 'react'\nimport type { SelectionSnapshot } from './hq-edge.js'\nimport {\n formatSelectionChip,\n selectionChipAriaLabel,\n type Translate,\n} from './chip-text.js'\n\nconst ROOT: CSSProperties = {\n display: 'inline-flex',\n alignItems: 'center',\n gap: 6,\n // The dock is full-width; the chip itself is only as wide as its text so it\n // reads as an ambient tag rather than a toolbar.\n alignSelf: 'flex-start',\n maxWidth: '100%',\n padding: '3px 10px',\n borderRadius: 999,\n boxSizing: 'border-box',\n fontSize: 12,\n lineHeight: '18px',\n color: 'var(--dsw-alias-label-secondary)',\n background: 'color-mix(in srgb, var(--dsw-alias-label-primary) 5%, transparent)',\n border: '1px solid color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent)',\n}\n\n/** The leading dot — the only ornament, and it carries no meaning. */\nconst DOT: CSSProperties = {\n width: 6,\n height: 6,\n borderRadius: '50%',\n flex: '0 0 auto',\n background: 'var(--dsw-alias-state-success, #34c759)',\n}\n\nconst TEXT: CSSProperties = {\n // One line, ellipsised. The label list is host-bounded and already short.\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n}\n\nexport interface SelectionChipProps {\n /** The latest snapshot, or `null` when the state is unknown. */\n snapshot: SelectionSnapshot | null\n /** Bound dictionary (the entry's locale seat). */\n t: Translate\n}\n\n/**\n * Render the chip, or `null` when there is nothing grounded to show.\n *\n * Returning `null` (rather than an empty fragment) lets the dock skip the row\n * entirely, so an absent selection costs no vertical space in the composer.\n */\nexport function SelectionChip({ snapshot, t }: SelectionChipProps): ReactElement | null {\n const text = formatSelectionChip(snapshot, t)\n\n if (text.length === 0) return null\n\n const aria = selectionChipAriaLabel(snapshot, t)\n\n return createElement(\n 'div',\n {\n style: ROOT,\n // `data-*` markers so the entry can be identified in tests and by the\n // host's own diagnostics without depending on the copy.\n 'data-dsh-plugin': 'huaqiu-eda-selection',\n 'data-dsh-part': 'selection-chip',\n 'data-editor': snapshot?.editor ?? 'UNSPECIFIED',\n 'data-revision': snapshot?.revision ?? '',\n role: 'status',\n 'aria-label': aria.length > 0 ? aria : undefined,\n },\n createElement('span', { style: DOT, 'aria-hidden': true }),\n createElement('span', { style: TEXT }, text),\n )\n}\n","/**\n * zh / en copy for the ambient `Current selection` dock entry.\n *\n * Chinese is the source of truth: `EdaSelectionKey` is derived from it and\n * `en` is typed against it, so a key added to one language without the other\n * is a COMPILE error rather than a silent Chinese fallback at runtime.\n *\n * Kept in its own module (rather than inlined in the component) so the copy is\n * reviewable on its own and the test file can assert bilingual balance\n * directly, mirroring `@huaqiu/dsh-auth` / `@huaqiu/dsh-tool-schematic-gen`.\n */\n\nexport const zh = {\n /** Dock-entry label shown wherever the owner projects one. */\n 'dock.label': '当前选中',\n /** The chip itself: \"Current selection · 3 items (PCB)\". */\n 'chip.label': '当前选中',\n 'chip.sep': '·',\n /** Editor names, spelled for a human reader. */\n 'editor.pcb': 'PCB',\n 'editor.schematic': '原理图',\n 'editor.unspecified': '编辑器',\n /** \"{count} items\" / \"{count} 项\". */\n 'chip.items': '{count} 项',\n /** One item — English needs the singular form. */\n 'chip.item': '1 项',\n /** Screen-reader label for the chip. */\n 'chip.aria': '当前编辑器选中 {count} 项',\n /** Bounded label list, shown after the count when the host supplied any. */\n 'chip.labels': ':{labels}',\n} as const\n\nexport type EdaSelectionKey = keyof typeof zh\n\nexport const en: Record<EdaSelectionKey, string> = {\n 'dock.label': 'Current selection',\n 'chip.label': 'Current selection',\n 'chip.sep': '·',\n 'editor.pcb': 'PCB',\n 'editor.schematic': 'Schematic',\n 'editor.unspecified': 'editor',\n 'chip.items': '{count} items',\n 'chip.item': '1 item',\n 'chip.aria': 'Current editor selection: {count} items',\n 'chip.labels': ': {labels}',\n}\n","/**\n * Structural view of the `hqEdge` host service's `selection` face.\n *\n * Declared structurally and NEVER imported from `@hqedge/dsh-edge-bridge`:\n * that package is a DSH host plugin (its root export is a cordis entry), so\n * importing it here would pull a plugin into a plugin. The contract is instead\n * pinned by the edge-bridge's own unit tests, which assert the exact same\n * field names and semantics this interface describes.\n *\n * Both halves of the bridge publish this shape, and both normalise it through\n * the same `normalizeSelectionSnapshot` rules — so a browser-side chip and a\n * node-side prompt provider reading the same snapshot always agree.\n *\n * @module\n */\n\n/** Editor identity, as an enum NAME (never the wire number). */\nexport type SelectionEditorName = 'UNSPECIFIED' | 'SCHEMATIC' | 'PCB'\n\n/**\n * One selection summary.\n *\n * `revision` is a STRING: protobuf uint64 crosses the HTTP boundary as bigint,\n * and `JSON.stringify` throws on bigint, so hq-edge serialises it as a string.\n */\nexport interface SelectionSnapshot {\n /** Whether the FOCUSED editor currently has a selection. */\n hasSelection: boolean\n /** Number of selected items. Zero when `hasSelection` is false. */\n itemCount: number\n /** Which editor the selection belongs to. */\n editor: SelectionEditorName\n /**\n * Bounded, display-only labels (reference designators, symbol names, …).\n *\n * Host-truncated to a small fixed count and per-label length. Treat as a\n * PARTIAL view: never infer `itemCount` from `labels.length`, and never\n * claim these are all the selected items.\n */\n labels: string[]\n /** Monotonic per-host revision. Empty before the first published summary. */\n revision: string\n}\n\n/** The `selection` face of the `hqEdge` host service. */\nexport interface HqEdgeSelection {\n /**\n * The latest known selection summary, or `null` when UNKNOWN.\n *\n * `null` means \"nothing has been published yet, or the sync failed\" — which\n * is deliberately NOT the same thing as a known-empty selection. A consumer\n * must render nothing for either, but must never treat `null` as\n * \"definitely nothing selected\".\n */\n getCurrent(): SelectionSnapshot | null\n /**\n * Subscribe to selection changes. Fires immediately with the current state\n * when one is already known.\n * @returns a disposer removing exactly this listener.\n */\n onChanged(listener: (selection: SelectionSnapshot | null) => void): () => void\n}\n\n/** The slice of the `hqEdge` service this plugin consumes. */\nexport interface HqEdgeLike {\n selection?: HqEdgeSelection\n}\n\n/**\n * Normalize one raw summary into {@link SelectionSnapshot}, or `null` when it\n * carries no usable state.\n *\n * The bridge already normalizes, but this plugin can be pointed at a host\n * service that predates the selection face (or at a hand-rolled test double),\n * so the same defensive rules are applied again here:\n *\n * - null / undefined / non-object → `null` (unknown)\n * - `hasSelection` not a boolean → `null` (cannot tell selected from not)\n * - unknown editor name → `UNSPECIFIED`\n * - non-string labels are dropped; the array is copied\n *\n * A well-formed `hasSelection: false` is KEPT: that is a known-empty state,\n * and dropping it would erase the distinction the whole design rests on.\n *\n * @param raw - value read from `hqEdge.selection.getCurrent()`.\n * @returns the normalized snapshot, or `null` for unknown/malformed.\n */\nexport function normalizeSelectionSnapshot(raw: unknown): SelectionSnapshot | null {\n if (raw === null || raw === undefined) return null\n if (typeof raw !== 'object' || Array.isArray(raw)) return null\n\n const record = raw as Record<string, unknown>\n\n if (typeof record.hasSelection !== 'boolean') return null\n\n const itemCount =\n typeof record.itemCount === 'number' && Number.isFinite(record.itemCount) && record.itemCount > 0\n ? Math.floor(record.itemCount)\n : 0\n\n const editor = record.editor\n const knownEditor: SelectionEditorName =\n editor === 'SCHEMATIC' || editor === 'PCB' ? editor : 'UNSPECIFIED'\n\n const labels = Array.isArray(record.labels)\n ? record.labels.filter((label): label is string => typeof label === 'string')\n : []\n\n const revision =\n typeof record.revision === 'string'\n ? record.revision\n : typeof record.revision === 'number' && Number.isFinite(record.revision)\n ? String(record.revision)\n : ''\n\n return { hasSelection: record.hasSelection, itemCount, editor: knownEditor, labels, revision }\n}\n\n/**\n * Whether a snapshot is worth surfacing at all.\n *\n * Both the chip and the prompt section use this single gate, so the two can\n * never disagree about when the selection \"exists\". Unknown (`null`) and\n * known-empty both answer \"no\" — but for different reasons, and the chip keeps\n * them apart internally so a later transition is not swallowed.\n */\nexport function hasVisibleSelection(snapshot: SelectionSnapshot | null): boolean {\n return snapshot !== null && snapshot.hasSelection && snapshot.itemCount > 0\n}\n","/**\n * `@huaqiu/dsh-eda-selection` — browser half.\n *\n * Contributes ONE entry to the public `conversation.input.dock` slot: an\n * ambient `Current selection · N items (editor)` chip that appears above the\n * composer whenever the FOCUSED KiCad editor has a selection, and disappears\n * the moment it does not.\n *\n * ── Where the state comes from ──────────────────────────────────────────────\n * The `hqEdge` host service provided by `@hqedge/dsh-edge-bridge`. Its\n * `selection` face is fed by the bridge's OWN `/runtime/dsh` poll, so this\n * plugin adds NO second polling loop and never talks to HQ Edge itself.\n *\n * `inject` declares `hqEdge`, so DSH holds this plugin's activation until the\n * bridge has provided it — `ctx.get('hqEdge')` inside `apply()` therefore\n * resolves. The accessor is still built lazily (a closure, not a captured\n * value) because the service object is created once but its `selection` face\n * is read on every subscription.\n *\n * The bridge's `onChanged` replays the current state immediately when one is\n * already known, so a chip mounted after the first summary does not wait for a\n * change; and it returns a disposer, which is what the slot's own lifecycle\n * expects.\n *\n * ── Unknown vs known-empty ──────────────────────────────────────────────────\n * `hqEdge.selection.getCurrent()` returns `null` when the state is UNKNOWN\n * (nothing published yet, or the sync failed). That is NOT a selection, and\n * the chip renders nothing for it — a chip must never appear on screen that\n * the host does not currently back.\n *\n * ── No second composer ──────────────────────────────────────────────────────\n * This entry never touches the draft, the input state, or the reference\n * pipeline. It is a read-only projection of host state; the agent reaches the\n * real selection through the existing EDA selection tools.\n *\n * @module @huaqiu/dsh-eda-selection/client\n */\nimport { createElement, useEffect, useState, type ReactElement } from 'react'\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport { SelectionChip } from './SelectionChip.js'\nimport { en, zh, type EdaSelectionKey } from './locales.js'\nimport {\n normalizeSelectionSnapshot,\n type HqEdgeLike,\n type SelectionSnapshot,\n} from './hq-edge.js'\n\nexport const inject = ['slots', 'locale', 'hqEdge'] as const\n\n/** This plugin's dictionary namespace. */\nexport const EDA_SELECTION_NS = 'huaqiu:eda-selection'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n 'huaqiu:eda-selection': EdaSelectionKey\n }\n}\n\n/**\n * Minimal structural client context (the DSH client runtime provides these\n * faces). Declared structurally, never imported, so the package stays\n * independently installable — the same pattern as\n * `@huaqiu/dsh-tool-schematic-gen` and `@huaqiu/dsh-huaqiu-about`.\n */\nexport interface ClientContext {\n slots?: {\n inject(key: string, callback: () => () => void): () => void\n register(\n spec: {\n name: string\n id?: string\n order?: number\n label?: () => string\n locale?: string\n },\n component: unknown,\n ): () => void\n }\n locale?: {\n register(namespace: string, dictionaries: unknown): unknown\n bind(namespace: string): (key: string, params?: Record<string, unknown>) => string\n }\n get<T = unknown>(name: string): T | undefined\n effect?(fn: () => (() => void) | void, label?: string): void\n}\n\n/** Translation function shape (the locale service's `bind` result). */\nexport type Translate = (key: string, params?: Record<string, unknown>) => string\n\n/**\n * Resolve the `hqEdge` service defensively.\n *\n * `inject` already guarantees it exists by `apply()` time; the try/catch and\n * the shape checks keep a standalone DSH (no edge-bridge) from taking the\n * plugin down. In that case the chip simply never renders.\n */\nfunction getHqEdge(ctx: ClientContext): HqEdgeLike | undefined {\n try {\n const edge = ctx.get<HqEdgeLike | undefined>('hqEdge')\n if (!edge || !edge.selection || typeof edge.selection.getCurrent !== 'function') return undefined\n return edge\n } catch {\n return undefined\n }\n}\n\n/**\n * Read and normalize the current snapshot from the bridge.\n *\n * The bridge already normalizes; re-normalizing here keeps this plugin honest\n * when pointed at an older bridge (or a test double) that does not.\n */\nfunction readSnapshot(edge: HqEdgeLike | undefined): SelectionSnapshot | null {\n if (!edge) return null\n try {\n return normalizeSelectionSnapshot(edge.selection?.getCurrent())\n } catch {\n return null\n }\n}\n\nexport interface SelectionDockProps {\n /** Bound dictionary, injected by the client entry. */\n t: Translate\n /** Lazy bridge accessor — a closure over `ctx`, never a captured value. */\n getEdge: () => HqEdgeLike | undefined\n}\n\n/**\n * The dock component: subscribes to the bridge and renders the chip.\n *\n * A hook rather than a plain read, because the snapshot arrives asynchronously\n * (the bridge's first poll lands after this component mounts).\n */\nexport function SelectionDock({ t, getEdge }: SelectionDockProps): ReactElement | null {\n const [snapshot, setSnapshot] = useState<SelectionSnapshot | null>(null)\n\n useEffect(() => {\n const edge = getEdge()\n\n // No bridge (standalone DSH) or a bridge without the selection face\n // (older edge-bridge): render nothing, permanently. Not an error.\n if (!edge || typeof edge.selection?.onChanged !== 'function') {\n setSnapshot(null)\n return\n }\n\n // `onChanged` replays the current state immediately when one is known, so\n // this read is only needed for the unknown-at-mount case.\n setSnapshot(readSnapshot(edge))\n\n let alive = true\n const dispose = edge.selection.onChanged((next) => {\n if (!alive) return\n // Normalize on the way in too: a listener must never be handed a shape\n // it did not expect, whatever the bridge did.\n setSnapshot(normalizeSelectionSnapshot(next))\n })\n\n return () => {\n alive = false\n dispose()\n }\n }, [getEdge])\n\n return createElement(SelectionChip, { snapshot, t })\n}\n\n/**\n * Install the dock entry on the running client context.\n *\n * Never throws: a deployment without the slot service, the locale service, or\n * the `hqEdge` bridge must still load — it simply shows no chip.\n */\nexport function apply(ctx: ClientContext): void {\n // Dictionaries first, so the first render already has localized text. The\n // untyped register form is used because this namespace is declared by this\n // package (not by DSH), which keeps DSH core untouched.\n const locale = ctx.locale\n if (locale && typeof locale.register === 'function') {\n try {\n locale.register(EDA_SELECTION_NS, { zh, en })\n } catch {\n // A duplicate registration (hot reload, double mount) is not fatal —\n // the first registration's dictionaries are already live.\n }\n }\n\n const t: Translate =\n locale && typeof locale.bind === 'function'\n ? locale.bind(EDA_SELECTION_NS)\n : (key: string) => key\n\n const getEdge = (): HqEdgeLike | undefined => getHqEdge(ctx)\n\n const slots = ctx.slots\n if (!slots || typeof slots.inject !== 'function' || typeof slots.register !== 'function') {\n return\n }\n\n ctx.effect?.(\n () =>\n slots.inject('conversation.input.dock', () =>\n slots.register(\n {\n name: 'conversation.input.dock',\n // Our own id: a fresh id is added BESIDE the shipped entries\n // (queue / todo / goal) instead of replacing one of them.\n id: 'eda-selection',\n // Ascending among the dock entries. `queue` is 20 and `todo` is 0,\n // so 5 puts the ambient chip near the top — closest to the composer\n // it describes — without displacing the todo dock at 0.\n order: 5,\n label: () => t('dock.label'),\n locale: EDA_SELECTION_NS,\n },\n () => createElement(SelectionDock, { t, getEdge }),\n ),\n ),\n 'huaqiu eda-selection: composer dock chip',\n )\n}\n"],"mappings":"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"}
@@ -0,0 +1,137 @@
1
+ //#region src/client/hq-edge.d.ts
2
+ /**
3
+ * Structural view of the `hqEdge` host service's `selection` face.
4
+ *
5
+ * Declared structurally and NEVER imported from `@hqedge/dsh-edge-bridge`:
6
+ * that package is a DSH host plugin (its root export is a cordis entry), so
7
+ * importing it here would pull a plugin into a plugin. The contract is instead
8
+ * pinned by the edge-bridge's own unit tests, which assert the exact same
9
+ * field names and semantics this interface describes.
10
+ *
11
+ * Both halves of the bridge publish this shape, and both normalise it through
12
+ * the same `normalizeSelectionSnapshot` rules — so a browser-side chip and a
13
+ * node-side prompt provider reading the same snapshot always agree.
14
+ *
15
+ * @module
16
+ */
17
+ /** Editor identity, as an enum NAME (never the wire number). */
18
+ type SelectionEditorName = 'UNSPECIFIED' | 'SCHEMATIC' | 'PCB';
19
+ /**
20
+ * One selection summary.
21
+ *
22
+ * `revision` is a STRING: protobuf uint64 crosses the HTTP boundary as bigint,
23
+ * and `JSON.stringify` throws on bigint, so hq-edge serialises it as a string.
24
+ */
25
+ interface SelectionSnapshot {
26
+ /** Whether the FOCUSED editor currently has a selection. */
27
+ hasSelection: boolean;
28
+ /** Number of selected items. Zero when `hasSelection` is false. */
29
+ itemCount: number;
30
+ /** Which editor the selection belongs to. */
31
+ editor: SelectionEditorName;
32
+ /**
33
+ * Bounded, display-only labels (reference designators, symbol names, …).
34
+ *
35
+ * Host-truncated to a small fixed count and per-label length. Treat as a
36
+ * PARTIAL view: never infer `itemCount` from `labels.length`, and never
37
+ * claim these are all the selected items.
38
+ */
39
+ labels: string[];
40
+ /** Monotonic per-host revision. Empty before the first published summary. */
41
+ revision: string;
42
+ }
43
+ /** The `selection` face of the `hqEdge` host service. */
44
+ interface HqEdgeSelection {
45
+ /**
46
+ * The latest known selection summary, or `null` when UNKNOWN.
47
+ *
48
+ * `null` means "nothing has been published yet, or the sync failed" — which
49
+ * is deliberately NOT the same thing as a known-empty selection. A consumer
50
+ * must render nothing for either, but must never treat `null` as
51
+ * "definitely nothing selected".
52
+ */
53
+ getCurrent(): SelectionSnapshot | null;
54
+ /**
55
+ * Subscribe to selection changes. Fires immediately with the current state
56
+ * when one is already known.
57
+ * @returns a disposer removing exactly this listener.
58
+ */
59
+ onChanged(listener: (selection: SelectionSnapshot | null) => void): () => void;
60
+ }
61
+ /** The slice of the `hqEdge` service this plugin consumes. */
62
+ interface HqEdgeLike {
63
+ selection?: HqEdgeSelection;
64
+ }
65
+ //#endregion
66
+ //#region src/prompt.d.ts
67
+ /** Unique prompt-section name. */
68
+ declare const EDA_SELECTION_SECTION = "eda:selection";
69
+ /**
70
+ * Centrally allocated placement: 590, between DSH's `PLAN_POLICY` (500) and
71
+ * `TEAM_POLICY` (600).
72
+ *
73
+ * That is where the sibling Huaqiu tool-policy section
74
+ * (`huaqiu:schematic-gen`) already sits, so both Huaqiu policy blocks are
75
+ * adjacent and both precede the tool-list sections (`TOOL_*` at 1000+) they
76
+ * refer to. `ctx.systemPrompt.getSectionOrder()` only resolves DSH's own named
77
+ * positions, so the number is pinned here and asserted by a test.
78
+ */
79
+ declare const EDA_SELECTION_SECTION_ORDER = 590;
80
+ /**
81
+ * Structural view of `ctx.systemPrompt` — only the face we use.
82
+ *
83
+ * `text` accepts a provider, which the registry re-evaluates at every
84
+ * assembly with that assembly's `AssembleContext`. Returning `''` contributes
85
+ * nothing: `renderPrompt` filters empty sections out before joining.
86
+ */
87
+ interface SystemPromptLike {
88
+ section(section: {
89
+ name: string;
90
+ order: number;
91
+ text: string | ((context: unknown) => string);
92
+ }): () => void;
93
+ }
94
+ /**
95
+ * Build the section text for one snapshot.
96
+ *
97
+ * Returns `''` for every "contribute nothing" case — an unknown state
98
+ * (`null`) or a known-empty selection. One gate for both, so the section and
99
+ * the browser chip can never disagree about when the selection exists.
100
+ *
101
+ * @param snapshot - the current summary, or `null` when unknown.
102
+ * @returns the section text, or `''` when it must contribute nothing.
103
+ */
104
+ declare function edaSelectionSectionText(snapshot: SelectionSnapshot | null): string;
105
+ /**
106
+ * Register the dynamic selection section.
107
+ *
108
+ * Never throws: a DSH deployment without the prompt service, or one where a
109
+ * duplicate registration races, must still load. A duplicate is the expected
110
+ * hot-reload case, so it is swallowed rather than surfaced.
111
+ *
112
+ * @param systemPrompt - `ctx.systemPrompt`, or undefined.
113
+ * @param readSelection - reads the current summary. Must not perform I/O.
114
+ * @returns the section's disposer, or `undefined` when nothing was registered.
115
+ */
116
+ declare function registerEdaSelectionSection(systemPrompt: unknown, readSelection: () => SelectionSnapshot | null): (() => void) | undefined;
117
+ //#endregion
118
+ //#region src/index.d.ts
119
+ /** Plugin id — matches package.json. */
120
+ declare const name = "@huaqiu/dsh-eda-selection";
121
+ /**
122
+ * Cordis services this half depends on.
123
+ *
124
+ * `hqEdge` is the bridge's node-side twin. It is declared here so DSH holds
125
+ * this plugin until the bridge has provided it, and every read is still
126
+ * guarded: a standalone DSH deployment without the bridge must not fail.
127
+ *
128
+ * `systemPrompt` carries the section. Declared for the same reason.
129
+ */
130
+ declare const inject: readonly ["hqEdge", "systemPrompt"];
131
+ /** Host plugin body — register the dynamic selection section. */
132
+ declare function apply(ctx: {
133
+ hqEdge?: HqEdgeLike;
134
+ systemPrompt?: SystemPromptLike;
135
+ }): () => void;
136
+ //#endregion
137
+ export { EDA_SELECTION_SECTION, EDA_SELECTION_SECTION_ORDER, type SystemPromptLike, apply, edaSelectionSectionText, inject, name, registerEdaSelectionSection };