minecodex 2.0.5 → 2.0.7

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 (85) hide show
  1. package/DESIGN.md +19 -0
  2. package/README.md +5 -1
  3. package/apps/dashboard/DESIGN.md +53 -31
  4. package/apps/dashboard/app.js +55 -39
  5. package/apps/dashboard/assets/OFL.txt +93 -0
  6. package/apps/dashboard/assets/info.svg +15 -0
  7. package/apps/dashboard/assets/moon.svg +13 -0
  8. package/apps/dashboard/assets/refresh-cw.svg +16 -0
  9. package/apps/dashboard/assets/silkscreen.ttf +0 -0
  10. package/apps/dashboard/assets/sun.svg +21 -0
  11. package/apps/dashboard/assets/terrain.js +89 -0
  12. package/apps/dashboard/assets/triangle-alert.svg +15 -0
  13. package/apps/dashboard/favicon.svg +12 -17
  14. package/apps/dashboard/index.html +24 -18
  15. package/apps/dashboard/styles.css +147 -181
  16. package/assets/brand/README.md +19 -0
  17. package/assets/brand/minecodex-icon-dark.svg +9 -0
  18. package/assets/brand/minecodex-icon-light.svg +9 -0
  19. package/assets/brand/minecodex-icon.svg +8 -0
  20. package/assets/brand/references/lucide-LICENSE.txt +43 -0
  21. package/assets/readme/hero.svg +93 -0
  22. package/features/hide-upsell-banner/README.md +7 -5
  23. package/features/hide-upsell-banner/codex-feature.json +2 -2
  24. package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +30 -12
  25. package/features/images/DESIGN.md +18 -3
  26. package/features/images/README.md +4 -3
  27. package/features/images/codex-feature.json +2 -2
  28. package/features/images/src/adapters/codex/native-image-modules.mjs +220 -0
  29. package/features/images/src/adapters/codex/native-image-source.mjs +113 -198
  30. package/features/images/src/assets/prompts-menu.svg +15 -0
  31. package/features/images/src/frontend/cloud-image-actions.mjs +8 -6
  32. package/features/images/src/frontend/local-image-source.mjs +2 -1
  33. package/features/images/src/library-prompt-page.mjs +10 -4
  34. package/features/images/src/library-prompt-source.mjs +4 -5
  35. package/features/model-slider/codex-feature.json +2 -2
  36. package/features/notes/DESIGN.md +1 -1
  37. package/features/notes/codex-feature.json +2 -2
  38. package/features/notes/src/frontend/entry.mjs +1 -1
  39. package/features/session-status/codex-feature.json +2 -2
  40. package/features/sidebar-focus/DESIGN.md +4 -4
  41. package/features/sidebar-focus/codex-feature.json +3 -2
  42. package/features/sidebar-focus/src/adapters/codex/renderer.mjs +1 -1
  43. package/features/sidebar-focus/src/sidebar-focus.mjs +9 -7
  44. package/features/sites/DESIGN.md +25 -3
  45. package/features/sites/README.md +6 -2
  46. package/features/sites/codex-feature.json +3 -2
  47. package/features/sites/src/adapters/codex/renderer.mjs +25 -4
  48. package/features/sites/src/backend/project-sites.mjs +18 -6
  49. package/features/sites/src/backend/sites-service.mjs +22 -10
  50. package/features/sites/src/domain/discovery-candidates.mjs +53 -0
  51. package/features/sites/src/frontend/entry.mjs +76 -38
  52. package/features/sites/src/frontend/styles.css +3 -3
  53. package/features/unread-inbox/DESIGN.md +24 -25
  54. package/features/unread-inbox/codex-feature.json +3 -2
  55. package/features/unread-inbox/src/adapters/codex/modules.mjs +52 -0
  56. package/features/unread-inbox/src/adapters/codex/renderer.mjs +90 -0
  57. package/features/unread-inbox/src/domain/notifications.mjs +21 -0
  58. package/features/unread-inbox/src/injection-source.mjs +5 -2
  59. package/features/unread-inbox/src/unread-inbox.mjs +212 -297
  60. package/features/usage-meter/codex-feature.json +2 -2
  61. package/package.json +11 -3
  62. package/packages/cli/native/MineCodex.app/Contents/Info.plist +4 -2
  63. package/packages/cli/native/MineCodex.app/Contents/MacOS/MineCodexServiceManager +0 -0
  64. package/packages/cli/native/MineCodex.app/Contents/Resources/MineCodex.icns +0 -0
  65. package/packages/cli/native/MineCodex.app/Contents/_CodeSignature/CodeResources +11 -0
  66. package/packages/cli/native/README.md +31 -0
  67. package/packages/cli/src/browser-control-panel.mjs +48 -0
  68. package/packages/cli/src/commands.mjs +3 -1
  69. package/packages/cli/src/control-server.mjs +6 -3
  70. package/packages/cli/src/platform.mjs +5 -0
  71. package/packages/runtime-host/README.md +10 -5
  72. package/packages/runtime-host/src/codex-injection.mjs +12 -3
  73. package/packages/runtime-host/src/codex-runtime.mjs +10 -0
  74. package/packages/ui/DESIGN.md +5 -1
  75. package/packages/ui/assets/arrow-up-right.svg +13 -0
  76. package/packages/ui/assets/lucide-LICENSE.txt +43 -0
  77. package/packages/ui/assets/minecodex-menu-16.svg +7 -0
  78. package/packages/ui/assets/minecodex-menu-20.svg +7 -0
  79. package/packages/ui/src/components/account-menu.mjs +88 -0
  80. package/packages/ui/src/components/page.mjs +2 -2
  81. package/packages/ui/src/foundation/layers.mjs +1 -0
  82. package/packages/ui/src/install.mjs +3 -0
  83. package/packages/ui/src/native/account-menu.mjs +21 -0
  84. package/packages/ui/src/native/page.mjs +17 -2
  85. package/packages/ui/src/native/tokens.mjs +1 -0
@@ -7,7 +7,7 @@ const host = payload => config.requestHostAction("sites", payload);
7
7
  const owned = "data-mcx-sites";
8
8
  let disposed = false, state = { sites: [], projects: [], threads: [] }, addButton, rowsRoot, fallback, dialog, menu;
9
9
  let rowSignature = "", nativeButton, nativeInner, nativePlus, nativeSiteIcon, nativeMore, timer, revision = 0, opening = false;
10
- let addedTab, addedOnly = false, nativeExternal, nativeGlobe, nativeActionsClass, tabClasses;
10
+ let nativeExternal, nativeGlobe, nativeActionsClass;
11
11
  let currentPage, refreshAt = 0, pendingLayout = null;
