minecodex 2.0.8 → 2.0.10

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 +18 -4
  2. package/apps/dashboard/app.js +59 -10
  3. package/apps/dashboard/assets/diagnostics.js +18 -0
  4. package/apps/dashboard/index.html +5 -0
  5. package/apps/dashboard/styles.css +5 -1
  6. package/assets/brand/README.md +2 -2
  7. package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +2 -0
  8. package/features/images/src/adapters/codex/image-sources.mjs +8 -1
  9. package/features/images/src/adapters/codex/native-image-source.mjs +2 -2
  10. package/features/images/src/frontend/local-image-source.mjs +7 -0
  11. package/features/images/src/library-prompt-page.mjs +9 -0
  12. package/features/model-slider/src/adapters/codex/controller.mjs +14 -1
  13. package/features/model-slider/src/frontend/install.mjs +4 -1
  14. package/features/notes/src/frontend/entry.mjs +10 -0
  15. package/features/session-status/src/session-status.mjs +11 -0
  16. package/features/sidebar-focus/src/sidebar-focus.mjs +8 -0
  17. package/features/sites/src/frontend/entry.mjs +8 -0
  18. package/features/usage-meter/src/adapters/codex/renderer.mjs +2 -1
  19. package/features/usage-meter/src/usage-meter.mjs +8 -0
  20. package/package.json +1 -14
  21. package/packages/cli/src/commands.mjs +6 -1
  22. package/packages/cli/src/control-server.mjs +4 -1
  23. package/packages/cli/src/platform.mjs +10 -0
  24. package/packages/cli/src/runtime-manager.mjs +11 -3
  25. package/packages/codex-adapter/src/renderer/environment.mjs +6 -1
  26. package/packages/runtime-host/src/codex-injection.mjs +49 -16
  27. package/packages/runtime-host/src/codex-runtime.mjs +51 -0
  28. package/packages/runtime-host/src/feature-registry.mjs +2 -3
  29. package/packages/runtime-host/src/main.mjs +8 -4
  30. package/packages/runtime-host/src/renderer-self-check.mjs +59 -0
  31. package/packages/ui/src/components/composer-menu.mjs +7 -2
  32. package/packages/ui/src/native/theme.mjs +16 -1
  33. package/DESIGN.md +0 -192
  34. package/apps/dashboard/DESIGN.md +0 -67
  35. package/features/hide-upsell-banner/README.md +0 -30
  36. package/features/images/DESIGN.md +0 -151
  37. package/features/images/README.md +0 -37
  38. package/features/model-slider/DESIGN.md +0 -36
  39. package/features/model-slider/README.md +0 -45
  40. package/features/notes/DESIGN.md +0 -74
  41. package/features/notes/README.md +0 -33
  42. package/features/session-status/DESIGN.md +0 -45
  43. package/features/session-status/README.md +0 -43
  44. package/features/sidebar-focus/DESIGN.md +0 -17
  45. package/features/sites/DESIGN.md +0 -82
  46. package/features/sites/README.md +0 -31
  47. package/features/unread-inbox/DESIGN.md +0 -41
  48. package/features/unread-inbox/codex-feature.json +0 -23
  49. package/features/unread-inbox/src/adapters/codex/modules.mjs +0 -58
  50. package/features/unread-inbox/src/adapters/codex/renderer.mjs +0 -198
  51. package/features/unread-inbox/src/domain/notifications.mjs +0 -21
  52. package/features/unread-inbox/src/injection-source.mjs +0 -9
  53. package/features/unread-inbox/src/unread-inbox.mjs +0 -307
  54. package/features/usage-meter/DESIGN.md +0 -49
  55. package/features/usage-meter/README.md +0 -19
  56. package/packages/cli/native/README.md +0 -49
  57. package/packages/codex-adapter/README.md +0 -16
  58. package/packages/runtime-host/README.md +0 -213
  59. package/packages/ui/DESIGN.md +0 -38
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # MineCodex
6
6
 
7
- MineCodex 给 Codex Desktop 增加一组轻量、本地优先的扩展功能。所有数据均保存在本地,不经过任何第三方服务器,也不会改变 Codex 的原有使用习惯。
7
+ MineCodex 给 Codex Desktop 增加一组轻量、本地优先的扩展功能。配置、提示词和图片索引保存在本机。Images 的本地浏览不上传图片;使用云端收藏、加入文件夹或部分生成编辑操作时,所选图片会通过 Codex 原生能力上传至 OpenAI。
8
8
 
9
9
  ## 插件列表
10
10
 
@@ -16,6 +16,8 @@ MineCodex 给 Codex Desktop 增加一组轻量、本地优先的扩展功能。
16
16
  | **Images** | 在原生 Images 中浏览本地 Image Gen 图片,查看并复制提示词 |
17
17
  | **Hide Upsell Banner** | 隐藏输入框上方的用量提示横幅,仅优化界面展示,不影响正常的用量统计 |
18
18
  | **Session Status** | 在输入框下方查看当前任务的步骤、观测速度、Token 用量与上下文明细 |
19
+ | **Sidebar Focus** | 展开侧栏时聚焦当前任务路径,再次点击恢复布局 |
20
+ | **Sites** | 在原生 Sites 中收录项目网页与自定义网址,按需打开或启动 |
19
21
 
20
22
  ## 安装
21
23
 
@@ -53,6 +55,18 @@ mcx open
53
55
 
54
56
  运行 `mcx uninstall` 会移除名为 MineCodex 的后台项目,并默认保留配置设置、Notes 笔记和 Images 图片索引;只有显式加上 `--purge` 参数,才会清理 MineCodex 产生的本地配置。MineCodex 绝不会删除 `~/.codex/generated_images` 中的原始图片或 Notes 引用的本地文件。
55
57
 
