dsh-surface-bridge 0.1.0-alpha.2 → 0.1.0-alpha.3

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/README.md CHANGED
@@ -51,7 +51,14 @@ export function apply(ctx: ClientContext): void {
51
51
  }
52
52
  ```
53
53
 
54
- `selection` 的形状见 [`src/contract.ts`](src/contract.ts) 的 `SurfaceSelection`:源只投影**元素行**(id / 类型 / 文本 / 坐标 / 尺寸 / 非默认样式 / 连接关系),桥拥有信封(revision、count、bounds、模型文本、可选图片)。
54
+ 面板侧还要接住"点击 transcript 里那条选区 chip":桥把请求放进注册表,面板取走并选中元素。
55
+
56
+ ```ts
57
+ const request = ctx.surfaceBridge.takeFocus('excalidraw', scene) // 只取点名自己这张图的请求
58
+ if (request !== null) selectAndScrollIntoView(request.elementIds)
59
+ ```
60
+
61
+ `selection` 的形状见 [`src/contract.ts`](src/contract.ts) 的 `SurfaceSelection`:源只投影**元素行**(id / 类型 / 文本 / 坐标 / 尺寸 / 非默认样式 / 连接关系),桥拥有信封(revision、count、bounds、模型文本、可选图片)。文件类的面还可以给 `resource.display`(人读的名字,如 `main`)——桥不知道什么是扩展名,也不该知道。
55
62
 
56
63
  **图片不走文本。** 大载荷放进 `selection.images[]`,每条用 `elementId` 指向自己的元素;Host 把它们落成 durable 附件并以真正的 `image` 块追加到上下文。没有图片元素的选择,一个字节的图片都不产生。
57
64
 
@@ -72,21 +79,29 @@ const results = await ctx.surfaceBridgeHost.apply(sessionId, 'excalidraw', //
72
79
  Host 半在 `agent/pre-step` 里做三件事,而且只在**带着新用户输入的那一步**做:
73
80
 
74
81
  1. 若该 Session 有面在轮询(`isSurfaceLive`),向浏览器**读一次**当前选择;
75
- 2. 每个有选择的面,在消息末尾追加一条 `createUserMessage`(`source.kind = 'surface-selection'`,`form = 'snapshot'`);
76
- 3. 记下这个 turn 已经读过,**同一 turn 的后续 step 不再读**。
82
+ 2. 把选择**并进用户自己那条消息的最前面**:一个 `@"<显示名> · N 个元素"` mention(shell 把整个引号内容渲染成用户气泡里那个蓝色、带文件图标的引用 chip);图片以真正的 `image` 块加进同一条消息;
83
+ 3. 追加一条隐藏的明细消息(`source.kind = 'surface-selection'`)装元素表,并记下这个 turn 已经读过,**同一 turn 的后续 step 不再读**。
84
+
85
+ **为什么是一行、而且长在用户自己的气泡里。** Chat 视图对非 `user` 源的上下文节点**不渲染任何行**(`isVisibleChatNode` 把普通 Context 全部排除),所以可见的那一半必须是 `source.kind = 'user'` 的消息。而用户消息里行内有颜色的东西只有一种:`@…` 引用 chip,而且**它的显示文本就是 token 里的最后一段**——所以「文件 + 个数一起变蓝」只能把它们塞进同一个引号 token 里。于是选择以 mention 的形式并进用户那句话 —— 另起一条消息就会变成用户看到的"两行"。
77
86
 
78
87
  - **只在发送时读取**:用户画图期间什么都不发。这也让"发送时看到的选择"就是消息真正带上的选择。
79
88
  - **一个 turn 只读一次**:一个 turn 有多个 step(模型调用、工具结果、再调用),否则同一张图会被注入三遍。
80
89
  - **不可见的面答空**:读取的答案来自同一个注册表,而注册表只列"在屏幕上且有选择"的面,所以切会话/切标签后,旧选择既不会显示也不会被发送。
81
90
  - **无浏览器则跳过**:没有面在轮询(CLI、未打开过画布标签页)直接返回,不阻塞消息。
91
+ - **没有用户消息可并时**(turn 由通知、定时唤醒或恢复的 goal 打开):可见块另起一条 `user` 消息 —— 宁可多一行,也不能让选择只进模型、不进对话。
82
92
  - 形态与 `dsh-time-context`、`dsh-session-reference` 一致:都是官方认可的"生产者注入 user-role 上下文"路径。
83
93
 
84
- **在对话里长什么样(`form: 'notice'`)**:DSH 按消息源声明的 `form` 决定怎么渲染这条上下文,而**只有 `notice` 这一种 form 带 `summary`**,它的语义就是"不展开也要能读懂的一行"。所以选择集以一张带标题的卡片出现在对话里(`画布 · 2 个元素`),元素明细在展开里 —— 而不是一坨要滚过去的坐标文本。
94
+ **点击那条 chip 之后。** chip 的文本是给人读的(`main · 3 个元素`),不是路径 —— 所以 shell 自己那个"打开这个文件"的动作在这里用不了(它会把整串当路径去 claim,然后抛错)。于是这次点击由桥的客户端半边**接管**:捕获阶段 `stopPropagation`,自己打开这张图的**文件地址**(和文件树同一个标签页),再把请求交给画布面板。识别、定位、打开都靠下面这张表:
85
95
 
86
- 这不是自造 UI:卡片由 shell 自己渲染(shadcn/lucide 那套),本包只提供一行 summary。要注意的取舍是:
96
+ | 需要的东西 | 来自哪里 |
97
+ | --- | --- |
98
+ | 哪条消息 | 行上的 `data-chat-node-key`(shell 自己滚动/锚点也用它)+ `data-conversation-session` |
99
+ | 这是不是本包的 chip | chip 的 `title` 是否形如 `@"<名称> · N 个元素"` |
100
+ | 这条消息带了哪个选择 | 隐藏明细消息的 `source`:`surface` / `message` / `document` / `elements` |
101
+ | 打开哪个文件 | 同一条明细的 `source.path`(绝对路径)→ 按 shell 的语法组成 `dsh-resource://file/…`,与文件树点开同一个标签页 |
102
+ | 谁来选中元素 | 该面自己的面板:`requestFocus` / `takeFocus`,请求一直留着直到面板接住(点击本身往往就是打开这个面板的动作) |
87
103
 
