minecodex 2.0.12 → 2.0.13

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 (59) hide show
  1. package/README.md +12 -0
  2. package/apps/dashboard/app.js +37 -4
  3. package/apps/dashboard/assets/data-transfer.js +129 -0
  4. package/apps/dashboard/assets/download.svg +15 -0
  5. package/apps/dashboard/assets/upload.svg +15 -0
  6. package/apps/dashboard/index.html +10 -0
  7. package/apps/dashboard/styles.css +52 -1
  8. package/features/file-navigation/assets/book-open.svg +16 -0
  9. package/features/file-navigation/codex-feature.json +27 -0
  10. package/features/file-navigation/src/adapters/codex/modules.mjs +67 -0
  11. package/features/file-navigation/src/adapters/codex/navigation.mjs +84 -0
  12. package/features/file-navigation/src/domain/navigation.mjs +12 -0
  13. package/features/file-navigation/src/entry.mjs +34 -0
  14. package/features/file-navigation/src/frontend/navigation.mjs +140 -0
  15. package/features/file-navigation/src/injection-source.mjs +9 -0
  16. package/features/file-navigation/src/navigation.css +16 -0
  17. package/features/file-navigation/vendor/lucide-LICENSE +43 -0
  18. package/features/markdown-reading/assets/book-open.svg +16 -0
  19. package/features/markdown-reading/codex-feature.json +27 -0
  20. package/features/markdown-reading/src/adapters/codex/links.mjs +280 -0
  21. package/features/markdown-reading/src/adapters/codex/lists.mjs +168 -0
  22. package/features/markdown-reading/src/adapters/codex/markdown-native.mjs +28 -0
  23. package/features/markdown-reading/src/adapters/codex/preview.mjs +38 -0
  24. package/features/markdown-reading/src/adapters/codex/providers.mjs +18 -0
  25. package/features/markdown-reading/src/adapters/codex/reading-runtime.mjs +152 -0
  26. package/features/markdown-reading/src/adapters/codex/rich-text.mjs +561 -0
  27. package/features/markdown-reading/src/adapters/codex/table-actions.mjs +270 -0
  28. package/features/markdown-reading/src/adapters/codex/tables.mjs +493 -0
  29. package/features/markdown-reading/src/adapters/codex/toc-native.mjs +131 -0
  30. package/features/markdown-reading/src/adapters/codex/toc.mjs +334 -0
  31. package/features/markdown-reading/src/domain/footnotes.mjs +56 -0
  32. package/features/markdown-reading/src/injection-source.mjs +43 -0
  33. package/features/markdown-reading/src/markdown-reading.mjs +82 -0
  34. package/features/markdown-reading/src/reading.css +346 -0
  35. package/features/markdown-reading/vendor/lucide-LICENSE +43 -0
  36. package/features/model-slider/codex-feature.json +1 -0
  37. package/features/model-slider/src/backend/favorites.mjs +54 -0
  38. package/features/model-slider/src/domain/model-rules.mjs +6 -1
  39. package/features/model-slider/src/frontend/install.mjs +24 -3
  40. package/features/notes/assets/prompt-previews.json +26 -0
  41. package/features/notes/codex-feature.json +2 -1
  42. package/features/notes/src/backend/transfer.mjs +97 -0
  43. package/features/notes/src/frontend/entry.mjs +380 -10
  44. package/features/notes/src/frontend/icons.json +4 -1
  45. package/features/notes/src/frontend/styles.css +63 -0
  46. package/features/session-status/src/domain/session-stats.mjs +22 -8
  47. package/features/session-status/src/session-status.mjs +2 -2
  48. package/features/sites/src/backend/sites-service.mjs +20 -5
  49. package/features/sites/src/backend/transfer.mjs +43 -0
  50. package/package.json +11 -3
  51. package/packages/cli/src/config.mjs +4 -2
  52. package/packages/cli/src/control-server.mjs +23 -4
  53. package/packages/cli/src/data-transfer/service.mjs +42 -0
  54. package/packages/cli/src/runtime-manager.mjs +3 -0
  55. package/packages/runtime-host/src/codex-injection.mjs +1 -0
  56. package/packages/runtime-host/src/codex-runtime.mjs +11 -0
  57. package/packages/runtime-host/src/feature-registry.mjs +1 -0
  58. package/packages/ui/src/native/theme.mjs +22 -1
  59. package/packages/ui/src/native/tokens.mjs +4 -0
package/README.md CHANGED
@@ -18,6 +18,8 @@ MineCodex 给 Codex Desktop 增加一组轻量、本地优先的扩展功能。
18
18
  | **Session Status** | 在输入框下方查看当前任务的步骤、观测速度、Token 用量与上下文明细 |
19
19
  | **Sidebar Focus** | 展开侧栏时聚焦当前任务路径,再次点击恢复布局 |
20
20
  | **Sites** | 在原生 Sites 中收录项目网页与自定义网址,按需打开或启动 |
21
+ | **Markdown Reading** | 用 Pages 正文排版、自适应留白与原生目录阅读文件面板的 Markdown Preview,保留原生工具栏与文字选择;默认关闭 |
22
+ | **File Navigation** | 精简路径与分列文件浏览;在 Markdown 阅读卡片内独立勾选,支持单独启用,默认关闭 |
21
23
 
22
24
  ## 安装
23
25
 
@@ -94,3 +96,13 @@ MineCodex 是非官方独立扩展项目,与 OpenAI 无关联。项目自有
94
96
  ## 从账号菜单打开控制台
