codex-overleaf-link 2.6.0 → 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 (58) hide show
  1. package/README.md +9 -9
  2. package/README.zh-CN.md +9 -9
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +70 -0
  5. package/extension/src/content/composerAttachments.js +35 -8
  6. package/extension/src/content/contentRuntime.js +200 -51
  7. package/extension/src/content/diagnosticsController.js +77 -34
  8. package/extension/src/content/diagnosticsExport.js +1 -1
  9. package/extension/src/content/diagnosticsPanel.js +129 -42
  10. package/extension/src/content/generated/content.bundle.js +1283 -331
  11. package/extension/src/content/generated/content.bundle.meta.json +3 -3
  12. package/extension/src/content/localSkillsPanel.js +4 -2
  13. package/extension/src/content/markdownText.js +11 -9
  14. package/extension/src/content/modelPicker.js +28 -19
  15. package/extension/src/content/nativeCompatibilityController.js +23 -9
  16. package/extension/src/content/pageBridgeClient.js +5 -5
  17. package/extension/src/content/panelMaintenance.js +4 -0
  18. package/extension/src/content/panelRenderer.js +4 -1
  19. package/extension/src/content/providerSettingsDialog.js +26 -18
  20. package/extension/src/content/recentProjects.js +26 -16
  21. package/extension/src/content/runQueueScheduler.js +19 -9
  22. package/extension/src/content/runResultActions.js +51 -4
  23. package/extension/src/content/runReviewController.js +9 -3
  24. package/extension/src/content/scopedPersistencePanelState.js +34 -25
  25. package/extension/src/content/selectionContextView.js +30 -3
  26. package/extension/src/content/sharedSessionViewSync.js +9 -1
  27. package/extension/src/content/writebackOrchestrator.js +266 -50
  28. package/extension/src/content/writingStyleSettings.js +70 -17
  29. package/extension/src/page/overleafEditor.js +12 -3
  30. package/extension/src/page/overleafProjectSnapshot.js +56 -3
  31. package/extension/src/page/snapshotRouter.js +75 -66
  32. package/extension/src/page/treeOperations.js +200 -19
  33. package/extension/src/page/writebackRouter.js +152 -40
  34. package/extension/src/pageBridge.js +17 -7
  35. package/extension/src/shared/agentTranscript.js +23 -2
  36. package/extension/src/shared/auditRecords.js +1 -0
  37. package/extension/src/shared/compatibility.js +1 -1
  38. package/extension/src/shared/composerAttachmentDraft.js +22 -7
  39. package/extension/src/shared/failureReasons.js +15 -0
  40. package/extension/src/shared/i18n.js +16 -0
  41. package/extension/src/shared/pageRpcContract.js +1 -1
  42. package/extension/src/shared/projectFiles.js +2 -1
  43. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +34 -0
  44. package/extension/src/shared/staleGuard.js +58 -0
  45. package/extension/src/shared/storageMigration.js +104 -1
  46. package/extension/styles/panel.css +191 -63
  47. package/native-host/src/codexSessionRunner.js +1 -0
  48. package/native-host/src/debugLog.js +27 -3
  49. package/native-host/src/index.js +42 -11
  50. package/native-host/src/localSkills.js +1 -1
  51. package/native-host/src/mirrorWorkspace.js +47 -3
  52. package/native-host/src/nativeEnvironment.js +27 -5
  53. package/native-host/src/providerRuntime.js +1 -0
  54. package/native-host/src/skills/reviewer-response/SKILL.md +65 -0
  55. package/native-host/src/taskRunnerRuntime.js +11 -3
  56. package/native-host/src/updateManager.js +24 -6
  57. package/native-host/src/writingStyleRuntime.js +1 -1
  58. package/package.json +1 -1