88
- - `notice` **不允许再带 `sections`**(那是 `snapshot` 的字段),两者互斥。
89
- - summary 是**持久化在消息里的显示文本**,因此和注入正文一样是中文、不随 locale 变。模型读到的东西完全没变 —— 换的只是这一行怎么显示。
104
+ 监听是捕获阶段、只读、从不 `preventDefault`;只对**本包自己的 label 形态**(`@"… · N 个元素"`)接管,别人手打的 `@path` 或助手消息里的文件引用照旧由 shell 打开。任何一步缺失(老 shell 没有那个属性、明细消息不在已加载窗口里、面已卸载)都只是这一次点击什么都不做,不会抛错、也不会误开别的文件。明细消息的 `source` 是簿记而非模型内容,因此不花一个 prompt token。
90
105
 
91
106
  ## 路由
92
107
 
@@ -119,6 +134,9 @@ chip、输入行占位、`agent/pre-step` 注入、图片附件化、长轮询
119
134
  - **读取靠"有面在轮询"判定。** 从未打开过画布标签页的会话读不到东西——这是对的,那张画布根本不在屏幕上。
120
135
  - **每个面各自轮询。** 多个面同时挂在一个会话上时,读取请求会被先轮询到的那个取走。当前只有一个面,此路径正确;接第二个面时应把轮询收归桥的客户端半边(按 `operation.source` 分派)。
121
136
  - **回写不做持久队列。** 操作队列是同一个进程里两半之间的交接通道,画布关掉即失去意义;因此工具失败要响,而不是排队等。
137
+ - **点击定位依赖 shell 的 DOM 标识。** 行上的 `data-chat-node-key` / `data-conversation-session` 和 chip 上的 `data-ref-chip` 不是本包定义的接口,是 shell 自己导航时也在读的属性(它的 Esc 处理同样 `closest("[data-conversation-session]")`)。**升级 DSH 后要复验这三处**:`transcript-focus.test.mjs` 里有一条构建产物断言盯着它们。真要彻底不依赖,就得让 shell 提供自定义节点渲染器,那属于 DSH 侧改动。
138
+ - **chip 的 label 形态是握手协议。** 宿主写 `@"<显示名> · N 个元素"`,客户端按同一条正则读回来;两边改动必须同时。写歪了不会误伤别人:识别不出来的 chip 交回 shell,只是点不动。
139
+ - **一个面同时只能挂一个待处理定位请求。** 连点两次 chip 是"看后一次",不排队;这符合"再点一次"的直觉,也避免旧请求在新画布上突然生效。
122
140
 