95
97
 
96
98
  在 Codex Navbar 底部打开账号菜单,点击 Settings 下方的 **MineCodex ↗**,即可在外部默认浏览器打开本地控制台,等同于 `mcx gui`。macOS 默认 Safari、Chrome/Chromium、Edge、Brave 和 Vivaldi 会定位已有控制台标签页,没有时才新建;首次定位可能需要系统自动化权限。其他浏览器与 Windows 仍由系统打开 URL。
99
+
100
+ ### 在电脑之间迁移数据
101
+
102
+ 在 `mcx gui` 底部打开「数据迁移」,导出 `.mcx.json` 文件,再到另一台 Mac 或 Windows
103
+ 电脑的同一入口导入。确认前可查看新增、重复和冲突数量。只补充缺失项,重复项跳过,
104
+ 冲突保留本机版本,不删除已有数据。
105
+
106
+ 支持 Prompts 与分类、公网 Sites、模型收藏。模型旧收藏在新版模型滑条首次启动后收录,
107
+ 导入收藏在下次启动 Codex 后载入。图片文件、本地站点、账号信息与设备设置不包含在文件中;
108
+ 可迁移功能的特殊说明可通过 info 查看。这是手动迁移,不是自动云同步。文件包含提示词正文,请妥善保管。
@@ -211,9 +211,35 @@ function createPluginCard(feature) {
211
211
  return card;
212
212
  }
213
213
 
