pinokiod 8.0.37 → 8.0.39

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.
@@ -17,21 +17,50 @@
17
17
  --vault-warning: #b06a09;
18
18
  --vault-warning-soft: color-mix(in srgb, var(--vault-warning) 8%, var(--task-panel));
19
19
  --vault-selected: color-mix(in srgb, var(--task-accent) 10%, var(--task-panel));
20
+ --vault-inline: 18px;
21
+ --vault-control-height: 28px;
22
+ --vault-row-height: 44px;
23
+ --vault-tree-row: 26px;
24
+ --vault-tree-path-row: 34px;
20
25
  }
21
26
  body.dark.vault-page {
22
27
  --vault-warning: #dca451;
23
28
  --vault-warning-soft: color-mix(in srgb, var(--vault-warning) 8%, var(--task-panel));
24
29
  --vault-selected: color-mix(in srgb, var(--task-accent) 9%, var(--task-panel));
25
30
  }
26
- .vault-shell .task-shell-header { padding: 18px 24px 17px; }
27
- .vault-shell .task-shell-header-main { max-width: 900px; }
28
- .vault-shell .task-shell-body { padding: 0; }
29
- .vault-body { min-width: 0; color: var(--task-text); }
31
+ .vault-shell,
32
+ .vault-shell * { box-sizing: border-box; }
33
+ body.vault-page .task-container {
34
+ display: flex;
35
+ min-height: 0;
36
+ overflow: hidden;
37
+ }
38
+ .vault-shell {
39
+ display: flex;
40
+ min-height: 0;
41
+ flex: 1 1 auto;
42
+ flex-direction: column;
43
+ }
44
+ .vault-shell .task-shell-body {
45
+ display: block;
46
+ min-height: 0;
47
+ flex: 1 1 auto;
48
+ overflow: hidden;
49
+ padding: 0;
50
+ }
51
+ .vault-body {
52
+ display: flex;
53
+ height: 100%;
54
+ min-width: 0;
55
+ min-height: 0;
56
+ flex-direction: column;
57
+ color: var(--task-text);
58
+ }
30
59
  .vault-overview {
31
60
  display: flex;
32
- align-items: stretch;
33
- gap: 20px;
34
- padding: 18px 24px;
61
+ align-items: center;
62
+ gap: 10px;
63
+ padding: 7px var(--vault-inline);
35
64
  border-bottom: 1px solid var(--task-border);
36
65
  }
37
66
  .vault-metrics {
@@ -41,32 +70,34 @@ body.dark.vault-page {
41
70
  align-items: stretch;
42
71
  }
43
72
  .vault-metric {
44
- min-width: 112px;
45
- padding: 1px 22px;
73
+ min-width: 0;
74
+ padding: 0 12px;
75
+ border: 0;
46
76
  border-left: 1px solid var(--task-border);
47
77
  }
48
78
  .vault-metric:first-child { padding-left: 0; border-left: 0; }
49
79
  .vault-metric-value {
50
80
  display: block;
51
81
  color: var(--task-text);
52
- font-size: 17px;
53
- line-height: 1.25;
82
+ font-size: 15px;
83
+ line-height: 1.15;
54
84
  font-weight: 650;
55
85
  letter-spacing: -0.015em;
56
86
  white-space: nowrap;
57
87
  }
58
88
  .vault-metric-label {
59
89
  display: block;
60
- margin-top: 3px;
90
+ margin-top: 1px;
61
91
  color: var(--task-muted);
62
- font-size: 12px;
63
- line-height: 1.3;
92
+ font-size: 10.5px;
93
+ line-height: 1.2;
64
94
  white-space: nowrap;
65
95
  }
66
96
  button.vault-metric {
67
97
  appearance: none;
68
98
  background: transparent;
69
99
  color: inherit;
100
+ font: inherit;
70
101
  text-align: left;
71
102
  cursor: pointer;
72
103
  }
@@ -91,13 +122,13 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
91
122
  }
92
123
  .vault-button {
93
124
  display: inline-flex;
94
- min-height: 36px;
125
+ min-height: var(--vault-control-height);
95
126
  align-items: center;
96
127
  justify-content: center;
97
- gap: 8px;
98
- padding: 7px 14px;
99
- border-radius: 8px;
100
- font-size: 13px;
128
+ gap: 7px;
129
+ padding: 6px 12px;
130
+ border-radius: 7px;
131
+ font-size: 12px;
101
132
  line-height: 1;
102
133
  white-space: nowrap;
103
134
  }
@@ -109,9 +140,40 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
109
140
  }
110
141
  .vault-button.primary:hover { background: var(--task-accent-hover); }
111
142
  .vault-button:disabled { opacity: .5; cursor: default; }
143
+ .vault-icon-button {
144
+ width: var(--vault-control-height);
145
+ height: var(--vault-control-height);
146
+ display: inline-flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ padding: 0;
150
+ border-radius: 7px;
151
+ font-size: 12px;
152
+ }
153
+ .vault-icon-button:hover { background: var(--task-soft); }
154
+ .vault-advanced { position: relative; }
155
+ .vault-advanced > summary { list-style: none; cursor: pointer; }
156
+ .vault-advanced > summary::-webkit-details-marker { display: none; }
157
+ .vault-advanced-menu {
158
+ position: absolute;
159
+ z-index: 20;
160
+ top: calc(100% + 6px);
161
+ right: 0;
162
+ width: 272px;
163
+ box-sizing: border-box;
164
+ padding: 12px;
165
+ border: 1px solid var(--task-border-strong);
166
+ border-radius: 9px;
167
+ background: var(--task-panel);
168
+ box-shadow: 0 12px 30px color-mix(in srgb, var(--task-text) 10%, transparent);
169
+ }
170
+ .vault-advanced-title { color: var(--task-text); font-size: 12.5px; font-weight: 650; }
171
+ .vault-advanced-copy { margin: 4px 0 10px; color: var(--task-muted); font-size: 11px; line-height: 1.45; }
112
172
  .vault-button:focus-visible,
113
173
  .vault-icon-button:focus-visible,
114
174
  .vault-text-button:focus-visible,
175
+ button.vault-metric:focus-visible,
176
+ .vault-rail-add:focus-visible,
115
177
  .vault-nav-row:focus-visible,
116
178
  .vault-search input:focus-visible,
