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.
@@ -1,56 +1,56 @@
1
- // ── fetch interception: sidebar file operations ───────────────────────
2
- function methodOf(init) {
3
- return (init?.method ?? 'GET').toUpperCase()
4
- }
1
+ // ── fetch interception: sidebar file operations ───────────────────────
2
+ function methodOf(init) {
3
+ return (init?.method ?? 'GET').toUpperCase()
4
+ }
5
5
 
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
- }
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
- /** 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
- }
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
- /** 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
- }
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
- /** 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
- }
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
- 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
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
- // ── 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
- }
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
- /** 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
- }
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
- /** 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
- }
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
- * 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
- }
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
- * 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 }
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
- * 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 () => { 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
- * A floating preview window. Instead of re-implementing rendering, it
109
- * asks the sidebar registry for the file's viewer (`matchFileViewer`),
110
- * fetches the bytes the viewer's fetchStrategy needs (fsRead text /
111
- * mediaUrl / customData), then mounts that viewer's own component — so
112
- * code gets syntax highlighting and markdown gets rendered by the SAME
113
- * built-in renderers the sidebar's editor tab uses.
114
- *
115
- * Media caveat: the sidebar's own media route (/sidebar/file) only serves
116
- * files inside the session working directory, while file activity records
117
- * files the agent touched anywhere (/tmp scratch files, sibling repos…).
118
- * Media bytes therefore come from OUR route (/file-activity/file), which
119
- * authorizes exactly the paths this session recorded; PDF is the one
120
- * built-in viewer that fetches its own URL internally (it ignores the
121
- * `mediaUrl` prop), so it gets a small iframe preview instead.
122
- */
123
- function FloatingPreview({ ctx, store, scope, preview, onClose }) {
124
- const sessionId = scope?.sessionId ?? ''
125
- const path = preview.abs
126
- const title = preview.name
127
- const service = ctx.betterSidebar
128
- const load = usePreviewLoader(service, path, sessionId, scope)
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
- // Clicking outside is the primary dismiss (the overlay's onClick);
131
- // Escape is a keyboard affordance. Both call onClose.
132
- useEffect(() => {
133
- if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return () => {}
134
- const handler = (event) => { if (event && event.key === 'Escape') onClose() }
135
- document.addEventListener('keydown', handler)
136
- return () => document.removeEventListener('keydown', handler)
137
- }, [onClose])
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
- return createElement(
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-overlay', onClick: onClose },
169
+ { className: 'dfa-fp-head' },
170
+ createElement('span', { className: 'dfa-fp-title' }, title),
142
171
  createElement(
143
- 'div',
144
- { className: 'dfa-fp', onClick: (event) => { if (event && event.stopPropagation) event.stopPropagation() } },
172
+ 'span',
173
+ { className: 'dfa-fp-actions' },
145
174
  createElement(
146
- 'div',
147
- { className: 'dfa-fp-head' },
148
- createElement('span', { className: 'dfa-fp-title' }, title),
149
- createElement(
150
- 'span',
151
- { className: 'dfa-fp-actions' },
152
- createElement('button', { className: 'dfa-iconbtn', title: strings.closePreview(), 'aria-label': strings.closePreview(), onClick: () => onClose() },
153
- icon.close(15),
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
- * Lightweight PDF preview. better-sidebar's built-in PdfView fetches
164
- * `/sidebar/file` internally (it ignores any injected `mediaUrl` prop),
165
- * and that route refuses files outside the session working directory — so
166
- * a recorded /tmp PDF would never load. This tiny view embeds the bytes
167
- * from OUR media route in a native browser PDF frame, with a download
168
- * fallback in its toolbar.
169
- */
170
- function PdfPreview({ src, download, title }) {
171
- return createElement('div', { className: 'dfa-pdf' },
172
- createElement('div', { className: 'dfa-pdf-toolbar' },
173
- createElement('a', { className: 'dfa-pdf-download', href: download, download: true, title: strings.downloadToView() },
174
- strings.downloadToView()),
175
- ),
176
- createElement('iframe', { className: 'dfa-pdf-frame', src, title }),
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
+ }