codex-overleaf-link 2.1.4 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +27 -23
  2. package/extension/bootstrap/background.js +14 -1
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/runtime-manifest.json +11 -0
  5. package/extension/src/background.js +507 -8
  6. package/extension/src/backgroundUpdateCoordinator.js +284 -19
  7. package/extension/src/content/activeTurnControl.js +179 -0
  8. package/extension/src/content/composerPanel.js +19 -6
  9. package/extension/src/content/contentRuntime.js +335 -317
  10. package/extension/src/content/diagnosticsPanel.js +132 -1
  11. package/extension/src/content/markdownDomRenderer.js +451 -0
  12. package/extension/src/content/markdownText.js +86 -7
  13. package/extension/src/content/mathText.js +49 -4
  14. package/extension/src/content/modelPicker.js +14 -5
  15. package/extension/src/content/nativeChannel.js +7 -1
  16. package/extension/src/content/otWarmMirror.js +1 -1
  17. package/extension/src/content/panelRenderer.js +112 -12
  18. package/extension/src/content/pendingInputView.js +106 -0
  19. package/extension/src/content/projectProviderSelection.js +68 -0
  20. package/extension/src/content/projectSessionCleanup.js +104 -0
  21. package/extension/src/content/providerSettingsCoordinator.js +170 -22
  22. package/extension/src/content/providerSettingsDialog.js +2 -2
  23. package/extension/src/content/recentProjects.js +24 -13
  24. package/extension/src/content/runController.js +2 -2
  25. package/extension/src/content/runGuidanceController.js +70 -0
  26. package/extension/src/content/runGuidanceView.js +48 -0
  27. package/extension/src/content/runQueueScheduler.js +260 -0
  28. package/extension/src/content/runTimelineView.js +33 -80
  29. package/extension/src/content/sessionManager.js +2 -2
  30. package/extension/src/content/sessionPersistence.js +62 -0
  31. package/extension/src/content/settingsPanel.js +1 -1
  32. package/extension/src/content/updateNotice.js +80 -1
  33. package/extension/src/page/writebackRouter.js +3 -9
  34. package/extension/src/shared/compatibility.js +2 -1
  35. package/extension/src/shared/i18n.js +54 -0
  36. package/extension/src/shared/providerProfiles.js +3 -2
  37. package/extension/src/shared/runInputQueue.js +253 -0
  38. package/extension/src/shared/sessionState.js +45 -22
  39. package/extension/src/shared/storageDb.js +39 -44
  40. package/extension/src/shared/storageMigration.js +123 -3
  41. package/extension/src/shared/storageRunActions.js +34 -1
  42. package/extension/src/shared/updateConsent.js +15 -4
  43. package/extension/styles/panel.css +370 -23
  44. package/extension/vendor/katex/katex.min.css +1 -1
  45. package/extension/vendor/markdown-it/LICENSE +22 -0
  46. package/extension/vendor/markdown-it/SOURCE.json +7 -0
  47. package/extension/vendor/markdown-it/VERSION +1 -0
  48. package/extension/vendor/markdown-it/markdown-it.min.js +3 -0
  49. package/native-host/src/anthropicBridgeRequest.js +3 -3
  50. package/native-host/src/anthropicBridgeResponse.js +58 -17
  51. package/native-host/src/anthropicMessagesBridge.js +156 -23
  52. package/native-host/src/chatBridgeRequest.js +1 -1
  53. package/native-host/src/chatBridgeResponse.js +12 -8
  54. package/native-host/src/chatCompletionsBridge.js +22 -40
  55. package/native-host/src/codexPromptAssembly.js +3 -1
  56. package/native-host/src/codexProviderLaunch.js +6 -3
  57. package/native-host/src/codexSessionRunner.js +66 -66
  58. package/native-host/src/codexSessionTiming.js +45 -0
  59. package/native-host/src/diffEngine.js +10 -5
  60. package/native-host/src/index.js +58 -3
  61. package/native-host/src/managedInstall.js +45 -7
  62. package/native-host/src/providerProfile.js +2 -2
  63. package/native-host/src/providerStreamLifecycle.js +100 -0
  64. package/native-host/src/responsesPassthroughBridge.js +636 -0
  65. package/native-host/src/taskRunnerRuntime.js +81 -2
  66. package/native-host/src/updateArchive.js +4 -3
  67. package/native-host/src/updateManager.js +306 -19
  68. package/native-host/src/updateTrust.js +1 -1
  69. package/package.json +3 -1