117
179
  .vault-select:focus-visible {
@@ -123,12 +185,16 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
123
185
  .vault-feedback {
124
186
  display: none;
125
187
  align-items: center;
126
- min-height: 46px;
127
- gap: 12px;
128
- padding: 8px 24px;
188
+ min-height: 38px;
189
+ gap: 9px;
190
+ padding: 6px var(--vault-inline);
129
191
  border-bottom: 1px solid var(--task-border);
130
192
  font-size: 12.5px;
131
193
  }
194
+ .vault-scan-state {
195
+ position: relative;
196
+ overflow: hidden;
197
+ }
132
198
  .vault-scan-state.show,
133
199
  .vault-result.show,
134
200
  .vault-feedback.show { display: flex; }
@@ -137,39 +203,117 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
137
203
  .vault-result strong { font-weight: 650; }
138
204
  .vault-result-detail,
139
205
  .vault-scan-detail { color: var(--task-muted); }
140
- .vault-result .vault-button { min-height: 30px; margin-left: auto; padding: 5px 11px; }
141
- .vault-feedback { min-height: 38px; background: var(--task-soft); }
206
+ .vault-scan-detail {
207
+ min-width: 0;
208
+ overflow: hidden;
209
+ text-overflow: ellipsis;
210
+ white-space: nowrap;
211
+ }
212
+ .vault-scan-percent {
213
+ flex: 0 0 auto;
214
+ margin-left: auto;
215
+ color: var(--task-muted);
216
+ font-size: 11.5px;
217
+ font-variant-numeric: tabular-nums;
218
+ font-weight: 650;
219
+ }
220
+ .vault-progress-track {
221
+ position: absolute;
222
+ right: 0;
223
+ bottom: 0;
224
+ left: 0;
225
+ height: 2px;
226
+ overflow: hidden;
227
+ background: color-mix(in srgb, var(--task-accent) 12%, transparent);
228
+ }
229
+ .vault-progress-bar {
230
+ display: block;
231
+ width: 100%;
232
+ height: 100%;
233
+ background: var(--task-accent);
234
+ transform-origin: left center;
235
+ }
236
+ .vault-progress-bar.determinate {
237
+ transform: scaleX(var(--vault-progress, 0));
238
+ transition: transform 320ms cubic-bezier(.22, 1, .36, 1);
239
+ }
240
+ .vault-progress-bar.indeterminate {
241
+ transform: scaleX(1);
242
+ animation: vault-progress-pulse 1.4s ease-in-out infinite;
243
+ }
244
+ @keyframes vault-progress-pulse {
245
+ 0%, 100% { opacity: .22; }
246
+ 50% { opacity: .78; }
247
+ }
248
+ .vault-result .vault-button { min-height: 28px; margin-left: auto; padding: 4px 10px; }
249
+ .vault-feedback { min-height: 34px; background: var(--task-soft); }
142
250
  .vault-feedback.error { color: var(--vault-warning); }
143
251
  .vault-explorer {
144
252
  display: grid;
145
- grid-template-columns: 278px minmax(0, 1fr);
146
- min-height: min(680px, calc(100vh - 248px));
253
+ min-height: 0;
254
+ flex: 1 1 auto;
255
+ grid-template-columns: 248px minmax(0, 1fr);
256
+ overflow: hidden;
147
257
  }
148
258
  .vault-rail {
149
259
  min-width: 0;
260
+ min-height: 0;
150
261
  border-right: 1px solid var(--task-border);
151
262
  background: color-mix(in srgb, var(--task-panel) 96%, var(--task-soft));
152
- overflow: auto;
263
+ overflow-x: hidden;
264
+ overflow-y: auto;
265
+ overscroll-behavior: contain;
266
+ }
267
+ .vault-rail-section {
268
+ display: block;
269
+ min-height: 0;
270
+ padding: 12px 9px;
153
271
  }
154
- .vault-rail-section { padding: 18px 12px; }
155
272
  .vault-rail-section + .vault-rail-section { border-top: 1px solid var(--task-border); }
156
273
  .vault-rail-label {
157
- margin: 0 8px 8px;
274
+ margin: 0 7px 6px;
158
275
  color: var(--task-muted);
159
276
  font-size: 10.5px;
160
277
  font-weight: 700;
161
278
  letter-spacing: .08em;
162
279
  }
163
- .vault-nav-list { display: grid; gap: 2px; }
280
+ .vault-rail-heading {
281
+ display: flex;
282
+ height: 20px;
283
+ align-items: center;
284
+ margin: 0 5px 4px 7px;
285
+ }
286
+ .vault-rail-heading .vault-rail-label { flex: 1 1 auto; margin: 0; }
287
+ .vault-rail-add {
288
+ width: 20px;
289
+ height: 20px;
290
+ display: inline-flex;
291
+ flex: 0 0 20px;
292
+ align-items: center;
293
+ justify-content: center;
294
+ padding: 0;
295
+ border: 0;
296
+ border-radius: 5px;
297
+ background: transparent;
298
+ color: var(--task-muted);
299
+ cursor: pointer;
300
+ font: inherit;
301
+ font-size: 10px;
302
+ }
303
+ .vault-rail-add:hover { background: var(--task-soft); color: var(--task-text); }
304
+ .vault-rail-add:disabled { opacity: .45; cursor: default; }
305
+ .vault-nav-list { display: grid; gap: 0; }
164
306
  .vault-nav-row {
165
307
  width: 100%;
166
308
  min-width: 0;
167
- min-height: 32px;
309
+ height: var(--vault-tree-row);
310
+ min-height: var(--vault-tree-row);
311
+ max-height: var(--vault-tree-row);
168
312
  display: grid;
169
313
  grid-template-columns: 18px minmax(0, 1fr) auto;
170
314
  align-items: center;
171
- gap: 8px;
172
- padding: 6px 8px;
315
+ gap: 6px;
316
+ padding: 2px 6px;
173
317
  border: 1px solid transparent;
174
318
  border-radius: 7px;
175
319
  background: transparent;
@@ -183,12 +327,12 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
183
327
  border-color: color-mix(in srgb, var(--task-accent) 72%, var(--task-border-strong));
184
328
  background: var(--vault-selected);
185
329
  }
186
- .vault-nav-row > i { width: 16px; color: var(--task-muted); text-align: center; font-size: 12px; }
330
+ .vault-nav-row > i { width: 15px; color: var(--task-muted); text-align: center; font-size: 11.5px; }
187
331
  .vault-nav-copy { min-width: 0; }
188
332
  .vault-nav-name {
189
333
  display: block;
190
334
  overflow: hidden;
191
- font-size: 12.5px;
335
+ font-size: 11.75px;
192
336
  font-weight: 520;
193
337
  line-height: 1.25;
194
338
  text-overflow: ellipsis;
@@ -197,9 +341,9 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
197
341
  .vault-nav-path {
198
342
  display: block;
199
343
  overflow: hidden;
200
- margin-top: 2px;
344
+ margin-top: 1px;
201
345
  color: var(--task-muted);
202
- font-size: 10.5px;
346
+ font-size: 10px;
203
347
  line-height: 1.25;
204
348
  text-overflow: ellipsis;
205
349
  white-space: nowrap;
@@ -210,16 +354,35 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
210
354
  font-variant-numeric: tabular-nums;
211
355
  }
212
356
  .vault-nav-count.attention { color: var(--vault-warning); font-weight: 650; }
357
+ #vault-locations {
358
+ display: flex;
359
+ min-height: 0;
360
+ flex-direction: column;
361
+ align-items: stretch;
362
+ justify-content: flex-start;
363
+ gap: 0;
364
+ }
213
365
  .vault-source-line {
214
366
  display: grid;
367
+ flex: 0 0 auto;
368
+ height: var(--vault-tree-row);
369
+ min-height: var(--vault-tree-row);
370
+ max-height: var(--vault-tree-row);
215
371
  grid-template-columns: 18px minmax(0, 1fr);
216
372
  align-items: start;
373
+ align-self: stretch;
374
+ margin: 0;
375
+ }
376
+ .vault-source-line.has-path {
377
+ height: var(--vault-tree-path-row);
378
+ min-height: var(--vault-tree-path-row);
379
+ max-height: var(--vault-tree-path-row);
217
380
  }
218
- .vault-source-line.depth-1 { padding-left: 18px; }
219
- .vault-source-line.depth-2 { padding-left: 36px; }
381
+ .vault-source-line.depth-1 { padding-left: 12px; }
382
+ .vault-source-line.depth-2 { padding-left: 24px; }
220
383
  .vault-source-toggle {
221
384
  width: 18px;
222
- height: 32px;
385
+ height: 100%;
223
386
  padding: 0;
224
387
  border: 0;
225
388
  background: transparent;
@@ -229,7 +392,12 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
229
392
  }
230
393
  .vault-source-toggle.placeholder { cursor: default; }
231
394
  .vault-source-line .vault-nav-row { grid-template-columns: 18px minmax(0, 1fr) auto; }
232
- .vault-rail-footer { padding: 0 20px 18px; }
395
+ .vault-source-line.has-path .vault-nav-row {
396
+ height: var(--vault-tree-path-row);
397
+ min-height: var(--vault-tree-path-row);
398
+ max-height: var(--vault-tree-path-row);
399
+ }
400
+ .vault-rail-footer { padding: 0 16px 12px; }
233
401
  .vault-text-button {
234
402
  padding: 2px 0;
235
403
  border: 0;
@@ -238,49 +406,27 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
238
406
  font-size: 11.5px;
239
407
  }
240
408
  .vault-text-button:hover { color: var(--task-text); }
241
- .vault-pane { min-width: 0; overflow: hidden; }
242
- .vault-pane-head {
409
+ .vault-pane {
243
410
  display: grid;
244
- grid-template-columns: minmax(0, 1fr) auto;
245
- align-items: center;
246
- gap: 16px;
247
- min-height: 112px;
248
- padding: 18px 24px 16px;
249
- border-bottom: 1px solid var(--task-border);
250
- }
251
- .vault-breadcrumb {
252
- margin-bottom: 8px;
253
- color: var(--task-muted);
254
- font-size: 11.5px;
255
- line-height: 1.4;
256
- }
257
- .vault-pane-title {
258
- margin: 0;
259
- color: var(--task-text);
260
- font-size: 18px;
261
- font-weight: 680;
262
- letter-spacing: -.015em;
263
- }
264
- .vault-pane-subtitle {
265
- margin: 5px 0 0;
266
- color: var(--task-muted);
267
- font-size: 12px;
268
- line-height: 1.45;
411
+ min-width: 0;
412
+ min-height: 0;
413
+ grid-template-rows: auto minmax(0, 1fr) auto;
414
+ overflow: hidden;
269
415
  }
270
- .vault-pane-action-note { color: var(--task-muted); font-size: 11.5px; text-align: right; }
416
+ .vault-pane-action-note { color: var(--task-muted); font-size: 11.5px; white-space: nowrap; }
271
417
  .vault-toolbar {
272
418
  display: flex;
273
419
  align-items: center;
274
- gap: 10px;
275
- min-height: 56px;
276
- padding: 10px 24px;
420
+ gap: 8px;
421
+ min-height: 44px;
422
+ padding: 6px var(--vault-inline);
277
423
  border-bottom: 1px solid var(--task-border);
278
424
  }
279
425
  .vault-search { position: relative; width: min(320px, 55%); }
280
426
  .vault-search i {
281
427
  position: absolute;
282
428
  top: 50%;
283
- left: 12px;
429
+ left: 10px;
284
430
  color: var(--task-muted);
285
431
  font-size: 12px;
286
432
  transform: translateY(-50%);
@@ -288,21 +434,25 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
288
434
  }
289
435
  .vault-search input,
290
436
  .vault-select {
291
- height: 34px;
437
+ height: 30px;
292
438
  box-sizing: border-box;
293
439
  border: 1px solid var(--task-border-strong);
294
440
  border-radius: 7px;
295
441
  background: var(--task-panel);
296
442
  color: var(--task-text);
297
443
  font: inherit;
298
- font-size: 12.5px;
444
+ font-size: 12px;
299
445
  }
300
- .vault-search input { width: 100%; padding: 6px 12px 6px 34px; }
446
+ .vault-search input { width: 100%; padding: 5px 10px 5px 30px; }
301
447
  .vault-search input::placeholder { color: var(--task-muted); }
302
- .vault-select { padding: 5px 30px 5px 10px; }
448
+ .vault-select { padding: 4px 28px 4px 9px; }
303
449
  .vault-toolbar-count { margin-left: auto; color: var(--task-muted); font-size: 11.5px; white-space: nowrap; }
304
450
  .vault-table { min-width: 660px; }
305
- .vault-table-wrap { overflow: auto; }
451
+ .vault-table-wrap {
452
+ min-height: 0;
453
+ overflow: auto;
454
+ overscroll-behavior: contain;
455
+ }
306
456
  .vault-columns,
307
457
  .vault-file-row,
308
458
  .vault-group-row {
@@ -317,24 +467,24 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
317
467
  grid-template-columns: minmax(210px, 1.5fr) minmax(64px, .4fr) minmax(140px, 1fr) minmax(96px, .55fr) minmax(76px, auto);
318
468
  }
319
469
  .vault-columns {
320
- min-height: 38px;
321
- padding: 0 20px;
470
+ min-height: 34px;
471
+ padding: 0 var(--vault-inline);
322
472
  border-bottom: 1px solid var(--task-border);
323
473
  color: var(--task-muted);
324
474
  font-size: 10.5px;
325
475
  font-weight: 600;
326
476
  }
327
477
  .vault-file-row {
328
- min-height: 52px;
329
- padding: 0 20px;
478
+ min-height: var(--vault-row-height);
479
+ padding: 6px var(--vault-inline);
330
480
  border-bottom: 1px solid var(--task-border);
331
481
  font-size: 12px;
332
482
  }
333
483
  .vault-file-row:hover { background: color-mix(in srgb, var(--task-soft) 64%, transparent); }
334
- .vault-file-row.directory { min-height: 46px; background: color-mix(in srgb, var(--task-soft) 38%, transparent); }
484
+ .vault-file-row.directory { min-height: 38px; padding-top: 4px; padding-bottom: 4px; background: color-mix(in srgb, var(--task-soft) 38%, transparent); }
335
485
  .vault-group-row {
336
- min-height: 58px;
337
- padding: 0 20px;
486
+ min-height: 48px;
487
+ padding: 6px var(--vault-inline);
338
488
  border-bottom: 1px solid var(--task-border-strong);
339
489
  background: color-mix(in srgb, var(--task-soft) 56%, transparent);
340
490
  }
@@ -343,18 +493,18 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
343
493
  display: flex;
344
494
  min-width: 0;
345
495
  align-items: center;
346
- gap: 9px;
496
+ gap: 8px;
347
497
  color: var(--task-text);
348
498
  font-size: 12.5px;
349
499
  font-weight: 600;
350
500
  }
351
501
  .vault-group-title span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
352
- .vault-group-meta { margin: 3px 0 0 24px; color: var(--task-muted); font-size: 10.5px; }
502
+ .vault-group-meta { margin: 2px 0 0 23px; color: var(--task-muted); font-size: 10px; }
353
503
  .vault-group-action { grid-column: 5; justify-self: end; }
354
- .vault-group-action .vault-button { min-height: 32px; padding: 5px 11px; }
355
- .vault-name-cell { display: flex; min-width: 0; align-items: center; gap: 9px; }
356
- .vault-name-cell.indent-1 { padding-left: 22px; }
357
- .vault-name-cell.indent-2 { padding-left: 44px; }
504
+ .vault-group-action .vault-button { min-height: 28px; padding: 4px 9px; }
505
+ .vault-name-cell { display: flex; min-width: 0; align-items: center; gap: 8px; }
506
+ .vault-name-cell.indent-1 { padding-left: 18px; }
507
+ .vault-name-cell.indent-2 { padding-left: 36px; }
358
508
  .vault-name-icon { width: 16px; flex: 0 0 16px; color: var(--task-muted); text-align: center; }
359
509
  .vault-disclosure {
360
510
  width: 16px;
@@ -372,9 +522,9 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
372
522
  display: block;
373
523
  overflow: hidden;
374
524
  color: var(--task-text);
375
- font-size: 12.25px;
525
+ font-size: 12px;
376
526
  font-weight: 520;
377
- line-height: 1.35;
527
+ line-height: 1.3;
378
528
  text-overflow: ellipsis;
379
529
  white-space: nowrap;
380
530
  }
@@ -383,7 +533,7 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
383
533
  overflow: hidden;
384
534
  color: var(--task-muted);
385
535
  font-size: 10.5px;
386
- line-height: 1.4;
536
+ line-height: 1.3;
387
537
  text-overflow: ellipsis;
388
538
  white-space: nowrap;
389
539
  }