123
141
  ## 源码
124
142
 
package/lib/client.js CHANGED
@@ -35,7 +35,7 @@ var SurfaceBridgeRegistry = class {
35
35
  revision = 0;
36
36
  /** @inheritdoc */
37
37
  registerSource(descriptor) {
38
- this.entries.set(descriptor.id, { descriptor, selection: null, visible: false });
38
+ this.entries.set(descriptor.id, { descriptor, selection: null, visible: false, focus: null });
39
39
  this.bump();
40
40
  return () => {
41
41
  this.entries.delete(descriptor.id);
@@ -78,6 +78,23 @@ var SurfaceBridgeRegistry = class {
78
78
  });
79
79
  }
80
80
  /** @inheritdoc */
81
+ requestFocus(source, target) {
82
+ const entry = this.entries.get(source);
83
+ if (entry === void 0) return;
84
+ entry.focus = target;
85
+ this.bump();
86
+ }
87
+ /** @inheritdoc */
88
+ takeFocus(source, document2) {
89
+ const entry = this.entries.get(source);
90
+ const focus = entry?.focus ?? null;
91
+ if (entry === void 0 || focus === null) return null;
92
+ if (document2 !== void 0 && focus.document !== void 0 && focus.document !== document2) return null;
93
+ entry.focus = null;
94
+ this.bump();
95
+ return focus;
96
+ }
97
+ /** @inheritdoc */
81
98
  subscribe(listener) {
82
99
  this.listeners.add(listener);
83
100
  return () => {
@@ -271,6 +288,118 @@ function SurfaceSelectionChip({
271
288
  )) });
272
289
  }
273
290
 
