dsh-thoughtdag 0.5.17 → 0.5.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -12
- package/dist-app/assets/{canonical-BYB1lzcN.js → canonical-bSxBmsm7.js} +2 -2
- package/dist-app/assets/{canvas-record-DEyrBPBB.js → canvas-record-qO1KINUh.js} +1 -1
- package/dist-app/assets/{claude-code-session-D_7zmlSg.js → claude-code-session-DkNDRQp8.js} +1 -1
- package/dist-app/assets/{codex-session-uM9wSh4p.js → codex-session-B2hAZ_ax.js} +1 -1
- package/dist-app/assets/{dsh-session-hF4KiJEe.js → dsh-session-AQk-ryCb.js} +1 -1
- package/dist-app/assets/{experiment-loop-BpXLtMih.js → experiment-loop-BzzzDlbH.js} +1 -1
- package/dist-app/assets/http-bridge-BnYAV4lB.js +1 -0
- package/dist-app/assets/{index-Bmw4g-fF.js → index-BH9ozg79.js} +186 -186
- package/dist-app/assets/{index-C3A0a4vg.js → index-BmwffA_p.js} +1 -1
- package/dist-app/assets/{index-Dx__HVNV.js → index-YWyx765g.js} +3 -3
- package/dist-app/assets/{index-BEFqbPg2.js → index-aOSCk5D-.js} +1 -1
- package/dist-app/assets/{pi-session-Cza2Qn3S.js → pi-session-CNcLBGJ1.js} +1 -1
- package/dist-app/assets/{sensitive-scan-xUG_OUN8.js → sensitive-scan-Cs2TuwIB.js} +1 -1
- package/dist-app/assets/{session-handoff-BMAOplpO.js → session-handoff-CLQ0ZIYD.js} +2 -2
- package/dist-app/assets/{shared-CIY4bfom.js → shared-LHIg0YHa.js} +1 -1
- package/dist-app/assets/{turndown-plugin-gfm.cjs-8Yws7MsX.js → turndown-plugin-gfm.cjs-j992pbuV.js} +1 -1
- package/dist-app/assets/{update-check-Bpoi6nTP.js → update-check-3zMP9HL5.js} +1 -1
- package/dist-app/index.html +1 -1
- package/lib/client.js +181 -71
- package/lib/index.js +27 -1
- package/lib/runtime/backup-write.cjs +56 -0
- package/package.json +1 -1
- package/dist-app/assets/http-bridge-CeoNZsls.js +0 -1
package/lib/client.js
CHANGED
|
@@ -1,28 +1,45 @@
|
|
|
1
1
|
// dsh-thoughtdag client half — the lightest possible browser shim.
|
|
2
|
-
// It
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
2
|
+
// It puts ThoughtDAG where the harness puts its own pages: an entry in the
|
|
3
|
+
// sidebar's panel list (beside Plugins and Scheduled tasks) that opens the
|
|
4
|
+
// canvas in the main column, through the host's own `main` panel slot and
|
|
5
|
+
// `layout.selectPanel`. The canvas itself is a SAME-ORIGIN iframe at
|
|
6
|
+
// /thoughtdag/ (the SPA is served by the host half on the same web server —
|
|
7
|
+
// no CORS, no second origin). The iframe never lives inside the host's React
|
|
8
|
+
// tree: the main slot renders only the selected panel and would unmount it on
|
|
9
|
+
// every switch, reloading the canvas. So the panel page is a placeholder that
|
|
10
|
+
// fills the main column, and the iframe is a fixed layer laid exactly over
|
|
11
|
+
// it while the page is mounted — the host owns navigation, chrome, locale and
|
|
12
|
+
// theme; the canvas keeps its state across switches.
|
|
13
13
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// Hosts without the panel slot (older harnesses) get the previous door: a
|
|
15
|
+
// floating 对话 | 思维图 pill over the chat and a full-window overlay (0.4.19
|
|
16
|
+
// to 0.5.18; its placement was the subject of #55, #61, #63 and #73 — every
|
|
17
|
+
// spot in a stranger's chrome collides with something, which is why the
|
|
18
|
+
// panel entry replaces it wherever the host offers one).
|
|
19
|
+
//
|
|
20
|
+
// All conversation smarts live inside the ThoughtDAG app; this file only
|
|
21
|
+
// opens the door and forwards the current session id so the canvas can offer
|
|
22
|
+
// to mirror it. Same pattern as dsh-synapse: window.__ModuleLoader__.load
|
|
23
|
+
// with a module whose inject lists the client services it reads and whose
|
|
24
|
+
// apply touches the DOM.
|
|
17
25
|
|
|
18
26
|
window.__ModuleLoader__.load({
|
|
19
27
|
id: 'dsh-thoughtdag',
|
|
20
|
-
factory: () => {
|
|
28
|
+
factory: (require) => {
|
|
21
29
|
const module = { exports: {} }
|
|
22
30
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
31
|
+
// the host's React, for the two slot components (the sidebar glyph and
|
|
32
|
+
// the panel page); absent on a host that lends none, which also has no
|
|
33
|
+
// panel slot to fill
|
|
34
|
+
let React = null
|
|
35
|
+
try { React = require('react') } catch { React = null }
|
|
36
|
+
|
|
37
|
+
// sessions: the current session for the canvas to mirror. uiWorkspace: the
|
|
38
|
+
// harness's directory picker (the OS dialog when the harness runs on this
|
|
39
|
+
// machine, its in-app browser when reached remotely). slots / locale /
|
|
40
|
+
// layout: the panel entry, its label in the host's language, and the
|
|
41
|
+
// switch back to the conversation.
|
|
42
|
+
module.exports.inject = ['sessions', 'uiWorkspace', 'slots', 'locale', 'layout']
|
|
26
43
|
module.exports.apply = ctx => {
|
|
27
44
|
const currentSession = () => {
|
|
28
45
|
const snapshot = ctx.sessions.list.getSnapshot()
|
|
@@ -32,32 +49,37 @@ window.__ModuleLoader__.load({
|
|
|
32
49
|
return session === undefined ? null : { id, title: session.displayTitle ?? null, cwd: session.cwd ?? null }
|
|
33
50
|
}
|
|
34
51
|
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
// The sheet is OWNED: the host's module loader claims every untagged
|
|
53
|
+
// <style> for whichever plugin is materializing at that moment and
|
|
54
|
+
// removes it again when that plugin is replaced or reloaded; untagged,
|
|
55
|
+
// this sheet went with a stranger and the overlay collapsed into the
|
|
56
|
+
// document flow (#70). data-plugin names the owner, data-plugin-css the
|
|
57
|
+
// sheet; and apply reuses what an earlier run left, so nothing doubles.
|
|
58
|
+
const CSS_ID = 'dsh-thoughtdag/client.css'
|
|
59
|
+
let style = document.head.querySelector(`style[data-plugin-css="${CSS_ID}"]`)
|
|
60
|
+
if (!style) {
|
|
61
|
+
style = document.createElement('style')
|
|
62
|
+
style.setAttribute('data-plugin', 'dsh-thoughtdag')
|
|
63
|
+
style.setAttribute('data-plugin-css', CSS_ID)
|
|
64
|
+
document.head.append(style)
|
|
65
|
+
}
|
|
66
|
+
// Panel mode: .dsh-td-page is the placeholder the host renders in its
|
|
67
|
+
// main column; on macOS it pads by the window's top strip (the host's
|
|
68
|
+
// own entry pages do the same) so the canvas toolbar never sits under
|
|
69
|
+
// the traffic lights. .dsh-td-overlay is the iframe's fixed layer, laid
|
|
70
|
+
// over the page by place() — no inset of its own.
|
|
71
|
+
// Fallback mode (no panel slot): the floating pill in the window's top
|
|
72
|
+
// strip, the macOS title band over the strip while the canvas is up
|
|
73
|
+
// (#39), the overlay filling the window below the strip.
|
|
74
|
+
style.textContent = '.dsh-td-page{display:block;width:100%;height:100%;min-height:100%;box-sizing:border-box}html[data-platform="darwin"] .dsh-td-page{padding-top:var(--dsh-frame-top-clearance,0px)}.dsh-td-overlay{position:fixed;z-index:100;background:#faf9f7}.dsh-td-overlay[hidden]{display:none}.dsh-td-overlay iframe{display:block;width:100%;height:100%;border:0}.dsh-td-overlay.dsh-td-full{inset:var(--dsh-frame-top-clearance,0px) 0 0 0}.dsh-td-switch{position:fixed;z-index:120;top:12px;left:60%;display:flex;align-items:center;gap:2px;transform:translateX(-50%);-webkit-app-region:no-drag;border:1px solid #d1d5db;border-radius:999px;background:rgba(255,255,255,.96);padding:3px;backdrop-filter:blur(10px)}.dsh-td-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-td-switch button:hover{background:#f3f4f6;color:#111827}.dsh-td-switch button.active{background:#111827;color:#fff}.dsh-td-switch .dsh-td-mark{display:inline-block;width:8px;height:8px;border-radius:50%;background:#6d5dfc;box-shadow:0 -7px 0 #a99cff,0 7px 0 #f0a35a;margin:0 6px 0 8px;flex:none}.dsh-td-switch[hidden]{display:none}html[data-platform] .dsh-td-switch,html[data-windows-titlebar] .dsh-td-switch{top:max(2px,calc((var(--dsh-frame-top-clearance,40px) - 36px) / 2))}.dsh-td-bar{position:fixed;z-index:121;top:0;left:0;right:0;height:var(--dsh-frame-top-clearance,0px);display:flex;align-items:center;gap:14px;padding:0 16px;box-sizing:border-box;background:#faf9f7;border-bottom:1px solid #e7e2d9;font:500 12px Inter,system-ui,sans-serif;color:#6b7280;overflow:hidden;-webkit-app-region:drag;user-select:none}.dsh-td-bar[hidden]{display:none}html[data-platform="darwin"] .dsh-td-bar{padding-left:max(var(--dsh-frame-leading-clearance,0px),84px)}html[data-windows-titlebar] .dsh-td-bar{padding-right:calc(100% - env(titlebar-area-width,100%) + 16px)}.dsh-td-bar .dsh-td-brand{display:flex;align-items:center;gap:7px;color:#111827;font-weight:600;font-size:13px;white-space:nowrap}.dsh-td-bar .dsh-td-brand i{display:inline-block;width:8px;height:8px;border-radius:50%;background:#6d5dfc;box-shadow:0 -7px 0 #a99cff,0 7px 0 #f0a35a}.dsh-td-bar .dsh-td-switch{position:static;transform:none;-webkit-app-region:no-drag}.dsh-td-bar .dsh-td-title{min-width:0;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#6b7280}'
|
|
57
75
|
|
|
58
|
-
|
|
59
|
-
host
|
|
60
|
-
|
|
76
|
+
let host = document.body.querySelector(':scope > [data-dsh-thoughtdag]')
|
|
77
|
+
if (!host) {
|
|
78
|
+
host = document.createElement('div')
|
|
79
|
+
host.setAttribute('data-dsh-thoughtdag', '')
|
|
80
|
+
document.body.append(host)
|
|
81
|
+
}
|
|
82
|
+
host.innerHTML = '<div class="dsh-td-switch" role="group" aria-label="view switch" hidden><i class="dsh-td-mark" title="ThoughtDAG"></i><button type="button" data-view="dialog" class="active" aria-pressed="true">对话</button><button type="button" data-view="map" aria-pressed="false">思维图</button></div><div class="dsh-td-bar" data-window-drag hidden><span class="dsh-td-brand"><i></i>ThoughtDAG</span><div class="dsh-td-switch" role="group" aria-label="view switch"><button type="button" data-view="dialog" aria-pressed="false">对话</button><button type="button" data-view="map" class="active" aria-pressed="true">思维图</button></div><span class="dsh-td-title"></span></div><section class="dsh-td-overlay" hidden><iframe title="ThoughtDAG" data-src="/thoughtdag/"></iframe></section>'
|
|
61
83
|
|
|
62
84
|
// the plugin's version, for the canvas's update dialog and release history
|
|
63
85
|
let pluginVersion = null
|
|
@@ -66,52 +88,139 @@ window.__ModuleLoader__.load({
|
|
|
66
88
|
const switchEl = host.querySelector(':scope > .dsh-td-switch')
|
|
67
89
|
const bar = host.querySelector('.dsh-td-bar')
|
|
68
90
|
const barTitle = bar.querySelector('.dsh-td-title')
|
|
69
|
-
|
|
91
|
+
const dialogBtn = host.querySelector('[data-view="dialog"]')
|
|
92
|
+
const mapBtn = host.querySelector('[data-view="map"]')
|
|
93
|
+
const overlay = host.querySelector('.dsh-td-overlay')
|
|
94
|
+
const frame = host.querySelector('iframe')
|
|
95
|
+
// the desktop host marks its root; the strip is the window's top band (not in native fullscreen)
|
|
70
96
|
const desktop = () => document.documentElement.dataset.platform !== undefined || document.documentElement.hasAttribute('data-windows-titlebar')
|
|
71
97
|
const strip = () => parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--dsh-frame-top-clearance')) || 0
|
|
98
|
+
const send = (type, payload) => frame.contentWindow?.postMessage({ source: 'dsh-thoughtdag', type, ...payload }, location.origin)
|
|
99
|
+
const syncCurrent = () => send('td:current-session', { session: currentSession() })
|
|
100
|
+
// the SPA boots on first open, never while hidden: a canvas that
|
|
101
|
+
// measures itself inside a display:none frame fits its view to a 0×0
|
|
102
|
+
// box and shows nothing when revealed
|
|
103
|
+
const ensureSrc = () => { if (!frame.src) frame.src = frame.dataset.src + (pluginVersion ? (frame.dataset.src.includes('?') ? '&' : '?') + 'dv=' + encodeURIComponent(pluginVersion) : '') }
|
|
104
|
+
|
|
105
|
+
// ── panel mode: the host's sidebar entry and main-column page ──
|
|
106
|
+
const PANEL_ID = 'thoughtdag'
|
|
107
|
+
const NS = 'dsh-thoughtdag'
|
|
108
|
+
let panelMode = false
|
|
109
|
+
// the page element while the panel is selected (null otherwise)
|
|
110
|
+
let pageEl = null
|
|
111
|
+
let pageObserver = null
|
|
112
|
+
const place = () => {
|
|
113
|
+
if (!pageEl) return
|
|
114
|
+
let box = pageEl
|
|
115
|
+
// the page fills its column; should a host give it no height, the
|
|
116
|
+
// nearest ancestor with one is the column
|
|
117
|
+
for (let i = 0; i < 3 && box.getBoundingClientRect().height < 40 && box.parentElement; i++) box = box.parentElement
|
|
118
|
+
const r = box.getBoundingClientRect()
|
|
119
|
+
const pad = box === pageEl ? parseFloat(getComputedStyle(pageEl).paddingTop) || 0 : 0
|
|
120
|
+
overlay.style.left = r.left + 'px'
|
|
121
|
+
overlay.style.top = (r.top + pad) + 'px'
|
|
122
|
+
overlay.style.width = r.width + 'px'
|
|
123
|
+
overlay.style.height = Math.max(0, r.height - pad) + 'px'
|
|
124
|
+
}
|
|
125
|
+
const openPanel = el => {
|
|
126
|
+
pageEl = el
|
|
127
|
+
ensureSrc()
|
|
128
|
+
overlay.classList.remove('dsh-td-full')
|
|
129
|
+
overlay.hidden = false
|
|
130
|
+
place()
|
|
131
|
+
pageObserver = new ResizeObserver(place)
|
|
132
|
+
pageObserver.observe(el)
|
|
133
|
+
if (el.parentElement) pageObserver.observe(el.parentElement)
|
|
134
|
+
syncCurrent()
|
|
135
|
+
// no band: the host's own column is the canvas's edge; the SPA shows its own switch
|
|
136
|
+
send('td:view', { shown: true, bar: false, desktop: desktop(), panel: true })
|
|
137
|
+
window.setTimeout(() => { if (pageEl) { syncCurrent(); send('td:view', { shown: true, bar: false, desktop: desktop(), panel: true }) } }, 400)
|
|
138
|
+
}
|
|
139
|
+
const closePanel = () => {
|
|
140
|
+
pageEl = null
|
|
141
|
+
pageObserver?.disconnect()
|
|
142
|
+
pageObserver = null
|
|
143
|
+
overlay.hidden = true
|
|
144
|
+
send('td:view', { shown: false, bar: false, desktop: desktop(), panel: true })
|
|
145
|
+
}
|
|
146
|
+
// set once the host has declared the panel slots and taken our entries; a
|
|
147
|
+
// host that never declares them (older harnesses) gets the fallback below
|
|
148
|
+
let registered = false
|
|
149
|
+
if (React && ctx.slots && ctx.layout && ctx.locale && typeof ctx.effect === 'function') {
|
|
150
|
+
try {
|
|
151
|
+
const h = React.createElement
|
|
152
|
+
// the three-dot mark, as the sidebar glyph
|
|
153
|
+
const PanelIcon = ({ size }) => h('svg', { width: size ?? 16, height: size ?? 16, viewBox: '0 0 16 16', 'aria-hidden': 'true' },
|
|
154
|
+
h('circle', { cx: 8, cy: 3, r: 2.2, fill: '#a99cff' }),
|
|
155
|
+
h('circle', { cx: 8, cy: 8, r: 2.2, fill: '#6d5dfc' }),
|
|
156
|
+
h('circle', { cx: 8, cy: 13, r: 2.2, fill: '#f0a35a' }))
|
|
157
|
+
// the page: a placeholder the iframe layer is laid over while mounted
|
|
158
|
+
const PanelPage = () => {
|
|
159
|
+
const ref = React.useRef(null)
|
|
160
|
+
React.useEffect(() => {
|
|
161
|
+
if (ref.current) openPanel(ref.current)
|
|
162
|
+
return () => closePanel()
|
|
163
|
+
}, [])
|
|
164
|
+
return h('div', { ref, className: 'dsh-td-page', 'data-td-page': '' })
|
|
165
|
+
}
|
|
166
|
+
ctx.effect(() => ctx.locale.register(NS, { zh: { panel: 'ThoughtDAG' }, en: { panel: 'ThoughtDAG' } }), 'dsh-thoughtdag: dictionaries')
|
|
167
|
+
const t = ctx.locale.bind(NS)
|
|
168
|
+
ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: PANEL_ID, locale: NS, inject: () => ({}) }, PanelPage))
|
|
169
|
+
ctx.slots.inject('sidebar.panellist', () => { const dispose = ctx.slots.register({ name: 'sidebar.panellist', id: PANEL_ID, order: 30, locale: NS, label: () => t('panel') }, PanelIcon); registered = true; return dispose })
|
|
170
|
+
panelMode = true
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.warn('[dsh-thoughtdag] panel entry unavailable, using the floating switch:', error)
|
|
173
|
+
panelMode = false
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── fallback mode: the floating pill and the full-window overlay ──
|
|
178
|
+
let spaReady = false
|
|
72
179
|
// the band only on macOS: there the strip under the traffic lights is transparent and the host's session
|
|
73
180
|
// header would show through the overlay (#39). On Windows the strip is the host's own chrome (its menu,
|
|
74
181
|
// the caption buttons), opaque and above us; a band there painted over the host's menu (#55). With no
|
|
75
182
|
// band the overlay starts below the chrome and the canvas shows its own switch.
|
|
76
|
-
const barUp = () => document.documentElement.dataset.platform === 'darwin' && strip() > 0
|
|
77
|
-
let spaReady = false
|
|
78
|
-
const dialogBtn = host.querySelector('[data-view="dialog"]')
|
|
79
|
-
const mapBtn = host.querySelector('[data-view="map"]')
|
|
80
|
-
const overlay = host.querySelector('.dsh-td-overlay')
|
|
81
|
-
const frame = host.querySelector('iframe')
|
|
82
|
-
|
|
183
|
+
const barUp = () => !panelMode && document.documentElement.dataset.platform === 'darwin' && strip() > 0
|
|
83
184
|
const setView = map => {
|
|
84
185
|
dialogBtn.classList.toggle('active', !map)
|
|
85
186
|
dialogBtn.setAttribute('aria-pressed', String(!map))
|
|
86
187
|
mapBtn.classList.toggle('active', map)
|
|
87
188
|
mapBtn.setAttribute('aria-pressed', String(map))
|
|
88
189
|
}
|
|
89
|
-
const
|
|
190
|
+
const closeFallback = () => { overlay.hidden = true; bar.hidden = true; switchEl.hidden = false; setView(false); send('td:view', { shown: false, bar: false, desktop: desktop() }) }
|
|
90
191
|
const showBand = () => { bar.hidden = !barUp(); send('td:view', { shown: true, bar: !bar.hidden, desktop: desktop() }) }
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const syncCurrent = () => {
|
|
96
|
-
const session = currentSession()
|
|
97
|
-
send('td:current-session', { session })
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
mapBtn.addEventListener('click', () => {
|
|
192
|
+
const openFallback = () => {
|
|
193
|
+
overlay.classList.add('dsh-td-full')
|
|
194
|
+
overlay.style.left = overlay.style.top = overlay.style.width = overlay.style.height = ''
|
|
101
195
|
overlay.hidden = false
|
|
102
196
|
setView(true)
|
|
103
197
|
bar.hidden = !barUp()
|
|
104
198
|
if (spaReady || !bar.hidden) switchEl.hidden = true
|
|
105
|
-
|
|
106
|
-
// measures itself inside a display:none frame fits its view to a 0×0
|
|
107
|
-
// box and shows nothing when revealed
|
|
108
|
-
if (!frame.src) frame.src = frame.dataset.src + (pluginVersion ? (frame.dataset.src.includes('?') ? '&' : '?') + 'dv=' + encodeURIComponent(pluginVersion) : '')
|
|
199
|
+
ensureSrc()
|
|
109
200
|
syncCurrent()
|
|
110
201
|
showBand()
|
|
111
202
|
// let the SPA boot, then re-sync so its listener is ready
|
|
112
203
|
window.setTimeout(() => { syncCurrent(); showBand() }, 400)
|
|
113
|
-
}
|
|
114
|
-
|
|
204
|
+
}
|
|
205
|
+
let fallbackOn = false
|
|
206
|
+
const enableFallback = () => {
|
|
207
|
+
if (fallbackOn) return
|
|
208
|
+
fallbackOn = true
|
|
209
|
+
panelMode = false
|
|
210
|
+
switchEl.hidden = false
|
|
211
|
+
window.addEventListener('resize', () => { if (!overlay.hidden) showBand() })
|
|
212
|
+
bar.querySelector('[data-view="dialog"]').addEventListener('click', closeFallback)
|
|
213
|
+
mapBtn.addEventListener('click', openFallback)
|
|
214
|
+
dialogBtn.addEventListener('click', closeFallback)
|
|
215
|
+
}
|
|
216
|
+
if (!panelMode) enableFallback()
|
|
217
|
+
// the slot system takes registrations only once the host declares the
|
|
218
|
+
// slot; a host that has not declared sidebar.panellist within a moment
|
|
219
|
+
// of our apply has no panel list at all, so the floating switch returns
|
|
220
|
+
else window.setTimeout(() => { if (!registered) { console.warn('[dsh-thoughtdag] no sidebar panel list on this host, using the floating switch'); enableFallback() } }, 2500)
|
|
221
|
+
|
|
222
|
+
// back to the conversation: the host's own navigation in panel mode, the overlay's close otherwise
|
|
223
|
+
const close = () => { if (panelMode) { try { ctx.layout.selectPanel(null) } catch { closePanel() } } else closeFallback() }
|
|
115
224
|
|
|
116
225
|
window.addEventListener('message', event => {
|
|
117
226
|
if (event.origin !== location.origin || event.data?.source !== 'dsh-thoughtdag') return
|
|
@@ -132,12 +241,13 @@ window.__ModuleLoader__.load({
|
|
|
132
241
|
return
|
|
133
242
|
}
|
|
134
243
|
if (event.data.type === 'td:request-current') {
|
|
135
|
-
// the SPA has booted and shows its own switch: the pill steps aside
|
|
244
|
+
// the SPA has booted and shows its own switch: in fallback mode the pill steps aside
|
|
136
245
|
spaReady = true
|
|
137
|
-
if (!overlay.hidden) { switchEl.hidden = true; showBand() }
|
|
246
|
+
if (!panelMode && !overlay.hidden) { switchEl.hidden = true; showBand() }
|
|
247
|
+
if (panelMode && pageEl) send('td:view', { shown: true, bar: false, desktop: desktop(), panel: true })
|
|
138
248
|
return syncCurrent()
|
|
139
249
|
}
|
|
140
|
-
// the canvas names its project: the band shows it
|
|
250
|
+
// the canvas names its project: the band shows it (fallback mode only)
|
|
141
251
|
if (event.data.type === 'td:title') { barTitle.textContent = typeof event.data.name === 'string' ? event.data.name : ''; return }
|
|
142
252
|
// the canvas forked or continued a session: stage it and go back to the
|
|
143
253
|
// chat, which now shows exactly the context the canvas produced
|
package/lib/index.js
CHANGED
|
@@ -81,6 +81,10 @@ export const inject = ['webServer', 'sessions', 'sessionController', 'agents', '
|
|
|
81
81
|
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
82
82
|
const require = createRequire(import.meta.url)
|
|
83
83
|
const APP_DIR = resolve(__dirname, '../dist-app')
|
|
84
|
+
// the path rules for the canvas's local backup (#66); the route below shells
|
|
85
|
+
// this, and it is required HERE because the handler needs it in scope — inside
|
|
86
|
+
// agentsHttp() it would be a function-local binding the handler cannot see
|
|
87
|
+
const { writeBackup, BackupRequestError } = require(resolve(__dirname, 'runtime', 'backup-write.cjs'))
|
|
84
88
|
|
|
85
89
|
// The plugin's own version travels into the canvas's URL (?dv=), so the
|
|
86
90
|
// update dialog and the release history know which release runs here, as
|
|
@@ -422,7 +426,13 @@ function contentOf(body) {
|
|
|
422
426
|
throw new HttpError(400, 'body needs text or blocks')
|
|
423
427
|
}
|
|
424
428
|
|
|
425
|
-
|
|
429
|
+
// The attribution on every message this plugin injects. The kind is this
|
|
430
|
+
// producer's OWN: the harness's v4 session format refuses the bare shared
|
|
431
|
+
// literal 'plugin' (its validator throws "format v4 message requires a
|
|
432
|
+
// producer-owned source kind" before the Session adopts the event, so the
|
|
433
|
+
// targeted turn dies and nothing is persisted, #69); only the v3→v4 migration
|
|
434
|
+
// ever rewrote it. scripts/test-inject-source.mjs holds this line to that rule.
|
|
435
|
+
const SOURCE = { kind: 'plugin:dsh-thoughtdag', plugin: 'dsh-thoughtdag' }
|
|
426
436
|
|
|
427
437
|
// ── the other agents' session files on this machine ────────────────────
|
|
428
438
|
|
|
@@ -1217,6 +1227,22 @@ export async function apply(ctx, config) {
|
|
|
1217
1227
|
const body = req.method === 'POST' ? await readJson(req, MAX_WRITE_BODY_BYTES).catch(() => null) : null
|
|
1218
1228
|
if (await agentsHttp().handle(req, res, path, body)) return
|
|
1219
1229
|
}
|
|
1230
|
+
// ── the canvas's local backup, written by THIS host (#66) ──
|
|
1231
|
+
// The File System Access API needs a directory handle, and Chromium's own
|
|
1232
|
+
// picker never returns inside the harness frame on Windows, so the canvas
|
|
1233
|
+
// cannot write the file itself there. It keeps the absolute path the
|
|
1234
|
+
// harness's own picker handed it and asks this endpoint to write instead.
|
|
1235
|
+
// The path rules live in runtime/backup-write.cjs so they can be tested
|
|
1236
|
+
// without a harness: scripts/test-backup-write.mjs.
|
|
1237
|
+
if (path === '/backup' && req.method === 'POST') {
|
|
1238
|
+
const body = await readJson(req, MAX_WRITE_BODY_BYTES).catch(() => null)
|
|
1239
|
+
try {
|
|
1240
|
+
return sendJson(res, 200, { file: await writeBackup(body) })
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
if (error instanceof BackupRequestError) return sendJson(res, 400, { error: error.message })
|
|
1243
|
+
return sendJson(res, 500, { error: 'could not write the backup: ' + (error instanceof Error ? error.message : String(error)) })
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1220
1246
|
// ── model connection (the SPA's proxy protocol, on the harness's providers) ──
|
|
1221
1247
|
if (path === '/models' && req.method === 'GET') return sendJson(res, 200, await modelsPayload(ctx, registeredProviders))
|
|
1222
1248
|
// the canvas registers its browser-stored interfaces at boot (and after a change): the list answers with them in it
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The canvas's local backup, written by the HOST (#66).
|
|
2
|
+
//
|
|
3
|
+
// The File System Access API needs a directory handle, and Chromium's own
|
|
4
|
+
// picker never returns inside the harness frame on Windows — the canvas cannot
|
|
5
|
+
// write the file itself there. So the canvas keeps the absolute path the
|
|
6
|
+
// harness's own picker handed it and asks the host to write instead.
|
|
7
|
+
//
|
|
8
|
+
// This lives beside the agent runtimes rather than inline in the plugin, so the
|
|
9
|
+
// path rules can be tested without a harness: scripts/test-backup-write.mjs.
|
|
10
|
+
// Pure Node, no harness context.
|
|
11
|
+
'use strict';
|
|
12
|
+
const { mkdir, writeFile } = require('node:fs/promises');
|
|
13
|
+
const { dirname, isAbsolute, resolve, sep } = require('node:path');
|
|
14
|
+
|
|
15
|
+
/** An error the endpoint should report as 400 rather than 500. */
|
|
16
|
+
class BackupRequestError extends Error {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Write one canvas backup under `dir`.
|
|
20
|
+
*
|
|
21
|
+
* @param {{ dir: unknown, name: unknown, json: unknown }} body the request body
|
|
22
|
+
* @returns {Promise<string>} the absolute file written
|
|
23
|
+
* @throws {BackupRequestError} when the request itself is not writable
|
|
24
|
+
*/
|
|
25
|
+
async function writeBackup(body) {
|
|
26
|
+
const dir = typeof body?.dir === 'string' ? body.dir : '';
|
|
27
|
+
const name = typeof body?.name === 'string' ? body.name : '';
|
|
28
|
+
const json = typeof body?.json === 'string' ? body.json : null;
|
|
29
|
+
// an absolute path is the whole safety property: a relative one would resolve
|
|
30
|
+
// against the plugin's cwd, which the canvas cannot know.
|
|
31
|
+
// isAbsolute, NOT resolve(dir).startsWith(sep) — on Windows a drive path
|
|
32
|
+
// starts with "C:" and would never match the separator, so that test refuses
|
|
33
|
+
// every real request there while passing on POSIX.
|
|
34
|
+
if (!dir || !isAbsolute(dir) || !name || json === null) {
|
|
35
|
+
throw new BackupRequestError('dir must be an absolute path; name and json are required');
|
|
36
|
+
}
|
|
37
|
+
// `name` is a RELATIVE path the canvas chose (native canvases at the folder
|
|
38
|
+
// root, session mirrors under cli/), so it is sanitized segment by segment
|
|
39
|
+
const parts = name.replace(/\\/g, '/').split('/').filter((p) => p && p !== '.' && p !== '..');
|
|
40
|
+
if (parts.length === 0) throw new BackupRequestError('name must have at least one path segment');
|
|
41
|
+
const last = parts[parts.length - 1].replace(/[<>:"|?*\u0000-\u001f]/g, '_').slice(0, 120);
|
|
42
|
+
if (!last) throw new BackupRequestError('name resolves to an empty file name');
|
|
43
|
+
const target = resolve(dir, ...parts.slice(0, -1), last);
|
|
44
|
+
// Belt and braces, and it has to agree with the gate above: compare against
|
|
45
|
+
// resolve(dir) — NOT resolve(dir) + sep, which would reject a Windows path
|
|
46
|
+
// for the same reason the startsWith(sep) test did.
|
|
47
|
+
const inside = resolve(dir);
|
|
48
|
+
if (target !== inside && !target.startsWith(inside.endsWith(sep) ? inside : inside + sep)) {
|
|
49
|
+
throw new BackupRequestError('name escapes the backup directory');
|
|
50
|
+
}
|
|
51
|
+
await mkdir(dirname(target), { recursive: true });
|
|
52
|
+
await writeFile(target, json, 'utf8');
|
|
53
|
+
return target;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { writeBackup, BackupRequestError };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-thoughtdag",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.19",
|
|
4
4
|
"description": "ThoughtDAG for DeepSeek Harness: turn the current DSH session into an editable thought graph on an infinite canvas. Source, releases and issues: github.com/chenxiachan/thoughtdag",
|
|
5
5
|
"author": "Xia Chen",
|
|
6
6
|
"repository": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{I as f}from"./index-Bmw4g-fF.js";async function y(l){if(window.desktopAgents)return!0;const r=l.replace(/\/+$/,"");let i;try{const e=await fetch(r+"/agents/available",{credentials:"same-origin"});if(!e.ok)return!1;i=await e.json()}catch{return!1}const s=async(e,t)=>{const n=await fetch(r+e,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"same-origin",body:JSON.stringify(t??{})});if(!n.ok)throw new Error(`${e}: ${n.status}`);return n.json()},c=[];let o=null;const d=()=>{o||(o=new EventSource(r+"/agents/events"),o.onmessage=e=>{try{const t=JSON.parse(e.data);for(const n of c)n(t)}catch{}})},w=f,g={nativePicker:w},p=()=>new Promise(e=>{if(!w)return e(null);const t=`cwd-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,8)}`,n=u=>{const a=u.data;u.origin!==window.location.origin||a?.source!=="dsh-thoughtdag"||a.type!=="td:picked-cwd"||a.requestId!==t||(window.removeEventListener("message",n),a.unsupported&&(g.nativePicker=!1),e(typeof a.path=="string"&&a.path?a.path:null))};window.addEventListener("message",n),window.parent.postMessage({source:"dsh-thoughtdag",type:"td:pick-cwd",requestId:t},window.location.origin)});return window.desktopAgents={capabilities:g,available:async()=>i,models:async e=>(await fetch(r+"/agents/models"+(e?`?runtime=${encodeURIComponent(e)}`:""),{credentials:"same-origin"})).json(),run:async e=>(d(),(await s("/agents/run",e)).runId),abort:async e=>(await s("/agents/abort",{runId:e})).ok,answer:async(e,t,n)=>(await s("/agents/answer",{runId:e,requestId:t,response:n})).ok,workspace:async e=>(await s("/agents/workspace",{canvasId:e})).dir,pickCwd:()=>p(),guardWrite:async(e,t)=>(await s("/agents/guard",{cwd:e,config:t})).ok,writeMaterials:async(e,t)=>s("/agents/materials",{cwd:e,files:t}),onEvent:e=>{c.push(e),d()}},!0}export{y as installAgentsHttpBridge};
|