@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 +21 -0
- package/cordis.patch.yml +9 -0
- package/lib/client.js +357 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.mts +137 -0
- package/lib/index.mjs +171 -0
- package/package.json +58 -3
- package/src/client/SelectionChip.tsx +95 -0
- package/src/client/chip-text.ts +111 -0
- package/src/client/hq-edge.ts +129 -0
- package/src/client/index.ts +222 -0
- package/src/client/locales.ts +46 -0
- package/src/index.ts +92 -0
- package/src/prompt.ts +127 -0
- package/README.md +0 -3
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/dsh-eda-selection` — browser half.
|
|
3
|
+
*
|
|
4
|
+
* Contributes ONE entry to the public `conversation.input.dock` slot: an
|
|
5
|
+
* ambient `Current selection · N items (editor)` chip that appears above the
|
|
6
|
+
* composer whenever the FOCUSED KiCad editor has a selection, and disappears
|
|
7
|
+
* the moment it does not.
|
|
8
|
+
*
|
|
9
|
+
* ── Where the state comes from ──────────────────────────────────────────────
|
|
10
|
+
* The `hqEdge` host service provided by `@hqedge/dsh-edge-bridge`. Its
|
|
11
|
+
* `selection` face is fed by the bridge's OWN `/runtime/dsh` poll, so this
|
|
12
|
+
* plugin adds NO second polling loop and never talks to HQ Edge itself.
|
|
13
|
+
*
|
|
14
|
+
* `inject` declares `hqEdge`, so DSH holds this plugin's activation until the
|
|
15
|
+
* bridge has provided it — `ctx.get('hqEdge')` inside `apply()` therefore
|
|
16
|
+
* resolves. The accessor is still built lazily (a closure, not a captured
|
|
17
|
+
* value) because the service object is created once but its `selection` face
|
|
18
|
+
* is read on every subscription.
|
|
19
|
+
*
|
|
20
|
+
* The bridge's `onChanged` replays the current state immediately when one is
|
|
21
|
+
* already known, so a chip mounted after the first summary does not wait for a
|
|
22
|
+
* change; and it returns a disposer, which is what the slot's own lifecycle
|
|
23
|
+
* expects.
|
|
24
|
+
*
|
|
25
|
+
* ── Unknown vs known-empty ──────────────────────────────────────────────────
|
|
26
|
+
* `hqEdge.selection.getCurrent()` returns `null` when the state is UNKNOWN
|
|
27
|
+
* (nothing published yet, or the sync failed). That is NOT a selection, and
|
|
28
|
+
* the chip renders nothing for it — a chip must never appear on screen that
|
|
29
|
+
* the host does not currently back.
|
|
30
|
+
*
|
|
31
|
+
* ── No second composer ──────────────────────────────────────────────────────
|
|
32
|
+
* This entry never touches the draft, the input state, or the reference
|
|
33
|
+
* pipeline. It is a read-only projection of host state; the agent reaches the
|
|
34
|
+
* real selection through the existing EDA selection tools.
|
|
35
|
+
*
|
|
36
|
+
* @module @huaqiu/dsh-eda-selection/client
|
|
37
|
+
*/
|
|
38
|
+
import { createElement, useEffect, useState, type ReactElement } from 'react'
|
|
39
|
+
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
40
|
+
import { SelectionChip } from './SelectionChip.js'
|
|
41
|
+
import { en, zh, type EdaSelectionKey } from './locales.js'
|
|
42
|
+
import {
|
|
43
|
+
normalizeSelectionSnapshot,
|
|
44
|
+
type HqEdgeLike,
|
|
45
|
+
type SelectionSnapshot,
|
|
46
|
+
} from './hq-edge.js'
|
|
47
|
+
|
|
48
|
+
export const inject = ['slots', 'locale', 'hqEdge'] as const
|
|
49
|
+
|
|
50
|
+
/** This plugin's dictionary namespace. */
|
|
51
|
+
export const EDA_SELECTION_NS = 'huaqiu:eda-selection'
|
|
52
|
+
|
|
53
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
54
|
+
interface LocaleNamespaceMap {
|
|
55
|
+
'huaqiu:eda-selection': EdaSelectionKey
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Minimal structural client context (the DSH client runtime provides these
|
|
61
|
+
* faces). Declared structurally, never imported, so the package stays
|
|
62
|
+
* independently installable — the same pattern as
|
|
63
|
+
* `@huaqiu/dsh-tool-schematic-gen` and `@huaqiu/dsh-huaqiu-about`.
|
|
64
|
+
*/
|
|
65
|
+
export interface ClientContext {
|
|
66
|
+
slots?: {
|
|
67
|
+
inject(key: string, callback: () => () => void): () => void
|
|
68
|
+
register(
|
|
69
|
+
spec: {
|
|
70
|
+
name: string
|
|
71
|
+
id?: string
|
|
72
|
+
order?: number
|
|
73
|
+
label?: () => string
|
|
74
|
+
locale?: string
|
|
75
|
+
},
|
|
76
|
+
component: unknown,
|
|
77
|
+
): () => void
|
|
78
|
+
}
|
|
79
|
+
locale?: {
|
|
80
|
+
register(namespace: string, dictionaries: unknown): unknown
|
|
81
|
+
bind(namespace: string): (key: string, params?: Record<string, unknown>) => string
|
|
82
|
+
}
|
|
83
|
+
get<T = unknown>(name: string): T | undefined
|
|
84
|
+
effect?(fn: () => (() => void) | void, label?: string): void
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Translation function shape (the locale service's `bind` result). */
|
|
88
|
+
export type Translate = (key: string, params?: Record<string, unknown>) => string
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the `hqEdge` service defensively.
|
|
92
|
+
*
|
|
93
|
+
* `inject` already guarantees it exists by `apply()` time; the try/catch and
|
|
94
|
+
* the shape checks keep a standalone DSH (no edge-bridge) from taking the
|
|
95
|
+
* plugin down. In that case the chip simply never renders.
|
|
96
|
+
*/
|
|
97
|
+
function getHqEdge(ctx: ClientContext): HqEdgeLike | undefined {
|
|
98
|
+
try {
|
|
99
|
+
const edge = ctx.get<HqEdgeLike | undefined>('hqEdge')
|
|
100
|
+
if (!edge || !edge.selection || typeof edge.selection.getCurrent !== 'function') return undefined
|
|
101
|
+
return edge
|
|
102
|
+
} catch {
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Read and normalize the current snapshot from the bridge.
|
|
109
|
+
*
|
|
110
|
+
* The bridge already normalizes; re-normalizing here keeps this plugin honest
|
|
111
|
+
* when pointed at an older bridge (or a test double) that does not.
|
|
112
|
+
*/
|
|
113
|
+
function readSnapshot(edge: HqEdgeLike | undefined): SelectionSnapshot | null {
|
|
114
|
+
if (!edge) return null
|
|
115
|
+
try {
|
|
116
|
+
return normalizeSelectionSnapshot(edge.selection?.getCurrent())
|
|
117
|
+
} catch {
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface SelectionDockProps {
|
|
123
|
+
/** Bound dictionary, injected by the client entry. */
|
|
124
|
+
t: Translate
|
|
125
|
+
/** Lazy bridge accessor — a closure over `ctx`, never a captured value. */
|
|
126
|
+
getEdge: () => HqEdgeLike | undefined
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The dock component: subscribes to the bridge and renders the chip.
|
|
131
|
+
*
|
|
132
|
+
* A hook rather than a plain read, because the snapshot arrives asynchronously
|
|
133
|
+
* (the bridge's first poll lands after this component mounts).
|
|
134
|
+
*/
|
|
135
|
+
export function SelectionDock({ t, getEdge }: SelectionDockProps): ReactElement | null {
|
|
136
|
+
const [snapshot, setSnapshot] = useState<SelectionSnapshot | null>(null)
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
const edge = getEdge()
|
|
140
|
+
|
|
141
|
+
// No bridge (standalone DSH) or a bridge without the selection face
|
|
142
|
+
// (older edge-bridge): render nothing, permanently. Not an error.
|
|
143
|
+
if (!edge || typeof edge.selection?.onChanged !== 'function') {
|
|
144
|
+
setSnapshot(null)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// `onChanged` replays the current state immediately when one is known, so
|
|
149
|
+
// this read is only needed for the unknown-at-mount case.
|
|
150
|
+
setSnapshot(readSnapshot(edge))
|
|
151
|
+
|
|
152
|
+
let alive = true
|
|
153
|
+
const dispose = edge.selection.onChanged((next) => {
|
|
154
|
+
if (!alive) return
|
|
155
|
+
// Normalize on the way in too: a listener must never be handed a shape
|
|
156
|
+
// it did not expect, whatever the bridge did.
|
|
157
|
+
setSnapshot(normalizeSelectionSnapshot(next))
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
return () => {
|
|
161
|
+
alive = false
|
|
162
|
+
dispose()
|
|
163
|
+
}
|
|
164
|
+
}, [getEdge])
|
|
165
|
+
|
|
166
|
+
return createElement(SelectionChip, { snapshot, t })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Install the dock entry on the running client context.
|
|
171
|
+
*
|
|
172
|
+
* Never throws: a deployment without the slot service, the locale service, or
|
|
173
|
+
* the `hqEdge` bridge must still load — it simply shows no chip.
|
|
174
|
+
*/
|
|
175
|
+
export function apply(ctx: ClientContext): void {
|
|
176
|
+
// Dictionaries first, so the first render already has localized text. The
|
|
177
|
+
// untyped register form is used because this namespace is declared by this
|
|
178
|
+
// package (not by DSH), which keeps DSH core untouched.
|
|
179
|
+
const locale = ctx.locale
|
|
180
|
+
if (locale && typeof locale.register === 'function') {
|
|
181
|
+
try {
|
|
182
|
+
locale.register(EDA_SELECTION_NS, { zh, en })
|
|
183
|
+
} catch {
|
|
184
|
+
// A duplicate registration (hot reload, double mount) is not fatal —
|
|
185
|
+
// the first registration's dictionaries are already live.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const t: Translate =
|
|
190
|
+
locale && typeof locale.bind === 'function'
|
|
191
|
+
? locale.bind(EDA_SELECTION_NS)
|
|
192
|
+
: (key: string) => key
|
|
193
|
+
|
|
194
|
+
const getEdge = (): HqEdgeLike | undefined => getHqEdge(ctx)
|
|
195
|
+
|
|
196
|
+
const slots = ctx.slots
|
|
197
|
+
if (!slots || typeof slots.inject !== 'function' || typeof slots.register !== 'function') {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
ctx.effect?.(
|
|
202
|
+
() =>
|
|
203
|
+
slots.inject('conversation.input.dock', () =>
|
|
204
|
+
slots.register(
|
|
205
|
+
{
|
|
206
|
+
name: 'conversation.input.dock',
|
|
207
|
+
// Our own id: a fresh id is added BESIDE the shipped entries
|
|
208
|
+
// (queue / todo / goal) instead of replacing one of them.
|
|
209
|
+
id: 'eda-selection',
|
|
210
|
+
// Ascending among the dock entries. `queue` is 20 and `todo` is 0,
|
|
211
|
+
// so 5 puts the ambient chip near the top — closest to the composer
|
|
212
|
+
// it describes — without displacing the todo dock at 0.
|
|
213
|
+
order: 5,
|
|
214
|
+
label: () => t('dock.label'),
|
|
215
|
+
locale: EDA_SELECTION_NS,
|
|
216
|
+
},
|
|
217
|
+
() => createElement(SelectionDock, { t, getEdge }),
|
|
218
|
+
),
|
|
219
|
+
),
|
|
220
|
+
'huaqiu eda-selection: composer dock chip',
|
|
221
|
+
)
|
|
222
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zh / en copy for the ambient `Current selection` dock entry.
|
|
3
|
+
*
|
|
4
|
+
* Chinese is the source of truth: `EdaSelectionKey` is derived from it and
|
|
5
|
+
* `en` is typed against it, so a key added to one language without the other
|
|
6
|
+
* is a COMPILE error rather than a silent Chinese fallback at runtime.
|
|
7
|
+
*
|
|
8
|
+
* Kept in its own module (rather than inlined in the component) so the copy is
|
|
9
|
+
* reviewable on its own and the test file can assert bilingual balance
|
|
10
|
+
* directly, mirroring `@huaqiu/dsh-auth` / `@huaqiu/dsh-tool-schematic-gen`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const zh = {
|
|
14
|
+
/** Dock-entry label shown wherever the owner projects one. */
|
|
15
|
+
'dock.label': '当前选中',
|
|
16
|
+
/** The chip itself: "Current selection · 3 items (PCB)". */
|
|
17
|
+
'chip.label': '当前选中',
|
|
18
|
+
'chip.sep': '·',
|
|
19
|
+
/** Editor names, spelled for a human reader. */
|
|
20
|
+
'editor.pcb': 'PCB',
|
|
21
|
+
'editor.schematic': '原理图',
|
|
22
|
+
'editor.unspecified': '编辑器',
|
|
23
|
+
/** "{count} items" / "{count} 项". */
|
|
24
|
+
'chip.items': '{count} 项',
|
|
25
|
+
/** One item — English needs the singular form. */
|
|
26
|
+
'chip.item': '1 项',
|
|
27
|
+
/** Screen-reader label for the chip. */
|
|
28
|
+
'chip.aria': '当前编辑器选中 {count} 项',
|
|
29
|
+
/** Bounded label list, shown after the count when the host supplied any. */
|
|
30
|
+
'chip.labels': ':{labels}',
|
|
31
|
+
} as const
|
|
32
|
+
|
|
33
|
+
export type EdaSelectionKey = keyof typeof zh
|
|
34
|
+
|
|
35
|
+
export const en: Record<EdaSelectionKey, string> = {
|
|
36
|
+
'dock.label': 'Current selection',
|
|
37
|
+
'chip.label': 'Current selection',
|
|
38
|
+
'chip.sep': '·',
|
|
39
|
+
'editor.pcb': 'PCB',
|
|
40
|
+
'editor.schematic': 'Schematic',
|
|
41
|
+
'editor.unspecified': 'editor',
|
|
42
|
+
'chip.items': '{count} items',
|
|
43
|
+
'chip.item': '1 item',
|
|
44
|
+
'chip.aria': 'Current editor selection: {count} items',
|
|
45
|
+
'chip.labels': ': {labels}',
|
|
46
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/dsh-eda-selection` — node half.
|
|
3
|
+
*
|
|
4
|
+
* Registers ONE dynamic system-prompt section, `eda:selection`, that reports
|
|
5
|
+
* the focused editor's selection to the agent. Nothing else: no tools, no
|
|
6
|
+
* routes, no I/O.
|
|
7
|
+
*
|
|
8
|
+
* ── Why a section and not a context ─────────────────────────────────────────
|
|
9
|
+
* A *section* is part of the system prompt (stable policy text); a *context* is
|
|
10
|
+
* materialized as a durable user-role snapshot. The selection summary is
|
|
11
|
+
* neither — it is ambient state that changes between turns and must not be
|
|
12
|
+
* persisted into the conversation history as if the user had said it. A
|
|
13
|
+
* section whose provider re-evaluates at each assembly is exactly that.
|
|
14
|
+
*
|
|
15
|
+
* ── Why the provider performs no I/O ────────────────────────────────────────
|
|
16
|
+
* The provider reads `hqEdge.selection.getCurrent()` — an in-memory value the
|
|
17
|
+
* bridge keeps fresh from its own `/runtime/dsh` poll. Prompt assembly happens
|
|
18
|
+
* on the agent's hot path, so an HTTP call here would add latency to every
|
|
19
|
+
* turn and could fail; a stale-but-free read is strictly better than a fresh
|
|
20
|
+
* blocking one. When the value is unknown the section contributes NOTHING.
|
|
21
|
+
*
|
|
22
|
+
* ── What the section must not do ────────────────────────────────────────────
|
|
23
|
+
* It must not imply that selection CONTENT was inserted into the conversation.
|
|
24
|
+
* The summary is a count, an editor identity and a bounded label list — enough
|
|
25
|
+
* to decide whether the existing selection tools are worth calling, never a
|
|
26
|
+
* substitute for calling them.
|
|
27
|
+
*
|
|
28
|
+
* @module @huaqiu/dsh-eda-selection
|
|
29
|
+
*/
|
|
30
|
+
import {
|
|
31
|
+
EDA_SELECTION_SECTION,
|
|
32
|
+
EDA_SELECTION_SECTION_ORDER,
|
|
33
|
+
edaSelectionSectionText,
|
|
34
|
+
registerEdaSelectionSection,
|
|
35
|
+
type SystemPromptLike,
|
|
36
|
+
} from './prompt.js'
|
|
37
|
+
import { normalizeSelectionSnapshot, type HqEdgeLike } from './client/hq-edge.js'
|
|
38
|
+
import { getLogger } from '@huaqiu/dsh-plugin-log'
|
|
39
|
+
|
|
40
|
+
/** Plugin id — matches package.json. */
|
|
41
|
+
export const name = '@huaqiu/dsh-eda-selection'
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Cordis services this half depends on.
|
|
45
|
+
*
|
|
46
|
+
* `hqEdge` is the bridge's node-side twin. It is declared here so DSH holds
|
|
47
|
+
* this plugin until the bridge has provided it, and every read is still
|
|
48
|
+
* guarded: a standalone DSH deployment without the bridge must not fail.
|
|
49
|
+
*
|
|
50
|
+
* `systemPrompt` carries the section. Declared for the same reason.
|
|
51
|
+
*/
|
|
52
|
+
export const inject = ['hqEdge', 'systemPrompt'] as const
|
|
53
|
+
|
|
54
|
+
/** Shared component name for the unified DSH-plugin log. */
|
|
55
|
+
const COMPONENT = 'dsh-eda-selection'
|
|
56
|
+
const log = getLogger(COMPONENT)
|
|
57
|
+
|
|
58
|
+
/** Host plugin body — register the dynamic selection section. */
|
|
59
|
+
export function apply(ctx: {
|
|
60
|
+
hqEdge?: HqEdgeLike
|
|
61
|
+
systemPrompt?: SystemPromptLike
|
|
62
|
+
}): () => void {
|
|
63
|
+
// Lazy, never captured once: the bridge's `selection` face is a live object,
|
|
64
|
+
// but reading it lazily keeps this plugin working if the service is swapped.
|
|
65
|
+
const readSelection = (): ReturnType<typeof normalizeSelectionSnapshot> => {
|
|
66
|
+
try {
|
|
67
|
+
const edge = ctx.hqEdge
|
|
68
|
+
if (!edge || !edge.selection || typeof edge.selection.getCurrent !== 'function') return null
|
|
69
|
+
return normalizeSelectionSnapshot(edge.selection.getCurrent())
|
|
70
|
+
} catch {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const dispose = registerEdaSelectionSection(ctx.systemPrompt, readSelection)
|
|
76
|
+
|
|
77
|
+
if (dispose === undefined) {
|
|
78
|
+
log.warn('systemPrompt service unavailable — the selection prompt section was not registered')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
dispose?.()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
EDA_SELECTION_SECTION,
|
|
88
|
+
EDA_SELECTION_SECTION_ORDER,
|
|
89
|
+
edaSelectionSectionText,
|
|
90
|
+
registerEdaSelectionSection,
|
|
91
|
+
type SystemPromptLike,
|
|
92
|
+
}
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `eda:selection` system-prompt section.
|
|
3
|
+
*
|
|
4
|
+
* Split out from `src/index.ts` so the text builder is unit-testable without a
|
|
5
|
+
* cordis context, a bridge, or a prompt registry — the same call
|
|
6
|
+
* `@huaqiu/dsh-tool-schematic-gen` makes for its `prompt.ts`.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import type { SelectionSnapshot } from './client/hq-edge.js'
|
|
11
|
+
|
|
12
|
+
/** Unique prompt-section name. */
|
|
13
|
+
export const EDA_SELECTION_SECTION = 'eda:selection'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Centrally allocated placement: 590, between DSH's `PLAN_POLICY` (500) and
|
|
17
|
+
* `TEAM_POLICY` (600).
|
|
18
|
+
*
|
|
19
|
+
* That is where the sibling Huaqiu tool-policy section
|
|
20
|
+
* (`huaqiu:schematic-gen`) already sits, so both Huaqiu policy blocks are
|
|
21
|
+
* adjacent and both precede the tool-list sections (`TOOL_*` at 1000+) they
|
|
22
|
+
* refer to. `ctx.systemPrompt.getSectionOrder()` only resolves DSH's own named
|
|
23
|
+
* positions, so the number is pinned here and asserted by a test.
|
|
24
|
+
*/
|
|
25
|
+
export const EDA_SELECTION_SECTION_ORDER = 590
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Structural view of `ctx.systemPrompt` — only the face we use.
|
|
29
|
+
*
|
|
30
|
+
* `text` accepts a provider, which the registry re-evaluates at every
|
|
31
|
+
* assembly with that assembly's `AssembleContext`. Returning `''` contributes
|
|
32
|
+
* nothing: `renderPrompt` filters empty sections out before joining.
|
|
33
|
+
*/
|
|
34
|
+
export interface SystemPromptLike {
|
|
35
|
+
section(section: {
|
|
36
|
+
name: string
|
|
37
|
+
order: number
|
|
38
|
+
text: string | ((context: unknown) => string)
|
|
39
|
+
}): () => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Editor display name for the prompt (English: the prompt is model-facing). */
|
|
43
|
+
function editorName(editor: SelectionSnapshot['editor']): string {
|
|
44
|
+
switch (editor) {
|
|
45
|
+
case 'PCB':
|
|
46
|
+
return 'PCB (pcbnew)'
|
|
47
|
+
case 'SCHEMATIC':
|
|
48
|
+
return 'schematic (eeschema)'
|
|
49
|
+
default:
|
|
50
|
+
return 'an unidentified editor'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** How many labels the section may quote. The host already bounds the list. */
|
|
55
|
+
export const MAX_SECTION_LABELS = 5
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the section text for one snapshot.
|
|
59
|
+
*
|
|
60
|
+
* Returns `''` for every "contribute nothing" case — an unknown state
|
|
61
|
+
* (`null`) or a known-empty selection. One gate for both, so the section and
|
|
62
|
+
* the browser chip can never disagree about when the selection exists.
|
|
63
|
+
*
|
|
64
|
+
* @param snapshot - the current summary, or `null` when unknown.
|
|
65
|
+
* @returns the section text, or `''` when it must contribute nothing.
|
|
66
|
+
*/
|
|
67
|
+
export function edaSelectionSectionText(snapshot: SelectionSnapshot | null): string {
|
|
68
|
+
if (snapshot === null || !snapshot.hasSelection || snapshot.itemCount <= 0) return ''
|
|
69
|
+
|
|
70
|
+
const lines: string[] = [
|
|
71
|
+
'Current EDA selection (Huaqiu EDA host):',
|
|
72
|
+
`- The focused editor is ${editorName(snapshot.editor)}.`,
|
|
73
|
+
`- It currently has ${snapshot.itemCount} selected item(s).`,
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
const labels = snapshot.labels.slice(0, MAX_SECTION_LABELS)
|
|
77
|
+
if (labels.length > 0) {
|
|
78
|
+
lines.push(`- Selected labels (partial, host-truncated): ${labels.join(', ')}.`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
lines.push(
|
|
82
|
+
'- This is a SUMMARY only. The selected items themselves are NOT in this ' +
|
|
83
|
+
'conversation — call the existing selection tools (`get_pcb_selection`, ' +
|
|
84
|
+
'`get_selection_netlist`) when you need the actual entities, nets or ' +
|
|
85
|
+
'netlist. Never claim to know the selected content without calling them.',
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return lines.join('\n')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Register the dynamic selection section.
|
|
93
|
+
*
|
|
94
|
+
* Never throws: a DSH deployment without the prompt service, or one where a
|
|
95
|
+
* duplicate registration races, must still load. A duplicate is the expected
|
|
96
|
+
* hot-reload case, so it is swallowed rather than surfaced.
|
|
97
|
+
*
|
|
98
|
+
* @param systemPrompt - `ctx.systemPrompt`, or undefined.
|
|
99
|
+
* @param readSelection - reads the current summary. Must not perform I/O.
|
|
100
|
+
* @returns the section's disposer, or `undefined` when nothing was registered.
|
|
101
|
+
*/
|
|
102
|
+
export function registerEdaSelectionSection(
|
|
103
|
+
systemPrompt: unknown,
|
|
104
|
+
readSelection: () => SelectionSnapshot | null,
|
|
105
|
+
): (() => void) | undefined {
|
|
106
|
+
if (
|
|
107
|
+
systemPrompt === undefined || systemPrompt === null ||
|
|
108
|
+
typeof (systemPrompt as SystemPromptLike).section !== 'function'
|
|
109
|
+
) {
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const dispose = (systemPrompt as SystemPromptLike).section({
|
|
115
|
+
name: EDA_SELECTION_SECTION,
|
|
116
|
+
order: EDA_SELECTION_SECTION_ORDER,
|
|
117
|
+
// A provider, not a constant: the selection changes between turns and the
|
|
118
|
+
// registry re-evaluates this at every assembly. It performs NO I/O — it
|
|
119
|
+
// reads the bridge's in-memory cache.
|
|
120
|
+
text: () => edaSelectionSectionText(readSelection()),
|
|
121
|
+
})
|
|
122
|
+
return typeof dispose === 'function' ? dispose : undefined
|
|
123
|
+
} catch {
|
|
124
|
+
// Duplicate registration (hot reload) — the first one is still live.
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
127
|
+
}
|
package/README.md
DELETED