@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/README.md +103 -0
- package/client.js +1625 -0
- package/cordis.patch.yml +11 -0
- package/index.js +519 -0
- package/package.json +44 -0
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
|
+
});
|