dsh-file-activity 0.5.0 → 0.5.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/CHANGELOG.md +19 -0
- package/README.md +3 -4
- package/lib/api-route.js +18 -4
- package/lib/client.js +1270 -845
- package/lib/cwd.js +14 -0
- package/lib/index.js +20 -12
- package/lib/media-route.js +8 -3
- package/lib/parts/api.part.js +41 -41
- package/lib/parts/apply.part.js +69 -60
- package/lib/parts/auto-open.part.js +69 -68
- package/lib/parts/format.part.js +26 -26
- package/lib/parts/i18n.part.js +41 -40
- package/lib/parts/interceptor.part.js +50 -50
- package/lib/parts/preview.part.js +205 -160
- package/lib/parts/rows.part.js +157 -113
- package/lib/parts/store.part.js +16 -16
- package/lib/parts/styles.part.js +8 -5
- package/lib/parts/tree.part.js +79 -66
- package/lib/parts/view.part.js +172 -118
- package/lib/state.js +15 -8
- package/lib/store.js +4 -11
- package/package.json +3 -2
- package/lib/fence.js +0 -57
- package/lib/http.js +0 -34
- package/lib/parts/icons.part.js +0 -48
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// ── fetch interception: sidebar file operations ───────────────────────
|
|
2
|
+
function methodOf(init) {
|
|
3
|
+
return (init?.method ?? 'GET').toUpperCase()
|
|
4
|
+
}
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
/** POST body as a plain object (non-string bodies are ignored). */
|
|
7
|
+
function parseBody(init) {
|
|
8
|
+
return typeof init?.body === 'string' ? JSON.parse(init.body) : {}
|
|
9
|
+
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
/** Record fs.read / fs.write POSTs observed on the sidebar API. */
|
|
12
|
+
function recordSidebarFs(url, init) {
|
|
13
|
+
if (url.pathname !== '/sidebar/api/fs.read' && url.pathname !== '/sidebar/api/fs.write') return
|
|
14
|
+
if (methodOf(init) !== 'POST') return
|
|
15
|
+
const body = parseBody(init)
|
|
16
|
+
if (typeof body.sessionId !== 'string' || typeof body.path !== 'string') return
|
|
17
|
+
postRecord(body.sessionId, body.path, url.pathname === '/sidebar/api/fs.write' ? 'write' : 'read')
|
|
18
|
+
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
/** Record sidebar media opens (/sidebar/file?sessionId=...&path=...). */
|
|
21
|
+
function recordMediaOpen(url, init) {
|
|
22
|
+
if (url.pathname !== '/sidebar/file' || methodOf(init) !== 'GET') return
|
|
23
|
+
const sessionId = url.searchParams.get('sessionId')
|
|
24
|
+
const path = url.searchParams.get('path')
|
|
25
|
+
if (sessionId !== null && path !== null) postRecord(sessionId, path, 'read')
|
|
26
|
+
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
/** Observe a resolved fetch URL and record sidebar file operations. */
|
|
29
|
+
function observeSidebarFetch(url, init) {
|
|
30
|
+
try {
|
|
31
|
+
recordSidebarFs(url, init)
|
|
32
|
+
recordMediaOpen(url, init)
|
|
33
|
+
} catch {
|
|
34
|
+
// observation must never break the underlying call
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
observeSidebarFetch(url, init)
|
|
51
|
-
return result
|
|
52
|
-
}
|
|
53
|
-
return () => {
|
|
54
|
-
window.fetch = original
|
|
55
|
-
}
|
|
38
|
+
function installFetchInterceptor() {
|
|
39
|
+
const original = window.fetch.bind(window)
|
|
40
|
+
window.fetch = (input, init) => {
|
|
41
|
+
const result = original(input, init)
|
|
42
|
+
let url
|
|
43
|
+
try {
|
|
44
|
+
if (typeof input === 'string') url = new URL(input, window.location.href)
|
|
45
|
+
else if (input instanceof URL) url = input
|
|
46
|
+
else return result // Request instances: skip observation
|
|
47
|
+
} catch {
|
|
48
|
+
return result
|
|
56
49
|
}
|
|
50
|
+
observeSidebarFetch(url, init)
|
|
51
|
+
return result
|
|
52
|
+
}
|
|
53
|
+
return () => {
|
|
54
|
+
window.fetch = original
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,178 +1,223 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// ── floating preview window (reuses the sidebar's native viewer) ──────
|
|
2
|
+
/** Resolve a possibly-relative path against the session cwd. */
|
|
3
|
+
function resolvePath(path, cwd) {
|
|
4
|
+
if (typeof path !== 'string' || path === '') return path
|
|
5
|
+
if (path.startsWith('/')) return path
|
|
6
|
+
if (typeof cwd === 'string' && cwd !== '') return `${cwd.replace(/\/+$/, '')}/${path}`
|
|
7
|
+
return path
|
|
8
|
+
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
/** Whether the fs.read API response carries a text content payload. */
|
|
11
|
+
function isFsReadOk(json) {
|
|
12
|
+
return json !== null && typeof json === 'object' && json.ok === true && typeof json.value?.content === 'string'
|
|
13
|
+
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
/** Error load state from an fs.read API response (or a generic message). */
|
|
16
|
+
function fsReadError(json, viewer) {
|
|
17
|
+
return { status: 'error', viewer, message: json?.error?.message ?? strings.previewFailed() }
|
|
18
|
+
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Load fsRead content through the sidebar API and resolve the viewer's
|
|
22
|
+
* load state (ready with text, or error with the API message).
|
|
23
|
+
*/
|
|
24
|
+
async function loadFsReadContent(viewer, path, scope, sessionId) {
|
|
25
|
+
const target = resolvePath(path, scope?.cwd ?? '')
|
|
26
|
+
const response = await fetch('/sidebar/api/fs.read', {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'content-type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({ sessionId, path: target }),
|
|
30
|
+
})
|
|
31
|
+
const json = await response.json()
|
|
32
|
+
if (isFsReadOk(json)) return { status: 'ready', viewer, content: json.value.content }
|
|
33
|
+
return fsReadError(json, viewer)
|
|
34
|
+
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Fetch the bytes the viewer's fetchStrategy needs (fsRead text /
|
|
38
|
+
* mediaUrl / customData) and resolve its load state.
|
|
39
|
+
*/
|
|
40
|
+
async function fetchPreviewLoad(viewer, path, scope, sessionId) {
|
|
41
|
+
const strategy = viewer.fetchStrategy
|
|
42
|
+
if (strategy === 'fsRead') return loadFsReadContent(viewer, path, scope, sessionId)
|
|
43
|
+
if (strategy === 'mediaUrl') {
|
|
44
|
+
return { status: 'ready', viewer, mediaUrl: mediaUrlOf(sessionId, path) }
|
|
45
|
+
}
|
|
46
|
+
if (strategy === 'custom') {
|
|
47
|
+
const data = await (viewer.load?.(path, scope) ?? Promise.resolve(undefined))
|
|
48
|
+
return { status: 'ready', viewer, customData: data }
|
|
49
|
+
}
|
|
50
|
+
// 'binary-download' and anything else: mount the viewer's own
|
|
51
|
+
// component (it handles the download / media itself).
|
|
52
|
+
return { status: 'ready', viewer }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the file's viewer through the sidebar registry and load the
|
|
57
|
+
* bytes it needs; failures become an error state shown in the window.
|
|
58
|
+
*/
|
|
59
|
+
function usePreviewLoader(service, path, sessionId, scope) {
|
|
60
|
+
const [load, setLoad] = useState({ status: 'loading', viewer: null })
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
let cancelled = false
|
|
63
|
+
const viewer = service?.matchFileViewer?.(path)
|
|
64
|
+
if (!viewer) {
|
|
65
|
+
setLoad({ status: 'error', viewer: null, message: strings.previewUnsupported() })
|
|
66
|
+
return () => {
|
|
67
|
+
cancelled = true
|
|
49
68
|
}
|
|
50
|
-
// 'binary-download' and anything else: mount the viewer's own
|
|
51
|
-
// component (it handles the download / media itself).
|
|
52
|
-
return { status: 'ready', viewer }
|
|
53
69
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
setLoad({ status: 'error', viewer: null, message: strings.previewUnsupported() })
|
|
66
|
-
return () => { cancelled = true }
|
|
67
|
-
}
|
|
68
|
-
setLoad({ status: 'loading', viewer })
|
|
69
|
-
fetchPreviewLoad(viewer, path, scope, sessionId)
|
|
70
|
-
.then((next) => {
|
|
71
|
-
if (!cancelled) setLoad(next)
|
|
72
|
-
})
|
|
73
|
-
.catch((error) => {
|
|
74
|
-
if (!cancelled) setLoad({ status: 'error', viewer, message: error instanceof Error ? error.message : String(error) })
|
|
70
|
+
setLoad({ status: 'loading', viewer })
|
|
71
|
+
fetchPreviewLoad(viewer, path, scope, sessionId)
|
|
72
|
+
.then((next) => {
|
|
73
|
+
if (!cancelled) setLoad(next)
|
|
74
|
+
})
|
|
75
|
+
.catch((error) => {
|
|
76
|
+
if (!cancelled)
|
|
77
|
+
setLoad({
|
|
78
|
+
status: 'error',
|
|
79
|
+
viewer,
|
|
80
|
+
message: error instanceof Error ? error.message : String(error),
|
|
75
81
|
})
|
|
76
|
-
return () => { cancelled = true }
|
|
77
|
-
}, [path, sessionId, scope])
|
|
78
|
-
return load
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Preview window body: loading note / error panel / viewer mount. */
|
|
82
|
-
function renderPreviewBody(load, ctx, store, scope, path, title, sessionId) {
|
|
83
|
-
if (load.status === 'loading') {
|
|
84
|
-
return createElement('div', { className: 'dfa-fp-note' }, strings.loading())
|
|
85
|
-
}
|
|
86
|
-
if (load.status === 'error') {
|
|
87
|
-
return createElement('div', { className: 'dfa-fp-err' },
|
|
88
|
-
strings.previewFailed(),
|
|
89
|
-
load.message
|
|
90
|
-
? createElement('div', { style: { marginTop: '6px', fontSize: '11px', opacity: 0.85 } }, load.message)
|
|
91
|
-
: null,
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
if (load.viewer.id === 'pdf') {
|
|
95
|
-
const url = mediaUrlOf(sessionId, path)
|
|
96
|
-
return createElement(PdfPreview, { src: url, download: `${url}&download=1`, title })
|
|
97
|
-
}
|
|
98
|
-
return createElement(load.viewer.component, {
|
|
99
|
-
ctx, store, scope, path, title,
|
|
100
|
-
viewerId: load.viewer.id,
|
|
101
|
-
content: load.content,
|
|
102
|
-
mediaUrl: load.mediaUrl,
|
|
103
|
-
customData: load.customData,
|
|
104
82
|
})
|
|
83
|
+
return () => {
|
|
84
|
+
cancelled = true
|
|
105
85
|
}
|
|
86
|
+
}, [path, sessionId, scope])
|
|
87
|
+
return load
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Preview window body: loading note / error panel / viewer mount. */
|
|
91
|
+
function renderPreviewBody(load, ctx, store, scope, path, title, sessionId) {
|
|
92
|
+
if (load.status === 'loading') {
|
|
93
|
+
return createElement('div', { className: 'dfa-fp-note' }, strings.loading())
|
|
94
|
+
}
|
|
95
|
+
if (load.status === 'error') {
|
|
96
|
+
return createElement(
|
|
97
|
+
'div',
|
|
98
|
+
{ className: 'dfa-fp-err' },
|
|
99
|
+
strings.previewFailed(),
|
|
100
|
+
load.message
|
|
101
|
+
? createElement('div', { style: { marginTop: '6px', fontSize: '11px', opacity: 0.85 } }, load.message)
|
|
102
|
+
: null,
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
if (load.viewer.id === 'pdf') {
|
|
106
|
+
const url = mediaUrlOf(sessionId, path)
|
|
107
|
+
return createElement(PdfPreview, { src: url, download: `${url}&download=1`, title })
|
|
108
|
+
}
|
|
109
|
+
return createElement(load.viewer.component, {
|
|
110
|
+
ctx,
|
|
111
|
+
store,
|
|
112
|
+
scope,
|
|
113
|
+
path,
|
|
114
|
+
title,
|
|
115
|
+
viewerId: load.viewer.id,
|
|
116
|
+
content: load.content,
|
|
117
|
+
mediaUrl: load.mediaUrl,
|
|
118
|
+
customData: load.customData,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
106
121
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
122
|
+
/**
|
|
123
|
+
* A floating preview window. Instead of re-implementing rendering, it
|
|
124
|
+
* asks the sidebar registry for the file's viewer (`matchFileViewer`),
|
|
125
|
+
* fetches the bytes the viewer's fetchStrategy needs (fsRead text /
|
|
126
|
+
* mediaUrl / customData), then mounts that viewer's own component — so
|
|
127
|
+
* code gets syntax highlighting and markdown gets rendered by the SAME
|
|
128
|
+
* built-in renderers the sidebar's editor tab uses.
|
|
129
|
+
*
|
|
130
|
+
* Media caveat: the sidebar's own media route (/sidebar/file) only serves
|
|
131
|
+
* files inside the session working directory, while file activity records
|
|
132
|
+
* files the agent touched anywhere (/tmp scratch files, sibling repos…).
|
|
133
|
+
* Media bytes therefore come from OUR route (/file-activity/file), which
|
|
134
|
+
* authorizes exactly the paths this session recorded; PDF is the one
|
|
135
|
+
* built-in viewer that fetches its own URL internally (it ignores the
|
|
136
|
+
* `mediaUrl` prop), so it gets a small iframe preview instead.
|
|
137
|
+
*/
|
|
138
|
+
function FloatingPreview({ ctx, store, scope, preview, onClose }) {
|
|
139
|
+
const sessionId = scope?.sessionId ?? ''
|
|
140
|
+
const path = preview.abs
|
|
141
|
+
const title = preview.name
|
|
142
|
+
const service = ctx.betterSidebar
|
|
143
|
+
const load = usePreviewLoader(service, path, sessionId, scope)
|
|
129
144
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
// Clicking outside is the primary dismiss (the overlay's onClick);
|
|
146
|
+
// Escape is a keyboard affordance. Both call onClose.
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return () => {}
|
|
149
|
+
const handler = (event) => {
|
|
150
|
+
if (event && event.key === 'Escape') onClose()
|
|
151
|
+
}
|
|
152
|
+
document.addEventListener('keydown', handler)
|
|
153
|
+
return () => document.removeEventListener('keydown', handler)
|
|
154
|
+
}, [onClose])
|
|
138
155
|
|
|
139
|
-
|
|
156
|
+
return createElement(
|
|
157
|
+
'div',
|
|
158
|
+
{ className: 'dfa-fp-overlay', onClick: onClose },
|
|
159
|
+
createElement(
|
|
160
|
+
'div',
|
|
161
|
+
{
|
|
162
|
+
className: 'dfa-fp',
|
|
163
|
+
onClick: (event) => {
|
|
164
|
+
if (event && event.stopPropagation) event.stopPropagation()
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
createElement(
|
|
140
168
|
'div',
|
|
141
|
-
{ className: 'dfa-fp-
|
|
169
|
+
{ className: 'dfa-fp-head' },
|
|
170
|
+
createElement('span', { className: 'dfa-fp-title' }, title),
|
|
142
171
|
createElement(
|
|
143
|
-
'
|
|
144
|
-
{ className: 'dfa-fp'
|
|
172
|
+
'span',
|
|
173
|
+
{ className: 'dfa-fp-actions' },
|
|
145
174
|
createElement(
|
|
146
|
-
'
|
|
147
|
-
{
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
'
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
),
|
|
155
|
-
),
|
|
175
|
+
'button',
|
|
176
|
+
{
|
|
177
|
+
className: 'dfa-iconbtn',
|
|
178
|
+
title: strings.closePreview(),
|
|
179
|
+
'aria-label': strings.closePreview(),
|
|
180
|
+
onClick: () => onClose(),
|
|
181
|
+
},
|
|
182
|
+
icon.close(15),
|
|
156
183
|
),
|
|
157
|
-
createElement('div', { className: 'dfa-fp-body' }, renderPreviewBody(load, ctx, store, scope, path, title, sessionId)),
|
|
158
184
|
),
|
|
159
|
-
)
|
|
160
|
-
|
|
185
|
+
),
|
|
186
|
+
createElement(
|
|
187
|
+
'div',
|
|
188
|
+
{ className: 'dfa-fp-body' },
|
|
189
|
+
renderPreviewBody(load, ctx, store, scope, path, title, sessionId),
|
|
190
|
+
),
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
}
|
|
161
194
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Lightweight PDF preview. better-sidebar's built-in PdfView fetches
|
|
197
|
+
* `/sidebar/file` internally (it ignores any injected `mediaUrl` prop),
|
|
198
|
+
* and that route refuses files outside the session working directory — so
|
|
199
|
+
* a recorded /tmp PDF would never load. This tiny view embeds the bytes
|
|
200
|
+
* from OUR media route in a native browser PDF frame, with a download
|
|
201
|
+
* fallback in its toolbar.
|
|
202
|
+
*/
|
|
203
|
+
function PdfPreview({ src, download, title }) {
|
|
204
|
+
return createElement(
|
|
205
|
+
'div',
|
|
206
|
+
{ className: 'dfa-pdf' },
|
|
207
|
+
createElement(
|
|
208
|
+
'div',
|
|
209
|
+
{ className: 'dfa-pdf-toolbar' },
|
|
210
|
+
createElement(
|
|
211
|
+
'a',
|
|
212
|
+
{
|
|
213
|
+
className: 'dfa-pdf-download',
|
|
214
|
+
href: download,
|
|
215
|
+
download: true,
|
|
216
|
+
title: strings.downloadToView(),
|
|
217
|
+
},
|
|
218
|
+
strings.downloadToView(),
|
|
219
|
+
),
|
|
220
|
+
),
|
|
221
|
+
createElement('iframe', { className: 'dfa-pdf-frame', src, title }),
|
|
222
|
+
)
|
|
223
|
+
}
|