codex-overleaf-link 2.5.1 → 2.6.5

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 (111) 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 +148 -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 +46 -10
  9. package/extension/src/content/composerPanel.js +92 -12
  10. package/extension/src/content/contentRuntime.js +1114 -321
  11. package/extension/src/content/contextTray.js +8 -2
  12. package/extension/src/content/contextUsageView.js +267 -0
  13. package/extension/src/content/diagnosticsController.js +77 -34
  14. package/extension/src/content/diagnosticsExport.js +1 -1
  15. package/extension/src/content/diagnosticsPanel.js +147 -42
  16. package/extension/src/content/generated/content.bundle.js +5761 -1237
  17. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  18. package/extension/src/content/legacyGlobalRegistry.js +4 -0
  19. package/extension/src/content/localSkillsPanel.js +6 -4
  20. package/extension/src/content/markdownDomRenderer.js +5 -3
  21. package/extension/src/content/markdownText.js +53 -14
  22. package/extension/src/content/modelPicker.js +29 -19
  23. package/extension/src/content/nativeCompatibilityController.js +52 -19
  24. package/extension/src/content/otWarmMirror.js +1 -1
  25. package/extension/src/content/pageBridgeClient.js +7 -7
  26. package/extension/src/content/panelMaintenance.js +10 -2
  27. package/extension/src/content/panelRenderer.js +55 -28
  28. package/extension/src/content/pendingInputView.js +1 -1
  29. package/extension/src/content/projectSettingsCoordinator.js +30 -6
  30. package/extension/src/content/providerSettingsDialog.js +28 -19
  31. package/extension/src/content/recentProjects.js +30 -19
  32. package/extension/src/content/runActivitySummary.js +122 -14
  33. package/extension/src/content/runCompletionMeta.js +194 -6
  34. package/extension/src/content/runController.js +2 -1
  35. package/extension/src/content/runFailureNotice.js +8 -1
  36. package/extension/src/content/runPresence.js +18 -8
  37. package/extension/src/content/runQueueScheduler.js +27 -13
  38. package/extension/src/content/runRecoveryActions.js +2 -1
  39. package/extension/src/content/runResultActions.js +73 -24
  40. package/extension/src/content/runReviewController.js +102 -37
  41. package/extension/src/content/runTimelineView.js +224 -25
  42. package/extension/src/content/scopedPersistencePanelState.js +124 -46
  43. package/extension/src/content/selectionContextView.js +30 -3
  44. package/extension/src/content/sensitivePreflight.js +14 -1
  45. package/extension/src/content/sessionManager.js +49 -3
  46. package/extension/src/content/sessionMenuView.js +54 -18
  47. package/extension/src/content/sessionPersistence.js +25 -5
  48. package/extension/src/content/settingsPanel.js +15 -2
  49. package/extension/src/content/settingsWorkbench.js +127 -89
  50. package/extension/src/content/sharedSessionViewSync.js +13 -4
  51. package/extension/src/content/subagentActivityView.js +23 -7
  52. package/extension/src/content/updateNotice.js +60 -6
  53. package/extension/src/content/writebackOrchestrator.js +301 -72
  54. package/extension/src/content/writingStyleSettings.js +70 -17
  55. package/extension/src/page/compileBridge.js +193 -180
  56. package/extension/src/page/overleafCapabilities.js +4 -1
  57. package/extension/src/page/overleafEditor.js +12 -3
  58. package/extension/src/page/overleafProjectSnapshot.js +56 -3
  59. package/extension/src/page/snapshotRouter.js +75 -66
  60. package/extension/src/page/textCoordinates.js +17 -1
  61. package/extension/src/page/textFileCreator.js +9 -1
  62. package/extension/src/page/treeOperations.js +200 -19
  63. package/extension/src/page/writebackRouter.js +172 -113
  64. package/extension/src/pageBridge.js +124 -73
  65. package/extension/src/popup.js +1 -1
  66. package/extension/src/shared/agentTranscript.js +372 -18
  67. package/extension/src/shared/auditRecords.js +3 -0
  68. package/extension/src/shared/compatibility.js +1 -1
  69. package/extension/src/shared/compileAdapter.js +58 -17
  70. package/extension/src/shared/composerAttachmentDraft.js +27 -7
  71. package/extension/src/shared/contextUsage.js +125 -0
  72. package/extension/src/shared/failureReasons.js +57 -3
  73. package/extension/src/shared/i18n.js +315 -29
  74. package/extension/src/shared/lineReferences.js +38 -31
  75. package/extension/src/shared/pageRpcContract.js +1 -1
  76. package/extension/src/shared/panelIcons.js +39 -0
  77. package/extension/src/shared/projectFiles.js +2 -1
  78. package/extension/src/shared/reviewing.js +130 -1
  79. package/extension/src/shared/runActivityModel.js +70 -4
  80. package/extension/src/shared/runExecutionSnapshot.js +13 -0
  81. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
  82. package/extension/src/shared/sessionState.js +63 -4
  83. package/extension/src/shared/sessionTitle.js +14 -1
  84. package/extension/src/shared/sharedSessionBridge.js +81 -13
  85. package/extension/src/shared/staleGuard.js +130 -63
  86. package/extension/src/shared/storageDb.js +35 -3
  87. package/extension/src/shared/storageMigration.js +111 -2
  88. package/extension/src/shared/storageRunActions.js +62 -8
  89. package/extension/src/shared/undoOperations.js +126 -28
  90. package/extension/src/shared/updateRuntimeIdentity.js +16 -2
  91. package/extension/src/shared/writebackSettlement.js +21 -3
  92. package/extension/styles/panel.css +1293 -549
  93. package/native-host/src/bootstrapUpdate.js +52 -16
  94. package/native-host/src/chatBridgeRequest.js +60 -6
  95. package/native-host/src/chatBridgeResponse.js +10 -1
  96. package/native-host/src/chatCompletionsBridge.js +29 -2
  97. package/native-host/src/codexPromptAssembly.js +86 -118
  98. package/native-host/src/codexSessionRunner.js +127 -17
  99. package/native-host/src/debugLog.js +27 -3
  100. package/native-host/src/index.js +42 -11
  101. package/native-host/src/localSkills.js +1 -1
  102. package/native-host/src/mirrorWorkspace.js +47 -3
  103. package/native-host/src/nativeEnvironment.js +27 -5
  104. package/native-host/src/providerContextError.js +20 -0
  105. package/native-host/src/providerRuntime.js +1 -0
  106. package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
  107. package/native-host/src/taskRunnerRuntime.js +28 -8
  108. package/native-host/src/textPatch.js +473 -377
  109. package/native-host/src/updateManager.js +144 -21
  110. package/native-host/src/writingStyleRuntime.js +1 -1
  111. package/package.json +5 -8
