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/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
+ })
@@ -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