291
+ // src/client/transcript.ts
292
+ var NODE_KEY_ATTRIBUTE = "data-chat-node-key";
293
+ var SESSION_ATTRIBUTE = "data-conversation-session";
294
+ var CHIP_ATTRIBUTE = "data-ref-chip";
295
+ var SELECTION_SOURCE = "surface-selection";
296
+ function documentOfMention(token) {
297
+ if (typeof token !== "string" || !token.startsWith("@")) return void 0;
298
+ const raw = token.slice(1);
299
+ const name = raw.length > 2 && raw.startsWith('"') && raw.endsWith('"') ? raw.slice(1, -1) : raw;
300
+ return name.length === 0 ? void 0 : name;
301
+ }
302
+ var SELECTION_LABEL = /^@"(.+?) · (\d+) 个元素"$/u;
303
+ function selectionLabelOf(token) {
304
+ if (typeof token !== "string") return void 0;
305
+ const match = SELECTION_LABEL.exec(token);
306
+ const document2 = match?.[1];
307
+ return document2 === void 0 || document2.length === 0 ? void 0 : { document: document2 };
308
+ }
309
+ function selectionTargetOf(node, messageId, document2) {
310
+ if (node === void 0 || node.kind !== "context") return void 0;
311
+ const source = node.data?.source;
312
+ if (typeof source !== "object" || source === null) return void 0;
313
+ const fields = source;
314
+ if (fields.kind !== SELECTION_SOURCE) return void 0;
315
+ if (fields.message !== messageId) return void 0;
316
+ if (fields.document !== void 0 && fields.document !== document2) return void 0;
317
+ if (typeof fields.surface !== "string" || fields.surface.length === 0) return void 0;
318
+ if (!Array.isArray(fields.elements)) return void 0;
319
+ const elementIds = fields.elements.filter((id) => typeof id === "string" && id.length > 0);
320
+ if (elementIds.length === 0) return void 0;
321
+ return {
322
+ surface: fields.surface,
323
+ ...typeof fields.document === "string" ? { document: fields.document } : {},
324
+ ...typeof fields.path === "string" && fields.path.length > 0 ? { path: fields.path } : {},
325
+ elementIds
326
+ };
327
+ }
328
+ function targetForMessage(snapshot, messageId, document2) {
329
+ for (const node of snapshot.nodes.values()) {
330
+ const target = selectionTargetOf(node, messageId, document2);
331
+ if (target !== void 0) return target;
332
+ }
333
+ return void 0;
334
+ }
335
+ function snapshotOf(ctx, sessionId) {
336
+ const sessions = ctx.get("sessions");
337
+ const conversation = ctx.get("uiConversation");
338
+ if (sessions === void 0 || conversation === void 0) return void 0;
339
+ const binding = sessions.binding(sessionId);
340
+ if (binding === void 0 || binding === null) return void 0;
341
+ return conversation.binding(binding)?.target("chat").getSnapshot();
342
+ }
343
+ function encodeSegment(segment) {
344
+ return encodeURIComponent(segment).replace(/%3A/giu, ":");
345
+ }
346
+ function fileAddressOf(sessionId, cwd, path) {
347
+ const normalized = path.replace(/\\/gu, "/");
348
+ const root = cwd === void 0 ? "" : cwd.replace(/\\/gu, "/").replace(/\/+$/u, "");
349
+ const relative = root !== "" && normalized.startsWith(`${root}/`) ? normalized.slice(root.length + 1) : normalized;
350
+ const encoded = relative.split("/").map(encodeSegment).join("/");
351
+ return `dsh-resource://file/session/${encodeSegment(sessionId)}/${encoded}`;
352
+ }
353
+ function openDocument(ctx, sessionId, path) {
354
+ const sidebar = ctx.get("sidebarRight");
355
+ if (sidebar === void 0) return;
356
+ const sessions = ctx.get("sessions");
357
+ const cwd = sessions?.list.getSnapshot().byId[sessionId]?.cwd;
358
+ try {
359
+ sidebar.openResource(fileAddressOf(sessionId, cwd, path));
360
+ } catch {
361
+ }
362
+ }
363
+ function clickedChip(target) {
364
+ if (!(target instanceof Element)) return void 0;
365
+ const chip = target.closest(`[${CHIP_ATTRIBUTE}]`);
366
+ if (chip === null) return void 0;
367
+ const token = chip.getAttribute("title");
368
+ if (token === null || !token.startsWith("@")) return void 0;
369
+ const sessionId = chip.closest(`[${SESSION_ATTRIBUTE}]`)?.getAttribute(SESSION_ATTRIBUTE);
370
+ const nodeKey = chip.closest(`[${NODE_KEY_ATTRIBUTE}]`)?.getAttribute(NODE_KEY_ATTRIBUTE);
371
+ if (sessionId === null || sessionId === void 0 || nodeKey === null || nodeKey === void 0) return void 0;
372
+ return { token, sessionId, nodeKey };
373
+ }
374
+ function installTranscriptFocus(ctx, bridge) {
375
+ const onClick = (event) => {
376
+ const chip = clickedChip(event.target);
377
+ if (chip === void 0) return;
378
+ const label = selectionLabelOf(chip.token);
379
+ const document2 = label?.document ?? documentOfMention(chip.token);
380
+ if (document2 === void 0) return;
381
+ const snapshot = snapshotOf(ctx, chip.sessionId);
382
+ const messageId = snapshot?.nodes.get(chip.nodeKey)?.id;
383
+ const target = typeof messageId === "string" && snapshot !== void 0 ? targetForMessage(snapshot, messageId, document2) : void 0;
384
+ if (target === void 0) {
385
+ if (label !== void 0) event.stopPropagation();
386
+ return;
387
+ }
388
+ if (label !== void 0) {
389
+ event.stopPropagation();
390
+ if (target.path !== void 0) openDocument(ctx, chip.sessionId, target.path);
391
+ }
392
+ bridge.requestFocus(target.surface, {
393
+ ...target.document === void 0 ? {} : { document: target.document },
394
+ elementIds: target.elementIds
395
+ });
396
+ };
397
+ document.addEventListener("click", onClick, true);
398
+ return () => {
399
+ document.removeEventListener("click", onClick, true);
400
+ };
401
+ }
402
+
274
403
  // src/client/locales.ts
275
404
  var NS = "surface-bridge";
