dsh-synapse 0.4.1 → 0.4.2
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 -21
- package/README.md +79 -81
- package/app.js +1344 -1344
- package/client.js +336 -241
- package/cordis.patch.yml +13 -13
- package/deepseek-mark.svg +3 -3
- package/docs/architecture.md +127 -104
- package/docs/development.md +138 -137
- package/docs/en/README.md +161 -161
- package/docs/images/native-webui.jpg +0 -0
- package/docs/images/synapse-map.jpg +0 -0
- package/docs/zh-CN/README.md +154 -154
- package/index.js +761 -754
- package/package.json +47 -45
- package/styles.css +317 -317
- package/docs/images/native-webui.png +0 -0
- package/docs/images/synapse-ui.png +0 -0
package/client.js
CHANGED
|
@@ -1,241 +1,336 @@
|
|
|
1
|
-
window.__ModuleLoader__.load({
|
|
2
|
-
id: 'dsh-synapse',
|
|
3
|
-
factory: () => {
|
|
4
|
-
const module = { exports: {} }
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
send('synapse:
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-synapse',
|
|
3
|
+
factory: () => {
|
|
4
|
+
const module = { exports: {} }
|
|
5
|
+
// Consumer identity for owned Session references. The Client records
|
|
6
|
+
// positive reference counts per source; nothing else reads this string.
|
|
7
|
+
const SESSION_SOURCE = 'synapse'
|
|
8
|
+
// Shown when a Session is gone or no longer reachable from this browser.
|
|
9
|
+
const NO_SESSION = '关联的 DSH 会话已不可用'
|
|
10
|
+
const currentSession = ctx => {
|
|
11
|
+
const snapshot = ctx.sessions.list.getSnapshot()
|
|
12
|
+
// 0.1.7 removed `list.current`: the Main view's own reference count is the
|
|
13
|
+
// Client's current-Session signal. `ui-workspace` derives the highlighted
|
|
14
|
+
// Session row from the same fact, so both stay in step by construction.
|
|
15
|
+
const session = Object.values(snapshot.byId).find(row => (row.retainedBy?.mainView ?? 0) > 0)
|
|
16
|
+
return session === undefined ? null : { id: session.id, title: session.displayTitle, cwd: session.cwd ?? null }
|
|
17
|
+
}
|
|
18
|
+
const sessionSnapshot = ctx => {
|
|
19
|
+
const snapshot = ctx.sessions.list.getSnapshot()
|
|
20
|
+
return snapshot.ids.map(id => {
|
|
21
|
+
const session = snapshot.byId[id]
|
|
22
|
+
return session === undefined ? null : { id, title: session.displayTitle, cwd: session.cwd ?? null, parentId: session.parentId ?? null, blank: session.blank }
|
|
23
|
+
}).filter(Boolean)
|
|
24
|
+
}
|
|
25
|
+
const workspaceSnapshot = ctx => {
|
|
26
|
+
const sessions = ctx.sessions.list.getSnapshot()
|
|
27
|
+
const snapshot = ctx.workspaces.list.getSnapshot()
|
|
28
|
+
const accounted = new Set(snapshot.items.flatMap(workspace => workspace.sessionIds))
|
|
29
|
+
return [
|
|
30
|
+
...snapshot.items.map(workspace => ({ id: workspace.workspaceId, title: workspace.title, path: workspace.path, sessionIds: workspace.sessionIds })),
|
|
31
|
+
{ id: 'dsh-ungrouped', title: '未分组', path: null, sessionIds: sessions.ids.filter(id => !accounted.has(id)) },
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports.inject = ['sessions', 'workspaces', 'uiWorkspace', 'uiConversation']
|
|
36
|
+
module.exports.apply = ctx => {
|
|
37
|
+
const prompt = async (sessionId, text) => {
|
|
38
|
+
// 0.1.7 owns Session lifetime explicitly: `scope()` only borrows an
|
|
39
|
+
// already-retained generation, so sending needs a reference of our own.
|
|
40
|
+
let reference
|
|
41
|
+
try {
|
|
42
|
+
reference = ctx.sessions.retain(sessionId, { source: SESSION_SOURCE })
|
|
43
|
+
await reference.ready
|
|
44
|
+
} catch {
|
|
45
|
+
reference?.release()
|
|
46
|
+
throw new Error(NO_SESSION)
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const result = await reference.binding.session.prompt([{ type: 'text', text }], 'queue')
|
|
50
|
+
if (!result.ok) throw new Error(result.error?.message ?? 'DSH 未接受这条消息')
|
|
51
|
+
} finally {
|
|
52
|
+
reference.release()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const style = document.createElement('style')
|
|
56
|
+
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}'
|
|
57
|
+
document.head.append(style)
|
|
58
|
+
const host = document.createElement('div')
|
|
59
|
+
host.className = 'dsh-synapse-host'
|
|
60
|
+
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>'
|
|
61
|
+
document.body.append(host)
|
|
62
|
+
const dialogButton = host.querySelector('[data-view="dialog"]')
|
|
63
|
+
const mapButton = host.querySelector('[data-view="map"]')
|
|
64
|
+
const overlay = host.querySelector('.dsh-synapse-overlay')
|
|
65
|
+
const frame = host.querySelector('iframe')
|
|
66
|
+
|
|
67
|
+
const setView = view => {
|
|
68
|
+
const showingMap = view === 'map'
|
|
69
|
+
dialogButton.classList.toggle('active', !showingMap)
|
|
70
|
+
dialogButton.setAttribute('aria-pressed', String(!showingMap))
|
|
71
|
+
mapButton.classList.toggle('active', showingMap)
|
|
72
|
+
mapButton.setAttribute('aria-pressed', String(showingMap))
|
|
73
|
+
}
|
|
74
|
+
const close = () => {
|
|
75
|
+
window.clearTimeout(mapOpenFallback)
|
|
76
|
+
mapOpening = false
|
|
77
|
+
overlay.classList.remove('is-opening')
|
|
78
|
+
overlay.hidden = true
|
|
79
|
+
setView('dialog')
|
|
80
|
+
}
|
|
81
|
+
const send = (type, payload) => { frame.contentWindow?.postMessage({ source: 'dsh-synapse', type, ...payload }, location.origin) }
|
|
82
|
+
let syncQueued = false
|
|
83
|
+
let knownSessionIds = new Set()
|
|
84
|
+
// One live record per catalogued Session. The pre-0.1.7 Client minted a
|
|
85
|
+
// Session scope on demand for any listed id, so live state was readable
|
|
86
|
+
// for every card; 0.1.7 mints none, so a Chat view is readable only while
|
|
87
|
+
// something holds a reference. A card whose Session is not observable
|
|
88
|
+
// still reports its `running` flag from the list row, because only the
|
|
89
|
+
// streaming text needs the view.
|
|
90
|
+
const liveSessions = new Map()
|
|
91
|
+
// References this plugin owns, so it can read a running Session's Chat
|
|
92
|
+
// view without depending on another surface retaining it.
|
|
93
|
+
const ownReferences = new Map()
|
|
94
|
+
|
|
95
|
+
const publishLiveSession = id => {
|
|
96
|
+
if (overlay.hidden) return
|
|
97
|
+
const text = liveSessions.get(id)?.chat?.getSnapshot()?.legacy.partial?.blocks
|
|
98
|
+
.filter(block => block.kind === 'text').map(block => block.text).join('\n') ?? ''
|
|
99
|
+
const running = ctx.sessions.list.getSnapshot().byId[id]?.running === true
|
|
100
|
+
send('synapse:live-reply', { sessionId: id, running, text })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const detachLiveSession = record => {
|
|
104
|
+
for (const unsubscribe of record.unsubscribers) unsubscribe()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// A running Session is the only kind that produces live text, so it is the
|
|
108
|
+
// only kind worth opening a reference — and its history read — for.
|
|
109
|
+
const syncObservedSessions = () => {
|
|
110
|
+
const rows = ctx.sessions.list.getSnapshot()
|
|
111
|
+
for (const id of rows.ids) {
|
|
112
|
+
const held = ownReferences.has(id)
|
|
113
|
+
const wanted = rows.byId[id]?.running === true && ctx.sessions.binding(id) === undefined
|
|
114
|
+
if (wanted && !held) {
|
|
115
|
+
try {
|
|
116
|
+
const reference = ctx.sessions.retain(id, { source: SESSION_SOURCE })
|
|
117
|
+
void reference.ready.catch(() => {})
|
|
118
|
+
ownReferences.set(id, reference)
|
|
119
|
+
} catch { /* the Session is not retainable right now */ }
|
|
120
|
+
} else if (!wanted && held) {
|
|
121
|
+
ownReferences.get(id).release()
|
|
122
|
+
ownReferences.delete(id)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const [id, reference] of ownReferences) {
|
|
126
|
+
if (rows.ids.includes(id)) continue
|
|
127
|
+
reference.release()
|
|
128
|
+
ownReferences.delete(id)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const syncLiveSessions = () => {
|
|
133
|
+
syncObservedSessions()
|
|
134
|
+
const ids = ctx.sessions.list.getSnapshot().ids
|
|
135
|
+
for (const id of ids) {
|
|
136
|
+
const binding = ctx.sessions.binding(id)
|
|
137
|
+
let record = liveSessions.get(id)
|
|
138
|
+
// Rebuild when the observable source appears or disappears, and when a
|
|
139
|
+
// same-id generation replaced the borrowed binding.
|
|
140
|
+
if (record !== undefined && record.binding !== binding) {
|
|
141
|
+
detachLiveSession(record)
|
|
142
|
+
record = undefined
|
|
143
|
+
}
|
|
144
|
+
if (record === undefined) {
|
|
145
|
+
let chat
|
|
146
|
+
if (binding !== undefined) {
|
|
147
|
+
try { chat = ctx.uiConversation.binding(id).target('chat') } catch { chat = undefined }
|
|
148
|
+
}
|
|
149
|
+
liveSessions.set(id, {
|
|
150
|
+
binding,
|
|
151
|
+
chat,
|
|
152
|
+
// `target('chat')` is the only reader that activates the view
|
|
153
|
+
// target: its snapshot stays undefined until something subscribes.
|
|
154
|
+
unsubscribers: chat === undefined
|
|
155
|
+
? []
|
|
156
|
+
: [chat.subscribe(() => publishLiveSession(id)), binding.session.subscribe(() => publishLiveSession(id))],
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const [id, record] of liveSessions) {
|
|
161
|
+
if (ids.includes(id)) continue
|
|
162
|
+
detachLiveSession(record)
|
|
163
|
+
liveSessions.delete(id)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Always runs after `syncLiveSessions`, so a newly attached Session gets
|
|
167
|
+
// its first report here rather than publishing twice on the same sync.
|
|
168
|
+
const publishLiveSessions = () => { for (const id of liveSessions.keys()) publishLiveSession(id) }
|
|
169
|
+
const syncSessions = () => {
|
|
170
|
+
if (syncQueued) return
|
|
171
|
+
syncQueued = true
|
|
172
|
+
queueMicrotask(() => {
|
|
173
|
+
syncQueued = false
|
|
174
|
+
const sessions = sessionSnapshot(ctx)
|
|
175
|
+
const sessionIds = new Set(sessions.map(session => session.id))
|
|
176
|
+
const removedSessionIds = [...knownSessionIds].filter(id => !sessionIds.has(id))
|
|
177
|
+
knownSessionIds = sessionIds
|
|
178
|
+
void fetch('/synapse/api/sessions/sync', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessions, removedSessionIds }) }).catch(() => {})
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
const syncTheme = () => {
|
|
182
|
+
const dark = document.body?.hasAttribute?.('data-ds-dark-theme') === true
|
|
183
|
+
send('synapse:theme', { dark })
|
|
184
|
+
}
|
|
185
|
+
const syncCurrentSession = () => {
|
|
186
|
+
syncSessions()
|
|
187
|
+
syncLiveSessions()
|
|
188
|
+
publishLiveSessions()
|
|
189
|
+
syncTheme()
|
|
190
|
+
if (!overlay.hidden) {
|
|
191
|
+
send('synapse:workspaces', { workspaces: workspaceSnapshot(ctx) })
|
|
192
|
+
send('synapse:current-session', { session: currentSession(ctx) })
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
let mapOpenFallback = 0
|
|
196
|
+
let mapOpening = false
|
|
197
|
+
const showMapOverlay = () => {
|
|
198
|
+
window.clearTimeout(mapOpenFallback)
|
|
199
|
+
mapOpening = false
|
|
200
|
+
overlay.hidden = false
|
|
201
|
+
overlay.classList.remove('is-opening')
|
|
202
|
+
syncCurrentSession()
|
|
203
|
+
}
|
|
204
|
+
const open = () => {
|
|
205
|
+
window.clearTimeout(mapOpenFallback)
|
|
206
|
+
mapOpening = true
|
|
207
|
+
setView('map')
|
|
208
|
+
// Keep the iframe laid out while hidden so its canvas can receive a
|
|
209
|
+
// real scroll offset. display:none would clamp scrollTop back to zero.
|
|
210
|
+
overlay.hidden = false
|
|
211
|
+
overlay.classList.add('is-opening')
|
|
212
|
+
window.requestAnimationFrame(() => {
|
|
213
|
+
send('synapse:map-opened')
|
|
214
|
+
syncCurrentSession()
|
|
215
|
+
})
|
|
216
|
+
mapOpenFallback = window.setTimeout(showMapOverlay, 300)
|
|
217
|
+
}
|
|
218
|
+
const onFrameLoad = () => {
|
|
219
|
+
syncCurrentSession()
|
|
220
|
+
if (mapOpening) send('synapse:map-opened')
|
|
221
|
+
}
|
|
222
|
+
const onMessage = event => {
|
|
223
|
+
if (event.origin !== location.origin || event.data?.source !== 'dsh-synapse') return
|
|
224
|
+
if (event.data.type === 'synapse:close') return close()
|
|
225
|
+
if (event.data.type === 'synapse:map-ready') return showMapOverlay()
|
|
226
|
+
if (event.data.type === 'synapse:request-current') {
|
|
227
|
+
send('synapse:workspaces', { workspaces: workspaceSnapshot(ctx) })
|
|
228
|
+
return send('synapse:current-session', { session: currentSession(ctx) })
|
|
229
|
+
}
|
|
230
|
+
if (event.data.type === 'synapse:open-session') {
|
|
231
|
+
// 0.1.7 moved navigation out of the Session Controller: selecting a
|
|
232
|
+
// Session and showing its Conversation is one Workspace UI action.
|
|
233
|
+
try { ctx.uiWorkspace.openSession(event.data.sessionId); close() } catch { send('synapse:bridge-error', { message: NO_SESSION }) }
|
|
234
|
+
// Best-effort anchor to the requested turn: chat nodes expose their
|
|
235
|
+
// source event seq (anchorSeq) and render with data-chat-anchor-key,
|
|
236
|
+
// so resolve seq -> node key -> scroll once the view materializes.
|
|
237
|
+
const seq = event.data.seq
|
|
238
|
+
if (Number.isInteger(seq)) {
|
|
239
|
+
const tryScroll = attempt => {
|
|
240
|
+
const record = liveSessions.get(event.data.sessionId)
|
|
241
|
+
if (record === undefined) {
|
|
242
|
+
if (attempt < 3) window.setTimeout(() => tryScroll(attempt + 1), 500)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
let key = undefined
|
|
246
|
+
for (const node of record.chat.getSnapshot()?.nodes.values() ?? []) {
|
|
247
|
+
if (node.anchorSeq === seq) { key = node.key; break }
|
|
248
|
+
}
|
|
249
|
+
if (key !== undefined) {
|
|
250
|
+
const row = document.querySelector(`[data-chat-anchor-key="${CSS.escape(key)}"]`)
|
|
251
|
+
if (row instanceof HTMLElement) row.scrollIntoView({ block: 'start' })
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
// The requested turn may sit outside the loaded window; the jump
|
|
255
|
+
// loader pages history back through the seq, then the retry reads
|
|
256
|
+
// the rebuilt node set.
|
|
257
|
+
if (attempt === 0 && typeof record.session.loadThrough === 'function') {
|
|
258
|
+
void record.session.loadThrough(seq).catch(() => {})
|
|
259
|
+
}
|
|
260
|
+
if (attempt < 3) window.setTimeout(() => tryScroll(attempt + 1), 500)
|
|
261
|
+
}
|
|
262
|
+
window.setTimeout(() => tryScroll(0), 300)
|
|
263
|
+
}
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
if (event.data.type === 'synapse:activate-session') {
|
|
267
|
+
// Bidirectional current-session sync: switch DSH's current session
|
|
268
|
+
// without closing the map; the sessions-list subscription re-sends
|
|
269
|
+
// synapse:current-session so the map follows the new highlight.
|
|
270
|
+
try { ctx.uiWorkspace.openSession(event.data.sessionId) } catch { send('synapse:bridge-error', { message: NO_SESSION }) }
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
if (event.data.type === 'synapse:fork-session') {
|
|
274
|
+
const atSeq = Number.isInteger(event.data.atSeq) ? event.data.atSeq : undefined
|
|
275
|
+
ctx.sessions.fork({ sessionId: event.data.sessionId, atSeq, increaseTitle: true }).then(id => {
|
|
276
|
+
const snapshot = ctx.sessions.list.getSnapshot()
|
|
277
|
+
send('synapse:forked-session', { requestId: event.data.requestId, session: { id, title: snapshot.byId[id]?.displayTitle ?? 'DSH 分支' } })
|
|
278
|
+
}).catch(() => { send('synapse:bridge-error', { message: 'DSH 分支创建失败,请确认源会话已经完成当前轮次' }) })
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
if (event.data.type === 'synapse:send-message') {
|
|
282
|
+
const text = typeof event.data.text === 'string' ? event.data.text.trim() : ''
|
|
283
|
+
if (text === '') return send('synapse:bridge-error', { requestId: event.data.requestId, message: '消息不能为空' })
|
|
284
|
+
prompt(event.data.sessionId, text).then(() => {
|
|
285
|
+
send('synapse:message-sent', { requestId: event.data.requestId, sessionId: event.data.sessionId })
|
|
286
|
+
}).catch(error => {
|
|
287
|
+
send('synapse:bridge-error', { requestId: event.data.requestId, message: error instanceof Error ? error.message : 'DSH 消息发送失败' })
|
|
288
|
+
})
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
if (event.data.type === 'synapse:create-session') {
|
|
292
|
+
const workspaceId = typeof event.data.workspaceId === 'string' && event.data.workspaceId !== '' && event.data.workspaceId !== 'dsh-ungrouped' ? event.data.workspaceId : undefined
|
|
293
|
+
const cwd = typeof event.data.cwd === 'string' && event.data.cwd !== '' ? event.data.cwd : undefined
|
|
294
|
+
const create = workspaceId === undefined ? ctx.sessions.create(cwd === undefined ? {} : { cwd }) : ctx.sessions.create({ workspaceId })
|
|
295
|
+
create.then(id => {
|
|
296
|
+
const snapshot = ctx.sessions.list.getSnapshot()
|
|
297
|
+
send('synapse:created-session', { requestId: event.data.requestId, session: { id, title: snapshot.byId[id]?.displayTitle ?? '新会话', cwd: snapshot.byId[id]?.cwd ?? cwd ?? null } })
|
|
298
|
+
}).catch(() => { send('synapse:bridge-error', { requestId: event.data.requestId, message: 'DSH 会话创建失败,请先在 DSH 选择工作目录' }) })
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const onKeyDown = event => { if (event.key === 'Escape' && !overlay.hidden) close() }
|
|
302
|
+
// Follow DSH's live theme switch: body[data-ds-dark-theme] is the web
|
|
303
|
+
// client's dark-mode signal, mirrored into the map iframe via synapse:theme.
|
|
304
|
+
const themeObserver = typeof MutationObserver === 'undefined'
|
|
305
|
+
? null
|
|
306
|
+
: new MutationObserver(() => syncTheme())
|
|
307
|
+
if (themeObserver !== null && document.body) {
|
|
308
|
+
themeObserver.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })
|
|
309
|
+
}
|
|
310
|
+
const unsubscribeSessions = ctx.sessions.list.subscribe(syncCurrentSession)
|
|
311
|
+
const unsubscribeWorkspaces = ctx.workspaces.list.subscribe(syncCurrentSession)
|
|
312
|
+
dialogButton.addEventListener('click', close)
|
|
313
|
+
mapButton.addEventListener('click', open)
|
|
314
|
+
frame.addEventListener('load', onFrameLoad)
|
|
315
|
+
window.addEventListener('message', onMessage)
|
|
316
|
+
window.addEventListener('keydown', onKeyDown)
|
|
317
|
+
ctx.effect(() => () => {
|
|
318
|
+
dialogButton.removeEventListener('click', close)
|
|
319
|
+
mapButton.removeEventListener('click', open)
|
|
320
|
+
frame.removeEventListener('load', onFrameLoad)
|
|
321
|
+
window.removeEventListener('message', onMessage)
|
|
322
|
+
window.removeEventListener('keydown', onKeyDown)
|
|
323
|
+
themeObserver?.disconnect()
|
|
324
|
+
unsubscribeSessions()
|
|
325
|
+
unsubscribeWorkspaces()
|
|
326
|
+
for (const record of liveSessions.values()) detachLiveSession(record)
|
|
327
|
+
liveSessions.clear()
|
|
328
|
+
for (const reference of ownReferences.values()) reference.release()
|
|
329
|
+
ownReferences.clear()
|
|
330
|
+
host.remove()
|
|
331
|
+
style.remove()
|
|
332
|
+
}, 'synapse: web workspace switch')
|
|
333
|
+
}
|
|
334
|
+
return module.exports
|
|
335
|
+
},
|
|
336
|
+
})
|