@@ -1,12 +1,12 @@
1
1
  (function initStorageDb(root, factory) {
2
2
  if (typeof module === 'object' && module.exports) {
3
- module.exports = factory(require('./storageRunActions'));
3
+ module.exports = factory(require('./storageRunActions'), require('./runInputQueue'));
4
4
  } else {
5
- root.CodexOverleafStorageDb = factory(root.CodexOverleafStorageRunActions);
5
+ root.CodexOverleafStorageDb = factory(root.CodexOverleafStorageRunActions, root.CodexOverleafRunInputQueue);
6
6
  }
7
- })(typeof globalThis !== 'undefined' ? globalThis : window, function storageDbFactory(StorageRunActions) {
7
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function storageDbFactory(StorageRunActions, RunInputQueue) {
8
8
  'use strict';
9
- if (!StorageRunActions?.compactRunsForStorage || !StorageRunActions?.compactRunActionPayload || !StorageRunActions?.compactProviderSnapshot) {
9
+ if (!StorageRunActions?.compactRunsForStorage || !StorageRunActions?.compactRunActionPayload || !StorageRunActions?.compactProviderSnapshot || !StorageRunActions?.compactStructuredEventValue || !StorageRunActions?.hashString || !RunInputQueue?.compactForStorage) {
10
10
  throw new Error('Codex Overleaf storage run-action helpers are unavailable.');
11
11
  }
12
12
  // IndexedDB versions are monotonic; v2.0 RC profiles have already opened v3.
@@ -272,6 +272,11 @@
272
272
  codexThreadId: typeof input.codexThreadId === 'string' ? input.codexThreadId : '',
273
273
  status: typeof input.status === 'string' && input.status ? input.status : 'active',
274
274
  focusFiles: normalizePathList(input.focusFiles),
275
+ pendingInputs: RunInputQueue.compactForStorage(input.pendingInputs, {
276
+ normalizeField: normalizeTextField,
277
+ normalizeDisplay: normalizeDisplayTextForStorage,
278
+ normalizePaths: normalizePathList
279
+ }),
275
280
  history: compactHistoryForStorage(input.history),
276
281
  runs: compactRunsForStorage(input.runs, options),
277
282
  task: normalizeDisplayTextForStorage(input.task, SESSION_STORAGE_LIMITS.taskChars),
@@ -663,8 +668,23 @@
663
668
  undoBaseFiles: actionPayload.undoBaseFiles,
664
669
  undoTrackedChanges: actionPayload.undoTrackedChanges,
665
670
  undoExpectedFiles: actionPayload.undoExpectedFiles,
666
- undoStatus: normalizeDisplayTextForStorage(run.undoStatus, SESSION_STORAGE_LIMITS.statusTextChars)
671
+ undoStatus: normalizeDisplayTextForStorage(run.undoStatus, SESSION_STORAGE_LIMITS.statusTextChars),
672
+ nativeRequestId: normalizeTextField(run.nativeRequestId, 160),
673
+ codexTurnId: normalizeTextField(run.codexTurnId, 160),
674
+ nativeEventSeq: Number.isFinite(Number(run.nativeEventSeq)) ? Number(run.nativeEventSeq) : 0,
675
+ queueItemId: normalizeTextField(run.queueItemId, 160)
667
676
  };
677
+ if (run.interruptedDraft && typeof run.interruptedDraft === 'object') {
678
+ compact.interruptedDraft = {
679
+ requestId: normalizeTextField(run.interruptedDraft.requestId, 160),
680
+ threadId: normalizeTextField(run.interruptedDraft.threadId, 160),
681
+ turnId: normalizeTextField(run.interruptedDraft.turnId, 160),
682
+ lastEventSeq: Number.isFinite(Number(run.interruptedDraft.lastEventSeq))
683
+ ? Number(run.interruptedDraft.lastEventSeq)
684
+ : 0,
685
+ reason: normalizeTextField(run.interruptedDraft.reason, 80)
686
+ };
687
+ }
668
688
  if (VALID_TRACKED_CHANGE_STATUSES[run.trackedChangeStatus] === true) {
669
689
  compact.trackedChangeStatus = run.trackedChangeStatus;
670
690
  }
@@ -712,27 +732,11 @@
712
732
  // its shape while redacting + truncating every string field. Bounded depth +
713
733
  // breadth so a pathological value can't blow up the stored record.
714
734
  function compactStructuredEventValueForStorage(value, depth) {
715
- var d = depth || 0;
716
- if (typeof value === 'string') {
717
- return normalizeDisplayTextForStorage(value, SESSION_STORAGE_LIMITS.reportDetailChars);
718
- }
719
- if (value === null || typeof value === 'number' || typeof value === 'boolean') {
720
- return value;
721
- }
722
- if (d > 6 || typeof value !== 'object') {
723
- return null;
724
- }
725
- if (Array.isArray(value)) {
726
- return value.slice(0, 32).map(function (item) {
727
- return compactStructuredEventValueForStorage(item, d + 1);
728
- });
729
- }
730
- var out = {};
731
- var keys = Object.keys(value).slice(0, 32);
732
- for (var i = 0; i < keys.length; i++) {
733
- out[keys[i]] = compactStructuredEventValueForStorage(value[keys[i]], d + 1);
734
- }
735
- return out;
735
+ return StorageRunActions.compactStructuredEventValue(value, {
736
+ normalizeString: function (text) {
737
+ return normalizeDisplayTextForStorage(text, SESSION_STORAGE_LIMITS.reportDetailChars);
738
+ }
739
+ }, depth);
736
740
  }
737
741
 
738
742
  function getEventDetailLimit(event) {
@@ -1142,7 +1146,11 @@
1142
1146
  return Promise.resolve([]);
1143
1147
  }
1144
1148
  return getAllSessions().then(function (all) {
1145
- return filterRecentProjectsAcrossAccount(all, { accountScopeId: accountScopeId, limit: limit });
1149
+ return filterRecentProjectsAcrossAccount(all, {
1150
+ accountScopeId: accountScopeId,
1151
+ limit: limit,
1152
+ deletedSessionIdsByProject: options && options.deletedSessionIdsByProject
1153
+ });
1146
1154
  });
1147
1155
  }
1148
1156
 
@@ -1153,6 +1161,7 @@
1153
1161
  function filterRecentProjectsAcrossAccount(sessions, options) {
1154
1162
  var accountScopeId = options && options.accountScopeId;
1155
1163
  var limit = options && Number.isFinite(options.limit) ? options.limit : 10;
1164
+ var deletedSessionIdsByProject = options && options.deletedSessionIdsByProject;
1156
1165
  if (!accountScopeId) {
1157
1166
  return [];
1158
1167
  }
@@ -1163,6 +1172,8 @@
1163
1172
  var s = all[i];
1164
1173
  if (!s) continue;
1165
1174
  if (s.accountScopeId !== accountScopeId) continue;
1175
+ if (deletedSessionIdsByProject && Array.isArray(deletedSessionIdsByProject[s.projectId])
1176
+ && deletedSessionIdsByProject[s.projectId].indexOf(s.id) !== -1) continue;
1166
1177
  if (typeof s.lastActivityAt !== 'string' || !s.lastActivityAt) continue;
1167
1178
  if (!isValidOverleafProjectId(s.projectId)) continue;
1168
1179
  var prev = byProject[s.projectId];
@@ -1303,23 +1314,7 @@
1303
1314
  }
1304
1315
  }
1305
1316
 
1306
- function hashString(value) {
1307
- var hash = 2166136261;
1308
- var text = String(value || '');
1309
- for (var i = 0; i < text.length; i++) {
1310
- hash ^= text.charCodeAt(i);
1311
- hash = Math.imul(hash, 16777619);
1312
- }
1313
- return (hash >>> 0).toString(16).padStart(8, '0');
1314
- }
1315
-
1316
- function cloneJsonValue(value) {
1317
- try {
1318
- return JSON.parse(JSON.stringify(value));
1319
- } catch (_error) {
1320
- return Array.isArray(value) ? value.slice() : value;
1321
- }
1322
- }
1317
+ var hashString = StorageRunActions.hashString;
1323
1318
 
1324
1319
  return {
1325
1320
  TARGET_SCHEMA_VERSION: TARGET_SCHEMA_VERSION,
@@ -8,6 +8,9 @@
8
8
  'use strict';
9
9
 
10
10
  var PREFS_KEY = 'codexOverleafPrefs';
11
+ var SESSION_TOMBSTONES_KEY = 'codexOverleafSessionTombstones';
12
+ var SESSION_TOMBSTONE_ENTRY_PREFIX = 'codexOverleafSessionTombstone:v2:';
13
+ var MAX_DELETED_SESSION_IDS_PER_PROJECT = 100;
11
14
  var CUSTOM_INSTRUCTIONS_MAX_CHARS = 12000;
12
15
  var PROJECT_PREF_KEY_MAX_CHARS = 160;
13
16
 
@@ -15,15 +18,20 @@
15
18
  var StorageDb = (typeof window !== 'undefined' && window.CodexOverleafStorageDb)
16
19
  ? window.CodexOverleafStorageDb
17
20
  : require('./storageDb');
18
- return chrome.storage.local.get([PREFS_KEY, legacyStorageKey]).then(function (stored) {
21
+ return chrome.storage.local.get(null).then(function (stored) {
19
22
  var prefs = normalizePrefs(stored[PREFS_KEY] || {});
23
+ var tombstones = collectSessionTombstones(stored);
24
+ var deletedSessionIds = getDeletedSessionIds(tombstones, projectId);
20
25
  var schemaVersion = prefs.storageSchemaVersion || 0;
21
26
 
22
27
  if (schemaVersion >= StorageDb.TARGET_SCHEMA_VERSION) {
23
28
  var activeSessionByProject = prefs.activeSessionByProject || {};
24
29
  var activeSessionId = activeSessionByProject[projectId] || '';
25
30
  return StorageDb.getAllByIndex('sessions', 'projectId', projectId).then(function (sessions) {
26
- return { prefs: prefs, sessions: sessions, activeSessionId: activeSessionId, migrated: false };
31
+ var visibleSessions = sessions.filter(function (session) {
32
+ return deletedSessionIds.indexOf(session && session.id) === -1;
33
+ });
34
+ return { prefs: prefs, sessions: visibleSessions, activeSessionId: activeSessionId, migrated: false };
27
35
  });
28
36
  }
29
37
 
@@ -35,6 +43,7 @@
35
43
  for (var i = 0; i < legacySessions.length; i++) {
36
44
  var legacy = legacySessions[i];
37
45
  if (!legacy || !legacy.id) { continue; }
46
+ if (deletedSessionIds.indexOf(legacy.id) !== -1) { continue; }
38
47
  var record = StorageDb.buildSessionRecord(
39
48
  buildLegacySessionRecordInput(projectId, legacyBlob, legacy)
40
49
  );
@@ -101,6 +110,112 @@
101
110
  });
102
111
  }
103
112
 
113
+ function normalizeDeletedSessionIdsByProject(value) {
114
+ var result = {};
115
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
116
+ return result;
117
+ }
118
+ Object.keys(value).forEach(function (rawProjectId) {
119
+ var projectId = normalizeProjectPrefKey(rawProjectId);
120
+ if (!projectId || !Array.isArray(value[rawProjectId])) {
121
+ return;
122
+ }
123
+ var seen = {};
124
+ var ids = value[rawProjectId].map(function (id) {
125
+ return normalizeTextField(id, PROJECT_PREF_KEY_MAX_CHARS);
126
+ }).filter(function (id) {
127
+ if (!id || seen[id]) { return false; }
128
+ seen[id] = true;
129
+ return true;
130
+ }).slice(-MAX_DELETED_SESSION_IDS_PER_PROJECT);
131
+ if (ids.length) {
132
+ result[projectId] = ids;
133
+ }
134
+ });
135
+ return result;
136
+ }
137
+
138
+ function getDeletedSessionIds(tombstones, projectId) {
139
+ var normalized = normalizeDeletedSessionIdsByProject(tombstones);
140
+ var key = normalizeProjectPrefKey(projectId);
141
+ return key && Array.isArray(normalized[key]) ? normalized[key].slice() : [];
142
+ }
143
+
144
+ function loadSessionTombstones() {
145
+ return chrome.storage.local.get(null).then(function (stored) {
146
+ return collectSessionTombstones(stored);
147
+ });
148
+ }
149
+
150
+ function addSessionTombstones(projectId, sessionIds) {
151
+ var key = normalizeProjectPrefKey(projectId);
152
+ if (!key) {
153
+ return loadSessionTombstones();
154
+ }
155
+ var incoming = (Array.isArray(sessionIds) ? sessionIds : [])
156
+ .map(function (id) { return normalizeTextField(id, PROJECT_PREF_KEY_MAX_CHARS); })
157
+ .filter(Boolean);
158
+ if (!incoming.length) {
159
+ return loadSessionTombstones();
160
+ }
161
+ var deletedAt = new Date().toISOString();
162
+ var payload = {};
163
+ incoming.forEach(function (sessionId) {
164
+ payload[buildSessionTombstoneEntryKey(key, sessionId)] = {
165
+ projectId: key,
166
+ sessionId: sessionId,
167
+ deletedAt: deletedAt
168
+ };
169
+ });
170
+ return chrome.storage.local.set(payload)
171
+ .then(function () { return pruneSessionTombstoneEntries(key); })
172
+ .then(loadSessionTombstones);
173
+ }
174
+
175
+ function collectSessionTombstones(stored) {
176
+ var tombstones = normalizeDeletedSessionIdsByProject(
177
+ stored && stored[SESSION_TOMBSTONES_KEY] || {}
178
+ );
179
+ Object.keys(stored || {}).forEach(function (storageKey) {
180
+ if (storageKey.indexOf(SESSION_TOMBSTONE_ENTRY_PREFIX) !== 0) {
181
+ return;
182
+ }
183
+ var entry = stored[storageKey];
184
+ var projectId = normalizeProjectPrefKey(entry && entry.projectId);
185
+ var sessionId = normalizeTextField(entry && entry.sessionId, PROJECT_PREF_KEY_MAX_CHARS);
186
+ if (!projectId || !sessionId) {
187
+ return;
188
+ }
189
+ tombstones[projectId] = (tombstones[projectId] || [])
190
+ .filter(function (id) { return id !== sessionId; })
191
+ .concat(sessionId)
192
+ .slice(-MAX_DELETED_SESSION_IDS_PER_PROJECT);
193
+ });
194
+ return normalizeDeletedSessionIdsByProject(tombstones);
195
+ }
196
+
197
+ function buildSessionTombstoneEntryKey(projectId, sessionId) {
198
+ return SESSION_TOMBSTONE_ENTRY_PREFIX +
199
+ encodeURIComponent(projectId) + ':' +
200
+ encodeURIComponent(sessionId);
201
+ }
202
+
203
+ function pruneSessionTombstoneEntries(projectId) {
204
+ return chrome.storage.local.get(null).then(function (stored) {
205
+ var entries = Object.keys(stored || {}).filter(function (storageKey) {
206
+ var entry = stored[storageKey];
207
+ return storageKey.indexOf(SESSION_TOMBSTONE_ENTRY_PREFIX) === 0 &&
208
+ normalizeProjectPrefKey(entry && entry.projectId) === projectId;
209
+ }).sort(function (leftKey, rightKey) {
210
+ var left = String(stored[leftKey] && stored[leftKey].deletedAt || '');
211
+ var right = String(stored[rightKey] && stored[rightKey].deletedAt || '');
212
+ return left === right ? leftKey.localeCompare(rightKey) : left.localeCompare(right);
213
+ });
214
+ var obsolete = entries.slice(0, Math.max(0, entries.length - MAX_DELETED_SESSION_IDS_PER_PROJECT));
215
+ return obsolete.length ? chrome.storage.local.remove(obsolete) : undefined;
216
+ });
217
+ }
218
+
104
219
  function normalizePrefs(prefs) {
105
220
  var source = prefs && typeof prefs === 'object' ? prefs : {};
106
221
  return Object.assign({}, source, {
@@ -162,8 +277,13 @@
162
277
 
163
278
  return {
164
279
  PREFS_KEY: PREFS_KEY,
280
+ SESSION_TOMBSTONES_KEY: SESSION_TOMBSTONES_KEY,
165
281
  runMigrationIfNeeded: runMigrationIfNeeded,
166
282
  savePrefs: savePrefs,
167
- loadPrefs: loadPrefs
283
+ loadPrefs: loadPrefs,
284
+ loadSessionTombstones: loadSessionTombstones,
285
+ addSessionTombstones: addSessionTombstones,
286
+ getDeletedSessionIds: getDeletedSessionIds,
287
+ normalizeDeletedSessionIdsByProject: normalizeDeletedSessionIdsByProject
168
288
  };
169
289
  });
@@ -101,9 +101,42 @@
101
101
  };
102
102
  }
103
103
 
104
+ function compactStructuredEventValue(value, options, depth) {
105
+ var config = options || {};
106
+ var d = depth || 0;
107
+ if (typeof value === 'string') {
108
+ return typeof config.normalizeString === 'function' ? config.normalizeString(value) : value;
109
+ }
110
+ if (value === null || typeof value === 'number' || typeof value === 'boolean') return value;
111
+ if (d > 6 || typeof value !== 'object') return null;
112
+ if (Array.isArray(value)) {
113
+ return value.slice(0, 32).map(function (item) {
114
+ return compactStructuredEventValue(item, config, d + 1);
115
+ });
116
+ }
117
+ var out = {};
118
+ var keys = Object.keys(value).slice(0, 32);
119
+ for (var i = 0; i < keys.length; i++) {
120
+ out[keys[i]] = compactStructuredEventValue(value[keys[i]], config, d + 1);
121
+ }
122
+ return out;
123
+ }
124
+
125
+ function hashString(value) {
126
+ var hash = 2166136261;
127
+ var text = String(value || '');
128
+ for (var i = 0; i < text.length; i++) {
129
+ hash ^= text.charCodeAt(i);
130
+ hash = Math.imul(hash, 16777619);
131
+ }
132
+ return (hash >>> 0).toString(16).padStart(8, '0');
133
+ }
134
+
104
135
  return {
136
+ compactStructuredEventValue: compactStructuredEventValue,
105
137
  compactRunsForStorage: compactRunsForStorage,
106
138
  compactRunActionPayload: compactRunActionPayload,
107
- compactProviderSnapshot: compactProviderSnapshot
139
+ compactProviderSnapshot: compactProviderSnapshot,
140
+ hashString: hashString
108
141
  };
109
142
  });