@@ -103,7 +103,12 @@
103
103
  try {
104
104
  result = await spec.run({ collectOnly: true });
105
105
  } catch (error) {
106
- result = projectableResult(() => ({ status: 'failed', summary: tr('diagnosticsCheckErrored'), technical: error?.message || String(error) }));
106
+ result = projectableResult(() => ({
107
+ status: 'failed',
108
+ summary: tr('diagnosticsCheckErrored'),
109
+ nextStep: tr('runOutcomeNext'),
110
+ technical: error?.message || String(error)
111
+ }));
107
112
  }
108
113
  const bucket = diagnosticsHealthBucket(result?.status);
109
114
  if (bucket === 'fail') {
@@ -118,7 +123,9 @@
118
123
  status: result?.status || 'info',
119
124
  title: tr(spec.label),
120
125
  summary: translated?.summary || translated?.subtitle || '',
121
- nextStep: bucket === 'ok' ? '' : (translated?.nextStep || '')
126
+ nextStep: bucket === 'ok' ? '' : (translated?.nextStep || ''),
127
+ installCommand: translated?.installCommand,
128
+ technical: translated?.technical
122
129
  };
123
130
  });
124
131
  }
@@ -211,13 +218,15 @@
211
218
  const codexOk = environment.codex?.ok === true;
212
219
  const latexTools = environment.latex?.available || [];
213
220
  const latexOk = latexTools.length > 0;
214
- const bullets = [
221
+ const facts = [
215
222
  codexOk
216
- ? tx(`Codex available: ${environment.codex.path || 'found on PATH'}`, `Codex 可用:${environment.codex.path || '已在 PATH 中找到'}`)
217
- : tx('Codex was not found: the extension cannot start local Codex.', 'Codex 没有找到:插件不能启动本地 Codex。'),
223
+ ? (environment.codex.path
224
+ ? { label: 'Codex', value: environment.codex.path, kind: 'path' }
225
+ : { label: 'Codex', value: tx('Found on PATH', '已在 PATH 中找到') })
226
+ : { label: 'Codex', value: tx('Not found', '未找到'), tone: 'fail' },
218
227
  latexOk
219
- ? tx(`LaTeX tools available: ${latexTools.join(', ')}`, `LaTeX 工具可用:${latexTools.join(', ')}`)
220
- : tx('LaTeX tools were not found: text editing works, but local compile checks are limited.', 'LaTeX 工具没有找到:可以编辑文本,但不能在本地编译验证。')
228
+ ? { label: 'LaTeX', value: latexTools.join(', ') }
229
+ : { label: 'LaTeX', value: tx('Not found', '未找到'), tone: 'warn' }
221
230
  ];
222
231
  if (codexOk && latexOk) {
223
232
  return {
@@ -225,7 +234,7 @@
225
234
  subtitle: tx('Codex and LaTeX are available.', 'Codex 和 LaTeX 都可以使用。'),
226
235
  status: 'completed',
227
236
  summary: tx('Codex is connected and local LaTeX tools are available. Codex can read, edit, and locally check this Overleaf project.', 'Codex 已连接,本机 LaTeX 工具可用。你可以让 Codex 读取、修改并本地检查 Overleaf 项目。'),
228
- bullets,
237
+ facts,
229
238
  technical: JSON.stringify(environment, null, 2)
230
239
  };
231
240
  }
@@ -236,7 +245,7 @@
236
245
  summary: codexOk
237
246
  ? tx('Local Codex can run, but LaTeX compile tools were not found. File edits are unaffected, but compile verification is limited.', '本机 Codex 可以运行,但没有找到 LaTeX 编译工具;修改文件不受影响,编译验证会受限。')
238
247
  : tx('The extension reached the native host, but local Codex was not found, so tasks cannot start.', '插件已经连上 native host,但没有找到本机 Codex,所以不能启动任务。'),
239
- bullets,
248
+ facts,
240
249
  nextStep: codexOk
241
250
  ? tx('For compile verification, make sure latexmk or pdflatex is available on your Terminal PATH.', '如需编译验证,请确认 latexmk 或 pdflatex 在终端 PATH 中可用。')
242
251
  : tx('Make sure codex works in Terminal, then reload the extension.', '请确认终端里可以运行 codex,然后重新加载扩展。'),
@@ -281,7 +290,7 @@
281
290
  subtitle: messages.subtitle,
282
291
  status: 'failed',
283
292
  summary: messages.summary,
284
- bullets: formatNativeCompatibilityBullets(compatibility),
293
+ facts: formatNativeCompatibilityFacts(compatibility, statusValue),
285
294
  nextStep: messages.nextStep,
286
295
  installCommand: compatibility.installCommand,
287
296
  technical: formatNativeCompatibilityTechnicalDetails(compatibility, response)
@@ -320,14 +329,24 @@
320
329
  }
