codex-overleaf-link 1.3.6 → 1.3.8

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.
@@ -38,6 +38,12 @@
38
38
  skipped: true,
39
39
  pending: true
40
40
  };
41
+ var VALID_TRACKED_CHANGE_STATUSES = {
42
+ pending: true,
43
+ accepted: true,
44
+ rejected: true,
45
+ needs_review: true
46
+ };
41
47
  var SECRET_REDACTION_PATTERNS = [
42
48
  /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
43
49
  /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/gi,
@@ -232,6 +238,27 @@
232
238
  function buildSessionRecord(input) {
233
239
  var now = new Date().toISOString();
234
240
  var titleSource = input.titleSource === 'manual' ? 'manual' : 'auto';
241
+ var updatedAt = typeof input.updatedAt === 'string' ? input.updatedAt : now;
242
+ // Welcome-panel + write-guard v1.3.8 add-on (Task 3): persist the four
243
+ // Recent-projects fields on every session record so the cross-project
244
+ // query (`listRecentProjectsAcrossAccount`) can filter / sort / render
245
+ // without touching the raw `task` text.
246
+ //
247
+ // `accountScopeId` is derived via the page-side injection point — T4
248
+ // installs the real implementation on `window.codexOverleafDeriveAccountScopeId`.
249
+ // For T3 the fallback is `() => null`; records persisted with a null scope
250
+ // surface `accountScopeUnavailable: true` and are filtered out of the
251
+ // cross-project query (spec §5.2 degraded mode).
252
+ var safeTaskSummary = typeof input.safeTaskSummary === 'string'
253
+ ? input.safeTaskSummary
254
+ : deriveSafeTaskSummaryFromInput(input);
255
+ var accountScopeId = resolveAccountScopeId(input);
256
+ var accountScopeUnavailable = typeof input.accountScopeUnavailable === 'boolean'
257
+ ? input.accountScopeUnavailable
258
+ : !accountScopeId;
259
+ var lastActivityAt = typeof input.lastActivityAt === 'string' && input.lastActivityAt
260
+ ? input.lastActivityAt
261
+ : updatedAt;
235
262
  return {
236
263
  id: input.id || generateId('ses'),
237
264
  projectId: input.projectId || '',
@@ -249,10 +276,77 @@
249
276
  speedTier: typeof input.speedTier === 'string' ? input.speedTier : '',
250
277
  requireReviewing: input.requireReviewing !== false,
251
278
  createdAt: typeof input.createdAt === 'string' ? input.createdAt : now,
252
- updatedAt: typeof input.updatedAt === 'string' ? input.updatedAt : now
279
+ updatedAt: updatedAt,
280
+ lastActivityAt: lastActivityAt,
281
+ accountScopeId: accountScopeId,
282
+ accountScopeUnavailable: accountScopeUnavailable,
283
+ safeTaskSummary: safeTaskSummary
253
284
  };
254
285
  }
255
286
 
287
+ function resolveAccountScopeId(input) {
288
+ if (input && typeof input.accountScopeId === 'string' && input.accountScopeId) {
289
+ return input.accountScopeId;
290
+ }
291
+ var globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : null);
292
+ var derive = globalScope && globalScope.window && globalScope.window.codexOverleafDeriveAccountScopeId;
293
+ if (!(derive instanceof Function)) {
294
+ // No injection installed (T3 fallback): tests + early page-load reads
295
+ // both land here. Returning `null` puts the session in degraded mode
296
+ // and excludes it from the cross-project query.
297
+ return null;
298
+ }
299
+ try {
300
+ var value = derive();
301
+ return typeof value === 'string' && value ? value : null;
302
+ } catch (_error) {
303
+ return null;
304
+ }
305
+ }
306
+
307
+ function deriveSafeTaskSummaryFromInput(input) {
308
+ var SessionState = loadSessionState();
309
+ if (!SessionState || !(SessionState.computeSafeTaskSummary instanceof Function)) {
310
+ return '';
311
+ }
312
+ var task = typeof input.task === 'string' && input.task ? input.task : '';
313
+ if (!task && Array.isArray(input.runs) && input.runs.length) {
314
+ var firstRun = input.runs[0];
315
+ if (firstRun && typeof firstRun.task === 'string') {
316
+ task = firstRun.task;
317
+ }
318
+ }
319
+ return SessionState.computeSafeTaskSummary(task);
320
+ }
321
+
322
+ // Module-local lazy require of the sibling sessionState module. CommonJS
323
+ // resolves once; subsequent calls hit the require cache. We avoid an import
324
+ // at the top of the file so the module surface remains friendly to the
325
+ // page-side IIFE wrapper (no `require` available in that context — the page
326
+ // build uses the global `CodexOverleafSessionState` via the IIFE branch).
327
+ var _sessionStateCache = null;
328
+ function loadSessionState() {
329
+ if (_sessionStateCache !== null) {
330
+ return _sessionStateCache || null;
331
+ }
332
+ var globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : null);
333
+ if (globalScope && globalScope.CodexOverleafSessionState) {
334
+ _sessionStateCache = globalScope.CodexOverleafSessionState;
335
+ return _sessionStateCache;
336
+ }
337
+ if (typeof require === 'function') {
338
+ try {
339
+ _sessionStateCache = require('./sessionState');
340
+ return _sessionStateCache;
341
+ } catch (_error) {
342
+ _sessionStateCache = false;
343
+ return null;
344
+ }
345
+ }
346
+ _sessionStateCache = false;
347
+ return null;
348
+ }
349
+
256
350
  function buildTurnRecord(input) {
257
351
  var now = new Date().toISOString();
258
352
  return {
@@ -542,7 +636,7 @@
542
636
  }
543
637
 
544
638
  function compactRunForStorage(run) {
545
- return {
639
+ var compact = {
546
640
  id: run.id,
547
641
  task: normalizeDisplayTextForStorage(run.task || 'untitled task', SESSION_STORAGE_LIMITS.taskChars),
548
642
  mode: typeof run.mode === 'string' ? redactSecretLikeText(run.mode) : '',
@@ -551,6 +645,7 @@
551
645
  speedTier: typeof run.speedTier === 'string' ? redactSecretLikeText(run.speedTier) : '',
552
646
  status: normalizeRunStatus(run.status),
553
647
  statusText: normalizeDisplayTextForStorage(run.statusText, SESSION_STORAGE_LIMITS.statusTextChars),
648
+ runProjectId: normalizeProjectPrefKey(run.runProjectId),
554
649
  startedAt: typeof run.startedAt === 'string' ? redactSecretLikeText(run.startedAt) : '',
555
650
  finishedAt: typeof run.finishedAt === 'string' ? redactSecretLikeText(run.finishedAt) : '',
556
651
  events: compactRunEventsForStorage(run.events),
@@ -562,6 +657,10 @@
562
657
  undoExpectedFiles: [],
563
658
  undoStatus: normalizeDisplayTextForStorage(run.undoStatus, SESSION_STORAGE_LIMITS.statusTextChars)
564
659
  };
660
+ if (VALID_TRACKED_CHANGE_STATUSES[run.trackedChangeStatus] === true) {
661
+ compact.trackedChangeStatus = run.trackedChangeStatus;
662
+ }
663
+ return compact;
565
664
  }
566
665
 
567
666
  function compactRunEventsForStorage(events) {
@@ -746,7 +845,19 @@
746
845
  }
747
846
 
748
847
  function normalizeRunStatus(status) {
749
- return ['running', 'completed', 'failed'].indexOf(status) !== -1 ? status : 'completed';
848
+ // Welcome-panel + write-guard v1.3.8 add-on (Task 2/3): the run-status
849
+ // enum gained three post-navigation values. The storage normalizer must
850
+ // accept them so a settled run round-trips intact through `buildSessionRecord`.
851
+ // Unknown legacy values fall through to `completed` (the historical default).
852
+ return [
853
+ 'pending',
854
+ 'running',
855
+ 'completed',
856
+ 'failed',
857
+ 'background_completed',
858
+ 'needs_review_after_navigation',
859
+ 'abandoned_after_navigation'
860
+ ].indexOf(status) !== -1 ? status : 'completed';
750
861
  }
751
862
 
752
863
  function normalizeEventStatus(status) {
@@ -954,6 +1065,113 @@
954
1065
  return result;
955
1066
  }
956
1067
 
1068
+ // Welcome-panel + write-guard v1.3.8 add-on (Task 3): the Recent-projects
1069
+ // dashboard variant calls `listRecentProjectsAcrossAccount` to get the
1070
+ // sorted, deduped, capped list of projects in the current account scope.
1071
+ //
1072
+ // Contract (spec §5.6.1):
1073
+ // Input: { accountScopeId: string, limit?: number = 10 }
1074
+ // Output: Array<{ projectId, lastActivityAt, safeTaskSummary, primaryStatusBadge }>
1075
+ //
1076
+ // Fail-closed: if `accountScopeId` is falsy, returns `[]`. This is the
1077
+ // privacy floor — degraded mode must never leak across accounts.
1078
+ //
1079
+ // Implementation: full scan of the sessions store, group by `projectId`,
1080
+ // keep the row with the largest `lastActivityAt`, sort desc, cap at
1081
+ // `limit`. Full scan is acceptable for v1 given the small session count
1082
+ // (max ~12 per project × small number of projects). A future index on
1083
+ // `accountScopeId + lastActivityAt` is possible without a contract change.
1084
+ function listRecentProjectsAcrossAccount(options) {
1085
+ var accountScopeId = options && options.accountScopeId;
1086
+ var limit = options && Number.isFinite(options.limit) ? options.limit : 10;
1087
+ if (!accountScopeId) {
1088
+ return Promise.resolve([]);
1089
+ }
1090
+ return getAllSessions().then(function (all) {
1091
+ return filterRecentProjectsAcrossAccount(all, { accountScopeId: accountScopeId, limit: limit });
1092
+ });
1093
+ }
1094
+
1095
+ // Pure helper extracted from `listRecentProjectsAcrossAccount` so the
1096
+ // filtering / dedupe / sort / cap behavior can be tested without an
1097
+ // IndexedDB stub. Same contract as the async wrapper; takes the raw session
1098
+ // records as an array instead of opening the database.
1099
+ function filterRecentProjectsAcrossAccount(sessions, options) {
1100
+ var accountScopeId = options && options.accountScopeId;
1101
+ var limit = options && Number.isFinite(options.limit) ? options.limit : 10;
1102
+ if (!accountScopeId) {
1103
+ return [];
1104
+ }
1105
+ var byProject = {}; // projectId → session
1106
+ var all = Array.isArray(sessions) ? sessions : [];
1107
+ for (var i = 0; i < all.length; i++) {
1108
+ var s = all[i];
1109
+ if (!s) continue;
1110
+ if (s.accountScopeId !== accountScopeId) continue;
1111
+ if (typeof s.lastActivityAt !== 'string' || !s.lastActivityAt) continue;
1112
+ if (typeof s.projectId !== 'string' || !s.projectId) continue;
1113
+ var prev = byProject[s.projectId];
1114
+ if (!prev || prev.lastActivityAt < s.lastActivityAt) {
1115
+ byProject[s.projectId] = s;
1116
+ }
1117
+ }
1118
+ var survivors = [];
1119
+ var keys = Object.keys(byProject);
1120
+ for (var j = 0; j < keys.length; j++) {
1121
+ survivors.push(byProject[keys[j]]);
1122
+ }
1123
+ survivors.sort(function (a, b) {
1124
+ return b.lastActivityAt.localeCompare(a.lastActivityAt);
1125
+ });
1126
+ var rows = [];
1127
+ for (var k = 0; k < survivors.length && k < limit; k++) {
1128
+ var session = survivors[k];
1129
+ rows.push({
1130
+ projectId: session.projectId,
1131
+ lastActivityAt: session.lastActivityAt,
1132
+ safeTaskSummary: typeof session.safeTaskSummary === 'string' ? session.safeTaskSummary : '',
1133
+ primaryStatusBadge: derivePrimaryStatusBadge(session)
1134
+ });
1135
+ }
1136
+ return rows;
1137
+ }
1138
+
1139
+ // Per spec §5.10:
1140
+ // 1. trackedChangeStatus if set (pending/accepted/rejected/needs_review)
1141
+ // 2. else run status (running/completed/failed/background_completed/
1142
+ // needs_review_after_navigation/abandoned_after_navigation)
1143
+ // 3. fallback `pending`.
1144
+ function derivePrimaryStatusBadge(session) {
1145
+ var runs = session && Array.isArray(session.runs) ? session.runs : [];
1146
+ if (!runs.length) return 'pending';
1147
+ var latestRun = runs[runs.length - 1];
1148
+ if (!latestRun) return 'pending';
1149
+ if (typeof latestRun.trackedChangeStatus === 'string' && latestRun.trackedChangeStatus) {
1150
+ return latestRun.trackedChangeStatus;
1151
+ }
1152
+ if (typeof latestRun.status === 'string' && latestRun.status) {
1153
+ return latestRun.status;
1154
+ }
1155
+ return 'pending';
1156
+ }
1157
+
1158
+ // Full scan of the sessions store, used by the cross-project query above.
1159
+ // We deliberately do not go through `getAllByIndex` because the existing
1160
+ // session indexes are keyed by `projectId` / `updatedAt` — neither matches
1161
+ // the account-scope filter. A direct `getAll` over the whole store is the
1162
+ // simplest correct read.
1163
+ function getAllSessions() {
1164
+ return openDb().then(function (db) {
1165
+ return new Promise(function (resolve, reject) {
1166
+ var tx = db.transaction('sessions', 'readonly');
1167
+ var store = tx.objectStore('sessions');
1168
+ var request = store.getAll();
1169
+ request.onsuccess = function (event) { resolve(event.target.result || []); };
1170
+ request.onerror = function (event) { reject(event.target.error); };
1171
+ });
1172
+ });
1173
+ }
1174
+
957
1175
  function buildActiveSessionByProject(existing, projectId, sessionId) {
958
1176
  var result = {};
959
1177
  if (existing && typeof existing === 'object') {
@@ -1059,6 +1277,10 @@
1059
1277
  buildAuditLogRecord: buildAuditLogRecord,
1060
1278
  extractLightweightPrefs: extractLightweightPrefs,
1061
1279
  buildActiveSessionByProject: buildActiveSessionByProject,
1062
- createEventBuffer: createEventBuffer
1280
+ createEventBuffer: createEventBuffer,
1281
+ listRecentProjectsAcrossAccount: listRecentProjectsAcrossAccount,
1282
+ filterRecentProjectsAcrossAccount: filterRecentProjectsAcrossAccount,
1283
+ derivePrimaryStatusBadge: derivePrimaryStatusBadge,
1284
+ getAllSessions: getAllSessions
1063
1285
  };
1064
1286
  });
@@ -193,11 +193,15 @@ function formatWriteExpectation({ mode = 'auto', task = '', skillInvocation = nu
193
193
  return '- This is read-only. Inspect and explain; do not edit files.';
194
194
  }
195
195
  if (requestImpliesFileChanges(task)) {
196
- return [
196
+ const lines = [
197
197
  '- The request asks for file changes. You must edit the local workspace when you find concrete fixes.',
198
198
  '- Do not stop at a suggestion list or say you will not modify files unless no safe concrete edit exists.',
199
199
  '- If you intentionally leave files unchanged, explain the specific blocker in the final answer.'
200
- ].join('\n');
200
+ ];
201
+ if (requestTargetsAbstract(task)) {
202
+ lines.push('- The request targets the abstract/summary. Locate the LaTeX abstract environment or summary paragraph and edit that local project file directly.');
203
+ }
204
+ return lines.join('\n');
201
205
  }
202
206
  return [
203
207
  '- This mode can write. If the request asks for corrections, revisions, fixes, polishing, updates, or implementation, edit the local workspace directly.',
@@ -209,6 +213,10 @@ function requestImpliesFileChanges(task = '') {
209
213
  return /修正|修复|修改|改[一-龥]*|完善|补全|补充|润色|重写|改写|整理|调整|应用|写入|fix|correct|repair|revise|edit|update|rewrite|polish|improve|apply/i.test(String(task || ''));
210
214
  }
211
215
 
216
+ function requestTargetsAbstract(task = '') {
217
+ return /摘要|abstract|summary/i.test(String(task || ''));
218
+ }
219
+
212
220
  function normalizeFocusFiles(value) {
213
221
  const seen = new Set();
214
222
  const files = [];
@@ -821,13 +821,22 @@ function runCodexAppServerSession(input) {
821
821
 
822
822
  if (message.method) {
823
823
  recordAssistantMessage(message);
824
- emitCodexEvent(input.emit, 'codex.session.event', message.method, {
824
+ // For `error` events surface the actual error text as the visible
825
+ // title so the run timeline reads "Reconnecting... 2/5" instead of
826
+ // a generic "error". Other methods continue to use the method name.
827
+ const eventTitle = message.method === 'error'
828
+ ? (extractCodexAppServerErrorMessage(message.params) || message.method)
829
+ : message.method;
830
+ emitCodexEvent(input.emit, 'codex.session.event', eventTitle, {
825
831
  method: message.method,
826
832
  params: message.params || {}
827
833
  }, inferNotificationStatus(message));
828
834
  if (message.method === 'turn/completed' && (!activeTurnId || message.params?.turn?.id === activeTurnId || message.params?.turnId === activeTurnId)) {
829
835
  succeed();
830
836
  }
837
+ if (message.method === 'error' && isTransientCodexAppServerError(message.params)) {
838
+ return;
839
+ }
831
840
  if (message.method === 'error') {
832
841
  fail(new Error(message.params?.error?.message || 'Codex turn failed'));
833
842
  }
@@ -1211,12 +1220,35 @@ function emitCodexEvent(emit, type, title, detail = {}, status = 'running') {
1211
1220
  }
1212
1221
 
1213
1222
  function inferNotificationStatus(message) {
1223
+ if (message.method === 'error' && isTransientCodexAppServerError(message.params)) {
1224
+ return 'warning';
1225
+ }
1214
1226
  if (/completed|updated|delta|started/.test(message.method || '')) {
1215
1227
  return /completed/.test(message.method || '') ? 'completed' : 'running';
1216
1228
  }
1217
1229
  return 'running';
1218
1230
  }
1219
1231
 
1232
+ function isTransientCodexAppServerError(params = {}) {
1233
+ const message = extractCodexAppServerErrorMessage(params);
1234
+ return /^Reconnecting(?:\.\.\.|…)?\s+\d+\s*\/\s*\d+\.?$/i.test(message);
1235
+ }
1236
+
1237
+ function extractCodexAppServerErrorMessage(params = {}) {
1238
+ const candidates = [
1239
+ params?.error?.message,
1240
+ params?.message,
1241
+ params?.title,
1242
+ params?.error
1243
+ ];
1244
+ for (const candidate of candidates) {
1245
+ if (typeof candidate === 'string' && candidate.trim()) {
1246
+ return candidate.trim();
1247
+ }
1248
+ }
1249
+ return '';
1250
+ }
1251
+
1220
1252
  function normalizeReasoningEffort(value) {
1221
1253
  return ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(value) ? value : null;
1222
1254
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-overleaf-link",
3
- "version": "1.3.6",
3
+ "version": "1.3.8",
4
4
  "description": "Cross-platform Chrome bridge that connects Codex to the active Overleaf project.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",