@@ -392,7 +542,7 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
392
542
  min-width: 0;
393
543
  color: var(--task-muted);
394
544
  font-size: 10.5px;
395
- line-height: 1.4;
545
+ line-height: 1.35;
396
546
  overflow-wrap: anywhere;
397
547
  white-space: normal;
398
548
  word-break: break-word;
@@ -406,47 +556,65 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
406
556
  .vault-row-action { justify-self: end; }
407
557
  .vault-row-action .vault-text-button { color: var(--task-text); font-size: 11.5px; }
408
558
  .vault-detail {
409
- padding: 12px 24px 14px 64px;
559
+ padding: 8px var(--vault-inline) 9px 56px;
410
560
  border-bottom: 1px solid var(--task-border);
411
561
  background: color-mix(in srgb, var(--task-soft) 48%, transparent);
412
562
  }
413
- .vault-detail-label { margin-bottom: 7px; color: var(--task-muted); font-size: 10.5px; font-weight: 650; }
414
- .vault-location-detail { display: flex; align-items: center; gap: 8px; padding: 3px 0; color: var(--task-text); font-size: 11px; }
563
+ .vault-detail-label { margin-bottom: 5px; color: var(--task-muted); font-size: 10px; font-weight: 650; }
564
+ .vault-location-detail { display: flex; align-items: center; gap: 7px; padding: 2px 0; color: var(--task-text); font-size: 10.5px; }
415
565
  .vault-location-detail i { color: var(--task-muted); }
