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/cwd.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-file-activity — session working-directory resolution.
|
|
3
|
+
*
|
|
4
|
+
* 从 http.js 拆出(issue #45:http.js 的 JSON 工具已抽到 dsh-shared,
|
|
5
|
+
* sessionCwdOf 为 file-activity 特有,保留本地)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Session working directory, mirroring better-sidebar's resolution. */
|
|
9
|
+
export function sessionCwdOf(ctx, sessionId) {
|
|
10
|
+
const session = ctx.sessions.get(sessionId)
|
|
11
|
+
const headerCwd = session?.header?.cwd
|
|
12
|
+
if (typeof headerCwd === 'string' && headerCwd !== '') return headerCwd
|
|
13
|
+
return process.cwd()
|
|
14
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
* State (recent history + per-file counts) is kept per session and persisted
|
|
15
15
|
* to $DSH_HOME/file-activity.json (atomic tmp+rename, debounced).
|
|
16
16
|
*/
|
|
17
|
-
import { isTrustedApiRequest } from '
|
|
17
|
+
import { isTrustedApiRequest } from 'dsh-shared'
|
|
18
18
|
import { createApiHandler } from './api-route.js'
|
|
19
19
|
import { createMediaHandler } from './media-route.js'
|
|
20
20
|
import { createFsObserver } from './observer.js'
|
|
21
21
|
import { createStore } from './store.js'
|
|
22
22
|
import { parseBashFileOps } from './bash-parse.js'
|
|
23
|
-
import { sessionCwdOf } from './
|
|
23
|
+
import { sessionCwdOf } from './cwd.js'
|
|
24
24
|
|
|
25
25
|
export const name = 'dsh-file-activity'
|
|
26
26
|
|
|
@@ -69,19 +69,27 @@ export function apply(ctx) {
|
|
|
69
69
|
// ── routes ──────────────────────────────────────────────────────────────
|
|
70
70
|
const fence = (request) => isTrustedApiRequest(request, ctx.webRuntime.trustedHosts)
|
|
71
71
|
|
|
72
|
-
ctx.effect(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
ctx.effect(
|
|
73
|
+
() =>
|
|
74
|
+
ctx.webServer.register({
|
|
75
|
+
kind: 'prefix',
|
|
76
|
+
path: '/file-activity/api',
|
|
77
|
+
handler: createApiHandler({ ctx, store, fence }),
|
|
78
|
+
}),
|
|
79
|
+
'dsh-file-activity: /file-activity/api routes',
|
|
80
|
+
)
|
|
77
81
|
|
|
78
82
|
// Media route for the floating preview (see media-route.js for the
|
|
79
83
|
// authorization model: recorded paths only, same trust fence).
|
|
80
|
-
ctx.effect(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
ctx.effect(
|
|
85
|
+
() =>
|
|
86
|
+
ctx.webServer.register({
|
|
87
|
+
kind: 'prefix',
|
|
88
|
+
path: '/file-activity/file',
|
|
89
|
+
handler: createMediaHandler({ ctx, store, fence }),
|
|
90
|
+
}),
|
|
91
|
+
'dsh-file-activity: /file-activity/file media route',
|
|
92
|
+
)
|
|
85
93
|
|
|
86
94
|
// Tear down on unload: flush pending persistence.
|
|
87
95
|
ctx.effect(() => store.dispose, 'dsh-file-activity: persistence teardown')
|
package/lib/media-route.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { readFile, stat } from 'node:fs/promises'
|
|
12
12
|
import { basename, isAbsolute, join } from 'node:path'
|
|
13
|
-
import {
|
|
13
|
+
import { writeJson } from 'dsh-shared'
|
|
14
|
+
import { sessionCwdOf } from './cwd.js'
|
|
14
15
|
import { isRecordedPath } from './state.js'
|
|
15
16
|
|
|
16
17
|
/** Cap for the plugin's own media route (bytes): images / PDFs only. */
|
|
@@ -58,12 +59,16 @@ export function createMediaHandler({ ctx, store, fence }) {
|
|
|
58
59
|
const sessionId = url.searchParams.get('sessionId')
|
|
59
60
|
const raw = url.searchParams.get('path')
|
|
60
61
|
assertMediaParams(sessionId, raw)
|
|
61
|
-
if (!isRecordedPath(store.state, sessionId, raw))
|
|
62
|
+
if (!isRecordedPath(store.state, sessionId, raw))
|
|
63
|
+
throw mediaError(403, "path is not in this session's file activity")
|
|
62
64
|
const abs = isAbsolute(raw) ? raw : join(sessionCwdOf(ctx, sessionId), raw)
|
|
63
65
|
await serveMedia(response, abs, url)
|
|
64
66
|
} catch (error) {
|
|
65
67
|
const status = typeof error?.status === 'number' ? error.status : 400
|
|
66
|
-
writeJson(response, status, {
|
|
68
|
+
writeJson(response, status, {
|
|
69
|
+
ok: false,
|
|
70
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
71
|
+
})
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
}
|
package/lib/parts/api.part.js
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
// ── data access (host routes) ─────────────────────────────────────────
|
|
2
|
+
async function fetchStats(sessionId) {
|
|
3
|
+
const response = await fetch(`/file-activity/api/stats?sessionId=${encodeURIComponent(sessionId)}`)
|
|
4
|
+
const json = await response.json()
|
|
5
|
+
if (json === null || typeof json !== 'object' || json.ok !== true) return null
|
|
6
|
+
return json.value
|
|
7
|
+
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
9
|
+
/** Resolve the session working directory through the sidebar's native API. */
|
|
10
|
+
async function fetchSessionCwd(sessionId) {
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch('/sidebar/api/session.cwd', {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'content-type': 'application/json' },
|
|
15
|
+
body: JSON.stringify({ sessionId }),
|
|
16
|
+
})
|
|
17
|
+
const json = await response.json()
|
|
18
|
+
const cwd = json?.value?.cwd
|
|
19
|
+
return typeof cwd === 'string' && cwd !== '' ? cwd : ''
|
|
20
|
+
} catch {
|
|
21
|
+
return ''
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
function postRecord(sessionId, path, op) {
|
|
26
|
+
if (typeof sessionId !== 'string' || sessionId === '' || typeof path !== 'string' || path === '') return
|
|
27
|
+
void fetch('/file-activity/api/record', {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'content-type': 'application/json' },
|
|
30
|
+
body: JSON.stringify({ sessionId, path, op }),
|
|
31
|
+
}).catch(() => {})
|
|
32
|
+
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
function postClear(sessionId) {
|
|
35
|
+
void fetch('/file-activity/api/clear', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'content-type': 'application/json' },
|
|
38
|
+
body: JSON.stringify({ sessionId }),
|
|
39
|
+
}).catch(() => {})
|
|
40
|
+
}
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
/** Plugin media route URL for a recorded path (authorized per session). */
|
|
43
|
+
function mediaUrlOf(sessionId, path) {
|
|
44
|
+
return `/file-activity/file?${new URLSearchParams({ sessionId, path })}`
|
|
45
|
+
}
|
package/lib/parts/apply.part.js
CHANGED
|
@@ -1,76 +1,85 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
}, 'dsh-file-activity: styles')
|
|
1
|
+
// ── plugin body ───────────────────────────────────────────────────────
|
|
2
|
+
/**
|
|
3
|
+
* The stylesheet is pure static CSS and must NOT depend on the
|
|
4
|
+
* betterSidebar service: inject it first, unconditionally. If it lived
|
|
5
|
+
* behind the `service === undefined` early return, an HMR rebuild or
|
|
6
|
+
* service reload could leave the already-rendered tab WITHOUT its
|
|
7
|
+
* stylesheet — the raw white-text list you see when the CSS is gone.
|
|
8
|
+
* Each fiber owns its own <style> element and the disposer removes
|
|
9
|
+
* only that element, so a rebuild always keeps at least one copy.
|
|
10
|
+
*/
|
|
11
|
+
function injectStyles(ctx) {
|
|
12
|
+
ctx.effect(() => {
|
|
13
|
+
if (typeof document === 'undefined' || document === null || typeof document.head === 'undefined') return () => {}
|
|
14
|
+
const style = document.createElement('style')
|
|
15
|
+
style.setAttribute('data-dsh-file-activity', 'styles')
|
|
16
|
+
style.textContent = STYLES
|
|
17
|
+
document.head.appendChild(style)
|
|
18
|
+
return () => {
|
|
19
|
+
if (style.parentNode) style.parentNode.removeChild(style)
|
|
22
20
|
}
|
|
21
|
+
}, 'dsh-file-activity: styles')
|
|
22
|
+
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
/** Register the tab (enabled by default in the Side card settings). */
|
|
36
|
+
function registerTab(ctx, dataStore) {
|
|
37
|
+
const service = ctx.betterSidebar
|
|
38
|
+
ctx.effect(
|
|
39
|
+
() =>
|
|
40
|
+
service.registerTab({
|
|
39
41
|
id: TAB_ID,
|
|
40
42
|
title: () => strings.title(),
|
|
41
43
|
icon: (size) => icon.clock(size),
|
|
42
44
|
order: 15,
|
|
43
45
|
single: true,
|
|
44
46
|
settings: {
|
|
45
|
-
pluginToggles: [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
pluginToggles: [
|
|
48
|
+
{
|
|
49
|
+
key: 'autoOpen',
|
|
50
|
+
title: () => (isZh() ? '会话开始时自动打开' : 'Auto-open on session start'),
|
|
51
|
+
desc: () =>
|
|
52
|
+
isZh()
|
|
53
|
+
? '每个会话首次打开时自动显示本页(可在侧边栏设置中关闭)'
|
|
54
|
+
: 'Opens this tab once per session by default (turn off here)',
|
|
55
|
+
type: 'switch',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
51
58
|
},
|
|
52
59
|
component: (props) => createElement(FileActivityView, { ...props, dataStore }),
|
|
53
|
-
}),
|
|
54
|
-
|
|
60
|
+
}),
|
|
61
|
+
'dsh-file-activity: tab registration',
|
|
62
|
+
)
|
|
63
|
+
}
|
|
55
64
|
|
|
56
|
-
|
|
65
|
+
exports.inject = ['betterSidebar']
|
|
57
66
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
exports.apply = function apply(ctx) {
|
|
68
|
+
// Stylesheet first, unconditionally (HMR pitfall — see injectStyles).
|
|
69
|
+
injectStyles(ctx)
|
|
70
|
+
const service = ctx.betterSidebar
|
|
71
|
+
if (service === undefined) return
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
// Per-session data store: { bySession: { [sessionId]: { recent, counts, loading } }, preview }
|
|
74
|
+
// Each conversation reads/writes only its own bucket, so switching
|
|
75
|
+
// sessions never leaks another session's file activity into the view.
|
|
76
|
+
const dataStore = createStore({ bySession: {}, preview: null })
|
|
77
|
+
mountProbe()
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
// sidebar operations → host record route
|
|
80
|
+
ctx.effect(() => installFetchInterceptor(), 'dsh-file-activity: sidebar fetch observation')
|
|
81
|
+
registerTab(ctx, dataStore)
|
|
73
82
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
// auto-open once per session (default on)
|
|
84
|
+
ctx.effect(() => installAutoOpen(ctx, TAB_ID), 'dsh-file-activity: auto-open')
|
|
85
|
+
}
|
|
@@ -1,75 +1,76 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
return false
|
|
1
|
+
// ── auto-open (enabled by default) ────────────────────────────────────
|
|
2
|
+
function findTabIn(state, tabId) {
|
|
3
|
+
const leaves = (node) => (node.kind === 'leaf' ? [node] : (node.children ?? []).flatMap(leaves))
|
|
4
|
+
for (const node of [state?.splits, state?.bottomSplits]) {
|
|
5
|
+
if (node === undefined || node === null) continue
|
|
6
|
+
for (const leaf of leaves(node)) {
|
|
7
|
+
if ((leaf.tabs ?? []).some((tab) => tab.type === tabId)) return true
|
|
11
8
|
}
|
|
9
|
+
}
|
|
10
|
+
return false
|
|
11
|
+
}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
/** Current sidebar snapshot, or null when the service is not ready. */
|
|
14
|
+
function sidebarSnapshot(service) {
|
|
15
|
+
try {
|
|
16
|
+
return service.getSnapshot?.()
|
|
17
|
+
} catch {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
/** Persist the auto-opened marker for this session. */
|
|
38
|
+
function markAutoOpened(sessionId) {
|
|
39
|
+
try {
|
|
40
|
+
window.localStorage.setItem(AUTO_OPEN_KEY + sessionId, '1')
|
|
41
|
+
} catch {
|
|
42
|
+
// ignore
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
46
|
+
/** Open the tab once per session unless disabled in the plugin settings. */
|
|
47
|
+
function tryAutoOpen(service, tabId) {
|
|
48
|
+
const snapshot = sidebarSnapshot(service)
|
|
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
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
service.openTab({ type: tabId, title: strings.title(), path: '' })
|
|
60
|
+
markAutoOpened(sessionId)
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('[dsh-file-activity] auto-open failed:', error)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
function installAutoOpen(ctx, tabId) {
|
|
67
|
+
const service = ctx.betterSidebar
|
|
68
|
+
tryAutoOpen(service, tabId)
|
|
69
|
+
let off = () => {}
|
|
70
|
+
try {
|
|
71
|
+
off = service.subscribeState?.(() => tryAutoOpen(service, tabId)) ?? off
|
|
72
|
+
} catch {
|
|
73
|
+
// service may lack subscribeState on older versions
|
|
74
|
+
}
|
|
75
|
+
return off
|
|
76
|
+
}
|
package/lib/parts/format.part.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
// ── path / time formatting helpers ────────────────────────────────────
|
|
2
|
+
function basenameOf(path) {
|
|
3
|
+
const norm = path.split('\\').join('/')
|
|
4
|
+
const idx = norm.lastIndexOf('/')
|
|
5
|
+
return idx === -1 ? norm : norm.slice(idx + 1)
|
|
6
|
+
}
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
/** Compact relative time: 刚刚 / N 分钟前 / N 小时前 / N 天前 / MM/DD. */
|
|
9
|
+
function formatRelative(time) {
|
|
10
|
+
if (typeof time !== 'number' || !Number.isFinite(time)) return ''
|
|
11
|
+
const diff = Date.now() - time
|
|
12
|
+
if (diff < 30_000) return strings.justNow()
|
|
13
|
+
const minutes = Math.floor(diff / 60_000)
|
|
14
|
+
if (minutes < 60) return strings.minutesAgo(minutes)
|
|
15
|
+
const hours = Math.floor(minutes / 60)
|
|
16
|
+
if (hours < 24) return strings.hoursAgo(hours)
|
|
17
|
+
const days = Math.floor(hours / 24)
|
|
18
|
+
if (days < 7) return strings.daysAgo(days)
|
|
19
|
+
const date = new Date(time)
|
|
20
|
+
return `${date.getMonth() + 1}/${date.getDate()}`
|
|
21
|
+
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
/** Local wall-clock HH:MM:SS (used in tooltips; full precision). */
|
|
24
|
+
function formatTime(time) {
|
|
25
|
+
const date = new Date(time)
|
|
26
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
27
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
|
28
|
+
}
|
package/lib/parts/i18n.part.js
CHANGED
|
@@ -1,41 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
// ── i18n ──────────────────────────────────────────────────────────────
|
|
2
|
+
function isZh() {
|
|
3
|
+
try {
|
|
4
|
+
const lang = (navigator.language || 'en').toLowerCase()
|
|
5
|
+
return lang.startsWith('zh')
|
|
6
|
+
} catch {
|
|
7
|
+
return false
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
11
|
+
const strings = {
|
|
12
|
+
title: () => (isZh() ? '文件活动' : 'File Activity'),
|
|
13
|
+
recent: () => (isZh() ? '最近访问' : 'Recent'),
|
|
14
|
+
stats: () => (isZh() ? '文件统计' : 'File Stats'),
|
|
15
|
+
empty: () => (isZh() ? '暂无文件活动记录' : 'No file activity yet'),
|
|
16
|
+
emptyHint: () =>
|
|
17
|
+
isZh()
|
|
18
|
+
? '在侧边栏打开文件、编辑保存,或让 agent 读写文件(创建/读取/修改),都会记录在这里。点击任意文件将在侧边栏内用原生预览打开(代码高亮 / Markdown 渲染 / 图片 / PDF…)。'
|
|
19
|
+
: 'Opening files in the sidebar, editing, or agent file operations (create/read/modify) are recorded here. Click any file to open it in the sidebar with native preview (syntax highlighting / Markdown rendering / images / PDF…).',
|
|
20
|
+
refresh: () => (isZh() ? '刷新' : 'Refresh'),
|
|
21
|
+
clear: () => (isZh() ? '清空' : 'Clear'),
|
|
22
|
+
clearConfirm: () => (isZh() ? '确定清空当前会话的全部文件活动记录?' : 'Clear all file activity for this session?'),
|
|
23
|
+
read: () => (isZh() ? '读取' : 'read'),
|
|
24
|
+
create: () => (isZh() ? '新增' : 'create'),
|
|
25
|
+
modify: () => (isZh() ? '修改' : 'modify'),
|
|
26
|
+
delete: () => (isZh() ? '删除' : 'delete'),
|
|
27
|
+
readShort: () => (isZh() ? '读' : 'R'),
|
|
28
|
+
createShort: () => (isZh() ? '增' : 'C'),
|
|
29
|
+
modifyShort: () => (isZh() ? '改' : 'M'),
|
|
30
|
+
loadError: () => (isZh() ? '加载失败' : 'Load failed'),
|
|
31
|
+
created: () => (isZh() ? '创建' : 'Created'),
|
|
32
|
+
lastSeen: () => (isZh() ? '最近访问' : 'Last seen'),
|
|
33
|
+
justNow: () => (isZh() ? '刚刚' : 'just now'),
|
|
34
|
+
minutesAgo: (m) => (isZh() ? `${m} 分钟前` : `${m}m ago`),
|
|
35
|
+
hoursAgo: (h) => (isZh() ? `${h} 小时前` : `${h}h ago`),
|
|
36
|
+
daysAgo: (d) => (isZh() ? `${d} 天前` : `${d}d ago`),
|
|
37
|
+
closePreview: () => (isZh() ? '关闭预览' : 'Close preview'),
|
|
38
|
+
loading: () => (isZh() ? '加载中…' : 'Loading…'),
|
|
39
|
+
previewUnsupported: () => (isZh() ? '该文件类型暂不支持预览' : 'This file type cannot be previewed yet'),
|
|
40
|
+
previewFailed: () => (isZh() ? '预览加载失败' : 'Preview failed to load'),
|
|
41
|
+
downloadToView: () => (isZh() ? '下载查看' : 'download to view'),
|
|
42
|
+
}
|