dsh-surface-bridge 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -0
  3. package/cordis.patch.yml +9 -0
  4. package/lib/client.js +310 -0
  5. package/lib/types/client/SurfaceSelectionDock.d.ts +37 -0
  6. package/lib/types/client/SurfaceSelectionDock.js +125 -0
  7. package/lib/types/client/index.d.ts +40 -0
  8. package/lib/types/client/index.js +41 -0
  9. package/lib/types/client/locales.d.ts +30 -0
  10. package/lib/types/client/locales.js +36 -0
  11. package/lib/types/client/service.d.ts +46 -0
  12. package/lib/types/client/service.js +89 -0
  13. package/lib/types/client/transport.d.ts +19 -0
  14. package/lib/types/client/transport.js +38 -0
  15. package/lib/types/contract.d.ts +329 -0
  16. package/lib/types/contract.js +38 -0
  17. package/lib/types/host/narrow.d.ts +25 -0
  18. package/lib/types/host/narrow.js +193 -0
  19. package/lib/types/host/render.d.ts +63 -0
  20. package/lib/types/host/render.js +228 -0
  21. package/lib/types/host/routes.d.ts +31 -0
  22. package/lib/types/host/routes.js +108 -0
  23. package/lib/types/host/service.d.ts +41 -0
  24. package/lib/types/host/service.js +93 -0
  25. package/lib/types/host/store.d.ts +85 -0
  26. package/lib/types/host/store.js +206 -0
  27. package/lib/types/index.d.ts +93 -0
  28. package/lib/types/index.js +132 -0
  29. package/package.json +88 -0
  30. package/src/client/SurfaceSelectionDock.module.css +186 -0
  31. package/src/client/SurfaceSelectionDock.tsx +245 -0
  32. package/src/client/index.ts +65 -0
  33. package/src/client/locales.ts +42 -0
  34. package/src/client/service.ts +110 -0
  35. package/src/client/transport.ts +39 -0
  36. package/src/contract.ts +351 -0
  37. package/src/css-modules.d.ts +10 -0
  38. package/src/host/narrow.ts +180 -0
  39. package/src/host/render.ts +226 -0
  40. package/src/host/routes.ts +117 -0
  41. package/src/host/service.ts +116 -0
  42. package/src/host/store.ts +236 -0
  43. package/src/index.ts +194 -0
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Browser half of dsh-surface-bridge.
3
+ *
4
+ * Two things live here and nothing else: the `ctx.surfaceBridge` registry a
5
+ * business surface publishes into, and the single `conversation.input.dock` entry
6
+ * that renders every surface's selection. Keeping the chip in this bundle — rather
7
+ * than letting each surface draw its own — is what makes the composer row stable
8
+ * as more surfaces adopt the seam.
9
+ *
10
+ * @module dsh-surface-bridge/client
11
+ */
12
+
13
+ import type { Context as ClientContext } from '@deepseek-ai/cordis'
14
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
15
+ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
16
+ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
17
+ import type {} from '@deepseek-ai/dsh-client-ui-session/client'
18
+ import type { LocaleNamespaceMap } from '@deepseek-ai/dsh-client-ui-slots'
19
+ import { SurfaceBridgeRegistry, type SurfaceBridgeService } from './service.ts'
20
+ import { SurfaceSelectionChip } from './SurfaceSelectionDock.tsx'
21
+ import { NS, en, zh, type SurfaceBridgeKey } from './locales.ts'
22
+
23
+ declare module '@deepseek-ai/cordis' {
24
+ interface Context {
25
+ /** Browser face of the surface bridge; the seam a business surface publishes into. */
26
+ surfaceBridge: SurfaceBridgeService
27
+ }
28
+ }
29
+
30
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
31
+ interface LocaleNamespaceMap {
32
+ /** The selection chip's copy. */
33
+ 'surface-bridge': SurfaceBridgeKey
34
+ }
35
+ }
36
+
37
+ /** The slot registry, the locale registry, and the composer's overlay slot declaration. */
38
+ export const inject = ['slots', 'locale'] as const
39
+
40
+ /** Slot entry id; unique within the chip slot so a re-registration shadows instead of throwing. */
41
+ const CHIP_ENTRY_ID = 'surface-bridge'
42
+
43
+ /**
44
+ * Client plugin body: provide the registry, register its copy, and contribute the
45
+ * one chip entry.
46
+ *
47
+ * The seat is `conversation.input.overlay` — inside the composer card, on the
48
+ * shell's top-edge anchor — rather than `conversation.input.dock`, which renders
49
+ * above the card in the same stack as the to-do dock. The overlay seat is what
50
+ * makes the chip a region of the input box instead of one more bar in that stack.
51
+ *
52
+ * @param ctx - Client root context.
53
+ */
54
+ export function apply(ctx: ClientContext): void {
55
+ const bridge = new SurfaceBridgeRegistry()
56
+ ctx.effect(() => ctx.reflect.provide('surfaceBridge', bridge), 'dsh-surface-bridge: client registry')
57
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-surface-bridge: dictionaries')
58
+ ctx.slots.inject('conversation.input.overlay', () => ctx.slots.register({
59
+ name: 'conversation.input.overlay',
60
+ id: CHIP_ENTRY_ID,
61
+ order: 5,
62
+ locale: NS,
63
+ inject: () => ({ bridge }),
64
+ }, SurfaceSelectionChip))
65
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Copy owned by the surface bridge.
3
+ *
4
+ * The bridge renders chrome inside the composer, so every string it shows comes from
5
+ * here and is registered for both shipped locales. The model-facing text is not here:
6
+ * it belongs to the Host renderer, which is not a locale consumer.
7
+ *
8
+ * The chip's visible label is deliberately terse — `画布 · 3 个元素` — because it sits
9
+ * on the input line, not in a panel of its own. The long form ("已选择 3 个元素") is
10
+ * the chip's accessible name and the summary menu's heading, where there is room for
11
+ * it and where it explains rather than labels.
12
+ *
13
+ * @module dsh-surface-bridge/client/locales
14
+ */
15
+
16
+ /** Dictionary namespace owned by this plugin. */
17
+ export const NS = 'surface-bridge'
18
+
19
+ /** Chinese dictionary. */
20
+ export const zh = {
21
+ 'chip.aria': '已选择 {count} 个元素,展开查看',
22
+ 'chip.count': '{count} 个元素',
23
+ 'menu.title': '已选择 {count} 个元素',
24
+ 'menu.empty': '(没有元素明细)',
25
+ 'menu.more': '还有 {count} 个元素未列出',
26
+ 'action.reveal': '定位到画布',
27
+ 'action.clear': '清空选择',
28
+ } as const
29
+
30
+ /** English dictionary; key set must match {@link zh} exactly. */
31
+ export const en: Record<keyof typeof zh, string> = {
32
+ 'chip.aria': '{count} elements selected, expand for details',
33
+ 'chip.count': '{count} elements',
34
+ 'menu.title': '{count} elements selected',
35
+ 'menu.empty': '(no element details)',
36
+ 'menu.more': '{count} more elements not listed',
37
+ 'action.reveal': 'Reveal surface',
38
+ 'action.clear': 'Clear selection',
39
+ }
40
+
41
+ /** Dictionary key union for this namespace. */
42
+ export type SurfaceBridgeKey = keyof typeof zh
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Browser half of the bridge: the source registry and the one live selection store.
3
+ *
4
+ * A business surface registers a descriptor once and then publishes its selection
5
+ * whenever the user changes it. The bridge owns everything after that — the chip and
6
+ * the answer to a read.
7
+ *
8
+ * Note what is *not* here: any network. Nothing leaves the page when the user selects
9
+ * something. The registry is the single local answer to "what is selected", read by
10
+ * the surface's own operation loop when the Host asks, which is why a selection only
11
+ * ever reaches a model as part of a message being sent.
12
+ *
13
+ * The vocabulary lives in `../contract.ts` because the Host half, the browser half,
14
+ * and every consumer must spell it the same way.
15
+ *
16
+ * @module dsh-surface-bridge/client/service
17
+ */
18
+
19
+ import type {
20
+ SurfaceBridgeService,
21
+ SurfaceSelection,
22
+ SurfaceSourceDescriptor,
23
+ } from '../contract.ts'
24
+
25
+ export type { SurfaceBridgeService, SurfaceIconProps, SurfaceSourceDescriptor } from '../contract.ts'
26
+
27
+ /** One source's live entry. */
28
+ interface SourceEntry {
29
+ readonly descriptor: SurfaceSourceDescriptor
30
+ selection: SurfaceSelection | null
31
+ /** Whether the surface is on screen for the current Session; see {@link SurfaceBridgeService.setVisible}. */
32
+ visible: boolean
33
+ }
34
+
35
+ /** Default implementation of the bridge's browser-side registry. */
36
+ export class SurfaceBridgeRegistry implements SurfaceBridgeService {
37
+ private readonly entries = new Map<string, SourceEntry>()
38
+ private readonly listeners = new Set<() => void>()
39
+ private revision = 0
40
+
41
+ /** @inheritdoc */
42
+ registerSource(descriptor: SurfaceSourceDescriptor): () => void {
43
+ this.entries.set(descriptor.id, { descriptor, selection: null, visible: false })
44
+ this.bump()
45
+ return () => {
46
+ this.entries.delete(descriptor.id)
47
+ this.bump()
48
+ }
49
+ }
50
+
51
+ /** @inheritdoc */
52
+ setVisible(source: string, visible: boolean): void {
53
+ const entry = this.entries.get(source)
54
+ if (entry === undefined || entry.visible === visible) return
55
+ entry.visible = visible
56
+ this.bump()
57
+ }
58
+
59
+ /** @inheritdoc */
60
+ isVisible(source: string): boolean {
61
+ return this.entries.get(source)?.visible === true
62
+ }
63
+
64
+ /** @inheritdoc */
65
+ publish(source: string, selection: SurfaceSelection | null): void {
66
+ const entry = this.entries.get(source)
67
+ if (entry === undefined) return
68
+ entry.selection = selection
69
+ this.bump()
70
+ }
71
+
72
+ /** @inheritdoc */
73
+ read(source: string): SurfaceSelection | null {
74
+ return this.entries.get(source)?.selection ?? null
75
+ }
76
+
77
+ /** @inheritdoc */
78
+ active(): readonly { descriptor: SurfaceSourceDescriptor; selection: SurfaceSelection }[] {
79
+ const out: { descriptor: SurfaceSourceDescriptor; selection: SurfaceSelection }[] = []
80
+ for (const entry of this.entries.values()) {
81
+ // A hidden surface offers nothing: the chip must never advertise context the
82
+ // user cannot see, and the read answer draws on the same rule, so an
83
+ // invisible selection can never reach a model either.
84
+ if (entry.selection === null || !entry.visible) continue
85
+ out.push({ descriptor: entry.descriptor, selection: entry.selection })
86
+ }
87
+ return out.sort((left, right) => {
88
+ const order = (left.descriptor.order ?? 0) - (right.descriptor.order ?? 0)
89
+ if (order !== 0) return order
90
+ return left.descriptor.id < right.descriptor.id ? -1 : left.descriptor.id > right.descriptor.id ? 1 : 0
91
+ })
92
+ }
93
+
94
+ /** @inheritdoc */
95
+ subscribe(listener: () => void): () => void {
96
+ this.listeners.add(listener)
97
+ return () => { this.listeners.delete(listener) }
98
+ }
99
+
100
+ /** @inheritdoc */
101
+ version(): number {
102
+ return this.revision
103
+ }
104
+
105
+ /** Advance the version and notify every subscriber. */
106
+ private bump(): void {
107
+ this.revision += 1
108
+ for (const listener of [...this.listeners]) listener()
109
+ }
110
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Browser-side transport of the bridge.
3
+ *
4
+ * One long poll, held by the Host for up to twenty seconds, that carries both
5
+ * directions: the surface asks for work and reports what happened. A failure —
6
+ * including an abort from a page unload or a Connection generation change — is an
7
+ * ordinary reason to poll again rather than a broken surface.
8
+ *
9
+ * There is deliberately no "here is my selection" call. A selection is read by the
10
+ * Host when a model step needs it, so nothing travels while the user is merely
11
+ * drawing.
12
+ *
13
+ * @module dsh-surface-bridge/client/transport
14
+ */
15
+
16
+ import type { OperationSettleResponse, OperationsPollResponse, SurfaceOperation, SurfaceOperationResult } from '../contract.ts'
17
+ import { OPS_PATH, SETTLE_PATH } from '../contract.ts'
18
+
19
+ /** Hold one poll for pending operations. */
20
+ export async function pollOperations(sessionId: string, holdMs: number, signal: AbortSignal): Promise<readonly SurfaceOperation[]> {
21
+ const url = `${OPS_PATH}?sessionId=${encodeURIComponent(sessionId)}&hold=${String(holdMs)}`
22
+ const response = await fetch(url, { signal, headers: { accept: 'application/json' } })
23
+ if (!response.ok) throw new Error(`operations poll failed: HTTP ${response.status}`)
24
+ const body = await response.json() as OperationsPollResponse
25
+ return Array.isArray(body.operations) ? body.operations : []
26
+ }
27
+
28
+ /** Report one operation's outcome back to the Host. */
29
+ export async function settleOperation(sessionId: string, result: SurfaceOperationResult, signal?: AbortSignal): Promise<void> {
30
+ const response = await fetch(SETTLE_PATH, {
31
+ method: 'POST',
32
+ headers: { 'content-type': 'application/json' },
33
+ body: JSON.stringify({ sessionId, result }),
34
+ ...(signal === undefined ? {} : { signal }),
35
+ })
36
+ if (!response.ok) throw new Error(`operation settle failed: HTTP ${response.status}`)
37
+ const body = await response.json() as OperationSettleResponse
38
+ if (body.accepted !== true) throw new Error('operation settle was not accepted')
39
+ }
@@ -0,0 +1,351 @@
1
+ /**
2
+ * The surface-bridge contract: the vocabulary a business surface (a right-Sidebar
3
+ * canvas, grid, or notebook) uses to hand its current selection to the composer,
4
+ * and the vocabulary the Host uses to turn that selection into model context.
5
+ *
6
+ * The contract is deliberately surface-agnostic. Nothing here mentions a canvas,
7
+ * a cell, or a table: a source projects its own selection into {@link SurfaceElement}
8
+ * rows, and the bridge owns the envelope (revision, count, bounds, the rendered
9
+ * model text, the optional raster). That split is what lets a second surface adopt
10
+ * the seam without the bridge learning what a shape is.
11
+ *
12
+ * @module dsh-surface-bridge/contract
13
+ */
14
+
15
+ /**
16
+ * Route paths owned by the bridge.
17
+ *
18
+ * Spelled once, in the module both halves already import, because the browser's
19
+ * `fetch` and the Host's route registration are two halves of one wire: a typo in
20
+ * either would 404 at runtime with no compile-time signal.
21
+ */
22
+ export const OPS_PATH = '/api/data-canvas/ops'
23
+ export const SETTLE_PATH = '/api/data-canvas/settle'
24
+
25
+ /**
26
+ * Reserved operation a Host read uses to ask a live surface what is selected.
27
+ *
28
+ * Its payload carries `consume`: a *consuming* read is one taken to put the selection
29
+ * into a message, and the surface drops its selection once it has answered, so the
30
+ * chip disappears from the composer at exactly the moment the context is spent. A
31
+ * non-consuming read is a peek (a tool asking what is selected) and changes nothing.
32
+ *
33
+ * It carries no source: only the browser knows which surfaces exist and which are
34
+ * on screen, so the Host asks "what has anyone selected?" and gets back a list. The
35
+ * surface answers through the ordinary settle channel with that list in
36
+ * {@link SurfaceOperationResult.value}. One queue item rather than a second channel,
37
+ * so the browser needs no extra listener and a read costs one round trip on a
38
+ * connection that is already parked and idle.
39
+ */
40
+ export const READ_SELECTION_OP = '__surface-read-selection'
41
+
42
+ /** Media types an attached raster may use; the set the Host's attachment service admits. */
43
+ export type SurfaceRasterMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif'
44
+
45
+ /** How one element connects to others in the same selection. */
46
+ export interface SurfaceElementLinks {
47
+ /** Source-side endpoint id (an arrow's start). */
48
+ readonly from?: string
49
+ /** Target-side endpoint id (an arrow's end). */
50
+ readonly to?: string
51
+ /** Container this element is bound into (text inside a shape, label on a line). */
52
+ readonly container?: string
53
+ /** Elements bound to this one. */
54
+ readonly bound?: readonly string[]
55
+ }
56
+
57
+ /** One element of a selection, projected to the facts a model can act on. */
58
+ export interface SurfaceElement {
59
+ /** Stable identity inside the source; the handle every write-back operation addresses. */
60
+ readonly id: string
61
+ /** Source-defined kind, e.g. `rectangle`, `arrow`, `text`, `image`, `cell`. */
62
+ readonly type: string
63
+ /** One-line human label shown in the chip's summary list, e.g. `矩形 "下单"`. */
64
+ readonly label: string
65
+ /** Text the element carries, if any. Absent for purely graphical elements. */
66
+ readonly text?: string
67
+ /** Left edge in source coordinates. */
68
+ readonly x: number
69
+ /** Top edge in source coordinates. */
70
+ readonly y: number
71
+ readonly width: number
72
+ readonly height: number
73
+ /** Rotation in degrees, when the source has one. */
74
+ readonly angle?: number
75
+ /** Presentation facts worth sending, already projected to short names. */
76
+ readonly style?: Readonly<Record<string, string | number>>
77
+ /**
78
+ * Where a heavy payload lives, never the payload itself. An image element
79
+ * reports its asset handle here and the bytes travel as {@link SurfaceSelection.raster}.
80
+ */
81
+ readonly asset?: Readonly<Record<string, string>>
82
+ /** Connection relations to other elements. */
83
+ readonly links?: SurfaceElementLinks
84
+ }
85
+
86
+ /** A rectangle in source coordinates. */
87
+ export interface SurfaceBounds {
88
+ readonly x: number
89
+ readonly y: number
90
+ readonly width: number
91
+ readonly height: number
92
+ }
93
+
94
+ /**
95
+ * The document a selection was taken from, when the surface is a view of one file.
96
+ *
97
+ * A right-Sidebar surface is usually a view of a project file — a scene, a workbook, a
98
+ * notebook — and the Agent's tools address that file by path. Without this field the
99
+ * context said "the user selected 30 elements" and nothing about *which* document, so a
100
+ * model that wrote to the same-named file in another directory believed it had edited the
101
+ * drawing on screen. It had not. The path travels with the selection so the write-back
102
+ * can name the exact file, and the row the user reads says which document they sent.
103
+ *
104
+ * Optional throughout: a surface that is not a view of a file simply omits it.
105
+ */
106
+ export interface SurfaceResource {
107
+ /** Absolute path on the Host, when the surface can name one. */
108
+ readonly path?: string
109
+ /** Short name for the chip and the model header, e.g. `main.excalidraw`. */
110
+ readonly name?: string
111
+ /**
112
+ * Version of the document this selection was projected from — a file mtime, a
113
+ * revision counter, whatever the surface uses. It is what lets the Agent detect that
114
+ * the file moved on between the selection and the edit.
115
+ */
116
+ readonly version?: number
117
+ }
118
+
119
+ /** A raster the source attached for one element or for the selection as a whole. */
120
+ export interface SurfaceRaster {
121
+ readonly mediaType: SurfaceRasterMediaType
122
+ /** Base64 bytes without a data-URL prefix. */
123
+ readonly data: string
124
+ /** Display name for the resulting attachment. */
125
+ readonly name?: string
126
+ /**
127
+ * Element this raster belongs to, when it is one element's own bytes rather
128
+ * than a render of the whole selection. The model-facing text lists the same
129
+ * id, so the model can pair each image with the row that locates it.
130
+ */
131
+ readonly elementId?: string
132
+ }
133
+
134
+ /** Everything one source wants the model to know about its current selection. */
135
+ export interface SurfaceSelection {
136
+ /** Source id; the same string the source registered under. */
137
+ readonly source: string
138
+ /**
139
+ * Monotonic revision minted by the source. The Host consumes each revision at
140
+ * most once, so a selection cannot leak into an unrelated later message.
141
+ */
142
+ readonly revision: number
143
+ /** Element count. May exceed `elements.length` when the source caps the list. */
144
+ readonly count: number
145
+ /** Human title of the surface, e.g. `Canvas`. Rendered in the chip and the model header. */
146
+ readonly title: string
147
+ /** One-line chip summary, e.g. `已选择 3 个元素`. */
148
+ readonly summary: string
149
+ /** Element projections, capped by the source. */
150
+ readonly elements: readonly SurfaceElement[]
151
+ /** True when `elements` is a truncated view of `count`. */
152
+ readonly truncated?: boolean
153
+ /** The document this selection came from, when the surface is a view of a file. */
154
+ readonly resource?: SurfaceResource
155
+ /** Bounding box of the whole selection, when the source can compute one. */
156
+ readonly bounds?: SurfaceBounds
157
+ /**
158
+ * Heavy element payloads the source chose to send as real images instead of
159
+ * text. A source sends at most a few, capped by its own byte budget; each one
160
+ * names the element it belongs to so the text can stay a set of locators.
161
+ */
162
+ readonly images?: readonly SurfaceRaster[]
163
+ /** Extra facts the source wants in the model text (scene size, per-type counts…). */
164
+ readonly notes?: readonly string[]
165
+ /** What the model may do to this surface; rendered as the available operations. */
166
+ readonly capabilities?: readonly string[]
167
+ }
168
+
169
+ /** One selection as the Host holds it, with the arrival time that makes staleness visible. */
170
+ export interface StoredSelection {
171
+ readonly selection: SurfaceSelection
172
+ /** Host clock at arrival. */
173
+ readonly receivedAt: number
174
+ }
175
+
176
+ /** One write-back operation an Agent asked the surface to perform. */
177
+ export interface SurfaceOperation {
178
+ /** Operation id minted by the Host; echoed back with the result. */
179
+ readonly id: string
180
+ /** Source the operation targets. */
181
+ readonly source: string
182
+ /** Source-defined verb, e.g. `update`, `add`, `delete`. */
183
+ readonly op: string
184
+ /** Source-defined payload. The bridge never interprets it. */
185
+ readonly payload: unknown
186
+ }
187
+
188
+ /** Outcome of one operation, reported by the surface that executed it. */
189
+ export interface SurfaceOperationResult {
190
+ readonly id: string
191
+ readonly ok: boolean
192
+ /** Source-reported detail on success (the element's final state, a count, …). */
193
+ readonly detail?: string
194
+ /** Failure message on `ok: false`. */
195
+ readonly error?: string
196
+ /**
197
+ * Value carried back for a read ({@link READ_SELECTION_OP}): every on-screen
198
+ * selection, as an array. Empty when nothing is selected or no surface is open.
199
+ */
200
+ readonly value?: unknown
201
+ }
202
+
203
+ /** Poll response of `GET /api/data-canvas/ops`. */
204
+ export interface OperationsPollResponse {
205
+ readonly operations: readonly SurfaceOperation[]
206
+ }
207
+
208
+ /** Reply body of `POST /api/data-canvas/ops/{id}`. */
209
+ export interface OperationSettleResponse {
210
+ readonly accepted: boolean
211
+ }
212
+
213
+ /** Body of a rejected bridge request. */
214
+ export interface BridgeErrorBody {
215
+ readonly ok: false
216
+ readonly error: string
217
+ }
218
+
219
+ /** One write-back operation as a business plugin's tool states it. */
220
+ export interface SurfaceApplyOperation {
221
+ /** Source-defined verb, e.g. `update`, `add`, `delete`. */
222
+ readonly op: string
223
+ /** Source-defined payload, passed through the bridge untouched. */
224
+ readonly payload: unknown
225
+ }
226
+
227
+ /** Glyph contract shared with the host shell's icon set. */
228
+ export interface SurfaceIconProps {
229
+ readonly size?: number
230
+ readonly className?: string
231
+ }
232
+
233
+ /** What a surface declares once, at registration. */
234
+ export interface SurfaceSourceDescriptor {
235
+ /** Stable source id; the same string published selections carry. */
236
+ readonly id: string
237
+ /** Human label for the chip, e.g. `Canvas`. */
238
+ readonly label: string
239
+ /** Glyph rendered before the label. */
240
+ readonly icon?: import('react').ComponentType<SurfaceIconProps>
241
+ /** Lower sorts first when several sources have selections at once. */
242
+ readonly order?: number
243
+ /**
244
+ * Bring the surface to the front. The chip's reveal action calls this; a source
245
+ * that cannot be revealed omits it and the action is not offered.
246
+ */
247
+ readonly reveal?: () => void
248
+ /**
249
+ * Bring one element into view. The chip's summary list calls this when a row is
250
+ * picked, so a user reading "3 elements selected" can see which one they mean.
251
+ */
252
+ readonly focusElement?: (elementId: string) => void
253
+ }
254
+
255
+ /**
256
+ * The `ctx.surfaceBridge` face a business surface publishes into.
257
+ *
258
+ * A surface registers once and then calls {@link publish} whenever the user's
259
+ * selection changes. Publishing is local — it drives the chip and answers the Host's
260
+ * read — and nothing about it touches the network.
261
+ */
262
+ export interface SurfaceBridgeService {
263
+ /** Register one surface. The returned disposer removes it and its selection. */
264
+ registerSource(descriptor: SurfaceSourceDescriptor): () => void
265
+ /**
266
+ * Tell the bridge whether a surface is on screen for the current Session.
267
+ *
268
+ * A surface that is hidden — the user switched tabs, switched Session, or
269
+ * collapsed the column — must not advertise a selection: the chip would offer
270
+ * to send context the user cannot see or clear. Hidden sources drop out of
271
+ * {@link active}, which both hides the chip and disarms the Host.
272
+ */
273
+ setVisible(source: string, visible: boolean): void
274
+ /** Whether a source is currently on screen. */
275
+ isVisible(source: string): boolean
276
+ /** Publish (or clear, with `null`) one source's selection. */
277
+ publish(source: string, selection: SurfaceSelection | null): void
278
+ /** Read one source's current selection. */
279
+ read(source: string): SurfaceSelection | null
280
+ /** Read every source that currently has a selection, in chip order. */
281
+ active(): readonly { descriptor: SurfaceSourceDescriptor; selection: SurfaceSelection }[]
282
+ /** Subscribe to any change (selection, sync state, roster). */
283
+ subscribe(listener: () => void): () => void
284
+ /** Snapshot version, for `useSyncExternalStore`. */
285
+ version(): number
286
+ }
287
+
288
+ /** The Host face of the bridge; what a business plugin's tools call. */
289
+ export interface SurfaceBridgeHostFace {
290
+ /**
291
+ * Ask the live surfaces what they have selected right now.
292
+ *
293
+ * Read on demand rather than pushed on every change: nothing travels and nothing
294
+ * is remembered until a model step actually needs it. A surface that is closed,
295
+ * off screen, or has cleared its selection contributes nothing, so an invisible
296
+ * selection can never reach a model.
297
+ *
298
+ * @param sessionId - Session whose composer is in play.
299
+ * @param signal - Aborts the wait when the turn is cancelled.
300
+ * @returns one entry per source that has something selected — or `undefined` when
301
+ * nothing answered at all. The difference matters and must not be flattened: an
302
+ * empty array means "the surface spoke and there is no selection", while
303
+ * `undefined` means "no surface is there to ask".
304
+ */
305
+ readSelections(sessionId: string, signal?: AbortSignal): Promise<readonly SurfaceSelection[] | undefined>
306
+ /**
307
+ * Read every on-screen selection **and spend it**: the surface drops each selection
308
+ * it reported, so it is not offered again unless the user selects something else.
309
+ *
310
+ * Used at the step that is about to carry the selection into a message. A peek
311
+ * ({@link readSelections}) leaves the selection in place.
312
+ *
313
+ * @param sessionId - Session whose composer is in play.
314
+ * @param signal - Aborts the wait when the turn is cancelled.
315
+ * @returns the same answer as {@link readSelections}.
316
+ */
317
+ consumeSelections(sessionId: string, signal?: AbortSignal): Promise<readonly SurfaceSelection[] | undefined>
318
+ /**
319
+ * Read one named source's current selection.
320
+ *
321
+ * @param sessionId - Session whose composer is in play.
322
+ * @param source - Source id to pick out of the answer.
323
+ * @param signal - Aborts the wait when the turn is cancelled.
324
+ * @returns that source's selection, `null` when it answered with none, or
325
+ * `undefined` when nothing answered.
326
+ */
327
+ readSelection(sessionId: string, source: string, signal?: AbortSignal): Promise<SurfaceSelection | null | undefined>
328
+ /**
329
+ * Whether a surface of this Session has polled recently enough to be considered open.
330
+ *
331
+ * @param sessionId - Session to test.
332
+ * @returns whether the surface is live.
333
+ */
334
+ isSurfaceLive(sessionId: string): boolean
335
+ /**
336
+ * Run write-back operations on one surface and wait for its report.
337
+ *
338
+ * @param sessionId - Session owning the surface.
339
+ * @param source - Source id.
340
+ * @param operations - Operations to run, in order.
341
+ * @param signal - Aborts the wait when the turn is cancelled.
342
+ * @returns one result per operation, in the same order; an unanswered operation
343
+ * comes back as an explicit failure rather than a silent success.
344
+ */
345
+ apply(
346
+ sessionId: string,
347
+ source: string,
348
+ operations: readonly SurfaceApplyOperation[],
349
+ signal?: AbortSignal,
350
+ ): Promise<readonly SurfaceOperationResult[]>
351
+ }
@@ -0,0 +1,10 @@
1
+ /* The client bundle compiles both forms: `*.module.css` to a hashed class map
2
+ * and a plain `*.css` to a side-effect style injection. Declaring them here keeps
3
+ * `tsc` (which only emits the Node half and the declarations) from rejecting the
4
+ * client half's imports. */
5
+ declare module '*.module.css' {
6
+ const classes: Record<string, string>
7
+ export default classes
8
+ }
9
+
10
+ declare module '*.css'