dsh-paste-chip
Collapses long pasted text into an expandable chip in the composer: hover shows the body and where it came from, sending expands it back in place, and long text written by other plugins is taken over the same way.
Install
dsh plugin --profile web add dsh-paste-chip
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-paste-chip
<p>
<a href="https://www.npmjs.com/package/dsh-paste-chip"><img alt="npm" src="https://img.shields.io/npm/v/dsh-paste-chip?color=2f81f7&label=npm"></a>
<a href="https://www.npmjs.com/package/dsh-paste-chip"><img alt="downloads" src="https://img.shields.io/npm/dw/dsh-paste-chip?color=3fb950"></a>
<a href="https://github.com/zengxiaolou/dsh-paste-chip/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/npm/l/dsh-paste-chip?color=bf8700"></a>
<img alt="dsh plugin" src="https://img.shields.io/badge/DSH-plugin-7aa2ff">
<a href="https://github.com/zengxiaolou/dsh-paste-chip/stargazers"><img alt="stars" src="https://img.shields.io/github/stars/zengxiaolou/dsh-paste-chip?color=8250df"></a>
</p>
把超长粘贴折叠成一张可展开的引用胶囊,避免长草稿把输入框拖死。
- 粘贴阈值:≥400 字符 或 ≥12 行 折叠成一张胶囊
- 外部渠道(标注浏览器 / 划词注释 / 任何调用 API 的插件):不看长度,一律成胶囊
- 上限:20 万字符;超过直接拒绝并提示改用附件(超长正文进模型上下文也没有意义)
- 胶囊显示
粘贴「会议纪要:本周三…」· 1.2k 字(英文Pasted「…」· 1.2k chars)——带正文前缀,两个同字数的胶囊也能分辨;整块删除、整块撤销 - 发送时展开回完整正文;正文找不到就明确拦住发送,绝不把标签当正文发出去
为什么需要它
DSH 的输入框每敲一个键都在做全量工作:重算整篇投影、把整段草稿写进 localStorage。草稿一旦到几十万字,就会一卡一顿,极端情况直接把界面拖死。
这个插件把「长文本」从草稿里搬走:
没有它 · 有它
12k 字粘贴后草稿就 12k 字,每键写盘 12k+ · 草稿里只有一张 24px 高的胶囊,每键写盘回到标签大小
正文和你说的话混在一起,找不着边界 · 正文独立存放,发送时按胶囊所在位置展开
想确认粘了什么,得在几千字里翻 · 悬停或点击就能看正文前缀与上下文
为什么不改 core
DSH 的客户端是打包在 app.asar 里的,改 core 要连带改 ElectronAsarIntegrity 哈希并重签,且下一个版本会被覆盖。本插件只走三条已有公开通道:
能力 · 通道
插入胶囊 · 作用域事件 slash/input-insert-reference(内置 slash 插件插 chip 用的就是它)
拿稳定锚点 · InputActions.captureInsertion() → 带 draftRev 的 TokenSpan
发送时展开 · 注册 InputTriggerSource,实现 codec.serialize(ref)
正文不放进 ref id(core 版把 base64 塞进 ref,中文要放大 4 倍),而是按短 id 存在插件自己的 localStorage(前缀 dsh-paste-chip.v1.,最多 12 条 / 2MB,超了淘汰最旧的)。草稿里只有一个短 id,每次按键的持久化体积回到标签大小。
工作方式
1. 在 document 的捕获阶段监听 paste,只处理本插件所属 composer 的编辑器
2. 短文本直接放行给 core,不干预
3. 长文本:preventDefault() → 正文入库 → actx.bail(...) 插入胶囊
4. 插入失败:清掉刚写的正文,退回纯文本粘贴并提示——不吞内容
安装
以本地目录方式装进 profile(路径换成你自己的):
1. ~/.dsh/profiles/<profile>/package.json → dependencies 加 "dsh-paste-chip": "link:/path/to/dsh-paste-chip"
2. 同一个 package.json → dsh.profile.bundles 数组末尾加 "dsh-paste-chip" ← 真正的加载入口
3. ~/.dsh/profiles/<profile>/cordis.patch.yml → 末尾加 - id: dsh-paste-chip
然后在 profile 目录跑一次 pnpm install(需要 Node ≥22.13),再重启 DSH 客户端。
宿主启动时按 dsh.profile.bundles 的顺序叠 bundle 层,再叠 cordis.patch.yml;只加依赖和 patch 行不会加载插件。
客户端半的改动刷新页面就会重新加载;宿主半(lib/index.js)改了才需要重启。
可配置项
全部阈值都能改,两种方式二选一:
A. cordis.patch.yml(profile 里那一行加上 config)
- id: dsh-paste-chip
config:
collapseChars: 600 # 粘贴折叠阈值(字符)
collapseLines: 15 # 粘贴折叠阈值(行)
previewChars: 500 # 悬停预览多少字
B. 设置页(装了 @deepseek-ai/dsh-settings 时自动出现「dsh-paste-chip」分区)
键 · 默认 · 范围 · 作用
collapseChars · 400 · 100–10000 · 粘贴折叠阈值(字符)
collapseLines · 12 · 3–200 · 粘贴折叠阈值(行)
maxChars · 200000 · 1000–2000000 · 单段正文上限,超过拒绝
externalMinChars · 8 · 1–200 · 外部渠道插入的兜底长度,低于它视为手打
warmupMs · 1200 · 0–10000 · 挂载后预热窗口,期间不接管(防恢复草稿误折叠)
inputGraceMs · 500 · 0–3000 · 用户输入后的宽限期
previewChars · 320 · 80–2000 · 悬停浮层预览字数
contextChars · 40 · 0–200 · 悬停浮层上下文各取多少字(0 = 关闭)
hoverDelayMs · 180 · 0–1000 · 悬停多久弹出预览
leaveGraceMs · 60 · 0–1000 · 移出后多久收起
storeMaxEntries · 12 · 1–100 · 正文最多存几条,超出淘汰最旧
storeMaxBytes · 2097152 · 64KB–8MB · 正文存储总字节上限
实现方式:宿主半把这些值汇总到 GET /api/dsh-paste-chip/config,客户端启动时读一次并覆盖默认值;读不到就用同一份默认值,越界一律夹到边界(客户端与宿主各夹一次)。默认值写在 lib/client.js 的 DEFAULT_SETTINGS 与 lib/index.js 的 DEFAULTS,两边必须保持一致。
给其他插件用的主动接法(可选)
默认是被动接法:任何插件用 setDraft / insertText 写进草稿的长文本,都会被差异观察器接住,对方不用改一行代码。
需要更精确控制时(原子插入、草稿里已有胶囊、不想晚一帧),生产者调一行服务:
// 在会话槽组件里:span 必须和当前 revision 一致
const span = props.inputActions.captureInsertion()
const result = ctx.pasteChip.insert(body, { sessionId, span, label: '注释 · 3 条' })
// result: { ok: true, ref, label } | { ok: false, reason }
失败原因(都不静默):empty、too-long(超 20 万字)、span-required(没给 span)、no-session(会话 actx 解析不到)、bail-refused(插入被拒,正文已清理)。
服务版本:ctx.pasteChip.version。
预览开关的事件(v0.1.22)
常驻预览是谁打开的、谁关心它被关掉,插件自己不知道。所以给一个订阅口:
const off = ctx.get('pasteChip').watchPreview((event) => {
// { phase: 'pin', ref }
// { phase: 'unpin', ref, reason: 'close' | 'dismiss' | 'switch' | 'removed' }
})
reason 是这套 API 的全部价值,别混用:
reason · 什么动作 · 联动方一般该做什么
close · 点了卡片右上角的 ✕ · 这是唯一的「用户主动关闭」,可以据此取消对应动作
dismiss · Esc / 点浮层外部 · 收起预览而已,不要当成取消
switch · 点了另一枚胶囊,预览换过去 · 同上,旧的那条只是被替换
removed · 外部插件调了 closePreview(ref) · 胶囊已经没了,卡片顺手收掉
ctx.pasteChip.closePreview(ref):只收指定 ref 的常驻预览(不是当前预览就返回 false,不误伤别人),广播 reason: 'removed'。
服务必须用 ctx.get('pasteChip') 取。cordis 上下文里直接读没在 inject 里声明的服务会抛
cannot get property ... without inject,属性访问永远是死的。
非粘贴渠道
标注浏览器、划词询问这类插件是用 setDraft / insertText 写草稿的,不触发 paste,所以另有一条通道:比对前后两次草稿,认出「这一次新塞进来的那一段」。
这条通道没有字数/行数阈值:凡是外部渠道传进来的非空白文本,一律成胶囊。 粘贴仍保留 400 字阈值(短粘贴保持原文,避免粘个链接也变胶囊)。
怎么区分「外部传进来的」和「用户自己打的」:真实键入一定先触发 beforeinput,外部 setDraft 不会。所以 beforeinput 之后 500ms 内的草稿变化算用户输入,不接管;超出这个窗口且不是用户输入的插入,一律折叠。
三道保护,缺一个都会误伤:
保护 · 为什么
挂载后 1200ms 预热窗口内不接管 · 会话切换/页面加载会"恢复"草稿,那段文本和外部插入长得一模一样
用户输入事件后 500ms 内不接管 · beforeinput / input / keydown / compositionstart / compositionend 任一命中都算。只监听 beforeinput 不够:输入法组合提交有时不发它,实测漏了 7 次,产出「粘贴文本 · 1 字」这种胶囊
少于 8 个字的插入不接管 · 误判兜底,不是产品阈值。手打/输入法提交一次就是 1~3 字;真实外部渠道塞的是整段注释。日志里 7 次误折叠全是 1 字
空白插入不接管 · 纯换行/空格没有折叠价值
草稿里已有胶囊时跳过 · 有 chip 时 clipboard 偏移与 detect 偏移不再一致,硬插会错位替换
只处理纯插入(removed === 0) · 替换、删除的量算不准,不接管
事件:insertion.detected / insertion.collapsed / insertion.skip(带原因)/ `insertion.warmup
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::8,最后更新 2026-10-26