dsh-synapse 0.4.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/LICENSE +21 -0
- package/README.md +81 -0
- package/app.js +1252 -0
- package/client.js +217 -0
- package/cordis.patch.yml +13 -0
- package/deepseek-mark.svg +3 -0
- package/docs/architecture.md +104 -0
- package/docs/development.md +137 -0
- package/docs/en/README.md +161 -0
- package/docs/images/native-webui.png +0 -0
- package/docs/images/synapse-ui.png +0 -0
- package/docs/zh-CN/README.md +154 -0
- package/index.js +802 -0
- package/package.json +44 -0
- package/styles.css +337 -0
package/client.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-synapse',
|
|
3
|
+
factory: () => {
|
|
4
|
+
const module = { exports: {} }
|
|
5
|
+
const currentSession = ctx => {
|
|
6
|
+
const snapshot = ctx.sessions.list.getSnapshot()
|
|
7
|
+
const id = snapshot.current
|
|
8
|
+
if (id === undefined) return null
|
|
9
|
+
const session = snapshot.byId[id]
|
|
10
|
+
return session === undefined ? null : { id, title: session.displayTitle, cwd: session.cwd ?? null }
|
|
11
|
+
}
|
|
12
|
+
const sessionSnapshot = ctx => {
|
|
13
|
+
const snapshot = ctx.sessions.list.getSnapshot()
|
|
14
|
+
return snapshot.ids.map(id => {
|
|
15
|
+
const session = snapshot.byId[id]
|
|
16
|
+
return session === undefined ? null : { id, title: session.displayTitle, cwd: session.cwd ?? null, parentId: session.parentId ?? null, blank: session.blank }
|
|
17
|
+
}).filter(Boolean)
|
|
18
|
+
}
|
|
19
|
+
const workspaceSnapshot = ctx => {
|
|
20
|
+
const sessions = ctx.sessions.list.getSnapshot()
|
|
21
|
+
const snapshot = ctx.workspaces.list.getSnapshot()
|
|
22
|
+
const accounted = new Set(snapshot.items.flatMap(workspace => workspace.sessionIds))
|
|
23
|
+
return [
|
|
24
|
+
...snapshot.items.map(workspace => ({ id: workspace.workspaceId, title: workspace.title, path: workspace.path, sessionIds: workspace.sessionIds })),
|
|
25
|
+
{ id: 'dsh-ungrouped', title: '未分组', path: null, sessionIds: sessions.ids.filter(id => !accounted.has(id)) },
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports.inject = ['sessions', 'workspaces']
|
|
30
|
+
module.exports.apply = ctx => {
|
|
31
|
+
const prompt = async (sessionId, text) => {
|
|
32
|
+
const scope = ctx.sessions.scope(sessionId)
|
|
33
|
+
const session = scope === undefined ? undefined : ctx.sessions.sessionOf(scope)
|
|
34
|
+
if (session === undefined) throw new Error('关联的 DSH 会话已不可用')
|
|
35
|
+
const result = await session.prompt([{ type: 'text', text }], 'queue')
|
|
36
|
+
if (!result.ok) throw new Error(result.error?.message ?? 'DSH 未接受这条消息')
|
|
37
|
+
}
|
|
38
|
+
const style = document.createElement('style')
|
|
39
|
+
style.textContent = '.dsh-synapse-switch{position:fixed;z-index:80;top:12px;left:50%;display:flex;gap:2px;transform:translateX(-50%);border:1px solid #d1d5db;border-radius:999px;background:rgba(255,255,255,.96);padding:3px;backdrop-filter:blur(10px)}.dsh-synapse-switch button{height:28px;border:0;border-radius:999px;background:transparent;padding:0 11px;color:#6b7280;font:600 12px Inter,system-ui,sans-serif;cursor:pointer;white-space:nowrap}.dsh-synapse-switch button:hover{background:#f3f4f6;color:#111827}.dsh-synapse-switch button.active{background:#111827;color:#fff}.dsh-synapse-switch button:focus-visible{outline:2px solid #111827;outline-offset:2px}.dsh-synapse-overlay{position:fixed;z-index:100;inset:0;background:#f5f7fa}.dsh-synapse-overlay.is-opening{visibility:hidden}.dsh-synapse-overlay[hidden]{display:none}.dsh-synapse-overlay iframe{display:block;width:100%;height:100%;border:0}'
|
|
40
|
+
document.head.append(style)
|
|
41
|
+
const host = document.createElement('div')
|
|
42
|
+
host.className = 'dsh-synapse-host'
|
|
43
|
+
host.innerHTML = '<div class="dsh-synapse-switch" role="group" aria-label="视图切换"><button type="button" data-view="dialog" class="active" aria-pressed="true">对话</button><button type="button" data-view="map" aria-pressed="false">会话地图</button></div><section class="dsh-synapse-overlay" hidden><iframe title="会话地图" src="/synapse/"></iframe></section>'
|
|
44
|
+
document.body.append(host)
|
|
45
|
+
const dialogButton = host.querySelector('[data-view="dialog"]')
|
|
46
|
+
const mapButton = host.querySelector('[data-view="map"]')
|
|
47
|
+
const overlay = host.querySelector('.dsh-synapse-overlay')
|
|
48
|
+
const frame = host.querySelector('iframe')
|
|
49
|
+
|
|
50
|
+
const setView = view => {
|
|
51
|
+
const showingMap = view === 'map'
|
|
52
|
+
dialogButton.classList.toggle('active', !showingMap)
|
|
53
|
+
dialogButton.setAttribute('aria-pressed', String(!showingMap))
|
|
54
|
+
mapButton.classList.toggle('active', showingMap)
|
|
55
|
+
mapButton.setAttribute('aria-pressed', String(showingMap))
|
|
56
|
+
}
|
|
57
|
+
const close = () => {
|
|
58
|
+
window.clearTimeout(mapOpenFallback)
|
|
59
|
+
mapOpening = false
|
|
60
|
+
overlay.classList.remove('is-opening')
|
|
61
|
+
overlay.hidden = true
|
|
62
|
+
setView('dialog')
|
|
63
|
+
}
|
|
64
|
+
const send = (type, payload) => { frame.contentWindow?.postMessage({ source: 'dsh-synapse', type, ...payload }, location.origin) }
|
|
65
|
+
let syncQueued = false
|
|
66
|
+
let knownSessionIds = new Set()
|
|
67
|
+
const liveUnsubscribers = new Map()
|
|
68
|
+
const syncLiveSessions = () => {
|
|
69
|
+
const snapshot = ctx.sessions.list.getSnapshot()
|
|
70
|
+
for (const id of snapshot.ids) {
|
|
71
|
+
if (liveUnsubscribers.has(id)) continue
|
|
72
|
+
const scope = ctx.sessions.scope(id)
|
|
73
|
+
const session = scope === undefined ? undefined : ctx.sessions.sessionOf(scope)
|
|
74
|
+
if (session === undefined) continue
|
|
75
|
+
const publish = () => {
|
|
76
|
+
if (overlay.hidden) return
|
|
77
|
+
const state = session.getSnapshot()
|
|
78
|
+
const text = state.partial?.blocks.filter(block => block.kind === 'text').map(block => block.text).join('\n') ?? ''
|
|
79
|
+
send('synapse:live-reply', { sessionId: id, running: state.running, text })
|
|
80
|
+
}
|
|
81
|
+
liveUnsubscribers.set(id, session.subscribe(publish))
|
|
82
|
+
publish()
|
|
83
|
+
}
|
|
84
|
+
for (const [id, unsubscribe] of liveUnsubscribers) if (!snapshot.ids.includes(id)) { unsubscribe(); liveUnsubscribers.delete(id) }
|
|
85
|
+
}
|
|
86
|
+
const syncSessions = () => {
|
|
87
|
+
if (syncQueued) return
|
|
88
|
+
syncQueued = true
|
|
89
|
+
queueMicrotask(() => {
|
|
90
|
+
syncQueued = false
|
|
91
|
+
const sessions = sessionSnapshot(ctx)
|
|
92
|
+
const sessionIds = new Set(sessions.map(session => session.id))
|
|
93
|
+
const removedSessionIds = [...knownSessionIds].filter(id => !sessionIds.has(id))
|
|
94
|
+
knownSessionIds = sessionIds
|
|
95
|
+
void fetch('/synapse/api/sessions/sync', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessions, removedSessionIds }) }).catch(() => {})
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
const syncTheme = () => {
|
|
99
|
+
const dark = document.body?.hasAttribute?.('data-ds-dark-theme') === true
|
|
100
|
+
send('synapse:theme', { dark })
|
|
101
|
+
}
|
|
102
|
+
const syncCurrentSession = () => {
|
|
103
|
+
syncSessions()
|
|
104
|
+
syncLiveSessions()
|
|
105
|
+
syncTheme()
|
|
106
|
+
if (!overlay.hidden) {
|
|
107
|
+
send('synapse:workspaces', { workspaces: workspaceSnapshot(ctx) })
|
|
108
|
+
send('synapse:current-session', { session: currentSession(ctx) })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
let mapOpenFallback = 0
|
|
112
|
+
let mapOpening = false
|
|
113
|
+
const showMapOverlay = () => {
|
|
114
|
+
window.clearTimeout(mapOpenFallback)
|
|
115
|
+
mapOpening = false
|
|
116
|
+
overlay.hidden = false
|
|
117
|
+
overlay.classList.remove('is-opening')
|
|
118
|
+
syncCurrentSession()
|
|
119
|
+
}
|
|
120
|
+
const open = () => {
|
|
121
|
+
window.clearTimeout(mapOpenFallback)
|
|
122
|
+
mapOpening = true
|
|
123
|
+
setView('map')
|
|
124
|
+
// Keep the iframe laid out while hidden so its canvas can receive a
|
|
125
|
+
// real scroll offset. display:none would clamp scrollTop back to zero.
|
|
126
|
+
overlay.hidden = false
|
|
127
|
+
overlay.classList.add('is-opening')
|
|
128
|
+
window.requestAnimationFrame(() => {
|
|
129
|
+
send('synapse:map-opened')
|
|
130
|
+
syncCurrentSession()
|
|
131
|
+
})
|
|
132
|
+
mapOpenFallback = window.setTimeout(showMapOverlay, 300)
|
|
133
|
+
}
|
|
134
|
+
const onFrameLoad = () => {
|
|
135
|
+
syncCurrentSession()
|
|
136
|
+
if (mapOpening) send('synapse:map-opened')
|
|
137
|
+
}
|
|
138
|
+
const onMessage = event => {
|
|
139
|
+
if (event.origin !== location.origin || event.data?.source !== 'dsh-synapse') return
|
|
140
|
+
if (event.data.type === 'synapse:close') return close()
|
|
141
|
+
if (event.data.type === 'synapse:map-ready') return showMapOverlay()
|
|
142
|
+
if (event.data.type === 'synapse:request-current') {
|
|
143
|
+
send('synapse:workspaces', { workspaces: workspaceSnapshot(ctx) })
|
|
144
|
+
return send('synapse:current-session', { session: currentSession(ctx) })
|
|
145
|
+
}
|
|
146
|
+
if (event.data.type === 'synapse:open-session') {
|
|
147
|
+
try { ctx.sessions.open(event.data.sessionId); close() } catch { send('synapse:bridge-error', { message: '关联的 DSH 会话已不可用' }) }
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
if (event.data.type === 'synapse:activate-session') {
|
|
151
|
+
// Bidirectional current-session sync: switch DSH's current session
|
|
152
|
+
// without closing the map; the sessions-list subscription re-sends
|
|
153
|
+
// synapse:current-session so the map follows the new highlight.
|
|
154
|
+
try { ctx.sessions.open(event.data.sessionId) } catch { send('synapse:bridge-error', { message: '关联的 DSH 会话已不可用' }) }
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
if (event.data.type === 'synapse:fork-session') {
|
|
158
|
+
const atSeq = Number.isInteger(event.data.atSeq) ? event.data.atSeq : undefined
|
|
159
|
+
ctx.sessions.fork({ sessionId: event.data.sessionId, atSeq, increaseTitle: true }).then(id => {
|
|
160
|
+
const snapshot = ctx.sessions.list.getSnapshot()
|
|
161
|
+
send('synapse:forked-session', { requestId: event.data.requestId, session: { id, title: snapshot.byId[id]?.displayTitle ?? 'DSH 分支' } })
|
|
162
|
+
}).catch(() => { send('synapse:bridge-error', { message: 'DSH 分支创建失败,请确认源会话已经完成当前轮次' }) })
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
if (event.data.type === 'synapse:send-message') {
|
|
166
|
+
const text = typeof event.data.text === 'string' ? event.data.text.trim() : ''
|
|
167
|
+
if (text === '') return send('synapse:bridge-error', { requestId: event.data.requestId, message: '消息不能为空' })
|
|
168
|
+
prompt(event.data.sessionId, text).then(() => {
|
|
169
|
+
send('synapse:message-sent', { requestId: event.data.requestId, sessionId: event.data.sessionId })
|
|
170
|
+
}).catch(error => {
|
|
171
|
+
send('synapse:bridge-error', { requestId: event.data.requestId, message: error instanceof Error ? error.message : 'DSH 消息发送失败' })
|
|
172
|
+
})
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
if (event.data.type === 'synapse:create-session') {
|
|
176
|
+
const workspaceId = typeof event.data.workspaceId === 'string' && event.data.workspaceId !== '' && event.data.workspaceId !== 'dsh-ungrouped' ? event.data.workspaceId : undefined
|
|
177
|
+
const cwd = typeof event.data.cwd === 'string' && event.data.cwd !== '' ? event.data.cwd : undefined
|
|
178
|
+
const create = workspaceId === undefined ? ctx.sessions.create(cwd === undefined ? {} : { cwd }) : ctx.sessions.create({ workspaceId })
|
|
179
|
+
create.then(id => {
|
|
180
|
+
const snapshot = ctx.sessions.list.getSnapshot()
|
|
181
|
+
send('synapse:created-session', { requestId: event.data.requestId, session: { id, title: snapshot.byId[id]?.displayTitle ?? '新会话', cwd: snapshot.byId[id]?.cwd ?? cwd ?? null } })
|
|
182
|
+
}).catch(() => { send('synapse:bridge-error', { requestId: event.data.requestId, message: 'DSH 会话创建失败,请先在 DSH 选择工作目录' }) })
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const onKeyDown = event => { if (event.key === 'Escape' && !overlay.hidden) close() }
|
|
186
|
+
// Follow DSH's live theme switch: body[data-ds-dark-theme] is the web
|
|
187
|
+
// client's dark-mode signal, mirrored into the map iframe via synapse:theme.
|
|
188
|
+
const themeObserver = typeof MutationObserver === 'undefined'
|
|
189
|
+
? null
|
|
190
|
+
: new MutationObserver(() => syncTheme())
|
|
191
|
+
if (themeObserver !== null && document.body) {
|
|
192
|
+
themeObserver.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })
|
|
193
|
+
}
|
|
194
|
+
const unsubscribeSessions = ctx.sessions.list.subscribe(syncCurrentSession)
|
|
195
|
+
const unsubscribeWorkspaces = ctx.workspaces.list.subscribe(syncCurrentSession)
|
|
196
|
+
dialogButton.addEventListener('click', close)
|
|
197
|
+
mapButton.addEventListener('click', open)
|
|
198
|
+
frame.addEventListener('load', onFrameLoad)
|
|
199
|
+
window.addEventListener('message', onMessage)
|
|
200
|
+
window.addEventListener('keydown', onKeyDown)
|
|
201
|
+
ctx.effect(() => () => {
|
|
202
|
+
dialogButton.removeEventListener('click', close)
|
|
203
|
+
mapButton.removeEventListener('click', open)
|
|
204
|
+
frame.removeEventListener('load', onFrameLoad)
|
|
205
|
+
window.removeEventListener('message', onMessage)
|
|
206
|
+
window.removeEventListener('keydown', onKeyDown)
|
|
207
|
+
themeObserver?.disconnect()
|
|
208
|
+
unsubscribeSessions()
|
|
209
|
+
unsubscribeWorkspaces()
|
|
210
|
+
for (const unsubscribe of liveUnsubscribers.values()) unsubscribe()
|
|
211
|
+
host.remove()
|
|
212
|
+
style.remove()
|
|
213
|
+
}, 'synapse: web workspace switch')
|
|
214
|
+
}
|
|
215
|
+
return module.exports
|
|
216
|
+
},
|
|
217
|
+
})
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# dsh-synapse only extends the Web profile: it reuses the existing DSH server
|
|
2
|
+
# rather than running a second application process.
|
|
3
|
+
- insert:
|
|
4
|
+
- id: synapse
|
|
5
|
+
name: dsh-synapse
|
|
6
|
+
config:
|
|
7
|
+
# A profile can override this path in its own cordis.patch.yml.
|
|
8
|
+
dataFile: !!js dshHomePath('synapse/workspaces.json')
|
|
9
|
+
autoProjection: true
|
|
10
|
+
projectionWorkspaceTitle: DSH 任务
|
|
11
|
+
# Extra authorities the /synapse Host check accepts (host or host:port);
|
|
12
|
+
# localhost and 127.0.0.1 are always allowed.
|
|
13
|
+
trustedHosts: []
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
2
|
+
<path fill="currentColor" d="M23.0584 4.95203C22.8129 4.83203 22.7074 5.06103 22.5639 5.17704C22.5149 5.21454 22.4734 5.26354 22.4319 5.30854C22.0734 5.69155 21.6543 5.94306 21.1073 5.91306C20.3073 5.86806 19.6243 6.11957 19.0203 6.73158C18.8918 5.97706 18.4652 5.52655 17.8162 5.23754C17.4767 5.08753 17.1332 4.93703 16.8952 4.61052C16.7292 4.37801 16.6837 4.11901 16.6007 3.8635C16.5477 3.70949 16.4952 3.55199 16.3177 3.52549C16.1252 3.49549 16.0497 3.65699 15.9742 3.792C15.6722 4.34401 15.5552 4.95203 15.5667 5.56805C15.5932 6.95359 16.1782 8.05712 17.3407 8.84215C17.4727 8.93215 17.5067 9.02215 17.4652 9.15366C17.3857 9.42416 17.2917 9.68667 17.2087 9.95718C17.1557 10.1297 17.0767 10.1677 16.8917 10.0922C16.2537 9.82568 15.7027 9.43117 15.2156 8.95465C14.3891 8.15513 13.6416 7.2726 12.7096 6.58158C12.4906 6.42007 12.2716 6.27007 12.045 6.12707C11.094 5.20354 12.1696 4.44502 12.4186 4.35501C12.6791 4.26101 12.5091 3.938 11.6675 3.942C10.826 3.9455 10.056 4.22751 9.07446 4.60302C8.93096 4.65952 8.77995 4.70052 8.62545 4.73452C7.73492 4.56552 6.80989 4.52802 5.84386 4.63702C4.02481 4.83953 2.57177 5.69955 1.50373 7.1676C0.220694 8.93215 -0.0813148 10.9372 0.288196 13.0283C0.676708 15.2323 1.80174 17.0569 3.53029 18.4834C5.32285 19.9625 7.38741 20.6875 9.74298 20.5485C11.1735 20.466 12.7661 20.2745 14.5626 18.7539C15.0156 18.9795 15.4912 19.0695 16.2797 19.137C16.8872 19.1935 17.4722 19.107 17.9252 19.013C18.6347 18.8629 18.5857 18.2059 18.3292 18.0854C16.2497 17.1169 16.7062 17.5109 16.2912 17.1919C17.3477 15.9419 18.9618 13.7198 19.4598 10.6942C19.5088 10.3602 19.5713 9.88968 19.5638 9.61917C19.5598 9.45417 19.5978 9.39016 19.7863 9.37116C20.3073 9.31116 20.8128 9.16866 21.2773 8.91315C22.6249 8.17713 23.1684 6.96809 23.2964 5.51905C23.3154 5.29754 23.2924 5.06853 23.0584 4.95203ZM11.3165 17.9954C9.30097 16.4109 8.32344 15.8894 7.91992 15.9119C7.54241 15.9344 7.61042 16.3664 7.69342 16.6479C7.78042 16.9259 7.89342 17.1174 8.05193 17.3614C8.16143 17.5229 8.23694 17.7629 7.94243 17.9434C7.29341 18.3449 6.16487 17.8084 6.11187 17.7819C4.79833 17.0084 3.7003 15.9874 2.92628 14.5908C2.17875 13.2468 1.74474 11.8047 1.67324 10.2657C1.65424 9.89418 1.76374 9.76267 2.13375 9.69517C2.62077 9.60517 3.12278 9.58617 3.6093 9.65767C5.66636 9.95818 7.41741 10.8777 8.88545 12.3348C9.72348 13.1643 10.3575 14.1558 11.0105 15.1243C11.705 16.1529 12.4521 17.1329 13.4036 17.9364C13.7396 18.2179 14.0076 18.4319 14.2641 18.5899C13.4906 18.6764 12.1996 18.6949 11.3165 17.9964V17.9954ZM12.2826 11.7817C12.2826 11.6167 12.4146 11.4852 12.5806 11.4852C12.6181 11.4852 12.6521 11.4927 12.6826 11.5037C12.7241 11.5187 12.7621 11.5412 12.7921 11.5752C12.8451 11.6277 12.8751 11.7027 12.8751 11.7817C12.8751 11.9467 12.7431 12.0782 12.5771 12.0782C12.4111 12.0782 12.2826 11.9467 12.2826 11.7817ZM15.2831 13.3208C15.0906 13.3998 14.8981 13.4673 14.7131 13.4748C14.4261 13.4898 14.1131 13.3733 13.9431 13.2308C13.6791 13.0093 13.4901 12.8853 13.4111 12.4988C13.3771 12.3338 13.3961 12.0782 13.4261 11.9317C13.4941 11.6162 13.4186 11.4137 13.1961 11.2297C13.0151 11.0797 12.7846 11.0382 12.5316 11.0382C12.4371 11.0382 12.3506 10.9967 12.2861 10.9632C12.1806 10.9107 12.0936 10.7792 12.1766 10.6177C12.2031 10.5652 12.3316 10.4377 12.3616 10.4152C12.7051 10.2197 13.1011 10.2837 13.4676 10.4302C13.8071 10.5692 14.0641 10.8242 14.4336 11.1847C14.8111 11.6202 14.8791 11.7402 15.0941 12.0672C15.2641 12.3228 15.4186 12.5853 15.5247 12.8858C15.5887 13.0733 15.5057 13.2268 15.2831 13.3208Z"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Architecture and runtime boundaries
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`dsh-synapse` is a presentation and organization layer for DeepSeek Harness conversations. It turns existing DSH sessions, turns, and forks into a visual map without replacing the systems that own those conversations.
|
|
6
|
+
|
|
7
|
+
## Web profile integration
|
|
8
|
+
|
|
9
|
+
The package contributes `cordis.patch.yml`, which inserts the `dsh-synapse` service into the DSH `web` profile. It reuses the existing DSH Web server and client runtime.
|
|
10
|
+
|
|
11
|
+
The plugin:
|
|
12
|
+
|
|
13
|
+
- does not start a second HTTP server;
|
|
14
|
+
- does not create a second model or agent runtime;
|
|
15
|
+
- does not replace DSH authentication or permission checks;
|
|
16
|
+
- does not support non-Web profiles unless those profiles explicitly add the plugin.
|
|
17
|
+
|
|
18
|
+
## Conversation ownership
|
|
19
|
+
|
|
20
|
+
DSH session logs remain the source of truth for conversation content and lifecycle. Native DSH operations own:
|
|
21
|
+
|
|
22
|
+
- creating and opening sessions;
|
|
23
|
+
- sending follow-up messages;
|
|
24
|
+
- forking sessions;
|
|
25
|
+
- archiving sessions;
|
|
26
|
+
- model and tool execution;
|
|
27
|
+
- permission and approval decisions.
|
|
28
|
+
|
|
29
|
+
Synapse projects committed DSH events into cards and sends user actions back through the native DSH session bridge.
|
|
30
|
+
|
|
31
|
+
## Canvas metadata
|
|
32
|
+
|
|
33
|
+
By default, Synapse stores canvas metadata at:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
$DSH_HOME/synapse/workspaces.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The file contains organizational state such as workspace mapping, card layout, and fork anchors. It does not replace session logs.
|
|
40
|
+
|
|
41
|
+
Consequences:
|
|
42
|
+
|
|
43
|
+
- deleting the file resets canvas organization but does not delete conversations;
|
|
44
|
+
- uninstalling the plugin keeps the file, so reinstalling restores the canvas;
|
|
45
|
+
- older schema versions migrate when loaded;
|
|
46
|
+
- two processes sharing the same file can still produce last-writer-wins replacement despite locking and external-change warnings.
|
|
47
|
+
|
|
48
|
+
Run one `dsh web` instance for each shared profile.
|
|
49
|
+
|
|
50
|
+
## Projection model
|
|
51
|
+
|
|
52
|
+
With `autoProjection` enabled, committed DSH session events are grouped by working directory and projected into the corresponding Synapse workspace.
|
|
53
|
+
|
|
54
|
+
Each user question becomes a conversation card. The following assistant messages are folded into that turn, and the final assistant reply is shown as the answer. Forked sessions connect to the parent turn at the durable DSH seed boundary rather than at an arbitrary canvas coordinate.
|
|
55
|
+
|
|
56
|
+
Projected card text is capped at 8000 characters. Longer messages receive a truncation marker in the card, while their complete content remains available from the conversation detail view.
|
|
57
|
+
|
|
58
|
+
Projection writes are coalesced during event bursts, and live updates reuse cached Markdown and patch the active card instead of rebuilding the complete canvas. Card coordinates remain visual metadata only and never determine conversation lineage.
|
|
59
|
+
|
|
60
|
+
## Tool process folding
|
|
61
|
+
|
|
62
|
+
Live events pair tool calls and results by `callId` and render them inside the related assistant reply instead of as standalone conversation cards.
|
|
63
|
+
|
|
64
|
+
Legacy v3 migrations did not always have durable call IDs. Those records pair each tool call with the next tool result by order during migration.
|
|
65
|
+
|
|
66
|
+
## Browser-local state
|
|
67
|
+
|
|
68
|
+
Some interaction state, such as dragged card positions and branch anchors, may be cached in browser local storage to keep the canvas responsive. Durable workspace metadata is still written through the Synapse service.
|
|
69
|
+
|
|
70
|
+
Private-browsing restrictions or local-storage failures must not prevent DSH conversations from operating; they only reduce persistence of visual preferences.
|
|
71
|
+
|
|
72
|
+
## Host validation
|
|
73
|
+
|
|
74
|
+
The `/synapse` endpoint always accepts `localhost` and `127.0.0.1`. Additional LAN or proxy authorities must be listed in `trustedHosts` as a host or `host:port` value.
|
|
75
|
+
|
|
76
|
+
This validation is part of the Web surface and does not replace broader network access controls.
|
|
77
|
+
|
|
78
|
+
## Model and KV-cache impact
|
|
79
|
+
|
|
80
|
+
Synapse reads session events only after DSH commits them. It does not add or modify:
|
|
81
|
+
|
|
82
|
+
- system prompts;
|
|
83
|
+
- user request content;
|
|
84
|
+
- model request headers;
|
|
85
|
+
- tool schemas or registries;
|
|
86
|
+
- provider routing;
|
|
87
|
+
- approval context.
|
|
88
|
+
|
|
89
|
+
As a result, the plugin has no direct model-experience effect and does not invalidate an otherwise reusable KV-cache prefix.
|
|
90
|
+
|
|
91
|
+
## Operational limitations
|
|
92
|
+
|
|
93
|
+
- Only the `web` profile is supported by the bundled patch.
|
|
94
|
+
- Canvas metadata and session content have different owners and backup requirements.
|
|
95
|
+
- A single shared metadata file is not a multi-writer database.
|
|
96
|
+
- Browser state can be cleared independently from DSH Home data.
|
|
97
|
+
- Historical migrations may have less precise tool-call pairing than live projection.
|
|
98
|
+
|
|
99
|
+
## Related documentation
|
|
100
|
+
|
|
101
|
+
- [Chinese user guide](zh-CN/README.md)
|
|
102
|
+
- [English user guide](en/README.md)
|
|
103
|
+
- [Development and release guide](development.md)
|
|
104
|
+
- [Project overview](../README.md)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Development and release guide
|
|
2
|
+
|
|
3
|
+
This document covers local validation, package inspection, GitHub Actions, and npm release automation for maintainers.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js `>= 22.19.0`
|
|
8
|
+
- pnpm through Corepack
|
|
9
|
+
- A checkout of this repository
|
|
10
|
+
|
|
11
|
+
## Local workflow
|
|
12
|
+
|
|
13
|
+
Install with the lockfile:
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
corepack pnpm install --frozen-lockfile
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Validate JavaScript syntax:
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
corepack pnpm run build
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Run the full test suite:
|
|
26
|
+
|
|
27
|
+
```powershell
|
|
28
|
+
corepack pnpm test
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Inspect the package archive:
|
|
32
|
+
|
|
33
|
+
```powershell
|
|
34
|
+
corepack pnpm pack
|
|
35
|
+
npm pack --dry-run --json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The package has no generated build output. `build` runs `node --check` over `index.js`, `client.js`, and `app.js`.
|
|
39
|
+
|
|
40
|
+
## GitHub Actions
|
|
41
|
+
|
|
42
|
+
Three workflows live under `.github/workflows/`.
|
|
43
|
+
|
|
44
|
+
### Pull request tests
|
|
45
|
+
|
|
46
|
+
`pr-tests.yml` runs for pull requests targeting `main` when they are opened, reopened, updated, or marked ready for review.
|
|
47
|
+
|
|
48
|
+
The workflow:
|
|
49
|
+
|
|
50
|
+
1. Checks out the pull request revision.
|
|
51
|
+
2. Installs pnpm 10 and Node.js 22.19.0.
|
|
52
|
+
3. Runs `pnpm install --frozen-lockfile`.
|
|
53
|
+
4. Runs `pnpm run build`.
|
|
54
|
+
5. Runs `pnpm test`.
|
|
55
|
+
|
|
56
|
+
A per-PR concurrency group cancels obsolete runs after a newer commit arrives.
|
|
57
|
+
|
|
58
|
+
### Main branch tests
|
|
59
|
+
|
|
60
|
+
`main-tests.yml` runs for every push to `main`, including direct commits and merged pull requests. It executes the same frozen installation, build validation, and full test suite.
|
|
61
|
+
|
|
62
|
+
### npm publishing
|
|
63
|
+
|
|
64
|
+
`npm-publish.yml` runs for pushed tags matching the broad GitHub pattern `v*.*.*`. The job then validates the tag strictly before publishing.
|
|
65
|
+
|
|
66
|
+
Accepted forms include:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
v0.4.0
|
|
70
|
+
v0.4.0-rc1
|
|
71
|
+
v0.4.0-rc.2
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The tag without the leading `v` must exactly equal `package.json.version`. A mismatch fails before installation or publication.
|
|
75
|
+
|
|
76
|
+
| Version | Git tag | npm dist-tag |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `0.4.0-rc1` | `v0.4.0-rc1` | `next` |
|
|
79
|
+
| `0.4.0` | `v0.4.0` | `latest` |
|
|
80
|
+
|
|
81
|
+
Every release tag reruns installation, build validation, and the complete test suite before calling `pnpm publish`.
|
|
82
|
+
|
|
83
|
+
## Configure npm authentication
|
|
84
|
+
|
|
85
|
+
Create a GitHub Actions repository secret named `NPM_TOKEN` in the repository where the tag workflow will run:
|
|
86
|
+
|
|
87
|
+
```powershell
|
|
88
|
+
gh secret set NPM_TOKEN --repo OWNER/dsh-synapse
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The token's npm account must have permission to create or publish the public, unscoped `dsh-synapse` package. The workflow passes the secret to the publish step as `NODE_AUTH_TOKEN`; pull request and ordinary test workflows never receive it.
|
|
92
|
+
|
|
93
|
+
## Release checklist
|
|
94
|
+
|
|
95
|
+
1. Confirm the full test suite passes on `main`.
|
|
96
|
+
2. Update `package.json.version` to the intended release version.
|
|
97
|
+
3. Commit and merge the version change.
|
|
98
|
+
4. Create a matching tag on that commit.
|
|
99
|
+
5. Push the tag.
|
|
100
|
+
6. Confirm the **Publish to npm** workflow passes.
|
|
101
|
+
7. Verify the npm dist-tag:
|
|
102
|
+
- prerelease versions use `next`
|
|
103
|
+
- stable versions use `latest`
|
|
104
|
+
|
|
105
|
+
Example stable release:
|
|
106
|
+
|
|
107
|
+
```powershell
|
|
108
|
+
# package.json already contains 0.4.0
|
|
109
|
+
git tag v0.4.0
|
|
110
|
+
git push origin v0.4.0
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Example prerelease:
|
|
114
|
+
|
|
115
|
+
```powershell
|
|
116
|
+
# package.json already contains 0.4.0-rc1
|
|
117
|
+
git tag v0.4.0-rc1
|
|
118
|
+
git push origin v0.4.0-rc1
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Publication failure conditions
|
|
122
|
+
|
|
123
|
+
The publish job stops without publishing when:
|
|
124
|
+
|
|
125
|
+
- the tag is not a supported version form;
|
|
126
|
+
- the tag and `package.json.version` differ;
|
|
127
|
+
- frozen installation fails;
|
|
128
|
+
- syntax validation or tests fail;
|
|
129
|
+
- `NPM_TOKEN` is missing, expired, or lacks package permissions;
|
|
130
|
+
- npm rejects the package name or an already-published version.
|
|
131
|
+
|
|
132
|
+
## Documentation
|
|
133
|
+
|
|
134
|
+
- [Chinese user guide](zh-CN/README.md)
|
|
135
|
+
- [English user guide](en/README.md)
|
|
136
|
+
- [Architecture and runtime boundaries](architecture.md)
|
|
137
|
+
- [Project overview](../README.md)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# dsh-synapse English Guide
|
|
2
|
+
|
|
3
|
+
`dsh-synapse` is a Web plugin for DeepSeek Harness (DSH). It adds a session map to the native conversation interface and organizes sessions, follow-ups, and forks from the same workspace as a browsable, draggable, and zoomable canvas.
|
|
4
|
+
|
|
5
|
+
The plugin does not replace DSH models, tools, sessions, permissions, or the Web server. DSH remains responsible for every conversation operation.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- A DeepSeek Harness release with the `dsh plugin` profile mechanism (2026-08 or later).
|
|
10
|
+
- Node.js `>= 22.19.0`.
|
|
11
|
+
- The `web` profile; other profiles are not currently supported.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### Install from npm
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
corepack pnpm dsh plugin --profile web add dsh-synapse
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The npm package ships prebuilt code, so no build-script permission is needed — the simplest install. GitHub and local-checkout routes below are alternatives.
|
|
22
|
+
|
|
23
|
+
### Install from GitHub
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
corepack pnpm dsh plugin --profile web add github:liangmianya/dsh-synapse
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
GitHub installs run the package `prepare` script, which validates JavaScript syntax with `node --check`.
|
|
30
|
+
|
|
31
|
+
### pnpm 10+ allowBuilds
|
|
32
|
+
|
|
33
|
+
pnpm 10 and later may block build scripts for Git dependencies by default. If installation is blocked, copy the **complete key printed by pnpm** into the DSH Web profile's `pnpm-workspace.yaml`:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
allowBuilds:
|
|
37
|
+
"dsh-synapse@https://codeload.github.com/liangmianya/dsh-synapse/tar.gz/<commit>": true
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Use the complete key containing the tarball URL and commit, not the bare package name `dsh-synapse`. The key changes when the upstream commit changes, so use the newly printed value when upgrading.
|
|
41
|
+
|
|
42
|
+
### Install a local checkout
|
|
43
|
+
|
|
44
|
+
```powershell
|
|
45
|
+
corepack pnpm dsh plugin --profile web add link:E:\path\to\dsh-synapse
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The `link:` form references the local checkout directly and is recommended for development. In a normal run, restart `dsh web` and refresh the page after editing the plugin.
|
|
49
|
+
|
|
50
|
+
## Start DSH Web
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
corepack pnpm dsh web
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Default address:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
http://127.0.0.1:3080/
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Let DSH choose a free port when 3080 is occupied:
|
|
63
|
+
|
|
64
|
+
```powershell
|
|
65
|
+
corepack pnpm dsh web --port 0
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Open the top **Session Map** switch after startup. Do not run two `dsh web` processes that share the same profile.
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
1. Select a working directory in DSH or open an existing session.
|
|
73
|
+
2. Send at least one message so the session enters workspace history.
|
|
74
|
+
3. Open **Session Map** from the top switch.
|
|
75
|
+
4. Click a card or sidebar session to synchronize the current session between the map and native chat.
|
|
76
|
+
5. Use **Branch** on a completed answer to preserve an alternative path.
|
|
77
|
+
6. Open **Details** from a card to inspect the complete conversation.
|
|
78
|
+
7. Use **Open in DSH** or the top **Dialogue** switch to return to native chat.
|
|
79
|
+
|
|
80
|
+
The canvas supports:
|
|
81
|
+
|
|
82
|
+
- Panning and zooming up to 4×.
|
|
83
|
+
- Dragging cards with persisted positions.
|
|
84
|
+
- Expanding or collapsing descendant conversation subtrees.
|
|
85
|
+
- One-click focus on the current session.
|
|
86
|
+
- Smooth card scrolling and Markdown table rendering.
|
|
87
|
+
- Folding tool calls and results into the related assistant answer by `callId`.
|
|
88
|
+
|
|
89
|
+
## Configuration
|
|
90
|
+
|
|
91
|
+
The plugin is inserted through the profile's `cordis.patch.yml`. Override it in your own patch by targeting the row id `synapse`.
|
|
92
|
+
|
|
93
|
+
> A DSH patch replaces the row's complete `config`, so restate every value that must remain active.
|
|
94
|
+
|
|
95
|
+
| Key | Default | Description |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `dataFile` | `$DSH_HOME/synapse/workspaces.json` | Canvas metadata persistence file |
|
|
98
|
+
| `autoProjection` | `true` | Automatically project committed DSH session events into cards |
|
|
99
|
+
| `projectionWorkspaceTitle` | `DSH 任务` | Title of the automatically projected workspace |
|
|
100
|
+
| `trustedHosts` | `[]` | Extra host or `host:port` values accepted by the `/synapse` Host check; `localhost` and `127.0.0.1` are always accepted |
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
- id: synapse
|
|
106
|
+
config:
|
|
107
|
+
dataFile: !!js dshHomePath('synapse/my-workspaces.json')
|
|
108
|
+
autoProjection: true
|
|
109
|
+
projectionWorkspaceTitle: My tasks
|
|
110
|
+
trustedHosts: []
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Add the actual host to `trustedHosts` when exposing DSH on a LAN.
|
|
114
|
+
|
|
115
|
+
## Uninstall and data cleanup
|
|
116
|
+
|
|
117
|
+
Remove the plugin:
|
|
118
|
+
|
|
119
|
+
```powershell
|
|
120
|
+
corepack pnpm dsh plugin --profile web remove dsh-synapse
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`remove` deletes the dependency and profile activation layer but keeps canvas data. Reinstalling reuses and migrates the old data when necessary.
|
|
124
|
+
|
|
125
|
+
For a complete cleanup, manually remove:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
$DSH_HOME/synapse/
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
A leftover `allowBuilds` key in `pnpm-workspace.yaml` is harmless and may also be removed.
|
|
132
|
+
|
|
133
|
+
## Data and runtime boundaries
|
|
134
|
+
|
|
135
|
+
- DSH session logs own the actual conversation content.
|
|
136
|
+
- Synapse `workspaces.json` stores only canvas metadata, layout, and branch anchors.
|
|
137
|
+
- Deleting `workspaces.json` loses canvas layout, never DSH sessions.
|
|
138
|
+
- Projected message text is capped at 8000 characters; longer card text ends with “—…(详情查看全文)”, while the full content remains available in conversation details.
|
|
139
|
+
- The plugin starts no second Web server and creates no second agent system.
|
|
140
|
+
|
|
141
|
+
See [Architecture and runtime boundaries](../architecture.md) for details.
|
|
142
|
+
|
|
143
|
+
## Model Experience
|
|
144
|
+
|
|
145
|
+
None, as dsh-synapse only reads committed session events and renders them; it adds no system-prompt prose, tool schemas, or request-context content to any model request.
|
|
146
|
+
|
|
147
|
+
### KV Cache effect
|
|
148
|
+
|
|
149
|
+
Does not invalidate. The plugin never changes request headers, system prompts, or tool registries, so an already-reusable KV prefix stays reusable; canvas projection consumes session logs only after they are committed.
|
|
150
|
+
|
|
151
|
+
## Known limitations
|
|
152
|
+
|
|
153
|
+
- Only the `web` profile is supported.
|
|
154
|
+
- Two DSH Web instances sharing one profile write the same `workspaces.json`. A cross-process write lock and external-modification warnings exist, but last-writer-wins replacement remains possible; run one instance.
|
|
155
|
+
- During v3 migration, legacy tool cards pair each call with the next result by order. Live events pair by `callId`.
|
|
156
|
+
|
|
157
|
+
## Development and releases
|
|
158
|
+
|
|
159
|
+
Contributor commands, GitHub Actions, and npm publishing are documented in the [Development and release guide](../development.md).
|
|
160
|
+
|
|
161
|
+
Return to the [project overview](../../README.md).
|
|
Binary file
|
|
Binary file
|