321
330
  }
322
331
 
323
- function formatNativeCompatibilityBullets(compatibility = {}) {
332
+ function formatNativeCompatibilityFacts(compatibility = {}, statusValue = '') {
324
333
  const native = compatibility.native || {};
325
334
  return [
326
- tx(`Status: ${compatibility.status || 'unknown'}`, `状态:${compatibility.status || 'unknown'}`),
327
- tx(`Classification: ${getNativeCompatibilityClassification(compatibility)}`, `兼容分类:${getNativeCompatibilityClassification(compatibility)}`),
328
- tx(`Extension version: ${compatibility.extensionVersion || 'unknown'}`, `扩展版本:${compatibility.extensionVersion || 'unknown'}`),
329
- tx(`Native version: ${native.version || 'unknown'}`, `Native 版本:${native.version || 'unknown'}`),
330
- tx(`Native protocol: ${formatNativeProtocolVersion(native)}`, `Native 协议:${formatNativeProtocolVersion(native)}`)
335
+ {
336
+ label: tx('Extension version', '扩展版本'),
337
+ value: compatibility.extensionVersion || 'unknown',
338
+ tone: statusValue === 'extension_too_old' ? 'fail' : undefined
339
+ },
340
+ {
341
+ label: tx('Native version', 'Native 版本'),
342
+ value: native.version || 'unknown',
343
+ tone: statusValue === 'native_too_old' ? 'fail' : undefined
344
+ },
345
+ {
346
+ label: tx('Native protocol', 'Native 协议'),
347
+ value: formatNativeProtocolVersion(native),
348
+ tone: statusValue === 'protocol_unsupported' ? 'fail' : undefined
349
+ }
331
350
  ];
332
351
  }
333
352
 
@@ -397,8 +416,13 @@
397
416
  const failure = mirrorState.failureActive && mirrorState.lastFailure ? { ...mirrorState.lastFailure } : null;
398
417
  const disabledResult = projectableResult(() => {
399
418
  const result = {
400
- title: tr('diagnosticsOtTitle'), subtitle: tr('diagnosticsOtSubtitle'), status: 'completed',
401
- summary: tr('diagnosticsOtSummaryDisabled'), bullets: [], nextStep: '', technical: ''
419
+ title: tr('diagnosticsOtTitle'),
420
+ subtitle: tr('diagnosticsOtSubtitle'),
421
+ status: 'completed',
422
+ summary: tr('diagnosticsOtSummaryDisabled'),
423
+ facts: [],
424
+ nextStep: '',
425
+ technical: ''
402
426
  };
403
427
  if (failure) {
404
428
  result.status = 'warning';
@@ -491,10 +515,14 @@
491
515
  status: enabled && !fallback && !bridgeFailed ? 'completed' : (enabled || bridgeFailed ? 'warning' : 'completed'),
492
516
  summary: tr(failureActive ? otWarmMirrorController.getFailureMessageKey(warmMirrorState.lastErrorCode)
493
517
  : enabled ? 'diagnosticsOtSummaryEnabled' : 'diagnosticsOtSummaryDisabled'),
494
- bullets: [
495
- `${tr('otStatus')}: ${formatOtStatusLabel(statusValue)}`,
496
- `${tr('otFreshFiles')}: ${otFreshFileCount}`,
497
- `${tr('otFallback')}: ${fallback ? tr('yes') : tr('no')}`
518
+ facts: [
519
+ { label: tr('otStatus'), value: formatOtStatusLabel(statusValue) },
520
+ { label: tr('otFreshFiles'), value: String(otFreshFileCount) },
521
+ {
522
+ label: tr('otFallback'),
523
+ value: fallback ? tr('yes') : tr('no'),
524
+ tone: enabled && fallback ? 'warn' : undefined
525
+ }
498
526
  ],
499
527
  nextStep: fallback ? tr('diagnosticsOtNextStep') : '',
500
528
  technical: formatOtDiagnosticsTechnicalDetails({
@@ -563,10 +591,23 @@
563
591
  const reviewingOk = probe?.reviewing?.ok === true;
564
592
  const writable = probe?.capabilities?.editor?.write !== false;
565
593
  const readable = probe?.editor?.ok === true;
566
- const bullets = [
567
- readable ? tx('Current file was read.', '当前文件已经读到。') : tx('Current file content has not been verified yet.', '还没有确认当前文件内容。'),
568
- writable ? tx('Editor write access will be verified again during writeback.', '当前编辑器写入能力会在写入时再次验证。') : tx('The current editor is not writable yet.', '当前编辑器暂时不可写。'),
569
- reviewingOk ? tx('Reviewing/Track Changes status is verified.', '留痕/Reviewing 状态已确认。') : tx('Reviewing/Track Changes status is not verified yet.', '留痕/Reviewing 状态还没有确认。')
594
+ const reviewingNeeded = mode !== 'ask';
595
+ const facts = [
596
+ {
597
+ label: tx('Current file', '当前文件'),
598
+ value: readable ? tx('Read', '已读取') : tx('Not verified', '未确认'),
599
+ tone: readable ? undefined : 'warn'
600
+ },
601
+ {
602
+ label: tx('Write access', '写入'),
603
+ value: writable ? tx('Checked again before writing', '写入前会再次验证') : tx('Not writable yet', '暂不可写'),
604
+ tone: writable ? undefined : 'warn'
605
+ },
606
+ {
607
+ label: tx('Track changes', '留痕'),
608
+ value: reviewingOk ? tx('Verified', '已确认') : tx('Not verified', '未确认'),
609
+ tone: reviewingOk || !reviewingNeeded ? undefined : 'warn'
610
+ }
570
611
  ];
571
612
 
572
613
  if (readable && writable) {
@@ -575,7 +616,7 @@
575
616
  subtitle: formatProbeStatusBar(probe),
576
617
  status: reviewingOk || mode === 'ask' ? 'completed' : 'warning',
577
618
  summary: tx('The current Overleaf file can be read and written. The extension still verifies before writing to avoid overwriting new user or collaborator edits.', '当前 Overleaf 文件可以读取和写入。写入前插件仍会再次验证,避免覆盖用户或协作者的新改动。'),
578
- bullets,
619
+ facts,
579
620
  nextStep: reviewingOk || mode === 'ask' ? '' : tx('If you need tracked writes, turn on Overleaf Reviewing/Track Changes first.', '如果需要留痕写入,请先在 Overleaf 开启 Reviewing/Track Changes。'),
580
621
  technical: formatPageDiagnosticsTechnicalDetails(probe)
581
622
  };
@@ -588,7 +629,7 @@
588
629
  summary: readable
589
630
  ? tx('The extension read the current Overleaf file, but has not verified that the current editor can be written.', '插件已读到当前 Overleaf 文件,但还没有确认当前编辑器可以写入。')
590
631
  : tx('The extension has not verified that the current editor can be written. Usually this means no .tex file is open yet, or Overleaf is still loading.', '插件没有确认当前编辑器可以写入。通常是还没点开 .tex 文件,或者 Overleaf 页面仍在加载。'),
591
- bullets,
632
+ facts,
592
633
  nextStep: tx('Open the target .tex file from the left Overleaf file tree, wait for the editor to load, then check again.', '在 Overleaf 左侧文件树点开要处理的 .tex 文件,等编辑器加载完成后重新检测。'),
593
634
  technical: formatPageDiagnosticsTechnicalDetails(probe)
594
635
  };
@@ -602,10 +643,10 @@
602
643
  const warnings = getProjectSnapshotWarnings(project);
603
644
  const warningText = [...warnings.blocking, ...warnings.nonBlocking].map(formatProjectSnapshotWarning);
604
645
  const source = project?.capabilities?.method || 'unknown';
605
- const bullets = [
606
- tx(`Text files: ${textCount}`, `文本文件:${textCount} 个`),
607
- tx(`Asset files: ${binaryCount}`, `资源文件:${binaryCount} 个`),
608
- project?.activePath ? tx(`Current file: ${project.activePath}`, `当前文件:${project.activePath}`) : tx('Current file: not detected', '当前文件:未识别')
646
+ const facts = [
647
+ { label: tx('Text files', '文本文件'), value: tx(String(textCount), `${textCount} 个`) },
648
+ { label: tx('Asset files', '资源文件'), value: tx(String(binaryCount), `${binaryCount} 个`) },
649
+ { label: tx('Current file', '当前文件'), value: project?.activePath || tx('Not detected', '未识别') }
609
650
  ];
610
651
  if (fullProject && files.length) {
611
652
  return {
@@ -616,7 +657,8 @@
616
657
  `The extension read the full Overleaf project: ${textCount} text file(s)${binaryCount ? `, ${binaryCount} asset file(s)` : ''}.`,
617
658
  `插件已读到完整 Overleaf 项目:${textCount} 个文本文件${binaryCount ? `,${binaryCount} 个资源文件` : ''}。`
618
659
  ),
619
- bullets: warningText.length ? [...bullets, ...warningText] : bullets,
660
+ facts,
661
+ notes: warningText,
620
662
  technical: formatProjectSnapshotLog(project)
621
663
  };
622
664
  }
@@ -630,7 +672,8 @@
630
672
  `没有读到完整的 Overleaf 项目。当前只读到 ${textCount} 个文本文件${binaryCount ? `,${binaryCount} 个资源文件` : ''}。`
631
673
  )
632
674
  : tx('The full Overleaf project was not read, and no usable file content was available.', '没有读到完整的 Overleaf 项目,也没有拿到可用文件内容。'),
633
- bullets: warningText.length ? [...bullets, ...warningText] : bullets,
675
+ facts,
676
+ notes: warningText,
634
677
  nextStep: tx('Reload Overleaf and wait for the left file tree to load, then retry. If you only want one file, use + to add @file context.', '刷新 Overleaf 页面,等左侧文件树加载完成后重试;如果只想处理一个文件,可以用 + 添加 @file 上下文。'),
635
678
  technical: formatProjectSnapshotLog(project)
636
679
  };
@@ -164,7 +164,7 @@
164
164
  },
165
165
  platform: nativeDiagnostics.platform,
166
166
  nativeEnvironment: nativeDiagnostics.nativeEnvironment,
167
- mirror,
167
+ mirror: mirror ?? { status: 'unavailable' },
168
168
  auditLogs,
169
169
  run: activeRecord?.runProjectId === projectId && matchesAccount(activeRecord, currentRunView?.runAccountScopeId)
170
170
  ? activeRecord : recentRuns[0] || {},
@@ -182,11 +182,18 @@
182
182
  const details = root.querySelector('[data-diagnostics-result-details]');
183
183
  const scroll = root.querySelector('.codex-diagnostics-result-scroll');
184
184
  const open = details?.open, scrollTop = scroll?.scrollTop;
185
- const checks = Array.from(root.querySelectorAll('.codex-diagnostics-check'), node => node.open);
185
+ const checks = Array.from(root.querySelectorAll('.codex-diagnostics-check'), node => ({
186
+ open: node.open,
187
+ technicalOpen: node.querySelector('.codex-diagnostics-technical')?.open
188
+ }));
186
189
  showResult(instance, result);
187
190
  if (details) details.open = open;
188
191
  root.querySelectorAll('.codex-diagnostics-check').forEach((node, index) => {
189
- if (index < checks.length) node.open = checks[index];
192
+ if (index < checks.length) {
193
+ node.open = checks[index].open;
194
+ const technical = node.querySelector('.codex-diagnostics-technical');
195
+ if (technical) technical.open = checks[index].technicalOpen;
196
+ }
190
197
  });
191
198
  if (scroll) scroll.scrollTop = scrollTop;
192
199
  }
@@ -203,45 +210,48 @@
203
210
  root.hidden = false;
204
211
  root.dataset.status = result.status || 'info';
205
212
  root.setAttribute('aria-label', result.title || t(instance, 'diagnosticsResult'));
206
- root.querySelector('[data-diagnostics-result-title]').textContent = result.title || t(instance, 'diagnosticsResult');
213
+ const titleNode = root.querySelector('[data-diagnostics-result-title]');
214
+ const dot = document.createElement('span');
215
+ dot.className = 'codex-diagnostics-status-dot';
216
+ dot.setAttribute('aria-hidden', 'true');
217
+ const titleText = document.createElement('span');
218
+ titleText.className = 'codex-diagnostics-result-title-text';
219
+ titleText.textContent = result.title || t(instance, 'diagnosticsResult');
220
+ titleNode.replaceChildren(dot, titleText);
207
221
  root.querySelector('[data-diagnostics-result-subtitle]').textContent = result.subtitle || '';
208
222
 
209
223
  const body = root.querySelector('[data-diagnostics-result-body]');
210
224
  body.textContent = '';
211
- if (result.summary) {
212
- appendParagraph(body, result.summary);
225
+ const facts = Array.isArray(result.facts) ? result.facts.filter(fact => fact?.label) : [];
226
+ const notes = [
227
+ ...(Array.isArray(result.notes) ? result.notes : []),
228
+ ...(Array.isArray(result.bullets) ? result.bullets : [])
229
+ ].filter(Boolean);
230
+ const hasChecks = Array.isArray(result.checks) && result.checks.length > 0;
231
+ if (result.summary && !(result.status === 'completed' && facts.length)) {
232
+ appendParagraph(body, result.summary, 'codex-diagnostics-summary');
213
233
  }
214
234
  // Aggregated health report: one status row per check (Run all checks).
215
- if (Array.isArray(result.checks) && result.checks.length) {
216
- renderCheckRows(body, result.checks);
217
- } else if (!result.summary) {
235
+ if (hasChecks) {
236
+ renderCheckRows(instance, body, result.checks);
237
+ } else if (!result.summary && !facts.length && !notes.length) {
218
238
  appendParagraph(body, t(instance, 'diagnosticsNoResult'));
219
239
  }
220
- if (Array.isArray(result.bullets) && result.bullets.length) {
221
- const list = document.createElement('ul');
222
- for (const item of result.bullets) {
223
- const li = document.createElement('li');
224
- li.textContent = item;
225
- list.append(li);
226
- }
227
- body.append(list);
240
+ if (facts.length) {
241
+ renderFacts(body, facts);
242
+ }
243
+ if (notes.length) {
244
+ renderNotes(body, notes);
228
245
  }
229
246
  if (result.nextStep) {
230
- const next = document.createElement('p');
231
- next.className = 'codex-diagnostics-next-step';
232
- next.textContent = `${t(instance, 'nextStepPrefix')}${result.nextStep}`;
233
- body.append(next);
247
+ appendNextStep(instance, body, result.nextStep, 'p', 'codex-diagnostics-next-step');
234
248
  }
235
249
  if (result.installCommand) {
236
250
  renderInstallCommand(instance, body, result.installCommand);
237
251
  }
238
252
 
239
253
  const details = root.querySelector('[data-diagnostics-result-details]');
240
- const technical = root.querySelector('[data-diagnostics-result-technical]');
241
- const technicalText = String(result.technical || '').trim();
242
- details.open = false;
243
- details.hidden = !technicalText;
244
- technical.textContent = technicalText;
254
+ renderTechnicalDetails(details, result.technical);
245
255
  closeMenu(instance);
246
256
  openFloating(instance, root, 400);
247
257
  if (wasHidden) root.querySelector('[data-diagnostics-result-close]')?.focus({ preventScroll: true });
@@ -324,14 +334,14 @@
324
334
  const wrap = document.createElement('div');
325
335
  wrap.className = 'codex-install-command';
326
336
 
327
- const label = document.createElement('div');
337
+ const head = document.createElement('div');
338
+ head.className = 'codex-install-command-head';
339
+ const label = document.createElement('span');
328
340
  label.className = 'codex-install-command-label';
329
341
  label.textContent = t(instance, 'runInTerminal');
330
- wrap.append(label);
331
342
 
332
343
  const code = document.createElement('code');
333
344
  code.textContent = command;
334
- wrap.append(code);
335
345
 
336
346
  const copyButton = document.createElement('button');
337
347
  copyButton.type = 'button';
@@ -343,18 +353,86 @@
343
353
  copyButton.textContent = t(instance, 'copyInstallCommand');
344
354
  }, 1400);
345
355
  });
346
- wrap.append(copyButton);
356
+ head.append(label, copyButton);
357
+ wrap.append(head, code);
347
358
 
348
359
  container.append(wrap);
349
360
  }
350
361
 
351
- function appendParagraph(container, text) {
362
+ function appendParagraph(container, text, className) {
352
363
  const paragraph = document.createElement('p');
364
+ if (className) {
365
+ paragraph.className = className;
366
+ }
353
367
  paragraph.textContent = text;
354
368
  container.append(paragraph);
355
369
  }
356
370
 
357
- // Map a result status to a coarse health bucket used for the row glyph + the
371
+ function appendNextStep(instance, container, text, tagName, className) {
372
+ const next = document.createElement(tagName);
373
+ next.className = className;
374
+ const label = document.createElement('span');
375
+ label.className = 'codex-diagnostics-next-label';
376
+ label.textContent = t(instance, 'nextStepPrefix');
377
+ next.append(label, document.createTextNode(text));
378
+ container.append(next);
379
+ }
380
+
381
+ // Facts pair a label and value, with optional tone and path presentation.
382
+ function renderFacts(container, facts) {
383
+ const list = document.createElement('dl');
384
+ list.className = 'codex-diagnostics-facts';
385
+ for (const fact of facts) {
386
+ const term = document.createElement('dt');
387
+ term.textContent = fact.label;
388
+ const value = document.createElement('dd');
389
+ const text = String(fact.value ?? '');
390
+ if (fact.kind === 'path') {
391
+ value.textContent = shortenPath(text);
392
+ value.title = text;
393
+ } else {
394
+ value.textContent = text;
395
+ }
396
+ if (fact.tone) {
397
+ value.dataset.tone = fact.tone;
398
+ }
399
+ list.append(term, value);
400
+ }
401
+ container.append(list);
402
+ }
403
+
404
+ // Shorten only the display; the full path stays in title and technical details.
405
+ function shortenPath(path) {
406
+ if (path.length <= 36) {
407
+ return path;
408
+ }
409
+ const parts = path.split('/');
410
+ if (parts.length < 5) {
411
+ return path;
412
+ }
413
+ const short = `${parts.slice(0, 3).join('/')}/…/${parts[parts.length - 1]}`;
414
+ return short.length < path.length ? short : path;
415
+ }
416
+
417
+ function renderNotes(container, notes) {
418
+ const list = document.createElement('ul');
419
+ list.className = 'codex-diagnostics-notes';
420
+ for (const note of notes) {
421
+ const item = document.createElement('li');
422
+ item.textContent = note;
423
+ list.append(item);
424
+ }
425
+ container.append(list);
426
+ }
427
+
428
+ function renderTechnicalDetails(details, value) {
429
+ const text = String(value || '').trim();
430
+ details.open = false;
431
+ details.hidden = !text;
432
+ details.querySelector('pre').textContent = text;
433
+ }
434
+
435
+ // Map a result status to a coarse health bucket used for the row dot + the
358
436
  // overall dot. 'completed' reads as a pass; anything not-ok is attention.
359
437
  function healthBucket(status) {
360
438
  if (status === 'completed' || status === 'ok') return 'ok';
@@ -363,10 +441,10 @@
363
441
  return 'info';
364
442
  }
365
443
 
366
- // Render the aggregated report as one scannable row per check: a status glyph
444
+ // Render the aggregated report as one scannable row per check: a status dot
367
445
  // (via CSS [data-status]), a plain-language title + summary, and an optional
368
- // actionable next step.
369
- function renderCheckRows(container, checks) {
446
+ // actionable next step, install command and technical evidence.
447
+ function renderCheckRows(instance, container, checks) {
370
448
  const list = document.createElement('div');
371
449
  list.className = 'codex-diagnostics-checks';
372
450
  for (const check of checks) {
@@ -377,16 +455,16 @@
377
455
  row.open = row.dataset.status === 'warn' || row.dataset.status === 'fail';
378
456
  const heading = document.createElement('summary');
379
457
 
380
- const glyph = document.createElement('span');
381
- glyph.className = 'codex-diagnostics-check-glyph';
382
- glyph.setAttribute('aria-hidden', 'true');
458
+ const dot = document.createElement('span');
459
+ dot.className = 'codex-diagnostics-check-dot';
460
+ dot.setAttribute('aria-hidden', 'true');
383
461
 
384
462
  const textWrap = document.createElement('div');
385
463
  textWrap.className = 'codex-diagnostics-check-text';
386
464
  const title = document.createElement('div');
387
465
  title.className = 'codex-diagnostics-check-title';
388
466
  title.textContent = check.title || '';
389
- heading.append(glyph, title);
467
+ heading.append(dot, title);
390
468
  if (check.summary) {
391
469
  const summary = document.createElement('div');
392
470
  summary.className = 'codex-diagnostics-check-summary';
@@ -394,10 +472,19 @@
394
472
  textWrap.append(summary);
395
473
  }
396
474
  if (check.nextStep) {
397
- const next = document.createElement('div');
398
- next.className = 'codex-diagnostics-check-next';
399
- next.textContent = `→ ${check.nextStep}`;
400
- textWrap.append(next);
475
+ appendNextStep(instance, textWrap, check.nextStep, 'div', 'codex-diagnostics-check-next');
476
+ }
477
+ if (check.installCommand) {
478
+ renderInstallCommand(instance, textWrap, check.installCommand);
479
+ }
480
+ if (check.technical) {
481
+ const details = document.createElement('details');
482
+ details.className = 'codex-diagnostics-technical';
483
+ const summary = document.createElement('summary');
484
+ summary.textContent = t(instance, 'technicalDetails');
485
+ details.append(summary, document.createElement('pre'));
486
+ renderTechnicalDetails(details, check.technical);
487
+ textWrap.append(details);
401
488
  }
402
489
 
403
490
  row.append(heading, textWrap);