codex-overleaf-link 2.5.1 → 2.6.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 (93) hide show
  1. package/README.md +149 -398
  2. package/README.zh-CN.md +153 -404
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +78 -3
  5. package/extension/src/backgroundSharedSessions.js +55 -2
  6. package/extension/src/backgroundUpdateCoordinator.js +128 -12
  7. package/extension/src/content/activeTurnControl.js +164 -7
  8. package/extension/src/content/composerAttachments.js +11 -2
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +920 -276
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsPanel.js +18 -0
  14. package/extension/src/content/generated/content.bundle.js +4496 -924
  15. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  16. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  17. package/extension/src/content/localSkillsPanel.js +2 -2
  18. package/extension/src/content/markdownDomRenderer.js +5 -3
  19. package/extension/src/content/markdownText.js +42 -5
  20. package/extension/src/content/modelPicker.js +1 -0
  21. package/extension/src/content/nativeCompatibilityController.js +29 -10
  22. package/extension/src/content/otWarmMirror.js +1 -1
  23. package/extension/src/content/pageBridgeClient.js +2 -2
  24. package/extension/src/content/panelMaintenance.js +6 -2
  25. package/extension/src/content/panelRenderer.js +51 -27
  26. package/extension/src/content/pendingInputView.js +1 -1
  27. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  28. package/extension/src/content/providerSettingsDialog.js +2 -1
  29. package/extension/src/content/recentProjects.js +4 -3
  30. package/extension/src/content/runActivitySummary.js +122 -14
  31. package/extension/src/content/runCompletionMeta.js +194 -6
  32. package/extension/src/content/runController.js +2 -1
  33. package/extension/src/content/runFailureNotice.js +8 -1
  34. package/extension/src/content/runPresence.js +18 -8
  35. package/extension/src/content/runQueueScheduler.js +9 -5
  36. package/extension/src/content/runRecoveryActions.js +2 -1
  37. package/extension/src/content/runResultActions.js +22 -20
  38. package/extension/src/content/runReviewController.js +94 -35
  39. package/extension/src/content/runTimelineView.js +224 -25
  40. package/extension/src/content/scopedPersistencePanelState.js +92 -23
  41. package/extension/src/content/sensitivePreflight.js +14 -1
  42. package/extension/src/content/sessionManager.js +49 -3
  43. package/extension/src/content/sessionMenuView.js +54 -18
  44. package/extension/src/content/sessionPersistence.js +25 -5
  45. package/extension/src/content/settingsPanel.js +15 -2
  46. package/extension/src/content/settingsWorkbench.js +127 -89
  47. package/extension/src/content/sharedSessionViewSync.js +4 -3
  48. package/extension/src/content/subagentActivityView.js +23 -7
  49. package/extension/src/content/updateNotice.js +60 -6
  50. package/extension/src/content/writebackOrchestrator.js +35 -22
  51. package/extension/src/page/compileBridge.js +193 -180
  52. package/extension/src/page/overleafCapabilities.js +4 -1
  53. package/extension/src/page/textCoordinates.js +17 -1
  54. package/extension/src/page/textFileCreator.js +9 -1
  55. package/extension/src/page/writebackRouter.js +24 -77
  56. package/extension/src/pageBridge.js +107 -66
  57. package/extension/src/popup.js +1 -1
  58. package/extension/src/shared/agentTranscript.js +350 -17
  59. package/extension/src/shared/auditRecords.js +2 -0
  60. package/extension/src/shared/compatibility.js +1 -1
  61. package/extension/src/shared/compileAdapter.js +58 -17
  62. package/extension/src/shared/composerAttachmentDraft.js +6 -1
  63. package/extension/src/shared/contextUsage.js +125 -0
  64. package/extension/src/shared/failureReasons.js +42 -3
  65. package/extension/src/shared/i18n.js +299 -29
  66. package/extension/src/shared/lineReferences.js +38 -31
  67. package/extension/src/shared/pageRpcContract.js +1 -1
  68. package/extension/src/shared/panelIcons.js +39 -0
  69. package/extension/src/shared/reviewing.js +130 -1
  70. package/extension/src/shared/runActivityModel.js +70 -4
  71. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  72. package/extension/src/shared/sessionState.js +63 -4
  73. package/extension/src/shared/sessionTitle.js +14 -1
  74. package/extension/src/shared/sharedSessionBridge.js +81 -13
  75. package/extension/src/shared/staleGuard.js +72 -63
  76. package/extension/src/shared/storageDb.js +35 -3
  77. package/extension/src/shared/storageMigration.js +7 -1
  78. package/extension/src/shared/storageRunActions.js +62 -8
  79. package/extension/src/shared/undoOperations.js +126 -28
  80. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  81. package/extension/src/shared/writebackSettlement.js +21 -3
  82. package/extension/styles/panel.css +1106 -490
  83. package/native-host/src/bootstrapUpdate.js +52 -16
  84. package/native-host/src/chatBridgeRequest.js +60 -6
  85. package/native-host/src/chatBridgeResponse.js +10 -1
  86. package/native-host/src/chatCompletionsBridge.js +29 -2
  87. package/native-host/src/codexPromptAssembly.js +86 -118
  88. package/native-host/src/codexSessionRunner.js +126 -17
  89. package/native-host/src/providerContextError.js +20 -0
  90. package/native-host/src/taskRunnerRuntime.js +18 -6
  91. package/native-host/src/textPatch.js +473 -377
  92. package/native-host/src/updateManager.js +120 -15
  93. package/package.json +5 -8