@@ -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) {
@@ -4,21 +4,32 @@
4
4
  })(typeof globalThis !== 'undefined' ? globalThis : this, function () {
5
5
  'use strict';
6
6
 
7
- const CATEGORIES = [
8
- ['general', 'settingsWorkbenchGeneral', 'appearance'],
9
- ['providers', 'settingsWorkbenchModels', 'bolt'],
10
- ['context', 'settingsWorkbenchContext', 'database'],
11
- ['instructions', 'settingsWorkbenchInstructions', 'pen'],
12
- ['protection', 'settingsWorkbenchProtection', 'shield'],
13
- ['history', 'settingsWorkbenchHistory', 'history'],
14
- ['updates', 'softwareUpdatesTitle', 'software']
7
+ // The sidebar is split by scope; a page's section decides whether its groups
8
+ // need an open project. Pages: [id, label key, icon].
9
+ const SECTIONS = [
10
+ ['project', 'settingsScopeProjectTitle', [
11
+ ['writing', 'settingsWorkbenchWriting', 'pen'],
12
+ ['protection', 'settingsWorkbenchProtection', 'shield'],
13
+ ['cache', 'otSettingsTitle', 'flask'],
14
+ ['history', 'settingsWorkbenchChanges', 'history']
15
+ ]],
16
+ ['global', 'settingsScopeGlobalTitle', [
17
+ ['appearance', 'appearanceTitle', 'appearance'],
18
+ ['providers', 'settingsWorkbenchModels', 'bolt'],
19
+ ['context', 'settingsWorkbenchContextSkills', 'database'],
20
+ ['storage', 'storageTitle', 'archive'],
21
+ ['updates', 'softwareUpdatesTitle', 'software']
22
+ ]]
15
23
  ];
16
24
  const GROUP_PAGES = {
17
- appearance: 'general', 'writing-style': 'general', providers: 'providers', 'context-loading': 'context',
18
- experimental: 'context', personalization: 'instructions', protection: 'protection',
19
- privacy: 'protection', history: 'history', storage: 'history', updates: 'updates'
25
+ 'writing-style': 'writing', personalization: 'writing', protection: 'protection', privacy: 'protection',
26
+ experimental: 'cache', history: 'history', appearance: 'appearance', providers: 'providers',
27
+ 'context-loading': 'context', skills: 'context', storage: 'storage', updates: 'updates'
20
28
  };
21
- const PROJECT_GROUPS = new Set(['writing-style', 'personalization', 'protection', 'privacy', 'experimental', 'history']);
29
+ // Groups without a mapping keep their original scope.
30
+ const FALLBACK_PAGES = { project: 'writing', global: 'context' };
31
+ // The skills list opens in place of the page that holds the Skills group.
32
+ const SKILLS_PAGE = 'context';
22
33
 
23
34
  function mount(instance, options = {}) {
24
35
  const container = instance.container, doc = container.ownerDocument;
@@ -50,93 +61,94 @@
50
61
  const status = settings.querySelector('[data-project-settings-status]');
51
62
  const dialog = make('dialog', 'codex-settings-workbench');
52
63
  dialog.setAttribute('aria-labelledby', 'codex-settings-workbench-title');
53
- const header = make('header', 'codex-wb-header');
54
- const heading = make('div', 'codex-wb-heading');
55
- const title = make('h2', '', 'settingsWorkbenchTitle');
64
+ const title = make('h2', 'codex-wb-title', 'settingsWorkbenchTitle');
56
65
  title.id = 'codex-settings-workbench-title';
57
- heading.append(title, make('p', '', 'settingsWorkbenchSubtitle'));
58
66
  closeButton.classList.add('codex-wb-close');
59
- closeButton.textContent = '\u00d7';
60
- header.append(heading, closeButton);
67
+ if (options.icon) closeButton.innerHTML = options.icon('close'); // Static, repository-owned icon path.
68
+ else closeButton.textContent = '\u00d7';
61
69
  originalHead?.remove();
62
70
 
63
71
  const layout = make('div', 'codex-wb-layout');
64
72
  const sidebar = make('aside', 'codex-wb-sidebar');
65
- const filters = make('div', 'codex-wb-filters');
66
- filters.setAttribute('role', 'group');
67
- const allFilter = button('', 'settingsWorkbenchAll');
68
- const projectFilter = button('', 'settingsScopeProjectTitle');
69
- filters.append(allFilter, projectFilter);
70
73
  const nav = make('nav', 'codex-wb-nav');
71
74
  nav.setAttribute('role', 'tablist');
72
75
  nav.setAttribute('aria-orientation', 'vertical');
73
- const brand = make('div', 'codex-wb-brand');
74
- brand.textContent = 'Codex Overleaf Link';
75
- sidebar.append(filters, nav, brand);
76
+ sidebar.append(title, nav);
76
77
  const main = make('div', 'codex-wb-main');
77
78
  const body = make('div', 'codex-wb-body');
78
79
  const footer = make('footer', 'codex-wb-footer');
79
80
  if (status) { status.setAttribute('aria-live', 'polite'); footer.append(status); }
80
81
  main.append(body, footer);
81
82
  layout.append(sidebar, main);
82
- dialog.append(header, layout);
83
- const pages = new Map(), navButtons = new Map(), groups = [];
84
- let active = 'general', filter = 'all', returnFocus = null, destroyed = false;
83
+ // The close button floats over the content's top-right corner.
84
+ dialog.append(layout, closeButton);
85
+ const pages = new Map(), navButtons = new Map(), pageScopes = new Map(), sectionLabels = new Map(), groups = [];
86
+ let active = 'writing', returnFocus = null, destroyed = false;
85
87
  let providerRequested = false, providerCloseGuard = null, closing = null;
86
88
 
87
- for (const [id, key, icon] of CATEGORIES) {
88
- const page = make('section', 'codex-wb-page');
89
- page.id = 'codex-settings-page-' + id;
90
- page.dataset.workbenchPage = id;
91
- page.setAttribute('role', 'tabpanel');
92
- page.setAttribute('aria-labelledby', 'codex-settings-nav-' + id);
93
- page.hidden = true;
94
- page.append(make('h3', 'codex-wb-page-title', key));
95
- pages.set(id, page);
96
- const tab = button('codex-wb-nav-button');
97
- tab.id = 'codex-settings-nav-' + id;
98
- tab.setAttribute('role', 'tab');
99
- tab.setAttribute('aria-controls', page.id);
100
- if (options.icon) {
101
- const glyph = make('span', 'codex-wb-nav-icon');
102
- glyph.innerHTML = options.icon(icon); // Static, repository-owned icon paths only.
103
- tab.append(glyph);
89
+ for (const [scope, sectionKey, items] of SECTIONS) {
90
+ // Visual section heading; each tab references it as its description.
91
+ const label = make('div', 'codex-wb-nav-section', sectionKey);
92
+ label.id = 'codex-settings-nav-section-' + scope;
93
+ label.setAttribute('aria-hidden', 'true');
94
+ nav.append(label);
95
+ sectionLabels.set(scope, label);
96
+ for (const [id, key, icon] of items) {
97
+ const page = make('section', 'codex-wb-page');
98
+ page.id = 'codex-settings-page-' + id;
99
+ page.dataset.workbenchPage = id;
100
+ page.setAttribute('role', 'tabpanel');
101
+ page.setAttribute('aria-labelledby', 'codex-settings-nav-' + id);
102
+ page.hidden = true;
103
+ page.append(make('h3', 'codex-wb-page-title', key), make('p', 'codex-wb-page-scope'));
104
+ pages.set(id, page);
105
+ pageScopes.set(id, scope);
106
+ const tab = button('codex-wb-nav-button');
107
+ tab.id = 'codex-settings-nav-' + id;
108
+ tab.setAttribute('role', 'tab');
109
+ tab.setAttribute('aria-controls', page.id);
110
+ tab.setAttribute('aria-describedby', label.id);
111
+ if (options.icon) {
112
+ const glyph = make('span', 'codex-wb-nav-icon');
113
+ glyph.innerHTML = options.icon(icon); // Static, repository-owned icon paths only.
114
+ tab.append(glyph);
115
+ }
116
+ tab.append(make('span', '', key));
117
+ listen(tab, 'click', () => select(id));
118
+ nav.append(tab);
119
+ navButtons.set(id, tab);
104
120
  }
105
- tab.append(make('span', '', key));
106
- listen(tab, 'click', () => select(id));
107
- nav.append(tab);
108
- navButtons.set(id, tab);
109
121
  }
110
- function addGroup(node, category, projectOnly) {
122
+ function addGroup(node, category) {
123
+ const projectOnly = pageScopes.get(category) === 'project';
111
124
  node.hidden = true;
112
125
  node.dataset.workbenchScope = projectOnly ? 'project' : 'global';
113
126
  const summary = node.querySelector('summary');
114
- const badge = make('span', 'codex-wb-scope-label',
115
- projectOnly ? 'settingsScopeProjectTitle' : 'settingsScopeGlobalTitle');
116
- (summary || node).append(badge);
127
+ if (summary) {
128
+ // Groups are plain sections: always open while their page is shown.
129
+ summary.tabIndex = -1;
130
+ listen(summary, 'click', event => event.preventDefault());
131
+ }
117
132
  pages.get(category).append(node);
118
- groups.push({ node, category, projectOnly, expanded: true });
133
+ groups.push({ node, category, projectOnly });
119
134
  }
135
+ const fallbackPage = scope => FALLBACK_PAGES[
136
+ scope && !scope.classList.contains('codex-project-settings-scope--global') ? 'project' : 'global'];
120
137
  // Move existing controls rather than copying them. Their listeners, unsaved
121
138
  // values, identity, validation and persistence callbacks remain attached.
122
139
  for (const group of Array.from(settings.querySelectorAll('[data-set-group]'))) {
123
140
  const key = group.dataset.setGroup;
124
- const originalScope = group.closest('.codex-project-settings-scope');
125
- const projectOnly = PROJECT_GROUPS.has(key)
126
- || (!GROUP_PAGES[key] && originalScope && !originalScope.classList.contains('codex-project-settings-scope--global'));
127
- addGroup(group, GROUP_PAGES[key] || 'instructions', Boolean(projectOnly));
141
+ addGroup(group, GROUP_PAGES[key] || fallbackPage(group.closest('.codex-project-settings-scope')));
128
142
  }
129
- // Preserve non-card controls, including the legacy skill-loading entry.
130
- // Scope headings are replaced by labels on the actual groups.
143
+ // Preserve non-card controls. The sidebar sections replace the scope headings.
131
144
  for (const scope of Array.from(settings.querySelectorAll('.codex-project-settings-scope'))) {
132
145
  scope.querySelectorAll('.codex-project-settings-scope-title').forEach(node => node.remove());
133
- if (scope.children.length) {
134
- addGroup(scope, 'instructions', !scope.classList.contains('codex-project-settings-scope--global'));
135
- } else scope.remove();
146
+ if (scope.children.length) addGroup(scope, fallbackPage(scope));
147
+ else scope.remove();
136
148
  }
137
149
  for (const node of Array.from(settings.children)) {
138
150
  if (node === status) continue;
139
- addGroup(node, 'instructions', false);
151
+ addGroup(node, FALLBACK_PAGES.global);
140
152
  }
141
153
  const providerHost = make('div', 'codex-wb-provider-host');
142
154
  providerHost.dataset.providerSettingsHost = '';
@@ -218,29 +230,54 @@
218
230
  && /^\/project\/[^/]+/.test(win?.location?.pathname || '');
219
231
  }
220
232
  function available(item) {
221
- return (!item.projectOnly || hasProject()) && (filter !== 'project' || item.projectOnly);
233
+ return !item.projectOnly || hasProject();
234
+ }
235
+ // A page whose only visible group repeats the page title hides that group's
236
+ // title; the group header keeps only its Saved mark.
237
+ function refreshSoleSections() {
238
+ for (const page of pages.values()) {
239
+ const pageGroups = Array.from(page.querySelectorAll('.codex-set-group'));
240
+ const visibleGroups = pageGroups.filter(node => !node.hidden);
241
+ const pageTitle = page.querySelector('.codex-wb-page-title')?.textContent.trim();
242
+ for (const node of pageGroups) {
243
+ const groupTitle = node.querySelector('.codex-set-group-head .codex-set-group-title');
244
+ const sole = visibleGroups.length === 1 && visibleGroups[0] === node
245
+ && groupTitle && groupTitle.textContent.trim() === pageTitle;
246
+ if (sole) node.dataset.soleSection = 'true';
247
+ else delete node.dataset.soleSection;
248
+ }
249
+ }
250
+ }
251
+ // Each page states its scope once, under its title.
252
+ function renderPageScope(page, scope, projectName) {
253
+ const line = page.querySelector('.codex-wb-page-scope');
254
+ if (!line) return;
255
+ const text = t(scope === 'project' ? 'settingsPageScopeProject' : 'settingsPageScopeGlobal');
256
+ if (scope !== 'project' || !projectName) { line.textContent = text; return; }
257
+ const name = doc.createElement('b');
258
+ name.textContent = projectName;
259
+ line.replaceChildren(text + ' \u00b7 ', name);
222
260
  }
223
261
  function refresh() {
224
262
  if (destroyed) return;
225
- if (!hasProject()) filter = 'all';
226
- if (panel.dataset.view === 'skills') { active = 'instructions'; filter = 'all'; }
263
+ if (panel.dataset.view === 'skills') active = SKILLS_PAGE;
227
264
  const availablePages = new Set(groups.filter(available).map(item => item.category));
228
- if (!availablePages.has(active)) active = CATEGORIES.find(([id]) => availablePages.has(id))?.[0] || 'general';
265
+ if (!availablePages.has(active)) active = Array.from(navButtons.keys()).find(id => availablePages.has(id)) || 'appearance';
229
266
  for (const node of dialog.querySelectorAll('[data-i18n]')) {
230
267
  // Existing controls use their established translation pass; update the
231
268
  // newly owned chrome only, without replacing controls or skill rows.
232
- if (node.closest('.codex-wb-header, .codex-wb-nav, .codex-wb-filters, .codex-wb-footer')
269
+ if (node.closest('.codex-wb-nav, .codex-wb-footer')
270
+ || node.classList.contains('codex-wb-title')
233
271
  || node.classList.contains('codex-wb-page-title')
234
- || node.classList.contains('codex-wb-scope-label')
235
272
  || node.classList.contains('codex-wb-theme-caption')
236
273
  || node.classList.contains('codex-wb-skills-caption')) node.textContent = t(node.dataset.i18n);
237
274
  }
238
- projectFilter.disabled = !hasProject();
239
- projectFilter.title = hasProject() ? t('settingsWorkbenchProjectFilter') : t('settingsWorkbenchOpenProject');
240
- filters.setAttribute('aria-label', t('settingsWorkbenchFilter'));
275
+ const projectName = hasProject() ? String(instance.getProjectName?.() || '').trim() : '';
276
+ for (const [id, page] of pages) renderPageScope(page, pageScopes.get(id), projectName);
277
+ for (const [scope, label] of sectionLabels) {
278
+ label.hidden = !Array.from(pageScopes).some(([id, pageScope]) => pageScope === scope && availablePages.has(id));
279
+ }
241
280
  nav.setAttribute('aria-label', t('settingsWorkbenchCategories'));
242
- allFilter.setAttribute('aria-pressed', String(filter === 'all'));
243
- projectFilter.setAttribute('aria-pressed', String(filter === 'project'));
244
281
  closeButton.title = t('settingsWorkbenchClose');
245
282
  closeButton.setAttribute('aria-label', t('settingsWorkbenchClose'));
246
283
  const skillsBack = skills?.querySelector('[data-skills-back]');
@@ -248,6 +285,7 @@
248
285
  skillsBack.title = t('settingsBack');
249
286
  skillsBack.setAttribute('aria-label', t('settingsBack'));
250
287
  }
288
+ if (status) status.hidden = Boolean(status.dataset.statusPage) && status.dataset.statusPage !== active;
251
289
  for (const [id, tab] of navButtons) {
252
290
  tab.hidden = !availablePages.has(id);
253
291
  tab.setAttribute('aria-selected', String(id === active));
@@ -256,12 +294,11 @@
256
294
  }
257
295
  for (const item of groups) {
258
296
  const visible = available(item) && item.category === active && panel.dataset.view !== 'skills';
259
- if (item.node.tagName === 'DETAILS' && item.node.hidden !== !visible) {
260
- if (!visible) item.expanded = item.node.open;
261
- item.node.open = visible && item.expanded;
262
- }
297
+ // Opening a group when its page is shown keeps the lazy toggle loaders (history, storage).
298
+ if (item.node.tagName === 'DETAILS' && item.node.open !== visible) item.node.open = visible;
263
299
  item.node.hidden = !visible;
264
300
  }
301
+ refreshSoleSections();
265
302
  updateThemes();
266
303
  }
267
304
  function requestProviders() {
@@ -277,9 +314,17 @@
277
314
  try { Promise.resolve(instance.callbacks.onProvidersOpen()).catch(failed); }
278
315
  catch (error) { failed(error); }
279
316
  }
317
+ // Shows the page that holds a group and returns the group, or null when the
318
+ // group is unavailable (for example a project group outside a project).
319
+ function revealGroup(key) {
320
+ const item = groups.find(entry => entry.node.dataset?.setGroup === key);
321
+ if (!item || !available(item)) return null;
322
+ active = item.category;
323
+ refresh();
324
+ return item.node;
325
+ }
280
326
  function revealProviders() {
281
327
  active = 'providers';
282
- filter = 'all';
283
328
  providerRequested = true;
284
329
  pages.get('providers').dataset.providerEmbedded = 'true';
285
330
  panel.dataset.view = 'settings';
@@ -330,13 +375,6 @@
330
375
  if (['settings', 'skills'].includes(panel.dataset.view)) show();
331
376
  else hide();
332
377
  };
333
- listen(allFilter, 'click', () => { filter = 'all'; refresh(); });
334
- listen(projectFilter, 'click', () => {
335
- filter = 'project';
336
- if (panel.dataset.view === 'skills') instance.callbacks.onSkillsBack?.();
337
- refresh();
338
- body.scrollTop = 0;
339
- });
340
378
  listen(nav, 'keydown', event => {
341
379
  if (!['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) return;
342
380
  const tabs = Array.from(navButtons.values()).filter(tab => !tab.hidden);
@@ -363,7 +401,7 @@
363
401
  observer.observe(panel, { attributes: true, attributeFilter: ['data-view', 'data-settings-scope', 'data-theme'] });
364
402
  refresh();
365
403
  return {
366
- show, hide, refresh, revealProviders,
404
+ show, hide, refresh, revealGroup, revealProviders,
367
405
  setProviderCloseGuard(guard) { providerCloseGuard = guard; },
368
406
  destroy() {
369
407
  destroyed = true;
@@ -9,7 +9,11 @@
9
9
 
10
10
  function applySnapshot(options, sessions) {
11
11
  const previous = options.getState(), scope = options.getScope();
12
- const next = options.normalize({ ...previous, sessions }, { restoreRunningRuns: false });
12
+ const input = sessions.length ? { ...previous, sessions } : {
13
+ ...previous, session: null, sessions: [], runs: [], activeSessionId: '',
14
+ task: '', focusFiles: [], selectionContext: null, composerAttachments: []
15
+ };
16
+ const next = options.normalize(input, { restoreRunningRuns: false });
13
17
  const unchangedSnapshot = JSON.stringify(next);
14
18
  options.setState(next);
15
19
  try {
@@ -49,9 +53,10 @@
49
53
  if (!scope.projectId || !scope.accountScopeId) { pending = false; return; }
50
54
  if (saving || isBusy()) { schedule(1000); return; }
51
55
  const local = getState();
52
- // A draft or a not-yet-persisted edit always takes priority over passive
53
- // history refresh. It remains on screen until the user saves/sends it.
54
- if (local.task || (local.sessions || []).some(session => StorageDb.hasUnpersistedSharedSession(session))) {
56
+ // A locally edited draft or a not-yet-persisted edit takes priority over
57
+ // passive history refresh. Callers may distinguish imported shared drafts.
58
+ if ((options.hasLocalDraft ? options.hasLocalDraft(local) : Boolean(local.task))
59
+ || (local.sessions || []).some(session => StorageDb.hasUnpersistedSharedSession(session))) {
55
60
  schedule(1500); return;
56
61
  }
57
62
  const capturedVersion = version, capturedSignature = signature(local);
@@ -87,6 +92,10 @@
87
92
  }
88
93
  const unsubscribe = Bridge.subscribe(change => {
89
94
  const scope = getScope();
95
+ if (options.isDashboard?.()) {
96
+ if (scope.accountScopeId && change.accountScopeId === scope.accountScopeId) options.refreshDashboard?.();
97
+ return;
98
+ }
90
99
  if (!change.projectId || change.projectId === scope.projectId) {
91
100
  retryCount = 0; pending = true; schedule();
92
101
  }
@@ -3,18 +3,31 @@
3
3
  })(typeof globalThis !== 'undefined' ? globalThis : window, function subagentActivityViewFactory(Agents, Activity) {
4
4
  'use strict';
5
5
  function create(deps) {
6
- const { tx, getPanel, plain, make, icon, formatElapsed, renderBlocks } = deps;
6
+ const { tx, getPanel, plain, plainThought, make, icon, formatElapsed, renderBlocks, glyph = () => '' } = deps;
7
+ const withGlyph = (node, name, label) => {
8
+ node.insertAdjacentHTML('beforeend', glyph(name));
9
+ const text = make('span', ''); text.textContent = label; node.append(text);
10
+ return node;
11
+ };
7
12
  let viewer = null;
8
13
  function statusLabel(status) {
9
14
  return ({ pending: tx('Starting', '启动中'), running: tx('Running', '运行中'),
10
15
  completed: tx('Completed', '已完成'), failed: tx('Failed', '失败'),
11
16
  cancelled: tx('Stopped', '已停止'), unknown: tx('Status unconfirmed', '状态未确认') })[status] || '';
12
17
  }
18
+ // A row preview is one plain line: a final message keeps its words, not its
19
+ // Markdown ("- `main.tex` is …" reads "main.tex is …").
20
+ function plainMessage(value) {
21
+ return value.replace(/^\s*(?:#{1,6}\s+|>\s?|[-*+]\s+|\d+[.)]\s+)/gm, '')
22
+ .replace(/!?\[([^\]]*)\]\([^)\s]*\)/g, '$1').replace(/`([^`\n]+)`/g, '$1')
23
+ .replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, '$2').replace(/\s+/g, ' ').trim();
24
+ }
13
25
  function latest(agent) {
14
26
  const blocks = Activity.project({ status: agent.status, events: agent.events || [] }).blocks;
15
27
  const last = blocks.at(-1);
16
28
  const item = last?.kind === 'exploreGroup' ? last.items.at(-1) : last;
17
- return plain(item?.meta?.target || item?.meta?.command || item?.event?.title || agent.task).slice(0, 180);
29
+ const text = plain(item?.meta?.target || item?.meta?.command || item?.event?.title || agent.task);
30
+ return (item?.kind === 'message' ? plainMessage(text) : text).slice(0, 180);
18
31
  }
19
32
  function elapsed(agent, run) {
20
33
  const duration = Agents.elapsedMs(agent, run, Date.now());
@@ -42,9 +55,9 @@
42
55
  shell.setAttribute('aria-label', tx('Subagent conversation', '子代理对话'));
43
56
  const header = make('header', 'codex-subagent-view-head');
44
57
  const back = make('button', 'codex-subagent-back');
45
- back.type = 'button'; back.textContent = tx('Back to main task', '返回主任务');
58
+ back.type = 'button'; withGlyph(back, 'chevronLeft', tx('Back to main task', '返回主任务'));
46
59
  back.addEventListener('click', close);
47
- const readonly = make('span', 'codex-subagent-readonly'); readonly.textContent = tx('Read only', '只读');
60
+ const readonly = withGlyph(make('span', 'codex-subagent-readonly'), 'lock', tx('Read only', '只读'));
48
61
  header.append(back, readonly);
49
62
  const parent = make('div', 'codex-subagent-parent');
50
63
  const parentLabel = make('span', 'codex-subagent-parent-label');
@@ -112,7 +125,7 @@
112
125
  const mark = icon('agent'), copy = make('span', 'run-subagent-copy');
113
126
  const name = make('span', 'run-subagent-name'), activity = make('span', 'run-subagent-activity');
114
127
  copy.append(name, activity);
115
- const tail = make('span', 'run-subagent-tail'), status = make('span', ''), time = make('span', 'run-subagent-time');
128
+ const tail = make('span', 'run-subagent-tail'), status = make('span', 'run-subagent-status'), time = make('span', 'run-subagent-time');
116
129
  tail.append(status, time);
117
130
  el.append(mark, copy, tail);
118
131
  el.addEventListener('click', () => open(card, agent.key, el));
@@ -154,15 +167,17 @@
154
167
  const childStatus = Agents.effectiveStatus(agent, card.run);
155
168
  viewer.title.textContent = plain(agent.title || agent.jobId || agent.threadId);
156
169
  viewer.meta.textContent = [statusLabel(childStatus), elapsed(agent, card.run), agent.model, agent.reasoningEffort].filter(Boolean).join(' · ');
170
+ viewer.meta.dataset.state = childStatus;
157
171
  viewer.prompt.textContent = plain(agent.task);
158
172
  viewer.assignment.hidden = !viewer.prompt.textContent;
159
173
  const parent = Activity.project(card.run), last = parent.blocks.at(-1);
160
174
  const lastItem = last?.kind === 'exploreGroup' ? last.items.at(-1) : last;
161
- const parentWork = plain(lastItem?.meta?.target || lastItem?.meta?.command || lastItem?.event?.title).slice(0, 150);
175
+ const parentWork = plainThought(plain(lastItem?.meta?.target || lastItem?.meta?.command || lastItem?.event?.title)).slice(0, 150);
162
176
  viewer.parentLabel.textContent = tx('Main agent · ', '主 agent · ') + (card.run.status === 'running'
163
177
  ? parentWork || tx('Working', '处理中') : tx('Run ended', '本轮已结束'));
164
178
  const running = (card.run.subagents || []).filter(item => ['pending', 'running'].includes(Agents.effectiveStatus(item, card.run))).length;
165
179
  viewer.parentCount.textContent = tx(running + ' subagents active', running + ' 个子任务进行中');
180
+ viewer.parentCount.hidden = running === 0;
166
181
  for (const sibling of card.run.subagents || []) {
167
182
  let button = viewer.siblingRows.get(sibling.key);
168
183
  if (!button) {
@@ -172,7 +187,8 @@
172
187
  }
173
188
  button.textContent = plain(sibling.title || sibling.jobId || sibling.threadId);
174
189
  button.setAttribute('aria-pressed', String(sibling.key === agent.key));
175
- button.title = statusLabel(Agents.effectiveStatus(sibling, card.run));
190
+ button.dataset.state = Agents.effectiveStatus(sibling, card.run);
191
+ button.title = statusLabel(button.dataset.state);
176
192
  }
177
193
  const childRun = { id: card.id + ':' + agent.key, status: ['pending', 'running'].includes(childStatus) ? 'running' : childStatus,
178
194
  events: agent.events || [], mode: 'ask' };