dsh-file-activity 0.5.4 → 0.5.6
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 +23 -0
- package/README.md +4 -4
- package/assets/preview-float-0.5.6.png +0 -0
- package/lib/client.js +113 -12
- package/lib/media-route.js +1 -2
- package/lib/parts/apply.part.js +26 -2
- package/lib/parts/i18n.part.js +2 -0
- package/lib/parts/preview.part.js +69 -9
- package/lib/parts/styles.part.js +11 -1
- package/lib/parts/view.part.js +5 -0
- package/package.json +6 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录 dsh-file-activity 的所有版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [0.5.6] - 2026-09-04
|
|
6
|
+
|
|
7
|
+
### 变更
|
|
8
|
+
|
|
9
|
+
- chore(release): dsh-file-activity v0.5.6(#111 浮窗预览 + 验证清单)
|
|
10
|
+
- fix(file-activity): #111 HTML 预览内容撑满浮窗 (#116)
|
|
11
|
+
- docs: #106 安装命令统一加 --trust-lockfile (#113)
|
|
12
|
+
|
|
13
|
+
## [0.5.5] - 2026-09-02
|
|
14
|
+
|
|
15
|
+
### 变更
|
|
16
|
+
|
|
17
|
+
- fix(scripts): #72 插件依赖未随安装自动安装(dsh-shared 未发布 npm) (#96)
|
|
18
|
+
- fix(file-activity): 浮窗预览失焦自动关闭(issue #76) (#95)
|
|
19
|
+
- fix(file-activity): #72 补充 dsh-better-sidebar 级联安装——dependencies 声明 + 缺失提示
|
|
20
|
+
- style: prettier 全量格式修复(CI 格式门禁)
|
|
21
|
+
|
|
22
|
+
## [Unreleased]
|
|
23
|
+
|
|
24
|
+
### 变更
|
|
25
|
+
|
|
26
|
+
- fix(ui): 浮窗预览失焦自动关闭——点击外部任意处/切换页签即关,预览失败 2.5s 自动关闭或点击任意处关闭,头部「点击外部关闭」提示(issue #76)
|
|
27
|
+
|
|
5
28
|
## [0.5.4] - 2026-09-01
|
|
6
29
|
|
|
7
30
|
### 变更
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
<img alt="文件活动插件截图(最近访问 / 文件统计)" src="https://unpkg.com/dsh-file-activity/assets/screenshot.png" width="340" />
|
|
7
7
|
<br />
|
|
8
8
|
<img alt="浮窗预览:点击文件复用侧边栏内置 Markdown 渲染" src="https://unpkg.com/dsh-file-activity/assets/preview-float.png" width="340" />
|
|
9
|
+
<br />
|
|
10
|
+
<img alt="浮窗 HTML 预览:内容撑满整个浮窗主体(issue #111)" src="https://unpkg.com/dsh-file-activity/assets/preview-float-0.5.6.png" width="340" />
|
|
9
11
|
</div>
|
|
10
12
|
|
|
11
13
|
**DSH 侧边栏文件活动插件**(基于 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 扩展):在 better-sidebar 中新增「文件活动」页签,记录 agent 工具与侧边栏自身的文件读取/新增/修改事件,提供**最近访问**(LRU)与**文件统计**(树形目录)两块视图,点击文件弹出浮窗预览。
|
|
@@ -36,9 +38,7 @@
|
|
|
36
38
|
|
|
37
39
|
## 安装
|
|
38
40
|
|
|
39
|
-
> 💡 **npm 安装(普通用户推荐)**:`dsh plugin --profile web add dsh-file-activity
|
|
40
|
-
|
|
41
|
-
前置:已安装 `dsh-better-sidebar`(v0.12+,推荐 v0.14)。
|
|
41
|
+
> 💡 **npm 安装(普通用户推荐)**:`dsh plugin --profile web add dsh-file-activity --trust-lockfile`——无需克隆本仓库;依赖自动级联安装:`dsh-better-sidebar`(宿主,提供侧边栏扩展点)与 `dsh-shared`(server 端共享工具包)均声明为 dependencies,安装时自动安装并加入 profile bundles,无需手动单独处理。
|
|
42
42
|
|
|
43
43
|
```sh
|
|
44
44
|
# 方式一:dsh plugin(推荐)
|
|
@@ -75,7 +75,7 @@ dsh plugin --profile web add link:<仓库路径>/plugins/dsh-file-activity
|
|
|
75
75
|
- **Markdown**(`.md` / `.markdown`):渲染后的富文本预览,可切换 预览/编辑 模式。
|
|
76
76
|
- **图片**(png / jpg / jpeg / gif / webp / svg / bmp / ico / avif):通过媒体路由渲染。
|
|
77
77
|
- **PDF**:浏览器原生 PDF 查看器(浮窗内嵌,工具栏提供下载)。
|
|
78
|
-
- **HTML
|
|
78
|
+
- **HTML**:沙箱化渲染预览,内容**撑满整个浮窗主体**(iframe 随浮窗尺寸自适应、内部滚动,与代码/Markdown/图片预览体验一致,issue #111)。
|
|
79
79
|
- 其余文件按内置 viewer 的 `matchFileViewer` 回退匹配(如 `binary-download` 下载未知二进制)。
|
|
80
80
|
|
|
81
81
|
> 💡 **工作区外文件也能预览**:文件活动记录的是 agent 实际触碰过的文件,路径可能在会话工作区之外(如 `/tmp` 临时文件)。侧边栏自带的媒体路由只允许工作区内文件,因此图片/PDF 的字节由插件自身的 `/file-activity/file` 路由提供——它按会话记录授权(只放行本会话记录过的路径,未记录的路径一律 403),fence 与侧边栏一致。Markdown/代码/HTML 文本则仍走侧边栏 `fs.read`(不被工作区限制)。
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -90,6 +90,8 @@ const strings = {
|
|
|
90
90
|
fileOutside: () =>
|
|
91
91
|
isZh() ? '文件位于工作区外,暂无法读取内容' : 'The file is outside the workspace and cannot be read',
|
|
92
92
|
downloadToView: () => (isZh() ? '下载查看' : 'download to view'),
|
|
93
|
+
clickOutsideToClose: () => (isZh() ? '点击外部关闭' : 'Click outside to close'),
|
|
94
|
+
autoCloseHint: () => (isZh() ? '预览失败,即将自动关闭' : 'Preview failed — closing automatically'),
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
// ── path / time formatting helpers ────────────────────────────────────
|
|
@@ -776,8 +778,18 @@ const STYLES = `
|
|
|
776
778
|
display:flex; flex-direction:column; overflow:hidden; }
|
|
777
779
|
.dfa-fp-head { display:flex; align-items:center; gap:6px; padding:6px 8px; border-bottom:1px solid var(--dsw-alias-border-l1); flex:none; }
|
|
778
780
|
.dfa-fp-title { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font:var(--dsw-font-s-strong-14); color:var(--dsw-alias-label-primary); }
|
|
781
|
+
.dfa-fp-hint { flex:none; font:var(--dsw-font-xxs-12); color:var(--dsw-alias-label-tertiary); opacity:0.8; }
|
|
779
782
|
.dfa-fp-actions { display:flex; align-items:center; gap:2px; flex:none; }
|
|
780
|
-
|
|
783
|
+
/* issue #111: the preview body is a FLEX COLUMN so the mounted viewer
|
|
784
|
+
component (better-sidebar's TextEditor for html/code/markdown, the image
|
|
785
|
+
wrap, and the pdf frame) actually fills the window. Their roots size via
|
|
786
|
+
flex:1 (html iframe .editorHtml, .editorCm, .editorMd, .editorImageWrap,
|
|
787
|
+
.dfa-pdf), which is IGNORED in a block context — so the HTML iframe, which
|
|
788
|
+
otherwise has a fixed browser-default height, rendered as a thin top strip
|
|
789
|
+
with the rest of the window dark. Making the body a flex column lets every
|
|
790
|
+
viewer's flex:1 child stretch to the full .dfa-fp-body height and scroll
|
|
791
|
+
internally, so content fills the window and follows window resizes. */
|
|
792
|
+
.dfa-fp-body { flex:1; display:flex; flex-direction:column; overflow:auto; padding:10px 12px; min-height:0; }
|
|
781
793
|
.dfa-fp-note { color:var(--dsw-alias-label-tertiary); font:var(--dsw-font-xxs-12); }
|
|
782
794
|
.dfa-fp-err { color:var(--dsw-alias-state-error-primary); font:var(--dsw-font-xxs-12); white-space:pre-wrap; word-break:break-all; }
|
|
783
795
|
/* PDF preview: a native browser PDF frame filled from the plugin's own media
|
|
@@ -1124,6 +1136,11 @@ function FileActivityView({ ctx, store, scope, visible, dataStore }) {
|
|
|
1124
1136
|
useEffect(() => {
|
|
1125
1137
|
dataStore.set({ preview: null })
|
|
1126
1138
|
}, [sessionId, dataStore])
|
|
1139
|
+
// Switching tabs hides this view: close any floating preview so it never
|
|
1140
|
+
// lingers over the main UI (issue #76).
|
|
1141
|
+
useEffect(() => {
|
|
1142
|
+
closePreviewOnHidden(visible, dataStore)
|
|
1143
|
+
}, [visible, dataStore])
|
|
1127
1144
|
const toggleDir = (path) => setCollapsedDirs((prev) => toggleInSet(prev, path))
|
|
1128
1145
|
const openPreview = (path) => dataStore.set({ preview: { abs: path, name: basenameOf(path) } })
|
|
1129
1146
|
const closePreview = () => dataStore.set({ preview: null })
|
|
@@ -1176,6 +1193,34 @@ function fsReadError(json, viewer) {
|
|
|
1176
1193
|
return { status: 'error', viewer, message }
|
|
1177
1194
|
}
|
|
1178
1195
|
|
|
1196
|
+
/** Milliseconds after which an error-state preview closes itself (issue #76):
|
|
1197
|
+
* a shell that failed to load any content is useless, so it must not linger
|
|
1198
|
+
* over the main UI until the user finds the × button. */
|
|
1199
|
+
const AUTO_CLOSE_MS = 2500
|
|
1200
|
+
|
|
1201
|
+
/** Whether a pointerdown target lies inside the floating window. The window
|
|
1202
|
+
* surface carries the `.dfa-fp` class; anything else counts as "outside"
|
|
1203
|
+
* and dismisses the preview (issue #76 — click anywhere outside closes). */
|
|
1204
|
+
function isInsideFloating(target) {
|
|
1205
|
+
if (!target || typeof target.closest !== 'function') return false
|
|
1206
|
+
return target.closest('.dfa-fp') !== null
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/** Click behavior for the window surface: in the error state ANY click
|
|
1210
|
+
* closes the shell (there is no content to interact with), otherwise the
|
|
1211
|
+
* click is swallowed so the viewer's own interactions keep working. */
|
|
1212
|
+
function previewClickAction(load, event) {
|
|
1213
|
+
if (load.status === 'error') return 'close'
|
|
1214
|
+
if (event && event.stopPropagation) event.stopPropagation()
|
|
1215
|
+
return 'stop'
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/** Close the floating preview when the tab goes hidden (switching tabs /
|
|
1219
|
+
* operating the main UI), so it never lingers over the interface. */
|
|
1220
|
+
function closePreviewOnHidden(visible, dataStore) {
|
|
1221
|
+
if (!visible) dataStore.set({ preview: null })
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1179
1224
|
/**
|
|
1180
1225
|
* Load fsRead content through the sidebar API and resolve the viewer's
|
|
1181
1226
|
* load state (ready with text, or error with the API message). When the
|
|
@@ -1275,6 +1320,7 @@ function renderPreviewBody(load, ctx, store, scope, path, title, sessionId) {
|
|
|
1275
1320
|
load.message
|
|
1276
1321
|
? createElement('div', { style: { marginTop: '6px', fontSize: '11px', opacity: 0.85 } }, load.message)
|
|
1277
1322
|
: null,
|
|
1323
|
+
createElement('div', { style: { marginTop: '6px', fontSize: '11px', opacity: 0.7 } }, strings.autoCloseHint()),
|
|
1278
1324
|
)
|
|
1279
1325
|
}
|
|
1280
1326
|
if (load.viewer.id === 'pdf') {
|
|
@@ -1310,15 +1356,25 @@ function renderPreviewBody(load, ctx, store, scope, path, title, sessionId) {
|
|
|
1310
1356
|
* built-in viewer that fetches its own URL internally (it ignores the
|
|
1311
1357
|
* `mediaUrl` prop), so it gets a small iframe preview instead.
|
|
1312
1358
|
*/
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1359
|
+
/**
|
|
1360
|
+
* Dismissal affordances (issue #76 — the preview must never linger):
|
|
1361
|
+
* 1. pointerdown anywhere OUTSIDE the window closes it (capture phase, so
|
|
1362
|
+
* it fires even if another element sits above the scrim);
|
|
1363
|
+
* 2. the scrim's own onClick (kept as a fallback);
|
|
1364
|
+
* 3. Escape;
|
|
1365
|
+
* 4. the × button;
|
|
1366
|
+
* 5. an error state closes itself after AUTO_CLOSE_MS (no content to show).
|
|
1367
|
+
*/
|
|
1368
|
+
function usePreviewDismiss(load, onClose) {
|
|
1369
|
+
useEffect(() => {
|
|
1370
|
+
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return () => {}
|
|
1371
|
+
const handler = (event) => {
|
|
1372
|
+
if (!isInsideFloating(event?.target)) onClose()
|
|
1373
|
+
}
|
|
1374
|
+
document.addEventListener('pointerdown', handler, true)
|
|
1375
|
+
return () => document.removeEventListener('pointerdown', handler, true)
|
|
1376
|
+
}, [onClose])
|
|
1319
1377
|
|
|
1320
|
-
// Clicking outside is the primary dismiss (the overlay's onClick);
|
|
1321
|
-
// Escape is a keyboard affordance. Both call onClose.
|
|
1322
1378
|
useEffect(() => {
|
|
1323
1379
|
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return () => {}
|
|
1324
1380
|
const handler = (event) => {
|
|
@@ -1328,6 +1384,16 @@ function FloatingPreview({ ctx, store, scope, preview, onClose }) {
|
|
|
1328
1384
|
return () => document.removeEventListener('keydown', handler)
|
|
1329
1385
|
}, [onClose])
|
|
1330
1386
|
|
|
1387
|
+
useEffect(() => {
|
|
1388
|
+
if (load.status !== 'error') return undefined
|
|
1389
|
+
if (typeof window === 'undefined' || typeof window.setTimeout !== 'function') return undefined
|
|
1390
|
+
const timer = window.setTimeout(onClose, AUTO_CLOSE_MS)
|
|
1391
|
+
return () => window.clearTimeout(timer)
|
|
1392
|
+
}, [load.status, onClose])
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/** The floating window element: head (title + hint + close) and body. */
|
|
1396
|
+
function renderFloatingWindow(load, ctx, store, scope, path, title, sessionId, onClose) {
|
|
1331
1397
|
return createElement(
|
|
1332
1398
|
'div',
|
|
1333
1399
|
{ className: 'dfa-fp-overlay', onClick: onClose },
|
|
@@ -1336,13 +1402,14 @@ function FloatingPreview({ ctx, store, scope, preview, onClose }) {
|
|
|
1336
1402
|
{
|
|
1337
1403
|
className: 'dfa-fp',
|
|
1338
1404
|
onClick: (event) => {
|
|
1339
|
-
if (event
|
|
1405
|
+
if (previewClickAction(load, event) === 'close') onClose()
|
|
1340
1406
|
},
|
|
1341
1407
|
},
|
|
1342
1408
|
createElement(
|
|
1343
1409
|
'div',
|
|
1344
1410
|
{ className: 'dfa-fp-head' },
|
|
1345
1411
|
createElement('span', { className: 'dfa-fp-title' }, title),
|
|
1412
|
+
createElement('span', { className: 'dfa-fp-hint' }, strings.clickOutsideToClose()),
|
|
1346
1413
|
createElement(
|
|
1347
1414
|
'span',
|
|
1348
1415
|
{ className: 'dfa-fp-actions' },
|
|
@@ -1367,6 +1434,16 @@ function FloatingPreview({ ctx, store, scope, preview, onClose }) {
|
|
|
1367
1434
|
)
|
|
1368
1435
|
}
|
|
1369
1436
|
|
|
1437
|
+
function FloatingPreview({ ctx, store, scope, preview, onClose }) {
|
|
1438
|
+
const sessionId = scope?.sessionId ?? ''
|
|
1439
|
+
const path = preview.abs
|
|
1440
|
+
const title = preview.name
|
|
1441
|
+
const service = ctx.betterSidebar
|
|
1442
|
+
const load = usePreviewLoader(service, path, sessionId, scope)
|
|
1443
|
+
usePreviewDismiss(load, onClose)
|
|
1444
|
+
return renderFloatingWindow(load, ctx, store, scope, path, title, sessionId, onClose)
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1370
1447
|
/**
|
|
1371
1448
|
* Lightweight PDF preview. better-sidebar's built-in PdfView fetches
|
|
1372
1449
|
* `/sidebar/file` internally (it ignores any injected `mediaUrl` prop),
|
|
@@ -1467,7 +1544,17 @@ exports.apply = function apply(ctx) {
|
|
|
1467
1544
|
// Stylesheet first, unconditionally (HMR pitfall — see injectStyles).
|
|
1468
1545
|
injectStyles(ctx)
|
|
1469
1546
|
const service = ctx.betterSidebar
|
|
1470
|
-
if (service === undefined)
|
|
1547
|
+
if (service === undefined) {
|
|
1548
|
+
// 依赖缺失提示(issue #72 同类问题):dsh-file-activity 的 client 端
|
|
1549
|
+
// 依赖 dsh-better-sidebar 提供侧边栏扩展点,未安装时静默返回会让用户
|
|
1550
|
+
// 以为插件坏了——明确提示安装方式。
|
|
1551
|
+
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
1552
|
+
console.warn(
|
|
1553
|
+
'[dsh-file-activity] dsh-better-sidebar 未安装:文件活动页签无法挂载。请安装宿主插件:dsh plugin --profile web add dsh-better-sidebar dsh-file-activity',
|
|
1554
|
+
)
|
|
1555
|
+
}
|
|
1556
|
+
return
|
|
1557
|
+
}
|
|
1471
1558
|
|
|
1472
1559
|
// Per-session data store: { bySession: { [sessionId]: { recent, counts, loading } }, preview }
|
|
1473
1560
|
// Each conversation reads/writes only its own bucket, so switching
|
|
@@ -1485,7 +1572,21 @@ exports.apply = function apply(ctx) {
|
|
|
1485
1572
|
|
|
1486
1573
|
// Internal functions exposed for the render-path test suite only; inert in
|
|
1487
1574
|
// the browser bundle (plain properties on the exports object).
|
|
1488
|
-
exports.__test = {
|
|
1575
|
+
exports.__test = {
|
|
1576
|
+
loadFsReadContent,
|
|
1577
|
+
fsReadError,
|
|
1578
|
+
fetchTextContent,
|
|
1579
|
+
textUrlOf,
|
|
1580
|
+
strings,
|
|
1581
|
+
renderPreviewBody,
|
|
1582
|
+
previewClickAction,
|
|
1583
|
+
isInsideFloating,
|
|
1584
|
+
closePreviewOnHidden,
|
|
1585
|
+
AUTO_CLOSE_MS,
|
|
1586
|
+
// Static stylesheet text, so the render-path suite can assert the floating
|
|
1587
|
+
// preview body keeps its flex-fill container (issue #111).
|
|
1588
|
+
STYLES,
|
|
1589
|
+
}
|
|
1489
1590
|
|
|
1490
1591
|
|
|
1491
1592
|
return module.exports
|
package/lib/media-route.js
CHANGED
|
@@ -84,8 +84,7 @@ async function serveRecordedFile(request, response, ctx, store) {
|
|
|
84
84
|
const sessionId = url.searchParams.get('sessionId')
|
|
85
85
|
const raw = url.searchParams.get('path')
|
|
86
86
|
assertMediaParams(sessionId, raw)
|
|
87
|
-
if (!isRecordedPath(store.state, sessionId, raw))
|
|
88
|
-
throw mediaError(403, "path is not in this session's file activity")
|
|
87
|
+
if (!isRecordedPath(store.state, sessionId, raw)) throw mediaError(403, "path is not in this session's file activity")
|
|
89
88
|
const abs = isAbsolute(raw) ? raw : join(sessionCwdOf(ctx, sessionId), raw)
|
|
90
89
|
if (url.searchParams.get('as') === 'text') {
|
|
91
90
|
await serveText(response, abs)
|
package/lib/parts/apply.part.js
CHANGED
|
@@ -68,7 +68,17 @@ exports.apply = function apply(ctx) {
|
|
|
68
68
|
// Stylesheet first, unconditionally (HMR pitfall — see injectStyles).
|
|
69
69
|
injectStyles(ctx)
|
|
70
70
|
const service = ctx.betterSidebar
|
|
71
|
-
if (service === undefined)
|
|
71
|
+
if (service === undefined) {
|
|
72
|
+
// 依赖缺失提示(issue #72 同类问题):dsh-file-activity 的 client 端
|
|
73
|
+
// 依赖 dsh-better-sidebar 提供侧边栏扩展点,未安装时静默返回会让用户
|
|
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
|
+
}
|
|
72
82
|
|
|
73
83
|
// Per-session data store: { bySession: { [sessionId]: { recent, counts, loading } }, preview }
|
|
74
84
|
// Each conversation reads/writes only its own bucket, so switching
|
|
@@ -86,4 +96,18 @@ exports.apply = function apply(ctx) {
|
|
|
86
96
|
|
|
87
97
|
// Internal functions exposed for the render-path test suite only; inert in
|
|
88
98
|
// the browser bundle (plain properties on the exports object).
|
|
89
|
-
exports.__test = {
|
|
99
|
+
exports.__test = {
|
|
100
|
+
loadFsReadContent,
|
|
101
|
+
fsReadError,
|
|
102
|
+
fetchTextContent,
|
|
103
|
+
textUrlOf,
|
|
104
|
+
strings,
|
|
105
|
+
renderPreviewBody,
|
|
106
|
+
previewClickAction,
|
|
107
|
+
isInsideFloating,
|
|
108
|
+
closePreviewOnHidden,
|
|
109
|
+
AUTO_CLOSE_MS,
|
|
110
|
+
// Static stylesheet text, so the render-path suite can assert the floating
|
|
111
|
+
// preview body keeps its flex-fill container (issue #111).
|
|
112
|
+
STYLES,
|
|
113
|
+
}
|
package/lib/parts/i18n.part.js
CHANGED
|
@@ -42,4 +42,6 @@ const strings = {
|
|
|
42
42
|
fileOutside: () =>
|
|
43
43
|
isZh() ? '文件位于工作区外,暂无法读取内容' : 'The file is outside the workspace and cannot be read',
|
|
44
44
|
downloadToView: () => (isZh() ? '下载查看' : 'download to view'),
|
|
45
|
+
clickOutsideToClose: () => (isZh() ? '点击外部关闭' : 'Click outside to close'),
|
|
46
|
+
autoCloseHint: () => (isZh() ? '预览失败,即将自动关闭' : 'Preview failed — closing automatically'),
|
|
45
47
|
}
|
|
@@ -26,6 +26,34 @@ function fsReadError(json, viewer) {
|
|
|
26
26
|
return { status: 'error', viewer, message }
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** Milliseconds after which an error-state preview closes itself (issue #76):
|
|
30
|
+
* a shell that failed to load any content is useless, so it must not linger
|
|
31
|
+
* over the main UI until the user finds the × button. */
|
|
32
|
+
const AUTO_CLOSE_MS = 2500
|
|
33
|
+
|
|
34
|
+
/** Whether a pointerdown target lies inside the floating window. The window
|
|
35
|
+
* surface carries the `.dfa-fp` class; anything else counts as "outside"
|
|
36
|
+
* and dismisses the preview (issue #76 — click anywhere outside closes). */
|
|
37
|
+
function isInsideFloating(target) {
|
|
38
|
+
if (!target || typeof target.closest !== 'function') return false
|
|
39
|
+
return target.closest('.dfa-fp') !== null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Click behavior for the window surface: in the error state ANY click
|
|
43
|
+
* closes the shell (there is no content to interact with), otherwise the
|
|
44
|
+
* click is swallowed so the viewer's own interactions keep working. */
|
|
45
|
+
function previewClickAction(load, event) {
|
|
46
|
+
if (load.status === 'error') return 'close'
|
|
47
|
+
if (event && event.stopPropagation) event.stopPropagation()
|
|
48
|
+
return 'stop'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Close the floating preview when the tab goes hidden (switching tabs /
|
|
52
|
+
* operating the main UI), so it never lingers over the interface. */
|
|
53
|
+
function closePreviewOnHidden(visible, dataStore) {
|
|
54
|
+
if (!visible) dataStore.set({ preview: null })
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
/**
|
|
30
58
|
* Load fsRead content through the sidebar API and resolve the viewer's
|
|
31
59
|
* load state (ready with text, or error with the API message). When the
|
|
@@ -125,6 +153,7 @@ function renderPreviewBody(load, ctx, store, scope, path, title, sessionId) {
|
|
|
125
153
|
load.message
|
|
126
154
|
? createElement('div', { style: { marginTop: '6px', fontSize: '11px', opacity: 0.85 } }, load.message)
|
|
127
155
|
: null,
|
|
156
|
+
createElement('div', { style: { marginTop: '6px', fontSize: '11px', opacity: 0.7 } }, strings.autoCloseHint()),
|
|
128
157
|
)
|
|
129
158
|
}
|
|
130
159
|
if (load.viewer.id === 'pdf') {
|
|
@@ -160,15 +189,25 @@ function renderPreviewBody(load, ctx, store, scope, path, title, sessionId) {
|
|
|
160
189
|
* built-in viewer that fetches its own URL internally (it ignores the
|
|
161
190
|
* `mediaUrl` prop), so it gets a small iframe preview instead.
|
|
162
191
|
*/
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Dismissal affordances (issue #76 — the preview must never linger):
|
|
194
|
+
* 1. pointerdown anywhere OUTSIDE the window closes it (capture phase, so
|
|
195
|
+
* it fires even if another element sits above the scrim);
|
|
196
|
+
* 2. the scrim's own onClick (kept as a fallback);
|
|
197
|
+
* 3. Escape;
|
|
198
|
+
* 4. the × button;
|
|
199
|
+
* 5. an error state closes itself after AUTO_CLOSE_MS (no content to show).
|
|
200
|
+
*/
|
|
201
|
+
function usePreviewDismiss(load, onClose) {
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return () => {}
|
|
204
|
+
const handler = (event) => {
|
|
205
|
+
if (!isInsideFloating(event?.target)) onClose()
|
|
206
|
+
}
|
|
207
|
+
document.addEventListener('pointerdown', handler, true)
|
|
208
|
+
return () => document.removeEventListener('pointerdown', handler, true)
|
|
209
|
+
}, [onClose])
|
|
169
210
|
|
|
170
|
-
// Clicking outside is the primary dismiss (the overlay's onClick);
|
|
171
|
-
// Escape is a keyboard affordance. Both call onClose.
|
|
172
211
|
useEffect(() => {
|
|
173
212
|
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return () => {}
|
|
174
213
|
const handler = (event) => {
|
|
@@ -178,6 +217,16 @@ function FloatingPreview({ ctx, store, scope, preview, onClose }) {
|
|
|
178
217
|
return () => document.removeEventListener('keydown', handler)
|
|
179
218
|
}, [onClose])
|
|
180
219
|
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (load.status !== 'error') return undefined
|
|
222
|
+
if (typeof window === 'undefined' || typeof window.setTimeout !== 'function') return undefined
|
|
223
|
+
const timer = window.setTimeout(onClose, AUTO_CLOSE_MS)
|
|
224
|
+
return () => window.clearTimeout(timer)
|
|
225
|
+
}, [load.status, onClose])
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** The floating window element: head (title + hint + close) and body. */
|
|
229
|
+
function renderFloatingWindow(load, ctx, store, scope, path, title, sessionId, onClose) {
|
|
181
230
|
return createElement(
|
|
182
231
|
'div',
|
|
183
232
|
{ className: 'dfa-fp-overlay', onClick: onClose },
|
|
@@ -186,13 +235,14 @@ function FloatingPreview({ ctx, store, scope, preview, onClose }) {
|
|
|
186
235
|
{
|
|
187
236
|
className: 'dfa-fp',
|
|
188
237
|
onClick: (event) => {
|
|
189
|
-
if (event
|
|
238
|
+
if (previewClickAction(load, event) === 'close') onClose()
|
|
190
239
|
},
|
|
191
240
|
},
|
|
192
241
|
createElement(
|
|
193
242
|
'div',
|
|
194
243
|
{ className: 'dfa-fp-head' },
|
|
195
244
|
createElement('span', { className: 'dfa-fp-title' }, title),
|
|
245
|
+
createElement('span', { className: 'dfa-fp-hint' }, strings.clickOutsideToClose()),
|
|
196
246
|
createElement(
|
|
197
247
|
'span',
|
|
198
248
|
{ className: 'dfa-fp-actions' },
|
|
@@ -217,6 +267,16 @@ function FloatingPreview({ ctx, store, scope, preview, onClose }) {
|
|
|
217
267
|
)
|
|
218
268
|
}
|
|
219
269
|
|
|
270
|
+
function FloatingPreview({ ctx, store, scope, preview, onClose }) {
|
|
271
|
+
const sessionId = scope?.sessionId ?? ''
|
|
272
|
+
const path = preview.abs
|
|
273
|
+
const title = preview.name
|
|
274
|
+
const service = ctx.betterSidebar
|
|
275
|
+
const load = usePreviewLoader(service, path, sessionId, scope)
|
|
276
|
+
usePreviewDismiss(load, onClose)
|
|
277
|
+
return renderFloatingWindow(load, ctx, store, scope, path, title, sessionId, onClose)
|
|
278
|
+
}
|
|
279
|
+
|
|
220
280
|
/**
|
|
221
281
|
* Lightweight PDF preview. better-sidebar's built-in PdfView fetches
|
|
222
282
|
* `/sidebar/file` internally (it ignores any injected `mediaUrl` prop),
|
package/lib/parts/styles.part.js
CHANGED
|
@@ -62,8 +62,18 @@ const STYLES = `
|
|
|
62
62
|
display:flex; flex-direction:column; overflow:hidden; }
|
|
63
63
|
.dfa-fp-head { display:flex; align-items:center; gap:6px; padding:6px 8px; border-bottom:1px solid var(--dsw-alias-border-l1); flex:none; }
|
|
64
64
|
.dfa-fp-title { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font:var(--dsw-font-s-strong-14); color:var(--dsw-alias-label-primary); }
|
|
65
|
+
.dfa-fp-hint { flex:none; font:var(--dsw-font-xxs-12); color:var(--dsw-alias-label-tertiary); opacity:0.8; }
|
|
65
66
|
.dfa-fp-actions { display:flex; align-items:center; gap:2px; flex:none; }
|
|
66
|
-
|
|
67
|
+
/* issue #111: the preview body is a FLEX COLUMN so the mounted viewer
|
|
68
|
+
component (better-sidebar's TextEditor for html/code/markdown, the image
|
|
69
|
+
wrap, and the pdf frame) actually fills the window. Their roots size via
|
|
70
|
+
flex:1 (html iframe .editorHtml, .editorCm, .editorMd, .editorImageWrap,
|
|
71
|
+
.dfa-pdf), which is IGNORED in a block context — so the HTML iframe, which
|
|
72
|
+
otherwise has a fixed browser-default height, rendered as a thin top strip
|
|
73
|
+
with the rest of the window dark. Making the body a flex column lets every
|
|
74
|
+
viewer's flex:1 child stretch to the full .dfa-fp-body height and scroll
|
|
75
|
+
internally, so content fills the window and follows window resizes. */
|
|
76
|
+
.dfa-fp-body { flex:1; display:flex; flex-direction:column; overflow:auto; padding:10px 12px; min-height:0; }
|
|
67
77
|
.dfa-fp-note { color:var(--dsw-alias-label-tertiary); font:var(--dsw-font-xxs-12); }
|
|
68
78
|
.dfa-fp-err { color:var(--dsw-alias-state-error-primary); font:var(--dsw-font-xxs-12); white-space:pre-wrap; word-break:break-all; }
|
|
69
79
|
/* PDF preview: a native browser PDF frame filled from the plugin's own media
|
package/lib/parts/view.part.js
CHANGED
|
@@ -158,6 +158,11 @@ function FileActivityView({ ctx, store, scope, visible, dataStore }) {
|
|
|
158
158
|
useEffect(() => {
|
|
159
159
|
dataStore.set({ preview: null })
|
|
160
160
|
}, [sessionId, dataStore])
|
|
161
|
+
// Switching tabs hides this view: close any floating preview so it never
|
|
162
|
+
// lingers over the main UI (issue #76).
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
closePreviewOnHidden(visible, dataStore)
|
|
165
|
+
}, [visible, dataStore])
|
|
161
166
|
const toggleDir = (path) => setCollapsedDirs((prev) => toggleInSet(prev, path))
|
|
162
167
|
const openPreview = (path) => dataStore.set({ preview: { abs: path, name: basenameOf(path) } })
|
|
163
168
|
const closePreview = () => dataStore.set({ preview: null })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-file-activity",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
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",
|
|
@@ -43,16 +43,15 @@
|
|
|
43
43
|
]
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"dsh-better-sidebar": ">=0.14.0 <0.18.0",
|
|
48
|
+
"dsh-shared": "^0.1.0"
|
|
49
|
+
},
|
|
46
50
|
"peerDependencies": {
|
|
47
51
|
"cordis": "^4.0.0-rc.8",
|
|
48
|
-
"
|
|
49
|
-
"react": "^18.2.0 || ^19.2.0",
|
|
50
|
-
"dsh-shared": "^0.1.0"
|
|
52
|
+
"react": "^18.2.0 || ^19.2.0"
|
|
51
53
|
},
|
|
52
54
|
"peerDependenciesMeta": {
|
|
53
|
-
"dsh-better-sidebar": {
|
|
54
|
-
"optional": true
|
|
55
|
-
},
|
|
56
55
|
"cordis": {
|
|
57
56
|
"optional": true
|
|
58
57
|
}
|