@@ -7,6 +7,10 @@
7
7
  })(typeof window !== 'undefined' ? window : globalThis, function sensitivePreflightFactory() {
8
8
  'use strict';
9
9
 
10
+ // Report evidence admits detector identities only, never matched text or previews.
11
+ const REPORT_DETECTOR_IDS = new Set(['private-key', 'bearer-token', 'api-token', 'aws-access-key',
12
+ 'google-api-key', 'huggingface-token', 'gitlab-token', 'stripe-live-secret', 'jwt-token', 'secret-assignment']);
13
+
10
14
  function create({
11
15
  SensitiveScan, normalizeGovernanceRules, isTextSnapshotFile, sendBackgroundNative,
12
16
  getCurrentProjectId, appendRunEvent, appendCompletionReport, showPluginConfirm, tr, tx
@@ -62,12 +66,21 @@
62
66
  }
63
67
  // Blocking an extra message must not finish the task already in progress.
64
68
  if (guidance) return { blocked: true, findings };
65
- appendCompletionReport({
69
+ const detectorIds = [...new Set(findings.map(finding => finding?.detectorId))];
70
+ appendCompletionReport({ conclusionKey: 'sensitive_blocked',
66
71
  conclusion: tx(
67
72
  'This run was blocked before Codex received project context because sensitive content was detected.',
68
73
  '检测到敏感内容,本轮已在向 Codex 发送项目上下文前阻止。'
69
74
  ),
70
75
  status: 'blocked',
76
+ unchangedReason: tx('Sensitive content was found before sending.', '发送前发现了敏感内容。'),
77
+ failure: { code: 'sensitive_content_blocked', stage: 'preflight', severity: 'blocked',
78
+ retryable: false, changedDocument: false,
79
+ evidence: {
80
+ // An unknown type makes the cause generic, even alongside one known type.
81
+ detectorIds: detectorIds.every(id => REPORT_DETECTOR_IDS.has(id)) ? detectorIds.slice(0, 5) : [],
82
+ findingCount: findings.length
83
+ } },
71
84
  operations: [],
72
85
  applyResults: [],
73
86
  nextStep: tx(
@@ -11,7 +11,8 @@
11
11
  const {
12
12
  SessionPanel,
13
13
  SessionState,
14
- SessionMenuView
14
+ SessionMenuView,
15
+ PanelIcons
15
16
  } = deps;
16
17
  const {
17
18
  createSession,
@@ -30,6 +31,9 @@
30
31
  saveState,
31
32
  saveStateSoon,
32
33
  readPanelInputs,
34
+ flushComposerDraft,
35
+ setSessionTransition,
36
+ getDraftScope,
33
37
  applyStateToPanel,
34
38
  syncSessionProvider,
35
39
  getPanel,
@@ -47,6 +51,7 @@
47
51
  getActiveSession,
48
52
  getPanel,
49
53
  getSessionDisplayTitle,
54
+ icons: PanelIcons,
50
55
  getState,
51
56
  isDisplayableSession,
52
57
  isSessionRunning,
@@ -60,7 +65,31 @@
60
65
  closeSessionMenu
61
66
  } = sessionMenuView;
62
67
 
63
- async function startNewSession() {
68
+ let transitionToken = null;
69
+
70
+ async function withSessionTransition(change) {
71
+ if (transitionToken) return;
72
+ const token = {};
73
+ transitionToken = token;
74
+ const scope = getDraftScope?.();
75
+ try {
76
+ // Lock before the first wait, including an outstanding IME commit.
77
+ await setSessionTransition?.(true);
78
+ if (getDraftScope?.() !== scope) return;
79
+ return await change(scope);
80
+ } finally {
81
+ if (transitionToken === token) {
82
+ transitionToken = null;
83
+ setSessionTransition?.(false);
84
+ }
85
+ }
86
+ }
87
+
88
+ function startNewSession() {
89
+ return withSessionTransition(createNewSession);
90
+ }
91
+
92
+ async function createNewSession(draftScope) {
64
93
  // Leaving a still-running session is allowed but never silent: the run
65
94
  // keeps going in the background and lands in its own session.
66
95
  const runningActive = getActiveSession(getState());
@@ -74,7 +103,10 @@
74
103
  return;
75
104
  }
76
105
  }
106
+ if (getDraftScope?.() !== draftScope) return;
77
107
  readPanelInputs();
108
+ await flushComposerDraft?.();
109
+ if (getDraftScope?.() !== draftScope) return;
78
110
  // Reuse an already-empty, idle active session instead of minting a ghost.
79
111
  // Empty sessions are invisible in the list yet still consume the storage cap
80
112
  // (slice(-20) / maxSessions), so spamming "New Session" could silently evict
@@ -99,6 +131,7 @@
99
131
  if (!evictionApproved) {
100
132
  return;
101
133
  }
134
+ if (getDraftScope?.() !== draftScope) return;
102
135
  }
103
136
  const session = createSession({
104
137
  mode: getState().mode,
@@ -114,15 +147,22 @@
114
147
  activeSessionId: session.id
115
148
  }));
116
149
  applyStateToPanel();
150
+ const destinationScope = getDraftScope?.();
117
151
  await syncSessionProvider?.();
152
+ if (getDraftScope?.() !== destinationScope) return;
118
153
  await saveState();
154
+ if (getDraftScope?.() !== destinationScope) return;
119
155
  getPanel().querySelector('[data-task]')?.focus();
120
156
  }
121
157
 
122
- async function switchSession(sessionId) {
158
+ function switchSession(sessionId) {
123
159
  if (sessionId === getState().activeSessionId) {
124
160
  return;
125
161
  }
162
+ return withSessionTransition(scope => switchToSession(sessionId, scope));
163
+ }
164
+
165
+ async function switchToSession(sessionId, draftScope) {
126
166
  const runningActive = getActiveSession(getState());
127
167
  if (runningActive && isSessionRunning(runningActive)) {
128
168
  const approved = await showPluginConfirm({
@@ -134,10 +174,15 @@
134
174
  return;
135
175
  }
136
176
  }
177
+ if (getDraftScope?.() !== draftScope) return;
137
178
  readPanelInputs();
179
+ await flushComposerDraft?.();
180
+ if (getDraftScope?.() !== draftScope) return;
138
181
  setState(setActiveSession(getState(), sessionId));
139
182
  applyStateToPanel();
183
+ const destinationScope = getDraftScope?.();
140
184
  await syncSessionProvider?.();
185
+ if (getDraftScope?.() !== destinationScope) return;
141
186
  await saveState();
142
187
  }
143
188
 
@@ -295,6 +340,7 @@
295
340
  bindActiveSessionHeader,
296
341
  startNewSession,
297
342
  switchSession,
343
+ isTransitioning: () => transitionToken !== null,
298
344
  deleteSessionWithConfirm,
299
345
  renderSessionList,
300
346
  commitSessionRename,
@@ -11,6 +11,7 @@
11
11
  getPanel,
12
12
  getSessionDisplayTitle,
13
13
  getState,
14
+ icons = null,
14
15
  isDisplayableSession,
15
16
  isSessionRunning,
16
17
  startNewSession,
@@ -20,19 +21,20 @@
20
21
 
21
22
  function applySessionLabel() {
22
23
  const trigger = getPanel().querySelector('[data-session-menu-trigger]');
23
- if (trigger) {
24
- trigger.title = tr('sessionMenuOpen');
25
- trigger.setAttribute('aria-label', tr('sessionMenuOpen'));
26
- }
27
24
  const textNode = getPanel().querySelector('[data-session-label-text]');
28
25
  const deleteButton = getPanel().querySelector('[data-session-delete]');
29
26
  const renameButton = getPanel().querySelector('[data-session-rename]');
30
27
  const renameInput = getPanel().querySelector('[data-session-rename-input]');
31
28
  const active = getActiveSession(getState());
29
+ const label = active && isDisplayableSession(active)
30
+ ? getSessionDisplayTitle(active)
31
+ : tr('newSessionFallback');
32
32
  if (textNode) {
33
- textNode.textContent = active && isDisplayableSession(active)
34
- ? getSessionDisplayTitle(active)
35
- : tr('newSessionFallback');
33
+ textNode.textContent = label;
34
+ }
35
+ if (trigger) {
36
+ trigger.title = `${label} · ${tr('sessionMenuOpen')}`;
37
+ trigger.setAttribute('aria-label', `${tr('sessionMenuOpen')}: ${label}`);
36
38
  }
37
39
  if (deleteButton) {
38
40
  const running = active ? isSessionRunning(active) : false;
@@ -111,11 +113,15 @@
111
113
  .slice()
112
114
  .sort((a, b) => String(b.updatedAt || b.createdAt || '').localeCompare(String(a.updatedAt || a.createdAt || '')));
113
115
  for (const session of sessions) {
114
- const row = document.createElement('button');
115
- row.type = 'button';
116
+ const row = document.createElement('div');
116
117
  row.className = 'codex-session-menu-item';
118
+ row.setAttribute('role', 'none');
117
119
  row.dataset.active = session.id === state?.activeSessionId ? 'true' : 'false';
118
120
  row.dataset.running = isSessionRunning(session) ? 'true' : 'false';
121
+ const switchButton = document.createElement('button');
122
+ switchButton.type = 'button';
123
+ switchButton.className = 'codex-session-menu-switch';
124
+ switchButton.setAttribute('role', 'menuitem');
119
125
  const title = document.createElement('span');
120
126
  title.className = 'codex-session-menu-item-title';
121
127
  title.textContent = getSessionDisplayTitle(session);
@@ -127,26 +133,56 @@
127
133
  if (absoluteTs) {
128
134
  time.title = new Date(absoluteTs).toLocaleString();
129
135
  }
130
- row.append(title, time);
131
- row.addEventListener('click', event => {
136
+ switchButton.append(title, time);
137
+ switchButton.addEventListener('click', event => {
132
138
  event.stopPropagation();
133
139
  closeSessionMenu();
134
140
  if (session.id !== getState()?.activeSessionId) {
135
141
  switchSession(session.id);
136
142
  }
137
143
  });
144
+ const deleteButton = menuAction('codex-session-menu-item-delete', 'close', '',
145
+ row.dataset.running === 'true' ? tr('deleteRunningSession') : tr('deleteSession'),
146
+ () => deleteSessionWithConfirm(session.id));
147
+ deleteButton.setAttribute('role', 'menuitem');
148
+ deleteButton.disabled = row.dataset.running === 'true';
149
+ row.append(switchButton, deleteButton);
138
150
  menu.append(row);
139
151
  }
140
- const createButton = document.createElement('button');
141
- createButton.type = 'button';
142
- createButton.className = 'codex-session-menu-new';
143
- createButton.textContent = `+ ${tr('newSession')}`;
144
- createButton.addEventListener('click', event => {
152
+ const footer = document.createElement('div');
153
+ footer.className = 'codex-session-menu-footer';
154
+ const active = getActiveSession(state);
155
+ const running = active ? isSessionRunning(active) : false;
156
+ const soleEmpty = Boolean(active) && !isDisplayableSession(active) && (state?.sessions || []).length <= 1;
157
+ const createButton = menuAction('codex-session-menu-new', 'plus', tr('sessionMenuNewShort'), tr('newSession'), () => startNewSession());
158
+ const renameButton = menuAction('codex-session-menu-rename', 'pencil', tr('sessionMenuRenameShort'), tr('renameSession'), () => beginActiveSessionRename());
159
+ renameButton.disabled = !active;
160
+ const deleteButton = menuAction('codex-session-menu-delete', 'close', tr('sessionMenuDeleteShort'),
161
+ running ? tr('deleteRunningSession') : tr('deleteSession'), () => {
162
+ if (getState()?.activeSessionId) deleteSessionWithConfirm(getState().activeSessionId);
163
+ });
164
+ deleteButton.disabled = !active || running || soleEmpty;
165
+ footer.append(createButton, renameButton, deleteButton);
166
+ menu.append(footer);
167
+ }
168
+
169
+ function menuAction(className, iconName, label, description, onSelect) {
170
+ const button = document.createElement('button');
171
+ button.type = 'button';
172
+ button.className = className;
173
+ button.title = description;
174
+ button.setAttribute('aria-label', description);
175
+ const glyph = icons?.svg?.(iconName);
176
+ if (glyph) button.insertAdjacentHTML('beforeend', glyph);
177
+ const text = document.createElement('span');
178
+ text.textContent = label;
179
+ button.append(text);
180
+ button.addEventListener('click', event => {
145
181
  event.stopPropagation();
146
182
  closeSessionMenu();
147
- startNewSession();
183
+ onSelect();
148
184
  });
149
- menu.append(createButton);
185
+ return button;
150
186
  }
151
187
 
152
188
  function beginActiveSessionRename() {
@@ -74,20 +74,40 @@
74
74
  .filter(record => !deleted.has(record.id))
75
75
  .map(record => [record.id, record]));
76
76
  const mergedRecords = sessionRecords.map(record => StorageDb.mergeSessionReviewState?.(
77
- record, existingById.get(record.id), options.reviewRunIds
77
+ record, existingById.get(record.id), options.reviewRunIds, { shared: StorageDb.sharedSessionsEnabled?.() === true }
78
78
  ) || record);
79
+ const reviewRunIds = new Set(options.reviewRunIds || []);
80
+ // Shared storage merges local edits and explicitly requested Undo markers
81
+ // against the page baseline. Legacy and ordinary stale saves keep the age guard.
79
82
  const writable = mergedRecords.filter(record => {
80
83
  const existing = existingById.get(record.id);
81
84
  if (!existing) return true;
82
- return (Date.parse(record.updatedAt || '') || 0) >= (Date.parse(existing.updatedAt || '') || 0);
85
+ return (Date.parse(record.updatedAt || '') || 0) >= (Date.parse(existing.updatedAt || '') || 0)
86
+ || (StorageDb.sharedSessionsEnabled?.() === true
87
+ && (StorageDb.hasUnpersistedSharedSession?.(record) === true
88
+ || (record.runs || []).some(run => reviewRunIds.has(run.id) && run.undoOwnerProtocol === 1)));
83
89
  });
84
90
  const cleanedExisting = existingSessions.filter((record, index) =>
85
91
  record !== rawExistingSessions[index] && !writable.some(item => item.id === record.id)
86
92
  );
87
93
  if (cleanedExisting.length || writable.length) {
88
- const written = await StorageDb.putRecords('sessions', [...cleanedExisting, ...writable], {
89
- reviewRunIds: options.reviewRunIds
90
- });
94
+ const reviewReceipts = [];
95
+ if (StorageDb.sharedSessionsEnabled?.() === true && reviewRunIds.size) {
96
+ for (const record of writable) {
97
+ const ids = (record.runs || []).filter(run => reviewRunIds.has(run.id)).map(run => run.id);
98
+ if (ids.length) reviewReceipts.push(await StorageDb.putSessionReviewState(record, ids));
99
+ }
100
+ }
101
+ let written;
102
+ try {
103
+ written = await StorageDb.putRecords('sessions', [...cleanedExisting, ...writable], {
104
+ reviewRunIds: options.reviewRunIds, reviewReceipts
105
+ });
106
+ } catch (error) {
107
+ if (error.code === 'shared_session_conflict' && reviewReceipts.length)
108
+ Object.defineProperty(error, 'reviewReceipts', { value: reviewReceipts, configurable: true });
109
+ throw error;
110
+ }
91
111
  // Check the actual durable result, including a decision already saved
92
112
  // from another origin, instead of acknowledging only the proposed view.
93
113
  return written.filter(record => writable.some(candidate => candidate.id === record.id));
@@ -9,8 +9,10 @@
9
9
 
10
10
  function codexIcon(name) {
11
11
  const icons = {
12
+ archive: '<rect x="2.5" y="3" width="11" height="3.2" rx=".8"/><path d="M3.5 6.2v6.3a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V6.2"/><path d="M6.5 9h3"/>',
12
13
  appearance: '<path d="M8 2.2v1.1"/><path d="M8 12.7v1.1"/><path d="M2.2 8h1.1"/><path d="M12.7 8h1.1"/><path d="m3.9 3.9.8.8"/><path d="m11.3 11.3.8.8"/><path d="m12.1 3.9-.8.8"/><path d="m4.7 11.3-.8.8"/><circle cx="8" cy="8" r="2.4"/>',
13
14
  bolt: '<path d="M8.9 1.8 3.8 8.8h3.3l-.5 5.4 5.5-7.5H8.8z"/>',
15
+ close: '<path d="m4.6 4.6 6.8 6.8M11.4 4.6l-6.8 6.8"/>',
14
16
  database: '<ellipse cx="8" cy="4" rx="4.5" ry="1.9"/><path d="M3.5 4v4c0 1 2 1.9 4.5 1.9s4.5-.9 4.5-1.9V4"/><path d="M3.5 8v4c0 1 2 1.9 4.5 1.9s4.5-.9 4.5-1.9V8"/>',
15
17
  flask: '<path d="M6 2.5h4"/><path d="M7 2.5v4.1l-3.4 5.3A1.4 1.4 0 0 0 4.8 14h6.4a1.4 1.4 0 0 0 1.2-2.1L9 6.6V2.5"/><path d="M5.3 10.5h5.4"/>',
16
18
  history: '<path d="M4.2 3.4h7.6"/><path d="M4.2 8h7.6"/><path d="M4.2 12.6h4.6"/><path d="M2.2 3.4h.1"/><path d="M2.2 8h.1"/><path d="M2.2 12.6h.1"/>',
@@ -36,7 +38,9 @@
36
38
  container,
37
39
  callbacks: options.callbacks || {},
38
40
  i18n: options.i18n || null,
39
- button: options.button || null
41
+ button: options.button || null,
42
+ // Names the open project in project-page scope lines; may return ''.
43
+ getProjectName: typeof options.getProjectName === 'function' ? options.getProjectName : null
40
44
  };
41
45
 
42
46
  container.innerHTML = `
@@ -211,6 +215,7 @@
211
215
  <p class="codex-set-row-help" data-update-settings-summary data-i18n="softwareUpdatesHelp">Automatically checks signed stable releases for the Extension and Native Host.</p>
212
216
  <p class="codex-set-note" data-update-settings-state aria-live="polite" hidden></p>
213
217
  <button type="button" class="codex-set-btn" data-check-updates data-i18n="checkForUpdates">Check for updates</button>
218
+ <button type="button" class="codex-set-btn" data-restore-previous-version hidden>Restore previous version</button>
214
219
  </div>
215
220
  </div>
216
221
  </details>
@@ -334,6 +339,7 @@
334
339
  return host || null;
335
340
  },
336
341
  setProviderCloseGuard: guard => instance.workbench?.setProviderCloseGuard(guard),
342
+ revealGroup: key => instance.workbench?.revealGroup(key) || null,
337
343
  loadState: state => loadState(instance, state),
338
344
  readState: () => readState(instance),
339
345
  setStatus: (text, status) => setStatus(instance, text, status),
@@ -464,7 +470,9 @@
464
470
  };
465
471
  }
466
472
 
467
- function setStatus(target, text, status = 'info') {
473
+ // options.page names the workbench page a message belongs to; the workbench
474
+ // shows it only while that page is active. Untagged messages show everywhere.
475
+ function setStatus(target, text, status = 'info', options = {}) {
468
476
  const instance = target?._instance || target;
469
477
  const element = instance?.container?.querySelector('[data-project-settings-status]');
470
478
  if (!element) {
@@ -472,6 +480,9 @@
472
480
  }
473
481
  element.textContent = text || '';
474
482
  element.dataset.status = status;
483
+ if (options.page) element.dataset.statusPage = options.page;
484
+ else delete element.dataset.statusPage;
485
+ instance.workbench?.refresh();
475
486
  }
476
487
 
477
488
  function clearStatus(target) {
@@ -482,6 +493,8 @@
482
493
  }
483
494
  element.textContent = '';
484
495
  delete element.dataset.status;
496
+ delete element.dataset.statusPage;
497
+ instance.workbench?.refresh();
485
498
  }
486
499
 
487
500
  function setSkillsSummary(target, text) {