npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-surface-bridge

v0.1.0-alpha.9

Published

Shared seam between a right-Sidebar business surface and the DSH composer: one selection chip, one Host-context injection point, one write-back channel

Readme

dsh-surface-bridge

DSH 右侧栏的业务面(画布、表格、Notebook)与 DSH 输入栏之间的唯一接缝:一条 chip、一处上下文注入点、一条回写通道。

┌─────────────┐  publish(selection)  ┌────────────────────────┐
│ 业务插件    │ ───────────────────► │ dsh-surface-bridge     │
│ (画布/表格) │ ◄─────────────────── │  · 输入行上的 chip      │
└─────────────┘   同一个长轮询        │  · 注册表(仅本地)      │
        ▲                             └───────────┬────────────┘
        │  read:现在选了什么?                     │ agent/pre-step
        └──────────────────────────────────────────┘
                                                 ▼
                                        user/message(上下文)

关键:选择是"被读"的,不是"被推"的。 用户画图、框选时没有任何网络流量;只有当一条消息要发出去、模型步骤开始准备时才向浏览器要一次。publish 是纯本地动作(驱动 chip + 作为那次读取的答案),不碰网络。

为什么它必须是一个已挂载的 bundle,而不是 packages/ 里的纯库

这是本包存在的全部理由。设想把联动逻辑写成一个库,让每个业务插件 import:

| 方案 | 结果 | | --- | --- | | 每个插件各写一套 chip + 注入 | 输入栏里 N 个 chip 抢位置;同一套 Host 注入被复制 N 次;视觉与行为各自漂移 | | 联动逻辑做成纯库,各插件 import | 看起来更轻,但库会被分别打进每个插件自己的 client bundle。于是 ctx 上出现 N 份服务实例,各自注册一个 conversation.input.dock 条目 —— chip 依然是 N 个。纯库无法保证"全局唯一" | | 联动逻辑做成独立 bundle(本包) | 唯一实例由 DSH 的 Loader 保证;业务插件只 inject 服务 + registerSource(...) |

因此本包是一个独立 bundle,而不是一个给各插件 import 的库:唯一实例由 DSH 的 Loader 保证,业务插件只 inject 服务 + registerSource(...)。

给业务插件用的 API

浏览器侧:ctx.surfaceBridge

export const inject = ['surfaceBridge', 'locale'] as const

export function apply(ctx: ClientContext): void {
  ctx.effect(() => ctx.surfaceBridge.registerSource({
    id: 'excalidraw',
    label: '画布',
    icon: IconEditOutlineRegular,
    order: 10,
    reveal: () => { ctx.sidebarRight.openTab('data-excalidraw') },   // chip 上的「定位」
    focusElement: (id) => { canvas.focusElement(id) },               // 点摘要某一行
  }), 'my-surface: source')

  // 用户选择变化时:
  ctx.surfaceBridge.publish('excalidraw', selectionOrNull)
}

面板侧还要接住"点击 transcript 里那条选区 chip":桥把请求放进注册表,面板取走并选中元素。

const request = ctx.surfaceBridge.takeFocus('excalidraw', scene)   // 只取点名自己这张图的请求
if (request !== null) selectAndScrollIntoView(request.elementIds)

selection 的形状见 src/contract.ts 的 SurfaceSelection:源只投影元素行(id / 类型 / 文本 / 坐标 / 尺寸 / 非默认样式 / 连接关系),桥拥有信封(revision、count、bounds、模型文本、可选图片)。文件类的面还可以给 resource.display(人读的名字,如 main)——桥不知道什么是扩展名,也不该知道。

图片不走文本。 大载荷放进 selection.images[],每条用 elementId 指向自己的元素;Host 把它们落成 durable 附件并以真正的 image 块追加到上下文。没有图片元素的选择,一个字节的图片都不产生。

Host 侧:ctx.surfaceBridgeHost

const live = ctx.surfaceBridgeHost.isSurfaceLive(sessionId)                  // 画布是否开着
const all = await ctx.surfaceBridgeHost.readSelections(sessionId, signal)    // 现在选了什么
const one = await ctx.surfaceBridgeHost.readSelection(sessionId, 'excalidraw', signal)
const results = await ctx.surfaceBridgeHost.apply(sessionId, 'excalidraw',   // 回写并等结果
  [{ op: 'update', payload: { id, fields } }], exec.signal)

apply 在画布未打开或超时未回传时返回明确的失败结果,绝不假装成功。

上下文是怎么进模型的

Host 半在 agent/pre-step 里做三件事,而且只在带着新用户输入的那一步做:

  1. 若该 Session 有面在轮询(isSurfaceLive),向浏览器读一次当前选择;
  2. 把选择并进用户自己那条消息的最前面:一个 @"<显示名> · N 个元素" mention(shell 把整个引号内容渲染成用户气泡里那个蓝色、带文件图标的引用 chip);图片以真正的 image 块加进同一条消息;
  3. 追加一条隐藏的明细消息(source.kind = 'surface-selection')装元素表,并记下这个 turn 已经读过,同一 turn 的后续 step 不再读。

