pinokiod 8.0.61 → 8.0.63

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.61",
3
+ "version": "8.0.63",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -241,12 +241,7 @@
241
241
  iframe.addEventListener('focus', markActive);
242
242
  iframe.addEventListener('pointerdown', markActive);
243
243
 
244
- entry = {
245
- container,
246
- iframe,
247
- updateNodeLocation,
248
- vaultAutoSettingsReady: false,
249
- };
244
+ entry = { container, iframe, updateNodeLocation };
250
245
  leafElements.set(node.id, entry);
251
246
  return entry;
252
247
  }
@@ -641,7 +636,6 @@
641
636
  leaf.src = state.defaultPath;
642
637
  const entry = leafElements.get(leaf.id);
643
638
  if (entry) {
644
- entry.vaultAutoSettingsReady = false;
645
639
  entry.iframe.src = leaf.src;
646
640
  }
647
641
  cleanupSessionIfSingleLeaf();
@@ -688,7 +682,6 @@
688
682
  return false;
689
683
  }
690
684
  node.src = normalized;
691
- entry.vaultAutoSettingsReady = false;
692
685
  entry.iframe.src = normalized;
693
686
  state.activeLeafId = frameId;
694
687
  saveStateToStorage();
@@ -699,16 +692,6 @@
699
692
  if (!event || !event.data || typeof event.data !== 'object') {
700
693
  return;
701
694
  }