58
+ ## 数据与图片操作
59
+
60
+ 本地 Image Gen 浏览、搜索、提示词查看和下载不会创建云端图片。首次收藏或加入文件夹时,会将所选图片上传到 OpenAI 的 Library;收藏和文件夹关系由云端保存。首次上传后取消文件夹选择,已产生的云端副本仍会保留,界面会提示“图片已保存到 Library,但未加入文件夹”。
61
+
62
+ 图片生成编辑可能通过原生能力准备临时上传附件;查看图片不会触发这类操作。从 Image Gen 删除仅隐藏本地索引条目,保留原始图片和云端副本。卸载或 `--purge` 不会删除云端图片,也不会删除 Codex 的原始生成图片。
63
+
64
+ ## 更新后的自检
65
+
66
+ MineCodex 在每个 Codex 窗口启动并完成注入后,后台检查一次功能安装、原生锚点与实际使用的设计变量。新窗口、页面重载或重新接入时重新检查;结果代表检查时的页面,不持续监测后续页面切换。控制台区分“正在自检”“部分窗口可用”“外观适配需要更新”“暂不可用”和“当前页面不适用”。没有输入框或未打开 Sites / Images 时,相关功能可能不适用,并不直接表示版本不兼容。
67
+
68
+ 可使用控制台的“复制诊断信息”提供自检结果。诊断包含功能、窗口、锚点与设计变量证据,不包含提示词、聊天内容、账号资料或安装路径。自检可用表示当前检查项通过,实际保存、上传、模型切换等操作仍以各自的结果为准。
69
+
56
70
  ## 工作原理
57
71
 
58
72
  MineCodex 统一管理 Codex 内的菜单入口、主题风格与界面适配。Model Slider、Prompts 各自管理操作界面,Images 在原生 Images 中接入本地生成图片和提示词查看;Session Status 只读汇总本机 Codex 会话事件,Hide Upsell Banner 优化界面展示。所有功能按需开启,关闭后即停止运行。
@@ -67,16 +81,16 @@ npm run pack:check
67
81
  npm run pack:audit