416
566
  .vault-empty {
417
567
  display: grid;
418
- min-height: 260px;
568
+ min-height: 190px;
419
569
  place-items: center;
420
- padding: 40px;
570
+ padding: 28px;
421
571
  text-align: center;
422
572
  }
423
573
  .vault-empty-inner { max-width: 420px; }
424
574
  .vault-empty i { color: var(--task-muted); font-size: 18px; }
425
- .vault-empty h3 { margin: 12px 0 5px; font-size: 15px; }
575
+ .vault-empty h3 { margin: 9px 0 4px; font-size: 14px; }
426
576
  .vault-empty p { margin: 0; color: var(--task-muted); font-size: 12px; line-height: 1.55; }
427
- .vault-empty .vault-button { margin-top: 14px; }
577
+ .vault-empty .vault-button { margin-top: 10px; }
428
578
  .vault-pane-footer {
429
- padding: 14px 24px 16px;
579
+ padding: 9px var(--vault-inline) 11px;
430
580
  color: var(--task-muted);
431
581
  font-size: 10.5px;
432
582
  }
433
583
  .vault-event-kind { font-weight: 570; }
434
584
  .vault-event-time { color: var(--task-muted); font-size: 10.5px; }
435
585
  .vault-unavailable { color: var(--task-muted); font-size: 11px; }
