dsh-plugin-sprig 0.0.0-stage → 1.0.0

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
@@ -1,3 +1,42 @@
1
- # Temporary Holding Version
1
+ # dsh-plugin-sprig
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ DSH client plugin that turns selected conversation text into a [Sprig](https://github.com/thatwzzh/sprig) explanation card.
4
+
5
+ Select text in a DSH conversation → the native quote bar becomes **Copy / Quote / Explain with Sprig**.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ dsh plugin add dsh-plugin-sprig
11
+ ```
12
+
13
+ Or from DSH settings → **Plugins → Add plugin** and paste `dsh-plugin-sprig`.
14
+
15
+ Then make sure **Sprig is running** and **cross-app selection is enabled** in Sprig's settings, and refresh the DSH page (or start a new session).
16
+
17
+ ## What it does
18
+
19
+ - Reuses DSH's own `quote` surface (same key, lower priority) instead of adding a second right-click menu that duplicates the native Copy.
20
+ - Sends at most **one real user/assistant message either side** of the selection — `<system-reminder>`, `available_skills`, and context nodes are filtered out, capped at 1800 characters.
21
+ - Delivers the payload to a **running Sprig** over the local bridge on `127.0.0.1:53917`. Sprig creates the card and explains.
22
+ - Sprig carries the source with the explanation, so a selection inside a DSH answer still knows which conversation it came from.
23
+
24
+ ## Requirements
25
+
26
+ - Sprig running on the same machine, with **cross-app selection** enabled.
27
+ - DSH **Web UI**. The terminal/CLI does not work — there is no browser surface to hook into.
28
+
29
+ ## Limitations
30
+
31
+ - The plugin hooks DSH's internal quote surface. A major DSH upgrade may change that surface and require an update here.
32
+ - The bridge listens on loopback only, has no token, and can only create explanation cards. It never reads local data.
33
+ - Request logs go to `/tmp/sprig-bridge.log`.
34
+
35
+ ## Links
36
+
37
+ - Repo: [thatwzzh/sprig](https://github.com/thatwzzh/sprig) · plugin source at [`dsh-plugins/sprig`](https://github.com/thatwzzh/sprig/tree/main/dsh-plugins/sprig)
38
+ - Sprig site: [sprig-site.app.workbuddy.host](https://sprig-site.app.workbuddy.host/?sharecode=HVF3SOVDGG)
39
+
40
+ ## License
41
+
42
+ MIT
package/client.js ADDED
@@ -0,0 +1,210 @@
1
+ /**
2
+ * DSH Client 插件:把「复制 / 引用 / 用知枝解释」合并到 DSH 原有的选词操作条。
3
+ *
4
+ * 做法:复用 message-quotes 的 selection 交互(由它 request.open() 打开 quote 表面),
5
+ * 但把 key='quote' 的表面替换成同时包含三个操作的组件,避免右键时和原生 Copy 菜单叠在一起。
6
+ */
7
+ window.__ModuleLoader__.load({
8
+ id: '@local/sprig',
9
+ factory(require) {
10
+ const React = require('react')
11
+ const h = React.createElement
12
+ const BRIDGE = 'http://127.0.0.1:53917/v1/explain'
13
+
14
+ // 只取用户/助手真实对话;system-reminder、技能清单、注入上下文不参与解释。
15
+ const SYSTEM_REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>/gi
16
+ const AVAILABLE_SKILLS = /<available_skills>[\s\S]*?<\/available_skills>/gi
17
+
18
+ function cleanText(raw) {
19
+ return String(raw || '')
20
+ .replace(SYSTEM_REMINDER, '')
21
+ .replace(AVAILABLE_SKILLS, '')
22
+ .trim()
23
+ }
24
+
25
+ function nodeText(node) {
26
+ if (!node) return ''
27
+ if (node.kind === 'user' || node.kind === 'steering') {
28
+ return cleanText((node.content || [])
29
+ .filter(block => block && block.type === 'text' && typeof block.text === 'string')
30
+ .map(block => block.text)
31
+ .join('\n'))
32
+ }
33
+ if (node.kind === 'assistant') {
34
+ return cleanText((node.blocks || [])
35
+ .filter(block => block && block.kind === 'text' && typeof block.text === 'string')
36
+ .map(block => block.text)
37
+ .join('\n'))
38
+ }
39
+ // context(系统注入)、tool、command 等节点不是对话上下文
40
+ return ''
41
+ }
42
+
43
+ function roleLabel(node) {
44
+ if (!node) return '消息'
45
+ if (node.kind === 'assistant') return '助手'
46
+ return '用户'
47
+ }
48
+
49
+ function excerptAround(text, selection, limit) {
50
+ if (text.length <= limit) return text
51
+ const index = text.indexOf(selection)
52
+ if (index < 0) return text.slice(0, limit) + '…'
53
+ const before = Math.max(0, Math.floor((limit - selection.length) / 2))
54
+ const start = Math.max(0, index - before)
55
+ const head = start > 0 ? '…' : ''
56
+ const tail = start + limit < text.length ? '…' : ''
57
+ return head + text.slice(start, start + limit) + tail
58
+ }
59
+
60
+ function buildContext(nodes, selection) {
61
+ const items = (nodes || [])
62
+ .filter(node => node && (node.kind === 'user' || node.kind === 'steering' || node.kind === 'assistant'))
63
+ .map(node => ({ node, text: nodeText(node) }))
64
+ .filter(item => item.text.length > 0)
65
+
66
+ if (items.length === 0) return ''
67
+ const matched = items.findIndex(item => item.text.includes(selection))
68
+ const center = matched >= 0 ? matched : items.length - 1
69
+ const indexes = []
70
+ if (center - 1 >= 0) indexes.push(center - 1)
71
+ indexes.push(center)
72
+ if (center + 1 < items.length) indexes.push(center + 1)
73
+
74
+ const blocks = []
75
+ let remaining = 1800
76
+ for (const index of indexes) {
77
+ if (remaining < 80) break
78
+ const item = items[index]
79
+ const selected = index === center
80
+ const budget = Math.min(selected ? 1000 : 400, remaining - 20)
81
+ const text = excerptAround(item.text, selection, budget)
82
+ const block = `【${roleLabel(item.node)}】\n${text}`
83
+ if (block.length <= remaining) {
84
+ blocks.push(block)
85
+ remaining -= block.length
86
+ }
87
+ }
88
+ return blocks.join('\n\n').slice(0, 1800)
89
+ }
90
+
91
+ const baseButton = {
92
+ height: 26,
93
+ padding: '0 11px',
94
+ borderRadius: 7,
95
+ border: '1px solid var(--dsw-alias-border, rgba(127,127,127,.35))',
96
+ background: 'var(--dsw-alias-bg-elevated, #fff)',
97
+ color: 'var(--dsw-alias-text-primary, #111)',
98
+ font: '12px -apple-system, system-ui, sans-serif',
99
+ cursor: 'pointer',
100
+ whiteSpace: 'nowrap',
101
+ }
102
+
103
+ function SelectionActions({ dismiss, readText, clearSelection, inputActions, useSession, useChat, sessionId }) {
104
+ const enabled = useSession(s => !s.removed && (s.subagent === null || s.subagent.address.mode === 'continuable'))
105
+ const nodes = useChat(snapshot => (snapshot && snapshot.legacy ? snapshot.legacy.nodes : []))
106
+ const [busy, setBusy] = React.useState(false)
107
+ const [message, setMessage] = React.useState('')
108
+
109
+ React.useEffect(() => {
110
+ if (!enabled) dismiss()
111
+ }, [enabled, dismiss])
112
+ if (!enabled) return null
113
+
114
+ const currentText = () => {
115
+ const text = readText()
116
+ if (text === null || !text.trim()) {
117
+ dismiss()
118
+ return null
119
+ }
120
+ return text
121
+ }
122
+
123
+ const copy = async () => {
124
+ const text = currentText()
125
+ if (text === null) return
126
+ try {
127
+ await navigator.clipboard.writeText(text)
128
+ clearSelection()
129
+ dismiss()
130
+ } catch (error) {
131
+ setMessage('复制失败')
132
+ }
133
+ }
134
+
135
+ const quote = () => {
136
+ const text = currentText()
137
+ if (text === null) return
138
+ if (!inputActions.insertQuote(text)) {
139
+ setMessage('当前会话不能引用')
140
+ return
141
+ }
142
+ clearSelection()
143
+ dismiss()
144
+ }
145
+
146
+ const explain = async () => {
147
+ const selection = currentText()
148
+ if (selection === null) return
149
+ setBusy(true)
150
+ setMessage('')
151
+ try {
152
+ const response = await fetch(BRIDGE, {
153
+ method: 'POST',
154
+ headers: { 'Content-Type': 'application/json' },
155
+ body: JSON.stringify({
156
+ selection,
157
+ context: buildContext(nodes, selection),
158
+ title: 'DSH 对话',
159
+ appName: 'DeepSeek Harness',
160
+ sessionID: String(sessionId || ''),
161
+ messageRole: 'assistant',
162
+ }),
163
+ })
164
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
165
+ clearSelection()
166
+ dismiss()
167
+ } catch (error) {
168
+ setMessage('知枝未运行,或本机 bridge 被占用;先打开知枝再试')
169
+ setBusy(false)
170
+ }
171
+ }
172
+
173
+ return h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
174
+ h('button', {
175
+ type: 'button',
176
+ style: baseButton,
177
+ onPointerDown: event => event.preventDefault(),
178
+ onClick: copy,
179
+ }, '复制'),
180
+ h('button', {
181
+ type: 'button',
182
+ style: baseButton,
183
+ onPointerDown: event => event.preventDefault(),
184
+ onClick: quote,
185
+ }, '引用'),
186
+ h('button', {
187
+ type: 'button',
188
+ style: { ...baseButton, fontWeight: 500 },
189
+ disabled: busy,
190
+ onPointerDown: event => event.preventDefault(),
191
+ onClick: explain,
192
+ }, busy ? '正在发送…' : '用知枝解释'),
193
+ message ? h('span', { style: { fontSize: 11, opacity: 0.7, maxWidth: 260 } }, message) : null)
194
+ }
195
+
196
+ return {
197
+ inject: ['slots'],
198
+ apply(ctx) {
199
+ // key 复用 quote:不新增表面,直接替换为三合一的选词操作条。
200
+ ctx.slots.inject('conversation.chat.text-interaction', () => ctx.slots.register({
201
+ name: 'conversation.chat.text-interaction',
202
+ key: 'quote',
203
+ // message-quotes 已以 priority 0 占了 quote;同一 key 必须用更低的 priority
204
+ // 才能覆盖(slot 规则:数值越低越优先渲染)。
205
+ priority: -10,
206
+ }, SelectionActions))
207
+ },
208
+ }
209
+ },
210
+ })
@@ -0,0 +1,4 @@
1
+ - insert:
2
+ - id: sprig-dsh
3
+ name: '@local/sprig'
4
+ config: {}
package/docs/DETAIL.md ADDED
@@ -0,0 +1,18 @@
1
+ # 知枝选词解释
2
+
3
+ 在 DSH 对话里选中文字后,原来的「引用」操作条会变成 **复制 / 引用 / 用知枝解释** 三个按钮,不再额外弹一个和原生 Copy 重叠的右键菜单。
4
+
5
+ - 插件只读取选中文字,以及选中消息前后各最多一条**真实用户/助手消息**;系统注入的 `<system-reminder>`、技能清单、context 节点会被过滤,上下文总长 ≤1800 字。
6
+ - 通过本机 `127.0.0.1:53917` 的 bridge 发送给 **正在运行的知枝**,由知枝建卡并流式解释。
7
+ - 不会把内容上传到第三方;上下文只在本机 DSH 和知枝之间传递。
8
+
9
+ ## 前置条件
10
+
11
+ 1. 知枝已启动(菜单栏有图标)。
12
+ 2. 知枝设置里启用了「启用跨应用划词」。
13
+ 3. 选中 DSH 中已展示的用户内容或已完成的 Assistant 文本;工具、注入上下文、流式输出不参与选区交互。
14
+
15
+ ## 限制
16
+
17
+ - 这是 DSH Client 插件,只出现在 Web UI;终端/CLI 不生效。
18
+ - 知枝退出后按钮会提示「知枝未运行」,重试即可。
package/icon.svg ADDED
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <rect width="64" height="64" rx="14" fill="#2f6feb"/>
3
+ <path d="M32 13l4.2 13.8L50 31l-13.8 4.2L32 49l-4.2-13.8L14 31l13.8-4.2z" fill="#fff"/>
4
+ </svg>
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ /** Host half of the Sprig bundle; the Client module owns selection UI. */
2
+ export function apply() {}
package/locale/en.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "meta": {
3
+ "title": "Sprig selection explain",
4
+ "description": "Select text in a DSH conversation and send it with nearby messages to the local Sprig app for an explanation card. Sprig must be running.",
5
+ "detail": "docs/DETAIL.md"
6
+ }
7
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "meta": {
3
+ "title": "知枝选词解释",
4
+ "description": "选中 DSH 对话里的文字,点「用知枝解释」,把选区、相邻对话上下文和来源发送到本机知枝生成解释卡片。需要知枝正在运行。",
5
+ "detail": "docs/DETAIL.md"
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "dsh-plugin-sprig",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "DSH client plugin: send selected conversation text to Sprig and get a source-aware explanation card.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/thatwzzh/sprig.git",
10
+ "directory": "dsh-plugins/sprig"
11
+ },
12
+ "homepage": "https://github.com/thatwzzh/sprig/tree/main/dsh-plugins/sprig",
13
+ "bugs": {
14
+ "url": "https://github.com/thatwzzh/sprig/issues"
15
+ },
16
+ "keywords": [
17
+ "dsh",
18
+ "dsh-plugin",
19
+ "sprig",
20
+ "selection",
21
+ "explain",
22
+ "macos"
23
+ ],
24
+ "exports": {
25
+ ".": "./index.js",
26
+ "./client": "./client.js",
27
+ "./locale/*.json": "./locale/*.json",
28
+ "./icon": "./icon.svg"
29
+ },
30
+ "files": [
31
+ "index.js",
32
+ "client.js",
33
+ "cordis.patch.yml",
34
+ "locale/*.json",
35
+ "docs",
36
+ "icon.svg",
37
+ "README.md"
38
+ ],
39
+ "dsh": {
40
+ "bundle": {
41
+ "patch": "./cordis.patch.yml"
42
+ },
43
+ "client": {
44
+ "platform": "web",
45
+ "immediately": true,
46
+ "inject": [
47
+ "@deepseek-ai/dsh-client-ui-conversation",
48
+ "@deepseek-ai/dsh-client-ui-chat",
49
+ "@deepseek-ai/dsh-message-quotes"
50
+ ]
51
+ }
52
+ }
53
+ }