为什么是一行、而且长在用户自己的气泡里。 Chat 视图对非 user 源的上下文节点不渲染任何行(isVisibleChatNode 把普通 Context 全部排除),所以可见的那一半必须是 source.kind = 'user' 的消息。而用户消息里行内有颜色的东西只有一种:@… 引用 chip,而且它的显示文本就是 token 里的最后一段——所以「文件 + 个数一起变蓝」只能把它们塞进同一个引号 token 里。于是选择以 mention 的形式并进用户那句话 —— 另起一条消息就会变成用户看到的"两行"。

  • 只在发送时读取:用户画图期间什么都不发。这也让"发送时看到的选择"就是消息真正带上的选择。
  • 一个 turn 只读一次:一个 turn 有多个 step(模型调用、工具结果、再调用),否则同一张图会被注入三遍。
  • 不可见的面答空:读取的答案来自同一个注册表,而注册表只列"在屏幕上且有选择"的面,所以切会话/切标签后,旧选择既不会显示也不会被发送。
  • 无浏览器则跳过:没有面在轮询(CLI、未打开过画布标签页)直接返回,不阻塞消息。
  • 没有用户消息可并时(turn 由通知、定时唤醒或恢复的 goal 打开):可见块另起一条 user 消息 —— 宁可多一行,也不能让选择只进模型、不进对话。
  • 形态与 dsh-time-context、dsh-session-reference 一致:都是官方认可的"生产者注入 user-role 上下文"路径。

点击那条 chip 之后。 chip 的文本是给人读的(main · 3 个元素),不是路径 —— 所以 shell 自己那个"打开这个文件"的动作在这里用不了(它会把整串当路径去 claim,然后抛错)。于是这次点击由桥的客户端半边接管:捕获阶段 stopPropagation,自己打开这张图的文件地址(和文件树同一个标签页),再把请求交给画布面板。识别、定位、打开都靠下面这张表:

| 需要的东西 | 来自哪里 | | --- | --- | | 哪条消息 | 行上的 data-chat-node-key(shell 自己滚动/锚点也用它)+ data-conversation-session | | 这是不是本包的 chip | chip 的 title 是否形如 @"<名称> · N 个元素" | | 这条消息带了哪个选择 | 隐藏明细消息的 source:surface / message / document / elements | | 打开哪个文件 | 同一条明细的 source.path(绝对路径)→ 按 shell 的语法组成 dsh-resource://file/…,与文件树点开同一个标签页 | | 谁来选中元素 | 该面自己的面板:requestFocus / takeFocus,请求一直留着直到面板接住(点击本身往往就是打开这个面板的动作) |

监听是捕获阶段、只读、从不 preventDefault;只对本包自己的 label 形态(@"… · N 个元素")接管,别人手打的 @path 或助手消息里的文件引用照旧由 shell 打开。任何一步缺失(老 shell 没有那个属性、明细消息不在已加载窗口里、面已卸载)都只是这一次点击什么都不做,不会抛错、也不会误开别的文件。明细消息的 source 是簿记而非模型内容,因此不花一个 prompt token。

路由

全部挂在平台共享 API 通道(ctx.connection.fetch.register)上,因此继承平台的信任与鉴权围栏,不另开 webserver:

只有两条路由,服务于一个双向长轮询:

| 路由 | 方向 | 用途 | | --- | --- | --- | | GET /api/data-canvas/ops?sessionId=&hold= | 浏览器 ← Host | 长轮询(最长 20s):取待执行操作,以及 Host 的读取请求 | | POST /api/data-canvas/settle | 浏览器 → Host | 回传执行结果,或回传一次读取的答案 |

读取复用了同一条队列(READ_SELECTION_OP,不带 source —— 只有浏览器知道有哪些面、哪些在屏幕上),所以浏览器不需要第二个监听器,一次读取就是一个已经parked 的连接上的往返。路径常量在 src/contract.ts 里只写一遍。

校验没有消失,只是换了一端:旧设计要在 ingest 端校验推送来的选择,现在改为在读取答案上校验(src/host/narrow.ts)——浏览器仍然是边界,只是方向反了。

扩展一个新业务面

  1. 建一个带 dsh.client 的 bundle,dsh.client.inject 里加上 dsh-surface-bridge(Loader 会保证桥先注册)。
  2. inject: ['surfaceBridge', ...],registerSource 一个描述符,并在可见性变化时 setVisible。
  3. 用户选择变化时把投影好的 SurfaceSelection publish 到注册表(本地)。
  4. 自己的操作循环里把 READ_SELECTION_OP 答成 [selection] 或 []。
  5. Host 侧工具通过 ctx.surfaceBridgeHost 读选择、送操作。

chip、输入行占位、agent/pre-step 注入、图片附件化、长轮询回写全部由本包负责。

已知边界

  • 读取窗口是 600ms。 一次读取最多等这么久就拿不到答案(消息照发,只是不带选择)。本地环回上是毫秒级;设短是为了不为了上下文而拖延用户的消息。
  • 读取靠"有面在轮询"判定。 从未打开过画布标签页的会话读不到东西——这是对的,那张画布根本不在屏幕上。
  • 每个面各自轮询。 多个面同时挂在一个会话上时,读取请求会被先轮询到的那个取走。当前只有一个面,此路径正确;接第二个面时应把轮询收归桥的客户端半边(按 operation.source 分派)。
  • 回写不做持久队列。 操作队列是同一个进程里两半之间的交接通道,画布关掉即失去意义;因此工具失败要响,而不是排队等。
  • 点击定位依赖 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 侧改动。
  • chip 的 label 形态是握手协议。 宿主写 @"<显示名> · N 个元素",客户端按同一条正则读回来;两边改动必须同时。写歪了不会误伤别人:识别不出来的 chip 交回 shell,只是点不动。
  • 一个面同时只能挂一个待处理定位请求。 连点两次 chip 是"看后一次",不排队;这符合"再点一次"的直觉,也避免旧请求在新画布上突然生效。

源码

发布的包里带 src/。构建是两个 half 一起出:

pnpm build   # tsc(Host 半 + 类型)+ esbuild(唯一 client.js)
pnpm test