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
package/lib/parts/view.part.js
CHANGED
|
@@ -1,129 +1,183 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// ── view component ────────────────────────────────────────────────────
|
|
2
|
+
/** Shared empty bucket for sessions that have never loaded data (stable ref). */
|
|
3
|
+
const EMPTY_SESSION = { recent: [], counts: {}, loading: true }
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Polling loader for one session: fetches stats on mount and on a fixed
|
|
7
|
+
* interval while visible, prefers the sidebar's authoritative session.cwd
|
|
8
|
+
* for relative display, and writes results into the per-session bucket.
|
|
9
|
+
*/
|
|
10
|
+
function useSessionLoader(visible, sessionId, scope, dataStore, setCwd, setError) {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!visible || sessionId === '') return
|
|
13
|
+
let cancelled = false
|
|
14
|
+
const load = () => {
|
|
15
|
+
void fetchStats(sessionId)
|
|
16
|
+
.then((value) => {
|
|
17
|
+
if (cancelled || value === null) return
|
|
18
|
+
setCwd((prev) => prev || value.cwd || '')
|
|
19
|
+
const current = dataStore.getSnapshot()
|
|
20
|
+
dataStore.set({
|
|
21
|
+
bySession: {
|
|
22
|
+
...(current.bySession ?? {}),
|
|
23
|
+
[sessionId]: {
|
|
24
|
+
recent: value.recent ?? [],
|
|
25
|
+
counts: value.counts ?? {},
|
|
26
|
+
loading: false,
|
|
23
27
|
},
|
|
24
|
-
}
|
|
25
|
-
setError(false)
|
|
26
|
-
}).catch(() => {
|
|
27
|
-
if (!cancelled) setError(true)
|
|
28
|
+
},
|
|
28
29
|
})
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!cancelled
|
|
30
|
+
setError(false)
|
|
31
|
+
})
|
|
32
|
+
.catch(() => {
|
|
33
|
+
if (!cancelled) setError(true)
|
|
33
34
|
})
|
|
34
|
-
const timer = window.setInterval(load, POLL_MS)
|
|
35
|
-
return () => {
|
|
36
|
-
cancelled = true
|
|
37
|
-
window.clearInterval(timer)
|
|
38
|
-
}
|
|
39
|
-
}, [visible, sessionId, dataStore])
|
|
40
35
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
load()
|
|
37
|
+
void fetchSessionCwd(sessionId).then((cwd) => {
|
|
38
|
+
if (!cancelled && cwd !== '') setCwd(cwd)
|
|
39
|
+
})
|
|
40
|
+
const timer = window.setInterval(load, POLL_MS)
|
|
41
|
+
return () => {
|
|
42
|
+
cancelled = true
|
|
43
|
+
window.clearInterval(timer)
|
|
46
44
|
}
|
|
45
|
+
}, [visible, sessionId, dataStore])
|
|
46
|
+
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
/** Error banner element, or null when the last load succeeded. */
|
|
49
|
+
function renderError(error) {
|
|
50
|
+
if (!error) return null
|
|
51
|
+
return createElement(
|
|
52
|
+
'div',
|
|
53
|
+
{
|
|
54
|
+
style: {
|
|
55
|
+
color: 'var(--dsw-alias-state-error-primary)',
|
|
56
|
+
padding: '4px 6px',
|
|
57
|
+
font: 'var(--dsw-font-xxs-12)',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
strings.loadError(),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** "最近访问" section: collapsible head with refresh/clear actions. */
|
|
65
|
+
function renderRecentSection(recent, recentOpen, onToggle, onRefresh, onClear, onOpen) {
|
|
66
|
+
return createElement(
|
|
67
|
+
'div',
|
|
68
|
+
{ className: 'dfa-section' },
|
|
69
|
+
createElement(
|
|
70
|
+
'div',
|
|
71
|
+
{ className: 'dfa-section-head' },
|
|
72
|
+
createElement(
|
|
73
|
+
'button',
|
|
74
|
+
{ className: 'dfa-section-head-toggle', onClick: onToggle },
|
|
75
|
+
recentOpen ? icon.chevronDown(13) : icon.chevronRight(13),
|
|
76
|
+
strings.recent(),
|
|
77
|
+
),
|
|
78
|
+
createElement(
|
|
79
|
+
'span',
|
|
80
|
+
{ className: 'dfa-section-head-actions' },
|
|
53
81
|
createElement(
|
|
54
|
-
'
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
),
|
|
62
|
-
createElement('span', { className: 'dfa-section-head-actions' },
|
|
63
|
-
createElement('button', { className: 'dfa-iconbtn dfa-iconbtn-xs', onClick: onRefresh, title: strings.refresh(), 'aria-label': strings.refresh() }, icon.refresh(14)),
|
|
64
|
-
createElement('button', { className: 'dfa-iconbtn dfa-iconbtn-xs dfa-iconbtn-danger', onClick: onClear, title: strings.clear(), 'aria-label': strings.clear() }, icon.trash(14)),
|
|
65
|
-
),
|
|
82
|
+
'button',
|
|
83
|
+
{
|
|
84
|
+
className: 'dfa-iconbtn dfa-iconbtn-xs',
|
|
85
|
+
onClick: onRefresh,
|
|
86
|
+
title: strings.refresh(),
|
|
87
|
+
'aria-label': strings.refresh(),
|
|
88
|
+
},
|
|
89
|
+
icon.refresh(14),
|
|
66
90
|
),
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
91
|
+
createElement(
|
|
92
|
+
'button',
|
|
93
|
+
{
|
|
94
|
+
className: 'dfa-iconbtn dfa-iconbtn-xs dfa-iconbtn-danger',
|
|
95
|
+
onClick: onClear,
|
|
96
|
+
title: strings.clear(),
|
|
97
|
+
'aria-label': strings.clear(),
|
|
98
|
+
},
|
|
99
|
+
icon.trash(14),
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
!recentOpen
|
|
104
|
+
? null
|
|
105
|
+
: recent.length === 0
|
|
106
|
+
? createElement(
|
|
107
|
+
'div',
|
|
108
|
+
{ className: 'dfa-empty' },
|
|
109
|
+
strings.empty(),
|
|
110
|
+
createElement('span', { className: 'dfa-empty-hint' }, strings.emptyHint()),
|
|
111
|
+
)
|
|
112
|
+
: createElement(
|
|
113
|
+
'div',
|
|
114
|
+
{ className: 'dfa-list' },
|
|
115
|
+
recent.map((entry) => recentEntry(entry, onOpen)),
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
}
|
|
77
119
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
120
|
+
/** "文件统计" section: the directory tree, or an empty hint. */
|
|
121
|
+
function renderStatsSection(tree, collapsedDirs, onToggleDir, onOpen) {
|
|
122
|
+
return createElement(
|
|
123
|
+
'div',
|
|
124
|
+
{ className: 'dfa-section' },
|
|
125
|
+
createElement('div', { className: 'dfa-section-head' }, strings.stats()),
|
|
126
|
+
tree.children.length === 0
|
|
127
|
+
? createElement('div', { className: 'dfa-empty' }, strings.empty())
|
|
128
|
+
: createElement(
|
|
129
|
+
'div',
|
|
130
|
+
{ className: 'dfa-list' },
|
|
131
|
+
tree.children.map((child) => renderTreeNode(child, 0, collapsedDirs, onToggleDir, onOpen)),
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
89
135
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
136
|
+
/**
|
|
137
|
+
* The file-activity tab. Each session renders only its own store bucket:
|
|
138
|
+
* a fresh conversation shows an empty list immediately, with no residue
|
|
139
|
+
* from the previous session. Clicking any file opens a FLOATING preview
|
|
140
|
+
* that reuses the sidebar's NATIVE viewer via matchFileViewer.
|
|
141
|
+
*/
|
|
142
|
+
function FileActivityView({ ctx, store, scope, visible, dataStore }) {
|
|
143
|
+
const data = useSyncExternalStore(dataStore.subscribe, dataStore.getSnapshot)
|
|
144
|
+
const [cwd, setCwd] = useState(scope?.cwd || '')
|
|
145
|
+
const [error, setError] = useState(false)
|
|
146
|
+
const [recentOpen, setRecentOpen] = useState(true)
|
|
147
|
+
const [collapsedDirs, setCollapsedDirs] = useState(() => new Set())
|
|
148
|
+
const sessionId = scope?.sessionId ?? ''
|
|
149
|
+
const sessionData = (data.bySession ?? {})[sessionId] ?? EMPTY_SESSION
|
|
150
|
+
const tree = useMemo(() => buildTree(sessionData.counts ?? {}), [sessionData.counts])
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (scope?.cwd) setCwd(scope.cwd)
|
|
153
|
+
}, [scope?.cwd])
|
|
154
|
+
useSessionLoader(visible, sessionId, scope, dataStore, setCwd, setError)
|
|
155
|
+
// Switching conversations closes any floating preview left open by the
|
|
156
|
+
// previous session (preview is shared UI state; session data never
|
|
157
|
+
// crosses sessions anymore).
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
dataStore.set({ preview: null })
|
|
160
|
+
}, [sessionId, dataStore])
|
|
161
|
+
const toggleDir = (path) => setCollapsedDirs((prev) => toggleInSet(prev, path))
|
|
162
|
+
const openPreview = (path) => dataStore.set({ preview: { abs: path, name: basenameOf(path) } })
|
|
163
|
+
const closePreview = () => dataStore.set({ preview: null })
|
|
164
|
+
const onClear = () => clearSessionData(dataStore, sessionId)
|
|
165
|
+
const onRefresh = () => refreshSessionData(dataStore, sessionId, setCwd, setError)
|
|
166
|
+
const recent = sessionData.recent ?? []
|
|
167
|
+
return createElement(
|
|
168
|
+
'div',
|
|
169
|
+
{ className: 'dfa' },
|
|
170
|
+
renderError(error),
|
|
171
|
+
renderRecentSection(recent, recentOpen, () => setRecentOpen((v) => !v), onRefresh, onClear, openPreview),
|
|
172
|
+
renderStatsSection(tree, collapsedDirs, toggleDir, openPreview),
|
|
173
|
+
data.preview
|
|
174
|
+
? createElement(FloatingPreview, {
|
|
175
|
+
ctx,
|
|
176
|
+
store,
|
|
177
|
+
scope,
|
|
178
|
+
preview: data.preview,
|
|
179
|
+
onClose: closePreview,
|
|
180
|
+
})
|
|
181
|
+
: null,
|
|
182
|
+
)
|
|
183
|
+
}
|
package/lib/state.js
CHANGED
|
@@ -38,13 +38,17 @@ export async function loadState(file) {
|
|
|
38
38
|
/** Map a raw operation kind (tool name or client op) to 'read' | 'write' | 'edit' | 'delete'. */
|
|
39
39
|
export function mapOp(op) {
|
|
40
40
|
switch (op) {
|
|
41
|
-
case 'write':
|
|
41
|
+
case 'write':
|
|
42
|
+
return 'write'
|
|
42
43
|
case 'edit':
|
|
43
|
-
case 'str_replace_editor':
|
|
44
|
-
|
|
44
|
+
case 'str_replace_editor':
|
|
45
|
+
return 'edit'
|
|
46
|
+
case 'delete':
|
|
47
|
+
return 'delete'
|
|
45
48
|
case 'read':
|
|
46
49
|
case 'read_image':
|
|
47
|
-
default:
|
|
50
|
+
default:
|
|
51
|
+
return 'read'
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
|
|
@@ -91,8 +95,9 @@ function pushRecent(recent, path, op, time) {
|
|
|
91
95
|
|
|
92
96
|
/** A record target is valid when both ids are non-empty strings (no NUL). */
|
|
93
97
|
function isValidRecordTarget(sessionId, path) {
|
|
94
|
-
return
|
|
95
|
-
&& typeof path === 'string' && path !== '' && !path.includes('\0')
|
|
98
|
+
return (
|
|
99
|
+
typeof sessionId === 'string' && sessionId !== '' && typeof path === 'string' && path !== '' && !path.includes('\0')
|
|
100
|
+
)
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
/** 'write' → create/modify by the known-file registry; 'edit' → modify; else read. */
|
|
@@ -119,7 +124,8 @@ function bumpCount(counts, finalOp, firstSeen, timestamp) {
|
|
|
119
124
|
* Returns { state, trimmed } where trimmed reports whether anything changed.
|
|
120
125
|
*/
|
|
121
126
|
export function trimLoadedState(loaded) {
|
|
122
|
-
if (loaded.sessions === undefined || loaded.sessions === null || typeof loaded.sessions !== 'object')
|
|
127
|
+
if (loaded.sessions === undefined || loaded.sessions === null || typeof loaded.sessions !== 'object')
|
|
128
|
+
loaded.sessions = {}
|
|
123
129
|
let trimmed = false
|
|
124
130
|
for (const session of Object.values(loaded.sessions)) {
|
|
125
131
|
if (Array.isArray(session.recent)) {
|
|
@@ -150,7 +156,8 @@ export function trimLoadedState(loaded) {
|
|
|
150
156
|
export function isRecordedPath(state, sessionId, path) {
|
|
151
157
|
const session = state.sessions[sessionId]
|
|
152
158
|
if (session === undefined) return false
|
|
153
|
-
if (session.counts !== undefined && typeof session.counts[path] === 'object' && session.counts[path] !== null)
|
|
159
|
+
if (session.counts !== undefined && typeof session.counts[path] === 'object' && session.counts[path] !== null)
|
|
160
|
+
return true
|
|
154
161
|
// Deleted files no longer exist on disk — a delete history entry must not
|
|
155
162
|
// authorize media preview for them.
|
|
156
163
|
if (Array.isArray(session.recent)) return session.recent.some((entry) => entry.path === path && entry.op !== 'delete')
|
package/lib/store.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (tmp+rename) and a teardown flush. The exposed store object carries a live
|
|
5
5
|
* `state` reference so route handlers always read the current document.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { atomicWriteJson } from 'dsh-shared'
|
|
8
8
|
import { applyRecord, createState, loadState, mapOp, stateFile, trimLoadedState } from './state.js'
|
|
9
9
|
|
|
10
10
|
/** Build the per-apply store: { state, record, schedulePersist, dispose }. */
|
|
@@ -30,16 +30,9 @@ export function createStore(ctx) {
|
|
|
30
30
|
|
|
31
31
|
/** Write the current state atomically (serialized through dirtyChain). */
|
|
32
32
|
function persistNow(handle) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
await writeFile(tmp, snapshot, 'utf8')
|
|
38
|
-
await rename(tmp, handle.file)
|
|
39
|
-
} catch (error) {
|
|
40
|
-
handle.ctx.logger?.warn(`[dsh-file-activity] persist failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
41
|
-
}
|
|
42
|
-
}).catch(() => {})
|
|
33
|
+
handle.dirtyChain = handle.dirtyChain
|
|
34
|
+
.then(() => atomicWriteJson(handle.file, handle.store.state, handle.ctx.logger, '[dsh-file-activity]'))
|
|
35
|
+
.catch(() => {})
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
/** Debounced (500ms) schedule of a persist. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-file-activity",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "DSH 侧边栏文件活动插件:按 LRU 记录文件读取/新增/修改事件(最近访问),按绝对路径树形统计(文件统计),点击文件浮窗预览(代码高亮/Markdown/图片/PDF)。DSH web plugin: file activity tracker — LRU recent-access list plus per-file read/create/modify counts in a folder tree, with floating preview.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"cordis": "^4.0.0-rc.8",
|
|
48
48
|
"dsh-better-sidebar": "^0.14.0",
|
|
49
|
-
"react": "^18.2.0"
|
|
49
|
+
"react": "^18.2.0 || ^19.2.0",
|
|
50
|
+
"dsh-shared": "^0.1.0"
|
|
50
51
|
},
|
|
51
52
|
"peerDependenciesMeta": {
|
|
52
53
|
"dsh-better-sidebar": {
|
package/lib/fence.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Trust fence for /file-activity routes: only loopback / explicitly trusted
|
|
3
|
-
* hosts may call the plugin's HTTP endpoints (same behavioral contract as the
|
|
4
|
-
* /api gateway). 403-refuses everything else, including cross-site requests.
|
|
5
|
-
*/
|
|
6
|
-
function header(headers, name) {
|
|
7
|
-
const value = headers[name]
|
|
8
|
-
return typeof value === 'string' ? value : undefined
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function parseAuthority(authority) {
|
|
12
|
-
try {
|
|
13
|
-
return new URL(`http://${authority}`)
|
|
14
|
-
} catch {
|
|
15
|
-
return undefined
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function isLoopbackHostname(hostname) {
|
|
20
|
-
if (hostname === 'localhost' || hostname === '[::1]') return true
|
|
21
|
-
const parts = hostname.split('.')
|
|
22
|
-
return parts.length === 4
|
|
23
|
-
&& parts[0] === '127'
|
|
24
|
-
&& parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function canonicalAuthority(entry, entryUrl) {
|
|
28
|
-
const port = entryUrl.port !== '' ? entryUrl.port : new URL(`https://${entry}`).port
|
|
29
|
-
return port === '' ? entryUrl.hostname : `${entryUrl.hostname}:${port}`
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isTrustedAuthority(hostUrl, trustedHosts) {
|
|
33
|
-
return trustedHosts.some((entry) => {
|
|
34
|
-
const entryUrl = parseAuthority(entry)
|
|
35
|
-
if (entryUrl === undefined) return false
|
|
36
|
-
return canonicalAuthority(entry, entryUrl) === entryUrl.hostname
|
|
37
|
-
? entryUrl.hostname === hostUrl.hostname
|
|
38
|
-
: entryUrl.host === hostUrl.host
|
|
39
|
-
})
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Host-header trust fence (same behavioral contract as the /api gateway). */
|
|
43
|
-
export function isTrustedApiRequest(request, trustedHosts) {
|
|
44
|
-
const host = header(request.headers, 'host')
|
|
45
|
-
if (host === undefined) return false
|
|
46
|
-
const hostUrl = parseAuthority(host)
|
|
47
|
-
if (hostUrl === undefined) return false
|
|
48
|
-
if (!isLoopbackHostname(hostUrl.hostname) && !isTrustedAuthority(hostUrl, trustedHosts)) return false
|
|
49
|
-
if (header(request.headers, 'sec-fetch-site') === 'cross-site') return false
|
|
50
|
-
const origin = header(request.headers, 'origin')
|
|
51
|
-
if (origin === undefined) return true
|
|
52
|
-
try {
|
|
53
|
-
return new URL(origin).host === hostUrl.host
|
|
54
|
-
} catch {
|
|
55
|
-
return false
|
|
56
|
-
}
|
|
57
|
-
}
|
package/lib/http.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP helpers shared by the /file-activity routes: bounded JSON body
|
|
3
|
-
* reading, JSON responses, and session working-directory resolution.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/** Read a JSON request body (bounded). */
|
|
7
|
-
export async function readJsonBody(request) {
|
|
8
|
-
let body = ''
|
|
9
|
-
for await (const chunk of request) {
|
|
10
|
-
body += chunk
|
|
11
|
-
if (body.length > 1_000_000) throw new Error('request body too large')
|
|
12
|
-
}
|
|
13
|
-
if (body === '') return {}
|
|
14
|
-
return JSON.parse(body)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function writeJson(response, status, value) {
|
|
18
|
-
const payload = JSON.stringify(value)
|
|
19
|
-
response.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-cache' })
|
|
20
|
-
response.end(payload)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function writeError(response, error) {
|
|
24
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
25
|
-
writeJson(response, 400, { ok: false, error: { message } })
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Session working directory, mirroring better-sidebar's resolution. */
|
|
29
|
-
export function sessionCwdOf(ctx, sessionId) {
|
|
30
|
-
const session = ctx.sessions.get(sessionId)
|
|
31
|
-
const headerCwd = session?.header?.cwd
|
|
32
|
-
if (typeof headerCwd === 'string' && headerCwd !== '') return headerCwd
|
|
33
|
-
return process.cwd()
|
|
34
|
-
}
|
package/lib/parts/icons.part.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// ── icons (inline, stroke=currentColor, matching better-sidebar) ──────
|
|
2
|
-
const ICON_STROKE = 1.8
|
|
3
|
-
const iconSvg = (children, size) =>
|
|
4
|
-
createElement('svg', {
|
|
5
|
-
width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
|
|
6
|
-
stroke: 'currentColor', strokeWidth: ICON_STROKE, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
7
|
-
'aria-hidden': 'true',
|
|
8
|
-
}, children.map((child, i) => (child === null || child === undefined || typeof child === 'boolean')
|
|
9
|
-
? child
|
|
10
|
-
: createElement(child.type, { key: i, ...child.props })))
|
|
11
|
-
|
|
12
|
-
const icon = {
|
|
13
|
-
clock: (size = 16) => iconSvg([
|
|
14
|
-
createElement('circle', { cx: 12, cy: 12, r: 9 }),
|
|
15
|
-
createElement('path', { d: 'M12 7v5l3 2' }),
|
|
16
|
-
], size),
|
|
17
|
-
refresh: (size = 16) => iconSvg([
|
|
18
|
-
createElement('path', { d: 'M21 12a9 9 0 1 1-2.64-6.36' }),
|
|
19
|
-
createElement('polyline', { points: '21 3 21 9 15 9' }),
|
|
20
|
-
], size),
|
|
21
|
-
trash: (size = 16) => iconSvg([
|
|
22
|
-
createElement('path', { d: 'M3 6h18' }),
|
|
23
|
-
createElement('path', { d: 'M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6' }),
|
|
24
|
-
createElement('path', { d: 'M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2' }),
|
|
25
|
-
], size),
|
|
26
|
-
chevronRight: (size = 14) => iconSvg([
|
|
27
|
-
createElement('polyline', { points: '9 6 15 12 9 18' }),
|
|
28
|
-
], size),
|
|
29
|
-
chevronDown: (size = 14) => iconSvg([
|
|
30
|
-
createElement('polyline', { points: '6 9 12 15 18 9' }),
|
|
31
|
-
], size),
|
|
32
|
-
file: (size = 16) => iconSvg([
|
|
33
|
-
createElement('path', { d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }),
|
|
34
|
-
createElement('path', { d: 'M14 2v6h6' }),
|
|
35
|
-
], size),
|
|
36
|
-
folder: (size = 16) => iconSvg([
|
|
37
|
-
createElement('path', { d: 'M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z' }),
|
|
38
|
-
], size),
|
|
39
|
-
external: (size = 15) => iconSvg([
|
|
40
|
-
createElement('path', { d: 'M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' }),
|
|
41
|
-
createElement('polyline', { points: '15 3 21 3 21 9' }),
|
|
42
|
-
createElement('line', { x1: 10, y1: 14, x2: 21, y2: 3 }),
|
|
43
|
-
], size),
|
|
44
|
-
close: (size = 15) => iconSvg([
|
|
45
|
-
createElement('line', { x1: 18, y1: 6, x2: 6, y2: 18 }),
|
|
46
|
-
createElement('line', { x1: 6, y1: 6, x2: 18, y2: 18 }),
|
|
47
|
-
], size),
|
|
48
|
-
}
|