@yiln-dsh/dsh-plugin-file-explorer 0.4.0 → 0.5.0
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 +26 -34
- package/client.js +303 -284
- package/index.js +24 -4
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
# dsh-plugin-file-explorer
|
|
1
|
+
# @yiln-dsh/dsh-plugin-file-explorer
|
|
2
2
|
|
|
3
|
-
A DSH `dsh.bundle` that
|
|
4
|
-
|
|
3
|
+
A DSH `dsh.bundle` that contributes a workspace file explorer page to the
|
|
4
|
+
`@yiln-dsh/dsh-plugin-right-panel` host.
|
|
5
5
|
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **Files**
|
|
6
|
+
- Registers two independent right-panel pages: **Files** and **Git Graph**. The
|
|
7
|
+
host owns the right-column layout, navigation rail, width, and collapse
|
|
8
|
+
behavior.
|
|
9
|
+
- **Files** page: lists the active session's workspace directory, with folder
|
|
10
10
|
navigation, editable path input, parent/refresh buttons, file and folder
|
|
11
11
|
icons, and file sizes. File rows reveal view / download / delete buttons on
|
|
12
|
-
hover; delete uses a second-click confirm.
|
|
13
|
-
|
|
12
|
+
hover; delete uses a second-click confirm. Image View supports fullscreen,
|
|
13
|
+
25%-400% zoom, wheel zoom, and drag-to-pan after zooming.
|
|
14
|
+
- **Git Graph** page: a vscode/le-git-graph style commit graph rendered from
|
|
14
15
|
the repository containing the current directory —
|
|
15
16
|
- colored lane graph with commit dots and merge curves (all branches or the
|
|
16
17
|
current branch, refreshable),
|
|
@@ -21,35 +22,26 @@ GUI with a workspace file explorer.
|
|
|
21
22
|
A/M/D/R status badges,
|
|
22
23
|
- click a file to open its diff patch (commit shows `git show`, working tree
|
|
23
24
|
shows `git diff HEAD`) in a dialog.
|
|
24
|
-
- The
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
the plugin captures the shell layout store actions when the root entry wires
|
|
28
|
-
them, restores the saved width right after `openDetails()`, and saves the
|
|
29
|
-
column width after every resizer drag.
|
|
30
|
-
- **Collapse to icon bar**: a collapse button sits left of the header title
|
|
31
|
-
(chevrons pointing right, matching the DSH session menu's compact icon
|
|
32
|
-
style). Clicking it closes the details column and leaves a slim rail pinned
|
|
33
|
-
to the right edge with two icons — **Files** and **Git Graph** — clicking
|
|
34
|
-
either reopens the drawer on that tab at the last chosen width. The layout
|
|
35
|
-
keeps the slot subtree mounted at width 0, so the rail survives and the
|
|
36
|
-
panel state is preserved.
|
|
25
|
+
- The host keeps a real 56px right-edge icon rail when collapsed. Other
|
|
26
|
+
right-panel pages can appear alongside Files and overflow into the host's
|
|
27
|
+
searchable More menu.
|
|
37
28
|
|
|
38
29
|
## Layout
|
|
39
30
|
|
|
40
31
|
| File | Content |
|
|
41
32
|
| --- | --- |
|
|
42
33
|
| `index.js` | Host half: registers exact `/ _dsh/file-explorer/*` routes backed by the Host `fs`/`subprocess` services, plus read-only git routes (`/ _dsh/file-explorer/git-log`, `git-commit`, `git-diff`, `git-status`) that run `git` with machine-readable separators and return JSON only. |
|
|
43
|
-
| `client.js` | Client half:
|
|
34
|
+
| `client.js` | Client half: registers independent keyed `right-panel.page` entries for Files and Git Graph. |
|
|
44
35
|
| `cordis.patch.yml` | Composition patch that mounts the host row — declared with `inject: [webServer]`, so it activates only after the stock webserver service (`127.0.0.1`) is up. |
|
|
45
36
|
|
|
46
37
|
## Install
|
|
47
38
|
|
|
48
|
-
The
|
|
39
|
+
The package version is `@yiln-dsh/dsh-plugin-file-explorer@0.5.0`.
|
|
49
40
|
|
|
50
|
-
|
|
41
|
+
The right-panel package must be installed in the same `web` profile:
|
|
51
42
|
|
|
52
43
|
```bash
|
|
44
|
+
dsh plugin --profile web add file:/path/to/dsh-plugin-right-panel
|
|
53
45
|
dsh plugin --profile web add file:/path/to/dsh-plugin-file-explorer
|
|
54
46
|
```
|
|
55
47
|
|
|
@@ -61,18 +53,18 @@ pnpm pack
|
|
|
61
53
|
```
|
|
62
54
|
|
|
63
55
|
```bash
|
|
64
|
-
dsh plugin --profile web add ./dsh-plugin-file-explorer-0.
|
|
56
|
+
dsh plugin --profile web add ./yiln-dsh-dsh-plugin-file-explorer-0.5.0.tgz
|
|
65
57
|
```
|
|
66
58
|
|
|
67
59
|
### npm package
|
|
68
60
|
|
|
69
61
|
```bash
|
|
70
62
|
cd /path/to/dsh-plugin-file-explorer
|
|
71
|
-
npm publish
|
|
63
|
+
npm publish --access public
|
|
72
64
|
```
|
|
73
65
|
|
|
74
66
|
```bash
|
|
75
|
-
dsh plugin --profile web add dsh-plugin-file-explorer
|
|
67
|
+
dsh plugin --profile web add @yiln-dsh/dsh-plugin-file-explorer@latest
|
|
76
68
|
```
|
|
77
69
|
|
|
78
70
|
The profile must be the `web` profile. The host row serves the API routes and
|
|
@@ -83,8 +75,9 @@ apply the new profile composition.
|
|
|
83
75
|
|
|
84
76
|
- The Host falls back to `sandboxPolicy.workspaceRoot` when no path is passed,
|
|
85
77
|
and returns `{ ok, path, parent, entries }` JSON — never live objects.
|
|
86
|
-
- `read` previews up to
|
|
87
|
-
|
|
78
|
+
- `read` previews up to 1 MiB of UTF-8 text or 16 MiB of recognized images;
|
|
79
|
+
image previews return a data URL for inline rendering. `download` returns a
|
|
80
|
+
base64 data URL (64 MiB cap) that the browser triggers with `<a download>`.
|
|
88
81
|
- `delete` removes regular files (`rm -f`) and directories recursively
|
|
89
82
|
(`rm -rf`); both are called only after a second-click confirm in the UI.
|
|
90
83
|
- Git routes are read-only. They resolve the repository root with
|
|
@@ -97,7 +90,6 @@ apply the new profile composition.
|
|
|
97
90
|
sentinel) and file paths against a non-option, non-control-character check.
|
|
98
91
|
- Rows have a fixed 34px height, so the hover action swap never changes the
|
|
99
92
|
row height; directories reveal a delete-only action set on hover.
|
|
100
|
-
- The browser module
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
`useSessions` hook, and its own stylesheet.
|
|
93
|
+
- The browser module registers `file-explorer.files` and
|
|
94
|
+
`file-explorer.git` into the keyed `right-panel.page` Slot and uses the
|
|
95
|
+
standard session `useSessions` hook.
|
package/client.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-plugin-file-explorer — browser bundle (static DSH client module).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the conversation and keeps the native resizer handle.
|
|
4
|
+
* Registers the file explorer as a page in the `right-panel` host. The host
|
|
5
|
+
* owns the right layout column, width, navigation rail, and page switching.
|
|
7
6
|
*
|
|
8
|
-
* Two views behind the
|
|
7
|
+
* Two views behind the page's local tabs:
|
|
9
8
|
* - Files: directory browser (list / read / download / delete via the
|
|
10
9
|
* /_dsh/file-explorer API).
|
|
11
10
|
* - Git Graph: a vscode/git-graph style commit DAG with colored lanes, ref
|
|
@@ -20,12 +19,12 @@ window.__ModuleLoader__.load({
|
|
|
20
19
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
21
20
|
let React = require("react");
|
|
22
21
|
|
|
23
|
-
const inject = ["slots", "
|
|
22
|
+
const inject = ["slots", "rightPanel"];
|
|
24
23
|
|
|
25
24
|
function apply(ctx) {
|
|
26
25
|
const slots = ctx.get('slots')
|
|
27
|
-
const
|
|
28
|
-
if (slots === undefined) return
|
|
26
|
+
const rightPanel = ctx.get('rightPanel')
|
|
27
|
+
if (slots === undefined || rightPanel === undefined) return
|
|
29
28
|
|
|
30
29
|
const style = document.createElement("style");
|
|
31
30
|
style.id = "dsh-plugin-file-explorer-style";
|
|
@@ -77,35 +76,6 @@ window.__ModuleLoader__.load({
|
|
|
77
76
|
.dsh-fe-icon:hover {
|
|
78
77
|
background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2, #e7e5e4));
|
|
79
78
|
}
|
|
80
|
-
.dsh-fe-tabs {
|
|
81
|
-
display: flex;
|
|
82
|
-
gap: 2px;
|
|
83
|
-
padding: 8px 12px 0;
|
|
84
|
-
}
|
|
85
|
-
.dsh-fe-tab {
|
|
86
|
-
flex: 1 1 0;
|
|
87
|
-
height: 26px;
|
|
88
|
-
border: 0;
|
|
89
|
-
border-radius: 6px;
|
|
90
|
-
background: transparent;
|
|
91
|
-
color: var(--dsw-alias-label-secondary, #4b5563);
|
|
92
|
-
cursor: pointer;
|
|
93
|
-
font: inherit;
|
|
94
|
-
font-size: 12px;
|
|
95
|
-
display: inline-flex;
|
|
96
|
-
align-items: center;
|
|
97
|
-
justify-content: center;
|
|
98
|
-
gap: 6px;
|
|
99
|
-
}
|
|
100
|
-
.dsh-fe-tab:hover {
|
|
101
|
-
background: var(--dsw-alias-bg-layer-2, #e7e5e4);
|
|
102
|
-
}
|
|
103
|
-
.dsh-fe-tab-active,
|
|
104
|
-
.dsh-fe-tab-active:hover {
|
|
105
|
-
background: var(--dsw-alias-bg-layer-2, #e7e5e4);
|
|
106
|
-
color: var(--dsw-alias-label-primary, #111827);
|
|
107
|
-
font-weight: 600;
|
|
108
|
-
}
|
|
109
79
|
.dsh-fe-path {
|
|
110
80
|
min-width: 0;
|
|
111
81
|
margin: 8px 12px 0;
|
|
@@ -298,6 +268,14 @@ window.__ModuleLoader__.load({
|
|
|
298
268
|
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.22);
|
|
299
269
|
overflow: hidden;
|
|
300
270
|
}
|
|
271
|
+
.dsh-fe-modal-fullscreen {
|
|
272
|
+
width: 100vw;
|
|
273
|
+
height: 100vh;
|
|
274
|
+
max-width: none;
|
|
275
|
+
max-height: none;
|
|
276
|
+
border: 0;
|
|
277
|
+
border-radius: 0;
|
|
278
|
+
}
|
|
301
279
|
.dsh-fe-modal-head {
|
|
302
280
|
display: flex;
|
|
303
281
|
align-items: center;
|
|
@@ -319,6 +297,29 @@ window.__ModuleLoader__.load({
|
|
|
319
297
|
color: var(--dsw-alias-label-secondary, #6b7280);
|
|
320
298
|
font-size: 12px;
|
|
321
299
|
}
|
|
300
|
+
.dsh-fe-image-tools {
|
|
301
|
+
flex: 0 0 auto;
|
|
302
|
+
display: inline-flex;
|
|
303
|
+
align-items: center;
|
|
304
|
+
gap: 2px;
|
|
305
|
+
}
|
|
306
|
+
.dsh-fe-image-tool {
|
|
307
|
+
width: 28px;
|
|
308
|
+
height: 28px;
|
|
309
|
+
padding: 0;
|
|
310
|
+
}
|
|
311
|
+
.dsh-fe-image-tool:disabled {
|
|
312
|
+
opacity: 0.35;
|
|
313
|
+
cursor: default;
|
|
314
|
+
}
|
|
315
|
+
.dsh-fe-image-zoom-label {
|
|
316
|
+
display: inline-block;
|
|
317
|
+
min-width: 40px;
|
|
318
|
+
color: var(--dsw-alias-label-secondary, #6b7280);
|
|
319
|
+
font-size: 11px;
|
|
320
|
+
text-align: center;
|
|
321
|
+
white-space: nowrap;
|
|
322
|
+
}
|
|
322
323
|
.dsh-fe-modal-body {
|
|
323
324
|
flex: 1 1 auto;
|
|
324
325
|
min-height: 0;
|
|
@@ -331,6 +332,47 @@ window.__ModuleLoader__.load({
|
|
|
331
332
|
font-size: 12px;
|
|
332
333
|
line-height: 18px;
|
|
333
334
|
}
|
|
335
|
+
.dsh-fe-modal-body-image {
|
|
336
|
+
display: flex;
|
|
337
|
+
align-items: stretch;
|
|
338
|
+
justify-content: stretch;
|
|
339
|
+
min-height: 160px;
|
|
340
|
+
padding: 0;
|
|
341
|
+
overflow: hidden;
|
|
342
|
+
background: var(--dsw-alias-bg-base, #f5f5f4);
|
|
343
|
+
white-space: normal;
|
|
344
|
+
}
|
|
345
|
+
.dsh-fe-image-stage {
|
|
346
|
+
flex: 1 1 auto;
|
|
347
|
+
min-width: 0;
|
|
348
|
+
min-height: 0;
|
|
349
|
+
display: flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
justify-content: center;
|
|
352
|
+
overflow: hidden;
|
|
353
|
+
touch-action: none;
|
|
354
|
+
}
|
|
355
|
+
.dsh-fe-preview-image {
|
|
356
|
+
display: block;
|
|
357
|
+
max-width: 100%;
|
|
358
|
+
max-height: 60vh;
|
|
359
|
+
width: auto;
|
|
360
|
+
height: auto;
|
|
361
|
+
object-fit: contain;
|
|
362
|
+
user-select: none;
|
|
363
|
+
-webkit-user-drag: none;
|
|
364
|
+
transition: transform 120ms ease;
|
|
365
|
+
}
|
|
366
|
+
.dsh-fe-modal-fullscreen .dsh-fe-preview-image {
|
|
367
|
+
max-height: calc(100vh - 64px);
|
|
368
|
+
}
|
|
369
|
+
.dsh-fe-preview-image-pannable {
|
|
370
|
+
cursor: grab;
|
|
371
|
+
}
|
|
372
|
+
.dsh-fe-preview-image-dragging {
|
|
373
|
+
cursor: grabbing;
|
|
374
|
+
transition: none;
|
|
375
|
+
}
|
|
334
376
|
.dsh-git-bar {
|
|
335
377
|
display: flex;
|
|
336
378
|
align-items: center;
|
|
@@ -515,154 +557,19 @@ window.__ModuleLoader__.load({
|
|
|
515
557
|
font-size: 12px;
|
|
516
558
|
unicode-bidi: plaintext;
|
|
517
559
|
}
|
|
518
|
-
.dsh-fe-
|
|
560
|
+
.dsh-fe-page {
|
|
519
561
|
height: 100%;
|
|
520
|
-
|
|
521
|
-
box-sizing: border-box;
|
|
562
|
+
min-height: 0;
|
|
522
563
|
display: flex;
|
|
523
564
|
flex-direction: column;
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
padding: 10px 6px;
|
|
565
|
+
box-sizing: border-box;
|
|
566
|
+
overflow: hidden;
|
|
527
567
|
background: var(--dsw-specific-sidebar-fill, var(--dsw-alias-bg-base, #f5f5f4));
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
/* While collapsed, keep a real RAIL_WIDTH column in the layout (like the left
|
|
531
|
-
sidebar's 56px rail) instead of a floating overlay. This !important rule
|
|
532
|
-
overrides React's inline grid-template-columns on the app frame; the data
|
|
533
|
-
attribute is added by applyRail() and removed on expand/unload. */
|
|
534
|
-
[data-dsh-fe-rail] {
|
|
535
|
-
grid-template-columns: var(--dsh-fe-rail-sidebar, 280px) minmax(0, 1fr) 56px !important;
|
|
536
|
-
}
|
|
537
|
-
.dsh-fe-rail-button {
|
|
538
|
-
width: 36px;
|
|
539
|
-
height: 36px;
|
|
540
|
-
border: 0;
|
|
541
|
-
border-radius: 8px;
|
|
542
|
-
background: transparent;
|
|
543
|
-
color: var(--dsw-alias-label-secondary, #4b5563);
|
|
544
|
-
cursor: pointer;
|
|
545
|
-
display: inline-flex;
|
|
546
|
-
align-items: center;
|
|
547
|
-
justify-content: center;
|
|
548
|
-
}
|
|
549
|
-
.dsh-fe-rail-button:hover {
|
|
550
|
-
background: var(--dsw-alias-bg-layer-2, #e7e5e4);
|
|
551
|
-
color: var(--dsw-alias-brand-primary, #4f46e5);
|
|
568
|
+
color: var(--dsw-alias-label-primary, #111827);
|
|
552
569
|
}
|
|
553
570
|
`;
|
|
554
571
|
document.head.append(style);
|
|
555
572
|
|
|
556
|
-
// --- details-column width persistence ---------------------------------
|
|
557
|
-
// The shell's layout store is transient: openDetails() resets the column
|
|
558
|
-
// to its 360px contract default on every mount and session switch. The
|
|
559
|
-
// bound store actions reach the LayoutController after every client
|
|
560
|
-
// module has applied (the root entry's inject hook fires on first
|
|
561
|
-
// render and again on re-register), so wrapping attachPanels on the
|
|
562
|
-
// prototype captures them reliably. setDetails then restores the last
|
|
563
|
-
// user-chosen width, and a document-level pointerup listener persists
|
|
564
|
-
// the column width after each resizer drag.
|
|
565
|
-
const WIDTH_KEY = "dsh-plugin-file-explorer.detailsWidth";
|
|
566
|
-
const WIDTH_MIN = 300; // the layout contract's clamp floor
|
|
567
|
-
let panelActions = null;
|
|
568
|
-
let savedWidth = 0;
|
|
569
|
-
try { savedWidth = Number(window.localStorage.getItem(WIDTH_KEY)) || 0 } catch (_e) { }
|
|
570
|
-
|
|
571
|
-
const findDetailsColumn = () => {
|
|
572
|
-
// The details column's own class is the reliable anchor: the drag
|
|
573
|
-
// handle's previousElementSibling is the sidebar handle or the
|
|
574
|
-
// overlay layer, never the column itself.
|
|
575
|
-
const byClass = document.querySelector('[class*="detailsCol"]')
|
|
576
|
-
if (byClass !== null) return byClass
|
|
577
|
-
const handle = document.querySelector('[data-side="details"]')
|
|
578
|
-
if (handle !== null && handle.previousElementSibling !== null) return handle.previousElementSibling
|
|
579
|
-
return null
|
|
580
|
-
}
|
|
581
|
-
const persistWidth = () => {
|
|
582
|
-
const column = findDetailsColumn()
|
|
583
|
-
if (column === null) return
|
|
584
|
-
const width = Math.round(column.getBoundingClientRect().width)
|
|
585
|
-
if (width < WIDTH_MIN) return
|
|
586
|
-
savedWidth = width
|
|
587
|
-
try { window.localStorage.setItem(WIDTH_KEY, String(width)) } catch (_e) { }
|
|
588
|
-
}
|
|
589
|
-
const onDocumentPointerUp = () => { persistWidth() }
|
|
590
|
-
document.addEventListener("pointerup", onDocumentPointerUp, true)
|
|
591
|
-
ctx.effect(() => () => document.removeEventListener("pointerup", onDocumentPointerUp, true))
|
|
592
|
-
|
|
593
|
-
if (layout !== undefined) {
|
|
594
|
-
const proto = Object.getPrototypeOf(layout)
|
|
595
|
-
if (proto !== null && typeof proto.attachPanels === "function") {
|
|
596
|
-
const originalAttach = proto.attachPanels
|
|
597
|
-
proto.attachPanels = function (actions) {
|
|
598
|
-
panelActions = actions
|
|
599
|
-
return originalAttach.call(this, actions)
|
|
600
|
-
}
|
|
601
|
-
ctx.effect(() => () => { proto.attachPanels = originalAttach })
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
const restoreWidth = () => {
|
|
605
|
-
if (savedWidth < WIDTH_MIN) return
|
|
606
|
-
if (panelActions === null || typeof panelActions.setDetails !== "function") return
|
|
607
|
-
try { panelActions.setDetails(savedWidth) } catch (_e) { }
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// --- collapsed rail as a real layout column --------------------------
|
|
611
|
-
// The layout contract gives the sidebar a 56px rail when closed but
|
|
612
|
-
// closes the details column to 0px (no rail). To collapse like the left
|
|
613
|
-
// sidebar, keep the store closed (no drag handle / border) and force the
|
|
614
|
-
// frame's third grid track to RAIL_WIDTH via an !important stylesheet
|
|
615
|
-
// rule keyed on a data attribute we own. CSS !important beats React's
|
|
616
|
-
// inline grid style, so there is no fight with React re-renders: set the
|
|
617
|
-
// attribute to collapse, remove it to expand. A MutationObserver only
|
|
618
|
-
// refreshes the sidebar-width variable (the first track) when the app
|
|
619
|
-
// re-renders the frame.
|
|
620
|
-
const RAIL_WIDTH = 56;
|
|
621
|
-
let railActive = false;
|
|
622
|
-
const frameOfDetails = () => {
|
|
623
|
-
const column = findDetailsColumn();
|
|
624
|
-
if (column === null || column.parentElement === null) return null;
|
|
625
|
-
return column.parentElement;
|
|
626
|
-
};
|
|
627
|
-
const syncRailVar = () => {
|
|
628
|
-
const frame = frameOfDetails();
|
|
629
|
-
if (frame === null) return;
|
|
630
|
-
const sideCol = frame.children.length > 0 ? frame.children[0] : null;
|
|
631
|
-
const sidebarPx = sideCol !== null ? Math.round(sideCol.getBoundingClientRect().width) : 280;
|
|
632
|
-
try { frame.style.setProperty("--dsh-fe-rail-sidebar", `${sidebarPx}px`) } catch (_e) { }
|
|
633
|
-
};
|
|
634
|
-
const applyRail = () => {
|
|
635
|
-
const frame = frameOfDetails();
|
|
636
|
-
if (frame === null) return;
|
|
637
|
-
if (railActive) {
|
|
638
|
-
frame.setAttribute("data-dsh-fe-rail", "");
|
|
639
|
-
syncRailVar();
|
|
640
|
-
} else {
|
|
641
|
-
frame.removeAttribute("data-dsh-fe-rail");
|
|
642
|
-
}
|
|
643
|
-
};
|
|
644
|
-
const releaseRail = () => {
|
|
645
|
-
railActive = false;
|
|
646
|
-
applyRail();
|
|
647
|
-
};
|
|
648
|
-
let railObserver = null;
|
|
649
|
-
if (typeof MutationObserver === "function") {
|
|
650
|
-
railObserver = new MutationObserver(() => {
|
|
651
|
-
if (railActive) syncRailVar();
|
|
652
|
-
});
|
|
653
|
-
ctx.effect(() => {
|
|
654
|
-
const frame = frameOfDetails();
|
|
655
|
-
if (frame !== null) railObserver.observe(frame, { attributes: true, attributeFilter: ["style"] });
|
|
656
|
-
return () => {
|
|
657
|
-
railObserver?.disconnect();
|
|
658
|
-
const frame = frameOfDetails();
|
|
659
|
-
if (frame !== null) frame.removeAttribute("data-dsh-fe-rail");
|
|
660
|
-
railActive = false;
|
|
661
|
-
};
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
// -----------------------------------------------------------------------
|
|
665
|
-
|
|
666
573
|
async function api(method, payload) {
|
|
667
574
|
const options = { headers: { "content-type": "application/json" } };
|
|
668
575
|
options.method = "POST";
|
|
@@ -726,6 +633,29 @@ window.__ModuleLoader__.load({
|
|
|
726
633
|
React.createElement('line', { x1: '18', y1: '6', x2: '6', y2: '18' }),
|
|
727
634
|
React.createElement('line', { x1: '6', y1: '6', x2: '18', y2: '18' }),
|
|
728
635
|
)
|
|
636
|
+
const zoomOutIcon = React.createElement(React.Fragment, null,
|
|
637
|
+
React.createElement('circle', { cx: '10.5', cy: '10.5', r: '6.5' }),
|
|
638
|
+
React.createElement('line', { x1: '15.5', y1: '15.5', x2: '21', y2: '21' }),
|
|
639
|
+
React.createElement('line', { x1: '8', y1: '10.5', x2: '13', y2: '10.5' }),
|
|
640
|
+
)
|
|
641
|
+
const zoomInIcon = React.createElement(React.Fragment, null,
|
|
642
|
+
React.createElement('circle', { cx: '10.5', cy: '10.5', r: '6.5' }),
|
|
643
|
+
React.createElement('line', { x1: '15.5', y1: '15.5', x2: '21', y2: '21' }),
|
|
644
|
+
React.createElement('line', { x1: '8', y1: '10.5', x2: '13', y2: '10.5' }),
|
|
645
|
+
React.createElement('line', { x1: '10.5', y1: '8', x2: '10.5', y2: '13' }),
|
|
646
|
+
)
|
|
647
|
+
const fullscreenIcon = React.createElement(React.Fragment, null,
|
|
648
|
+
React.createElement('polyline', { points: '8 3 3 3 3 8' }),
|
|
649
|
+
React.createElement('polyline', { points: '16 3 21 3 21 8' }),
|
|
650
|
+
React.createElement('polyline', { points: '8 21 3 21 3 16' }),
|
|
651
|
+
React.createElement('polyline', { points: '16 21 21 21 21 16' }),
|
|
652
|
+
)
|
|
653
|
+
const fullscreenExitIcon = React.createElement(React.Fragment, null,
|
|
654
|
+
React.createElement('polyline', { points: '9 3 9 9 3 9' }),
|
|
655
|
+
React.createElement('polyline', { points: '15 3 15 9 21 9' }),
|
|
656
|
+
React.createElement('polyline', { points: '9 21 9 15 3 15' }),
|
|
657
|
+
React.createElement('polyline', { points: '15 21 15 15 21 15' }),
|
|
658
|
+
)
|
|
729
659
|
// Mirror of DeepSeek's sidebar IconPanelLeftOutline16 (spine on the
|
|
730
660
|
// right): this panel lives on the right edge, so the glyph mirrors the
|
|
731
661
|
// left sidebar's, signaling collapse into the right-edge icon rail.
|
|
@@ -1236,7 +1166,7 @@ window.__ModuleLoader__.load({
|
|
|
1236
1166
|
)
|
|
1237
1167
|
}
|
|
1238
1168
|
|
|
1239
|
-
function
|
|
1169
|
+
function FileExplorerPage(props) {
|
|
1240
1170
|
if (typeof props.useSessions !== 'function') return null
|
|
1241
1171
|
|
|
1242
1172
|
const sessionId = props.useSessions((state) => {
|
|
@@ -1263,24 +1193,17 @@ window.__ModuleLoader__.load({
|
|
|
1263
1193
|
const [draftPath, setDraftPath] = React.useState('')
|
|
1264
1194
|
const [preview, setPreview] = React.useState(null)
|
|
1265
1195
|
const [previewLoading, setPreviewLoading] = React.useState(false)
|
|
1196
|
+
const [imageScale, setImageScale] = React.useState(1)
|
|
1197
|
+
const [imageOffset, setImageOffset] = React.useState({ x: 0, y: 0 })
|
|
1198
|
+
const [imageFullscreen, setImageFullscreen] = React.useState(false)
|
|
1199
|
+
const [imageDragging, setImageDragging] = React.useState(false)
|
|
1200
|
+
const imageDrag = React.useRef(null)
|
|
1266
1201
|
const [pendingDelete, setPendingDelete] = React.useState(null)
|
|
1267
|
-
const [view, setView] = React.useState('files')
|
|
1268
|
-
const [collapsed, setCollapsed] = React.useState(false)
|
|
1269
|
-
|
|
1270
|
-
React.useEffect(() => {
|
|
1271
|
-
if (sessionId === undefined) return
|
|
1272
|
-
setCollapsed(false)
|
|
1273
|
-
if (layout !== undefined) {
|
|
1274
|
-
try { layout.openDetails() } catch (_e) { }
|
|
1275
|
-
}
|
|
1276
|
-
// Same-tick store updates batch in React, so restoring right after
|
|
1277
|
-
// openDetails() replaces the 360px default without a visible jump.
|
|
1278
|
-
restoreWidth()
|
|
1279
|
-
}, [sessionId])
|
|
1280
1202
|
|
|
1281
1203
|
React.useEffect(() => {
|
|
1282
1204
|
if (sessionId === undefined) {
|
|
1283
|
-
|
|
1205
|
+
setParentPath(null)
|
|
1206
|
+
setRequestPath('')
|
|
1284
1207
|
return
|
|
1285
1208
|
}
|
|
1286
1209
|
setParentPath(null)
|
|
@@ -1332,34 +1255,6 @@ window.__ModuleLoader__.load({
|
|
|
1332
1255
|
return aDir - bDir || a.name.localeCompare(b.name)
|
|
1333
1256
|
})
|
|
1334
1257
|
const isRoot = parentPath === null || parentPath === currentPath
|
|
1335
|
-
// Collapse into a slim icon rail that keeps a real 56px column in
|
|
1336
|
-
// the layout (like the left sidebar's rail): the details store stays
|
|
1337
|
-
// closed (no drag handle / border), and applyRail() toggles the
|
|
1338
|
-
// [data-dsh-fe-rail] attribute so the !important stylesheet rule
|
|
1339
|
-
// overrides the frame's third grid track. A MutationObserver keeps
|
|
1340
|
-
// the sidebar-width variable fresh across app re-renders.
|
|
1341
|
-
const collapse = () => {
|
|
1342
|
-
setCollapsed(true)
|
|
1343
|
-
railActive = true
|
|
1344
|
-
setPreview(null)
|
|
1345
|
-
setPendingDelete(null)
|
|
1346
|
-
setEditingPath(false)
|
|
1347
|
-
if (layout !== undefined) {
|
|
1348
|
-
try { layout.closeDetails() } catch (_e) { }
|
|
1349
|
-
}
|
|
1350
|
-
applyRail()
|
|
1351
|
-
}
|
|
1352
|
-
const expand = (nextView) => {
|
|
1353
|
-
releaseRail()
|
|
1354
|
-
setCollapsed(false)
|
|
1355
|
-
setView(nextView)
|
|
1356
|
-
if (layout !== undefined) {
|
|
1357
|
-
try { layout.openDetails() } catch (_e) { }
|
|
1358
|
-
}
|
|
1359
|
-
// Same-tick store updates batch in React, so restoring right after
|
|
1360
|
-
// openDetails() replaces the 360px default without a visible jump.
|
|
1361
|
-
restoreWidth()
|
|
1362
|
-
}
|
|
1363
1258
|
const reload = () => { setPendingDelete(null); setReloadTick((tick) => tick + 1) }
|
|
1364
1259
|
const startEditPath = () => {
|
|
1365
1260
|
setDraftPath(currentPath)
|
|
@@ -1373,8 +1268,67 @@ window.__ModuleLoader__.load({
|
|
|
1373
1268
|
setRequestPath(next)
|
|
1374
1269
|
}
|
|
1375
1270
|
|
|
1271
|
+
const resetImageZoom = () => {
|
|
1272
|
+
imageDrag.current = null
|
|
1273
|
+
setImageDragging(false)
|
|
1274
|
+
setImageScale(1)
|
|
1275
|
+
setImageOffset({ x: 0, y: 0 })
|
|
1276
|
+
}
|
|
1277
|
+
const resetImageView = () => {
|
|
1278
|
+
resetImageZoom()
|
|
1279
|
+
setImageFullscreen(false)
|
|
1280
|
+
}
|
|
1281
|
+
const closePreview = () => {
|
|
1282
|
+
resetImageView()
|
|
1283
|
+
setPreview(null)
|
|
1284
|
+
}
|
|
1285
|
+
const setImageZoom = (value) => {
|
|
1286
|
+
const next = Math.max(0.25, Math.min(4, Math.round(value * 4) / 4))
|
|
1287
|
+
setImageScale(next)
|
|
1288
|
+
if (next <= 1) setImageOffset({ x: 0, y: 0 })
|
|
1289
|
+
}
|
|
1290
|
+
const changeImageZoom = (delta) => setImageZoom(imageScale + delta)
|
|
1291
|
+
const handleImageWheel = (event) => {
|
|
1292
|
+
event.preventDefault()
|
|
1293
|
+
changeImageZoom(event.deltaY < 0 ? 0.25 : -0.25)
|
|
1294
|
+
}
|
|
1295
|
+
const startImageDrag = (event) => {
|
|
1296
|
+
if (imageScale <= 1 || event.button !== 0) return
|
|
1297
|
+
imageDrag.current = {
|
|
1298
|
+
pointerId: event.pointerId,
|
|
1299
|
+
startX: event.clientX,
|
|
1300
|
+
startY: event.clientY,
|
|
1301
|
+
originX: imageOffset.x,
|
|
1302
|
+
originY: imageOffset.y,
|
|
1303
|
+
}
|
|
1304
|
+
try { event.currentTarget.setPointerCapture(event.pointerId) } catch (_e) { }
|
|
1305
|
+
setImageDragging(true)
|
|
1306
|
+
}
|
|
1307
|
+
const moveImageDrag = (event) => {
|
|
1308
|
+
const drag = imageDrag.current
|
|
1309
|
+
if (!drag || drag.pointerId !== event.pointerId) return
|
|
1310
|
+
setImageOffset({
|
|
1311
|
+
x: drag.originX + event.clientX - drag.startX,
|
|
1312
|
+
y: drag.originY + event.clientY - drag.startY,
|
|
1313
|
+
})
|
|
1314
|
+
}
|
|
1315
|
+
const endImageDrag = (event) => {
|
|
1316
|
+
const drag = imageDrag.current
|
|
1317
|
+
if (!drag || drag.pointerId !== event.pointerId) return
|
|
1318
|
+
imageDrag.current = null
|
|
1319
|
+
setImageDragging(false)
|
|
1320
|
+
try { event.currentTarget.releasePointerCapture(event.pointerId) } catch (_e) { }
|
|
1321
|
+
}
|
|
1322
|
+
const handlePreviewKeyDown = (event) => {
|
|
1323
|
+
if (event.key !== 'Escape') return
|
|
1324
|
+
event.stopPropagation()
|
|
1325
|
+
if (imageFullscreen) setImageFullscreen(false)
|
|
1326
|
+
else closePreview()
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1376
1329
|
const openPreview = (entry) => {
|
|
1377
1330
|
setPendingDelete(null)
|
|
1331
|
+
resetImageView()
|
|
1378
1332
|
setPreviewLoading(true)
|
|
1379
1333
|
setPreview(null)
|
|
1380
1334
|
api('read', { path: entry.path })
|
|
@@ -1519,103 +1473,168 @@ window.__ModuleLoader__.load({
|
|
|
1519
1473
|
),
|
|
1520
1474
|
)
|
|
1521
1475
|
|
|
1522
|
-
// Collapsed: a slim icon rail (file / git) that keeps a real 56px
|
|
1523
|
-
// column in the layout (the [data-dsh-fe-rail] !important grid rule;
|
|
1524
|
-
// see applyRail above), mirroring the left sidebar's collapsed rail.
|
|
1525
|
-
// Placed after every handler declaration: a collapsed render must not
|
|
1526
|
-
// return early and leave `expand` uninitialized (TDZ error on click).
|
|
1527
|
-
if (collapsed) {
|
|
1528
|
-
return React.createElement('div', {
|
|
1529
|
-
className: 'dsh-fe-rail',
|
|
1530
|
-
role: 'region',
|
|
1531
|
-
'aria-label': 'File explorer (collapsed)',
|
|
1532
|
-
},
|
|
1533
|
-
React.createElement('button', {
|
|
1534
|
-
type: 'button',
|
|
1535
|
-
className: 'dsh-fe-rail-button',
|
|
1536
|
-
onClick: () => expand('files'),
|
|
1537
|
-
title: 'Files',
|
|
1538
|
-
'aria-label': 'Open file explorer',
|
|
1539
|
-
}, svgIcon(folderIcon, 16)),
|
|
1540
|
-
React.createElement('button', {
|
|
1541
|
-
type: 'button',
|
|
1542
|
-
className: 'dsh-fe-rail-button',
|
|
1543
|
-
onClick: () => expand('git'),
|
|
1544
|
-
title: 'Git Graph',
|
|
1545
|
-
'aria-label': 'Open git graph',
|
|
1546
|
-
}, svgIcon(branchIcon, 16)),
|
|
1547
|
-
)
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
1476
|
return React.createElement(React.Fragment, null,
|
|
1551
1477
|
React.createElement('div', {
|
|
1552
|
-
className: 'dsh-fe-
|
|
1478
|
+
className: 'dsh-fe-page',
|
|
1553
1479
|
role: 'region',
|
|
1554
|
-
'aria-label': '
|
|
1480
|
+
'aria-label': 'Files',
|
|
1555
1481
|
},
|
|
1556
|
-
|
|
1557
|
-
React.createElement('button', {
|
|
1558
|
-
type: 'button',
|
|
1559
|
-
className: 'dsh-fe-icon',
|
|
1560
|
-
onClick: collapse,
|
|
1561
|
-
'aria-label': 'Collapse file explorer into icon bar',
|
|
1562
|
-
title: 'Collapse',
|
|
1563
|
-
}, collapseIcon),
|
|
1564
|
-
React.createElement('div', { className: 'dsh-fe-title' }, view === 'git' ? 'Git Graph' : 'Files'),
|
|
1565
|
-
),
|
|
1566
|
-
React.createElement('div', { className: 'dsh-fe-tabs' },
|
|
1567
|
-
React.createElement('button', {
|
|
1568
|
-
type: 'button',
|
|
1569
|
-
className: 'dsh-fe-tab' + (view === 'files' ? ' dsh-fe-tab-active' : ''),
|
|
1570
|
-
onClick: () => setView('files'),
|
|
1571
|
-
}, svgIcon(folderIcon, 13), 'Files'),
|
|
1572
|
-
React.createElement('button', {
|
|
1573
|
-
type: 'button',
|
|
1574
|
-
className: 'dsh-fe-tab' + (view === 'git' ? ' dsh-fe-tab-active' : ''),
|
|
1575
|
-
onClick: () => setView('git'),
|
|
1576
|
-
}, svgIcon(branchIcon, 13), 'Git Graph'),
|
|
1577
|
-
),
|
|
1578
|
-
view === 'git'
|
|
1579
|
-
? React.createElement(GitGraphView, { api, cwd })
|
|
1580
|
-
: filesView,
|
|
1482
|
+
filesView,
|
|
1581
1483
|
),
|
|
1582
1484
|
preview &&
|
|
1583
1485
|
React.createElement('div', {
|
|
1584
1486
|
className: 'dsh-fe-overlay',
|
|
1585
|
-
onClick:
|
|
1487
|
+
onClick: closePreview,
|
|
1586
1488
|
},
|
|
1587
1489
|
React.createElement('div', {
|
|
1588
|
-
className: 'dsh-fe-modal',
|
|
1490
|
+
className: preview.kind === 'image' && imageFullscreen ? 'dsh-fe-modal dsh-fe-modal-fullscreen' : 'dsh-fe-modal',
|
|
1589
1491
|
role: 'dialog',
|
|
1590
1492
|
'aria-label': 'File preview',
|
|
1493
|
+
'aria-modal': true,
|
|
1494
|
+
tabIndex: -1,
|
|
1495
|
+
onKeyDown: handlePreviewKeyDown,
|
|
1591
1496
|
onClick: (event) => event.stopPropagation(),
|
|
1592
1497
|
},
|
|
1593
1498
|
React.createElement('div', { className: 'dsh-fe-modal-head' },
|
|
1594
1499
|
React.createElement('div', { className: 'dsh-fe-modal-title' }, preview.name || 'Preview'),
|
|
1595
|
-
React.createElement('div', { className: 'dsh-fe-modal-meta' }, preview.binary ? 'Binary' : formatSize(preview.size)),
|
|
1500
|
+
React.createElement('div', { className: 'dsh-fe-modal-meta' }, preview.kind === 'image' ? `Image / ${formatSize(preview.size)}` : preview.binary ? 'Binary' : formatSize(preview.size)),
|
|
1501
|
+
preview.kind === 'image' && preview.dataUrl
|
|
1502
|
+
? React.createElement('div', {
|
|
1503
|
+
className: 'dsh-fe-image-tools',
|
|
1504
|
+
role: 'toolbar',
|
|
1505
|
+
'aria-label': 'Image controls',
|
|
1506
|
+
},
|
|
1507
|
+
React.createElement('button', {
|
|
1508
|
+
type: 'button',
|
|
1509
|
+
className: 'dsh-fe-icon dsh-fe-image-tool',
|
|
1510
|
+
disabled: imageScale <= 0.25,
|
|
1511
|
+
onClick: () => changeImageZoom(-0.25),
|
|
1512
|
+
'aria-label': 'Zoom out',
|
|
1513
|
+
title: 'Zoom out',
|
|
1514
|
+
}, svgIcon(zoomOutIcon, 14)),
|
|
1515
|
+
React.createElement('button', {
|
|
1516
|
+
type: 'button',
|
|
1517
|
+
className: 'dsh-fe-icon dsh-fe-image-tool',
|
|
1518
|
+
onClick: resetImageZoom,
|
|
1519
|
+
'aria-label': 'Reset image zoom',
|
|
1520
|
+
title: 'Reset zoom',
|
|
1521
|
+
}, React.createElement('span', { className: 'dsh-fe-image-zoom-label' }, `${Math.round(imageScale * 100)}%`)),
|
|
1522
|
+
React.createElement('button', {
|
|
1523
|
+
type: 'button',
|
|
1524
|
+
className: 'dsh-fe-icon dsh-fe-image-tool',
|
|
1525
|
+
disabled: imageScale >= 4,
|
|
1526
|
+
onClick: () => changeImageZoom(0.25),
|
|
1527
|
+
'aria-label': 'Zoom in',
|
|
1528
|
+
title: 'Zoom in',
|
|
1529
|
+
}, svgIcon(zoomInIcon, 14)),
|
|
1530
|
+
React.createElement('button', {
|
|
1531
|
+
type: 'button',
|
|
1532
|
+
className: 'dsh-fe-icon dsh-fe-image-tool',
|
|
1533
|
+
onClick: () => setImageFullscreen((value) => !value),
|
|
1534
|
+
'aria-label': imageFullscreen ? 'Exit fullscreen' : 'Enter fullscreen',
|
|
1535
|
+
title: imageFullscreen ? 'Exit fullscreen' : 'Fullscreen',
|
|
1536
|
+
}, svgIcon(imageFullscreen ? fullscreenExitIcon : fullscreenIcon, 14)),
|
|
1537
|
+
)
|
|
1538
|
+
: null,
|
|
1596
1539
|
React.createElement('button', {
|
|
1597
1540
|
type: 'button',
|
|
1598
1541
|
className: 'dsh-fe-icon',
|
|
1599
|
-
onClick:
|
|
1542
|
+
onClick: closePreview,
|
|
1600
1543
|
'aria-label': 'Close preview',
|
|
1601
1544
|
}, svgIcon(closeIcon, 14)),
|
|
1602
1545
|
),
|
|
1603
|
-
React.createElement('div', {
|
|
1546
|
+
React.createElement('div', {
|
|
1547
|
+
className: preview.kind === 'image' ? 'dsh-fe-modal-body dsh-fe-modal-body-image' : 'dsh-fe-modal-body',
|
|
1548
|
+
onWheel: preview.kind === 'image' && preview.dataUrl ? handleImageWheel : undefined,
|
|
1549
|
+
onPointerDown: preview.kind === 'image' && preview.dataUrl ? startImageDrag : undefined,
|
|
1550
|
+
onPointerMove: preview.kind === 'image' && preview.dataUrl ? moveImageDrag : undefined,
|
|
1551
|
+
onPointerUp: preview.kind === 'image' && preview.dataUrl ? endImageDrag : undefined,
|
|
1552
|
+
onPointerCancel: preview.kind === 'image' && preview.dataUrl ? endImageDrag : undefined,
|
|
1553
|
+
},
|
|
1604
1554
|
previewLoading
|
|
1605
1555
|
? 'Loading...'
|
|
1606
1556
|
: preview.error
|
|
1607
1557
|
? preview.error
|
|
1608
|
-
: preview.
|
|
1558
|
+
: preview.kind === 'image'
|
|
1559
|
+
? preview.dataUrl
|
|
1560
|
+
? React.createElement('div', { className: 'dsh-fe-image-stage' },
|
|
1561
|
+
React.createElement('img', {
|
|
1562
|
+
className: 'dsh-fe-preview-image' + (imageScale > 1 ? ' dsh-fe-preview-image-pannable' : '') + (imageDragging ? ' dsh-fe-preview-image-dragging' : ''),
|
|
1563
|
+
style: { transform: `translate3d(${imageOffset.x}px, ${imageOffset.y}px, 0) scale(${imageScale})` },
|
|
1564
|
+
src: preview.dataUrl,
|
|
1565
|
+
alt: preview.name || 'Image preview',
|
|
1566
|
+
draggable: false,
|
|
1567
|
+
})
|
|
1568
|
+
)
|
|
1569
|
+
: preview.tooLarge ? 'Image is too large to preview here' : '(No preview)'
|
|
1570
|
+
: preview.text !== undefined && preview.text !== null
|
|
1571
|
+
? preview.text
|
|
1572
|
+
: preview.tooLarge
|
|
1573
|
+
? 'File is too large to preview here'
|
|
1574
|
+
: preview.binary ? 'Binary file cannot be previewed' : '(No preview)',
|
|
1609
1575
|
),
|
|
1610
1576
|
),
|
|
1611
1577
|
),
|
|
1612
1578
|
)
|
|
1613
1579
|
}
|
|
1614
1580
|
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1581
|
+
function FileExplorerGitPage(props) {
|
|
1582
|
+
if (typeof props.useSessions !== 'function') return null
|
|
1583
|
+
|
|
1584
|
+
const sessionId = props.useSessions((state) => {
|
|
1585
|
+
const current = state.current
|
|
1586
|
+
if (current === undefined) return undefined
|
|
1587
|
+
const row = state.byId[current]
|
|
1588
|
+
if (!row || row.blank === true) return undefined
|
|
1589
|
+
return current
|
|
1590
|
+
})
|
|
1591
|
+
const cwd = props.useSessions((state) => {
|
|
1592
|
+
const current = state.current
|
|
1593
|
+
if (current === undefined) return undefined
|
|
1594
|
+
const row = state.byId[current]
|
|
1595
|
+
return row ? row.cwd : undefined
|
|
1596
|
+
})
|
|
1597
|
+
|
|
1598
|
+
if (sessionId === undefined) return null
|
|
1599
|
+
return React.createElement('div', {
|
|
1600
|
+
className: 'dsh-fe-page',
|
|
1601
|
+
role: 'region',
|
|
1602
|
+
'aria-label': 'Git Graph',
|
|
1603
|
+
}, React.createElement(GitGraphView, { api, cwd }))
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
ctx.effect(() => {
|
|
1607
|
+
const disposeFilesPage = rightPanel.registerPage({
|
|
1608
|
+
id: 'file-explorer.files',
|
|
1609
|
+
title: 'Files',
|
|
1610
|
+
group: 'Workspace',
|
|
1611
|
+
order: 20,
|
|
1612
|
+
placement: 'rail',
|
|
1613
|
+
icon: (size) => svgIcon(folderIcon, size),
|
|
1614
|
+
})
|
|
1615
|
+
const disposeGitPage = rightPanel.registerPage({
|
|
1616
|
+
id: 'file-explorer.git',
|
|
1617
|
+
title: 'Git Graph',
|
|
1618
|
+
group: 'Workspace',
|
|
1619
|
+
order: 21,
|
|
1620
|
+
placement: 'rail',
|
|
1621
|
+
icon: (size) => svgIcon(branchIcon, size),
|
|
1622
|
+
})
|
|
1623
|
+
const disposeFilesSlot = slots.inject('right-panel.page', () => slots.register({
|
|
1624
|
+
name: 'right-panel.page',
|
|
1625
|
+
key: 'file-explorer.files',
|
|
1626
|
+
}, FileExplorerPage))
|
|
1627
|
+
const disposeGitSlot = slots.inject('right-panel.page', () => slots.register({
|
|
1628
|
+
name: 'right-panel.page',
|
|
1629
|
+
key: 'file-explorer.git',
|
|
1630
|
+
}, FileExplorerGitPage))
|
|
1631
|
+
return () => {
|
|
1632
|
+
disposeFilesSlot()
|
|
1633
|
+
disposeGitSlot()
|
|
1634
|
+
disposeFilesPage()
|
|
1635
|
+
disposeGitPage()
|
|
1636
|
+
}
|
|
1637
|
+
})
|
|
1619
1638
|
}
|
|
1620
1639
|
|
|
1621
1640
|
exports.apply = apply;
|
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* dsh-plugin-file-explorer — Host half (static DSH bundle).
|
|
3
3
|
*
|
|
4
4
|
* Registers exact HTTP routes under /_dsh/file-explorer for the browser
|
|
5
|
-
* bundle: list (with parent path), read (text preview), download (data URL),
|
|
5
|
+
* bundle: list (with parent path), read (text/image preview), download (data URL),
|
|
6
6
|
* and delete.
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -145,12 +145,32 @@ export function apply(ctx) {
|
|
|
145
145
|
const target = await fs.resolve(req.path)
|
|
146
146
|
const info = await fs.stat(target)
|
|
147
147
|
const size = info && typeof info.size === 'number' ? info.size : null
|
|
148
|
-
const
|
|
148
|
+
const name = target.displayPath.split('/').pop()
|
|
149
|
+
const mime = guessMime(name)
|
|
150
|
+
const isImage = mime.startsWith('image/')
|
|
151
|
+
const limit = isImage ? 16 * 1024 * 1024 : 1024 * 1024
|
|
149
152
|
if (size !== null && size > limit) {
|
|
150
|
-
sendJson(res, 200, {
|
|
153
|
+
sendJson(res, 200, {
|
|
154
|
+
ok: true,
|
|
155
|
+
name,
|
|
156
|
+
size,
|
|
157
|
+
tooLarge: true,
|
|
158
|
+
...(isImage ? { kind: 'image', mime } : {}),
|
|
159
|
+
})
|
|
151
160
|
return
|
|
152
161
|
}
|
|
153
162
|
const bytes = await fs.readBytes(target, undefined, limit)
|
|
163
|
+
if (isImage) {
|
|
164
|
+
sendJson(res, 200, {
|
|
165
|
+
ok: true,
|
|
166
|
+
name,
|
|
167
|
+
size,
|
|
168
|
+
kind: 'image',
|
|
169
|
+
mime,
|
|
170
|
+
dataUrl: `data:${mime};base64,${bytesToBase64(bytes)}`,
|
|
171
|
+
})
|
|
172
|
+
return
|
|
173
|
+
}
|
|
154
174
|
const text = new TextDecoder('utf-8', { fatal: false }).decode(bytes)
|
|
155
175
|
let binary = false
|
|
156
176
|
for (let i = 0; i < bytes.length; i += 1) {
|
|
@@ -159,7 +179,7 @@ export function apply(ctx) {
|
|
|
159
179
|
if (!binary && text.replace(/\uFFFD/g, '').length * 10 < text.length * 9) binary = true
|
|
160
180
|
sendJson(res, 200, {
|
|
161
181
|
ok: true,
|
|
162
|
-
name
|
|
182
|
+
name,
|
|
163
183
|
size,
|
|
164
184
|
text: binary ? null : text,
|
|
165
185
|
binary,
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yiln-dsh/dsh-plugin-file-explorer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"description": "DSH bundle plugin that
|
|
7
|
+
"description": "DSH bundle plugin that contributes a workspace file explorer page to the right-panel host.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "index.js",
|
|
10
10
|
"exports": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"client": {
|
|
20
20
|
"inject": [
|
|
21
|
+
"@yiln-dsh/dsh-plugin-right-panel",
|
|
21
22
|
"@deepseek-ai/dsh-client-runtime",
|
|
22
23
|
"@deepseek-ai/dsh-client-ui-layout",
|
|
23
24
|
"@deepseek-ai/dsh-client-ui-renderer"
|
|
@@ -35,7 +36,8 @@
|
|
|
35
36
|
"@deepseek-ai/cordis": ">=4.0.0",
|
|
36
37
|
"@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.0",
|
|
37
38
|
"@deepseek-ai/dsh-client-ui-layout": ">=0.1.0-rc.0",
|
|
38
|
-
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.0-rc.0"
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.0-rc.0",
|
|
40
|
+
"@yiln-dsh/dsh-plugin-right-panel": ">=0.1.0"
|
|
39
41
|
},
|
|
40
42
|
"engines": {
|
|
41
43
|
"node": ">=22"
|