214
+ function createPluginOption(feature) {
215
+ const option = element("div", "plugin-option");
216
+ option.dataset.featureId = feature.id;
217
+ const label = element("label");
218
+ const input = element("input");
219
+ input.type = "checkbox";
220
+ input.name = `feature-${feature.id}`;
221
+ const name = element("span", "plugin-option-name");
222
+ const dot = element("span", "status-dot");
223
+ label.append(input, name, dot);
224
+ option.append(label);
225
+ return option;
226
+ }
227
+
214
228
  function updatePluginCard(card, feature) {
215
229
  for (const button of elements.languageButtons) button.disabled = pendingFeatureIds.size > 0;
216
230
  const status = featureState(feature);
231
+ if (card.classList.contains("plugin-option")) {
232
+ const input = card.querySelector("input");
233
+ input.checked = Boolean(feature.enabled);
234
+ input.disabled = pendingFeatureIds.has(feature.id);
235
+ input.setAttribute("aria-label", feature.label);
236
+ card.querySelector(".plugin-option-name").textContent = feature.label;
237
+ const dot = card.querySelector(".status-dot");
238
+ dot.className = `status-dot ${status.className}`;
239
+ dot.setAttribute("aria-label", status.label);
240
+ dot.title = status.label;
241
+ return;
242
+ }
217
243
  const input = card.querySelector('input[type="checkbox"]');
218
244
  const image = card.querySelector(".plugin-icon img");
219
245
  const name = card.querySelector(".plugin-name");
@@ -232,7 +258,7 @@ function updatePluginCard(card, feature) {
232
258
  }
233
259
 
234
260
  function findPluginCard(featureId) {
235
- return [...elements.pluginList.children].find((child) => child.dataset.featureId === featureId) ?? null;
261
+ return [...elements.pluginList.querySelectorAll("[data-feature-id]")].find((child) => child.dataset.featureId === featureId) ?? null;
236
262
  }
237
263
 
238
264
  function renderPluginCards(features) {
@@ -245,12 +271,19 @@ function renderPluginCards(features) {
245
271
  for (const feature of features) {
246
272
  let card = findPluginCard(feature.id);
247
273
  if (!card) {
248
- card = createPluginCard(feature);
249
- elements.pluginList.append(card);
274
+ if (feature.id === "file-navigation") {
275
+ const parent = findPluginCard("markdown-reading");
276
+ if (!parent) continue;
277
+ card = createPluginOption(feature);
278
+ parent.append(card);
279
+ } else {
280
+ card = createPluginCard(feature);
281
+ elements.pluginList.append(card);
282
+ }
250
283
  }
251
284
  updatePluginCard(card, feature);
252
285
  }
253
- for (const card of [...elements.pluginList.children]) {
286
+ for (const card of [...elements.pluginList.querySelectorAll("[data-feature-id]")]) {
254
287
  if (card.dataset.featureId && !featureIds.has(card.dataset.featureId)) card.remove();
255
288
  }
256
289
  elements.pluginList.setAttribute("aria-busy", "false");
@@ -0,0 +1,129 @@
1
+ import { installFeatureTooltips } from './ui-tooltip.js';
2
+ const tooltipLifetime = new AbortController();
3
+ const { tooltipSurface } = installFeatureTooltips(tooltipLifetime.signal);
4
+ window.addEventListener('pagehide', event => { if (!event.persisted) tooltipLifetime.abort(); });
5
+ const zh = document.documentElement.lang.startsWith('zh');
6
+ const t = (cn, en) => zh ? cn : en;
7
+ const dialog = document.querySelector('#data-dialog');
8
+ const entry = document.querySelector('#data-button');
9
+ const list = document.querySelector('#data-list');
10
+ const featureNames = { notes: 'Prompts', sites: 'Sites', 'model-slider': t('模型滑条', 'Model Slider') };
11
+ const status = document.querySelector('#data-status');
12
+ const exportButton = document.querySelector('#data-export');
13
+ const importButton = document.querySelector('#data-import');
14
+ const input = document.querySelector('#data-file');
15
+ let bundle = null, busy = false;
16
+ entry.textContent = t('数据迁移', 'Transfer data');
17
+ document.querySelector('#data-title').textContent = t('数据迁移', 'Transfer data');
18
+ document.querySelector('#data-policy').textContent = t('仅补充缺失项,重复跳过,冲突保留本机。', 'Add missing items. Skip duplicates. Keep local conflicts.');
19
+ document.querySelector('#data-close').setAttribute('aria-label', t('关闭', 'Close'));
20
+ exportButton.querySelector('.button-label').textContent = t('导出', 'Export');
21
+ importButton.querySelector('.button-label').textContent = t('导入', 'Import');
22
+ async function request(url, data) {
23
+ const response = await fetch(url, data ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) } : {});
24
+ if (!response.ok) throw new Error(String(response.status));
25
+ return response.json();
26
+ }
27
+ function setBusy(value) {
28
+ busy = value;
29
+ exportButton.disabled = value;
30
+ importButton.disabled = value;
31
+ document.querySelector('#data-close').disabled = value;
32
+ list.setAttribute('aria-busy', String(value));
33
+ }
34
+ const featureHints = {
35
+ 'model-slider': t('仅匹配相同模型标识。本机缺少的模型保留收藏,不显示选项;导入后下次启动 Codex 生效。', 'Only exact model IDs match. Missing models stay saved but hidden. Imported favorites load next time Codex starts.'),
36
+ notes: t('包含分类、排序与使用次数。', 'Includes categories, order and usage counts.'),
37
+ sites: t('仅迁移公网网址,导入后归为外部网址。', 'Public URLs only; imported as external links.'),
38
+ };
39
+ let hintIndex = 0;
40
+ function row(name, detail, amount, hint = '') {
41
+ const item = document.createElement('div'); item.className = 'data-card';
42
+ const copy = document.createElement('div'), heading = document.createElement('div'), title = document.createElement('strong'), count = document.createElement('span');
43
+ heading.className = 'data-card-heading';
44
+ title.textContent = name; count.textContent = amount; count.className = 'data-count';
45
+ heading.append(title);
46
+ if (hint) {
47
+ const info = document.createElement('button'), icon = document.createElement('span'), tip = document.createElement('div');
48
+ info.type = 'button'; info.className = 'data-info'; info.setAttribute('aria-label', t(`${name} 说明`, `About ${name}`));
49
+ icon.className = 'ui-icon info-icon'; icon.setAttribute('aria-hidden', 'true'); info.append(icon);
50
+ tooltipSurface(tip);
51
+ tip.id = `data-hint-${++hintIndex}`; tip.className = 'data-tooltip'; tip.setAttribute('popover', 'auto'); tip.setAttribute('role', 'tooltip'); tip.textContent = hint;
52
+ info.setAttribute('aria-describedby', tip.id); info.setAttribute('aria-expanded', 'false');
53
+ info.addEventListener('click', () => {
54
+ tip.togglePopover();
55
+ if (!tip.matches(':popover-open')) return;
56
+ const anchor = info.getBoundingClientRect(), box = tip.getBoundingClientRect();
57
+ tip.style.left = `${Math.max(8, Math.min(anchor.left + anchor.width / 2 - box.width / 2, innerWidth - box.width - 8))}px`;
58
+ tip.style.top = `${anchor.top - box.height - 2 >= 8 ? anchor.top - box.height - 2 : anchor.bottom + 2}px`;
59
+ });
60
+ tip.addEventListener('toggle', () => info.setAttribute('aria-expanded', String(tip.matches(':popover-open'))));
61
+ heading.append(info, tip);
62
+ }
63
+ copy.append(heading);
64
+ if (detail) { const desc = document.createElement('p'); desc.textContent = detail; copy.append(desc); }
65
+ item.append(copy, count); list.append(item);
66
+ }
67
+ function showInventory(data) {
68
+ list.replaceChildren();
69
+ for (const feature of data.features.filter(item => item.portable)) {
70
+ const detail = feature.id === 'notes' ? t(`${feature.categories} 个分类`, `${feature.categories} categories`) : '';
71
+ const hint = feature.id === 'model-slider' && !feature.initialized
72
+ ? t('旧收藏在新版模型滑条首次启动后收录。', 'Legacy favorites are collected when the updated slider first starts.') + ' ' + featureHints[feature.id]
73
+ : featureHints[feature.id];
74
+ row(featureNames[feature.id] || feature.id, detail, t(`${feature.count} 项`, `${feature.count} items`), hint);
75
+ }
76
+ }
77
+ function showPreview(data) {
78
+ list.replaceChildren();
79
+ for (const feature of data.features) row(featureNames[feature.id] || feature.id, t(`${feature.duplicate} 项重复 · ${feature.conflict} 项冲突,保留本机${feature.categoriesAdded ? ` · 新增 ${feature.categoriesAdded} 个分类` : ''}`, `${feature.duplicate} duplicates · ${feature.conflict} conflicts kept locally${feature.categoriesAdded ? ` · ${feature.categoriesAdded} new categories` : ''}`), t(`新增 ${feature.added} 项`, `${feature.added} new`), featureHints[feature.id]);
80
+ if (!data.features.length) row(t('空文件', 'Empty file'), t('没有可导入的数据', 'No data to import'), '0');
81
+ }
82
+ entry.addEventListener('click', async () => {
83
+ if (busy) return;
84
+ bundle = null; input.value = ''; importButton.querySelector('.button-label').textContent = t('导入', 'Import');
85
+ status.textContent = t('正在读取本机数据…', 'Reading local data…');
86
+ list.replaceChildren(); dialog.showModal(); setBusy(true);
87
+ try { showInventory(await request('/api/data')); status.textContent = ''; }
88
+ catch { status.textContent = t('无法读取数据。请关闭后重试。', 'Could not read data. Close and try again.'); }
89
+ finally { setBusy(false); }
90
+ });
91
+ document.querySelector('#data-close').addEventListener('click', () => { if (!busy) dialog.close(); });
92
+ dialog.addEventListener('cancel', event => { if (busy) event.preventDefault(); });
93
+ exportButton.addEventListener('click', async () => {
94
+ setBusy(true); status.textContent = t('正在导出…', 'Exporting…');
95
+ try {
96
+ const data = await request('/api/data/export');
97
+ const url = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
98
+ const a = document.createElement('a'); a.href = url; a.download = `minecodex-${new Intl.DateTimeFormat('sv-SE').format(new Date())}.mcx.json`; a.click();
99
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
100
+ status.textContent = t('已导出', 'Exported');
101
+ } catch { status.textContent = t('导出失败,请重试。文件最多支持 16 MB。', 'Export failed. Try again. Files are limited to 16 MB.'); }
102
+ finally { setBusy(false); }
103
+ });
104
+ input.addEventListener('change', async () => {
105
+ const file = input.files[0]; if (!file) return;
106
+ bundle = null; setBusy(true); importButton.querySelector('.button-label').textContent = t('导入', 'Import');
107
+ try {
108
+ if (file.size > 16 * 1024 * 1024) throw new Error('size');
109
+ const data = JSON.parse(await file.text());
110
+ const preview = await request('/api/data/preview', data);
111
+ bundle = data; showPreview(preview);
112
+ status.textContent = file.name;
113
+ importButton.querySelector('.button-label').textContent = t('确认导入', 'Confirm import');
114
+ } catch { status.textContent = t('文件无效或超过 16 MB', 'Invalid file or larger than 16 MB'); }
115
+ finally { input.value = ''; setBusy(false); }
116
+ });
117
+ importButton.addEventListener('click', async () => {
118
+ if (!bundle) { input.click(); return; }
119
+ setBusy(true); status.textContent = t('正在导入…', 'Importing…');
120
+ try {
121
+ const result = await request('/api/data/import', bundle);
122
+ list.replaceChildren();
123
+ for (const feature of result.features) row(featureNames[feature.id] || feature.id, feature.status === 'failed' ? t('可重试', 'Retry available') : t(`跳过 ${feature.duplicate} 项重复 · 保留 ${feature.conflict} 项冲突${feature.categoriesAdded ? ` · 新增 ${feature.categoriesAdded} 个分类` : ''}`, `${feature.duplicate} duplicates skipped · ${feature.conflict} conflicts kept${feature.categoriesAdded ? ` · ${feature.categoriesAdded} new categories` : ''}`), feature.status === 'failed' ? t('失败', 'Failed') : t(`新增 ${feature.added} 项`, `${feature.added} added`), featureHints[feature.id]);
124
+ status.textContent = result.complete ? t('导入完成', 'Import complete') : t('部分失败,请重试', 'Partially imported. Retry.');
125
+ if (result.complete) bundle = null;
126
+ importButton.querySelector('.button-label').textContent = result.complete ? t('导入', 'Import') : t('重试导入', 'Retry import');
127
+ } catch { status.textContent = t('未能确认结果,请重试', 'Could not confirm the result. Retry.'); }
128
+ finally { setBusy(false); }
129
+ });
@@ -0,0 +1,15 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <path d="M12 15V3" />
13
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
14
+ <path d="m7 10 5 5 5-5" />
15
+ </svg>
@@ -0,0 +1,15 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <path d="M12 3v12" />
13
+ <path d="m17 8-5-5-5 5" />
14
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
15
+ </svg>
@@ -9,6 +9,7 @@
9
9
  <link rel="stylesheet" href="/styles.css?v=mcx-block-1">
10
10
  <script type="module" src="/app.js?v=mcx-block-1"></script>
11
11
  <script type="module" src="/assets/terrain.js"></script>
12
+ <script type="module" src="/assets/data-transfer.js"></script>
12
13
  </head>
13
14
  <body>
14
15
  <canvas id="agent-world" aria-hidden="true"></canvas>
@@ -71,11 +72,20 @@
71
72
  <dl>
72
73
  <div><dt id="version-label">Version</dt><dd id="version-value">—</dd></div>
73
74
  </dl>
75
+ <button id="data-button" class="button quiet" type="button">Transfer data</button>
74
76
  <button id="diagnostics-button" class="button quiet" type="button">Copy diagnostics</button>
75
77
  <button id="update-button" class="button quiet" type="button"><span class="ui-icon refresh-icon" aria-hidden="true"></span><span class="button-label">Check for updates</span></button>
76
78
  </footer>
77
79
  <p id="update-result" class="update-result" role="status" hidden></p>
78
80
  </main>
81
+ <dialog id="data-dialog" class="data-dialog" aria-labelledby="data-title">
82
+ <header class="data-header"><div><h2 id="data-title">Transfer data</h2></div><button id="data-close" class="button quiet" type="button" aria-label="Close"><svg viewBox="0 0 21 21" fill="none" aria-hidden="true"><path d="M14.6549 5.57307C14.9283 5.2997 15.3718 5.2997 15.6451 5.57307C15.9185 5.84643 15.9185 6.28993 15.6451 6.5633L11.3903 10.8182L15.6451 15.0731L15.735 15.1834C15.9141 15.4551 15.8842 15.8242 15.6451 16.0633C15.4061 16.3024 15.0369 16.3322 14.7653 16.1531L14.6549 16.0633L10.4 11.8084L6.14515 16.0633C5.87178 16.3367 5.42828 16.3367 5.15492 16.0633C4.88155 15.7899 4.88155 15.3464 5.15492 15.0731L9.4098 10.8182L5.15492 6.5633L5.06507 6.45295C4.88597 6.18128 4.91584 5.81214 5.15492 5.57307C5.39399 5.33399 5.76313 5.30413 6.0348 5.48322L6.14515 5.57307L10.4 9.82795L14.6549 5.57307Z" fill="currentColor"/></svg></button></header>
83
+ <div id="data-list" class="data-list"></div>
84
+ <p id="data-policy" class="data-policy"></p>
85
+ <p id="data-status" class="data-help data-status" role="status" aria-live="polite"></p>
86
+ <footer class="data-actions"><button id="data-export" class="button secondary" type="button"><span class="ui-icon download-icon" aria-hidden="true"></span><span class="button-label">Export</span></button><button id="data-import" class="button secondary" type="button"><span class="ui-icon upload-icon" aria-hidden="true"></span><span class="button-label">Import</span></button></footer>
87
+ <input id="data-file" type="file" accept=".json,.mcx.json,application/json" hidden>
88
+ </dialog>
79
89
  <div id="toast" class="toast" hidden role="status" aria-live="polite"></div>
80
90
  </body>
81
91
  </html>
@@ -224,7 +224,7 @@ h2 { font-size: 14px; font-weight: 620; }
224
224
  .diagnostics .button { display: inline-flex; align-items: center; gap: 7px; font-weight: 400; }
225
225
  .diagnostics .ui-icon { width: 15px; height: 15px; flex-basis: 15px; }
226
226
  .diagnostics dl { display: flex; flex-wrap: wrap; gap: 10px 24px; margin: 0; }
227
- .diagnostics dl div { display: flex; gap: 6px; }
227
+ .diagnostics dl div { display: flex; align-items: baseline; gap: 6px; }
228
228
  .diagnostics dt { color: var(--ink-tertiary); }
229
229
  .diagnostics dd { margin: 0; color: var(--ink-secondary); }
230
230
 
@@ -265,3 +265,54 @@ h2 { font-size: 14px; font-weight: 620; }
265
265
  @media (prefers-reduced-motion: reduce) {
266
266
  *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
267
267
  }
268
+
269
+ .plugin-option { margin-top:12px; padding-top:12px; border-top:1px solid var(--border); }
270
+ .plugin-option label { display:flex; align-items:center; gap:8px; font-size:13px; }
271
+ .plugin-option input { margin:0; accent-color:var(--ink); }
272
+ .plugin-option .status-dot { margin-left:auto; }
273
+
274
+ /* 数据迁移沿用控制台的不透明表面与卡片尺寸。 */
275
+ .data-dialog { width: min(520px, calc(100vw - 32px)); max-height: calc(100dvh - 48px); padding: 24px; border: 1px solid var(--border); border-bottom: 3px solid var(--block-shadow); border-radius: var(--card-radius); background: var(--surface); color: var(--ink); box-shadow: var(--shadow); overflow-y: auto; }
276
+ .data-dialog::backdrop { background: #10172280; }
277
+ .data-header { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 20px; }
278
+ .data-header h2 { margin: 0; font-size: 20px; line-height: 28px; font-weight: 600; }
279
+ .data-header #data-close { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 32px; padding: 0; width: 32px; height: 32px; min-height: 32px; }
280
+ .data-header #data-close svg { display: block; width: 16px; height: 16px; }
281
+ .data-list { display: grid; gap: 10px; }
282
+ .data-card { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 16px; border: 1px solid var(--border); border-radius: var(--card-radius); background: var(--surface); }
283
+ .data-card-heading { display: flex; align-items: center; gap: 6px; }
284
+ .data-card strong { font-size: 14px; font-weight: 600; line-height: 22px; }
285
+ .data-card p { margin: 4px 0 0; font-size: 12px; line-height: 1.6; color: var(--ink-tertiary); }
286
+ .data-count { white-space: nowrap; font-size: 13px; font-variant-numeric: tabular-nums; }
287
+ .data-info { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 4px; background: transparent; color: var(--ink-tertiary); cursor: pointer; }
288
+ .data-info .ui-icon { width: 14px; height: 14px; flex-basis: 14px; }
289
+ .data-info:hover, .data-info[aria-expanded="true"] { background: var(--surface-muted); color: var(--ink-secondary); }
290
+ .data-tooltip { inset: auto; margin: 0; width: max-content; max-width: min(320px, calc(100vw - 16px)); text-align: center; overflow-wrap: anywhere; }
291
+ .data-policy { margin: 14px 0 0; color: var(--ink-tertiary); font-size: 11px; line-height: 1.6; }
292
+ .data-status { margin: 12px 0 0; font-size: 12px; line-height: 1.6; color: var(--ink-secondary); overflow-wrap: anywhere; }
293
+ .data-status:empty { display: none; }
294
+ .data-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 24px; }
295
+ .data-actions .button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-width: 88px; }
296
+ .data-actions .ui-icon { width: 16px; height: 16px; flex-basis: 16px; }
297
+ .download-icon { --icon: url("/assets/download.svg"); }
298
+ .upload-icon { --icon: url("/assets/upload.svg"); }
299
+ @media (max-width: 600px) { .data-dialog { padding: 20px; } .data-card { gap: 12px; } }
300
+
301
+ /* 独立控制台没有原生主题桥,只映射 token;Tooltip 材质由共享组件安装。 */
302
+ :root {
303
+ --mcx-spacing: 4px;
304
+ --mcx-radius-2xl: 16px;
305
+ --mcx-codex-corner-shape: round;
306
+ --mcx-color-background-tooltip: var(--ink);
307
+ --mcx-color-text-tooltip: var(--surface);
308
+ --mcx-color-border-tooltip: color-mix(in oklab, var(--mcx-color-text-tooltip) 5%, transparent);
309
+ --mcx-shadow-tooltip: 0 8px 18px #0f172a33;
310
+ --mcx-font-sans-default: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
311
+ --mcx-text-sm: 13px;
312
+ --mcx-tooltip-compact-font-weight: 400;
313
+ --mcx-tracking-tooltip: -.15px;
314
+ }
315
+ :root[data-theme="dark"] { --mcx-color-background-tooltip: var(--surface); --mcx-color-text-tooltip: var(--ink); }
316
+ @supports (corner-shape: superellipse(1.5)) {
317
+ :root { --mcx-codex-corner-shape: superellipse(1.5); --mcx-radius-2xl: 24px; }
318
+ }
@@ -0,0 +1,16 @@
1
+ <!-- @license lucide-static v0.577.0 - ISC -->
2
+ <svg
3
+ class="lucide lucide-book-open"
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ width="24"
6
+ height="24"
7
+ viewBox="0 0 24 24"
8
+ fill="none"
9
+ stroke="currentColor"
10
+ stroke-width="2.1"
11
+ stroke-linecap="round"
12
+ stroke-linejoin="round"
13
+ >
14
+ <path d="M12 7v14" />
15
+ <path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
16
+ </svg>
@@ -0,0 +1,27 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "file-navigation",
4
+ "version": "0.1.0",
5
+ "label": {
6
+ "en": "Compact file navigation",
7
+ "zh-CN": "精简文件导航"
8
+ },
9
+ "description": {
10
+ "en": "Browse folders in columns from a compact file path.",
11
+ "zh-CN": "精简文件路径,使用分列列表浏览文件夹并切换文件。"
12
+ },
13
+ "entry": {
14
+ "kind": "page-script",
15
+ "script": "src/entry.mjs",
16
+ "sourceFactory": "src/injection-source.mjs",
17
+ "resources": {
18
+ "styles": "src/navigation.css"
19
+ }
20
+ },
21
+ "icon": {
22
+ "resource": "assets/book-open.svg"
23
+ },
24
+ "placement": {
25
+ "order": 91
26
+ }
27
+ }
@@ -0,0 +1,67 @@
1
+ // 依照原生 breadcrumb 的依赖角色查找公开模块,不绑定产物 hash 或短变量名。
2
+ export async function loadFileNavigationModules() {
3
+ const urls = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(n => n.href || n.src);
4
+ const breadcrumbUrl = urls.find(url => /\/review-file-source-breadcrumb-[^/]+\.js$/.test(url));
5
+ const mainUrl = urls.find(url => /\/app-main-[^/]+\.js$/.test(url));
6
+ if (!breadcrumbUrl || !mainUrl) throw new Error('file-navigation-modules-unavailable');
7
+ const read = async url => {
8
+ const response = await fetch(url);
9
+ if (!response.ok) throw new Error('file-navigation-source-unavailable');
10
+ return response.text();
11
+ };
12
+ const imports = (source, url) => {
13
+ const result = new Map();
14
+ for (const match of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g))
15
+ for (const entry of match[1].split(',')) {
16
+ const parts = entry.trim().split(/\s+as\s+/);
17
+ result.set(parts.at(-1), { name: parts[0], url: new URL(match[2], url).href });
18
+ }
19
+ return result;
20
+ };
21
+ const binding = async (map, name) => {
22
+ const value = map.get(name);
23
+ if (!value) throw new Error('file-navigation-dependency-unavailable');
24
+ return (await import(value.url))[value.name];
25
+ };
26
+ const source = await read(breadcrumbUrl), deps = imports(source, breadcrumbUrl);
27
+ const treeUrl = [...deps.values()].find(v => /\/base-file-tree-/.test(v.url))?.url;
28
+ const queryUrl = [...deps.values()].find(v => /\/workspace-directory-entries-query-/.test(v.url))?.url;
29
+ if (!treeUrl || !queryUrl) throw new Error('file-navigation-contract-changed');
30
+ const [treeSource, querySource, main] = await Promise.all([read(treeUrl), read(queryUrl), read(mainUrl)]);
31
+ const [treeModule, queryModule] = await Promise.all([import(treeUrl), import(queryUrl)]);
32
+ for (const module of [treeModule, queryModule])
33
+ for (const value of Object.values(module))
34
+ if (typeof value === 'function' && /^function ([\w$]+)\(\)\{return\(\1=/.test(String(value))) value();
35
+ const Tree = Object.values(treeModule).find(value => typeof value === 'function' && String(value).includes('contextMenuEnabled:'));
36
+ const reactOwner = treeSource.match(/\(0,([\w$]+)\.useState\)/)?.[1];
37
+ const reactFactory = reactOwner && treeSource.match(new RegExp(`\\b${reactOwner}=([\\w$]+)\\(\\)`))?.[1];
38
+ const rootOwner = main.match(/\(0,([\w$]+)\.createRoot\)/)?.[1];
39
+ const rootFactory = rootOwner && main.match(new RegExp(`\\b${rootOwner}=([\\w$]+)\\(\\)`))?.[1];
40
+ const requestName = querySource.match(/queryFn:\(\{signal:[\w$]+\}\)=>([\w$]+)\(`workspace-directory-entries`/)?.[1];
41
+ const [react, dom, request] = await Promise.all([
42
+ binding(imports(treeSource, treeUrl), reactFactory),
43
+ binding(imports(main, mainUrl), rootFactory),
44
+ binding(imports(querySource, queryUrl), requestName),
45
+ ]);
46
+ const React = react(), ReactDOM = dom();
47
+ if (!Tree || !React.createElement || !ReactDOM.createRoot || typeof request !== 'function')
48
+ throw new Error('file-navigation-components-unavailable');
49
+ const sharedUrl = [...imports(treeSource, treeUrl).values()].find(value => /\/app-shared-/.test(value.url))?.url;
50
+ const shared = sharedUrl && await import(sharedUrl);
51
+ const folder = shared && Object.values(shared).find(value => value?.name === 'folder-light-16');
52
+ if (!folder?.canvas?.viewBox || typeof folder.body !== 'string')
53
+ throw new Error('file-navigation-folder-icon-unavailable');
54
+ // 分列导航没有纵向展开状态;通过原生样式入口使用固定的原生文件夹图标。
55
+ const folderImage = `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${folder.canvas.viewBox}">${folder.body}</svg>`)}")`;
56
+ const columnStyles = `
57
+ /* 原生树的逻辑焦点也响应鼠标;描边仅保留给键盘可见焦点。 */
58
+ [data-type="item"]:not(:focus-visible)::before { outline:none; }
59
+ [data-item-type="folder"] [data-item-section="icon"] { width:16px; height:16px; transform:none; }
60
+ [data-item-type="folder"] [data-item-section="icon"] svg { display:none; }
61
+ [data-item-type="folder"] [data-item-section="icon"]::before {
62
+ content:""; display:block; width:16px; height:16px; background:currentColor;
63
+ mask:${folderImage} center / contain no-repeat;
64
+ }
65
+ `;
66
+ return { Tree, React, ReactDOM, request, columnStyles };
67
+ }
@@ -0,0 +1,84 @@
1
+ import { loadFileNavigationModules } from './modules.mjs';
2
+
3
+ export function createFileNavigationAdapter(codex, loadModules = loadFileNavigationModules) {
4
+ let pending;
5
+ const modules = () => pending ??= loadModules().catch(error => { pending = null; throw error; });
6
+ function read(nav) {
7
+ const button = [...nav.querySelectorAll('button')].at(-1);
8
+ const props = button && codex.props(button, p => typeof p.root === 'string' && typeof p.activePath === 'string' && typeof p.onSelectFile === 'function');
9
+ if (!props || !nav.parentElement || !props.hostId) return null;
10
+ const rootName = nav.querySelector('button')?.textContent;
11
+ const separator = nav.querySelector('li > span')?.cloneNode(true);
12
+ let boundary = nav.parentElement;
13
+ while (boundary.parentElement && boundary.getBoundingClientRect().height < 160) boundary = boundary.parentElement;
14
+ const key = JSON.stringify([props.hostId, props.root, props.activePath]);
15
+ return {
16
+ key, nav, capsule: nav.parentElement, boundary, rootName, path: props.activePath, separator,
17
+ async list(directory, signal) {
18
+ const { request } = await modules();
19
+ if (signal.aborted) throw new DOMException('Aborted', 'AbortError');
20
+ const result = await request('workspace-directory-entries', { params: {
21
+ hostId: props.hostId, workspaceRoot: props.root, directoryPath: directory || null, includeHidden: true,
22
+ }, signal });
23
+ if (!Array.isArray(result?.entries)) throw new Error('file-navigation-directory-contract-changed');
24
+ return result.entries.map(entry => {
25
+ if (typeof entry.path !== 'string' || !['file', 'directory'].includes(entry.type))
26
+ throw new Error('file-navigation-entry-contract-changed');
27
+ return { path: entry.path, name: entry.path.split('/').at(-1), directory: entry.type === 'directory' };
28
+ });
29
+ },
30
+ open(entry) {
31
+ if (entry.directory) return;
32
+ props.onSelectFile(`${props.root.replace(/[\\/]$/, '')}/${entry.path}`, { isPreview: true });
33
+ },
34
+ async prepare() {
35
+ const native = await modules();
36
+ const providers = [], seen = new Set();
37
+ for (let fiber = codex.currentFiber(nav); fiber; fiber = fiber.return) {
38
+ if (fiber.tag !== 10 || seen.has(fiber.type)) continue;
39
+ providers.push({ type: fiber.type, value: fiber.memoizedProps.value }); seen.add(fiber.type);
40
+ }
41
+ return {
42
+ mountColumn(element, entries, selected, onSelect, onBack) {
43
+ const { React, ReactDOM, Tree } = native;
44
+ const root = ReactDOM.createRoot(element);
45
+ const byName = new Map(entries.map(entry => [entry.name, entry]));
46
+ const activate = event => {
47
+ const row = event.nativeEvent.composedPath().find(node => node instanceof Element && node.hasAttribute('data-item-path'));
48
+ const entry = byName.get(row?.getAttribute('data-item-path')?.replace(/\/$/, ''));
49
+ if (entry) onSelect(entry);
50
+ };
51
+ let tree = React.createElement(Tree, {
52
+ cwd: props.root, hostId: props.hostId, contextMenuEnabled: false, surface: 'dropdown', unsafeCSS: native.columnStyles,
53
+ paths: entries.map(entry => ({ path: entry.path, displayPath: entry.name + (entry.directory ? '/' : '') })),
54
+ selectedPath: entries.find(entry => entry.path === selected)?.name,
55
+ initialExpandedPaths: [], flattenEmptyDirectories: false, revealSelectedPath: true, revealSelectedPathScrollOffset: 'center',
56
+ onClick: activate,
57
+ });
58
+ for (const provider of providers) tree = React.createElement(provider.type, { value: provider.value }, tree);
59
+ root.render(tree);
60
+ const keys = event => {
61
+ const row = event.composedPath().find(node => node instanceof Element && node.hasAttribute('data-item-path'));
62
+ const entry = byName.get(row?.getAttribute('data-item-path')?.replace(/\/$/, ''));
63
+ if (event.key === 'ArrowLeft') { event.preventDefault(); event.stopPropagation(); onBack(); }
64
+ if (entry && (event.key === 'Enter' || (event.key === 'ArrowRight' && entry.directory))) {
65
+ event.preventDefault(); event.stopPropagation(); onSelect(entry);
66
+ }
67
+ };
68
+ element.addEventListener('keydown', keys, true);
69
+ return {
70
+ dispose() { element.removeEventListener('keydown', keys, true); root.unmount(); },
71
+ focus() {
72
+ const shadow = element.querySelector('file-tree-container')?.shadowRoot;
73
+ (shadow?.querySelector('[aria-selected="true"]') ?? shadow?.querySelector('[role="treeitem"]'))?.focus();
74
+ },
75
+ };
76
+ },
77
+ };
78
+ },
79
+ };
80
+ }
81
+ return {
82
+ scan: () => [...document.querySelectorAll('nav:has(> ol)')].map(read).filter(Boolean),
83
+ };
84
+ }
@@ -0,0 +1,12 @@
1
+ export function fileNavigationPath(rootName, path) {
2
+ const parts = path.split('/').filter(Boolean);
3
+ const names = [rootName, ...parts];
4
+ return {
5
+ labels: names.length > 3 ? ['…', ...names.slice(-2)] : names,
6
+ directories: ['', ...parts.slice(0, -1).map((_, index) => parts.slice(0, index + 1).join('/'))],
7
+ };
8
+ }
9
+
10
+ export function selectFileNavigationDirectory(directories, index, path) {
11
+ return [...directories.slice(0, index + 1), path];
12
+ }
@@ -0,0 +1,34 @@
1
+ const adapter = createFileNavigationAdapter(config.codex);
2
+ const mounted = new Map();
3
+ const style = document.createElement('style');
4
+ style.setAttribute('data-mcx-file-navigation-style', ''); style.textContent = config.resources.styles;
5
+ document.head.append(style);
6
+ let frame = null, disposed = false;
7
+ function reconcile() {
8
+ frame = null;
9
+ if (disposed) return;
10
+ const views = adapter.scan(), active = new Set(views.map(view => view.nav));
11
+ for (const [nav, item] of mounted) if (!active.has(nav)) { item.session.dispose(); mounted.delete(nav); }
12
+ for (const view of views) {
13
+ const existing = mounted.get(view.nav);
14
+ if (existing?.key === view.key) continue;
15
+ existing?.session.dispose();
16
+ mounted.set(view.nav, { key: view.key, session: installFileNavigation(view, config.ui, config.locale) });
17
+ }
18
+ }
19
+ const observer = new MutationObserver(records => {
20
+ if (records.every(record => record.target.closest?.('.mcx-file-navigation-popover,.mcx-file-navigation-trigger,.cm-content'))) return;
21
+ if (frame === null && !disposed) frame = requestAnimationFrame(reconcile);
22
+ });
23
+ signal.addEventListener('abort', disposeFileNavigation, { once: true });
24
+ observer.observe(document.body, { childList: true, subtree: true }); reconcile();
25
+ config.registerSelfCheck?.(() => ({
26
+ state: mounted.size ? [...mounted.values()].every(item => item.session.available()) ? 'available' : 'unavailable' : 'not-applicable',
27
+ checks: [...mounted.values()].map(item => ({ name: 'file-navigation-trigger', available: item.session.available() })),
28
+ scopes: [...mounted.keys()].map(nav => ({ name: 'file-navigation', element: nav.parentElement })),
29
+ }));
30
+ function disposeFileNavigation() {
31
+ disposed = true; observer.disconnect(); if (frame !== null) cancelAnimationFrame(frame);
32
+ for (const item of mounted.values()) item.session.dispose(); mounted.clear(); style.remove();
33
+ }
34
+ return disposeFileNavigation;