68
82
  ```
69
83
 
70
- 维护者发布流程见 [GitHub Actions 发布说明](docs/npm-publishing.md)。发布工作流仅允许维护者本人触发,使用 npm 可信发布,无需长期访问令牌。
84
+ 维护者发布流程见 [GitHub Actions 发布说明](https://github.com/Vontean/MineCodex/blob/main/docs/npm-publishing.md)。发布工作流仅允许维护者本人触发,使用 npm 可信发布,无需长期访问令牌。
71
85
 
72
86
  ## 许可
73
87
 
74
88
  MineCodex 是非官方独立扩展项目,与 OpenAI 无关联。项目自有代码以 MIT License 授权;第三方图标、品牌和兼容资产遵循各自的权利说明,见 [`features/model-slider/THIRD_PARTY_NOTICES.md`](features/model-slider/THIRD_PARTY_NOTICES.md) 与 [`features/session-status/vendor/lucide-LICENSE`](features/session-status/vendor/lucide-LICENSE)。
75
89
 
76
- ### Sidebar Focus / 侧栏聚焦
90
+ ## 侧栏聚焦
77
91
 
78
92
  展开侧栏时,点击铃铛与搜索之间的聚焦按钮,保留当前任务路径并收起其他分支。再次点击恢复原先布局;切换任务时跟随路径,手动调整分支时退出聚焦。
79
93
 
80
- ### 从账号菜单打开控制台
94
+ ## 从账号菜单打开控制台
81
95
 
82
96
  在 Codex Navbar 底部打开账号菜单,点击 Settings 下方的 **MineCodex ↗**,即可在外部默认浏览器打开本地控制台,等同于 `mcx gui`。macOS 默认 Safari、Chrome/Chromium、Edge、Brave 和 Vivaldi 会定位已有控制台标签页,没有时才新建;首次定位可能需要系统自动化权限。其他浏览器与 Windows 仍由系统打开 URL。
@@ -1,4 +1,8 @@
1
- const locale = navigator.language.toLowerCase().startsWith("zh") ? "zh-CN" : "en";
1
+ import {diagnosticReport} from "./assets/diagnostics.js";
2
+ let savedLocale;
3
+ try { savedLocale = localStorage.getItem("minecodex-language"); } catch { /* 存储不可用时跟随浏览器语言。 */ }
4
+ const locale = ["en", "zh-CN"].includes(savedLocale) ? savedLocale
5
+ : navigator.language.toLowerCase().startsWith("zh") ? "zh-CN" : "en";
2
6
  document.documentElement.lang = locale;
3
7
 
4
8
  const copy = {
@@ -19,8 +23,13 @@ const copy = {
19
23
  configured: "Configured — starting in Codex…",
20
24
  launching: "Launching in Codex…",
21
25
  injected: "Active in Codex",
22
- healthy: "Healthy in Codex",
23
- failed: "Failed — restart Codex",
26
+ healthy: "Active in Codex",
27
+ checking: "Checking compatibility…",
28
+ partial: "Available in some windows",
29
+ degraded: "Appearance needs an update",
30
+ unavailable: "Temporarily unavailable — check for updates",
31
+ "not-applicable": "Not needed on this page",
32
+ failed: "Couldn’t start — try again",
24
33
  stopped: "Stopped",
25
34
  "restart-required": "Restart required — restart Codex",
26
35
  },
@@ -31,13 +40,16 @@ const copy = {
31
40
  darkAppearance: "Dark appearance",
32
41
  lightAppearance: "Light appearance",
33
42
  updateFailed: "Couldn’t check for updates. Please try again.",
43
+ copyDiagnostics: "Copy diagnostics",
44
+ diagnosticsCopied: "Diagnostics copied",
45
+ diagnosticsFailed: "Couldn’t copy diagnostics. Please try again.",
34
46
  checkUpdates: "Check for updates",
35
47
  checkingUpdates: "Checking…",
36
48
  current: "You’re up to date.",
37
49
  updateAvailable: (version) => `MineCodex ${version} is available. Run mcx update in Terminal.`,
38
50
  changeFailed: "The plugin setting could not be saved.",
39
- restartFailed: "MineCodex could not restart Codex. Check the service log and try again.",
40
- failureDetail: "Health or renderer status could not be confirmed.",
51
+ restartFailed: "MineCodex could not restart Codex. Please try again or copy diagnostics.",
52
+ failureDetail: "This extension could not be confirmed. Copy diagnostics to help investigate.",
41
53
  },
42
54
  "zh-CN": {
43
55
  connecting: "正在连接…",
@@ -56,8 +68,13 @@ const copy = {
56
68
  configured: "已配置——正在 Codex 中启动…",
57
69
  launching: "正在 Codex 中启动…",
58
70
  injected: "已在 Codex 中生效",
59
- healthy: "Codex 中运行正常",
60
- failed: "运行失败——请重启 Codex",
71
+ healthy: "已在 Codex 中生效",
72
+ checking: "正在自检…",
73
+ partial: "部分窗口可用",
74
+ degraded: "外观适配需要更新",
75
+ unavailable: "暂不可用——请检查更新",
76
+ "not-applicable": "当前页面不适用",
77
+ failed: "暂时无法启动——请重试",
61
78
  stopped: "已停止",
62
79
  "restart-required": "需要重启——请重启 Codex",
63
80
  },
@@ -68,13 +85,16 @@ const copy = {
68
85
  darkAppearance: "深色外观",
69
86
  lightAppearance: "浅色外观",
70
87
  updateFailed: "暂时无法检查更新,请稍后重试。",
88
+ copyDiagnostics: "复制诊断信息",
89
+ diagnosticsCopied: "诊断信息已复制",
90
+ diagnosticsFailed: "暂时无法复制诊断信息,请重试。",
71
91
  checkUpdates: "检查更新",
72
92
  checkingUpdates: "正在检查…",
73
93
  current: "当前已是最新版本。",
74
94
  updateAvailable: (version) => `发现 MineCodex ${version},请在终端运行 mcx update。`,
75
95
  changeFailed: "插件设置保存失败。",
76
- restartFailed: "MineCodex 无法重启 Codex。请检查服务日志后重试。",
77
- failureDetail: "无法确认插件健康状态或 Codex 渲染器状态。",
96
+ restartFailed: "MineCodex 无法重启 Codex。请重试或复制诊断信息协助排查。",
97
+ failureDetail: "暂时无法确认这项功能的状态,可复制诊断信息协助排查。",
78
98
  },
79
99
  }[locale];
80
100
 
@@ -94,10 +114,13 @@ const elements = {
94
114
  retryButton: document.querySelector("#retry-button"),
95
115
  versionLabel: document.querySelector("#version-label"),
96
116
  versionValue: document.querySelector("#version-value"),
117
+ languageToggle: document.querySelector(".language-toggle"),
118
+ languageButtons: [...document.querySelectorAll("[data-language-choice]")],
97
119
  themeToggle: document.querySelector(".theme-toggle"),
98
120
  themeButtons: [...document.querySelectorAll("[data-theme-choice]")],
99
121
  updateResult: document.querySelector("#update-result"),
100
122
  updateButton: document.querySelector("#update-button"),
123
+ diagnosticsButton: document.querySelector("#diagnostics-button"),
101
124
  toast: document.querySelector("#toast"),
102
125
  };
103
126
 
@@ -106,6 +129,8 @@ let toastTimer = null;
106
129
  const pendingFeatureIds = new Set();
107
130
 
108
131
  function applyStaticCopy() {
132
+ elements.languageToggle.setAttribute("aria-label", locale === "zh-CN" ? "语言" : "Language");
133
+ for (const button of elements.languageButtons) button.setAttribute("aria-pressed", String(button.dataset.languageChoice === locale));
109
134
  elements.runtimeLabel.textContent = copy.connecting;
110
135
  elements.restartTitle.textContent = copy.restartTitle;
111
136
  elements.restartCopy.textContent = copy.restartCopy;
@@ -115,6 +140,7 @@ function applyStaticCopy() {
115
140
  elements.errorCopy.textContent = copy.loadErrorCopy;
116
141
  elements.retryButton.textContent = copy.retry;
117
142
  elements.versionLabel.textContent = copy.version;
143
+ elements.diagnosticsButton.textContent = copy.copyDiagnostics;
118
144
  elements.updateButton.querySelector(".button-label").textContent = copy.checkUpdates;
119
145
  }
120
146
 
@@ -186,6 +212,7 @@ function createPluginCard(feature) {
186
212
  }
187
213
 
188
214
  function updatePluginCard(card, feature) {
215
+ for (const button of elements.languageButtons) button.disabled = pendingFeatureIds.size > 0;
189
216
  const status = featureState(feature);
190
217
  const input = card.querySelector('input[type="checkbox"]');
191
218
  const image = card.querySelector(".plugin-icon img");
@@ -285,7 +312,10 @@ async function load() {
285
312
  try {
286
313
  const response = await fetch("/api/status", { headers: { "Accept-Language": locale } });
287
314
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
288
- state = await response.json();
315
+ const fresh = await response.json();
316
+ if (pendingFeatureIds.size && state) fresh.features = fresh.features.map(feature =>
317
+ pendingFeatureIds.has(feature.id) ? state.features.find(previous=>previous.id === feature.id) ?? feature : feature);
318
+ state = fresh;
289
319
  elements.pluginList.hidden = false;
290
320
  render();
291
321
  } catch {
@@ -359,6 +389,16 @@ elements.retryButton.addEventListener("click", () => {
359
389
  void load();
360
390
  });
361
391
 
392
+ elements.diagnosticsButton.addEventListener("click", async () => {
393
+ try {
394
+ await navigator.clipboard.writeText(JSON.stringify(diagnosticReport(state),null,2));
395
+ showToast(copy.diagnosticsCopied);
396
+ } catch { showToast(copy.diagnosticsFailed); }
397
+ });
398
+
399
+ // 页面可见时更新自检结果;保留正在保存的卡片和键盘焦点。
400
+ setInterval(() => { if (!document.hidden && !pendingFeatureIds.size) void load(); }, 5000);
401
+
362
402
  elements.updateButton.addEventListener("click", async () => {
363
403
  elements.updateButton.disabled = true;
364
404
  elements.updateButton.querySelector(".button-label").textContent = copy.checkingUpdates;
@@ -397,6 +437,15 @@ elements.themeToggle.addEventListener("click", (event) => {
397
437
  try { localStorage.setItem("minecodex-appearance", selectedTheme); } catch { /* 保留内存中的选择。 */ }
398
438
  applyTheme();
399
439
  });
440
+ elements.languageToggle.addEventListener("click", (event) => {
441
+ const button = event.target.closest("[data-language-choice]");
442
+ if (!button || pendingFeatureIds.size || elements.restartButton.disabled) return;
443
+ const choice = button.dataset.languageChoice;
444
+ if (choice === locale) return;
445
+ try { localStorage.setItem("minecodex-language", choice); } catch { return; }
446
+ // 重载后以同一语言请求卡片内容,不影响功能配置与外观选择。
447
+ location.reload();
448
+ });
400
449
  systemTheme.addEventListener("change", applyTheme);
401
450
  document.addEventListener("keydown", () => { document.documentElement.dataset.input = "keyboard"; });
402
451
  document.addEventListener("pointerdown", () => { document.documentElement.dataset.input = "pointer"; });
@@ -0,0 +1,18 @@
1
+ // 只复制自检证据;不包含账号、路径、会话内容、绑定令牌或原始错误文本。
2
+ export function diagnosticReport(state) {
3
+ return {
4
+ minecodexVersion:state?.version ?? null,
5
+ codexBuild:state?.renderer?.codexBuild ?? state?.runtime?.codex?.version ?? null,
6
+ buildSource:state?.renderer?.buildSource ?? null,
7
+ verification:"runtime-observation-only",
8
+ targets:(state?.renderer?.targets ?? []).map(target => ({
9
+ targetId:target.targetId,checkedAt:target.checkedAt,reason:target.reason ?? null,
10
+ tokens:target.tokens,capabilities:target.capabilities,
11
+ plugins:(target.plugins ?? []).map(plugin => ({
12
+ featureId:plugin.featureId,installation:plugin.installation,state:plugin.state,reason:plugin.reason,
13
+ checks:plugin.checks,tokens:plugin.tokens,
14
+ })),
15
+ })),
16
+ services:(state?.features ?? []).map(feature => ({featureId:feature.id,state:feature.state,failureCode:feature.failure?.code ?? null})),
17
+ };
18
+ }
@@ -18,6 +18,10 @@
18
18
  <h1><span class="brand-icon" aria-hidden="true"></span><span class="brand-name">MineCodex</span></h1>
19
19
  </div>
20
20
  <div class="header-actions">
21
+ <div class="language-toggle" role="group" aria-label="Language">
22
+ <button data-language-choice="zh-CN" type="button" lang="zh-CN" aria-pressed="false">中文</button>
23
+ <button data-language-choice="en" type="button" lang="en" aria-pressed="false">EN</button>
24
+ </div>
21
25
  <div class="theme-toggle" role="group" aria-label="Appearance">
22
26
  <span class="theme-selection" aria-hidden="true"></span>
23
27
  <button data-theme-choice="light" type="button" aria-label="Light appearance" aria-pressed="false"><span class="ui-icon sun-icon" aria-hidden="true"></span></button>
@@ -67,6 +71,7 @@
67
71
  <dl>
68
72
  <div><dt id="version-label">Version</dt><dd id="version-value">—</dd></div>
69
73
  </dl>
74
+ <button id="diagnostics-button" class="button quiet" type="button">Copy diagnostics</button>
70
75
  <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>
71
76
  </footer>
72
77
  <p id="update-result" class="update-result" role="status" hidden></p>
@@ -145,7 +145,11 @@ h2 { font-size: 14px; font-weight: 620; }
145
145
  }
146
146
 
147
147
  .overview { margin: 0 0 32px; }
148
- .header-actions { flex-shrink: 0; }
148
+ .header-actions { display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
149
+ .language-toggle { display: flex; gap: 4px; padding: 4px; border-radius: 999px; background: var(--surface-muted); box-shadow: inset 0 0 0 1px var(--border); }
150
+ .language-toggle button { min-width: 40px; height: 32px; padding: 0 8px; border: 0; border-radius: 999px; background: transparent; color: var(--ink-secondary); font: inherit; font-size: 12px; cursor: pointer; }
151
+ .language-toggle button[aria-pressed="true"] { background: var(--surface); color: var(--ink); box-shadow: 0 1px 3px #0002; }
152
+ .language-toggle button:disabled { cursor: wait; }
149
153
 
150
154
  .runtime-state, .plugin-state {
151
155
  display: inline-flex;
@@ -1,6 +1,6 @@
1
1
  # MineCodex 图标
2
2
 
3
- `minecodex-icon.svg` 是透明底、单色的可编辑矢量图标,使用 `viewBox="0 0 24 24"` 和 `currentColor`。外圈与镐子分别位于 `codex-outline`、`pickaxe`,可单独编辑。`minecodex-icon.png` 是 512×512 透明底预览;`minecodex-icon-preview.png` 展示已选实心版的明暗主题及 16/24/32/48px 尺寸。当前主文件为实心版;`minecodex-icon-filled.svg` 是实心版副本,`minecodex-icon-outline.svg` 保留上一版空心设计。
3
+ `minecodex-icon.svg` 是透明底、单色的可编辑矢量图标,使用 `viewBox="0 0 24 24"` 和 `currentColor`。外圈与镐子分别位于 `codex-outline`、`pickaxe`,可单独编辑。本机 PNG 预览用于透明底、明暗主题及 16/24/32/48px 尺寸对比,已加入 Git 忽略规则。当前主文件为实心版;`minecodex-icon-filled.svg` 是实心版副本,`minecodex-icon-outline.svg` 保留上一版空心设计。
4
4
 
5
5
  ## 设计与来源
6
6
 
@@ -16,4 +16,4 @@
16
16
 
17
17
  `minecodex-icon.svg` 是唯一可编辑的生产源;运行 `npm run build:icons` 生成明暗 SVG、Dashboard favicon 和 README 顶部标识。`minecodex-icon-light.svg` 用于浅色背景,`minecodex-icon-dark.svg` 用于深色背景。Dashboard 标题通过 favicon 的 CSS mask 消费同一几何,颜色继承页面文字色。主题 SVG 与生产源、Lucide 许可、README 展示图均进入 npm 发布名单。
18
18
 
19
- 历史空心版和 PNG 对比图仅用于设计参考,不进入 npm 发布包。macOS 后台服务 1.0.1 已接入 ICNS 图标;`minecodex-service-icon.svg` 为同一几何配浅色圆角底板的原生图标源,更新遵循原生服务文档的注销、替换和重新注册流程。
19
+ 历史空心版仅用于设计参考,不进入 npm 发布包;PNG 对比图保留本机,不进入 GitHub 或 npm 发布包。macOS 后台服务 1.0.1 已接入 ICNS 图标;`minecodex-service-icon.svg` 为同一几何配浅色圆角底板的原生图标源,更新遵循原生服务文档的注销、替换和重新注册流程。
@@ -83,6 +83,8 @@ observer.observe(document.documentElement, {
83
83
  characterData: true,
84
84
  });
85
85
 
86
+ config.registerSelfCheck?.(() => ({state:hiddenBanners.size ? "available" : "not-applicable",reason:hiddenBanners.size ? null : "no-usage-banner",
87
+ checks:[...hiddenBanners.keys()].filter(node=>node.isConnected).map(()=>({name:"usage-banner-hidden",available:true}))}));
86
88
  return () => {
87
89
  disposed = true;
88
90
  observer.disconnect();
@@ -3,7 +3,14 @@ export function createImageSourcesAdapter(codex) {
3
3
  && props.tabs?.some(item => item.key === "generated")
4
4
  && props.tabs?.some(item => item.key === "uploaded");
5
5
 
6
- return {read() {
6
+ function applicability() {
7
+ const props=codex.props(codex.rail(),value=>Boolean((value.router ?? value.value?.router)?.state?.location));
8
+ const location=(props?.router ?? props?.value?.router)?.state?.location;
9
+ if (!location) return "unknown";
10
+ return location.pathname === "/space/files" && new URLSearchParams(location.search).get("tab") === "images"
11
+ ? "applicable" : "not-applicable";
12
+ }
13
+ return {applicability,read() {
7
14
  const tabs = [...document.querySelectorAll('[role="tablist"]')].find(node =>
8
15
  !node.closest("[data-minecodex-image-navigation]")
9
16
  && codex.props(node, isImageNavigation));
@@ -423,13 +423,13 @@ export function createNativeImageSource(codex, { requestHostAction, libraryApi,
423
423
  {id:"view-prompts", icon:promptIcon, text:context.intl.formatMessage({id:"minecodex.images.viewPrompts",defaultMessage:"View prompts"}),onSelect:() => {void open(image).then(opened=>{if(opened&&isCurrent())onPrompt(image);}).catch(operationError);}},
424
424
  {id:"favorite",icon:native.favoriteIcon(saved),text:saved
425
425
  ? message("library.favorites.remove","Remove from Favorites")
426
- : message("library.favorites.add","Add to Favorites"),onSelect:() => {void perform(async () => {
426
+ : message("minecodex.images.addToCloudFavorites",(context.intl.locale?.startsWith("zh") ? "加入收藏(上传至 Library)" : "Add to Favorites (uploads to Library)")),onSelect:() => {void perform(async () => {
427
427
  favoriteRevision.current++;
428
428
  const value=await cloud.favorite(image);
429
429
  if(active.current&&isCurrent()){favoriteRevision.current++;setSaved(value);}
430
430
  });}},
431
431
  action("download-a-copy", native.downloadIcon, {id:"libraryNext.actions.download", defaultMessage:"Download"}),
432
- {id:"folder",icon:native.folderIcon,text:message("libraryNext.actions.addToFolder","Add to folder"),onSelect:() => {void perform(() => folder(image));}},
432
+ {id:"folder",icon:native.folderIcon,text:message("minecodex.images.addToCloudFolder",(context.intl.locale?.startsWith("zh") ? "加入文件夹(上传至 Library)" : "Add to folder (uploads to Library)")),onSelect:() => {void perform(() => folder(image));}},
433
433
  {id:"delete",icon:native.deleteIcon,tone:"danger",text:message("libraryNext.actions.delete","Delete"),onSelect:()=>confirmRemove([image])},
434
434
  ].filter(Boolean);
435
435
  const dropdown = h(native.Dropdown, {
@@ -188,6 +188,13 @@ export function installLocalImageSource(config, signal, sources, native) {
188
188
  signal.addEventListener("abort", cleanup, {once:true});
189
189
  return {
190
190
  cleanup,
191
+ selfCheck() {
192
+ const entry=sources.read(),scope=sources.applicability();
193
+ if (!entry) return {state:scope === "applicable" ? "unavailable" : scope,reason:scope === "not-applicable" ? "outside-image-sources" : "image-sources-anchor-unavailable"};
194
+ const available=Boolean(mounted?.nav && mounted?.view && mounted.navigation.isConnected);
195
+ return {state:available ? "available" : mounted ? "unknown" : "unavailable",reason:available ? null : "native-image-mount-unavailable",
196
+ checks:[{name:"image-sources-anchor",available:true},{name:"native-image-mount",available}],scopes:[{name:"image-sources",element:entry.shell}]};
197
+ },
191
198
  readViewer:header => native.readViewer(header),
192
199
  pendingPrompt:() => pendingPrompt,
193
200
  consumePrompt:() => {pendingPrompt = null;},
@@ -262,6 +262,15 @@ localeObserver.observe(document.documentElement, { attributes:true, attributeFil
262
262
  localImageSource = installLocalImageSource(config, signal, createImageSourcesAdapter(config.codex), createNativeImageSource(config.codex, {
263
263
  requestHostAction:config.requestHostAction, libraryApi, createCloudImageActions, promptGlyphSource, signal,
264
264
  }));
265
+ config.registerSelfCheck?.(() => {
266
+ const source=localImageSource.selfCheck();
267
+ if (source.state !== "not-applicable") return source;
268
+ const header=document.querySelector('[data-testid="viewer-header"]');
269
+ if (!header || !libraryImage(header)) return source;
270
+ const available=Boolean(mounted?.button.isConnected);
271
+ return {state:available ? "available" : "unknown",reason:available ? null : "image-viewer-mount-pending",
272
+ checks:[{name:"image-viewer-anchor",available:true},{name:"prompt-control",available}],scopes:[{name:"image-viewer",element:header}]};
273
+ });
265
274
  const observer = new MutationObserver(() => {
266
275
  if (scheduled) return;
267
276
  scheduled = true;
@@ -89,5 +89,18 @@ function invokeNativeMenuItemSelect(item) {
89
89
  codex.report("fast-icons",Boolean(active && inactive),active && inactive ? null : "icon-resource-contract-changed");
90
90
  return {active:{path:active,viewBox:"0 0 24 24"},inactive:{path:inactive,viewBox:"0 0 20 20"}};
91
91
  }
92
- return {fastIcons,trigger:nativeComposerTrigger,read:nativeComposerModelController,descriptor:nativeModelDescriptorForItem,setOpen:setReactControlledMenuOpen,selectItem:invokeNativeMenuItemSelect};
92
+ function selfCheck() {
93
+ const trigger=nativeComposerTrigger();
94
+ if (!trigger) {
95
+ const path=codex.route();
96
+ const applicable=Boolean(codex.composer(null,{threadOnly:false})) || path?.startsWith("/local/");
97
+ return {state:applicable ? "unavailable" : path ? "not-applicable" : "unknown",reason:applicable ? "model-trigger-unavailable" : "no-task-composer",checks:[{name:"model-trigger",available:false}]};
98
+ }
99
+ const generic=codex.props(trigger,p=>!!p.selectedModel && typeof p.onModelChange === "function",40);
100
+ if (generic) return {state:"not-applicable",reason:"chat-model-picker"};
101
+ const available=Boolean(nativeComposerModelController());
102
+ return {state:available ? "available" : "unavailable",reason:available ? null : "model-controller-unavailable",
103
+ scopes:[{name:"model-trigger",element:trigger}],checks:[{name:"model-trigger",available:true},{name:"model-controller",available}]};
104
+ }
105
+ return {selfCheck,fastIcons,trigger:nativeComposerTrigger,read:nativeComposerModelController,descriptor:nativeModelDescriptorForItem,setOpen:setReactControlledMenuOpen,selectItem:invokeNativeMenuItemSelect};
93
106
  }
@@ -2173,5 +2173,8 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
2173
2173
  void loadNativeFastIconFromBundle();
2174
2174
  }
2175
2175
 
2176
- return { ensure: ensureModelSelector, dispose: restoreModelSelectorEnhancements };
2176
+ return { selfCheck: () => {
2177
+ const result=nativeModel.selfCheck();
2178
+ return {...result,scopes:[...(result.scopes ?? []),{name:"model-menu",element:modelSelectorControllerMenu?.menu}]};
2179
+ }, ensure: ensureModelSelector, dispose: restoreModelSelectorEnhancements };
2177
2180
  }
@@ -1376,6 +1376,16 @@ document.addEventListener(
1376
1376
  window.addEventListener("resize", position, { signal });
1377
1377
  window.addEventListener("scroll", position, { signal, capture: true });
1378
1378
  sync();
1379
+
1380
+ config.registerSelfCheck?.(() => {
1381
+ const rail=config.codex.railSlot(ownNavGroup), composer=config.codex.composer(null,{purpose:"insertion"});
1382
+ const checks=[{name:"prompts-navigation-anchor",available:Boolean(rail && navButton?.isConnected)}];
1383
+ if (composer) checks.push({name:"quick-prompts-anchor",available:Boolean(composer.add && button?.isConnected && composerRoot === composer.root)});
1384
+ const available=checks.every(check=>check.available);
1385
+ return {state:available ? "available" : "unavailable",reason:available ? null : "prompts-anchor-unavailable",checks,
1386
+ scopes:[{name:"navigation",element:navButton},{name:"composer",element:composer?.root},{name:"prompts-page",element:page},{name:"prompts-popup",element:pagePopup},{name:"quick-prompts",element:panel}]};
1387
+ });
1388
+
1379
1389
  return () => {
1380
1390
  disposed = true;
1381
1391
  observer.disconnect();
@@ -612,6 +612,17 @@ instanceObserver.observe(document.documentElement, {
612
612
  attributeFilter:["class", "style", "hidden", "inert", "aria-hidden", "data-composer-placement"],
613
613
  });
614
614
  syncInstances();
615
+ config.registerSelfCheck?.(() => {
616
+ const surfaces=[...document.querySelectorAll('[data-codex-composer-root]')].map(root=>config.codex.composerSurface(root)).filter(Boolean);
617
+ if (!surfaces.length) {
618
+ const path=config.codex.route();
619
+ return {state:path?.startsWith("/local/") ? "unavailable" : path ? "not-applicable" : "unknown",reason:"no-thread-surface"};
620
+ }
621
+ const identity=surfaces.every(surface=>!!surface.threadId),mounted=surfaces.every(surface=>instances.has(surface.root));
622
+ const available=identity && mounted;
623
+ return {state:available ? "available" : "unavailable",reason:available ? null : "thread-identity-or-status-anchor-unavailable",
624
+ checks:[{name:"thread-identity",available:identity},{name:"session-status-mount",available:mounted}],scopes:surfaces.map((surface,index)=>({name:`composer-${index+1}`,element:surface.root}))};
625
+ });
615
626
  return () => {
616
627
  instanceObserver.disconnect();
617
628
  for (const instance of instances.values()) instance.dispose();
@@ -86,4 +86,12 @@ observer.observe(document.documentElement, { subtree:true, childList:true, attri
86
86
  "data-app-action-sidebar-section-collapsed", "data-above-composer-conversation-id", "lang"
87
87
  ] });
88
88
  reconcile();
89
+ config.registerSelfCheck?.(() => {
90
+ const sidebar=document.querySelector("#app-shell-sidebar");
91
+ if (!sidebar) return {state:"unknown",reason:"sidebar-anchor-unavailable"};
92
+ if (!sidebar.checkVisibility({visibilityProperty:true})) return {state:"not-applicable",reason:"sidebar-hidden"};
93
+ const available=Boolean(button?.isConnected);
94
+ return {state:available ? "available" : "unavailable",reason:available ? null : "sidebar-focus-anchor-unavailable",
95
+ checks:[{name:"sidebar-focus-anchor",available}],scopes:[{name:"sidebar",element:sidebar}]};
96
+ });
89
97
  return () => { disposed = true; clearTimeout(timer); observer.disconnect(); button?.remove(); };
@@ -413,6 +413,14 @@ const observer = new MutationObserver(records => { if (records.some(r => !r.targ
413
413
  observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["aria-pressed"] });
414
414
  document.addEventListener("input", schedule); document.addEventListener("click", onClick, true); document.addEventListener("keydown", onKey, true); document.addEventListener("pointerdown", onPointer, true);
415
415
  refresh().catch(e => notice(e.message)); schedule();
416
+ config.registerSelfCheck?.(() => {
417
+ const path=nativeSites.path();
418
+ if (!path) return {state:"unknown",reason:"navigation-context-unavailable"};
419
+ if (path !== "/space/sites") return {state:"not-applicable",reason:"outside-sites-page"};
420
+ const page=nativeSites.pageInfo(),available=Boolean(page?.create && addButton?.isConnected);
421
+ return {state:available ? "available" : "unavailable",reason:available ? null : "sites-anchor-unavailable",
422
+ checks:[{name:"sites-page",available:Boolean(page)},{name:"sites-create-anchor",available}],scopes:[{name:"sites-page",element:page?.page},{name:"sites-dialog",element:dialog},{name:"sites-menu",element:menu}]};
423
+ });
416
424
  return () => {
417
425
  disposed = true; pendingLayout = null; clearTimeout(timer); observer.disconnect();
418
426
  document.removeEventListener("input", schedule); document.removeEventListener("click", onClick, true); document.removeEventListener("keydown", onKey, true); document.removeEventListener("pointerdown", onPointer, true);
@@ -53,7 +53,8 @@ function avatarAnchor(avatar, rail) {
53
53
  const windows=currentWindows(context), avatar=findAvatar(rail);
54
54
  const available=Boolean(avatar && context.router && windows);
55
55
  codex.report("usage-meter",available,available ? null : "account-quota-or-anchor-unavailable");
56
- return { rail,avatar,windows,available };
56
+ return { rail,avatar,windows,available,anchorAvailable:Boolean(avatar && context.router),
57
+ applicable:context.auth?.isLoading ? null : context.auth ? ["chatgpt","chatgptAuthTokens"].includes(context.auth.authMethod) : null };
57
58
  }
58
59
  function open(rail) { const {router}=nativeContext(rail); return router?.navigate("/settings/usage?tab=overview",{state:router.state?.location?.state}); }
59
60
  signal.addEventListener("abort",()=>unsubscribe?.(),{once:true});
@@ -158,4 +158,12 @@ const cleanup = () => {
158
158
  };
159
159
  signal.addEventListener("abort", cleanup, { once: true });
160
160
  sync();
161
+ config.registerSelfCheck?.(() => {
162
+ const snapshot=nativeUsage.read();
163
+ const available=Boolean(snapshot.available && record?.button.isConnected);
164
+ return {state:available ? "available" : snapshot.applicable === false ? "not-applicable" : !snapshot.anchorAvailable && snapshot.rail ? "unavailable" : "unknown",reason:available ? null : "quota-data-or-anchor-unavailable",
165
+ checks:[{name:"account-anchor",available:Boolean(snapshot.avatar)},{name:"account-quota",available:Boolean(snapshot.windows)},{name:"usage-meter-mount",available:Boolean(record?.button.isConnected)}],
166
+ scopes:[{name:"account",element:snapshot.avatar}]};
167
+ });
168
+
161
169
  return cleanup;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "2.0.8",
3
+ "version": "2.0.10",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,8 +27,6 @@
27
27
  "features/notes/assets",
28
28
  "features/notes/vendor/phosphor-LICENSE",
29
29
  "features/notes/codex-feature.json",
30
- "features/notes/DESIGN.md",
31
- "features/notes/README.md",
32
30
  "features/images/src",
33
31
  "features/images/web",
34
32
  "features/images/codex-feature.json",
@@ -36,8 +34,6 @@
36
34
  "features/hide-upsell-banner/codex-feature.json",
37
35
  "features/session-status/src",
38
36
  "features/session-status/codex-feature.json",
39
- "features/session-status/DESIGN.md",
40
- "features/session-status/README.md",
41
37
  "features/session-status/vendor/lucide-LICENSE",
42
38
  "features/session-status/vendor/phosphor-LICENSE",
43
39
  "apps/dashboard/app.js",
@@ -48,25 +44,16 @@
48
44
  "README.md",
49
45
  "features/usage-meter/src",
50
46
  "features/usage-meter/codex-feature.json",
51
- "features/usage-meter/DESIGN.md",
52
- "features/usage-meter/README.md",
53
- "features/unread-inbox/src",
54
- "features/unread-inbox/codex-feature.json",
55
- "features/unread-inbox/DESIGN.md",
56
47
  "features/sidebar-focus/src",
57
48
  "features/sidebar-focus/codex-feature.json",
58
- "features/sidebar-focus/DESIGN.md",
59
49
  "features/sidebar-focus/vendor/lucide-LICENSE",
60
50
  "features/sites/src",
61
51
  "features/sites/codex-feature.json",
62
- "features/sites/DESIGN.md",
63
- "features/sites/README.md",
64
52
  "features/sites/vendor/lucide-LICENSE",
65
53
  "packages/ui/src",
66
54
  "packages/codex-adapter/src",
67
55
  "packages/codex-adapter/package.json",
68
56
  "packages/ui/package.json",
69
- "DESIGN.md",
70
57
  "assets/brand/minecodex-icon.svg",
71
58
  "assets/brand/minecodex-icon-light.svg",
72
59
  "assets/brand/minecodex-icon-dark.svg",
@@ -202,7 +202,12 @@ function featureStatusText(feature) {
202
202
  saved: "saved; restart required",
203
203
  configured: "configured; launching",
204
204
  launching: "launching",
205
- injected: "injected into Codex",
205
+ injected: "available in Codex",
206
+ checking: "checking compatibility",
207
+ partial: "available in some windows",
208
+ degraded: "appearance needs an update",
209
+ unavailable: "temporarily unavailable; check for updates",
210
+ "not-applicable": "not needed on this page",
206
211
  healthy: "healthy in Codex",
207
212
  failed: "failed; run `mcx restart`",
208
213
  stopped: "stopped",
@@ -129,7 +129,7 @@ async function featureStatuses(paths, runtimeManager, locale, runtimeState = nul
129
129
  const plugin = ready.plugins.find((candidate) => candidate.featureId === feature.id) ?? null;
130
130
  const pluginState = plugin?.state ?? null;
131
131
  const applied = appliedFeatures.includes(feature.id)
132
- || ["configured", "launching", "ready", "injected"].includes(pluginState);
132
+ || ["configured", "launching", "ready", "injected", "checking", "partial", "degraded", "unavailable", "not-applicable"].includes(pluginState);
133
133
  const injected = Boolean(
134
134
  pluginState === "injected" && rendererCurrent,
135
135
  );
@@ -152,6 +152,8 @@ async function featureStatuses(paths, runtimeManager, locale, runtimeState = nul
152
152
  state = "failed";
153
153
  } else if (config.features[feature.id] !== applied) {
154
154
  state = "restart-required";
155
+ } else if (["checking","partial","degraded","unavailable","not-applicable"].includes(pluginState)) {
156
+ state = rendererCurrent ? pluginState : "checking";
155
157
  } else if (pluginState === "configured" || pluginState === "launching" || pluginState === "ready") {
156
158
  state = "launching";
157
159
  } else if (injected && healthy) {
@@ -178,6 +180,7 @@ async function featureStatuses(paths, runtimeManager, locale, runtimeState = nul
178
180
  injected,
179
181
  healthy,
180
182
  state,
183
+ targets: plugin?.targets ?? [],
181
184
  health: plugin?.health ?? null,
182
185
  failure,
183
186
  };
@@ -377,6 +377,14 @@ export class MacPlatformAdapter {
377
377
  return this.codexInstallation;
378
378
  }
379
379
 
380
+ async codexBuild() {
381
+ const installation=await this.codex();
382
+ try {
383
+ const {stdout}=await this.execFile("/usr/bin/plutil",["-extract","CFBundleShortVersionString","raw","-o","-",path.join(installation.appPath,"Contents","Info.plist")]);
384
+ return String(stdout).trim() || null;
385
+ } catch { return null; }
386
+ }
387
+
380
388
  async readCodexInstallationFingerprint(installation) {
381
389
  if (typeof this.fileSystem.stat !== "function") return null;
382
390
  const paths = [
@@ -838,6 +846,8 @@ export class WindowsPlatformAdapter {
838
846
  return this.codexInstallation;
839
847
  }
840
848
 
849
+ async codexBuild() { return (await this.codex()).version ?? null; }
850
+
841
851
  async readCodexInstallationFingerprint(installation) {
842
852
  if (typeof this.fileSystem.stat !== "function") return null;
843
853
  const paths = [