12
12
  const style = document.createElement("style");
13
13
  style.setAttribute(owned, "style");
@@ -147,31 +147,73 @@ function row(site, grid) {
147
147
  }
148
148
  return r;
149
149
  }
150
- function updateAddedTab(page) {
151
- const group = page.page.querySelector('[aria-label="Site ownership"]');
152
- if (!group) return;
153
- const tabs = [...group.querySelectorAll("button")].filter(b => !b.hasAttribute(owned));
154
- if (tabs.length !== 2) return;
155
- if (!tabClasses) tabClasses = { active: tabs.find(b => b.getAttribute("aria-pressed") === "true")?.className, inactive: tabs.find(b => b.getAttribute("aria-pressed") === "false")?.className };
156
- if (!addedTab?.isConnected) {
157
- addedTab = tabs[1].cloneNode(false); addedTab.setAttribute(owned, "added-tab"); addedTab.textContent = text("Added by you", "由你添加");
158
- addedTab.onclick = () => { addedOnly = true; rowSignature = ""; reconcile(); }; group.append(addedTab);
150
+ function updateBrowserMenu() {
151
+ const context = nativeSites.browserMenu();
152
+ if (!context || context.menu.querySelector(`[${owned}=browser-add]`)) return;
153
+ const item = context.downloads.cloneNode(true);
154
+ item.setAttribute(owned, "browser-add"); item.removeAttribute("id"); item.removeAttribute("data-radix-collection-item");
155
+ item.removeAttribute("data-highlighted"); item.tabIndex = -1;
156
+ item.querySelector('[data-menu-row-content]')?.replaceChildren(document.createTextNode(text("Add to Sites", "添加到站点")));
157
+ item.onclick = async event => {
158
+ event.preventDefault(); event.stopPropagation();
159
+ // 点击时重新读取菜单所属标签,避免使用菜单打开前的网址。
160
+ const current = nativeSites.browserMenu();
161
+ if (!current || current.menu !== context.menu) return;
162
+ current.close();
163
+ try {
164
+ const prepared = await host({ operation: "prepare-browser-site", ...current.page });
165
+ if (!disposed) showBrowserAdd(prepared);
166
+ } catch (error) { notice(error.message); }
167
+ };
168
+ item.onpointermove = () => item.focus({ preventScroll: true });
169
+ context.downloads.after(item);
170
+ }
171
+ function browserMenuKey(event) {
172
+ const root = event.target.closest?.('[role="menu"]');
173
+ const item = root?.querySelector(`[${owned}=browser-add]`);
174
+ if (!item) return;
175
+ const previous = item.previousElementSibling, next = item.nextElementSibling;
176
+ let target;
177
+ // 注入项不属于 Radix collection,只接管与它相邻的键盘移动。
178
+ if (event.key === "ArrowDown" && event.target === previous || event.key === "ArrowUp" && event.target === next) target = item;
179
+ else if (event.target === item) {
180
+ if (event.key === "ArrowUp") target = previous;
181
+ else if (event.key === "ArrowDown") target = next;
182
+ else if (event.key === "Enter" || event.key === " ") { event.preventDefault(); event.stopImmediatePropagation(); item.click(); return; }
159
183
  }
160
- tabs.forEach((tab, i) => {
161
- const selected = !addedOnly && (i === (page.shared ? 1 : 0));
162
- const cls = selected ? tabClasses.active : tabClasses.inactive;
163
- if (tab.className !== cls) tab.className = cls;
164
- if (tab.getAttribute("aria-pressed") !== String(selected)) tab.setAttribute("aria-pressed", String(selected));
184
+ if (target) { event.preventDefault(); event.stopImmediatePropagation(); target.focus(); }
185
+ }
186
+ function showBrowserAdd({ site, project, preferProject }) {
187
+ const d = newDialog(text("Add to Sites", "添加到站点"));
188
+ let useProject = preferProject;
189
+ const modes = element("div", "mcx-site-modes"); modes.setAttribute("role", "tablist");
190
+ modes.setAttribute("aria-label", text("Site type", "站点类型"));
191
+ const caption = element("p");
192
+ const tabs = [text("Project", "项目"), text("Custom", "自定义")].map((label, index) => {
193
+ const tab = element("button", "mcx-site-mode", label); tab.type = "button"; tab.setAttribute("role", "tab");
194
+ tab.disabled = index === 0 && !project;
195
+ tab.onclick = () => { useProject = index === 0; update(); };
196
+ modes.append(tab); return tab;
165
197
  });
166
- addedTab.className = addedOnly ? tabClasses.active : tabClasses.inactive;
167
- addedTab.setAttribute("aria-pressed", String(addedOnly));
168
- page.list?.querySelectorAll("[data-appgen-row], article").forEach(r => r.toggleAttribute("data-mcx-sites-native-hidden", addedOnly));
198
+ function update() {
199
+ tabs.forEach((tab, index) => tab.setAttribute("aria-selected", String(useProject === (index === 0))));
200
+ caption.textContent = useProject ? project.name : text("Saved as a custom URL", "保存为自定义网址");
201
+ }
202
+ modes.onkeydown = event => {
203
+ if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
204
+ event.preventDefault();
205
+ const index = event.key === "Home" ? (project ? 0 : 1) : event.key === "End" ? 1 : useProject || !project ? 1 : 0;
206
+ tabs[index].click(); tabs[index].focus();
207
+ };
208
+ d.append(modes, caption); update();
209
+ mountEditor(d, d, site, null, { getOrigin: () => useProject ? { kind: "project", projectId: project.id } : { kind: "external" } });
169
210
  }
170
211
  function reconcile() {
171
212
  if (disposed) return;
213
+ updateBrowserMenu();
172
214
  const page = nativeSites.pageInfo();
173
- if (!page) { addedOnly = false; addedTab?.remove(); addedTab = null; addButton?.remove(); addButton = null; rowsRoot?.remove(); rowsRoot = null; fallback?.remove(); fallback = null; rowSignature = ""; currentPage = null; closeMenu(); return; }
174
- cacheNative(page); updateAddedTab(page);
215
+ if (!page) { addButton?.remove(); addButton = null; rowsRoot?.remove(); rowsRoot = null; fallback?.remove(); fallback = null; rowSignature = ""; currentPage = null; closeMenu(); return; }
216
+ cacheNative(page);
175
217
  if (currentPage !== page.page) { currentPage = page.page; rowSignature = ""; if (Date.now() - refreshAt > 1500) refresh().catch(e => notice(e.message)); }
176
218
  if (!addButton?.isConnected && page.create) {
177
219
  addButton = button(text("Add site", "添加站点"), () => showDiscovery()); addButton.setAttribute(owned, "add");
@@ -179,23 +221,21 @@ function reconcile() {
179
221
  page.create.before(addButton);
180
222
  }
181
223
  const query = page.input.value.trim().toLowerCase();
182
- const sites = !addedOnly ? [] : state.sites.filter(s => [s.title, s.url || "", state.projects.find(p => p.id === s.origin.projectId)?.name || ""].some(v => v.toLowerCase().includes(query)));
183
- const signature = JSON.stringify([revision, query, page.shared, page.grid, addedOnly]);
224
+ const sites = state.sites.filter(s => [s.title, s.url || "", state.projects.find(p => p.id === s.origin.projectId)?.name || ""].some(v => v.toLowerCase().includes(query)));
225
+ const signature = JSON.stringify([revision, query, page.shared, page.grid]);
184
226
  if (signature === rowSignature && rowsRoot?.isConnected && (page.list ? rowsRoot.parentElement === page.list : fallback?.isConnected)) return;
185
227
  rowsRoot?.remove(); fallback?.remove(); fallback = null;
186
228
  document.querySelectorAll("[data-mcx-sites-empty-suppressed]").forEach(e => e.removeAttribute("data-mcx-sites-empty-suppressed"));
187
229
  rowsRoot = element("div"); rowsRoot.setAttribute(owned, "rows"); rowSignature = signature;
188
- if (!sites.length && !addedOnly) { rowsRoot = null; return; }
189
- if (addedOnly && !page.list && page.content.firstElementChild) page.content.firstElementChild.setAttribute("data-mcx-sites-empty-suppressed", "");
230
+ if (!sites.length) { rowsRoot = null; return; }
231
+ if (page.empty) page.empty.setAttribute("data-mcx-sites-empty-suppressed", "");
190
232
  if (page.list) page.list.append(rowsRoot);
191
233
  else {
192
234
  fallback = element("div", "@container pb-3"); fallback.setAttribute(owned, "fallback");
193
235
  const layout = element("div"); layout.setAttribute(owned, page.grid ? "grid" : "table"); fallback.append(layout); layout.append(rowsRoot);
194
- if (page.content.firstElementChild) page.content.firstElementChild.setAttribute("data-mcx-sites-empty-suppressed", "");
195
236
  page.content.append(fallback);
196
237
  }
197
238
  rowsRoot.append(...sites.map(s => row(s, page.grid)));
198
- if (addedOnly && !sites.length) { const empty = element("div", "col-span-full py-8 text-center text-sm text-secondary", text("No added sites", "暂无添加的站点")); rowsRoot.append(empty); }
199
239
  }
200
240
  function schedule() { clearTimeout(timer); timer = setTimeout(reconcile, 60); }
201
241
  function newDialog(title, back) {
@@ -274,15 +314,16 @@ async function mountDiscovery(d, panel) {
274
314
  });
275
315
  return;
276
316
  }
277
- if (!candidates.length) results.append(element("div", "mcx-site-empty", text("No project websites found", "尚未发现项目网页")));
278
- for (const candidate of candidates) {
317
+ const available = candidates.filter(candidate => !completed.has(candidate) && !state.sites.some(s =>
318
+ (candidate.entryId && s.entryId === candidate.entryId || candidate.url && s.url === candidate.url)));
319
+ if (!available.length) results.append(element("div", "mcx-site-empty", text("No new project websites found", "暂无可添加的项目网页")));
320
+ for (const candidate of available) {
279
321
  const item = element("label", "mcx-site-candidate");
280
322
  const art = element("span", "mcx-site-candidate-art"); if (nativeSiteIcon) art.append(nativeSiteIcon.cloneNode(true));
281
323
  const info = element("div", "mcx-site-candidate-info"); info.append(element("div", "", candidate.title), element("div", "mcx-site-url", siteSecondary(candidate)));
282
- const added = completed.has(candidate) || state.sites.some(s => (candidate.entryId && s.entryId === candidate.entryId || candidate.url && s.url === candidate.url) && s.origin.projectId === candidate.projectId);
283
324
  const checkbox = element("input", "mcx-site-checkbox"); checkbox.type = "checkbox";
284
- checkbox.checked = added || selected.has(candidate); checkbox.disabled = added || saving;
285
- checkbox.setAttribute("aria-label", (added ? text("Added: ", "已添加:") : "") + candidate.title);
325
+ checkbox.checked = selected.has(candidate); checkbox.disabled = saving;
326
+ checkbox.setAttribute("aria-label", candidate.title);
286
327
  checkbox.onchange = () => { if (checkbox.checked) selected.add(candidate); else selected.delete(candidate); updateActions(); };
287
328
  item.append(art, info, checkbox); if (candidate.url) loadPreview(art, candidate.url, candidate.status !== "running"); else art.classList.add("mcx-site-placeholder");
288
329
  results.append(item);
@@ -300,14 +341,14 @@ function showEditor(site, problem) {
300
341
  const d = newDialog(text("Edit site", "编辑站点"));
301
342
  mountEditor(d, d, site, problem).focus();
302
343
  }
303
- function mountEditor(d, container, site, problem) {
344
+ function mountEditor(d, container, site, problem, options = {}) {
304
345
  const form = element("form"), content = element("div", "mcx-site-content"); form.append(content); container.append(form);
305
346
  function field(label, key, value, type) { const l = element("label", "", label), i = element("input"); i.id = `mcx-site-${key}`; l.htmlFor = i.id; i.name = key; i.value = value || ""; i.type = type || "text"; content.append(l, i); return i; }
306
347
  const url = field(text("URL", "网址"), "url", site?.url, "url"); url.required = true; url.placeholder = "http://localhost:3000";
307
348
  const title = field(text("Name", "名称"), "title", site?.title); title.maxLength = 160; title.required = true;
308
349
  let origin = site?.origin || { kind: "external" };
309
350
  const error = element("div", "", problem); error.setAttribute("role", "alert"); content.append(error);
310
- const footer = config.ui.footer(element("div", "mcx-site-footer")); const cancel = dialogButton(text("Cancel", "取消"), () => d.close(), false, "ghost"); const save = dialogButton(site ? text("Save", "保存") : text("Add", "添加"), () => {}, true); save.type = "submit"; footer.append(cancel, save); form.append(footer);
351
+ const footer = config.ui.footer(element("div", "mcx-site-footer")); const cancel = dialogButton(text("Cancel", "取消"), () => d.close(), false, "ghost"); const save = dialogButton(site?.id ? text("Save", "保存") : text("Add", "添加"), () => {}, true); save.type = "submit"; footer.append(cancel, save); form.append(footer);
311
352
  let submitting = false;
312
353
  function validFields() {
313
354
  if (!title.value.trim() || title.value.trim().length > 160 || !url.value.trim() || !url.validity.valid) return false;
@@ -319,7 +360,7 @@ function mountEditor(d, container, site, problem) {
319
360
  form.onsubmit = async e => {
320
361
  e.preventDefault(); if (submitting || !validFields()) { updateSave(); return; }
321
362
  submitting = true; updateSave(); error.textContent = "";
322
- try { await host({ operation: "save", id: site?.id, site: { title: title.value.trim(), url: url.value.trim(), origin } }); await refresh(); d.close(); }
363
+ try { await host({ operation: "save", id: site?.id, site: { title: title.value.trim(), url: url.value.trim(), origin: options.getOrigin?.() || origin } }); await refresh(); d.close(); }
323
364
  catch (e) { error.textContent = e.message; }
324
365
  finally { submitting = false; updateSave(); }
325
366
  };
@@ -356,7 +397,6 @@ async function openSite(site) {
356
397
  finally { opening = false; }
357
398
  }
358
399
  function onClick(e) {
359
- if (addedOnly && e.target.closest('[aria-label="Site ownership"] button:not([data-mcx-sites])')) { addedOnly = false; rowSignature = ""; schedule(); }
360
400
  if (menu && !menu.node.contains(e.target) && !menu.trigger.contains(e.target)) closeMenu();
361
401
  if (e.target.closest(`[${owned}]`)) return;
362
402
  const row = e.target.closest("[data-appgen-row], [data-testid=sites-grid] > article");
@@ -367,7 +407,7 @@ function onClick(e) {
367
407
  const token = {}; pendingLayout = token; void applyWebLayout(token);
368
408
  }
369
409
  }
370
- function onKey(e) { if (e.key === "Escape" && menu) { e.preventDefault(); closeMenu(); } else if (e.key === "Tab" && menu) closeMenu(); }
410
+ function onKey(e) { browserMenuKey(e); if (e.defaultPrevented) return; if (e.key === "Escape" && menu) { e.preventDefault(); closeMenu(); } else if (e.key === "Tab" && menu) closeMenu(); }
371
411
  function onPointer(e) { if (e.target.closest('[role="separator"]')) pendingLayout = null; }
372
412
  const observer = new MutationObserver(records => { if (records.some(r => !r.target.closest?.(`[${owned}]`))) schedule(); });
373
413
  observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["aria-pressed"] });
@@ -376,8 +416,6 @@ refresh().catch(e => notice(e.message)); schedule();
376
416
  return () => {
377
417
  disposed = true; pendingLayout = null; clearTimeout(timer); observer.disconnect();
378
418
  document.removeEventListener("input", schedule); document.removeEventListener("click", onClick, true); document.removeEventListener("keydown", onKey, true); document.removeEventListener("pointerdown", onPointer, true);
379
- addedOnly = false; const page = nativeSites.pageInfo(); if (page) updateAddedTab(page);
380
- document.querySelectorAll("[data-mcx-sites-native-hidden]").forEach(e => e.removeAttribute("data-mcx-sites-native-hidden"));
381
419
  closeMenu(); dialog?.close(); document.querySelectorAll(`[${owned}]`).forEach(e => e.remove());
382
420
  document.querySelectorAll("[data-mcx-sites-empty-suppressed]").forEach(e => e.removeAttribute("data-mcx-sites-empty-suppressed"));
383
421
  };
@@ -25,7 +25,7 @@
25
25
  [data-mcx-sites=dialog] .mcx-site-candidate {padding:12px 0;border-bottom:1px solid var(--mcx-color-border)}
26
26
  [data-mcx-sites=dialog] .mcx-site-candidate>div {min-width:0}
27
27
  [data-mcx-sites=dialog] .mcx-site-url {color:var(--mcx-color-text-secondary);font-size:var(--mcx-text-sm);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
28
- [data-mcx-sites=dialog] label {display:block;margin:14px 0 6px;font-size:var(--mcx-text-sm);font-weight:var(--font-weight-medium)}
28
+ [data-mcx-sites=dialog] label:not(.mcx-site-candidate) {display:block;margin:14px 0 6px;font-size:var(--mcx-text-sm);font-weight:var(--font-weight-medium)}
29
29
  [data-mcx-sites=dialog] input {box-sizing:border-box;width:100%;min-height:36px;padding:8px 12px;border:1px solid var(--mcx-color-border-primary-outline);border-radius:var(--control-radius-md);background:transparent;color:var(--mcx-color-text-primary);outline:none;font:inherit}
30
30
  [data-mcx-sites=dialog] input:focus-visible {border-color:var(--mcx-color-ring);outline:none}
31
31
  [data-mcx-sites=dialog] [role=alert] {color:var(--mcx-color-text-danger);font-size:var(--mcx-text-sm);margin-top:12px}
@@ -34,7 +34,7 @@
34
34
  [data-mcx-sites=menu] svg {width:16px;height:16px;opacity:.75}
35
35
  [data-mcx-sites=menu] button:hover svg,[data-mcx-sites=menu] button:focus svg {opacity:1}
36
36
  [data-mcx-sites=notice] {position:fixed;bottom:24px;left:50%;transform:translateX(-50%);max-width:min(560px,90vw);z-index:1000;padding:12px 16px;border:1px solid var(--mcx-color-border);border-radius:var(--mcx-radius-xl);background:var(--color-surface-elevated);color:var(--mcx-color-text-primary);box-shadow:var(--mcx-shadow-xl-spread);font:var(--mcx-text-sm)/1.5 var(--mcx-font-sans-default)}
37
- [data-mcx-sites-empty-suppressed],[data-mcx-sites-native-hidden] {display:none!important}
37
+ [data-mcx-sites-empty-suppressed] {display:none!important}
38
38
  [data-mcx-sites=entry] .mcx-site-sharing svg {width:16px;height:16px;flex:none}
39
39
  @container (max-width:520px) {[data-mcx-sites=table] {grid-template-columns:minmax(0,1fr) auto}}
40
40
 
@@ -46,7 +46,7 @@
46
46
  [data-mcx-sites=dialog] .mcx-site-candidate {width:100%;padding:10px 8px;border:0;background:transparent;border-radius:var(--mcx-radius-lg);color:var(--mcx-color-text-primary);text-align:start;font:inherit}
47
47
  [data-mcx-sites=dialog] .mcx-site-candidate:hover:not(:disabled),[data-mcx-sites=dialog] .mcx-site-candidate:focus-visible {background:var(--mcx-color-background-primary-ghost-hover)}
48
48
  [data-mcx-sites=dialog] .mcx-site-candidate .mcx-site-candidate-info {flex:1;min-width:0}
49
- [data-mcx-sites=dialog] label.mcx-site-candidate {margin:0;font-size:inherit;font-weight:inherit;cursor:pointer}
49
+ [data-mcx-sites=dialog] label.mcx-site-candidate {display:flex;align-items:center;gap:12px;margin:0;font-size:inherit;font-weight:inherit;cursor:pointer}
50
50
  [data-mcx-sites=dialog] .mcx-site-candidate:has(input:disabled) {cursor:default}
51
51
  [data-mcx-sites=dialog] .mcx-site-candidate input.mcx-site-checkbox {appearance:auto;flex:none;width:16px;height:16px;min-height:0;margin:0;padding:0;accent-color:var(--mcx-color-text-primary)}
52
52
  [data-mcx-sites=dialog] .mcx-site-candidate input.mcx-site-checkbox:focus-visible {outline:2px solid var(--mcx-color-ring);outline-offset:2px}
@@ -1,40 +1,39 @@
1
- # 任务切换
1
+ # 任务切换:会话通知
2
2
 
3
- 通用视觉、材质、按钮、Tooltip 与原生 sidebar 层级遵循根 [DESIGN.md](../../DESIGN.md)。本文件只记录功能特有组件、布局、数据语义与通用组件选型。
3
+ 通用视觉、材质、按钮、Tooltip 与原生 sidebar 层级遵循根 [DESIGN.md](../../DESIGN.md)。本文件记录通知条的触发、布局和状态规则。
4
4
 
5
- 仅在线程侧栏收起时显示入口;展开时移除入口并关闭浮层。入口随 Codex 标题栏布局切换:
6
- - 普通/分栏:在主 Chat 标题前显示原生 list-bullet 图标;整段标题可点击。
7
- - 全宽标签页:在 Chat 标签页前注入独立的任务图标按钮。
8
- - 移除扩展铃铛,注入入口存在时,用原生完整 SVG 显示无蓝点的 Sidebar 图标;保留 React 原节点,卸载或展开侧栏时移除替代图标并恢复原节点。
9
- - 悬停入口150ms展开;鼠标离开入口与浮层250ms后关闭,跨越8px间隙进入浮层会取消关闭。点击入口确保打开;点击外部、Escape 关闭。Enter、空格、ArrowDown 可打开。
5
+ ## 触发与原生边界
10
6
 
11
- 入口克隆原生 Chat 标题控件,通用反馈遵循根规范。入口仅 hover 显示背景,不保持选中底色。标题区域 no-drag,其他标题栏区域保留窗口拖拽。
7
+ 会话侧栏不可见时,左上方自动显示通知堆栈,包括 Home 折叠状态,以及 Space、Projects 等没有会话侧栏的页面。判断真实会话导航内容的可见性,不把“侧栏展开偏好”当作当前页面具有会话侧栏的证据。Home 临时侧栏或其他原生侧栏 peeking 展开期间隐藏通知;离开后恢复,原生悬浮侧栏保持可进入、可点击。
12
8
 
13
- 浮层宽320px(窄窗口缩小),最大高440px(约5条,受视口高度限制);选择通用 Popover,功能圆角16px,外层上下 padding 6px、左右0;滚动容器左右 padding 6px,使滚动条靠近浮层边缘。位置对齐标题触发区域左侧、底缘下方8px,并受窗口边界约束。
9
+ 不创建额外入口,不改 Chat 标题的属性、热区和窗口拖拽行为,不替换 Sidebar.left 图标,也不修改 Home 原生蓝点。不点击 Activity、不切换原生筛选或展开偏好。旧入口在 Runtime Host 替换插件时由旧实例 cleanup 恢复。
14
10
 
15
- 列表集合为等待回复/批准、工作中、最近1小时打开过、未读的会话,按 Thread ID 去重。按需要回复、等待批准、未读、执行中、空闲排序,同级按消息时间倒序;排除当前会话。原生流式摘要变化时更新观测消息时间,不因菜单重绘更新时间。最近访问记录限定当前窗口运行期间。每条显示:
16
- 1. 单行标题(13px/18px),远程标识紧随标题且不显示主机名;右侧状态优先级为待确认标签、原生 loading、相对时间。背景仅 hover 时显示。
17
- 2. 单行省略的 AI 回答摘要(12px/16px)。
18
- 3. 所属 Project 原生图标(12px)与名称(若有),11px/16px 弱化文字,不合并分组。条目 padding 10px 12px,行间 gap 4px,圆角10px。
11
+ ## 通知条
19
12
 
20
- 时间使用原生 hover card 的 recencyAt,缺失时 updatedAt;每30秒刷新。工作状态读取原生 statusState.type=loading。缓存会话数据与原生导航回调,复用导航和已读更新,不缓存 DOM 行,不发送消息。
13
+ 堆栈以 Navbar 右侧8px、原生标题栏下沿8px为左上角原点;无标题栏内容时取 Navbar 顶缘。位置不依赖可点击的会话标题。宽320px,窄窗口缩小,右侧/底部保留12px。每条是独立的共享 opaque Popover,圆角16px,间距8px;没有列表标题、整块菜单底板或页面遮罩。堆栈位于页面之上、原生侧栏和原生模态浮层之下(当前角色实现层级30)。空白区域不拦截点击,溢出可滚动。默认仅显示前三条通知;超过三条时,第三条下方露出两层逐级缩进的卡片边缘,并显示“X more notifications”按钮,X 为总数减三。点击后展开全部,使用窗口剩余高度;只有展开内容超出该高度时才启用底部半条左右的渐隐,不再显示遮挡数量文字。滚动到底部解除渐隐。展开状态仅限当前页面;原生内存路由 pathname 变化(包括会话跳转)后恢复三条,并回到堆栈顶部。同页通知刷新、滚动及临时侧栏隐藏不重置。跨页收拢使用320ms缓出高度过渡,多余卡片淡出,层叠与按钮跟随收拢显现;尊重 reduced-motion。通知清空、账号切换或插件重载也重置。
21
14
 
22
- 打开时临时启用 Activity 以取得完整列表,关闭或卸载时恢复原先模式。只读取原生数据,预览不标记已读。界面及无障碍文案适配中英文。
15
+ 条目保持现有信息结构:13px/18px标题、可用时的12px/16px原生回答摘要(最多两行,超出截断;短摘要不强占两行)、11px/16px项目名与12px原生项目图标。远程图标紧随标题;状态显示需要回复/等待批准标签、相对时间。未读且已结束的条目在时间右侧显示蓝点。内容 padding 四周统一使用功能 spacing token `--mcx-notification-padding:14px`,行间4px。状态文字和无障碍标签随宿主语言,用户标题和摘要不翻译。
23
16
 
24
- 入口只显示任务图标,不显示数量。浮层标题为 Recent activity/最近活动,反映工作中、未读与最近访问的集合;不将最近访问误称为待处理。浮层无页面遮罩,点击外部关闭,Escape 关闭并返回入口焦点。
17
+ 新条目与堆栈推移采用320ms ease-out(cubic-bezier(.22,1,.36,1)),从当前视觉位置接续被打断的移动;状态解除以180ms淡出并收拢空位。内容更新保留卡片和点击目标,采用180ms淡入,不重播整卡入场;倒计时只更新标签内的数字与进度,不重建标签。尊重 reduced-motion。键盘可聚焦并点击条目;右上角外沿关闭按钮在 hover/focus 时显示,关闭仅影响本窗口通知,不标记原生已读。
25
18
 
26
- 标题固定、条目容器独立滚动;上下保留6px内边距;条目内容左右保留6px,滚动条不受外层横向内边距挤压。
19
+ ## 状态与生命周期
27
20
 
28
- 点击后先同步显示浮层与可用数据,首帧绘制后再切换原生 Activity 补全条目;关闭或卸载会取消尚未开始的切换。
21
+ - 范围沿用原有 Codex 会话类别:需要回复、等待批准、已结束且未读;排除单纯 Loading 和没有这些状态的最近访问会话。
22
+ - 每个 Thread key 只保留一条;按状态事件发生顺序倒序,而非按状态优先级。首次载入按原生消息时间排序。
23
+ - 需要回复/批准与已结束未读之间的变化产生新事件;未读状态下消息时间变化也产生新事件。Loading 不产生通知;普通摘要刷新和重绘不改变顺序。
24
+ - 当前可见会话、已解决/已读或重新进入 Loading、从原生目录移除的会话自动移除。身份消失不沿用 sidebar 残留选中项。
25
+ - 通知不自动超时消失;可点击跳转、关闭单条,或等原生状态消除。关闭后同一事件不重复弹出,新事件可以再次提醒。
26
+ - 侧栏显示时暂停呈现,保持状态跟踪;重新折叠时显示仍有效的通知。账号变化清空本窗口关闭记录。
27
+ - Needs input 的倒计时读取原生自动处理请求:pendingRequest 的 userInputRequestId 必须匹配 resolution 的 requestId,且 resolutionState.status 为 scheduled。标签同步剩余秒数及进度,暂停或延期跟随原生数据。归零不自行推断 Working;原生 attention 清除并恢复 Loading 后才退出。当前安装版本 Awaiting approval 没有倒计时契约,不伪造计时或自动批准。
28
+ - 不持久化通知正文,不发送消息、不执行批准操作。每秒读取原生 store,DOM/窗口布局变化也触发刷新;不为通知发送网络请求。首次连接读取安装包 JS 以解析原生角色。
29
29
 
30
- 消息刷新时保留列表滚动位置与条目键盘焦点。当前身份消失时清除选中状态,最近访问缓存按一小时过期。
30
+ ## 数据适配与验证边界
31
31
 
32
- 待确认状态读取原生行的 `statusPill.label.props.id=codex.localTaskRow.needsInput` 或 `chips[].id=awaiting-approval`,不根据摘要推断。Needs input/需要回复沿用原生 chart-blue 15% 背景,Awaiting approval/等待批准沿用 chart-green 20% 背景与圆角胶囊;适配紧凑行使用12px/16px文字。点击条目仍仅导航,不执行批准或原生 Snooze 动作。
32
+ Codex 26.930.21537,2026-10-03:隔离 Renderer 确认侧栏只挂载7条,而原生 `allSidebarThreadKeys` 目录含72条。Adapter 从 Navbar 当前 React scope 读取原生目录、需要回复/批准、工作与未读 selector,并复用原生会话导航函数。不会因项目折叠或 Space 页面隐藏 Home 列表而只剩可见条目。项目归属来自原生项目目录;摘要和项目图标在相应原生行已挂载时读取,缺失不编造或保留旧消息。
33
33
 
34
- 滚动条与原生 overflow 容器一致:thin,静止使用 `--color-border`,hover/active 使用 `--color-border-strong`,轨道透明;不使用全局较深的 `--scrollbar-color`。
34
+ 压缩符号及导出名通过语义调用关系动态解析;文件名哈希不作为契约。缺失模块/scope/状态契约时隐藏通知并通过 `nativeCapabilities()` 报告,不使用旧账号或旧会话快照假装成功。尚未创建的任务占位不显示,原有非 Codex 的 ChatGPT 会话不因本次改版新增覆盖范围。
35
35
 
36
- 入口提醒点:排除当前会话后,有需要回复或批准时为红色 `#FF3B30`,否则存在未读为原生蓝色 `--blue-400`;只有执行中或空闲则无点。点直径6px。无其他命中会话时不打开浮层,包括仅当前会话命中的情况。原生侧栏图标与 MineCodex 提醒随注入/卸载同步恢复。
36
+ 验证覆盖队列去重、状态新旧、已读/当前/移除、关闭与再次提醒、源码序列化、原生角色契约、侧栏可见性和卸载。真实等待批准/输入事件需要实际发生才能完成端到端验收;受控样例只证明呈现与交互,不冒充真实通知到达。
37
37
 
38
- 浮层标题颜色与原生 Sidebar 类别标题一致,使用 `--color-text-tertiary`。
39
-
40
- 当前通过 UI 组件库的 `config.ui.surface(panel, "popover")` 选择原生 Popover;功能继续拥有定位、尺寸与内容。
38
+ - 等待批准沿用原生绿色标签,通过共享主题映射读取 chart-green;远程任务保留标题旁的原生远程设备图标。
39
+ - 关闭按钮使用原生 SVG 在圆形容器内居中;常态与 hover 均保留不透明底色,避免露出通知边框。
@@ -7,14 +7,15 @@
7
7
  "zh-CN": "任务切换"
8
8
  },
9
9
  "description": {
10
- "en": "Switch between working and unread chats from the chat title or task button.",
11
- "zh-CN": "点击聊天标题或任务按钮,切换工作中与未读会话。"
10
+ "en": "Get chat notifications when the sidebar is hidden.",
11
+ "zh-CN": "收起侧栏时接收会话通知,点击返回聊天。"
12
12
  },
13
13
  "entry": {
14
14
  "kind": "page-script",
15
15
  "script": "src/unread-inbox.mjs",
16
16
  "sourceFactory": "src/injection-source.mjs"
17
17
  },
18
+ "icon": {"viewBox": "0 0 24 24", "markup": "<path d=\"M10.268 21a2 2 0 0 0 3.464 0\" />\n <path d=\"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326\" />"},
18
19
  "placement": {
19
20
  "order": 70
20
21
  }
@@ -0,0 +1,52 @@
1
+ // 从原生调用关系解析角色;构建哈希和压缩变量名不作为契约。
2
+ export function discoverThreadBindings(source) {
3
+ const requireMatch = (pattern, text = source) => {
4
+ const value = text.match(pattern)?.[1];
5
+ if (!value) throw new Error("Native task notification contract changed");
6
+ return value;
7
+ };
8
+ const rowStart = source.lastIndexOf("function ", source.indexOf("codex.localTaskRow.needsInput"));
9
+ const row = source.slice(rowStart, source.indexOf("var ", rowStart));
10
+ const attentionValue = requireMatch(/([\w$]+)===`approval`\|\|[\w$]+\?\{id:`awaiting-approval`/, row);
11
+ const statusValue = requireMatch(/[;,][\w$]+=([\w$]+);\([\w$]+===`response`/, row);
12
+ const escape = value => value.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");
13
+ const selectorFor = local => requireMatch(new RegExp(`${escape(local)}=[\\w$]+\\(([\\w$]+),`),row);
14
+ const resolutionValue = requireMatch(/([\w$]+)\?\.requestId===[\w$]+\.userInputRequestId/, row);
15
+ const pendingValue = requireMatch(/=([\w$]+)\?\.pendingRequest/, row);
16
+ const durationName = requireMatch(/durationMs:([\w$]+)/, row);
17
+ const durationMs = Number(requireMatch(new RegExp(`${escape(durationName)}=([0-9.eE+]+)`)));
18
+ const roles = {
19
+ resolution: selectorFor(resolutionValue),
20
+ pending: selectorFor(pendingValue),
21
+ catalog: requireMatch(/allSidebarThreadKeys:[\w$]+,pinnedThreadKeys:[\w$]+\}=[\w$]+\(([\w$]+)\)/),
22
+ thread: requireMatch(/let [\w$]+=[\w$]+\(([\w$]+),[\w$]+\);return [\w$]+==null\?\[\]:\[\{task:/),
23
+ projects: requireMatch(/([\w$]+)=[\w$]+\([\w$]+,\([\w$]+,\{get:[\w$]+,scope:[\w$]+\}\)=>\{let [\w$]+=[\w$]+!==`chatgpt`/),
24
+ attention: selectorFor(attentionValue),
25
+ status: selectorFor(statusValue),
26
+ unread: requireMatch(/let [\w$]+=[\w$]+\(([\w$]+),[\w$]+\),[\w$]+=[\w$]+\.roomId/,row),
27
+ navigate: requireMatch(/\.navigate\([\w$]+\([\w$]+\)\):([\w$]+)\(/,row),
28
+ };
29
+ const exports = source.slice(source.lastIndexOf("export{"));
30
+ return {countdownDuration:{value:durationMs}, ...Object.fromEntries(Object.entries(roles).map(([role,local]) => {
31
+ const name = exports.match(new RegExp(`(?:\\{|,)${escape(local)} as ([\\w$]+)(?:,|\\})`))?.[1];
32
+ if (name) return [role,{name}];
33
+ for (const match of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g)) {
34
+ const binding=match[1].split(",").map(s=>s.trim().split(/\s+as\s+/)).find(parts=>parts.at(-1)===local);
35
+ if (binding) return [role,{name:binding[0],file:match[2]}];
36
+ }
37
+ throw new Error(`Native task notification role unavailable: ${role}`);
38
+ }))};
39
+ }
40
+
41
+ export async function loadThreadBindings() {
42
+ const url=[...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(e=>e.href||e.src).find(url=>/\/app-initial-[^/]+\.js/.test(url));
43
+ if (!url) throw new Error("Native task notification module unavailable");
44
+ const source=await (await fetch(url)).text();
45
+ const bindings=discoverThreadBindings(source);
46
+ return Object.fromEntries(await Promise.all(Object.entries(bindings).map(async ([role,binding])=>{
47
+ if ("value" in binding) return [role,binding.value];
48
+ const value=(await import(binding.file ? new URL(binding.file,url).href : url))[binding.name];
49
+ if (value == null) throw new Error(`Native task notification role unavailable: ${role}`);
50
+ return [role,value];
51
+ })));
52
+ }
@@ -64,3 +64,93 @@ function threadRecord(row) {
64
64
  }
65
65
  return {read:threadRecord};
66
66
  }
67
+
68
+ // 会话侧栏是可卸载视图,通知从同一原生 store 读取完整目录。
69
+ export function createNotificationAdapter(codex, load, invalidate) {
70
+ let bindings=null, loading=null, retryAt=0, disposed=false;
71
+ function scopeFor(rail) {
72
+ let fiber=codex.currentFiber(rail);
73
+ for (let depth=0;fiber&&depth<160;depth++,fiber=fiber.return) {
74
+ for (const values of fiber.updateQueue?.memoCache?.data ?? []) {
75
+ const scope=values.find(value=>typeof value?.get==="function" && value.node?.store && value.query && value.scope);
76
+ if (scope) return scope;
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+ function ensure() {
82
+ if (bindings || loading || Date.now()<retryAt || disposed) return;
83
+ loading=load().then(value=>{if(!disposed){bindings=value;invalidate();}}).catch(()=>{
84
+ retryAt=Date.now()+30000;
85
+ codex.report("task-notification-catalog",false,"native-module-contract-unavailable");
86
+ }).finally(()=>{loading=null;});
87
+ }
88
+ function layout() {
89
+ const rail=codex.rail();
90
+ if (!rail) return {available:false};
91
+ // 桌面端使用内存路由,window.location 不随页面跳转变化。
92
+ let pageKey=null;
93
+ codex.props(rail,props=>{
94
+ pageKey=props.routeContext?.outlet?.props?.match?.pathname??null;
95
+ return pageKey!==null;
96
+ });
97
+ const home=rail.querySelector('[data-sidebar-destination="builtin:home"]');
98
+ const sidebar=document.querySelector('[data-slate-sidebar-content]:not([inert])');
99
+ const isVisible=e=>!!e && !e.closest('[inert],[aria-hidden="true"]') && e.checkVisibility() && e.getBoundingClientRect().width>0;
100
+ const homeSidebar=isVisible(sidebar) && isVisible(sidebar.querySelector('[data-app-action-sidebar-scroll]'));
101
+ const peeking=home?.getAttribute("aria-expanded")==="true" || !!document.querySelector('[data-slate-sidebar-peeking="true"]');
102
+ const titlebar=[...document.querySelectorAll('[data-app-shell-titlebar-slot="main"]')].find(isVisible);
103
+ const rect=rail.getBoundingClientRect(), titleRect=titlebar?.getBoundingClientRect();
104
+ return { available:true, pageKey, visible:!homeSidebar&&!peeking,
105
+ left:rect.right+8, top:Math.max(rect.top,titleRect?.bottom??0)+8 };
106
+ }
107
+ function snapshot() {
108
+ ensure();
109
+ const rail=codex.rail(), scope=rail && scopeFor(rail);
110
+ if (!bindings || !scope) {
111
+ if (!scope) codex.report("task-notification-catalog",false,"native-scope-unavailable");
112
+ return null;
113
+ }
114
+ try {
115
+ const catalog=scope.get(bindings.catalog);
116
+ const projectCatalog=scope.get(bindings.projects,"codex");
117
+ const projects=new Map();
118
+ for(const group of projectCatalog.projectGroups) for(const key of group.threadKeys) projects.set(key,{id:group.projectId,name:group.label});
119
+ const rows=new Map([...document.querySelectorAll('[data-app-action-sidebar-thread-row]')].map(row=>[row.dataset.appActionSidebarThreadId,row]));
120
+ const rowAdapter=createThreadAdapter(codex);
121
+ const records=catalog.allSidebarThreadKeys.flatMap(key=>{
122
+ const item=scope.get(bindings.thread,key), summary=item?.summary;
123
+ // 目录中的待创建占位还没有可导航的正式会话。
124
+ if (!summary?.conversationId || !summary.hostId) return [];
125
+ const {conversationId,hostId}=summary, args={conversationId,hostId};
126
+ const attention=scope.get(bindings.attention,args);
127
+ const status=scope.get(bindings.status,args);
128
+ const pending=scope.get(bindings.pending,{hostId,threadId:conversationId})?.pendingRequest;
129
+ const resolution=scope.get(bindings.resolution,args);
130
+ const countdown=attention==="response" && pending?.kind==="userInput" && resolution?.requestId===pending.userInputRequestId && resolution?.resolutionState?.status==="scheduled"
131
+ ? {deadlineMs:resolution.resolutionState.deadlineMs,durationMs:bindings.countdownDuration} : null;
132
+ const unread=scope.get(bindings.unread,{conversationId,fallbackRootUnread:summary.hasUnreadTurn});
133
+ const row=rows.get(key), mounted=row ? rowAdapter.read(row) : null;
134
+ return [{key,threadId:conversationId,hostId,title:summary.title||mounted?.title||"",
135
+ summary:mounted?.summary||"",project:projects.get(key)??mounted?.project??null,
136
+ timestamp:unread.unreadMessageAt??summary.recencyAt??summary.updatedAt??null,
137
+ unread:unread.hasUnread,working:status==="loading"&&attention!=="response",
138
+ attention:attention==="approval"?"approval":attention==="response"?"input":null,
139
+ countdown,spinner:mounted?.spinner||"",activate:()=>bindings.navigate(scope,conversationId,hostId==="local"?undefined:hostId)}];
140
+ });
141
+ let auth=null;
142
+ codex.props(rail,props=>{
143
+ auth=[props.auth,props.value].find(value=>value && typeof value==="object" && "authMethod" in value && "accountId" in value && "userId" in value)??null;
144
+ return Boolean(auth);
145
+ });
146
+ if(auth?.isLoading)return null;
147
+ const currentId=codex.composerSurface?.()?.threadId??codex.currentThreadId();
148
+ codex.report("task-notification-catalog",true);
149
+ return {records,currentKey:records.find(item=>item.threadId===currentId)?.key??null,account:auth ? `${auth.authMethod}:${auth.accountId}:${auth.userId}` : scope.node};
150
+ } catch {
151
+ codex.report("task-notification-catalog",false,"native-store-contract-unavailable");
152
+ return null;
153
+ }
154
+ }
155
+ return {snapshot,layout,dispose(){disposed=true;bindings=null;}};
156
+ }
@@ -0,0 +1,21 @@
1
+ // 状态变化产生新通知;流式摘要刷新只更新内容,不抢占顶部。
2
+ export function createNotificationQueue() {
3
+ const entries = new Map();
4
+ let sequence = 0;
5
+ function update(records, currentKey) {
6
+ const present = new Set();
7
+ for (const item of records) {
8
+ if (!item.key) continue;
9
+ present.add(item.key);
10
+ const state = item.attention || (item.unread && item.working === false ? "unread" : null);
11
+ if (!state || item.key === currentKey) { entries.delete(item.key); continue; }
12
+ const previous = entries.get(item.key);
13
+ const event = `${state}:${state === "unread" ? item.timestamp ?? "" : ""}`;
14
+ const changed = previous?.event !== event;
15
+ entries.set(item.key, { item, event, order:changed ? ++sequence : previous.order, dismissed:changed ? false : previous.dismissed });
16
+ }
17
+ for (const key of entries.keys()) if (!present.has(key)) entries.delete(key);
18
+ return [...entries.values()].filter(entry => !entry.dismissed).sort((a,b) => b.order-a.order).map(entry => entry.item);
19
+ }
20
+ return { update, dismiss(key) { const entry=entries.get(key); if (entry) entry.dismissed=true; }, clear() { entries.clear(); } };
21
+ }
@@ -1,6 +1,9 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { createThreadAdapter } from "./adapters/codex/renderer.mjs";
2
+ import { createThreadAdapter, createNotificationAdapter } from "./adapters/codex/renderer.mjs";
3
+ import { discoverThreadBindings, loadThreadBindings } from "./adapters/codex/modules.mjs";
4
+ import { createNotificationQueue } from "./domain/notifications.mjs";
3
5
  export async function createPageScriptSource() {
4
6
  const source=await readFile(new URL("./unread-inbox.mjs",import.meta.url),"utf8");
5
- return `const createThreadAdapter=${createThreadAdapter.toString()};\n${source}`;
7
+ return [createThreadAdapter,createNotificationAdapter,discoverThreadBindings,loadThreadBindings,createNotificationQueue]
8
+ .map(fn=>`const ${fn.name}=${fn.toString()};`).join("\n")+"\n"+source;
6
9
  }