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.
- package/DESIGN.md +19 -0
- package/README.md +5 -1
- package/apps/dashboard/DESIGN.md +53 -31
- package/apps/dashboard/app.js +55 -39
- package/apps/dashboard/assets/OFL.txt +93 -0
- package/apps/dashboard/assets/info.svg +15 -0
- package/apps/dashboard/assets/moon.svg +13 -0
- package/apps/dashboard/assets/refresh-cw.svg +16 -0
- package/apps/dashboard/assets/silkscreen.ttf +0 -0
- package/apps/dashboard/assets/sun.svg +21 -0
- package/apps/dashboard/assets/terrain.js +89 -0
- package/apps/dashboard/assets/triangle-alert.svg +15 -0
- package/apps/dashboard/favicon.svg +12 -17
- package/apps/dashboard/index.html +24 -18
- package/apps/dashboard/styles.css +147 -181
- package/assets/brand/README.md +19 -0
- package/assets/brand/minecodex-icon-dark.svg +9 -0
- package/assets/brand/minecodex-icon-light.svg +9 -0
- package/assets/brand/minecodex-icon.svg +8 -0
- package/assets/brand/references/lucide-LICENSE.txt +43 -0
- package/assets/readme/hero.svg +93 -0
- package/features/hide-upsell-banner/README.md +7 -5
- package/features/hide-upsell-banner/codex-feature.json +2 -2
- package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +30 -12
- package/features/images/DESIGN.md +18 -3
- package/features/images/README.md +4 -3
- package/features/images/codex-feature.json +2 -2
- package/features/images/src/adapters/codex/native-image-modules.mjs +220 -0
- package/features/images/src/adapters/codex/native-image-source.mjs +113 -198
- package/features/images/src/assets/prompts-menu.svg +15 -0
- package/features/images/src/frontend/cloud-image-actions.mjs +8 -6
- package/features/images/src/frontend/local-image-source.mjs +2 -1
- package/features/images/src/library-prompt-page.mjs +10 -4
- package/features/images/src/library-prompt-source.mjs +4 -5
- package/features/model-slider/codex-feature.json +2 -2
- package/features/notes/DESIGN.md +1 -1
- package/features/notes/codex-feature.json +2 -2
- package/features/notes/src/frontend/entry.mjs +1 -1
- package/features/session-status/codex-feature.json +2 -2
- package/features/sidebar-focus/DESIGN.md +4 -4
- package/features/sidebar-focus/codex-feature.json +3 -2
- package/features/sidebar-focus/src/adapters/codex/renderer.mjs +1 -1
- package/features/sidebar-focus/src/sidebar-focus.mjs +9 -7
- package/features/sites/DESIGN.md +25 -3
- package/features/sites/README.md +6 -2
- package/features/sites/codex-feature.json +3 -2
- package/features/sites/src/adapters/codex/renderer.mjs +25 -4
- package/features/sites/src/backend/project-sites.mjs +18 -6
- package/features/sites/src/backend/sites-service.mjs +22 -10
- package/features/sites/src/domain/discovery-candidates.mjs +53 -0
- package/features/sites/src/frontend/entry.mjs +76 -38
- package/features/sites/src/frontend/styles.css +3 -3
- package/features/unread-inbox/DESIGN.md +24 -25
- package/features/unread-inbox/codex-feature.json +3 -2
- package/features/unread-inbox/src/adapters/codex/modules.mjs +52 -0
- package/features/unread-inbox/src/adapters/codex/renderer.mjs +90 -0
- package/features/unread-inbox/src/domain/notifications.mjs +21 -0
- package/features/unread-inbox/src/injection-source.mjs +5 -2
- package/features/unread-inbox/src/unread-inbox.mjs +212 -297
- package/features/usage-meter/codex-feature.json +2 -2
- package/package.json +11 -3
- package/packages/cli/native/MineCodex.app/Contents/Info.plist +4 -2
- package/packages/cli/native/MineCodex.app/Contents/MacOS/MineCodexServiceManager +0 -0
- package/packages/cli/native/MineCodex.app/Contents/Resources/MineCodex.icns +0 -0
- package/packages/cli/native/MineCodex.app/Contents/_CodeSignature/CodeResources +11 -0
- package/packages/cli/native/README.md +31 -0
- package/packages/cli/src/browser-control-panel.mjs +48 -0
- package/packages/cli/src/commands.mjs +3 -1
- package/packages/cli/src/control-server.mjs +6 -3
- package/packages/cli/src/platform.mjs +5 -0
- package/packages/runtime-host/README.md +10 -5
- package/packages/runtime-host/src/codex-injection.mjs +12 -3
- package/packages/runtime-host/src/codex-runtime.mjs +10 -0
- package/packages/ui/DESIGN.md +5 -1
- package/packages/ui/assets/arrow-up-right.svg +13 -0
- package/packages/ui/assets/lucide-LICENSE.txt +43 -0
- package/packages/ui/assets/minecodex-menu-16.svg +7 -0
- package/packages/ui/assets/minecodex-menu-20.svg +7 -0
- package/packages/ui/src/components/account-menu.mjs +88 -0
- package/packages/ui/src/components/page.mjs +2 -2
- package/packages/ui/src/foundation/layers.mjs +1 -0
- package/packages/ui/src/install.mjs +3 -0
- package/packages/ui/src/native/account-menu.mjs +21 -0
- package/packages/ui/src/native/page.mjs +17 -2
- 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
|
|
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
|
|
151
|
-
const
|
|
152
|
-
if (!
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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) {
|
|
174
|
-
cacheNative(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 =
|
|
183
|
-
const signature = JSON.stringify([revision, query, page.shared, page.grid
|
|
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
|
|
189
|
-
if (
|
|
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
|
-
|
|
278
|
-
|
|
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 =
|
|
285
|
-
checkbox.setAttribute("aria-label",
|
|
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]
|
|
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
|
-
|
|
6
|
-
- 普通/分栏:在主 Chat 标题前显示原生 list-bullet 图标;整段标题可点击。
|
|
7
|
-
- 全宽标签页:在 Chat 标签页前注入独立的任务图标按钮。
|
|
8
|
-
- 移除扩展铃铛,注入入口存在时,用原生完整 SVG 显示无蓝点的 Sidebar 图标;保留 React 原节点,卸载或展开侧栏时移除替代图标并恢复原节点。
|
|
9
|
-
- 悬停入口150ms展开;鼠标离开入口与浮层250ms后关闭,跨越8px间隙进入浮层会取消关闭。点击入口确保打开;点击外部、Escape 关闭。Enter、空格、ArrowDown 可打开。
|
|
5
|
+
## 触发与原生边界
|
|
10
6
|
|
|
11
|
-
|
|
7
|
+
会话侧栏不可见时,左上方自动显示通知堆栈,包括 Home 折叠状态,以及 Space、Projects 等没有会话侧栏的页面。判断真实会话导航内容的可见性,不把“侧栏展开偏好”当作当前页面具有会话侧栏的证据。Home 临时侧栏或其他原生侧栏 peeking 展开期间隐藏通知;离开后恢复,原生悬浮侧栏保持可进入、可点击。
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
不创建额外入口,不改 Chat 标题的属性、热区和窗口拖拽行为,不替换 Sidebar.left 图标,也不修改 Home 原生蓝点。不点击 Activity、不切换原生筛选或展开偏好。旧入口在 Runtime Host 替换插件时由旧实例 cleanup 恢复。
|
|
14
10
|
|
|
15
|
-
|
|
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
|
-
|
|
13
|
+
堆栈以 Navbar 右侧8px、原生标题栏下沿8px为左上角原点;无标题栏内容时取 Navbar 顶缘。位置不依赖可点击的会话标题。宽320px,窄窗口缩小,右侧/底部保留12px。每条是独立的共享 opaque Popover,圆角16px,间距8px;没有列表标题、整块菜单底板或页面遮罩。堆栈位于页面之上、原生侧栏和原生模态浮层之下(当前角色实现层级30)。空白区域不拦截点击,溢出可滚动。默认仅显示前三条通知;超过三条时,第三条下方露出两层逐级缩进的卡片边缘,并显示“X more notifications”按钮,X 为总数减三。点击后展开全部,使用窗口剩余高度;只有展开内容超出该高度时才启用底部半条左右的渐隐,不再显示遮挡数量文字。滚动到底部解除渐隐。展开状态仅限当前页面;原生内存路由 pathname 变化(包括会话跳转)后恢复三条,并回到堆栈顶部。同页通知刷新、滚动及临时侧栏隐藏不重置。跨页收拢使用320ms缓出高度过渡,多余卡片淡出,层叠与按钮跟随收拢显现;尊重 reduced-motion。通知清空、账号切换或插件重载也重置。
|
|
21
14
|
|
|
22
|
-
|
|
15
|
+
条目保持现有信息结构:13px/18px标题、可用时的12px/16px原生回答摘要(最多两行,超出截断;短摘要不强占两行)、11px/16px项目名与12px原生项目图标。远程图标紧随标题;状态显示需要回复/等待批准标签、相对时间。未读且已结束的条目在时间右侧显示蓝点。内容 padding 四周统一使用功能 spacing token `--mcx-notification-padding:14px`,行间4px。状态文字和无障碍标签随宿主语言,用户标题和摘要不翻译。
|
|
23
16
|
|
|
24
|
-
|
|
17
|
+
新条目与堆栈推移采用320ms ease-out(cubic-bezier(.22,1,.36,1)),从当前视觉位置接续被打断的移动;状态解除以180ms淡出并收拢空位。内容更新保留卡片和点击目标,采用180ms淡入,不重播整卡入场;倒计时只更新标签内的数字与进度,不重建标签。尊重 reduced-motion。键盘可聚焦并点击条目;右上角外沿关闭按钮在 hover/focus 时显示,关闭仅影响本窗口通知,不标记原生已读。
|
|
25
18
|
|
|
26
|
-
|
|
19
|
+
## 状态与生命周期
|
|
27
20
|
|
|
28
|
-
|
|
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
|
-
|
|
32
|
+
Codex 26.930.21537,2026-10-03:隔离 Renderer 确认侧栏只挂载7条,而原生 `allSidebarThreadKeys` 目录含72条。Adapter 从 Navbar 当前 React scope 读取原生目录、需要回复/批准、工作与未读 selector,并复用原生会话导航函数。不会因项目折叠或 Space 页面隐藏 Home 列表而只剩可见条目。项目归属来自原生项目目录;摘要和项目图标在相应原生行已挂载时读取,缺失不编造或保留旧消息。
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
压缩符号及导出名通过语义调用关系动态解析;文件名哈希不作为契约。缺失模块/scope/状态契约时隐藏通知并通过 `nativeCapabilities()` 报告,不使用旧账号或旧会话快照假装成功。尚未创建的任务占位不显示,原有非 Codex 的 ChatGPT 会话不因本次改版新增覆盖范围。
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
验证覆盖队列去重、状态新旧、已读/当前/移除、关闭与再次提醒、源码序列化、原生角色契约、侧栏可见性和卸载。真实等待批准/输入事件需要实际发生才能完成端到端验收;受控样例只证明呈现与交互,不冒充真实通知到达。
|
|
37
37
|
|
|
38
|
-
|
|
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": "
|
|
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
|
|
7
|
+
return [createThreadAdapter,createNotificationAdapter,discoverThreadBindings,loadThreadBindings,createNotificationQueue]
|
|
8
|
+
.map(fn=>`const ${fn.name}=${fn.toString()};`).join("\n")+"\n"+source;
|
|
6
9
|
}
|