@yiln-dsh/dsh-plugin-file-explorer 0.4.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/client.js ADDED
@@ -0,0 +1,1625 @@
1
+ /**
2
+ * dsh-plugin-file-explorer — browser bundle (static DSH client module).
3
+ *
4
+ * Renders the file explorer into the `details` slot (right layout column),
5
+ * replacing the native tool-details panel while running. The column squeezes
6
+ * the conversation and keeps the native resizer handle.
7
+ *
8
+ * Two views behind the header tabs:
9
+ * - Files: directory browser (list / read / download / delete via the
10
+ * /_dsh/file-explorer API).
11
+ * - Git Graph: a vscode/git-graph style commit DAG with colored lanes, ref
12
+ * pills, a working-tree row, expandable commit details, and per-file diffs
13
+ * (via the /_dsh/file-explorer/git-* API).
14
+ */
15
+ window.__ModuleLoader__.load({
16
+ id: "@yiln-dsh/dsh-plugin-file-explorer",
17
+ factory: (require) => {
18
+ var module = { exports: {} };
19
+ var exports = module.exports;
20
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
21
+ let React = require("react");
22
+
23
+ const inject = ["slots", "layout"];
24
+
25
+ function apply(ctx) {
26
+ const slots = ctx.get('slots')
27
+ const layout = ctx.get('layout')
28
+ if (slots === undefined) return
29
+
30
+ const style = document.createElement("style");
31
+ style.id = "dsh-plugin-file-explorer-style";
32
+ style.setAttribute("data-plugin", "dsh-plugin-file-explorer");
33
+ style.textContent = `
34
+ .dsh-fe-panel {
35
+ height: 100%;
36
+ min-height: 0;
37
+ display: flex;
38
+ flex-direction: column;
39
+ box-sizing: border-box;
40
+ background: var(--dsw-specific-sidebar-fill, var(--dsw-alias-bg-base, #f5f5f4));
41
+ color: var(--dsw-alias-label-primary, #111827);
42
+ overflow: hidden;
43
+ }
44
+ .dsh-fe-header {
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: space-between;
48
+ gap: 8px;
49
+ min-height: 48px;
50
+ padding: 10px 12px 10px 14px;
51
+ border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.1));
52
+ box-sizing: border-box;
53
+ }
54
+ .dsh-fe-title {
55
+ flex: 1 1 auto;
56
+ min-width: 0;
57
+ font-size: 14px;
58
+ font-weight: 600;
59
+ color: var(--dsw-alias-label-primary, #111827);
60
+ white-space: nowrap;
61
+ overflow: hidden;
62
+ text-overflow: ellipsis;
63
+ }
64
+ .dsh-fe-icon {
65
+ flex: 0 0 auto;
66
+ cursor: pointer;
67
+ width: 28px;
68
+ height: 28px;
69
+ color: var(--dsw-alias-label-secondary, #4b5563);
70
+ background: transparent;
71
+ border: 0;
72
+ border-radius: 50%;
73
+ display: inline-flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ }
77
+ .dsh-fe-icon:hover {
78
+ background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2, #e7e5e4));
79
+ }
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
+ .dsh-fe-path {
110
+ min-width: 0;
111
+ margin: 8px 12px 0;
112
+ padding: 6px 8px;
113
+ border: 1px solid transparent;
114
+ border-radius: 6px;
115
+ font: inherit;
116
+ font-size: 12px;
117
+ line-height: 16px;
118
+ color: var(--dsw-alias-label-secondary, #57534e);
119
+ white-space: nowrap;
120
+ overflow: hidden;
121
+ text-overflow: ellipsis;
122
+ cursor: text;
123
+ box-sizing: border-box;
124
+ background: transparent;
125
+ text-align: left;
126
+ }
127
+ .dsh-fe-path:hover {
128
+ background: var(--dsw-alias-bg-layer-2, #e7e5e4);
129
+ }
130
+ .dsh-fe-path-input {
131
+ width: 100%;
132
+ box-sizing: border-box;
133
+ margin: 8px 12px 0;
134
+ padding: 5px 8px;
135
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.2));
136
+ border-radius: 6px;
137
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
138
+ color: var(--dsw-alias-label-primary, #111827);
139
+ font: inherit;
140
+ font-size: 12px;
141
+ line-height: 16px;
142
+ outline: none;
143
+ width: calc(100% - 24px);
144
+ }
145
+ .dsh-fe-toolbar {
146
+ display: flex;
147
+ align-items: center;
148
+ gap: 4px;
149
+ padding: 6px 12px 4px;
150
+ }
151
+ .dsh-fe-toolbutton {
152
+ flex: 0 0 auto;
153
+ width: 28px;
154
+ height: 28px;
155
+ border: 0;
156
+ border-radius: 6px;
157
+ background: transparent;
158
+ color: var(--dsw-alias-label-secondary, #4b5563);
159
+ cursor: pointer;
160
+ display: inline-flex;
161
+ align-items: center;
162
+ justify-content: center;
163
+ }
164
+ .dsh-fe-toolbutton:hover:not(:disabled) {
165
+ background: var(--dsw-alias-bg-layer-2, #e7e5e4);
166
+ color: var(--dsw-alias-label-primary, #111827);
167
+ }
168
+ .dsh-fe-toolbutton:disabled {
169
+ opacity: 0.35;
170
+ cursor: default;
171
+ }
172
+ .dsh-fe-list {
173
+ flex: 1 1 auto;
174
+ min-height: 0;
175
+ overflow-y: auto;
176
+ padding: 2px 8px 10px;
177
+ }
178
+ .dsh-fe-row {
179
+ width: 100%;
180
+ height: 34px;
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 8px;
184
+ padding: 0 8px;
185
+ border: 0;
186
+ border-radius: 6px;
187
+ background: transparent;
188
+ color: var(--dsw-alias-label-primary, #111827);
189
+ cursor: pointer;
190
+ text-align: left;
191
+ box-sizing: border-box;
192
+ font: inherit;
193
+ }
194
+ .dsh-fe-row:hover {
195
+ background: var(--dsw-alias-bg-layer-2, #e7e5e4);
196
+ }
197
+ .dsh-fe-marker {
198
+ flex: 0 0 auto;
199
+ width: 16px;
200
+ height: 16px;
201
+ display: inline-flex;
202
+ align-items: center;
203
+ justify-content: center;
204
+ }
205
+ .dsh-fe-marker-dir {
206
+ color: var(--dsw-alias-brand-primary, #4f46e5);
207
+ }
208
+ .dsh-fe-marker-file {
209
+ color: var(--dsw-alias-label-secondary, #78716c);
210
+ }
211
+ .dsh-fe-name {
212
+ flex: 1 1 auto;
213
+ min-width: 0;
214
+ overflow: hidden;
215
+ text-overflow: ellipsis;
216
+ white-space: nowrap;
217
+ font-size: 13px;
218
+ }
219
+ .dsh-fe-meta {
220
+ flex: 0 0 auto;
221
+ min-width: 56px;
222
+ display: inline-flex;
223
+ align-items: center;
224
+ justify-content: flex-end;
225
+ gap: 2px;
226
+ }
227
+ .dsh-fe-size {
228
+ color: var(--dsw-alias-label-secondary, #78716c);
229
+ font-size: 11px;
230
+ white-space: nowrap;
231
+ }
232
+ .dsh-fe-actions {
233
+ display: none;
234
+ align-items: center;
235
+ justify-content: flex-end;
236
+ gap: 2px;
237
+ }
238
+ .dsh-fe-row:hover .dsh-fe-actions {
239
+ display: inline-flex;
240
+ }
241
+ .dsh-fe-row:hover .dsh-fe-size {
242
+ display: none;
243
+ }
244
+ .dsh-fe-action {
245
+ width: 26px;
246
+ height: 26px;
247
+ border: 0;
248
+ border-radius: 6px;
249
+ background: transparent;
250
+ color: var(--dsw-alias-label-secondary, #57534e);
251
+ cursor: pointer;
252
+ display: inline-flex;
253
+ align-items: center;
254
+ justify-content: center;
255
+ }
256
+ .dsh-fe-action:hover {
257
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
258
+ color: var(--dsw-alias-label-primary, #111827);
259
+ }
260
+ .dsh-fe-action-danger:hover {
261
+ color: var(--dsw-alias-state-error-primary, #b91c1c);
262
+ }
263
+ .dsh-fe-action-confirm {
264
+ color: var(--dsw-alias-state-error-primary, #b91c1c);
265
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
266
+ }
267
+ .dsh-fe-status {
268
+ margin: 8px 10px;
269
+ padding: 10px 12px;
270
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.1));
271
+ border-radius: 6px;
272
+ color: var(--dsw-alias-label-secondary, #57534e);
273
+ font-size: 12px;
274
+ line-height: 16px;
275
+ overflow-wrap: anywhere;
276
+ }
277
+ .dsh-fe-status-error {
278
+ color: var(--dsw-alias-state-error-primary, #b91c1c);
279
+ }
280
+ .dsh-fe-overlay {
281
+ position: fixed;
282
+ inset: 0;
283
+ z-index: 1200;
284
+ display: flex;
285
+ align-items: center;
286
+ justify-content: center;
287
+ background: color-mix(in srgb, var(--dsw-alias-bg-base, #000) 45%, transparent);
288
+ pointer-events: auto;
289
+ }
290
+ .dsh-fe-modal {
291
+ width: min(680px, calc(100vw - 40px));
292
+ max-height: min(80vh, 640px);
293
+ display: flex;
294
+ flex-direction: column;
295
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
296
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.2));
297
+ border-radius: 8px;
298
+ box-shadow: 0 18px 50px rgba(0, 0, 0, 0.22);
299
+ overflow: hidden;
300
+ }
301
+ .dsh-fe-modal-head {
302
+ display: flex;
303
+ align-items: center;
304
+ gap: 8px;
305
+ padding: 10px 12px;
306
+ border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.1));
307
+ }
308
+ .dsh-fe-modal-title {
309
+ flex: 1 1 auto;
310
+ min-width: 0;
311
+ overflow: hidden;
312
+ text-overflow: ellipsis;
313
+ white-space: nowrap;
314
+ font-size: 13px;
315
+ font-weight: 600;
316
+ }
317
+ .dsh-fe-modal-meta {
318
+ flex: 0 0 auto;
319
+ color: var(--dsw-alias-label-secondary, #6b7280);
320
+ font-size: 12px;
321
+ }
322
+ .dsh-fe-modal-body {
323
+ flex: 1 1 auto;
324
+ min-height: 0;
325
+ overflow: auto;
326
+ padding: 12px 14px;
327
+ white-space: pre-wrap;
328
+ overflow-wrap: anywhere;
329
+ word-break: break-word;
330
+ font-family: var(--ds-font-family-code, ui-monospace, monospace);
331
+ font-size: 12px;
332
+ line-height: 18px;
333
+ }
334
+ .dsh-git-bar {
335
+ display: flex;
336
+ align-items: center;
337
+ gap: 4px;
338
+ padding: 6px 12px 2px;
339
+ }
340
+ .dsh-git-scope {
341
+ margin-left: auto;
342
+ height: 24px;
343
+ padding: 0 4px;
344
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.2));
345
+ border-radius: 6px;
346
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
347
+ color: var(--dsw-alias-label-secondary, #4b5563);
348
+ font: inherit;
349
+ font-size: 11px;
350
+ }
351
+ .dsh-git-info {
352
+ display: flex;
353
+ align-items: center;
354
+ gap: 6px;
355
+ min-width: 0;
356
+ padding: 4px 14px 6px;
357
+ color: var(--dsw-alias-label-secondary, #78716c);
358
+ font-size: 11px;
359
+ }
360
+ .dsh-git-info-name {
361
+ min-width: 0;
362
+ overflow: hidden;
363
+ text-overflow: ellipsis;
364
+ white-space: nowrap;
365
+ color: var(--dsw-alias-label-primary, #111827);
366
+ font-weight: 600;
367
+ }
368
+ .dsh-git-list {
369
+ flex: 1 1 auto;
370
+ min-height: 0;
371
+ overflow-y: auto;
372
+ padding: 2px 8px 10px;
373
+ }
374
+ .dsh-git-row {
375
+ width: 100%;
376
+ height: 38px;
377
+ display: flex;
378
+ align-items: stretch;
379
+ border: 0;
380
+ border-radius: 6px;
381
+ background: transparent;
382
+ cursor: pointer;
383
+ text-align: left;
384
+ padding: 0;
385
+ font: inherit;
386
+ overflow: hidden;
387
+ }
388
+ .dsh-git-row:hover {
389
+ background: var(--dsw-alias-bg-layer-2, #e7e5e4);
390
+ }
391
+ .dsh-git-cell {
392
+ flex: 0 0 auto;
393
+ display: block;
394
+ }
395
+ .dsh-git-main {
396
+ flex: 1 1 auto;
397
+ min-width: 0;
398
+ display: flex;
399
+ flex-direction: column;
400
+ justify-content: center;
401
+ gap: 1px;
402
+ padding: 0 8px 0 2px;
403
+ }
404
+ .dsh-git-line1 {
405
+ display: flex;
406
+ align-items: center;
407
+ gap: 4px;
408
+ min-width: 0;
409
+ }
410
+ .dsh-git-subject {
411
+ min-width: 0;
412
+ overflow: hidden;
413
+ text-overflow: ellipsis;
414
+ white-space: nowrap;
415
+ font-size: 12px;
416
+ color: var(--dsw-alias-label-primary, #111827);
417
+ }
418
+ .dsh-git-line2 {
419
+ overflow: hidden;
420
+ text-overflow: ellipsis;
421
+ white-space: nowrap;
422
+ font-size: 11px;
423
+ color: var(--dsw-alias-label-secondary, #78716c);
424
+ }
425
+ .dsh-git-pill {
426
+ flex: 0 0 auto;
427
+ max-width: 120px;
428
+ overflow: hidden;
429
+ text-overflow: ellipsis;
430
+ white-space: nowrap;
431
+ font-size: 10px;
432
+ line-height: 15px;
433
+ padding: 0 6px;
434
+ border-radius: 999px;
435
+ border: 1px solid transparent;
436
+ }
437
+ .dsh-git-pill-head {
438
+ background: var(--dsw-alias-brand-primary, #4f46e5);
439
+ color: #fff;
440
+ }
441
+ .dsh-git-pill-branch {
442
+ background: var(--dsw-alias-bg-layer-2, #e7e5e4);
443
+ color: var(--dsw-alias-label-secondary, #57534e);
444
+ }
445
+ .dsh-git-pill-remote {
446
+ background: transparent;
447
+ border-color: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.2));
448
+ color: var(--dsw-alias-label-secondary, #78716c);
449
+ }
450
+ .dsh-git-pill-tag {
451
+ background: rgba(217, 119, 6, 0.16);
452
+ color: #b45309;
453
+ }
454
+ .dsh-git-pill-detached {
455
+ background: transparent;
456
+ border-color: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.2));
457
+ color: var(--dsw-alias-label-secondary, #78716c);
458
+ font-style: italic;
459
+ }
460
+ .dsh-git-detail {
461
+ padding: 2px 8px 10px 36px;
462
+ }
463
+ .dsh-git-msg {
464
+ margin: 0 0 6px;
465
+ padding: 8px 10px;
466
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.1));
467
+ border-radius: 6px;
468
+ background: var(--dsw-alias-bg-layer-1, #ffffff);
469
+ color: var(--dsw-alias-label-primary, #111827);
470
+ font-family: var(--ds-font-family-code, ui-monospace, monospace);
471
+ font-size: 11px;
472
+ line-height: 16px;
473
+ white-space: pre-wrap;
474
+ overflow-wrap: anywhere;
475
+ }
476
+ .dsh-git-files-title {
477
+ margin: 0 2px 4px;
478
+ font-size: 11px;
479
+ color: var(--dsw-alias-label-secondary, #78716c);
480
+ }
481
+ .dsh-git-file {
482
+ width: 100%;
483
+ height: 24px;
484
+ display: flex;
485
+ align-items: center;
486
+ gap: 6px;
487
+ padding: 0 6px;
488
+ border: 0;
489
+ border-radius: 4px;
490
+ background: transparent;
491
+ cursor: pointer;
492
+ text-align: left;
493
+ font: inherit;
494
+ }
495
+ .dsh-git-file:hover {
496
+ background: var(--dsw-alias-bg-layer-2, #e7e5e4);
497
+ }
498
+ .dsh-git-badge {
499
+ flex: 0 0 auto;
500
+ width: 16px;
501
+ height: 16px;
502
+ border-radius: 4px;
503
+ display: inline-flex;
504
+ align-items: center;
505
+ justify-content: center;
506
+ font-size: 10px;
507
+ font-weight: 700;
508
+ color: #fff;
509
+ }
510
+ .dsh-git-file-path {
511
+ min-width: 0;
512
+ overflow: hidden;
513
+ text-overflow: ellipsis;
514
+ white-space: nowrap;
515
+ font-size: 12px;
516
+ unicode-bidi: plaintext;
517
+ }
518
+ .dsh-fe-rail {
519
+ height: 100%;
520
+ width: 100%;
521
+ box-sizing: border-box;
522
+ display: flex;
523
+ flex-direction: column;
524
+ align-items: center;
525
+ gap: 6px;
526
+ padding: 10px 6px;
527
+ background: var(--dsw-specific-sidebar-fill, var(--dsw-alias-bg-base, #f5f5f4));
528
+ border-left: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.16));
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);
552
+ }
553
+ `;
554
+ document.head.append(style);
555
+
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
+ async function api(method, payload) {
667
+ const options = { headers: { "content-type": "application/json" } };
668
+ options.method = "POST";
669
+ options.body = JSON.stringify(payload === undefined ? {} : payload);
670
+ const response = await fetch(`/_dsh/file-explorer/${method}`, options);
671
+ let data;
672
+ try {
673
+ data = await response.json();
674
+ } catch {
675
+ throw new Error(`file explorer API returned HTTP ${response.status}`);
676
+ }
677
+ if (!response.ok || data.ok === false) {
678
+ throw new Error(data.error || `file explorer API returned HTTP ${response.status}`);
679
+ }
680
+ return data;
681
+ }
682
+
683
+ function svgIcon(icon, size) {
684
+ return React.createElement('svg', {
685
+ viewBox: '0 0 24 24',
686
+ width: size || 16,
687
+ height: size || 16,
688
+ fill: 'none',
689
+ stroke: 'currentColor',
690
+ strokeWidth: 1.8,
691
+ strokeLinecap: 'round',
692
+ strokeLinejoin: 'round',
693
+ 'aria-hidden': true,
694
+ }, icon)
695
+ }
696
+ const folderIcon = React.createElement('path', { d: 'M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z' })
697
+ const fileIcon = React.createElement(React.Fragment, null,
698
+ React.createElement('path', { d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }),
699
+ React.createElement('polyline', { points: '14 2 14 8 20 8' }),
700
+ )
701
+ const upIcon = React.createElement(React.Fragment, null,
702
+ React.createElement('path', { d: 'M12 19V5' }),
703
+ React.createElement('polyline', { points: '5 12 12 5 19 12' }),
704
+ )
705
+ const refreshIcon = React.createElement(React.Fragment, null,
706
+ React.createElement('path', { d: 'M3 12a9 9 0 1 0 2.64-6.36' }),
707
+ React.createElement('polyline', { points: '21 3 21 9 15 9' }),
708
+ )
709
+ const eyeIcon = React.createElement(React.Fragment, null,
710
+ React.createElement('path', { d: 'M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z' }),
711
+ React.createElement('circle', { cx: '12', cy: '12', r: '3' }),
712
+ )
713
+ const downloadIcon = React.createElement(React.Fragment, null,
714
+ React.createElement('path', { d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' }),
715
+ React.createElement('polyline', { points: '7 10 12 15 17 10' }),
716
+ React.createElement('line', { x1: '12', y1: '15', x2: '12', y2: '3' }),
717
+ )
718
+ const trashIcon = React.createElement(React.Fragment, null,
719
+ React.createElement('path', { d: 'M3 6h18' }),
720
+ React.createElement('path', { d: 'M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2' }),
721
+ React.createElement('path', { d: 'M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6' }),
722
+ React.createElement('line', { x1: '10', y1: '11', x2: '10', y2: '17' }),
723
+ React.createElement('line', { x1: '14', y1: '11', x2: '14', y2: '17' }),
724
+ )
725
+ const closeIcon = React.createElement(React.Fragment, null,
726
+ React.createElement('line', { x1: '18', y1: '6', x2: '6', y2: '18' }),
727
+ React.createElement('line', { x1: '6', y1: '6', x2: '18', y2: '18' }),
728
+ )
729
+ // Mirror of DeepSeek's sidebar IconPanelLeftOutline16 (spine on the
730
+ // right): this panel lives on the right edge, so the glyph mirrors the
731
+ // left sidebar's, signaling collapse into the right-edge icon rail.
732
+ const collapseIcon = React.createElement('svg', {
733
+ viewBox: '0 0 16 16',
734
+ width: 16,
735
+ height: 16,
736
+ fill: 'none',
737
+ 'aria-hidden': true,
738
+ },
739
+ React.createElement('g', { transform: 'translate(16,0) scale(-1,1)' },
740
+ React.createElement('path', {
741
+ fillRule: 'evenodd',
742
+ clipRule: 'evenodd',
743
+ fill: 'currentColor',
744
+ d: 'M9.67272 0.522841C10.8339 0.522841 11.76 0.522714 12.4963 0.602493C13.2453 0.683657 13.8789 0.854248 14.4264 1.25197C14.7504 1.48739 15.0355 1.77247 15.2709 2.0965C15.6686 2.64394 15.8392 3.27758 15.9204 4.02655C16.0002 4.7629 16 5.68895 16 6.85014V9.14986C16 10.3111 16.0002 11.2371 15.9204 11.9735C15.8392 12.7224 15.6686 13.3561 15.2709 13.9035C15.0355 14.2275 14.7504 14.5126 14.4264 14.748C13.8789 15.1458 13.2453 15.3163 12.4963 15.3975C11.76 15.4773 10.8339 15.4772 9.67272 15.4772H6.3273C5.16611 15.4772 4.24006 15.4773 3.50371 15.3975C2.75474 15.3163 2.1211 15.1458 1.57366 14.748C1.24963 14.5126 0.964549 14.2275 0.729131 13.9035C0.331407 13.3561 0.160817 12.7224 0.0796529 11.9735C-0.000126137 11.2371 1.25338e-09 10.3111 1.25338e-09 9.14986V6.85014C1.25329e-09 5.68895 -0.000126137 4.7629 0.0796529 4.02655C0.160817 3.27758 0.331407 2.64394 0.729131 2.0965C0.964549 1.77247 1.24963 1.48739 1.57366 1.25197C2.1211 0.854248 2.75474 0.683657 3.50371 0.602493C4.24006 0.522714 5.16611 0.522841 6.3273 0.522841H9.67272ZM5.54303 1.88715V14.1118C5.78636 14.1128 6.04709 14.1169 6.3273 14.1169H9.67272C10.8639 14.1169 11.7032 14.1164 12.3493 14.0465C12.9824 13.9779 13.3497 13.8494 13.6268 13.6482C13.8354 13.4966 14.0195 13.3125 14.1711 13.1039C14.3723 12.8268 14.5007 12.4595 14.5693 11.8264C14.6393 11.1803 14.6398 10.341 14.6398 9.14986V6.85014C14.6398 5.65896 14.6393 4.81967 14.5693 4.1736C14.5007 3.54048 14.3723 3.17318 14.1711 2.89609C14.0195 2.68747 13.8354 2.50337 13.6268 2.35179C13.3497 2.1506 12.9824 2.02212 12.3493 1.95353C11.7032 1.88358 10.8639 1.88307 9.67272 1.88307H6.3273C6.04709 1.88307 5.78636 1.8862 5.54303 1.88715ZM4.1828 1.91166C3.99125 1.9216 3.8148 1.93577 3.65076 1.95353C3.01764 2.02212 2.65034 2.1506 2.37325 2.35179C2.16463 2.50337 1.98052 2.68747 1.82895 2.89609C1.62776 3.17318 1.49928 3.54048 1.43069 4.1736C1.36074 4.81967 1.36023 5.65896 1.36023 6.85014V9.14986C1.36023 10.341 1.36074 11.1803 1.43069 11.8264C1.49928 12.4595 1.62776 12.8268 1.82895 13.1039C1.98052 13.3125 2.16463 13.4966 2.37325 13.6482C2.65034 13.8494 3.01764 13.9779 3.65076 14.0465C3.81478 14.0642 3.99127 14.0774 4.1828 14.0873V1.91166Z',
745
+ }),
746
+ ),
747
+ )
748
+ const branchIcon = React.createElement(React.Fragment, null,
749
+ React.createElement('line', { x1: '6', y1: '3', x2: '6', y2: '15' }),
750
+ React.createElement('circle', { cx: '18', cy: '6', r: '3' }),
751
+ React.createElement('circle', { cx: '6', cy: '18', r: '3' }),
752
+ React.createElement('path', { d: 'M18 9a9 9 0 0 1-9 9' }),
753
+ )
754
+
755
+ function formatSize(size) {
756
+ if (typeof size !== 'number' || !Number.isFinite(size)) return ''
757
+ if (size < 1024) return `${size} B`
758
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
759
+ if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(1)} MB`
760
+ return `${(size / 1024 / 1024 / 1024).toFixed(1)} GB`
761
+ }
762
+
763
+ // --- git graph helpers -------------------------------------------------
764
+
765
+ const LANE_COLORS = ['#4f46e5', '#0891b2', '#16a34a', '#d97706', '#dc2626', '#7c3aed', '#db2777']
766
+ const STATUS_COLORS = {
767
+ A: '#16a34a', M: '#d97706', D: '#dc2626', R: '#2563eb', C: '#2563eb',
768
+ T: '#d97706', U: '#7c3aed', B: '#6b7280', X: '#6b7280', '?': '#6b7280',
769
+ }
770
+ const GIT_ROW_H = 38
771
+
772
+ function statusColor(letter) {
773
+ return STATUS_COLORS[letter] || '#6b7280'
774
+ }
775
+
776
+ function relTime(epochSeconds) {
777
+ if (typeof epochSeconds !== 'number' || !Number.isFinite(epochSeconds) || epochSeconds <= 0) return ''
778
+ const s = Math.max(0, Date.now() / 1000 - epochSeconds)
779
+ if (s < 60) return 'just now'
780
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`
781
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`
782
+ if (s < 604800) return `${Math.floor(s / 86400)}d ago`
783
+ if (s < 2592000) return `${Math.floor(s / 604800)}w ago`
784
+ if (s < 31536000) return `${Math.floor(s / 2592000)}mo ago`
785
+ return `${Math.floor(s / 31536000)}y ago`
786
+ }
787
+
788
+ function shortHash(hash) {
789
+ return typeof hash === 'string' && hash.length > 7 ? hash.slice(0, 7) : (hash || '')
790
+ }
791
+
792
+ // Turn the linear commit list (newest first) into graph rows with lane
793
+ // assignments and parent edges, mirroring git-graph/vscode style layout:
794
+ // each lane tracks the hash expected to appear there next; a commit
795
+ // consumes its lane and re-fills it with its first parent, while extra
796
+ // parents take new lanes (drawn as merge curves).
797
+ function buildGraphData(commits) {
798
+ const lanes = [] // expected hash per lane; null = free slot
799
+ const colorOf = new Map()
800
+ let nextColor = 0
801
+ const pickColor = (hash, preferred) => {
802
+ let c = colorOf.get(hash)
803
+ if (c === undefined) {
804
+ if (preferred === undefined) {
805
+ c = nextColor % LANE_COLORS.length
806
+ nextColor += 1
807
+ } else {
808
+ c = preferred % LANE_COLORS.length
809
+ }
810
+ colorOf.set(hash, c)
811
+ }
812
+ return c
813
+ }
814
+ const rows = commits.map((commit) => {
815
+ const parents = Array.isArray(commit.parents)
816
+ ? commit.parents.filter((p) => typeof p === 'string' && p !== '')
817
+ : []
818
+ const inLanes = lanes.slice()
819
+ let lane = lanes.indexOf(commit.hash)
820
+ const isNew = lane === -1
821
+ if (isNew) {
822
+ let free = lanes.indexOf(null)
823
+ if (free === -1) {
824
+ lanes.push(null)
825
+ free = lanes.length - 1
826
+ }
827
+ lane = free
828
+ }
829
+ const color = pickColor(commit.hash)
830
+ lanes[lane] = null
831
+ const edges = []
832
+ parents.forEach((p, pi) => {
833
+ let pl = lanes.indexOf(p)
834
+ if (pl === -1) {
835
+ let slot = pi === 0 ? lane : lanes.indexOf(null)
836
+ if (slot === -1) {
837
+ lanes.push(p)
838
+ slot = lanes.length - 1
839
+ } else {
840
+ lanes[slot] = p
841
+ }
842
+ pl = slot
843
+ }
844
+ const pColor = pickColor(p, pi === 0 ? color : undefined)
845
+ edges.push({ from: lane, to: pl, color: pColor })
846
+ })
847
+ return { commit, lane, color, isNew, inLanes, edges }
848
+ })
849
+ let width = 1
850
+ rows.forEach((r) => {
851
+ if (r.lane + 1 > width) width = r.lane + 1
852
+ r.edges.forEach((e) => { if (e.to + 1 > width) width = e.to + 1 })
853
+ })
854
+ return { rows, width, colorOf }
855
+ }
856
+
857
+ // One commit row's graph cell: pass-through lanes, its incoming stub,
858
+ // parent edges (straight for first parents, curves for merges), dot.
859
+ function GraphSvg(props) {
860
+ const { row, width, colorOf } = props
861
+ const laneW = 12
862
+ const padL = 9
863
+ const xOf = (l) => padL + l * laneW
864
+ // Cell width follows THIS row's actual lanes (lane indices are
865
+ // absolute, so columns stay aligned row-to-row; this just trims the
866
+ // empty right side that a global max-lane width would leave on
867
+ // mostly-linear rows). 1 lane = 24px, so linear history renders
868
+ // compact and the commit text sits right next to the graph.
869
+ let rowLanes = row.lane + 1
870
+ row.edges.forEach((e) => {
871
+ if (e.from + 1 > rowLanes) rowLanes = e.from + 1
872
+ if (e.to + 1 > rowLanes) rowLanes = e.to + 1
873
+ })
874
+ row.inLanes.forEach((hash, k) => {
875
+ if (hash !== null && hash !== undefined && k + 1 > rowLanes) rowLanes = k + 1
876
+ })
877
+ const w = rowLanes * laneW + padL + 3
878
+ const h = GIT_ROW_H
879
+ const cy = h / 2
880
+ const els = []
881
+ const colorOfHash = (hash) => {
882
+ const c = colorOf.get(hash)
883
+ return LANE_COLORS[(typeof c === 'number' ? c : 0) % LANE_COLORS.length]
884
+ }
885
+ row.inLanes.forEach((hash, k) => {
886
+ if (hash === null || hash === undefined || hash === row.commit.hash) return
887
+ els.push(React.createElement('line', {
888
+ key: 'p' + k,
889
+ x1: xOf(k), y1: 0, x2: xOf(k), y2: h,
890
+ stroke: colorOfHash(hash), strokeWidth: 1.5,
891
+ }))
892
+ })
893
+ if (!row.isNew) {
894
+ els.push(React.createElement('line', {
895
+ key: 'in',
896
+ x1: xOf(row.lane), y1: 0, x2: xOf(row.lane), y2: cy,
897
+ stroke: LANE_COLORS[row.color % LANE_COLORS.length], strokeWidth: 1.5,
898
+ }))
899
+ }
900
+ row.edges.forEach((edge, i) => {
901
+ const stroke = LANE_COLORS[edge.color % LANE_COLORS.length]
902
+ if (edge.from === edge.to) {
903
+ els.push(React.createElement('line', {
904
+ key: 'e' + i,
905
+ x1: xOf(edge.from), y1: cy, x2: xOf(edge.to), y2: h,
906
+ stroke, strokeWidth: 1.5,
907
+ }))
908
+ } else {
909
+ const bend = (h - cy) * 0.55
910
+ const d = 'M ' + xOf(edge.from) + ' ' + cy +
911
+ ' C ' + xOf(edge.from) + ' ' + (cy + bend) +
912
+ ', ' + xOf(edge.to) + ' ' + (h - bend) +
913
+ ', ' + xOf(edge.to) + ' ' + h
914
+ els.push(React.createElement('path', {
915
+ key: 'e' + i, d, fill: 'none', stroke, strokeWidth: 1.5,
916
+ }))
917
+ }
918
+ })
919
+ els.push(React.createElement('circle', {
920
+ key: 'dot',
921
+ cx: xOf(row.lane), cy, r: 3.5,
922
+ fill: LANE_COLORS[row.color % LANE_COLORS.length],
923
+ stroke: 'var(--dsw-alias-bg-base, #ffffff)', strokeWidth: 1,
924
+ }))
925
+ return React.createElement('svg', {
926
+ className: 'dsh-git-cell',
927
+ width: w, height: h,
928
+ viewBox: '0 0 ' + w + ' ' + h,
929
+ }, els)
930
+ }
931
+
932
+ // Uncommitted-changes pseudo row at the top of the graph.
933
+ function WorkTreeCell(props) {
934
+ const { lane, connectorColor } = props
935
+ const laneW = 12
936
+ const padL = 9
937
+ const xOf = (l) => padL + l * laneW
938
+ const laneIdx = typeof lane === 'number' && lane >= 0 ? lane : 0
939
+ const w = (laneIdx + 1) * laneW + padL + 3
940
+ const h = GIT_ROW_H
941
+ const cy = h / 2
942
+ const x = xOf(laneIdx)
943
+ const els = []
944
+ if (connectorColor !== null) {
945
+ els.push(React.createElement('line', {
946
+ key: 'c', x1: x, y1: cy, x2: x, y2: h,
947
+ stroke: connectorColor, strokeWidth: 1.5,
948
+ }))
949
+ }
950
+ els.push(React.createElement('circle', {
951
+ key: 'dot', cx: x, cy, r: 3.5,
952
+ fill: 'none',
953
+ stroke: 'var(--dsw-alias-label-secondary, #78716c)', strokeWidth: 1.2,
954
+ strokeDasharray: '2 2',
955
+ }))
956
+ return React.createElement('svg', {
957
+ className: 'dsh-git-cell',
958
+ width: w, height: h,
959
+ viewBox: '0 0 ' + w + ' ' + h,
960
+ }, els)
961
+ }
962
+
963
+ function RefPills(props) {
964
+ const refs = props.refs
965
+ if (!Array.isArray(refs) || refs.length === 0) return null
966
+ return React.createElement(React.Fragment, null, refs.map((ref, i) =>
967
+ React.createElement('span', {
968
+ key: i,
969
+ className: 'dsh-git-pill dsh-git-pill-' + ref.kind,
970
+ title: ref.name,
971
+ }, ref.name),
972
+ ))
973
+ }
974
+
975
+ function StatusBadge(props) {
976
+ return React.createElement('span', {
977
+ className: 'dsh-git-badge',
978
+ style: { background: statusColor(props.letter) },
979
+ }, props.letter)
980
+ }
981
+
982
+ function GitFileRow(props) {
983
+ const file = props.file
984
+ const label = file.oldPath ? file.oldPath + ' → ' + file.path : file.path
985
+ return React.createElement('button', {
986
+ type: 'button',
987
+ className: 'dsh-git-file',
988
+ onClick: () => props.onOpen(file),
989
+ title: label,
990
+ },
991
+ React.createElement(StatusBadge, { letter: file.status }),
992
+ React.createElement('span', { className: 'dsh-git-file-path' }, label),
993
+ )
994
+ }
995
+
996
+ // The whole Git Graph view: toolbar (refresh + branch scope), repo info
997
+ // line, the commit lane rows with an expandable per-commit detail, and a
998
+ // diff modal. Data comes entirely from the /_dsh/file-explorer/git-*
999
+ // JSON API.
1000
+ function GitGraphView(props) {
1001
+ const api = props.api
1002
+ const cwd = props.cwd
1003
+ const [scope, setScope] = React.useState('all')
1004
+ const [log, setLog] = React.useState(null)
1005
+ const [status, setStatus] = React.useState(null)
1006
+ const [error, setError] = React.useState(null)
1007
+ const [loading, setLoading] = React.useState(false)
1008
+ const [tick, setTick] = React.useState(0)
1009
+ const [selected, setSelected] = React.useState(null) // commit hash or 'WORKING'
1010
+ const [details, setDetails] = React.useState({}) // hash -> {message, files} | {error}
1011
+ const [diff, setDiff] = React.useState(null)
1012
+
1013
+ React.useEffect(() => {
1014
+ if (cwd === undefined) return
1015
+ let cancelled = false
1016
+ setLoading(true)
1017
+ setError(null)
1018
+ setLog(null)
1019
+ setSelected(null)
1020
+ setDetails({})
1021
+ setStatus(null)
1022
+ api('git-log', { path: cwd, all: scope === 'all', limit: 300 })
1023
+ .then((raw) => {
1024
+ if (cancelled) return
1025
+ if (raw && raw.ok === true) {
1026
+ setLog(raw)
1027
+ } else {
1028
+ setError(raw && typeof raw.error === 'string' ? raw.error : 'Failed to load git log')
1029
+ }
1030
+ })
1031
+ .catch((err) => {
1032
+ if (!cancelled) setError(err && typeof err.message === 'string' ? err.message : String(err))
1033
+ })
1034
+ .finally(() => {
1035
+ if (!cancelled) setLoading(false)
1036
+ })
1037
+ api('git-status', { path: cwd })
1038
+ .then((raw) => { if (!cancelled && raw && raw.ok === true) setStatus(raw) })
1039
+ .catch(() => { })
1040
+ return () => { cancelled = true }
1041
+ }, [cwd, scope, tick])
1042
+
1043
+ // Lazy-load the selected commit's changed-file list.
1044
+ React.useEffect(() => {
1045
+ if (selected === null || selected === 'WORKING') return
1046
+ if (details[selected]) return
1047
+ let cancelled = false
1048
+ api('git-commit', { path: cwd, hash: selected })
1049
+ .then((raw) => {
1050
+ if (cancelled) return
1051
+ if (raw && raw.ok === true) {
1052
+ setDetails((prev) => ({ ...prev, [selected]: raw }))
1053
+ } else {
1054
+ setDetails((prev) => ({ ...prev, [selected]: { error: raw && typeof raw.error === 'string' ? raw.error : 'Failed to load commit' } }))
1055
+ }
1056
+ })
1057
+ .catch((err) => {
1058
+ if (!cancelled) setDetails((prev) => ({ ...prev, [selected]: { error: err && typeof err.message === 'string' ? err.message : String(err) } }))
1059
+ })
1060
+ return () => { cancelled = true }
1061
+ }, [selected, cwd])
1062
+
1063
+ if (cwd === undefined) return null
1064
+
1065
+ const reload = () => { setTick((t) => t + 1) }
1066
+ const toggle = (key) => setSelected((cur) => (cur === key ? null : key))
1067
+
1068
+ const openDiff = (hash, file) => {
1069
+ setDiff({ title: file.path, hash, loading: true, text: null, error: null })
1070
+ api('git-diff', { path: cwd, hash, file: file.path })
1071
+ .then((raw) => {
1072
+ if (raw && raw.ok === true) {
1073
+ const note = raw.truncated ? '\n\n… (diff truncated)' : ''
1074
+ setDiff({ title: file.path, hash, loading: false, text: (raw.patch || '') + note, error: null })
1075
+ } else {
1076
+ setDiff({ title: file.path, hash, loading: false, text: null, error: raw && typeof raw.error === 'string' ? raw.error : 'Failed to load diff' })
1077
+ }
1078
+ })
1079
+ .catch((err) => {
1080
+ setDiff({ title: file.path, hash, loading: false, text: null, error: err && typeof err.message === 'string' ? err.message : String(err) })
1081
+ })
1082
+ }
1083
+
1084
+ const graph = log && Array.isArray(log.commits) ? buildGraphData(log.commits) : null
1085
+ const changes = status && Array.isArray(status.entries) ? status.entries : []
1086
+
1087
+ const renderCommitDetail = (hash, d) => {
1088
+ if (d === undefined) {
1089
+ return React.createElement('div', { className: 'dsh-git-detail' },
1090
+ React.createElement('div', { className: 'dsh-git-files-title' }, 'Loading changes…'))
1091
+ }
1092
+ if (d.error) {
1093
+ return React.createElement('div', { className: 'dsh-git-detail' },
1094
+ React.createElement('div', { className: 'dsh-git-files-title dsh-fe-status-error' }, d.error))
1095
+ }
1096
+ const files = Array.isArray(d.files) ? d.files : []
1097
+ return React.createElement('div', { className: 'dsh-git-detail' },
1098
+ React.createElement('div', { className: 'dsh-git-msg' }, d.message || '(no message)'),
1099
+ React.createElement('div', { className: 'dsh-git-files-title' },
1100
+ `${files.length} changed file${files.length === 1 ? '' : 's'}`),
1101
+ files.length === 0 ? null : files.map((f, i) =>
1102
+ React.createElement(GitFileRow, { key: i, file: f, onOpen: (file) => openDiff(hash, file) })),
1103
+ )
1104
+ }
1105
+
1106
+ const rows = []
1107
+ if (loading) {
1108
+ rows.push(React.createElement('div', { key: 'loading', className: 'dsh-fe-status' }, 'Loading…'))
1109
+ } else if (error) {
1110
+ rows.push(React.createElement('div', { key: 'error', className: 'dsh-fe-status dsh-fe-status-error' }, error))
1111
+ } else if (graph !== null) {
1112
+ if (changes.length > 0) {
1113
+ const first = graph.rows.length > 0 ? graph.rows[0] : null
1114
+ const connectorColor = first !== null && first.lane === 0
1115
+ ? LANE_COLORS[first.color % LANE_COLORS.length]
1116
+ : null
1117
+ rows.push(React.createElement('div', { key: 'worktree' },
1118
+ React.createElement('div', {
1119
+ className: 'dsh-git-row',
1120
+ onClick: () => toggle('WORKING'),
1121
+ role: 'button',
1122
+ title: 'Uncommitted changes',
1123
+ },
1124
+ React.createElement(WorkTreeCell, { lane: first !== null ? first.lane : 0, connectorColor }),
1125
+ React.createElement('div', { className: 'dsh-git-main' },
1126
+ React.createElement('div', { className: 'dsh-git-line1' },
1127
+ React.createElement('span', { className: 'dsh-git-subject' }, 'Uncommitted changes'),
1128
+ React.createElement('span', { className: 'dsh-git-pill dsh-git-pill-branch' }, String(changes.length)),
1129
+ ),
1130
+ React.createElement('div', { className: 'dsh-git-line2' }, 'Working tree'),
1131
+ ),
1132
+ ),
1133
+ selected === 'WORKING'
1134
+ ? React.createElement('div', { className: 'dsh-git-detail' },
1135
+ React.createElement('div', { className: 'dsh-git-files-title' },
1136
+ `${changes.length} changed file${changes.length === 1 ? '' : 's'} (vs HEAD)`),
1137
+ changes.map((f, i) =>
1138
+ React.createElement(GitFileRow, { key: i, file: f, onOpen: (file) => openDiff('WORKING', file) })),
1139
+ )
1140
+ : null,
1141
+ ))
1142
+ }
1143
+ graph.rows.forEach((row, idx) => {
1144
+ const c = row.commit
1145
+ rows.push(React.createElement('div', { key: c.hash + '-' + idx },
1146
+ React.createElement('div', {
1147
+ className: 'dsh-git-row',
1148
+ onClick: () => toggle(c.hash),
1149
+ role: 'button',
1150
+ title: c.subject,
1151
+ },
1152
+ React.createElement(GraphSvg, { row, width: graph.width, colorOf: graph.colorOf }),
1153
+ React.createElement('div', { className: 'dsh-git-main' },
1154
+ React.createElement('div', { className: 'dsh-git-line1' },
1155
+ React.createElement(RefPills, { refs: c.refs }),
1156
+ React.createElement('span', { className: 'dsh-git-subject' }, c.subject),
1157
+ ),
1158
+ React.createElement('div', { className: 'dsh-git-line2' },
1159
+ `${c.author || 'unknown'} · ${relTime(c.date)} · ${shortHash(c.hash)}`),
1160
+ ),
1161
+ ),
1162
+ selected === c.hash ? renderCommitDetail(c.hash, details[c.hash]) : null,
1163
+ ))
1164
+ })
1165
+ if (rows.length === 0) {
1166
+ rows.push(React.createElement('div', { key: 'empty', className: 'dsh-fe-status' }, 'No commits yet'))
1167
+ }
1168
+ } else {
1169
+ rows.push(React.createElement('div', { key: 'none', className: 'dsh-fe-status' }, 'No data'))
1170
+ }
1171
+
1172
+ const branchLabel = status
1173
+ ? (status.branch === null
1174
+ ? 'detached HEAD'
1175
+ : status.branch + (status.unborn ? ' (no commits yet)' : ''))
1176
+ : null
1177
+ const upstreamBits = []
1178
+ if (status && status.ahead > 0) upstreamBits.push('↑' + status.ahead)
1179
+ if (status && status.behind > 0) upstreamBits.push('↓' + status.behind)
1180
+
1181
+ return React.createElement(React.Fragment, null,
1182
+ React.createElement('div', { className: 'dsh-git-bar' },
1183
+ React.createElement('button', {
1184
+ type: 'button',
1185
+ className: 'dsh-fe-toolbutton',
1186
+ onClick: reload,
1187
+ 'aria-label': 'Refresh graph',
1188
+ title: 'Refresh',
1189
+ }, svgIcon(refreshIcon, 15)),
1190
+ React.createElement('select', {
1191
+ className: 'dsh-git-scope',
1192
+ value: scope,
1193
+ 'aria-label': 'Branch filter',
1194
+ onChange: (event) => setScope(event.target.value),
1195
+ },
1196
+ React.createElement('option', { value: 'all' }, 'All branches'),
1197
+ React.createElement('option', { value: 'current' }, 'Current branch'),
1198
+ ),
1199
+ ),
1200
+ branchLabel !== null
1201
+ ? React.createElement('div', { className: 'dsh-git-info' },
1202
+ svgIcon(branchIcon, 12),
1203
+ React.createElement('span', { className: 'dsh-git-info-name' }, branchLabel),
1204
+ upstreamBits.length > 0 ? React.createElement('span', null, upstreamBits.join(' ')) : null,
1205
+ React.createElement('span', null, `${changes.length} change${changes.length === 1 ? '' : 's'}`),
1206
+ )
1207
+ : null,
1208
+ React.createElement('div', { className: 'dsh-git-list' }, rows),
1209
+ diff !== null
1210
+ ? React.createElement('div', {
1211
+ className: 'dsh-fe-overlay',
1212
+ onClick: () => setDiff(null),
1213
+ },
1214
+ React.createElement('div', {
1215
+ className: 'dsh-fe-modal',
1216
+ role: 'dialog',
1217
+ 'aria-label': 'Diff',
1218
+ onClick: (event) => event.stopPropagation(),
1219
+ },
1220
+ React.createElement('div', { className: 'dsh-fe-modal-head' },
1221
+ React.createElement('div', { className: 'dsh-fe-modal-title' }, diff.title),
1222
+ React.createElement('div', { className: 'dsh-fe-modal-meta' },
1223
+ diff.hash === 'WORKING' ? 'working tree' : shortHash(diff.hash)),
1224
+ React.createElement('button', {
1225
+ type: 'button',
1226
+ className: 'dsh-fe-icon',
1227
+ onClick: () => setDiff(null),
1228
+ 'aria-label': 'Close diff',
1229
+ }, svgIcon(closeIcon, 14)),
1230
+ ),
1231
+ React.createElement('div', { className: 'dsh-fe-modal-body' },
1232
+ diff.loading ? 'Loading…' : (diff.error || diff.text || '(no diff)')),
1233
+ ),
1234
+ )
1235
+ : null,
1236
+ )
1237
+ }
1238
+
1239
+ function FileExplorerPanel(props) {
1240
+ if (typeof props.useSessions !== 'function') return null
1241
+
1242
+ const sessionId = props.useSessions((state) => {
1243
+ const current = state.current
1244
+ if (current === undefined) return undefined
1245
+ const row = state.byId[current]
1246
+ if (!row || row.blank === true) return undefined
1247
+ return current
1248
+ })
1249
+ const cwd = props.useSessions((state) => {
1250
+ const current = state.current
1251
+ if (current === undefined) return undefined
1252
+ const row = state.byId[current]
1253
+ return row ? row.cwd : undefined
1254
+ })
1255
+ const [requestPath, setRequestPath] = React.useState('')
1256
+ const [currentPath, setCurrentPath] = React.useState('')
1257
+ const [parentPath, setParentPath] = React.useState(null)
1258
+ const [entries, setEntries] = React.useState(null)
1259
+ const [error, setError] = React.useState(null)
1260
+ const [loading, setLoading] = React.useState(false)
1261
+ const [reloadTick, setReloadTick] = React.useState(0)
1262
+ const [editingPath, setEditingPath] = React.useState(false)
1263
+ const [draftPath, setDraftPath] = React.useState('')
1264
+ const [preview, setPreview] = React.useState(null)
1265
+ const [previewLoading, setPreviewLoading] = React.useState(false)
1266
+ 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
+
1281
+ React.useEffect(() => {
1282
+ if (sessionId === undefined) {
1283
+ releaseRail()
1284
+ return
1285
+ }
1286
+ setParentPath(null)
1287
+ setRequestPath(cwd === undefined ? '' : cwd)
1288
+ }, [sessionId, cwd])
1289
+
1290
+ React.useEffect(() => {
1291
+ if (sessionId === undefined) {
1292
+ setEntries(null)
1293
+ setError(null)
1294
+ return
1295
+ }
1296
+ let cancelled = false
1297
+ setLoading(true)
1298
+ setEntries(null)
1299
+ setError(null)
1300
+ api('list', { path: requestPath === '' ? null : requestPath })
1301
+ .then((raw) => {
1302
+ if (cancelled) return
1303
+ if (raw && raw.ok === true) {
1304
+ setCurrentPath(raw.path)
1305
+ setParentPath(raw.parent || null)
1306
+ setEntries(raw.entries || [])
1307
+ } else {
1308
+ setCurrentPath(requestPath)
1309
+ setParentPath(null)
1310
+ setEntries([])
1311
+ setError(raw && typeof raw.error === 'string' ? raw.error : 'Failed to list files')
1312
+ }
1313
+ })
1314
+ .catch((err) => {
1315
+ if (cancelled) return
1316
+ setCurrentPath(requestPath)
1317
+ setParentPath(null)
1318
+ setEntries([])
1319
+ setError(err && typeof err.message === 'string' ? err.message : String(err))
1320
+ })
1321
+ .finally(() => {
1322
+ if (!cancelled) setLoading(false)
1323
+ })
1324
+ return () => { cancelled = true }
1325
+ }, [sessionId, requestPath, reloadTick])
1326
+
1327
+ if (sessionId === undefined) return null
1328
+
1329
+ const sorted = (entries || []).slice().sort((a, b) => {
1330
+ const aDir = a.type === 'directory' ? 0 : 1
1331
+ const bDir = b.type === 'directory' ? 0 : 1
1332
+ return aDir - bDir || a.name.localeCompare(b.name)
1333
+ })
1334
+ 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
+ const reload = () => { setPendingDelete(null); setReloadTick((tick) => tick + 1) }
1364
+ const startEditPath = () => {
1365
+ setDraftPath(currentPath)
1366
+ setEditingPath(true)
1367
+ }
1368
+ const commitPath = () => {
1369
+ setPendingDelete(null)
1370
+ setEditingPath(false)
1371
+ const next = draftPath.trim()
1372
+ if (next.length === 0) return
1373
+ setRequestPath(next)
1374
+ }
1375
+
1376
+ const openPreview = (entry) => {
1377
+ setPendingDelete(null)
1378
+ setPreviewLoading(true)
1379
+ setPreview(null)
1380
+ api('read', { path: entry.path })
1381
+ .then((raw) => {
1382
+ setPreview(raw && raw.ok === true ? raw : { ok: false, error: raw && typeof raw.error === 'string' ? raw.error : 'Failed to read file' })
1383
+ })
1384
+ .catch((err) => {
1385
+ setPreview({ ok: false, error: err && typeof err.message === 'string' ? err.message : String(err) })
1386
+ })
1387
+ .finally(() => setPreviewLoading(false))
1388
+ }
1389
+
1390
+ const downloadFile = (entry) => {
1391
+ setPendingDelete(null)
1392
+ api('download', { path: entry.path })
1393
+ .then((raw) => {
1394
+ if (!raw || raw.ok !== true) {
1395
+ const message = raw && typeof raw.error === 'string' ? raw.error : 'Download failed'
1396
+ setError(message)
1397
+ return
1398
+ }
1399
+ try {
1400
+ const link = document.createElement('a')
1401
+ link.href = raw.dataUrl
1402
+ link.download = entry.name
1403
+ document.body.appendChild(link)
1404
+ link.click()
1405
+ link.remove()
1406
+ } catch (err) {
1407
+ setError(err && typeof err.message === 'string' ? err.message : String(err))
1408
+ }
1409
+ })
1410
+ .catch((err) => setError(err && typeof err.message === 'string' ? err.message : String(err)))
1411
+ }
1412
+
1413
+ const requestDelete = (entry) => {
1414
+ if (pendingDelete !== entry.path) {
1415
+ setPendingDelete(entry.path)
1416
+ return
1417
+ }
1418
+ setPendingDelete(null)
1419
+ api('delete', { path: entry.path })
1420
+ .then((raw) => {
1421
+ if (!raw || raw.ok !== true) {
1422
+ setError(raw && typeof raw.error === 'string' ? raw.error : 'Delete failed')
1423
+ return
1424
+ }
1425
+ reload()
1426
+ })
1427
+ .catch((err) => setError(err && typeof err.message === 'string' ? err.message : String(err)))
1428
+ }
1429
+
1430
+ const pathControl = editingPath
1431
+ ? React.createElement('input', {
1432
+ key: 'path-input',
1433
+ className: 'dsh-fe-path-input',
1434
+ value: draftPath,
1435
+ autoFocus: true,
1436
+ onChange: (event) => setDraftPath(event.target.value),
1437
+ onKeyDown: (event) => {
1438
+ if (event.key === 'Enter') commitPath()
1439
+ if (event.key === 'Escape') setEditingPath(false)
1440
+ },
1441
+ onBlur: () => setEditingPath(false),
1442
+ })
1443
+ : React.createElement('button', {
1444
+ type: 'button',
1445
+ className: 'dsh-fe-path',
1446
+ title: currentPath || 'workspace root',
1447
+ onClick: startEditPath,
1448
+ }, currentPath || 'workspace root')
1449
+
1450
+ const filesView = React.createElement(React.Fragment, null,
1451
+ pathControl,
1452
+ React.createElement('div', { className: 'dsh-fe-toolbar' },
1453
+ React.createElement('button', {
1454
+ type: 'button',
1455
+ className: 'dsh-fe-toolbutton',
1456
+ disabled: isRoot,
1457
+ onClick: () => {
1458
+ if (parentPath !== null && parentPath !== currentPath) setRequestPath(parentPath)
1459
+ },
1460
+ 'aria-label': 'Go to parent directory',
1461
+ title: 'Up',
1462
+ }, svgIcon(upIcon, 15)),
1463
+ React.createElement('button', {
1464
+ type: 'button',
1465
+ className: 'dsh-fe-toolbutton',
1466
+ onClick: reload,
1467
+ 'aria-label': 'Refresh file list',
1468
+ title: 'Refresh',
1469
+ }, svgIcon(refreshIcon, 15)),
1470
+ ),
1471
+ React.createElement('div', { className: 'dsh-fe-list' },
1472
+ loading
1473
+ ? React.createElement('div', { className: 'dsh-fe-status' }, 'Loading...')
1474
+ : error
1475
+ ? React.createElement('div', { className: 'dsh-fe-status dsh-fe-status-error' }, error)
1476
+ : sorted.length === 0
1477
+ ? React.createElement('div', { className: 'dsh-fe-status' }, 'Empty folder')
1478
+ : sorted.map((entry) => {
1479
+ const isDir = entry.type === 'directory'
1480
+ return React.createElement('div', {
1481
+ key: entry.path,
1482
+ className: 'dsh-fe-row',
1483
+ onClick: isDir ? () => setRequestPath(entry.path) : undefined,
1484
+ title: entry.path || entry.name,
1485
+ role: isDir ? 'button' : undefined,
1486
+ },
1487
+ React.createElement('span', {
1488
+ className: isDir ? 'dsh-fe-marker dsh-fe-marker-dir' : 'dsh-fe-marker dsh-fe-marker-file',
1489
+ }, svgIcon(isDir ? folderIcon : fileIcon, 16)),
1490
+ React.createElement('span', { className: 'dsh-fe-name' }, entry.name),
1491
+ React.createElement('span', { className: 'dsh-fe-meta' },
1492
+ React.createElement('span', { className: 'dsh-fe-size' }, isDir ? '' : formatSize(entry.size)),
1493
+ React.createElement('span', { className: 'dsh-fe-actions' },
1494
+ !isDir && React.createElement('button', {
1495
+ type: 'button',
1496
+ className: 'dsh-fe-action',
1497
+ onClick: (event) => { event.stopPropagation(); openPreview(entry) },
1498
+ 'aria-label': `View ${entry.name}`,
1499
+ title: 'View',
1500
+ }, svgIcon(eyeIcon, 14)),
1501
+ !isDir && React.createElement('button', {
1502
+ type: 'button',
1503
+ className: 'dsh-fe-action',
1504
+ onClick: (event) => { event.stopPropagation(); downloadFile(entry) },
1505
+ 'aria-label': `Download ${entry.name}`,
1506
+ title: 'Download',
1507
+ }, svgIcon(downloadIcon, 14)),
1508
+ React.createElement('button', {
1509
+ type: 'button',
1510
+ className: pendingDelete === entry.path ? 'dsh-fe-action dsh-fe-action-confirm' : 'dsh-fe-action dsh-fe-action-danger',
1511
+ onClick: (event) => { event.stopPropagation(); requestDelete(entry) },
1512
+ 'aria-label': pendingDelete === entry.path ? `Confirm deleting ${entry.name}` : `Delete ${entry.name}`,
1513
+ title: pendingDelete === entry.path ? 'Click again to delete' : 'Delete',
1514
+ }, svgIcon(trashIcon, 14)),
1515
+ ),
1516
+ ),
1517
+ )
1518
+ }),
1519
+ ),
1520
+ )
1521
+
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
+ return React.createElement(React.Fragment, null,
1551
+ React.createElement('div', {
1552
+ className: 'dsh-fe-panel',
1553
+ role: 'region',
1554
+ 'aria-label': 'File explorer',
1555
+ },
1556
+ React.createElement('div', { className: 'dsh-fe-header' },
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,
1581
+ ),
1582
+ preview &&
1583
+ React.createElement('div', {
1584
+ className: 'dsh-fe-overlay',
1585
+ onClick: () => setPreview(null),
1586
+ },
1587
+ React.createElement('div', {
1588
+ className: 'dsh-fe-modal',
1589
+ role: 'dialog',
1590
+ 'aria-label': 'File preview',
1591
+ onClick: (event) => event.stopPropagation(),
1592
+ },
1593
+ React.createElement('div', { className: 'dsh-fe-modal-head' },
1594
+ 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)),
1596
+ React.createElement('button', {
1597
+ type: 'button',
1598
+ className: 'dsh-fe-icon',
1599
+ onClick: () => setPreview(null),
1600
+ 'aria-label': 'Close preview',
1601
+ }, svgIcon(closeIcon, 14)),
1602
+ ),
1603
+ React.createElement('div', { className: 'dsh-fe-modal-body' },
1604
+ previewLoading
1605
+ ? 'Loading...'
1606
+ : preview.error
1607
+ ? preview.error
1608
+ : preview.text !== undefined ? preview.text : preview.tooLarge ? 'File is too large to preview here' : '(No preview)',
1609
+ ),
1610
+ ),
1611
+ ),
1612
+ )
1613
+ }
1614
+
1615
+ ctx.effect(() => slots.inject('details', () => slots.register(
1616
+ { name: 'details', priority: -6 },
1617
+ FileExplorerPanel,
1618
+ )))
1619
+ }
1620
+
1621
+ exports.apply = apply;
1622
+ exports.inject = inject;
1623
+ return module.exports;
1624
+ }
1625
+ });