@@ -18,6 +18,7 @@
18
18
  'waiting_for_idle',
19
19
  'applying',
20
20
  'awaiting_health',
21
+ 'rolling_back',
21
22
  'committed',
22
23
  'rolled_back',
23
24
  'failed'
@@ -27,7 +28,8 @@
27
28
  'staged',
28
29
  'waiting_for_idle',
29
30
  'applying',
30
- 'awaiting_health'
31
+ 'awaiting_health',
32
+ 'rolling_back'
31
33
  ]);
32
34
  const TERMINAL_STATES = new Set(['committed', 'rolled_back', 'failed']);
33
35
  const QUIET_FAILURE_CODES = new Set([
@@ -44,6 +46,7 @@
44
46
  waiting_for_idle: { value: 65, determinate: true, phase: 'waiting' },
45
47
  applying: { value: 82, determinate: false, phase: 'applying' },
46
48
  awaiting_health: { value: 94, determinate: false, phase: 'health' },
49
+ rolling_back: { value: 94, determinate: false, phase: 'rollback' },
47
50
  committed: { value: 100, determinate: true, phase: 'committed' },
48
51
  rolled_back: { value: 94, determinate: true, phase: 'rolled_back' },
49
52
  failed: { value: 0, determinate: true, phase: 'failed' }
@@ -63,8 +66,12 @@
63
66
  blocker: blockers[0] || '',
64
67
  blockers,
65
68
  transactionId: safeId(value.transactionId),
69
+ initiatedBy: ['automatic', 'manual'].includes(value.initiatedBy) ? value.initiatedBy : '',
66
70
  lastCheckedAt: finiteNumber(value.lastCheckedAt),
67
71
  stagedAt: finiteNumber(value.stagedAt),
72
+ phaseStartedAt: finiteNumber(value.phaseStartedAt),
73
+ deadlineAt: finiteNumber(value.deadlineAt),
74
+ heartbeatAt: finiteNumber(value.heartbeatAt),
68
75
  postponeUntil: finiteNumber(value.postponeUntil),
69
76
  code: safeToken(value.code),
70
77
  message: String(value.message || '').replace(/\s+/g, ' ').trim().slice(0, 300)
@@ -96,7 +103,11 @@
96
103
  const execution = EXECUTION_STATES.has(state.state);
97
104
  const available = state.state === 'update_available' && newer;
98
105
  const progress = getProgressModel(state);
99
- const quietFailure = state.state === 'failed' && QUIET_FAILURE_CODES.has(state.code);
106
+ const quietFailure = state.state === 'failed' &&
107
+ state.initiatedBy !== 'manual' &&
108
+ QUIET_FAILURE_CODES.has(state.code);
109
+ const visibleFailure = state.state === 'failed' &&
110
+ (state.initiatedBy === 'manual' || (!quietFailure && newer));
100
111
 
101
112
  return {
102
113
  state,
@@ -112,7 +123,7 @@
112
123
  snoozed,
113
124
  execution,
114
125
  showPanel: (available && !snoozed) || execution || state.state === 'rolled_back' ||
115
- (state.state === 'failed' && !quietFailure && newer),
126
+ visibleFailure,
116
127
  badge: getBadge(state, { available, snoozed }),
117
128
  actions: {
118
129
  check: ['idle', 'committed', 'rolled_back', 'failed'].includes(state.state),
@@ -135,7 +146,7 @@
135
146
  }
136
147
 
137
148
  function getBadge(state, flags) {
138
- if (state.state === 'applying' || state.state === 'awaiting_health') {
149
+ if (state.state === 'applying' || state.state === 'awaiting_health' || state.state === 'rolling_back') {
139
150
  return { text: '...', color: '#3578bd' };
140
151
  }
141
152
  if (state.state === 'failed' || state.state === 'rolled_back') {