dsh-file-activity 0.5.7 → 0.5.9
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 +50 -0
- package/README.md +29 -18
- package/lib/api-route.js +72 -76
- package/lib/bash-ops.js +65 -54
- package/lib/bash-parse.js +154 -162
- package/lib/client.js +739 -361
- package/lib/client.src.js +28 -21
- package/lib/cwd.js +6 -6
- package/lib/index.js +68 -83
- package/lib/media-route.js +119 -90
- package/lib/observer.js +30 -24
- package/lib/parts/api.part.js +1 -5
- package/lib/parts/apply.part.js +91 -82
- package/lib/parts/auto-open.part.js +83 -60
- package/lib/parts/document-previews.part.js +62 -0
- package/lib/parts/format.part.js +1 -2
- package/lib/parts/i18n.part.js +15 -1
- package/lib/parts/interceptor.part.js +16 -27
- package/lib/parts/preview-data.part.js +111 -0
- package/lib/parts/preview-float.part.js +276 -0
- package/lib/parts/rows.part.js +1 -11
- package/lib/parts/settings.part.js +71 -0
- package/lib/parts/store.part.js +19 -0
- package/lib/parts/styles.part.js +49 -3
- package/lib/parts/tree.part.js +1 -2
- package/lib/parts/view.part.js +72 -18
- package/lib/persist.js +137 -0
- package/lib/quota-trim.js +121 -0
- package/lib/quota.js +164 -0
- package/lib/state.js +336 -136
- package/lib/store.js +249 -76
- package/package.json +10 -6
- package/lib/parts/preview.part.js +0 -308
package/lib/parts/api.part.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
'use strict'
|
|
1
2
|
// ── data access (host routes) ─────────────────────────────────────────
|
|
2
3
|
async function fetchStats(sessionId) {
|
|
3
4
|
const response = await fetch(`/file-activity/api/stats?sessionId=${encodeURIComponent(sessionId)}`)
|
|
@@ -5,7 +6,6 @@ async function fetchStats(sessionId) {
|
|
|
5
6
|
if (json === null || typeof json !== 'object' || json.ok !== true) return null
|
|
6
7
|
return json.value
|
|
7
8
|
}
|
|
8
|
-
|
|
9
9
|
/** Resolve the session working directory through the sidebar's native API. */
|
|
10
10
|
async function fetchSessionCwd(sessionId) {
|
|
11
11
|
try {
|
|
@@ -21,7 +21,6 @@ async function fetchSessionCwd(sessionId) {
|
|
|
21
21
|
return ''
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
|
|
25
24
|
function postRecord(sessionId, path, op) {
|
|
26
25
|
if (typeof sessionId !== 'string' || sessionId === '' || typeof path !== 'string' || path === '') return
|
|
27
26
|
void fetch('/file-activity/api/record', {
|
|
@@ -30,7 +29,6 @@ function postRecord(sessionId, path, op) {
|
|
|
30
29
|
body: JSON.stringify({ sessionId, path, op }),
|
|
31
30
|
}).catch(() => {})
|
|
32
31
|
}
|
|
33
|
-
|
|
34
32
|
function postClear(sessionId) {
|
|
35
33
|
void fetch('/file-activity/api/clear', {
|
|
36
34
|
method: 'POST',
|
|
@@ -38,12 +36,10 @@ function postClear(sessionId) {
|
|
|
38
36
|
body: JSON.stringify({ sessionId }),
|
|
39
37
|
}).catch(() => {})
|
|
40
38
|
}
|
|
41
|
-
|
|
42
39
|
/** Plugin media route URL for a recorded path (authorized per session). */
|
|
43
40
|
function mediaUrlOf(sessionId, path) {
|
|
44
41
|
return `/file-activity/file?${new URLSearchParams({ sessionId, path })}`
|
|
45
42
|
}
|
|
46
|
-
|
|
47
43
|
/** Plugin text route URL (`as=text`): fs.read-shaped JSON for recorded text. */
|
|
48
44
|
function textUrlOf(sessionId, path) {
|
|
49
45
|
return `/file-activity/file?${new URLSearchParams({ sessionId, path, as: 'text' })}`
|
package/lib/parts/apply.part.js
CHANGED
|
@@ -1,99 +1,105 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return () => {
|
|
19
|
-
if (style.parentNode) style.parentNode.removeChild(style)
|
|
20
|
-
}
|
|
21
|
-
}, 'dsh-file-activity: styles')
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Mount probe: report client activation to the host state (synthetic
|
|
25
|
-
* session id, invisible in the UI — confirms the client half actually
|
|
26
|
-
* loaded after a page refresh). */
|
|
27
|
-
function mountProbe() {
|
|
28
|
-
void fetch('/file-activity/api/record', {
|
|
29
|
-
method: 'POST',
|
|
30
|
-
headers: { 'content-type': 'application/json' },
|
|
31
|
-
body: JSON.stringify({ sessionId: '__probe__', path: 'mounted', op: 'read' }),
|
|
32
|
-
}).catch(() => {})
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Register the tab (enabled by default in the Side card settings). */
|
|
36
|
-
function registerTab(ctx, dataStore) {
|
|
37
|
-
const service = ctx.betterSidebar
|
|
1
|
+
'use strict'
|
|
2
|
+
// ── plugin body (native sidebar extension points) ─────────────────────
|
|
3
|
+
//
|
|
4
|
+
// Everything this half needs comes from Cordis services the HOST provides
|
|
5
|
+
// (issue #187 batch 2): the tab registry, the slot registry, the document
|
|
6
|
+
// preview registry and the right-Sidebar navigation controller. There is no
|
|
7
|
+
// third-party sidebar package in the picture anymore, so the page keeps
|
|
8
|
+
// working on any host that ships those four services.
|
|
9
|
+
//
|
|
10
|
+
// The stylesheet is pure static CSS and must NOT depend on any of them:
|
|
11
|
+
// inject it first, unconditionally. If it lived behind an early return, an HMR
|
|
12
|
+
// rebuild or service reload could leave the already-rendered tab WITHOUT its
|
|
13
|
+
// stylesheet — the raw white-text list you see when the CSS is gone. Each
|
|
14
|
+
// fiber owns its own <style> element and the disposer removes only that
|
|
15
|
+
// element, so a rebuild always keeps at least one copy.
|
|
16
|
+
/** registerTab step one: the tab TYPE (what a file-activity page is). */
|
|
17
|
+
function registerTabType(ctx) {
|
|
38
18
|
ctx.effect(
|
|
39
19
|
() =>
|
|
40
|
-
|
|
20
|
+
ctx.sidebarRightTabs.register({
|
|
41
21
|
id: TAB_ID,
|
|
22
|
+
kind: TAB_KIND,
|
|
23
|
+
// No patterns: this is a page type, opened by kind (ctx.sidebarRight.openTab).
|
|
42
24
|
title: () => strings.title(),
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
},
|
|
59
|
-
component: (props) => createElement(FileActivityView, { ...props, dataStore }),
|
|
25
|
+
// Guide entry (issue #266 B): the sidebar's "new tab" page is the guide
|
|
26
|
+
// tab, and its doors come from `sidebarRightTabs.guide()` — the entries
|
|
27
|
+
// every registered type contributes. Without one the page was reachable
|
|
28
|
+
// ONLY through auto-open, so closing its chip made it impossible to
|
|
29
|
+
// reopen from the UI. Picking the capsule opens this kind in the guide's
|
|
30
|
+
// place (host GuideBody → actions.openTab(kind, { replaceTab: true })).
|
|
31
|
+
guide: [
|
|
32
|
+
{
|
|
33
|
+
order: 20,
|
|
34
|
+
title: () => strings.title(),
|
|
35
|
+
description: () => strings.guideDescription(),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
60
38
|
}),
|
|
61
|
-
'dsh-file-activity: tab
|
|
39
|
+
'dsh-file-activity: tab type',
|
|
62
40
|
)
|
|
63
41
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
42
|
+
/** registerTab step two: the body seat (keyed by the definition id). */
|
|
43
|
+
function registerTabBody(ctx, dataStore) {
|
|
44
|
+
ctx.effect(
|
|
45
|
+
() =>
|
|
46
|
+
ctx.slots.inject('sidebar.right.pane.tab', () =>
|
|
47
|
+
ctx.slots.register({ name: 'sidebar.right.pane.tab', key: TAB_ID }, function (props) {
|
|
48
|
+
const injected = Object.assign({}, props, { dataStore })
|
|
49
|
+
return createElement(FileActivityView, injected)
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
'dsh-file-activity: tab body',
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
/** registerTab step three: the chip title seat (a live, localized label). */
|
|
56
|
+
function registerTabTitle(ctx) {
|
|
57
|
+
ctx.effect(
|
|
58
|
+
() =>
|
|
59
|
+
ctx.slots.inject('sidebar.right.pane.tab.title', () =>
|
|
60
|
+
ctx.slots.register({ name: 'sidebar.right.pane.tab.title', key: TAB_ID }, () =>
|
|
61
|
+
createElement('span', { className: 'dfa-chip' }, strings.title()),
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
'dsh-file-activity: tab title',
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Run one registration, converting a throw into an observable failure.
|
|
69
|
+
*
|
|
70
|
+
* Swallowing is deliberate: the host throws for a taken id or a taken kind, and
|
|
71
|
+
* letting it escape would fail the WHOLE activation (the stylesheet, the
|
|
72
|
+
* floating preview and the settings tab would go with it). What must never
|
|
73
|
+
* happen is a SILENT failure, so the cause is logged, marked on <html> and
|
|
74
|
+
* rendered as a notice inside the tab body.
|
|
75
|
+
*/
|
|
76
|
+
function guarded(kind, register) {
|
|
77
|
+
try {
|
|
78
|
+
register()
|
|
79
|
+
} catch (error) {
|
|
80
|
+
markDegraded(kind, error)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.inject = ['slots', 'sidebarRightTabs', 'documentPreviews', 'sidebarRight']
|
|
67
84
|
exports.apply = function apply(ctx) {
|
|
68
|
-
// Stylesheet first, unconditionally (HMR pitfall — see
|
|
85
|
+
// Stylesheet first, unconditionally (HMR pitfall — see the header note).
|
|
69
86
|
injectStyles(ctx)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// 以为插件坏了——明确提示安装方式。
|
|
75
|
-
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
76
|
-
console.warn(
|
|
77
|
-
'[dsh-file-activity] dsh-better-sidebar 未安装:文件活动页签无法挂载。请安装宿主插件:dsh plugin --profile web add dsh-better-sidebar dsh-file-activity',
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Per-session data store: { bySession: { [sessionId]: { recent, counts, loading } }, preview }
|
|
84
|
-
// Each conversation reads/writes only its own bucket, so switching
|
|
85
|
-
// sessions never leaks another session's file activity into the view.
|
|
87
|
+
// Failing loudly beats failing silently: a rejected registration (id taken,
|
|
88
|
+
// kind taken, seat missing) is recorded so the user and the e2e suite can
|
|
89
|
+
// see WHY the tab never appeared.
|
|
90
|
+
guarded('tab', () => registerTabType(ctx))
|
|
86
91
|
const dataStore = createStore({ bySession: {}, preview: null })
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
registerSharedStore(dataStore)
|
|
93
|
+
registerTabBody(ctx, dataStore)
|
|
94
|
+
registerTabTitle(ctx)
|
|
95
|
+
registerPreviewOverlay(ctx, dataStore)
|
|
96
|
+
registerSettingsTab(ctx)
|
|
97
|
+
guarded('previews', () => registerDocumentPreviews(ctx))
|
|
89
98
|
// sidebar operations → host record route
|
|
90
99
|
ctx.effect(() => installFetchInterceptor(), 'dsh-file-activity: sidebar fetch observation')
|
|
91
|
-
registerTab(ctx, dataStore)
|
|
92
|
-
|
|
93
100
|
// auto-open once per session (default on)
|
|
94
|
-
ctx.effect(() => installAutoOpen(ctx
|
|
101
|
+
ctx.effect(() => installAutoOpen(ctx), 'dsh-file-activity: auto-open')
|
|
95
102
|
}
|
|
96
|
-
|
|
97
103
|
// Internal functions exposed for the render-path test suite only; inert in
|
|
98
104
|
// the browser bundle (plain properties on the exports object).
|
|
99
105
|
exports.__test = {
|
|
@@ -102,10 +108,13 @@ exports.__test = {
|
|
|
102
108
|
fetchTextContent,
|
|
103
109
|
textUrlOf,
|
|
104
110
|
strings,
|
|
111
|
+
viewerOf,
|
|
105
112
|
renderPreviewBody,
|
|
106
113
|
previewClickAction,
|
|
107
114
|
isInsideFloating,
|
|
108
115
|
closePreviewOnHidden,
|
|
116
|
+
autoOpenEnabled,
|
|
117
|
+
PREVIEW_EXTENSIONS,
|
|
109
118
|
AUTO_CLOSE_MS,
|
|
110
119
|
// Static stylesheet text, so the render-path suite can assert the floating
|
|
111
120
|
// preview body keeps its flex-fill container (issue #111).
|
|
@@ -1,76 +1,99 @@
|
|
|
1
|
+
'use strict'
|
|
1
2
|
// ── auto-open (enabled by default) ────────────────────────────────────
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
3
|
+
//
|
|
4
|
+
// Native flow: the tab is a PAGE type, so opening it means
|
|
5
|
+
// `ctx.sidebarRight.openTab(kind)` — there is no layout snapshot to inspect
|
|
6
|
+
// and no session id to read from a third-party sidebar service anymore.
|
|
7
|
+
//
|
|
8
|
+
// Timing is the whole problem. The navigation controller throws
|
|
9
|
+
// "sidebarRight: no session surface is mounted" when the right column has not
|
|
10
|
+
// mounted its session surface yet — which is exactly the state during the
|
|
11
|
+
// first paint of a fresh page. The previous third-party sidebar used to answer with a snapshot
|
|
12
|
+
// and the old code simply gave up when it was missing, which is how the tab
|
|
13
|
+
// could silently never auto-open. Here the attempt is RETRIED with the timers
|
|
14
|
+
// until the surface is up, and a permanent failure is recorded through
|
|
15
|
+
// markDegraded() instead of disappearing.
|
|
16
|
+
/** Retry cadence while the right column has not mounted its session surface. */
|
|
17
|
+
const AUTO_OPEN_RETRY_MS = 1000
|
|
18
|
+
/** Give up after this many attempts (~30s) and degrade observably. */
|
|
19
|
+
const AUTO_OPEN_MAX_TRIES = 30
|
|
20
|
+
/**
|
|
21
|
+
* Pending retry timer id (0 = none).
|
|
22
|
+
*
|
|
23
|
+
* There is deliberately NO "already attempted" flag guarding this module: a
|
|
24
|
+
* rejected registration or an HMR rebuild re-runs apply(), and the second run
|
|
25
|
+
* must still be able to bring the tab back. `openTab` is idempotent — it
|
|
26
|
+
* focuses the existing page instead of duplicating it.
|
|
27
|
+
*/
|
|
28
|
+
let autoOpenTimer = 0
|
|
29
|
+
/**
|
|
30
|
+
* Whether this BROWSER TAB already had its auto-open attempt.
|
|
31
|
+
*
|
|
32
|
+
* The native API exposes no "current session id" to a plugin (`openTab`
|
|
33
|
+
* targets the mounted session, and the layout store is the host's own), so a
|
|
34
|
+
* per-session marker cannot be written from here. The marker is therefore
|
|
35
|
+
* per-browser-tab: one attempt per page load, which is exactly the visible
|
|
36
|
+
* behaviour — the session's tab record itself is persisted host-side, so
|
|
37
|
+
* revisiting a session that already shows the tab merely focuses it.
|
|
38
|
+
*/
|
|
39
|
+
function isAutoOpenAttempted() {
|
|
40
|
+
const key = AUTO_OPEN_KEY + 'loaded'
|
|
15
41
|
try {
|
|
16
|
-
return
|
|
42
|
+
return window.sessionStorage.getItem(key) === '1'
|
|
17
43
|
} catch {
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** The user disabled auto-open for this tab in the sidebar settings. */
|
|
23
|
-
function isAutoOpenDisabled(snapshot, tabId) {
|
|
24
|
-
const settings = snapshot.prefs?.pluginSettings?.[tabId]
|
|
25
|
-
return settings !== undefined && settings.autoOpen === false
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Whether this session was already auto-opened (localStorage marker). */
|
|
29
|
-
function isAutoOpenMarked(sessionId) {
|
|
30
|
-
try {
|
|
31
|
-
return Boolean(window.localStorage.getItem(AUTO_OPEN_KEY + sessionId))
|
|
32
|
-
} catch {
|
|
33
|
-
return true
|
|
44
|
+
return false
|
|
34
45
|
}
|
|
35
46
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
function markAutoOpened(sessionId) {
|
|
47
|
+
/** Mark this browser tab as already attempted. */
|
|
48
|
+
function markAutoOpenAttempted() {
|
|
39
49
|
try {
|
|
40
|
-
window.
|
|
50
|
+
window.sessionStorage.setItem(AUTO_OPEN_KEY + 'loaded', '1')
|
|
41
51
|
} catch {
|
|
42
52
|
// ignore
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (snapshot === undefined || snapshot === null || snapshot.sessionId === undefined || snapshot.state === undefined)
|
|
50
|
-
return
|
|
51
|
-
const sessionId = snapshot.sessionId
|
|
52
|
-
if (isAutoOpenDisabled(snapshot, tabId)) return
|
|
53
|
-
if (isAutoOpenMarked(sessionId)) return
|
|
54
|
-
if (findTabIn(snapshot.state, tabId)) {
|
|
55
|
-
markAutoOpened(sessionId)
|
|
56
|
-
return
|
|
57
|
-
}
|
|
55
|
+
/** The most recent controller refusal, surfaced when retries run out. */
|
|
56
|
+
let lastAutoOpenError = null
|
|
57
|
+
/** One open attempt: true when the controller accepted it. */
|
|
58
|
+
function tryOpenTab(ctx, tries) {
|
|
58
59
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
ctx.sidebarRight.openTab(TAB_KIND, { revealIfOpened: false })
|
|
61
|
+
return true
|
|
61
62
|
} catch (error) {
|
|
62
|
-
|
|
63
|
+
if (tries === 0) lastAutoOpenError = error
|
|
64
|
+
return false
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Open the tab once the session surface exists, then stop. The tab record
|
|
69
|
+
* itself is persisted host-side, so a session that already shows it is not
|
|
70
|
+
* reopened: `openTab` focuses the existing page instead of duplicating it.
|
|
71
|
+
* @param ctx - client context carrying the navigation controller.
|
|
72
|
+
* @returns disposer cancelling a pending retry.
|
|
73
|
+
*/
|
|
74
|
+
function installAutoOpen(ctx) {
|
|
75
|
+
if (!autoOpenEnabled()) return () => {}
|
|
76
|
+
if (isAutoOpenAttempted()) return () => {}
|
|
77
|
+
let tries = 0
|
|
78
|
+
const attempt = () => {
|
|
79
|
+
tries += 1
|
|
80
|
+
if (tryOpenTab(ctx, tries - 1)) {
|
|
81
|
+
autoOpenTimer = 0
|
|
82
|
+
markAutoOpenAttempted()
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
if (tries >= AUTO_OPEN_MAX_TRIES) {
|
|
86
|
+
autoOpenTimer = 0
|
|
87
|
+
markDegraded('auto-open', lastAutoOpenError ?? 'sidebarRight: session surface never mounted')
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
autoOpenTimer = window.setTimeout(attempt, AUTO_OPEN_RETRY_MS)
|
|
91
|
+
}
|
|
92
|
+
attempt()
|
|
93
|
+
return () => {
|
|
94
|
+
if (autoOpenTimer !== 0) {
|
|
95
|
+
window.clearTimeout(autoOpenTimer)
|
|
96
|
+
autoOpenTimer = 0
|
|
97
|
+
}
|
|
74
98
|
}
|
|
75
|
-
return off
|
|
76
99
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// ── native document previews (metadata + plain-text renderer) ──────────
|
|
3
|
+
//
|
|
4
|
+
// The HOST owns file content on the native side: a registered preview only
|
|
5
|
+
// declares WHICH suffixes this implementation recognizes and HOW the document
|
|
6
|
+
// owner should deliver the bytes ('text-pages' = paged text window,
|
|
7
|
+
// 'bytes-complete' = whole file). The owner then injects the prepared
|
|
8
|
+
// DocumentContent into the 'sidebar.right.tab.document' seat, keyed by this
|
|
9
|
+
// implementation's id — so the renderer never fetches anything itself
|
|
10
|
+
// (the responsibility split that made 第三方查看器注册表的 matchFileViewer
|
|
11
|
+
// obsolete; its fetchStrategy belonged to the third-party viewer).
|
|
12
|
+
//
|
|
13
|
+
// Scope of the registration: filenames whose suffixes no shipped renderer
|
|
14
|
+
// claims. Anything the host already renders (code / markdown / image / pdf /
|
|
15
|
+
// html) keeps winning: those implementations outrank this one by longest
|
|
16
|
+
// suffix, and a 'fallback' band type (the shipped text preview) still beats
|
|
17
|
+
// nothing-else-matches. This descriptor exists so a recorded log/diff/ndjson
|
|
18
|
+
// file opens with a real renderer instead of the "nothing can view this"
|
|
19
|
+
// notice.
|
|
20
|
+
//
|
|
21
|
+
// Degradation is explicit and observable (issue #187 batch 2): a rejected
|
|
22
|
+
// registration is logged with its cause and marked on <html> so it can never
|
|
23
|
+
// fail silently.
|
|
24
|
+
/** Suffixes this implementation claims (no leading dot; compound is allowed). */
|
|
25
|
+
const PREVIEW_EXTENSIONS = ['log', 'jsonl', 'ndjson', 'diff', 'patch']
|
|
26
|
+
/**
|
|
27
|
+
* Record an extension-point failure so it is visible to users, tests and e2e.
|
|
28
|
+
* The <html> markers are `data-dfa-degraded` (which stage failed) plus
|
|
29
|
+
* `data-dfa-degraded-cause` (why), which makes a missing tab diagnosable from
|
|
30
|
+
* the page itself instead of only from the browser console.
|
|
31
|
+
*/
|
|
32
|
+
function markDegraded(kind, error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
34
|
+
if (typeof console !== 'undefined' && typeof console.error === 'function') {
|
|
35
|
+
console.error('[dsh-file-activity] ' + kind + ' 注册失败: ' + message)
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const root = typeof document === 'undefined' ? undefined : document.documentElement
|
|
39
|
+
if (root !== undefined && root !== null && root.dataset !== undefined) {
|
|
40
|
+
root.dataset.dfaDegraded = kind
|
|
41
|
+
root.dataset.dfaDegradedCause = message.slice(0, 200)
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// observation only: a missing document must not break activation
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Register the native document-preview descriptor (metadata only). */
|
|
48
|
+
function registerDocumentPreviews(ctx) {
|
|
49
|
+
ctx.effect(
|
|
50
|
+
() =>
|
|
51
|
+
ctx.documentPreviews.register({
|
|
52
|
+
id: TAB_ID,
|
|
53
|
+
extensions: PREVIEW_EXTENSIONS,
|
|
54
|
+
// 'extension' lets this implementation win over a shipped one on the
|
|
55
|
+
// same suffix; distinct suffixes keep the shipped renderers in charge.
|
|
56
|
+
priority: 'extension',
|
|
57
|
+
title: () => strings.title(),
|
|
58
|
+
loading: 'text-pages',
|
|
59
|
+
}),
|
|
60
|
+
'dsh-file-activity: document previews',
|
|
61
|
+
)
|
|
62
|
+
}
|
package/lib/parts/format.part.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
'use strict'
|
|
1
2
|
// ── path / time formatting helpers ────────────────────────────────────
|
|
2
3
|
function basenameOf(path) {
|
|
3
4
|
const norm = path.split('\\').join('/')
|
|
4
5
|
const idx = norm.lastIndexOf('/')
|
|
5
6
|
return idx === -1 ? norm : norm.slice(idx + 1)
|
|
6
7
|
}
|
|
7
|
-
|
|
8
8
|
/** Compact relative time: 刚刚 / N 分钟前 / N 小时前 / N 天前 / MM/DD. */
|
|
9
9
|
function formatRelative(time) {
|
|
10
10
|
if (typeof time !== 'number' || !Number.isFinite(time)) return ''
|
|
@@ -19,7 +19,6 @@ function formatRelative(time) {
|
|
|
19
19
|
const date = new Date(time)
|
|
20
20
|
return `${date.getMonth() + 1}/${date.getDate()}`
|
|
21
21
|
}
|
|
22
|
-
|
|
23
22
|
/** Local wall-clock HH:MM:SS (used in tooltips; full precision). */
|
|
24
23
|
function formatTime(time) {
|
|
25
24
|
const date = new Date(time)
|
package/lib/parts/i18n.part.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
'use strict'
|
|
1
2
|
// ── i18n ──────────────────────────────────────────────────────────────
|
|
2
3
|
function isZh() {
|
|
3
4
|
try {
|
|
@@ -7,7 +8,6 @@ function isZh() {
|
|
|
7
8
|
return false
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
|
-
|
|
11
11
|
const strings = {
|
|
12
12
|
title: () => (isZh() ? '文件活动' : 'File Activity'),
|
|
13
13
|
recent: () => (isZh() ? '最近访问' : 'Recent'),
|
|
@@ -43,5 +43,19 @@ const strings = {
|
|
|
43
43
|
isZh() ? '文件位于工作区外,暂无法读取内容' : 'The file is outside the workspace and cannot be read',
|
|
44
44
|
downloadToView: () => (isZh() ? '下载查看' : 'download to view'),
|
|
45
45
|
clickOutsideToClose: () => (isZh() ? '点击外部关闭' : 'Click outside to close'),
|
|
46
|
+
guideDescription: () =>
|
|
47
|
+
isZh()
|
|
48
|
+
? '查看 agent 与侧边栏读写过的文件(最近访问 + 目录统计)'
|
|
49
|
+
: 'Files the agent and the sidebar touched (recent + tree)',
|
|
50
|
+
autoOpenLabel: () => (isZh() ? '会话开始时自动打开' : 'Auto-open on session start'),
|
|
51
|
+
autoOpenHint: () =>
|
|
52
|
+
isZh()
|
|
53
|
+
? '每个会话首次打开时自动显示本页;关闭后仍可从侧边栏右上角的「新标签页」手动打开。'
|
|
54
|
+
: 'Shows this page once per session. When off, open it from the sidebar new-tab control.',
|
|
55
|
+
tabUnavailable: () => (isZh() ? '侧边栏扩展点不可用' : 'Sidebar extension point unavailable'),
|
|
56
|
+
tabUnavailableHint: () =>
|
|
57
|
+
isZh()
|
|
58
|
+
? '本插件未能注册页签(宿主原生 API 可能已变更)。原因见浏览器控制台。'
|
|
59
|
+
: 'This plugin could not register its tab (the host native API may have changed). See the browser console.',
|
|
46
60
|
autoCloseHint: () => (isZh() ? '预览失败,即将自动关闭' : 'Preview failed — closing automatically'),
|
|
47
61
|
}
|
|
@@ -1,40 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/** Record
|
|
12
|
-
function
|
|
13
|
-
if (url.pathname !== '/
|
|
14
|
-
if (
|
|
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
|
-
|
|
20
|
-
/** Record sidebar media opens (/sidebar/file?sessionId=...&path=...). */
|
|
21
|
-
function recordMediaOpen(url, init) {
|
|
22
|
-
if (url.pathname !== '/sidebar/file' || methodOf(init) !== 'GET') return
|
|
1
|
+
'use strict'
|
|
2
|
+
// ── fetch interception: the plugin's own file routes ───────────────────
|
|
3
|
+
//
|
|
4
|
+
// Before the native migration this module also watched the THIRD-PARTY
|
|
5
|
+
// sidebar's routes (/sidebar/api/fs.read|fs.write, /sidebar/file). Those are
|
|
6
|
+
// the host's internal surface now, fed by the host's own file provider and
|
|
7
|
+
// document owner — the plugin neither opens nor owns them, so observing them
|
|
8
|
+
// would record host traffic that is already recorded through the plugin's
|
|
9
|
+
// fs/observed host half. What remains is the plugin's own media/text route,
|
|
10
|
+
// whose reads ARE the user's previews.
|
|
11
|
+
/** Record requests for the plugin's own file route (media + text previews). */
|
|
12
|
+
function recordOwnMediaOpen(url, init) {
|
|
13
|
+
if (url.pathname !== '/file-activity/file') return
|
|
14
|
+
if ((init?.method ?? 'GET').toUpperCase() !== 'GET') return
|
|
23
15
|
const sessionId = url.searchParams.get('sessionId')
|
|
24
16
|
const path = url.searchParams.get('path')
|
|
25
17
|
if (sessionId !== null && path !== null) postRecord(sessionId, path, 'read')
|
|
26
18
|
}
|
|
27
|
-
|
|
28
|
-
/** Observe a resolved fetch URL and record sidebar file operations. */
|
|
19
|
+
/** Observe a resolved fetch URL and record plugin file operations. */
|
|
29
20
|
function observeSidebarFetch(url, init) {
|
|
30
21
|
try {
|
|
31
|
-
|
|
32
|
-
recordMediaOpen(url, init)
|
|
22
|
+
recordOwnMediaOpen(url, init)
|
|
33
23
|
} catch {
|
|
34
24
|
// observation must never break the underlying call
|
|
35
25
|
}
|
|
36
26
|
}
|
|
37
|
-
|
|
38
27
|
function installFetchInterceptor() {
|
|
39
28
|
const original = window.fetch.bind(window)
|
|
40
29
|
window.fetch = (input, init) => {
|