@weniatall/dsh-file-viewer 0.2.7 → 0.2.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/README.md +4 -0
- package/lib/client.js +125 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,8 @@ dsh plugin --profile web add <release-tgz-url>
|
|
|
46
46
|
- **会话中点击文件**:在会话回复(变更文件卡片、产物交付卡片、行内代码引用等)中点击任何文件路径,自动唤起文件查看抽屉并直接选中预览该文件。
|
|
47
47
|
- `Esc` 或点击抽屉外部遮罩关闭。
|
|
48
48
|
|
|
49
|
+
**桌面端接管说明**:桌面版宿主判定自己能在系统里打开路径,因此会在文件路径与更改卡片旁额外渲染原生「在应用中打开」按钮,更改卡片还会走 `/api/changes.open` 这条直接交给操作系统的路由。插件在这两处都会先接管,改为唤起文件查看抽屉:原生打开按钮按 `data-open-path-open` 识别,更改卡片打开路由则从 URL 里的 `sessionId / seq / index` 回查文件路径。取不到路径时一律放行,仍按宿主原方式打开,不会出现点了没反应。
|
|
50
|
+
|
|
49
51
|
顶部可切换工作区、刷新、显示或隐藏点文件。目录在树中原地展开,子级首次展开时才加载。
|
|
50
52
|
|
|
51
53
|
文件树支持**右键上下文菜单**与单项 `@` 快捷按钮:
|
|
@@ -59,6 +61,8 @@ dsh plugin --profile web add <release-tgz-url>
|
|
|
59
61
|
|
|
60
62
|
手机端抽屉占满全屏,文件列表与内容单列切换(左上角 `‹` 返回列表)。
|
|
61
63
|
|
|
64
|
+
桌面端(Windows)抽屉会自动避让窗口右上角的原生标题栏按钮(最小化 / 最大化 / 关闭):宿主在 `<html>` 上标记 `data-windows-titlebar` 并给出 `--dsh-windows-titlebar-height`,抽屉顶部从其下方开始,因此标题栏里的图标不会再压在窗口按钮下面;窗口进入全屏后该条带消失,抽屉相应贴到顶边。
|
|
65
|
+
|
|
62
66
|
## 访问边界与安全访问路径
|
|
63
67
|
|
|
64
68
|
插件**内置可访问**所有**已注册的工作区目录**以及**各会话的工作目录**。
|
package/lib/client.js
CHANGED
|
@@ -302,6 +302,15 @@ window.__ModuleLoader__.load({
|
|
|
302
302
|
.fv-scrim{position:absolute;inset:0;z-index:40;display:flex;align-items:stretch;justify-content:flex-end;background:transparent;pointer-events:auto;color-scheme:light dark;animation:fv-fade .16s ease-out}
|
|
303
303
|
.fv-shell{display:flex;flex-direction:column;width:min(64vw,1100px);min-width:0;min-height:0;overflow:hidden;border-left:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#fff));box-shadow:var(--dsw-shadow-lv3);font-family:var(--dsw-font-family);color:var(--dsw-alias-label-primary);animation:fv-slide .2s cubic-bezier(.2,.8,.2,1);transition:width .2s cubic-bezier(.2,.8,.2,1)}
|
|
304
304
|
.fv-shell[data-fullscreen="true"]{width:100vw;border-left:none}
|
|
305
|
+
/* Windows desktop paints the native caption buttons (minimize, maximize,
|
|
306
|
+
close) in a strip across the top of the window, above every page layer.
|
|
307
|
+
The shell's overlay seat spans the whole frame — padding box included —
|
|
308
|
+
so a trailing-edge drawer would start underneath that strip and its
|
|
309
|
+
header controls could never be clicked. Start the drawer below the
|
|
310
|
+
strip instead, which also leaves the strip free to drag the window.
|
|
311
|
+
The host sets data-fullscreen while the window is fullscreen and the
|
|
312
|
+
strip is hidden, so the offset has to drop then. */
|
|
313
|
+
html[data-windows-titlebar]:not([data-fullscreen]) .fv-scrim{top:var(--dsh-windows-titlebar-height,40px)}
|
|
305
314
|
@keyframes fv-fade{from{opacity:0}to{opacity:1}}
|
|
306
315
|
@keyframes fv-slide{from{transform:translateX(100%)}to{transform:translateX(0)}}
|
|
307
316
|
@media(prefers-reduced-motion:reduce){.fv-scrim,.fv-shell{animation:none}}
|
|
@@ -861,10 +870,27 @@ window.__ModuleLoader__.load({
|
|
|
861
870
|
// Guard so a hot reload or re-apply never double-wraps shared open
|
|
862
871
|
// handlers; older hosts used workspaces.openPath.
|
|
863
872
|
const wiredOpenPath = typeof WeakSet !== "undefined" ? new WeakSet() : null;
|
|
873
|
+
// The wrappers below outlive a single apply. They were closing over the
|
|
874
|
+
// store of whichever apply installed them, so a second apply left the Host
|
|
875
|
+
// calling the previous store: the click was swallowed, the drawer never
|
|
876
|
+
// opened, and the file viewer looked like it had stopped listening. These
|
|
877
|
+
// pointers are refreshed on every apply so the wrappers always write the
|
|
878
|
+
// store the mounted overlay is actually subscribed to.
|
|
879
|
+
let liveOpenStore = null;
|
|
880
|
+
let liveSessionStore = null;
|
|
864
881
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
882
|
+
/**
|
|
883
|
+
* Open the drawer on one path.
|
|
884
|
+
*
|
|
885
|
+
* `sessionId` is optional: a `dsh-resource://file/session/<id>/…` address
|
|
886
|
+
* names the session it belongs to, and honouring it keeps the drawer on that
|
|
887
|
+
* session's project instead of whatever session happens to be current.
|
|
888
|
+
*/
|
|
889
|
+
function openViewerForPath(openStore, sessionStore, path, sessionId) {
|
|
890
|
+
const target = liveOpenStore || openStore;
|
|
891
|
+
const session = liveSessionStore || sessionStore;
|
|
892
|
+
const sid = sessionId !== undefined && sessionId !== "" ? sessionId : (session ? session.get() : undefined);
|
|
893
|
+
target.set({ filePath: path, sessionId: sid, _t: Date.now() });
|
|
868
894
|
}
|
|
869
895
|
|
|
870
896
|
function parseFileResource(address) {
|
|
@@ -909,8 +935,7 @@ window.__ModuleLoader__.load({
|
|
|
909
935
|
sr.openResource = function(address, options) {
|
|
910
936
|
const parsed = parseFileResource(address);
|
|
911
937
|
if (parsed && parsed.path) {
|
|
912
|
-
|
|
913
|
-
openStore.set({ filePath: parsed.path, sessionId: sid, _t: Date.now() });
|
|
938
|
+
openViewerForPath(openStore, sessionStore, parsed.path, parsed.sessionId);
|
|
914
939
|
return;
|
|
915
940
|
}
|
|
916
941
|
const review = parseChangesReviewResource(address);
|
|
@@ -924,7 +949,7 @@ window.__ModuleLoader__.load({
|
|
|
924
949
|
.then((data) => {
|
|
925
950
|
const file = data?.files?.[idx]?.path || data?.files?.[0]?.path;
|
|
926
951
|
if (file) {
|
|
927
|
-
openStore
|
|
952
|
+
openViewerForPath(openStore, sessionStore, file);
|
|
928
953
|
}
|
|
929
954
|
})
|
|
930
955
|
.catch(() => {});
|
|
@@ -937,8 +962,7 @@ window.__ModuleLoader__.load({
|
|
|
937
962
|
sr.openResourceIn = function(sessionId, address, options) {
|
|
938
963
|
const parsed = parseFileResource(address);
|
|
939
964
|
if (parsed && parsed.path) {
|
|
940
|
-
|
|
941
|
-
openStore.set({ filePath: parsed.path, sessionId: sid, _t: Date.now() });
|
|
965
|
+
openViewerForPath(openStore, sessionStore, parsed.path, sessionId || parsed.sessionId);
|
|
942
966
|
return;
|
|
943
967
|
}
|
|
944
968
|
const review = parseChangesReviewResource(address);
|
|
@@ -951,7 +975,7 @@ window.__ModuleLoader__.load({
|
|
|
951
975
|
.then((data) => {
|
|
952
976
|
const file = data?.files?.[idx]?.path || data?.files?.[0]?.path;
|
|
953
977
|
if (file) {
|
|
954
|
-
openStore
|
|
978
|
+
openViewerForPath(openStore, sessionStore, file, sid);
|
|
955
979
|
}
|
|
956
980
|
})
|
|
957
981
|
.catch(() => {});
|
|
@@ -991,6 +1015,63 @@ window.__ModuleLoader__.load({
|
|
|
991
1015
|
};
|
|
992
1016
|
}
|
|
993
1017
|
|
|
1018
|
+
/**
|
|
1019
|
+
* Take over the Desktop Host's changed-file open route.
|
|
1020
|
+
*
|
|
1021
|
+
* The changes card's native open control does not go through the RPC
|
|
1022
|
+
* carrier: it POSTs `/api/changes.open?sessionId=…&seq=…&index=…`, which
|
|
1023
|
+
* hands the file to the operating system. Nothing in the RPC path can see
|
|
1024
|
+
* that call, so on Desktop the changes card opened the system editor while
|
|
1025
|
+
* the Web build (where the control is not mounted at all) opened the
|
|
1026
|
+
* viewer. Resolving the same coordinates through the summary route gives the
|
|
1027
|
+
* path, and a synthetic OK response keeps the Host's card state settled.
|
|
1028
|
+
*/
|
|
1029
|
+
function wrapChangesOpenFetch(openStore, sessionStore) {
|
|
1030
|
+
if (typeof window === "undefined" || typeof window.fetch !== "function") return;
|
|
1031
|
+
const originalFetch = window.fetch;
|
|
1032
|
+
if (wiredOpenPath === null || wiredOpenPath.has(originalFetch)) return;
|
|
1033
|
+
wiredOpenPath.add(originalFetch);
|
|
1034
|
+
|
|
1035
|
+
const summaryPath = (sessionId, seq) =>
|
|
1036
|
+
`/api/changes.summary?${new URLSearchParams({ sessionId, seq: String(seq) })}`;
|
|
1037
|
+
|
|
1038
|
+
window.fetch = function(input, init) {
|
|
1039
|
+
const requestUrl = typeof input === "string" ? input : (input && typeof input.url === "string" ? input.url : "");
|
|
1040
|
+
const method = String((init && init.method) || (input && input.method) || "GET").toUpperCase();
|
|
1041
|
+
if (method !== "POST" || !/\/api\/changes\.open\b/.test(requestUrl)) {
|
|
1042
|
+
return originalFetch.apply(this, arguments);
|
|
1043
|
+
}
|
|
1044
|
+
let coordinates;
|
|
1045
|
+
try {
|
|
1046
|
+
const query = new URLSearchParams(requestUrl.split("?")[1] || "");
|
|
1047
|
+
coordinates = {
|
|
1048
|
+
sessionId: query.get("sessionId") || "",
|
|
1049
|
+
seq: Number(query.get("seq")),
|
|
1050
|
+
index: Number(query.get("index") || 0),
|
|
1051
|
+
};
|
|
1052
|
+
} catch (_) {
|
|
1053
|
+
coordinates = null;
|
|
1054
|
+
}
|
|
1055
|
+
if (!coordinates || !coordinates.sessionId || !Number.isFinite(coordinates.seq)) {
|
|
1056
|
+
return originalFetch.apply(this, arguments);
|
|
1057
|
+
}
|
|
1058
|
+
const { sessionId, seq, index } = coordinates;
|
|
1059
|
+
return originalFetch(summaryPath(sessionId, seq), { signal: init && init.signal })
|
|
1060
|
+
.then((response) => (response && response.ok ? response.json() : null))
|
|
1061
|
+
.then((data) => {
|
|
1062
|
+
const files = Array.isArray(data && data.files) ? data.files : [];
|
|
1063
|
+
const entry = files[Number.isFinite(index) ? index : 0] || files[0];
|
|
1064
|
+
const file = entry && entry.path;
|
|
1065
|
+
if (!file) return originalFetch.apply(this, arguments);
|
|
1066
|
+
openViewerForPath(openStore, sessionStore, file, sessionId);
|
|
1067
|
+
// The Host only reads `response.ok`; a bodiless 204 settles its card
|
|
1068
|
+
// without claiming the OS actually opened anything.
|
|
1069
|
+
return new Response(null, { status: 204 });
|
|
1070
|
+
})
|
|
1071
|
+
.catch(() => originalFetch.apply(this, arguments));
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
994
1075
|
const COMMON_FILE_EXTENSIONS = new Set([
|
|
995
1076
|
"go", "ts", "js", "tsx", "jsx", "mjs", "cjs", "py", "java", "c", "cpp", "h", "hpp", "rs", "php", "rb", "swift", "kt", "scala",
|
|
996
1077
|
"md", "markdown", "txt", "json", "jsonc", "yaml", "yml", "toml", "xml", "html", "css", "scss", "less", "vue", "svelte",
|
|
@@ -1104,6 +1185,25 @@ window.__ModuleLoader__.load({
|
|
|
1104
1185
|
}
|
|
1105
1186
|
}
|
|
1106
1187
|
|
|
1188
|
+
// 3. The Host's native open control carries no path of its own: its label
|
|
1189
|
+
// is the action ("Open", "Open in …"), never the file. The path is
|
|
1190
|
+
// published by the surrounding row in a hidden description span, so
|
|
1191
|
+
// walk up a few levels and take the first span that holds a path. The
|
|
1192
|
+
// walk is bounded and every candidate still has to pass the path
|
|
1193
|
+
// shape check, so a miss falls through to the Host rather than opening
|
|
1194
|
+
// the wrong file.
|
|
1195
|
+
if (element.closest && element.closest("[data-open-path-open]")) {
|
|
1196
|
+
let scope = element.parentElement;
|
|
1197
|
+
for (let depth = 0; scope && depth < 4; depth += 1) {
|
|
1198
|
+
const hiddenPath = scope.querySelector ? scope.querySelector("span[hidden]") : null;
|
|
1199
|
+
if (hiddenPath && hiddenPath.textContent) {
|
|
1200
|
+
const p = normalizeClickedPath(hiddenPath.textContent);
|
|
1201
|
+
if (p) return p;
|
|
1202
|
+
}
|
|
1203
|
+
scope = scope.parentElement;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1107
1207
|
const sources = [
|
|
1108
1208
|
element.getAttribute("data-file-path"),
|
|
1109
1209
|
element.getAttribute("data-path"),
|
|
@@ -1121,6 +1221,13 @@ window.__ModuleLoader__.load({
|
|
|
1121
1221
|
function isFileOpenControl(element) {
|
|
1122
1222
|
if (!element) return false;
|
|
1123
1223
|
if (element.hasAttribute("data-file-path") || element.hasAttribute("data-path")) return true;
|
|
1224
|
+
// The Desktop Host answers `canOpenWorkspacePath` with true, so the shell
|
|
1225
|
+
// mounts its native "open in application" button beside every file path
|
|
1226
|
+
// (the changes card included). That button sits above the path control and
|
|
1227
|
+
// takes the click, so it has to count as a file control or the viewer
|
|
1228
|
+
// never sees the gesture. The `-more` chevron is a menu of applications,
|
|
1229
|
+
// not a plain open, so it is deliberately left to the Host.
|
|
1230
|
+
if (element.closest("[data-open-path-open]")) return true;
|
|
1124
1231
|
if (element.closest("[data-produced-files-row]")) return true;
|
|
1125
1232
|
// Support deliverables: changed files card (ChangedFiles) and presented file cards (PresentedFileCard)
|
|
1126
1233
|
if (element.closest("[data-changed-files], [data-presented-file], [data-presented-files-row]")) return true;
|
|
@@ -1484,6 +1591,11 @@ window.__ModuleLoader__.load({
|
|
|
1484
1591
|
// Which session the conversation header currently belongs to. The phone
|
|
1485
1592
|
// entry is drawn outside that header and cannot receive its props.
|
|
1486
1593
|
const sessionStore = createStore(undefined);
|
|
1594
|
+
// Publish this apply's stores to the Host wrappers. A re-apply installs new
|
|
1595
|
+
// stores but the wrappers are already in place, so they must follow the
|
|
1596
|
+
// ones the freshly mounted overlay subscribes to.
|
|
1597
|
+
liveOpenStore = openStore;
|
|
1598
|
+
liveSessionStore = sessionStore;
|
|
1487
1599
|
// The composer's draft accessors, captured from the slot that receives
|
|
1488
1600
|
// them. The drawer renders in a different subtree and cannot reach the
|
|
1489
1601
|
// input state through props.
|
|
@@ -3385,6 +3497,9 @@ window.__ModuleLoader__.load({
|
|
|
3385
3497
|
// Intercept any openWorkspacePath RPC calls (connection is declared in inject)
|
|
3386
3498
|
wrapConnectionRpc(ctx, openStore, sessionStore);
|
|
3387
3499
|
|
|
3500
|
+
// The changes card's native open route bypasses the RPC carrier entirely.
|
|
3501
|
+
wrapChangesOpenFetch(openStore, sessionStore);
|
|
3502
|
+
|
|
3388
3503
|
// Wrap sidebarRight if already registered or whenever injected. Direct
|
|
3389
3504
|
// property access is not safe here: sidebarRight is not part of this
|
|
3390
3505
|
// plugin's inject, so cordis would reject the read outright.
|
|
@@ -3432,7 +3547,7 @@ window.__ModuleLoader__.load({
|
|
|
3432
3547
|
LANG_OVERRIDES_KEY, LANG_OVERRIDES_MAX, readLangOverrides, writeLangOverrides, readLangOverride, writeLangOverride,
|
|
3433
3548
|
langOverrideKey, isKnownLanguageId, setLanguageCatalog, getLanguageCatalog: () => languageCatalog, LanguagePicker,
|
|
3434
3549
|
MARKDOWN_LABELS, MARKDOWN_CODE_LABELS, READ_BLOCK_LABELS, JSON_TREE_LABELS, DIFF_BLOCK_LABELS, ErrorBoundary,
|
|
3435
|
-
openViewerForPath, wrapWorkspaceOpenPath, wrapConnectionRpc, wrapSidebarRight, parseFileResource, setupGlobalFileClickInterceptor, extractClickedPath, normalizeClickedPath, looksLikeFilePath, isFileOpenControl, isFileMentionCode, renderRootOptions,
|
|
3550
|
+
openViewerForPath, wrapWorkspaceOpenPath, wrapConnectionRpc, wrapChangesOpenFetch, wrapSidebarRight, parseFileResource, setupGlobalFileClickInterceptor, extractClickedPath, normalizeClickedPath, looksLikeFilePath, isFileOpenControl, isFileMentionCode, renderRootOptions,
|
|
3436
3551
|
triggerDownload, performDownload, SvgIconDownload,
|
|
3437
3552
|
};
|
|
3438
3553
|
return module.exports;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weniatall/dsh-file-viewer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Workspace file viewer for DeepSeek Harness: file tree, syntax-highlighted code with language detection and manual override, images, PDF, Excel and Word",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|