276
405
  var zh = {
@@ -299,6 +428,7 @@ function apply(ctx) {
299
428
  const bridge = new SurfaceBridgeRegistry();
300
429
  ctx.effect(() => ctx.reflect.provide("surfaceBridge", bridge), "dsh-surface-bridge: client registry");
301
430
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-surface-bridge: dictionaries");
431
+ ctx.effect(() => installTranscriptFocus(ctx, bridge), "dsh-surface-bridge: transcript focus");
302
432
  ctx.slots.inject("conversation.input.overlay", () => ctx.slots.register({
303
433
  name: "conversation.input.overlay",
304
434
  id: CHIP_ENTRY_ID,
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Browser half of dsh-surface-bridge.
3
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.
4
+ * Three things live here and nothing else: the `ctx.surfaceBridge` registry a
5
+ * business surface publishes into, the single `conversation.input.dock` entry
6
+ * that renders every surface's selection, and the listener that turns a click on a
7
+ * sent selection's reference chip back into a request for the surface. Keeping the
8
+ * chip in this bundle — rather than letting each surface draw its own — is what
9
+ * makes the composer row stable as more surfaces adopt the seam.
9
10
  *
10
11
  * @module dsh-surface-bridge/client
11
12
  */
@@ -1,16 +1,18 @@
1
1
  /**
2
2
  * Browser half of dsh-surface-bridge.
3
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.
4
+ * Three things live here and nothing else: the `ctx.surfaceBridge` registry a
5
+ * business surface publishes into, the single `conversation.input.dock` entry
6
+ * that renders every surface's selection, and the listener that turns a click on a
7
+ * sent selection's reference chip back into a request for the surface. Keeping the
8
+ * chip in this bundle — rather than letting each surface draw its own — is what
9
+ * makes the composer row stable as more surfaces adopt the seam.
9
10
  *
10
11
  * @module dsh-surface-bridge/client
11
12
  */
12
13
  import { SurfaceBridgeRegistry } from "./service.js";
13
14
  import { SurfaceSelectionChip } from "./SurfaceSelectionDock.js";
15
+ import { installTranscriptFocus } from "./transcript.js";
14
16
  import { NS, en, zh } from "./locales.js";
15
17
  /** The slot registry, the locale registry, and the composer's overlay slot declaration. */
16
18
  export const inject = ['slots', 'locale'];
@@ -31,6 +33,10 @@ export function apply(ctx) {
31
33
  const bridge = new SurfaceBridgeRegistry();
32
34
  ctx.effect(() => ctx.reflect.provide('surfaceBridge', bridge), 'dsh-surface-bridge: client registry');
33
35
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-surface-bridge: dictionaries');
36
+ // The chip in the composer is one half of the round trip; this is the other. A selection
37
+ // leaves the composer when it is sent, so the only thing left to click is the reference in
38
+ // the message — and the elements it named can only be selected by the surface's own panel.
39
+ ctx.effect(() => installTranscriptFocus(ctx, bridge), 'dsh-surface-bridge: transcript focus');
34
40
  ctx.slots.inject('conversation.input.overlay', () => ctx.slots.register({
35
41
  name: 'conversation.input.overlay',
36
42
  id: CHIP_ENTRY_ID,
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * @module dsh-surface-bridge/client/service
17
17
  */
18
- import type { SurfaceBridgeService, SurfaceSelection, SurfaceSourceDescriptor } from '../contract.ts';
18
+ import type { SurfaceBridgeService, SurfaceFocusTarget, SurfaceSelection, SurfaceSourceDescriptor } from '../contract.ts';
19
19
  export type { SurfaceBridgeService, SurfaceIconProps, SurfaceSourceDescriptor } from '../contract.ts';
20
20
  /** Default implementation of the bridge's browser-side registry. */
21
21
  export declare class SurfaceBridgeRegistry implements SurfaceBridgeService {
@@ -38,6 +38,10 @@ export declare class SurfaceBridgeRegistry implements SurfaceBridgeService {
38
38
  selection: SurfaceSelection;
39
39
  }[];
40
40
  /** @inheritdoc */
41
+ requestFocus(source: string, target: SurfaceFocusTarget): void;
42
+ /** @inheritdoc */
43
+ takeFocus(source: string, document?: string): SurfaceFocusTarget | null;
44
+ /** @inheritdoc */
41
45
  subscribe(listener: () => void): () => void;
42
46
  /** @inheritdoc */
43
47
  version(): number;
@@ -22,7 +22,7 @@ export class SurfaceBridgeRegistry {
22
22
  revision = 0;
23
23
  /** @inheritdoc */
24
24
  registerSource(descriptor) {
25
- this.entries.set(descriptor.id, { descriptor, selection: null, visible: false });
25
+ this.entries.set(descriptor.id, { descriptor, selection: null, visible: false, focus: null });
26
26
  this.bump();
27
27
  return () => {
28
28
  this.entries.delete(descriptor.id);
@@ -72,6 +72,28 @@ export class SurfaceBridgeRegistry {
72
72
  });
73
73
  }
74
74
  /** @inheritdoc */
75
+ requestFocus(source, target) {
76
+ const entry = this.entries.get(source);
77
+ if (entry === undefined)
78
+ return;
79
+ entry.focus = target;
80
+ this.bump();
81
+ }
82
+ /** @inheritdoc */
83
+ takeFocus(source, document) {
84
+ const entry = this.entries.get(source);
85
+ const focus = entry?.focus ?? null;
86
+ if (entry === undefined || focus === null)
87
+ return null;
88
+ // A request for another document stays put: the panel that can honour it is the one
89
+ // that is a view of that document, and taking it here would lose it silently.
90
+ if (document !== undefined && focus.document !== undefined && focus.document !== document)
91
+ return null;
92
+ entry.focus = null;
93
+ this.bump();
94
+ return focus;
95
+ }
96
+ /** @inheritdoc */
75
97
  subscribe(listener) {
76
98
  this.listeners.add(listener);
77
99
  return () => { this.listeners.delete(listener); };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The way back from a sent selection to the surface that produced it.
3
+ *
4
+ * A selection leaves the composer when it is sent, and the chip that carried it disappears
5
+ * with it. What stays is the reference in the message: the bridge appends the document as an
6
+ * `@document` mention, and the shell draws that as a blue, clickable chip inside the person's
7
+ * own bubble. Clicking it opens the document — that much the shell does on its own — but the
8
+ * shell knows nothing about *elements*, and the person clicked a chip that says "1 个元素".
9
+ *
10
+ * So the click is observed here. The row the chip sits in carries the identity the shell
11
+ * itself navigates by — `data-conversation-session` and `data-chat-node-key`, the same
12
+ * attributes the shell's own escape-key handler reads — and the hidden detail message the
13
+ * bridge wrote carries the rest (see the `MessageSourceMap` augmentation in the package
14
+ * entry). Together they name the surface and the elements, and the request goes back through
15
+ * the bridge to the surface's own panel, which is the only part that can select and scroll.
16
+ *
17
+ * Every step is best-effort and read-only. The click is never intercepted or prevented: if
18
+ * any of this is missing — an older shell without the attribute, a message whose detail row
19
+ * is outside the loaded window, a surface that is no longer registered — the click still does
20
+ * what the shell's own action does, which is open the document.
21
+ *
22
+ * @module dsh-surface-bridge/client/transcript
23
+ */
24
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
25
+ import type { SurfaceBridgeService } from '../contract.ts';
26
+ /**
27
+ * Decode the document name out of a reference chip's token.
28
+ *
29
+ * The token is what the bridge wrote — `@main.excalidraw`, or `@"画布 2.excalidraw"` when the
30
+ * name carries whitespace, because the shell's own mention rule ends an unquoted token at the
31
+ * first space. The chip's `title` keeps the token verbatim, which is what makes it the honest
32
+ * thing to compare against the detail row's document.
33
+ *
34
+ * @param token - The chip's `title`, or anything else.
35
+ * @returns the document name, or `undefined` when this is not a document reference.
36
+ */
37
+ export declare function documentOfMention(token: string | null | undefined): string | undefined;
38
+ /**
39
+ * Read the bridge's own chip label off a token.
40
+ *
41
+ * @param token - The chip's `title`, or anything else.
42
+ * @returns the document the label names, or `undefined` for any other token.
43
+ */
44
+ export declare function selectionLabelOf(token: string | null | undefined): {
45
+ readonly document: string;
46
+ } | undefined;
47
+ /** The parts of a Chat node this module reads. */
48
+ interface ChatNodeLike {
49
+ readonly id?: unknown;
50
+ readonly kind?: unknown;
51
+ readonly data?: {
52
+ readonly source?: unknown;
53
+ };
54
+ }
55
+ /** The parts of a Chat snapshot this module reads. */
56
+ interface ChatSnapshotLike {
57
+ readonly nodes: {
58
+ get(key: string): ChatNodeLike | undefined;
59
+ values(): readonly ChatNodeLike[];
60
+ };
61
+ }
62
+ /** One selection a click can bring back: which surface, which elements. */
63
+ export interface SelectionTarget {
64
+ /** Surface that published the selection, as its id in the bridge registry. */
65
+ readonly surface: string;
66
+ /** What the chip reads for the document, when the detail row named one. */
67
+ readonly document?: string;
68
+ /** Absolute path of the document, when the surface knew it: what the click opens. */
69
+ readonly path?: string;
70
+ /** Elements to select and bring into view. */
71
+ readonly elementIds: readonly string[];
72
+ }
73
+ /**
74
+ * Read what one Chat node says about the selection that rode with a message.
75
+ *
76
+ * The node is the bridge's own hidden detail row, matched by the message it names — the
77
+ * message id, not the row's position, because a transcript that was reloaded, compacted or
78
+ * re-ordered still keeps the pairing. The document is checked too: a click on a chip must
79
+ * never select elements in a different drawing that happens to be open.
80
+ *
81
+ * @param node - A Chat node, of any kind.
82
+ * @param messageId - Message the click came from.
83
+ * @param document - Document the clicked chip names.
84
+ * @returns the surface and elements, or `undefined` when this node is not that selection.
85
+ */
86
+ export declare function selectionTargetOf(node: ChatNodeLike | undefined, messageId: string, document: string): SelectionTarget | undefined;
87
+ /**
88
+ * Find the selection one clicked message carried.
89
+ *
90
+ * @param snapshot - Chat snapshot of the Session the click happened in.
91
+ * @param messageId - Message the clicked row belongs to.
92
+ * @param document - Document the clicked chip names.
93
+ * @returns the target, or `undefined` when the message carried no selection of that document.
94
+ */
95
+ export declare function targetForMessage(snapshot: ChatSnapshotLike, messageId: string, document: string): SelectionTarget | undefined;
96
+ /**
97
+ * The `dsh-resource://file/…` address of one path, as the shell's own file links build it.
98
+ *
99
+ * The point of matching the shell exactly is that this opens *the same tab* the file tree opens
100
+ * for that file, rather than a second one for the same drawing. A path inside the Session's
101
+ * workspace becomes the workspace-relative address; anything else keeps its absolute spelling,
102
+ * which is also what the shell does.
103
+ *
104
+ * @param sessionId - Session whose workspace resolves the path.
105
+ * @param cwd - That Session's workspace root, when known.
106
+ * @param path - Absolute path of the document.
107
+ * @returns the file address.
108
+ */
109
+ export declare function fileAddressOf(sessionId: string, cwd: string | undefined, path: string): string;
110
+ /**
111
+ * Watch the transcript for clicks on a selection's reference chip.
112
+ *
113
+ * A capture-phase listener, and for the bridge's own chip form it is the *whole* handler: that
114
+ * chip's label is written for a person to read (`main · 2 个元素`), not a path the shell could
115
+ * open, so the click is taken over here — propagation stops, the document is opened at the
116
+ * address the file tree uses, and the surface is asked to select the elements again. A plain
117
+ * file mention (what older messages carry, and what anyone types by hand) is left alone: the
118
+ * shell opens it as it always did, and this only adds the element request.
119
+ *
120
+ * Every step is best-effort: a missing Session, a message whose detail row is outside the
121
+ * loaded window, a surface that has been unloaded — each one ends the handler without touching
122
+ * the event, except that the bridge's own label is always stopped, because the shell would
123
+ * refuse it and an uncaught refusal is worse than a click that does nothing.
124
+ *
125
+ * @param ctx - Client context, read lazily for the Session, Conversation and Sidebar services.
126
+ * @param bridge - The bridge the request lands in.
127
+ * @returns the disposer that removes the listener.
128
+ */
129
+ export declare function installTranscriptFocus(ctx: ClientContext, bridge: SurfaceBridgeService): () => void;
130
+ export {};