702
- if (event.data.e === 'vault-auto-settings-ready' &&
703
- event.origin === window.location.origin) {
704
- for (const entry of leafElements.values()) {
705
- if (entry.iframe && entry.iframe.contentWindow === event.source) {
706
- entry.vaultAutoSettingsReady = true;
707
- break;
708
- }
709
- }
710
- return;
711
- }
712
695
  if (event.data.e === 'layout-state-request') {
713
696
  let frameEntry = null;
714
697
  let frameId = null;
@@ -788,7 +771,6 @@
788
771
  if (node.type === 'leaf') {
789
772
  const entry = ensureLeafElement(node);
790
773
  if (entry && entry.iframe.src !== node.src) {
791
- entry.vaultAutoSettingsReady = false;
792
774
  entry.iframe.src = node.src;
793
775
  }
794
776
  }
@@ -871,24 +853,27 @@
871
853
  <circle cx="10" cy="10" r="7.5"></circle>
872
854
  <path d="m6.75 10.1 2.1 2.1 4.6-4.65"></path>
873
855
  </svg>`;
856
+ const MIN_CHECKING_VISIBLE_MS = 500;
874
857
  const COMPLETION_VISIBLE_MS = 4000;
875
858
 
876
859
  let eventSource = null;
877
- let visibleCheckingApps = new Set();
878
- const completions = new Map();
879
-
880
- function statusText(row) {
881
- if (row.state === 'paused') {
882
- return 'Automatic checks are off';
860
+ const cards = new Map();
861
+
862
+ function statusText(state) {
863
+ if (state === 'paused') return 'Automatic checks are paused';
864
+ if (state === 'result') return 'Possible duplicate files found';
865
+ if (state === 'complete') return 'No possible duplicate files found';
866
+ if (state === 'checking-again') {
867
+ return 'Checking again for possible duplicate files...';
883
868
  }
884
- return 'Checking for possible duplicate files';
869
+ return 'Checking for possible duplicate files...';
885
870
  }
886
871
 
887
- function actionFor(row) {
888
- if (row.state === 'paused') {
872
+ function actionFor(state) {
873
+ if (state === 'paused') {
889
874
  return { label: 'Resume', action: 'automatic_resume' };
890
875
  }
891
- if (row.state === 'result') {
876
+ if (state === 'result') {
892
877
  return { label: 'Review', action: 'automatic_review' };
893
878
  }
894
879
  return { label: 'Pause', action: 'automatic_pause' };
@@ -915,33 +900,42 @@
915
900
  return result || {};
916
901
  }
917
902
 
918
- function removeRow(item) {
919
- item.remove();
920
- tray.hidden = tray.childElementCount === 0;
921
- }
922
-
923
- function removeCompletion(app) {
924
- const completion = completions.get(app);
903
+ function stopCompletion(card) {
904
+ const completion = card && card.completion;
925
905
  if (!completion) return;
926
906
  if (completion.timer) window.clearTimeout(completion.timer);
927
- completions.delete(app);
928
- removeRow(completion.item);
907
+ card.completion = null;
929
908
  }
930
909
 
931
- function clearCompletions() {
932
- [...completions.keys()].forEach(removeCompletion);
910
+ function removeCard(app) {
911
+ const card = cards.get(app);
912
+ if (card) {
913
+ stopCompletion(card);
914
+ card.item.remove();
915
+ }
916
+ cards.delete(app);
917
+ tray.hidden = cards.size === 0;
918
+ }
919
+
920
+ function resetCards() {
921
+ [...cards.keys()].forEach(removeCard);
922
+ tray.replaceChildren();
923
+ tray.hidden = true;
933
924
  }
934
925
 
935
926
  function scheduleCompletion(completion) {
936
- if (completion.paused.size || completion.timer) return;
927
+ if (!completion.revealed || completion.paused.size || completion.timer) {
928
+ return;
929
+ }
937
930
  completion.startedAt = Date.now();
938
931
  completion.timer = window.setTimeout(() => {
939
932
  completion.timer = null;
940
- removeCompletion(completion.app);
933
+ removeCard(completion.app);
941
934
  }, completion.remaining);
942
935
  }
943
936
 
944
937
  function setCompletionPaused(completion, reason, paused) {
938
+ if (!completion || !completion.revealed) return;
945
939
  if (paused) {
946
940
  if (completion.paused.has(reason)) return;
947
941
  if (!completion.paused.size && completion.timer) {
@@ -957,60 +951,6 @@
957
951
  if (!completion.paused.size) scheduleCompletion(completion);
958
952
  }
959
953
 
960
- function startCompletion(app) {
961
- if (completions.has(app)) return;
962
- const item = document.createElement('div');
963
- item.className = 'vault-auto-scan-row';
964
- item.dataset.state = 'complete';
965
-
966
- const close = document.createElement('button');
967
- close.type = 'button';
968
- close.className = 'vault-auto-scan-close';
969
- close.setAttribute('aria-label',
970
- `Dismiss Disk Saver completion for ${app}`);
971
- close.title = 'Dismiss';
972
- close.textContent = '×';
973
-
974
- const icon = document.createElement('span');
975
- icon.className = 'vault-auto-scan-icon';
976
- icon.innerHTML = COMPLETE_ICON;
977
-
978
- const copy = document.createElement('span');
979
- copy.className = 'vault-auto-scan-copy';
980
- const appName = document.createElement('span');
981
- appName.className = 'vault-auto-scan-app';
982
- appName.textContent = app;
983
- appName.title = app;
984
- const status = document.createElement('span');
985
- status.className = 'vault-auto-scan-status';
986
- status.textContent = 'No possible duplicate files found';
987
- copy.append(appName, status);
988
- item.append(close, icon, copy);
989
-
990
- const completion = {
991
- app,
992
- item,
993
- timer: null,
994
- remaining: COMPLETION_VISIBLE_MS,
995
- startedAt: 0,
996
- paused: new Set()
997
- };
998
- close.addEventListener('click', () => removeCompletion(app));
999
- item.addEventListener('mouseenter', () =>
1000
- setCompletionPaused(completion, 'pointer', true));
1001
- item.addEventListener('mouseleave', () =>
1002
- setCompletionPaused(completion, 'pointer', false));
1003
- item.addEventListener('focusin', () =>
1004
- setCompletionPaused(completion, 'focus', true));
1005
- item.addEventListener('focusout', (event) => {
1006
- if (!item.contains(event.relatedTarget)) {
1007
- setCompletionPaused(completion, 'focus', false);
1008
- }
1009
- });
1010
- completions.set(app, completion);
1011
- scheduleCompletion(completion);
1012
- }
1013
-
1014
954
  function automaticSettingsKey(app) {
1015
955
  return `pinokio:vault:auto-settings:${encodeURIComponent(app)}`;
1016
956
  }
@@ -1024,24 +964,6 @@
1024
964
  try {
1025
965
  sessionStorage.setItem(storageKey, '1');
1026
966
  } catch (_) {}
1027
- let frameId = state.activeLeafId;
1028
- if (!frameId || !leafElements.has(frameId)) {
1029
- frameId = leafElements.keys().next().value || null;
1030
- }
1031
- const node = frameId ? nodeById.get(frameId) : null;
1032
- const entry = frameId ? leafElements.get(frameId) : null;
1033
- let currentPath = '';
1034
- let targetPath = '';
1035
- try {
1036
- currentPath = new URL(node?.src || '', window.location.href).pathname;
1037
- targetPath = new URL(href, window.location.href).pathname;
1038
- } catch (_) {}
1039
- if (entry && entry.vaultAutoSettingsReady &&
1040
- currentPath && currentPath === targetPath) {
1041
- entry.iframe?.contentWindow?.postMessage(
1042
- { e: 'vault-auto-settings' }, window.location.origin);
1043
- return true;
1044
- }
1045
967
  const opened = navigateActiveLeaf(href);
1046
968
  if (!opened) {
1047
969
  try { sessionStorage.removeItem(storageKey); } catch (_) {}
@@ -1067,166 +989,290 @@
1067
989
  return opened;
1068
990
  }
1069
991
 
992
+ function createCard(app) {
993
+ const item = document.createElement('section');
994
+ item.className = 'vault-auto-scan-row';
995
+
996
+ const close = document.createElement('button');
997
+ close.type = 'button';
998
+ close.className = 'vault-auto-scan-close';
999
+ close.setAttribute('aria-label',
1000
+ `Dismiss Disk Saver notification for ${app}`);
1001
+ close.title = 'Dismiss';
1002
+ close.textContent = '×';
1003
+
1004
+ const header = document.createElement('header');
1005
+ header.className = 'vault-auto-scan-header';
1006
+ const product = document.createElement('span');
1007
+ product.className = 'vault-auto-scan-product';
1008
+ product.textContent = 'Disk Saver';
1009
+ const separator = document.createElement('span');
1010
+ separator.className = 'vault-auto-scan-separator';
1011
+ separator.setAttribute('aria-hidden', 'true');
1012
+ separator.textContent = '·';
1013
+ const appName = document.createElement('span');
1014
+ appName.className = 'vault-auto-scan-app';
1015
+ appName.textContent = app;
1016
+ appName.title = app;
1017
+ header.append(product, separator, appName);
1018
+
1019
+ const messages = document.createElement('div');
1020
+ messages.className = 'vault-auto-scan-messages';
1021
+ const controls = document.createElement('div');
1022
+ controls.className = 'vault-auto-scan-controls';
1023
+ item.append(close, header, messages, controls);
1024
+
1025
+ const card = {
1026
+ app,
1027
+ item,
1028
+ messages,
1029
+ controls,
1030
+ currentState: null,
1031
+ currentNoticeId: null,
1032
+ checkingShownAt: 0,
1033
+ dismissPending: false,
1034
+ pointerInside: false,
1035
+ focusInside: false,
1036
+ completion: null
1037
+ };
1038
+ close.addEventListener('click', async () => {
1039
+ if (card.completion) {
1040
+ removeCard(app);
1041
+ return;
1042
+ }
1043
+ card.dismissPending = card.currentState === 'checking';
1044
+ close.disabled = true;
1045
+ try {
1046
+ const result = await requestAction(
1047
+ 'automatic_dismiss', app, card.currentNoticeId);
1048
+ if (result.stale) {
1049
+ card.dismissPending = false;
1050
+ await loadState();
1051
+ if (close.isConnected) close.disabled = false;
1052
+ return;
1053
+ }
1054
+ removeCard(app);
1055
+ } catch (error) {
1056
+ console.warn('[Disk Saver] Automatic check dismissal failed', error);
1057
+ card.dismissPending = false;
1058
+ close.disabled = false;
1059
+ }
1060
+ });
1061
+ item.addEventListener('mouseenter', () => {
1062
+ card.pointerInside = true;
1063
+ setCompletionPaused(card.completion, 'pointer', true);
1064
+ });
1065
+ item.addEventListener('mouseleave', () => {
1066
+ card.pointerInside = false;
1067
+ setCompletionPaused(card.completion, 'pointer', false);
1068
+ });
1069
+ item.addEventListener('focusin', () => {
1070
+ card.focusInside = true;
1071
+ setCompletionPaused(card.completion, 'focus', true);
1072
+ });
1073
+ item.addEventListener('focusout', (event) => {
1074
+ if (!item.contains(event.relatedTarget)) {
1075
+ card.focusInside = false;
1076
+ setCompletionPaused(card.completion, 'focus', false);
1077
+ }
1078
+ });
1079
+ cards.set(app, card);
1080
+ return card;
1081
+ }
1082
+
1083
+ function messageIcon(state) {
1084
+ if (state === 'paused') return PAUSE_ICON;
1085
+ if (state === 'result') return DRIVE_ICON;
1086
+ if (state === 'complete') return COMPLETE_ICON;
1087
+ return '';
1088
+ }
1089
+
1090
+ function appendMessage(card, state) {
1091
+ const previous = card.messages.querySelector('[data-current="true"]');
1092
+ if (previous) {
1093
+ previous.dataset.current = 'false';
1094
+ previous.removeAttribute('aria-current');
1095
+ }
1096
+ const message = document.createElement('div');
1097
+ message.className = 'vault-auto-scan-message';
1098
+ message.dataset.state = state === 'checking-again' ? 'checking' : state;
1099
+ message.dataset.current = 'true';
1100
+ message.setAttribute('aria-current', 'true');
1101
+ const icon = document.createElement('span');
1102
+ icon.className = 'vault-auto-scan-icon';
1103
+ icon.innerHTML = messageIcon(state);
1104
+ const status = document.createElement('span');
1105
+ status.className = 'vault-auto-scan-status';
1106
+ status.textContent = statusText(state);
1107
+ message.append(icon, status);
1108
+ card.messages.appendChild(message);
1109
+ }
1110
+
1111
+ function addSettingsButton(card) {
1112
+ const settings = document.createElement('button');
1113
+ settings.type = 'button';
1114
+ settings.className = 'vault-auto-scan-settings';
1115
+ settings.textContent = 'Automatic check settings';
1116
+ settings.addEventListener('click', async () => {
1117
+ settings.disabled = true;
1118
+ try {
1119
+ const result = await requestAction(
1120
+ 'automatic_settings', card.app);
1121
+ if (!result.href || !openAutomaticSettings(card.app, result.href)) {
1122
+ throw new Error('The app page is unavailable.');
1123
+ }
1124
+ } catch (error) {
1125
+ console.warn('[Disk Saver] Automatic check settings failed', error);
1126
+ } finally {
1127
+ if (settings.isConnected) settings.disabled = false;
1128
+ }
1129
+ });
1130
+ card.controls.appendChild(settings);
1131
+ }
1132
+
1133
+ function addStateAction(card, row) {
1134
+ const action = actionFor(row.state);
1135
+ const button = document.createElement('button');
1136
+ button.type = 'button';
1137
+ button.className = 'vault-auto-scan-action';
1138
+ button.textContent = action.label;
1139
+ button.addEventListener('click', async () => {
1140
+ button.disabled = true;
1141
+ try {
1142
+ const result = await requestAction(
1143
+ action.action, card.app, row.notice_id);
1144
+ if (result.stale) {
1145
+ await loadState();
1146
+ if (button.isConnected) button.disabled = false;
1147
+ return;
1148
+ }
1149
+ if (action.action === 'automatic_review' && result.href) {
1150
+ if (!openAutomaticReview(card.app, result.href)) {
1151
+ throw new Error('The app page could not be opened.');
1152
+ }
1153
+ removeCard(card.app);
1154
+ } else {
1155
+ await loadState();
1156
+ }
1157
+ } catch (error) {
1158
+ console.warn('[Disk Saver] Automatic check action failed', error);
1159
+ if (button.isConnected) button.disabled = false;
1160
+ }
1161
+ });
1162
+ card.controls.appendChild(button);
1163
+ }
1164
+
1165
+ function renderControls(card, row) {
1166
+ card.controls.replaceChildren();
1167
+ if (row.state === 'complete') return;
1168
+ if (row.state !== 'result') addSettingsButton(card);
1169
+ addStateAction(card, row);
1170
+ }
1171
+
1172
+ function updateCard(row) {
1173
+ let card = cards.get(row.app);
1174
+ if (!card) card = createCard(row.app);
1175
+ const unchanged = card.currentState === row.state &&
1176
+ card.currentNoticeId === (row.notice_id || null);
1177
+ if (unchanged) return card;
1178
+ stopCompletion(card);
1179
+ const displayState = row.state === 'checking' &&
1180
+ card.currentState === 'paused' ? 'checking-again' : row.state;
1181
+ appendMessage(card, displayState);
1182
+ card.currentState = row.state;
1183
+ card.currentNoticeId = row.notice_id || null;
1184
+ card.checkingShownAt = row.state === 'checking' ? Date.now() : 0;
1185
+ card.dismissPending = false;
1186
+ card.item.dataset.state = row.state;
1187
+ renderControls(card, row);
1188
+ return card;
1189
+ }
1190
+
1191
+ function revealCompletion(completion) {
1192
+ const card = cards.get(completion.app);
1193
+ if (!card || card.completion !== completion ||
1194
+ card.currentState !== 'checking' || card.dismissPending) return;
1195
+ completion.timer = null;
1196
+ completion.revealed = true;
1197
+ appendMessage(card, 'complete');
1198
+ card.currentState = 'complete';
1199
+ card.currentNoticeId = null;
1200
+ card.item.dataset.state = 'complete';
1201
+ renderControls(card, { state: 'complete' });
1202
+ if (card.pointerInside) completion.paused.add('pointer');
1203
+ if (card.focusInside) completion.paused.add('focus');
1204
+ scheduleCompletion(completion);
1205
+ }
1206
+
1207
+ function startCompletion(app) {
1208
+ const card = cards.get(app);
1209
+ if (!card || card.currentState !== 'checking' ||
1210
+ card.dismissPending || card.completion) return false;
1211
+ const completion = {
1212
+ app,
1213
+ revealed: false,
1214
+ timer: null,
1215
+ remaining: COMPLETION_VISIBLE_MS,
1216
+ startedAt: 0,
1217
+ paused: new Set()
1218
+ };
1219
+ card.completion = completion;
1220
+ const elapsed = Math.max(0, Date.now() - card.checkingShownAt);
1221
+ const delay = Math.max(0, MIN_CHECKING_VISIBLE_MS - elapsed);
1222
+ if (delay) {
1223
+ completion.timer = window.setTimeout(
1224
+ () => revealCompletion(completion), delay);
1225
+ } else {
1226
+ revealCompletion(completion);
1227
+ }
1228
+ return true;
1229
+ }
1230
+
1070
1231
  function render(snapshot, options = {}) {
1071
1232
  if (!snapshot || snapshot.global_scan_ready !== true) {
1072
- [...completions.keys()].forEach(removeCompletion);
1073
- visibleCheckingApps = new Set();
1074
- tray.replaceChildren();
1075
- tray.hidden = true;
1233
+ resetCards();
1076
1234
  return;
1077
1235
  }
1078
- const rows = snapshot && Array.isArray(snapshot.rows)
1236
+ const rows = Array.isArray(snapshot.rows)
1079
1237
  ? snapshot.rows
1080
1238
  : [];
1081
1239
  const validRows = rows.filter((row) =>
1082
1240
  row && typeof row.app === 'string' && row.app);
1083
1241
  const liveApps = new Set(validRows.map((row) => row.app));
1084
- [...completions.keys()].forEach((app) => {
1085
- if (liveApps.has(app)) removeCompletion(app);
1086
- });
1087
- const manualApps = new Set(snapshot && Array.isArray(snapshot.settings)
1242
+ const manualApps = new Set(Array.isArray(snapshot.settings)
1088
1243
  ? snapshot.settings.filter((setting) =>
1089
1244
  setting && setting.mode === 'manual').map((setting) => setting.app)
1090
1245
  : []);
1091
- manualApps.forEach(removeCompletion);
1092
-
1093
- const completion = snapshot && snapshot.completion;
1246
+ const retained = new Set();
1247
+ validRows.forEach((row) => {
1248
+ updateCard(row);
1249
+ retained.add(row.app);
1250
+ });
1251
+ const completion = snapshot.completion;
1094
1252
  if (options.acceptCompletion && completion &&
1095
1253
  completion.outcome === 'no_possible_duplicates' &&
1096
1254
  typeof completion.app === 'string' && completion.app &&
1097
- visibleCheckingApps.has(completion.app) &&
1098
1255
  !liveApps.has(completion.app) &&
1099
- !manualApps.has(completion.app)) {
1100
- startCompletion(completion.app);
1256
+ !manualApps.has(completion.app) &&
1257
+ startCompletion(completion.app)) {
1258
+ retained.add(completion.app);
1101
1259
  }
1102
-
1103
- const fragment = document.createDocumentFragment();
1260
+ cards.forEach((card, app) => {
1261
+ if (card.completion && !manualApps.has(app)) retained.add(app);
1262
+ });
1263
+ [...cards.keys()].forEach((app) => {
1264
+ if (!retained.has(app)) removeCard(app);
1265
+ });
1104
1266
  validRows.forEach((row) => {
1105
- const item = document.createElement('div');
1106
- item.className = 'vault-auto-scan-row';
1107
- item.dataset.state = row.state || 'checking';
1108
-
1109
- const close = document.createElement('button');
1110
- close.type = 'button';
1111
- close.className = 'vault-auto-scan-close';
1112
- close.setAttribute('aria-label', `Dismiss Disk Saver notification for ${row.app}`);
1113
- close.title = 'Dismiss';
1114
- close.textContent = '×';
1115
- close.addEventListener('click', async () => {
1116
- if (row.state === 'checking') visibleCheckingApps.delete(row.app);
1117
- close.disabled = true;
1118
- try {
1119
- const result = await requestAction(
1120
- 'automatic_dismiss', row.app, row.notice_id);
1121
- if (result.stale) {
1122
- await loadState();
1123
- if (close.isConnected) close.disabled = false;
1124
- return;
1125
- }
1126
- removeRow(item);
1127
- } catch (error) {
1128
- console.warn('[Disk Saver] Automatic check dismissal failed', error);
1129
- if (row.state === 'checking' && item.isConnected) {
1130
- visibleCheckingApps.add(row.app);
1131
- }
1132
- close.disabled = false;
1133
- }
1134
- });
1135
-
1136
- const icon = document.createElement('span');
1137
- icon.className = 'vault-auto-scan-icon';
1138
- if (row.state === 'paused') {
1139
- icon.innerHTML = PAUSE_ICON;
1140
- } else if (row.state === 'result') {
1141
- icon.innerHTML = DRIVE_ICON;
1142
- }
1143
-
1144
- const copy = document.createElement('span');
1145
- copy.className = 'vault-auto-scan-copy';
1146
- const appName = document.createElement('span');
1147
- appName.className = 'vault-auto-scan-app';
1148
- appName.textContent = row.app;
1149
- appName.title = row.app;
1150
- const status = document.createElement('span');
1151
- status.className = 'vault-auto-scan-status';
1152
- if (row.state === 'result') {
1153
- const detail = document.createElement('span');
1154
- detail.className = 'vault-auto-scan-detail';
1155
- detail.textContent = 'may have duplicate files';
1156
- status.append(detail);
1157
- } else {
1158
- status.textContent = statusText(row);
1159
- }
1160
- copy.append(appName, status);
1161
-
1162
- const controls = document.createElement('span');
1163
- controls.className = 'vault-auto-scan-controls';
1164
- if (row.state !== 'result') {
1165
- const settings = document.createElement('button');
1166
- settings.type = 'button';
1167
- settings.className = 'vault-auto-scan-settings';
1168
- settings.textContent = 'Automatic check settings';
1169
- settings.addEventListener('click', async () => {
1170
- settings.disabled = true;
1171
- try {
1172
- const result = await requestAction(
1173
- 'automatic_settings', row.app);
1174
- if (!result.href || !openAutomaticSettings(row.app, result.href)) {
1175
- throw new Error('The app page is unavailable.');
1176
- }
1177
- } catch (error) {
1178
- console.warn('[Disk Saver] Automatic check settings failed', error);
1179
- } finally {
1180
- if (settings.isConnected) settings.disabled = false;
1181
- }
1182
- });
1183
- copy.appendChild(settings);
1184
- }
1185
-
1186
- const action = actionFor(row);
1187
- const button = document.createElement('button');
1188
- button.type = 'button';
1189
- button.className = 'vault-auto-scan-action';
1190
- button.textContent = action.label;
1191
- button.addEventListener('click', async () => {
1192
- button.disabled = true;
1193
- try {
1194
- const result = await requestAction(
1195
- action.action, row.app, row.notice_id);
1196
- if (result.stale) {
1197
- await loadState();
1198
- if (button.isConnected) button.disabled = false;
1199
- return;
1200
- }
1201
- if (action.action === 'automatic_review' && result.href) {
1202
- if (!openAutomaticReview(row.app, result.href)) {
1203
- throw new Error('The app page could not be opened.');
1204
- }
1205
- removeRow(item);
1206
- } else {
1207
- await loadState();
1208
- }
1209
- } catch (error) {
1210
- console.warn('[Disk Saver] Automatic check action failed', error);
1211
- button.disabled = false;
1212
- }
1213
- });
1214
-
1215
- controls.appendChild(button);
1216
-
1217
- item.append(close, icon, copy, controls);
1218
- fragment.appendChild(item);
1267
+ const card = cards.get(row.app);
1268
+ if (card) tray.appendChild(card.item);
1219
1269
  });
1220
- completions.forEach((completionRow) => {
1221
- if (!liveApps.has(completionRow.app)) {
1222
- fragment.appendChild(completionRow.item);
1270
+ cards.forEach((card, app) => {
1271
+ if (card.completion && !liveApps.has(app)) {
1272
+ tray.appendChild(card.item);
1223
1273
  }
1224
1274
  });
1225
- tray.replaceChildren(fragment);
1226
- tray.hidden = tray.childElementCount === 0;
1227
- visibleCheckingApps = new Set(validRows
1228
- .filter((row) => row.state === 'checking')
1229
- .map((row) => row.app));
1275
+ tray.hidden = cards.size === 0;
1230
1276
  }
1231
1277
 
1232
1278
  async function loadState() {
@@ -1262,7 +1308,7 @@
1262
1308
  }
1263
1309
  };
1264
1310
  eventSource.onerror = () => {
1265
- clearCompletions();
1311
+ resetCards();
1266
1312
  loadState();
1267
1313
  };
1268
1314
  }
@@ -582,14 +582,6 @@ const connectAutomaticMode = () => {
582
582
  } catch (error) {}
583
583
  }
584
584
  }
585
- const requestAutomaticModeMenu = () => {
586
- if (!IS_APP_MODE) return
587
- if (automaticSettingsKey) {
588
- try { sessionStorage.removeItem(automaticSettingsKey) } catch (error) {}
589
- }
590
- state.automaticModeMenuRequested = true
591
- if (state.data) renderOverview()
592
- }
593
585
  const sourceById = (id) => (state.data.sources || []).find((source) => source.id === id)
594
586
  const sourceChildren = (id) => (state.data.sources || []).filter((source) => source.parent_id === id)
595
587
  const sourceIsWithinScope = (sourceId) => {
@@ -4186,12 +4178,6 @@ if (IS_APP_MODE) {
4186
4178
  const trigger = menu.querySelector("summary")
4187
4179
  if (trigger) trigger.focus()
4188
4180
  }, true)
4189
- window.addEventListener("message", (event) => {
4190
- if (event.source !== window.parent ||
4191
- event.origin !== window.location.origin ||
4192
- !event.data || event.data.e !== "vault-auto-settings") return
4193
- requestAutomaticModeMenu()
4194
- })
4195
4181
  loadAutomaticMode().finally(connectAutomaticMode)
4196
4182
  window.addEventListener("beforeunload", () => {
4197
4183
  if (automaticModeEventSource) automaticModeEventSource.close()
@@ -12889,28 +12889,6 @@ const rerenderMenuSection = (container, html) => {
12889
12889
 
12890
12890
 
12891
12891
  });
12892
- const openVaultAutomaticSettings = () => {
12893
- const frame = document.querySelector("iframe[name='app-vault']")
12894
- if (!frame || !frame.contentWindow) return false
12895
- frame.contentWindow.postMessage(
12896
- { e: "vault-auto-settings" }, window.location.origin)
12897
- return true
12898
- }
12899
- window.addEventListener("message", async (event) => {
12900
- if (event.source !== window.parent ||
12901
- event.origin !== window.location.origin ||
12902
- !event.data || event.data.e !== "vault-auto-settings") return
12903
- const diskSaverTab = document.querySelector("#save-space-tab")
12904
- if (!diskSaverTab) return
12905
- if (diskSaverTab.classList.contains("selected") &&
12906
- openVaultAutomaticSettings()) return
12907
- await renderSelection({ target: diskSaverTab, force: true })
12908
- openVaultAutomaticSettings()
12909
- })
12910
- if (window.parent !== window) {
12911
- window.parent.postMessage(
12912
- { e: "vault-auto-settings-ready" }, window.location.origin)
12913
- }
12914
12892
  renderSelection({ force: true })
12915
12893
  <% if (type === "browse" || type === "files") { %>
12916
12894
  const repoStatusCache = new Map()
@@ -161,15 +161,12 @@
161
161
 
162
162
  .vault-auto-scan-row {
163
163
  position: relative;
164
- display: grid;
165
- grid-template-columns: 20px minmax(0, 1fr) 76px;
166
- grid-template-rows: 16px 34px 18px;
167
- align-items: center;
168
- column-gap: 10px;
169
- min-height: 86px;
170
- padding: 9px 34px 9px 12px;
164
+ display: flex;
165
+ min-width: 0;
166
+ padding: 10px 12px;
171
167
  box-sizing: border-box;
172
- overflow: hidden;
168
+ flex-direction: column;
169
+ gap: 8px;
173
170
  color: var(--vault-notice-text);
174
171
  background: var(--vault-notice-surface);
175
172
  border: 1px solid var(--vault-notice-border);
@@ -179,10 +176,61 @@
179
176
  animation: vault-notice-enter 180ms cubic-bezier(0.25, 1, 0.5, 1);
180
177
  }
181
178
 
179
+ .vault-auto-scan-header {
180
+ display: flex;
181
+ min-width: 0;
182
+ min-height: 18px;
183
+ padding-right: 24px;
184
+ align-items: center;
185
+ gap: 6px;
186
+ font: 500 11px/18px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
187
+ }
188
+
189
+ .vault-auto-scan-product {
190
+ color: var(--vault-notice-text);
191
+ font-weight: 600;
192
+ white-space: nowrap;
193
+ }
194
+
195
+ .vault-auto-scan-separator {
196
+ color: var(--vault-notice-muted);
197
+ }
198
+
199
+ .vault-auto-scan-app {
200
+ min-width: 0;
201
+ overflow: hidden;
202
+ color: var(--vault-notice-muted);
203
+ text-overflow: ellipsis;
204
+ white-space: nowrap;
205
+ }
206
+
207
+ .vault-auto-scan-messages {
208
+ display: grid;
209
+ min-width: 0;
210
+ gap: 4px;
211
+ }
212
+
213
+ .vault-auto-scan-message {
214
+ display: grid;
215
+ min-width: 0;
216
+ min-height: 22px;
217
+ grid-template-columns: 20px minmax(0, 1fr);
218
+ align-items: center;
219
+ column-gap: 8px;
220
+ transition: opacity 120ms cubic-bezier(0.25, 1, 0.5, 1);
221
+ }
222
+
223
+ .vault-auto-scan-message[data-current="false"] {
224
+ opacity: 0.58;
225
+ }
226
+
227
+ .vault-auto-scan-message[data-current="true"] {
228
+ animation: vault-notice-message-enter 160ms
229
+ cubic-bezier(0.25, 1, 0.5, 1);
230
+ }
231
+
182
232
  .vault-auto-scan-icon {
183
233
  display: grid;
184
- grid-column: 1;
185
- grid-row: 1 / 4;
186
234
  place-items: center;
187
235
  width: 20px;
188
236
  height: 20px;
@@ -199,57 +247,43 @@
199
247
  stroke-width: 1.8;
200
248
  }
201
249
 
202
- .vault-auto-scan-row[data-state="checking"] .vault-auto-scan-icon {
250
+ .vault-auto-scan-message[data-state="checking"] .vault-auto-scan-icon {
203
251
  border: 1.5px solid var(--vault-notice-border);
204
- border-top-color: var(--vault-notice-text);
205
252
  border-radius: 50%;
206
253
  box-sizing: border-box;
207
- animation: vault-notice-spin 900ms linear infinite;
208
254
  }
209
255
 
210
- .vault-auto-scan-copy {
211
- display: grid;
212
- grid-column: 2;
213
- grid-row: 1 / 4;
214
- grid-template-rows: 16px 34px 18px;
215
- align-items: center;
216
- min-width: 0;
217
- }
218
-
219
- .vault-auto-scan-app {
220
- overflow: hidden;
221
- color: var(--vault-notice-muted);
222
- font-size: 11px;
223
- font-weight: 500;
224
- line-height: 16px;
225
- text-overflow: ellipsis;
226
- white-space: nowrap;
256
+ .vault-auto-scan-message[data-current="true"][data-state="checking"]
257
+ .vault-auto-scan-icon {
258
+ border-top-color: var(--vault-notice-text);
259
+ animation: vault-notice-spin 900ms linear infinite;
227
260
  }
228
261
 
229
262
  .vault-auto-scan-status {
230
- display: flex;
231
263
  min-width: 0;
232
- align-items: baseline;
233
264
  overflow: hidden;
234
265
  font-size: 13px;
235
266
  font-weight: 600;
236
- line-height: 18px;
267
+ line-height: 20px;
237
268
  text-overflow: ellipsis;
238
269
  white-space: nowrap;
239
270
  }
240
271
 
241
- .vault-auto-scan-detail {
242
- overflow: hidden;
243
- color: var(--vault-notice-muted);
244
- font-weight: 400;
245
- text-overflow: ellipsis;
272
+ .vault-auto-scan-message[data-current="false"] .vault-auto-scan-status {
273
+ font-weight: 500;
246
274
  }
247
275
 
248
276
  .vault-auto-scan-controls {
249
- display: grid;
250
- grid-column: 3;
251
- grid-row: 2;
277
+ display: flex;
278
+ min-height: 32px;
279
+ padding-left: 28px;
252
280
  align-items: center;
281
+ justify-content: flex-end;
282
+ gap: 8px;
283
+ }
284
+
285
+ .vault-auto-scan-controls:empty {
286
+ display: none;
253
287
  }
254
288
 
255
289
  .vault-auto-scan-settings,
@@ -261,7 +295,7 @@
261
295
  }
262
296
 
263
297
  .vault-auto-scan-settings {
264
- justify-self: start;
298
+ margin-right: auto;
265
299
  min-height: 18px;
266
300
  padding: 0;
267
301
  font: 500 10.5px/18px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
@@ -293,8 +327,7 @@
293
327
  }
294
328
 
295
329
  .vault-auto-scan-action {
296
- position: relative;
297
- width: 76px;
330
+ min-width: 76px;
298
331
  min-height: 32px;
299
332
  padding: 6px 10px;
300
333
  color: var(--vault-notice-text);
@@ -333,32 +366,7 @@
333
366
  opacity: 0.55;
334
367
  }
335
368
 
336
- .vault-auto-scan-row[data-state="result"],
337
- .vault-auto-scan-row[data-state="complete"] {
338
- grid-template-rows: 16px 30px;
339
- min-height: 66px;
340
- padding-top: 10px;
341
- padding-bottom: 10px;
342
- }
343
-
344
- .vault-auto-scan-row[data-state="result"] .vault-auto-scan-icon,
345
- .vault-auto-scan-row[data-state="result"] .vault-auto-scan-copy,
346
- .vault-auto-scan-row[data-state="result"] .vault-auto-scan-controls,
347
- .vault-auto-scan-row[data-state="complete"] .vault-auto-scan-icon,
348
- .vault-auto-scan-row[data-state="complete"] .vault-auto-scan-copy {
349
- grid-row: 1 / 3;
350
- }
351
-
352
- .vault-auto-scan-row[data-state="result"] .vault-auto-scan-copy,
353
- .vault-auto-scan-row[data-state="complete"] .vault-auto-scan-copy {
354
- grid-template-rows: 16px 30px;
355
- }
356
-
357
- .vault-auto-scan-row[data-state="complete"] .vault-auto-scan-copy {
358
- grid-column: 2 / 4;
359
- }
360
-
361
- .vault-auto-scan-row[data-state="complete"] .vault-auto-scan-icon {
369
+ .vault-auto-scan-message[data-state="complete"] .vault-auto-scan-icon {
362
370
  color: var(--vault-notice-accent);
363
371
  }
364
372
 
@@ -371,25 +379,28 @@
371
379
  to { transform: rotate(360deg); }
372
380
  }
373
381
 
382
+ @keyframes vault-notice-message-enter {
383
+ from { opacity: 0; transform: translateY(2px); }
384
+ to { opacity: 1; transform: translateY(0); }
385
+ }
386
+
374
387
  @media (pointer: coarse) {
375
- .vault-auto-scan-row,
376
- .vault-auto-scan-copy {
377
- grid-template-rows: 16px 44px 28px;
378
- }
379
- .vault-auto-scan-row {
380
- min-height: 106px;
381
- }
388
+ .vault-auto-scan-controls { min-height: 44px; }
382
389
  .vault-auto-scan-action {
383
390
  min-height: 44px;
384
391
  }
385
- .vault-auto-scan-settings { min-height: 28px; }
392
+ .vault-auto-scan-settings { min-height: 44px; }
386
393
  }
387
394
 
388
395
  @media (prefers-reduced-motion: reduce) {
389
396
  .vault-auto-scan-row {
390
397
  animation: none;
391
398
  }
392
- .vault-auto-scan-row[data-state="checking"] .vault-auto-scan-icon {
399
+ .vault-auto-scan-message[data-current="true"] {
400
+ animation: none;
401
+ }
402
+ .vault-auto-scan-message[data-current="true"][data-state="checking"]
403
+ .vault-auto-scan-icon {
393
404
  animation-duration: 1800ms;
394
405
  }
395
406
  }
@@ -101,10 +101,15 @@ test("the shared layout renders and reviews automatic possible-match notices", a
101
101
 
102
102
  const tray = dom.window.document.getElementById("vault-auto-scan-tray")
103
103
  assert.equal(tray.hidden, false)
104
+ assert.equal(tray.querySelector(
105
+ ".vault-auto-scan-product").textContent, "Disk Saver")
104
106
  assert.equal(tray.querySelector(
105
107
  ".vault-auto-scan-app").textContent, "ComfyUI")
106
108
  assert.equal(tray.querySelector(
107
- ".vault-auto-scan-detail").textContent, "may have duplicate files")
109
+ ".vault-auto-scan-status").textContent,
110
+ "Possible duplicate files found")
111
+ assert.equal(tray.querySelectorAll(
112
+ ".vault-auto-scan-message").length, 1)
108
113
  assert.equal(tray.querySelector(".vault-auto-scan-value"), null)
109
114
  assert.equal(tray.querySelector(
110
115
  ".vault-auto-scan-action").textContent, "Review")
@@ -131,9 +136,119 @@ test("the shared layout renders and reviews automatic possible-match notices", a
131
136
  assert.equal(requests.some((request) =>
132
137
  request && request.url === "/info/vault/automatic-scans"), false)
133
138
  assert.match(script, /window\.location\.assign\(/)
134
- assert.ok(script.indexOf("if (!openAutomaticReview(row.app, result.href))") <
135
- script.indexOf("removeRow(item);", script.indexOf(
136
- "if (!openAutomaticReview(row.app, result.href))")))
139
+ assert.ok(script.indexOf("if (!openAutomaticReview(card.app, result.href))") <
140
+ script.indexOf("removeCard(card.app);", script.indexOf(
141
+ "if (!openAutomaticReview(card.app, result.href))")))
142
+
143
+ dom.window.close()
144
+ })
145
+
146
+ test("automatic check states append within one card", async () => {
147
+ const template = await fs.promises.readFile(
148
+ path.join(root, "server", "views", "layout.ejs"), "utf8")
149
+ const script = await fs.promises.readFile(
150
+ path.join(root, "server", "public", "layout.js"), "utf8")
151
+ const html = ejs.render(template, {
152
+ theme: "light",
153
+ agent: "web",
154
+ initialPath: "/home",
155
+ defaultPath: "/home",
156
+ sessionId: null,
157
+ vaultEnabled: true
158
+ })
159
+ const dom = new JSDOM(html, {
160
+ runScripts: "outside-only",
161
+ pretendToBeVisual: true,
162
+ url: "http://localhost/"
163
+ })
164
+ const eventSources = []
165
+ dom.window.EventSource = class EventSource {
166
+ constructor(url) {
167
+ this.url = url
168
+ eventSources.push(this)
169
+ }
170
+ close() {}
171
+ }
172
+ dom.window.fetch = async (url) => {
173
+ throw new Error(`Unexpected request: ${url}`)
174
+ }
175
+
176
+ dom.window.eval(script)
177
+ await waitFor(() => eventSources.length === 1)
178
+ const send = (rows, settings = []) => eventSources[0].onmessage({
179
+ data: JSON.stringify({
180
+ enabled: true,
181
+ global_scan_ready: true,
182
+ rows,
183
+ settings
184
+ })
185
+ })
186
+ const row = (state, noticeId) => ({
187
+ app: "ComfyUI",
188
+ state,
189
+ notice_id: noticeId
190
+ })
191
+
192
+ send([row("checking", "checking:1:")])
193
+ const tray = dom.window.document.getElementById("vault-auto-scan-tray")
194
+ const card = tray.querySelector(".vault-auto-scan-row")
195
+ assert.ok(card)
196
+
197
+ send([row("paused", "paused:2:")], [
198
+ { app: "ComfyUI", mode: "manual" }
199
+ ])
200
+ assert.equal(tray.querySelector(".vault-auto-scan-row"), card)
201
+ assert.deepEqual([...card.querySelectorAll(".vault-auto-scan-status")]
202
+ .map((status) => status.textContent), [
203
+ "Checking for possible duplicate files...",
204
+ "Automatic checks are paused"
205
+ ])
206
+ assert.equal(card.querySelectorAll(
207
+ '.vault-auto-scan-message[data-current="true"]').length, 1)
208
+ assert.equal(card.querySelector(
209
+ '.vault-auto-scan-message[data-current="false"] button'), null)
210
+ assert.equal(card.querySelector(
211
+ ".vault-auto-scan-action").textContent, "Resume")
212
+
213
+ send([row("checking", "checking:3:")], [
214
+ { app: "ComfyUI", mode: "automatic" }
215
+ ])
216
+ assert.equal(tray.querySelector(".vault-auto-scan-row"), card)
217
+ assert.equal(card.querySelectorAll(".vault-auto-scan-message").length, 3)
218
+ assert.equal(card.querySelector(
219
+ '.vault-auto-scan-message[data-current="true"] .vault-auto-scan-status')
220
+ .textContent, "Checking again for possible duplicate files...")
221
+ assert.equal(card.querySelector(
222
+ ".vault-auto-scan-action").textContent, "Pause")
223
+
224
+ send([row("result", "result:4:result-a")], [
225
+ { app: "ComfyUI", mode: "automatic" }
226
+ ])
227
+ assert.equal(tray.querySelector(".vault-auto-scan-row"), card)
228
+ assert.equal(card.querySelectorAll(".vault-auto-scan-message").length, 4)
229
+ assert.equal(card.querySelector(
230
+ '.vault-auto-scan-message[data-current="true"] .vault-auto-scan-status')
231
+ .textContent, "Possible duplicate files found")
232
+ assert.equal(card.querySelector(".vault-auto-scan-settings"), null)
233
+ assert.equal(card.querySelector(
234
+ ".vault-auto-scan-action").textContent, "Review")
235
+
236
+ send([row("result", "result:4:result-a")], [
237
+ { app: "ComfyUI", mode: "automatic" }
238
+ ])
239
+ assert.equal(card.querySelectorAll(".vault-auto-scan-message").length, 4,
240
+ "repeated snapshots do not duplicate the current message")
241
+
242
+ send([
243
+ row("result", "result:4:result-a"),
244
+ { app: "OtherApp", state: "checking", notice_id: "checking:5:" }
245
+ ], [
246
+ { app: "ComfyUI", mode: "automatic" },
247
+ { app: "OtherApp", mode: "automatic" }
248
+ ])
249
+ assert.equal(tray.querySelectorAll(".vault-auto-scan-row").length, 2)
250
+ assert.deepEqual([...tray.querySelectorAll(".vault-auto-scan-app")]
251
+ .map((app) => app.textContent), ["ComfyUI", "OtherApp"])
137
252
 
138
253
  dom.window.close()
139
254
  })
@@ -205,7 +320,7 @@ test("checking notices expose settings, Pause, and dismissal", async () => {
205
320
  ".vault-auto-scan-app").textContent, "ComfyUI")
206
321
  assert.equal(tray.querySelector(
207
322
  ".vault-auto-scan-status").textContent,
208
- "Checking for possible duplicate files")
323
+ "Checking for possible duplicate files...")
209
324
  assert.equal(tray.querySelector(
210
325
  ".vault-auto-scan-settings").textContent,
211
326
  "Automatic check settings")
@@ -223,19 +338,9 @@ test("checking notices expose settings, Pause, and dismissal", async () => {
223
338
  assert.equal(tray.querySelector(
224
339
  ".vault-auto-scan-settings").disabled, false)
225
340
 
226
- const relayed = []
227
- iframe.contentWindow.postMessage = (message, targetOrigin) => {
228
- relayed.push({ message, targetOrigin })
229
- }
230
- dom.window.dispatchEvent(new dom.window.MessageEvent("message", {
231
- data: { e: "vault-auto-settings-ready" },
232
- origin: dom.window.location.origin,
233
- source: iframe.contentWindow
234
- }))
235
- tray.querySelector(".vault-auto-scan-settings").click()
236
- await waitFor(() => relayed.length === 1)
237
- assert.equal(relayed[0].message.e, "vault-auto-settings")
238
- assert.equal(relayed[0].targetOrigin, dom.window.location.origin)
341
+ assert.doesNotMatch(script, /vaultAutoSettingsReady|vault-auto-settings-ready/)
342
+ assert.doesNotMatch(script,
343
+ /postMessage\(\s*\{ e: ['"]vault-auto-settings['"]/)
239
344
 
240
345
  tray.querySelector(".vault-auto-scan-close").click()
241
346
  await waitFor(() => tray.hidden)
@@ -265,10 +370,21 @@ test("an empty automatic check briefly confirms completion", async () => {
265
370
  })
266
371
  const eventSources = []
267
372
  const requests = []
373
+ const revealTimers = []
268
374
  const completionTimers = []
269
375
  const nativeSetTimeout = dom.window.setTimeout.bind(dom.window)
270
376
  const nativeClearTimeout = dom.window.clearTimeout.bind(dom.window)
271
377
  dom.window.setTimeout = (callback, delay, ...args) => {
378
+ if (delay <= 500 && delay > 400) {
379
+ const timer = {
380
+ callback,
381
+ delay,
382
+ cleared: false,
383
+ id: 9000 + revealTimers.length
384
+ }
385
+ revealTimers.push(timer)
386
+ return timer.id
387
+ }
272
388
  if (delay <= 4000 && delay > 3500) {
273
389
  const timer = {
274
390
  callback,
@@ -282,7 +398,8 @@ test("an empty automatic check briefly confirms completion", async () => {
282
398
  return nativeSetTimeout(callback, delay, ...args)
283
399
  }
284
400
  dom.window.clearTimeout = (id) => {
285
- const timer = completionTimers.find((candidate) => candidate.id === id)
401
+ const timer = [...revealTimers, ...completionTimers]
402
+ .find((candidate) => candidate.id === id)
286
403
  if (timer) {
287
404
  timer.cleared = true
288
405
  return
@@ -375,15 +492,37 @@ test("an empty automatic check briefly confirms completion", async () => {
375
492
  }
376
493
  })
377
494
 
495
+ const checking = [...tray.querySelectorAll(".vault-auto-scan-row")]
496
+ .find((card) => card.querySelector(
497
+ ".vault-auto-scan-app").textContent === "ComfyUI")
498
+ assert.ok(checking)
499
+ assert.equal(checking.dataset.state, "checking")
500
+ assert.equal(checking.querySelectorAll(
501
+ ".vault-auto-scan-message").length, 1)
502
+ assert.equal(tray.querySelector(
503
+ '.vault-auto-scan-row[data-state="complete"]'), null,
504
+ "the checking phase remains visible before an immediate result")
505
+ assert.equal(revealTimers.length, 1)
506
+ assert.ok(revealTimers[0].delay <= 500)
507
+
508
+ revealTimers[0].callback()
378
509
  const completed = tray.querySelector(
379
510
  '.vault-auto-scan-row[data-state="complete"]')
380
511
  assert.ok(completed)
381
512
  assert.equal(completed.querySelector(
382
513
  ".vault-auto-scan-app").textContent, "ComfyUI")
514
+ assert.deepEqual([...completed.querySelectorAll(
515
+ ".vault-auto-scan-status")].map((status) => status.textContent), [
516
+ "Checking for possible duplicate files...",
517
+ "No possible duplicate files found"
518
+ ])
519
+ assert.equal(completed.querySelectorAll(
520
+ ".vault-auto-scan-message").length, 2)
383
521
  assert.equal(completed.querySelector(
384
- ".vault-auto-scan-status").textContent,
385
- "No possible duplicate files found")
386
- assert.ok(completed.querySelector(".vault-auto-scan-icon svg"))
522
+ '.vault-auto-scan-message[data-current="false"]')
523
+ .dataset.state, "checking")
524
+ assert.ok(completed.querySelector(
525
+ '.vault-auto-scan-message[data-current="true"] .vault-auto-scan-icon svg'))
387
526
  assert.equal(completed.querySelector(".vault-auto-scan-settings"), null)
388
527
  assert.equal(completed.querySelector(".vault-auto-scan-action"), null)
389
528
  assert.equal(completionTimers[0].delay, 4000)
@@ -426,6 +565,10 @@ test("an empty automatic check briefly confirms completion", async () => {
426
565
  }],
427
566
  settings: [{ app: "ComfyUI", mode: "automatic" }]
428
567
  })
568
+ const focusedClose = tray.querySelector(".vault-auto-scan-close")
569
+ focusedClose.focus()
570
+ const revealsBeforeFocusedCompletion = revealTimers.length
571
+ const timersBeforeFocusedCompletion = completionTimers.length
429
572
  send({
430
573
  enabled: true,
431
574
  rows: [],
@@ -436,6 +579,17 @@ test("an empty automatic check briefly confirms completion", async () => {
436
579
  }
437
580
  })
438
581
  assert.equal(tray.hidden, false)
582
+ assert.equal(revealTimers.length, revealsBeforeFocusedCompletion + 1)
583
+ assert.equal(completionTimers.length, timersBeforeFocusedCompletion,
584
+ "completion dismissal cannot start before its result is revealed")
585
+ revealTimers.at(-1).callback()
586
+ assert.equal(completionTimers.length, timersBeforeFocusedCompletion,
587
+ "completion does not start its timer while focus is already in the card")
588
+ focusedClose.dispatchEvent(new dom.window.FocusEvent("focusout", {
589
+ bubbles: true,
590
+ relatedTarget: null
591
+ }))
592
+ assert.equal(completionTimers.length, timersBeforeFocusedCompletion + 1)
439
593
  eventSources[0].onerror()
440
594
  await new Promise((resolve) => nativeSetTimeout(resolve, 0))
441
595
  assert.equal(tray.hidden, true,
@@ -614,6 +768,7 @@ test("the app sidebar mirrors Automatic and Manual Disk Saver modes", async () =
614
768
  assert.equal(label.textContent, "Auto")
615
769
  assert.match(template, /data-app-vault-mode/)
616
770
  assert.match(template, /app-vault-mode\.js/)
771
+ assert.doesNotMatch(template, /vault-auto-settings/)
617
772
  assert.match(template,
618
773
  /#save-space-tab\s*\{[^}]*width:\s*100%;[^}]*max-width:\s*none;/s)
619
774
  assert.match(template,