436
- @media (max-width: 1180px) {
586
+ @media (max-width: 1140px) {
437
587
  .vault-overview { align-items: flex-start; flex-direction: column; }
438
588
  .vault-overview-actions { width: 100%; justify-content: flex-start; }
439
- .vault-metric { padding-right: 16px; padding-left: 16px; }
440
- .vault-explorer { grid-template-columns: 236px minmax(0, 1fr); }
589
+ .vault-metric { padding-right: 12px; padding-left: 12px; }
590
+ .vault-explorer { grid-template-columns: 224px minmax(0, 1fr); }
441
591
  }
442
592
  @media (max-width: 820px) {
593
+ body.vault-page .task-container { display: block; overflow-y: auto; }
594
+ .vault-shell,
595
+ .vault-shell .task-shell-body,
596
+ .vault-body { height: auto; overflow: visible; }
443
597
  .vault-metrics { width: 100%; overflow-x: auto; }
444
- .vault-explorer { display: block; }
598
+ .vault-explorer { display: block; overflow: visible; }
445
599
  .vault-rail { max-height: 330px; border-right: 0; border-bottom: 1px solid var(--task-border); }
446
- .vault-pane-head { min-height: 94px; }
600
+ .vault-pane { display: block; overflow: visible; }
601
+ .vault-table-wrap { overflow-x: auto; overflow-y: visible; }
602
+ }
603
+ @media (pointer: coarse) {
604
+ .vault-button,
605
+ .vault-result .vault-button,
606
+ .vault-group-action .vault-button { min-height: 40px; }
607
+ .vault-icon-button { width: 40px; height: 40px; }
608
+ .vault-rail-add { width: 28px; height: 28px; flex-basis: 28px; }
447
609
  }
448
610
  @media (prefers-reduced-motion: reduce) {
449
611
  .vault-page * { scroll-behavior: auto !important; }
612
+ .vault-progress-bar { transition: none; }
613
+ .vault-progress-bar.indeterminate {
614
+ animation: none;
615
+ opacity: .5;
616
+ transform: scaleX(1);
617
+ }
450
618
  }
451
619
  </style>
452
620
  </head>
@@ -454,20 +622,21 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
454
622
  <%- include('partials/app_navheader', { agent }) %>
455
623
  <main>
456
624
  <div class='task-container'>
457
- <section class='task-shell vault-shell'>
458
- <header class='task-shell-header'>
459
- <div class='task-shell-header-main'>
460
- <h1 class='task-title' id='vault-title'></h1>
461
- <p class='task-description' id='vault-subtitle'></p>
462
- </div>
463
- </header>
625
+ <section class='task-shell vault-shell'>
464
626
  <div class='task-shell-body'>
465
627
  <div class='vault-body'>
466
628
  <section class='vault-overview'>
467
629
  <div class='vault-metrics' id='vault-metrics'></div>
468
630
  <div class='vault-overview-actions'>
469
631
  <button class='vault-button primary' id='btn-scan' type='button'></button>
470
- <button class='vault-button' id='btn-rebuild' type='button'></button>
632
+ <details class='vault-advanced' id='vault-advanced'>
633
+ <summary class='vault-icon-button' id='btn-vault-options'><i class='fa-solid fa-ellipsis'></i></summary>
634
+ <div class='vault-advanced-menu'>
635
+ <div class='vault-advanced-title' id='vault-repair-title'></div>
636
+ <p class='vault-advanced-copy' id='vault-repair-description'></p>
637
+ <button class='vault-button' id='btn-repair' type='button'></button>
638
+ </div>
639
+ </details>
471
640
  </div>
472
641
  </section>
473
642
  <div class='vault-scan-state' id='vault-scan-state'></div>
@@ -480,13 +649,15 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
480
649
  <div class='vault-nav-list' id='vault-views'></div>
481
650
  </div>
482
651
  <div class='vault-rail-section'>
483
- <div class='vault-rail-label' id='locations-label'></div>
652
+ <div class='vault-rail-heading'>
653
+ <div class='vault-rail-label' id='locations-label'></div>
654
+ <button class='vault-rail-add' id='btn-add-source' type='button' aria-label='Add external folder' title='Add external folder'><i class='fa-solid fa-plus'></i></button>
655
+ </div>
484
656
  <div id='vault-locations'></div>
485
657
  </div>
486
658
  <div class='vault-rail-footer' id='vault-rail-footer'></div>
487
659
  </aside>
488
- <section class='vault-pane'>
489
- <header class='vault-pane-head' id='vault-pane-head'></header>
660
+ <section class='vault-pane' aria-label='Vault files'>
490
661
  <div class='vault-toolbar' id='vault-toolbar'></div>
491
662
  <div class='vault-table-wrap' id='vault-table-wrap'></div>
492
663
  <footer class='vault-pane-footer' id='vault-pane-footer'></footer>
@@ -498,6 +669,7 @@ button.vault-metric:focus-visible .vault-metric-label { color: var(--task-text);
498
669
  </div>
499
670
  <%- include('partials/main_sidebar', { selected: 'vault' }) %>
500
671
  </main>
672
+ <script src="/Socket.js"></script>
501
673
  <script>
502
674
  const COPY = {
503
675
  title: "Vault",
@@ -513,8 +685,16 @@ const COPY = {
513
685
  pinokio: "Pinokio",
514
686
  apps: "Apps",
515
687
  external: "External folders",
688
+ add_external_folder: "Add external folder",
689
+ external_added: "Added to Locations. Run a scan when you’re ready.",
690
+ external_exists: "That folder is already in Locations.",
691
+ external_other_disk: "Added to Locations. It can be scanned, but this disk cannot share space with the Vault.",
516
692
  on_disk: "On disk",
517
693
  saved: "Saved",
694
+ already_shared: "Already shared",
695
+ already_shared_help: "Space already avoided by files sharing the same data",
696
+ saved_by_vault: "Saved by Vault",
697
+ saved_by_vault_help: "Space saved through your Deduplicate actions",
518
698
  can_save: "Can save",
519
699
  pinokio_folder: "Pinokio folder",
520
700
  last_scanned: "Last scanned",
@@ -522,9 +702,18 @@ const COPY = {
522
702
  scan: "Scan now",
523
703
  scan_again: "Scan again",
524
704
  scanning: "Scanning…",
525
- rebuild: "Rebuild index",
705
+ vault_options: "Vault options",
706
+ repair_index: "Repair Vault index",
707
+ repair_action: "Repair index",
708
+ repair_description: "Reconstruct Vault’s internal records if tracked files or sharing status look incorrect. This does not scan for new duplicates.",
709
+ repairing: "Repairing…",
710
+ repair_done: "Vault index repaired",
526
711
  scan_progress: "Scanning your configured locations",
712
+ scan_analyzing: "Analyzing large files",
713
+ scan_finishing: "Finishing scan",
714
+ scan_estimate_help: "Estimated from your last completed scan",
527
715
  scan_folders: "folders checked",
716
+ scan_files: "files checked",
528
717
  analyzing: "analyzing",
529
718
  waiting: "files waiting",
530
719
  scan_complete: "Scan complete",
@@ -571,9 +760,11 @@ const COPY = {
571
760
  reclaim: "Reclaim",
572
761
  reclaim_all: "Reclaim all",
573
762
  undo: "Undo",
574
- matching_locations: "Matching locations",
763
+ identical_contents_at: "Identical contents at",
575
764
  no_files: "Nothing tracked yet",
576
765
  no_files_hint: "Run a scan to find large files. Scanning never changes them.",
766
+ scan_waiting: "Waiting for scan results",
767
+ scan_waiting_hint: "Tracked files will appear here when this scan finishes.",
577
768
  no_duplicates: "Everything is already shared",
578
769
  no_duplicates_hint: "There are no files waiting for your review.",
579
770
  no_shared: "Nothing is shared yet",
@@ -616,8 +807,7 @@ const state = {
616
807
  scanRequested: false,
617
808
  scanBaseline: null,
618
809
  scanResult: null,
619
- feedback: null,
620
- priorScanning: false
810
+ feedback: null
621
811
  }
622
812
 
623
813
  const el = (id) => document.getElementById(id)
@@ -664,7 +854,7 @@ const sourcePath = (source) => {
664
854
  if (!source) return ""
665
855
  if (source.kind === "pinokio") return `~/${basename(source.root)}`
666
856
  if (source.kind === "external") return source.target_path || source.display_path || ""
667
- if (source.kind === "app") return "api"
857
+ if (source.kind === "app") return ""
668
858
  return source.display_path || source.root || ""
669
859
  }
670
860
  const isDescendantSource = (candidateId, parentId) => {
@@ -710,7 +900,7 @@ const buildItems = () => {
710
900
  items.push({
711
901
  kind: "file", path: independent.path, relative_path: independent.relative_path || basename(independent.path),
712
902
  source_id: independent.source_id, source_kind: independent.source_kind, source_label: independent.source_label,
713
- size: 0, status: "independent", saved: 0
903
+ size: independent.size || 0, status: "independent", saved: 0
714
904
  })
715
905
  }
716
906
  return items
@@ -780,7 +970,7 @@ const renderSourceNode = (source, depth, items) => {
780
970
  const pathText = source.kind === "virtual" ? (source.id === "apps" ? "api" : "") : sourcePath(source)
781
971
  const count = state.view === "duplicates" ? duplicateCount : trackedCount
782
972
  const icon = source.kind === "app" ? "fa-regular fa-folder-open" : "fa-regular fa-folder"
783
- let html = `<div class="vault-source-line depth-${Math.min(depth, 2)}">`
973
+ let html = `<div class="vault-source-line depth-${Math.min(depth, 2)} ${pathText ? "has-path" : ""}">`
784
974
  if (children.length) {
785
975
  html += `<button class="vault-source-toggle" type="button" data-toggle-source="${attr(source.id)}" aria-label="${collapsed ? COPY.expand : COPY.collapse}"><i class="fa-solid fa-chevron-${collapsed ? "right" : "down"}"></i></button>`
786
976
  } else {
@@ -808,24 +998,6 @@ const renderLocations = (items) => {
808
998
  }
809
999
 
810
1000
  const selectedSource = () => state.sourceId ? sourceById(state.sourceId) : null
811
- const viewTitle = () => {
812
- const source = selectedSource()
813
- if (state.view === "all" && source) return source.label
814
- return viewLabel[state.view]
815
- }
816
- const breadcrumb = () => {
817
- const source = selectedSource()
818
- if (!source) return ""
819
- const parts = [source.label]
820
- let current = source
821
- const seen = new Set()
822
- while (current && current.parent_id && !seen.has(current.id)) {
823
- seen.add(current.id)
824
- current = sourceById(current.parent_id)
825
- if (current) parts.unshift(current.label)
826
- }
827
- return parts.join(" / ")
828
- }
829
1001
  const scopeDuplicates = (sourceId) => state.data.duplicates.filter((item) => item.source_id === sourceId)
830
1002
  const batchAction = (source) => {
831
1003
  if (!source || source.kind === "virtual" || source.kind === "pinokio") return ""
@@ -836,29 +1008,24 @@ const batchAction = (source) => {
836
1008
  return `<button class="vault-button" type="button" data-deduplicate-scope="${attr(source.id)}">${esc(COPY.deduplicate)} ${countLabel(shareable.length)}</button>`
837
1009
  }
838
1010
 
839
- const renderPaneHead = (visibleItems) => {
840
- const source = selectedSource()
841
- const title = viewTitle()
842
- let subtitle = ""
1011
+ const toolbarSummary = (visibleItems) => {
843
1012
  if (state.view === "duplicates") {
844
1013
  const locations = new Set(visibleItems.map((item) => item.source_id).filter(Boolean)).size
845
1014
  const bytes = visibleItems.filter((item) => item.shareable).reduce((sum, item) => sum + item.size, 0)
846
- subtitle = `${countLabel(visibleItems.length)} in ${countLabel(locations, COPY.location, COPY.locations_lower)} · ${fmt(bytes)} ${COPY.can_be_saved}`
1015
+ return `${countLabel(visibleItems.length)} · ${countLabel(locations, COPY.location, COPY.locations_lower)} · ${fmt(bytes)} ${COPY.can_save_suffix}`
847
1016
  } else if (state.view === "activity") {
848
- subtitle = countLabel(state.data.events.length, COPY.event, COPY.events)
1017
+ return countLabel(visibleItems.length, COPY.event, COPY.events)
849
1018
  } else if (state.view === "reclaimable") {
850
- subtitle = `${countLabel(state.data.blobs.filter((blob) => blob.orphan).length)} · ${fmt(state.data.reclaimable)}`
851
- } else {
852
- const duplicateCount = visibleItems.filter((item) => item.status === "duplicate").length
853
- const saveable = visibleItems.filter((item) => item.status === "duplicate" && item.shareable).reduce((sum, item) => sum + item.size, 0)
854
- subtitle = `${countLabel(visibleItems.length)}${duplicateCount ? ` · ${countLabel(duplicateCount, COPY.duplicate.toLowerCase(), COPY.duplicates.toLowerCase())} · ${fmt(saveable)} ${COPY.can_save_suffix}` : ""}`
1019
+ return `${countLabel(visibleItems.length)} · ${fmt(state.data.reclaimable)}`
855
1020
  }
856
- const crumb = breadcrumb()
857
- el("vault-pane-head").innerHTML = `<div>
858
- ${crumb ? `<div class="vault-breadcrumb">${esc(crumb)}</div>` : ""}
859
- <h2 class="vault-pane-title">${esc(title)}</h2>
860
- <p class="vault-pane-subtitle">${esc(subtitle)}</p>
861
- </div><div>${state.view === "all" ? batchAction(source) : ""}</div>`
1021
+ const duplicateCount = visibleItems.filter((item) => item.status === "duplicate").length
1022
+ const saveable = visibleItems.filter((item) => item.status === "duplicate" && item.shareable).reduce((sum, item) => sum + item.size, 0)
1023
+ return `${countLabel(visibleItems.length)}${duplicateCount ? ` · ${countLabel(duplicateCount, COPY.duplicate.toLowerCase(), COPY.duplicates.toLowerCase())} · ${fmt(saveable)} ${COPY.can_save_suffix}` : ""}`
1024
+ }
1025
+
1026
+ const updateToolbarSummary = (visibleItems) => {
1027
+ const summary = el("vault-toolbar-summary")
1028
+ if (summary) summary.textContent = toolbarSummary(visibleItems)
862
1029
  }
863
1030
 
864
1031
  const searchPlaceholder = () => {
@@ -869,14 +1036,15 @@ const searchPlaceholder = () => {
869
1036
  const source = selectedSource()
870
1037
  return source && source.kind === "app" ? `Search in ${source.label}` : COPY.search_all
871
1038
  }
872
- const renderToolbar = (visibleCount) => {
1039
+ const renderToolbar = (visibleItems) => {
873
1040
  if (state.view === "reclaimable") {
874
1041
  const count = state.data.blobs.filter((blob) => blob.orphan).length
875
1042
  el("vault-toolbar").innerHTML = count
876
- ? `<span class="vault-toolbar-count">${fmt(state.data.reclaimable)}</span><button class="vault-button" type="button" id="btn-reclaim-all">${esc(COPY.reclaim_all)}</button>`
1043
+ ? `<span class="vault-toolbar-count" id="vault-toolbar-summary">${esc(toolbarSummary(visibleItems))}</span><button class="vault-button" type="button" id="btn-reclaim-all">${esc(COPY.reclaim_all)}</button>`
877
1044
  : ""
878
1045
  return
879
1046
  }
1047
+ const source = selectedSource()
880
1048
  el("vault-toolbar").innerHTML = `<label class="vault-search"><i class="fa-solid fa-magnifying-glass"></i><input id="vault-search" value="${attr(state.query)}" placeholder="${attr(searchPlaceholder())}" /></label>
881
1049
  ${state.view === "all" ? `<select class="vault-select" id="vault-status-filter" aria-label="${attr(COPY.all_statuses)}">
882
1050
  <option value="all" ${state.statusFilter === "all" ? "selected" : ""}>${esc(COPY.all_statuses)}</option>
@@ -885,7 +1053,8 @@ const renderToolbar = (visibleCount) => {
885
1053
  <option value="tracked" ${state.statusFilter === "tracked" ? "selected" : ""}>${esc(COPY.tracked)}</option>
886
1054
  <option value="independent" ${state.statusFilter === "independent" ? "selected" : ""}>${esc(COPY.independent)}</option>
887
1055
  </select>` : state.view === "duplicates" ? `<span class="vault-select">${esc(COPY.by_location)}</span>` : ""}
888
- <span class="vault-toolbar-count">${countLabel(visibleCount)}</span>`
1056
+ <span class="vault-toolbar-count" id="vault-toolbar-summary">${esc(toolbarSummary(visibleItems))}</span>
1057
+ ${state.view === "all" ? batchAction(source) : ""}`
889
1058
  }
890
1059
 
891
1060
  const statusMarkup = (item) => {
@@ -911,7 +1080,7 @@ const rowAction = (item) => {
911
1080
 
912
1081
  const fileDetail = (item) => {
913
1082
  if (!state.expandedFiles.has(item.path) || !item.locations || item.locations.length < 2) return ""
914
- return `<div class="vault-detail"><div class="vault-detail-label">${esc(COPY.matching_locations)}</div>${item.locations.map((location) => `
1083
+ return `<div class="vault-detail"><div class="vault-detail-label">${esc(COPY.identical_contents_at)} ${countLabel(item.locations.length, COPY.location, COPY.locations_lower)}</div>${item.locations.map((location) => `
915
1084
  <div class="vault-location-detail"><i class="fa-regular fa-file"></i><span>${esc(location.source_label || "")}${location.relative_path ? ` / ${esc(location.relative_path)}` : ""}</span></div>`).join("")}</div>`
916
1085
  }
917
1086
 
@@ -1032,15 +1201,18 @@ const renderInventoryGroups = (items) => {
1032
1201
  }
1033
1202
 
1034
1203
  const emptyState = (view) => {
1204
+ const scanning = scanActive(state.data && state.data.scan)
1035
1205
  const content = {
1036
- all: [COPY.no_files, COPY.no_files_hint, "fa-regular fa-folder-open"],
1206
+ all: scanning
1207
+ ? [COPY.scan_waiting, COPY.scan_waiting_hint, "fa-solid fa-circle-notch fa-spin"]
1208
+ : [COPY.no_files, COPY.no_files_hint, "fa-regular fa-folder-open"],
1037
1209
  duplicates: [COPY.no_duplicates, COPY.no_duplicates_hint, "fa-regular fa-circle-check"],
1038
1210
  shared: [COPY.no_shared, COPY.no_shared_hint, "fa-solid fa-link"],
1039
1211
  independent: [COPY.no_independent, COPY.no_independent_hint, "fa-solid fa-code-branch"],
1040
1212
  reclaimable: [COPY.no_reclaimable, COPY.no_reclaimable_hint, "fa-regular fa-circle-check"],
1041
1213
  activity: [COPY.no_activity, COPY.no_activity_hint, "fa-solid fa-wave-square"]
1042
1214
  }[view]
1043
- return `<div class="vault-empty"><div class="vault-empty-inner"><i class="${content[2]}"></i><h3>${esc(content[0])}</h3><p>${esc(content[1])}</p>${view === "duplicates" ? `<button class="vault-button" type="button" data-view="all">${esc(COPY.view_all)}</button>` : view === "all" ? `<button class="vault-button" type="button" id="btn-empty-scan">${esc(COPY.scan)}</button>` : ""}</div></div>`
1215
+ return `<div class="vault-empty"><div class="vault-empty-inner"><i class="${content[2]}"></i><h3>${esc(content[0])}</h3><p>${esc(content[1])}</p>${view === "duplicates" ? `<button class="vault-button" type="button" data-view="all">${esc(COPY.view_all)}</button>` : view === "all" && !scanning ? `<button class="vault-button" type="button" id="btn-empty-scan">${esc(COPY.scan)}</button>` : ""}</div></div>`
1044
1216
  }
1045
1217
 
1046
1218
  const renderReclaimable = () => {
@@ -1090,26 +1262,47 @@ const renderTable = (items) => {
1090
1262
  const renderOverview = () => {
1091
1263
  const data = state.data
1092
1264
  const last = data.last_scan
1093
- el("vault-title").textContent = COPY.title
1094
- el("vault-subtitle").textContent = data.enabled ? COPY.subtitle : COPY.disabled
1095
1265
  const folderMetric = last ? `<div class="vault-metric"><span class="vault-metric-value">${fmt(last.home_bytes_total == null ? last.bytes_total : last.home_bytes_total)}</span><span class="vault-metric-label">${esc(COPY.pinokio_folder)}</span></div>` : ""
1096
1266
  el("vault-metrics").innerHTML = `${folderMetric}
1097
1267
  <div class="vault-metric" title="${attr(`${fmt(data.bytes_without_sharing)} ${COPY.without_sharing}`)}"><span class="vault-metric-value">${fmt(data.bytes_on_disk)}</span><span class="vault-metric-label">${esc(COPY.on_disk)}</span></div>
1098
- <div class="vault-metric" title="${attr(`${fmt(data.lifetime_bytes_saved)} ${COPY.saved_all_time}`)}"><span class="vault-metric-value">${fmt(data.saved_by_sharing)}</span><span class="vault-metric-label">${esc(COPY.saved)}</span></div>
1268
+ <div class="vault-metric" title="${attr(COPY.already_shared_help)}"><span class="vault-metric-value">${fmt(data.saved_by_sharing)}</span><span class="vault-metric-label">${esc(COPY.already_shared)}</span></div>
1269
+ <div class="vault-metric" title="${attr(COPY.saved_by_vault_help)}"><span class="vault-metric-value">${fmt(data.lifetime_bytes_saved)}</span><span class="vault-metric-label">${esc(COPY.saved_by_vault)}</span></div>
1099
1270
  <button class="vault-metric" type="button" id="btn-review-metric"><span class="vault-metric-value">${fmt(data.pending_bytes)}</span><span class="vault-metric-label">${esc(COPY.can_save)}</span></button>
1100
1271
  <div class="vault-metric"><span class="vault-metric-value">${esc(timeAgo(last && last.ts))}</span><span class="vault-metric-label">${esc(COPY.last_scanned)}</span></div>`
1101
1272
  const scanning = data.scan && (data.scan.active || data.scan.queued > 0)
1102
1273
  el("btn-scan").innerHTML = scanning ? `<i class="fa-solid fa-circle-notch fa-spin"></i>${esc(COPY.scanning)}` : `<i class="fa-solid fa-rotate"></i>${esc(last ? COPY.scan_again : COPY.scan)}`
1103
1274
  el("btn-scan").disabled = !!scanning
1104
- el("btn-rebuild").textContent = COPY.rebuild
1275
+ el("btn-vault-options").setAttribute("aria-label", COPY.vault_options)
1276
+ el("vault-repair-title").textContent = COPY.repair_index
1277
+ el("vault-repair-description").textContent = COPY.repair_description
1278
+ el("btn-repair").textContent = COPY.repair_action
1279
+ el("btn-repair").disabled = !!scanning
1105
1280
  const scanState = el("vault-scan-state")
1106
1281
  if (scanning) {
1107
1282
  const scan = data.scan
1108
- const details = [`${scan.dirs || 0} ${COPY.scan_folders}`, fmt(scan.bytes_total || 0)]
1283
+ const walking = scan.walk_duration_ms == null
1284
+ const hashTotal = scan.hash_total || 0
1285
+ const hashDone = Math.min(scan.hashed || 0, hashTotal)
1286
+ const hashRatio = hashTotal ? Math.min(1, hashDone / hashTotal) : 1
1287
+ const phase = walking ? COPY.scan_progress : hashTotal ? COPY.scan_analyzing : COPY.scan_finishing
1288
+ const hasEstimate = Number.isFinite(scan.estimated_files) && scan.estimated_files > 0
1289
+ const walkWeight = Number.isFinite(scan.estimated_walk_weight) ? scan.estimated_walk_weight : 0.8
1290
+ const walkRatio = hasEstimate ? Math.min(1, (scan.files || 0) / scan.estimated_files) : null
1291
+ const progressRatio = walking
1292
+ ? (hasEstimate ? walkRatio * walkWeight : null)
1293
+ : (hasEstimate ? walkWeight + ((1 - walkWeight) * hashRatio) : hashRatio)
1294
+ const progressValue = progressRatio == null ? null : Math.max(0, Math.min(100, Math.round(progressRatio * 100)))
1295
+ const details = [`${scan.dirs || 0} ${COPY.scan_folders}`, `${scan.files || 0} ${COPY.scan_files}`, fmt(scan.bytes_total || 0)]
1109
1296
  if (scan.current_file) details.push(`${COPY.analyzing} ${scan.current_file}`)
1110
- if (scan.queued > 1) details.push(`${scan.queued} ${COPY.waiting}`)
1297
+ if (walking && scan.queued > 1) details.push(`${scan.queued} ${COPY.waiting}`)
1298
+ const estimated = hasEstimate && progressValue < 100
1299
+ const percent = progressValue == null ? "" : `<span class="vault-scan-percent"${estimated ? ` title="${attr(COPY.scan_estimate_help)}"` : ""}>${estimated ? "~" : ""}${progressValue}%</span>`
1300
+ const progressAttrs = progressValue == null
1301
+ ? `role="progressbar" aria-label="${attr(phase)}"`
1302
+ : `role="progressbar" aria-label="${attr(phase)}" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${progressValue}"${estimated ? ` aria-valuetext="${attr(`About ${progressValue} percent. ${COPY.scan_estimate_help}.`)}"` : ""}`
1303
+ const progressStyle = progressValue == null ? "" : ` style="--vault-progress:${progressRatio}"`
1111
1304
  scanState.classList.add("show")
1112
- scanState.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i><strong>${esc(COPY.scan_progress)}</strong><span class="vault-scan-detail">${esc(details.join(" · "))}</span>`
1305
+ scanState.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i><strong>${esc(phase)}</strong><span class="vault-scan-detail">${esc(details.join(" · "))}</span>${percent}<span class="vault-progress-track" ${progressAttrs}><span class="vault-progress-bar ${progressValue == null ? "indeterminate" : "determinate"}"${progressStyle}></span></span>`
1113
1306
  } else {
1114
1307
  scanState.classList.remove("show")
1115
1308
  scanState.innerHTML = ""
@@ -1153,35 +1346,58 @@ const render = () => {
1153
1346
  renderViews(items)
1154
1347
  renderLocations(items)
1155
1348
  const visible = activeItems(items)
1156
- renderPaneHead(visible)
1157
- renderToolbar(state.view === "reclaimable" ? state.data.blobs.filter((blob) => blob.orphan).length : state.view === "activity" ? state.data.events.length : visible.length)
1349
+ renderToolbar(visible)
1158
1350
  renderTable(visible)
1159
1351
  el("vault-pane-footer").textContent = state.view === "duplicates" ? COPY.duplicate_note : COPY.tracked_note
1160
1352
  }
1161
1353
 
1354
+ const scanActive = (scan) => !!(scan && (scan.active || scan.queued > 0))
1355
+ const fetchJson = async (url) => {
1356
+ const response = await fetch(url)
1357
+ if (!response.ok) throw new Error(`Vault status request failed (${response.status})`)
1358
+ return response.json()
1359
+ }
1360
+ const applyFullData = (data) => {
1361
+ const scanning = scanActive(data.scan)
1362
+ const completed = state.scanRequested && !scanning && data.last_scan && data.last_scan.ts !== state.scanBaseline
1363
+ state.data = data
1364
+ if (completed) {
1365
+ state.scanRequested = false
1366
+ const shareable = data.duplicates.filter((item) => item.shareable !== false)
1367
+ state.scanResult = {
1368
+ count: shareable.length,
1369
+ locations: new Set(shareable.map((item) => item.source_id).filter(Boolean)).size,
1370
+ bytes: shareable.reduce((sum, item) => sum + item.size, 0)
1371
+ }
1372
+ }
1373
+ render()
1374
+ return scanning
1375
+ }
1162
1376
  const refresh = async () => {
1377
+ let delay = 15000
1163
1378
  try {
1164
- const response = await fetch("/info/dedup")
1165
- const data = await response.json()
1166
- const scanning = data.scan && (data.scan.active || data.scan.queued > 0)
1167
- const completed = state.scanRequested && !scanning && data.last_scan && data.last_scan.ts !== state.scanBaseline
1168
- state.data = data
1169
- if (completed) {
1170
- state.scanRequested = false
1171
- const shareable = data.duplicates.filter((item) => item.shareable !== false)
1172
- state.scanResult = {
1173
- count: shareable.length,
1174
- locations: new Set(shareable.map((item) => item.source_id).filter(Boolean)).size,
1175
- bytes: shareable.reduce((sum, item) => sum + item.size, 0)
1379
+ const progressOnly = !!(state.data && scanActive(state.data.scan))
1380
+ if (progressOnly) {
1381
+ const progress = await fetchJson("/info/dedup?progress=1")
1382
+ state.data.scan = progress.scan
1383
+ state.data.last_scan = progress.last_scan
1384
+ if (scanActive(progress.scan)) {
1385
+ delay = 1500
1386
+ renderOverview()
1387
+ renderFeedback()
1388
+ } else {
1389
+ delay = applyFullData(await fetchJson("/info/dedup")) ? 1500 : 15000
1176
1390
  }
1391
+ } else {
1392
+ delay = applyFullData(await fetchJson("/info/dedup")) ? 1500 : 15000
1177
1393
  }
1178
- state.priorScanning = !!scanning
1179
- render()
1180
- clearTimeout(window.__vaultRefresh)
1181
- window.__vaultRefresh = setTimeout(refresh, scanning ? 1500 : 15000)
1182
1394
  } catch (error) {
1183
1395
  state.feedback = { error: true, message: error && error.message ? error.message : String(error) }
1184
1396
  renderFeedback()
1397
+ delay = 5000
1398
+ } finally {
1399
+ clearTimeout(window.__vaultRefresh)
1400
+ window.__vaultRefresh = setTimeout(refresh, delay)
1185
1401
  }
1186
1402
  }
1187
1403
 
@@ -1194,7 +1410,38 @@ const runAction = async (payload, success) => {
1194
1410
  await refresh()
1195
1411
  }
1196
1412
 
1413
+ const chooseExternalFolder = () => new Promise((resolve, reject) => {
1414
+ const picker = new Socket()
1415
+ let settled = false
1416
+ const finish = (value) => {
1417
+ if (settled) return
1418
+ settled = true
1419
+ resolve(value)
1420
+ }
1421
+ const fail = (error) => {
1422
+ if (settled) return
1423
+ settled = true
1424
+ reject(error)
1425
+ }
1426
+ picker.run({
1427
+ method: "kernel.bin.filepicker",
1428
+ params: { title: COPY.add_external_folder, type: "folder" }
1429
+ }, (packet) => {
1430
+ if (packet.type === "result") {
1431
+ const paths = packet.data && Array.isArray(packet.data.paths) ? packet.data.paths : []
1432
+ finish(paths[0] || null)
1433
+ picker.close()
1434
+ } else if (packet.type === "error") {
1435
+ const message = packet.data && packet.data.message ? packet.data.message : "The folder picker could not be opened."
1436
+ fail(new Error(message))
1437
+ picker.close()
1438
+ }
1439
+ }).then(() => finish(null)).catch(fail)
1440
+ })
1441
+
1197
1442
  document.addEventListener("click", async (event) => {
1443
+ const advanced = el("vault-advanced")
1444
+ if (advanced.open && !advanced.contains(event.target)) advanced.open = false
1198
1445
  const target = event.target.closest("button")
1199
1446
  if (!target) return
1200
1447
  if (target.dataset.view) {
@@ -1222,6 +1469,34 @@ document.addEventListener("click", async (event) => {
1222
1469
  if (state.expandedFiles.has(file)) state.expandedFiles.delete(file)
1223
1470
  else state.expandedFiles.add(file)
1224
1471
  render()
1472
+ } else if (target.id === "btn-add-source") {
1473
+ target.disabled = true
1474
+ try {
1475
+ const folderPath = await chooseExternalFolder()
1476
+ if (!folderPath) return
1477
+ const result = await post({ action: "add_source", path: folderPath })
1478
+ if (result.error) {
1479
+ state.feedback = { error: true, message: result.error }
1480
+ } else {
1481
+ state.view = "all"
1482
+ state.sourceId = result.source && result.source.id ? result.source.id : null
1483
+ state.query = ""
1484
+ state.feedback = {
1485
+ error: false,
1486
+ message: result.created === false
1487
+ ? COPY.external_exists
1488
+ : result.source && result.source.shareable === false
1489
+ ? COPY.external_other_disk
1490
+ : COPY.external_added
1491
+ }
1492
+ }
1493
+ await refresh()
1494
+ } catch (error) {
1495
+ state.feedback = { error: true, message: error && error.message ? error.message : String(error) }
1496
+ renderFeedback()
1497
+ } finally {
1498
+ target.disabled = false
1499
+ }
1225
1500
  } else if (target.id === "btn-scan" || target.id === "btn-empty-scan") {
1226
1501
  state.scanRequested = true
1227
1502
  state.scanBaseline = state.data.last_scan ? state.data.last_scan.ts : null
@@ -1229,8 +1504,11 @@ document.addEventListener("click", async (event) => {
1229
1504
  state.feedback = null
1230
1505
  await post({ action: "scan" })
1231
1506
  await refresh()
1232
- } else if (target.id === "btn-rebuild") {
1233
- await runAction({ action: "rebuild" }, COPY.done)
1507
+ } else if (target.id === "btn-repair") {
1508
+ target.disabled = true
1509
+ target.textContent = COPY.repairing
1510
+ await runAction({ action: "repair" }, COPY.repair_done)
1511
+ advanced.open = false
1234
1512
  } else if (target.id === "btn-review-result" || target.id === "btn-review-metric") {
1235
1513
  state.view = "duplicates"
1236
1514
  state.sourceId = null
@@ -1256,7 +1534,7 @@ document.addEventListener("input", (event) => {
1256
1534
  if (event.target.id !== "vault-search") return
1257
1535
  state.query = event.target.value
1258
1536
  const items = activeItems(buildItems())
1259
- renderPaneHead(items)
1537
+ updateToolbarSummary(items)
1260
1538
  renderTable(items)
1261
1539
  })
1262
1540
  document.addEventListener("change", (event) => {
@@ -1265,10 +1543,13 @@ document.addEventListener("change", (event) => {
1265
1543
  render()
1266
1544
  })
1267
1545
 
1268
- el("vault-title").textContent = COPY.title
1269
- el("vault-subtitle").textContent = COPY.subtitle
1270
1546
  el("btn-scan").textContent = COPY.scan
1271
- el("btn-rebuild").textContent = COPY.rebuild
1547
+ el("btn-add-source").setAttribute("aria-label", COPY.add_external_folder)
1548
+ el("btn-add-source").setAttribute("title", COPY.add_external_folder)
1549
+ el("btn-vault-options").setAttribute("aria-label", COPY.vault_options)
1550
+ el("vault-repair-title").textContent = COPY.repair_index
1551
+ el("vault-repair-description").textContent = COPY.repair_description
1552
+ el("btn-repair").textContent = COPY.repair_action
1272
1553
  refresh()
1273
1554
  </script>
1274
1555
  </body>