minecodex 2.0.2 → 2.0.4

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 (101) hide show
  1. package/DESIGN.md +149 -0
  2. package/apps/dashboard/DESIGN.md +45 -0
  3. package/features/images/DESIGN.md +71 -0
  4. package/features/images/README.md +2 -0
  5. package/features/images/codex-feature.json +2 -1
  6. package/features/images/src/adapters/codex/cloud-import.mjs +52 -0
  7. package/features/images/src/adapters/codex/image-prompt.mjs +22 -0
  8. package/features/images/src/adapters/codex/library-api.mjs +55 -0
  9. package/features/images/src/adapters/codex/library-viewer.mjs +18 -0
  10. package/features/images/src/adapters/codex/prompt-resolver-source.mjs +8 -0
  11. package/features/images/src/backend/cloud-library.mjs +10 -0
  12. package/features/images/src/domain/library-prompt.mjs +7 -0
  13. package/features/images/src/library-prompt-page.mjs +7 -75
  14. package/features/images/src/library-prompt-source.mjs +10 -0
  15. package/features/images/web/app.js +9 -9
  16. package/features/images/web/styles.css +219 -219
  17. package/features/model-slider/DESIGN.md +34 -0
  18. package/features/model-slider/src/adapters/codex/controller.mjs +93 -0
  19. package/features/model-slider/src/domain/model-rules.mjs +143 -0
  20. package/features/model-slider/src/frontend/install.mjs +2110 -0
  21. package/features/model-slider/src/injection-source.mjs +10 -0
  22. package/features/notes/DESIGN.md +13 -15
  23. package/features/notes/README.md +7 -1
  24. package/features/notes/assets/icons/book-open-nav-fill.svg +1 -0
  25. package/features/notes/codex-feature.json +5 -1
  26. package/features/notes/src/{default-prompts.mjs → backend/default-prompts.mjs} +1 -1
  27. package/features/notes/src/{product.mjs → backend/product.mjs} +1 -1
  28. package/features/notes/src/{quick-prompts-server.mjs → backend/quick-prompts-server.mjs} +2 -2
  29. package/features/notes/src/{quick-prompts-page.mjs → frontend/entry.mjs} +64 -257
  30. package/features/notes/src/frontend/icons.json +13 -0
  31. package/features/notes/src/frontend/styles.css +110 -0
  32. package/features/notes/src/main.mjs +2 -2
  33. package/features/notes/vendor/phosphor-LICENSE +21 -0
  34. package/features/session-status/DESIGN.md +10 -8
  35. package/features/session-status/README.md +4 -0
  36. package/features/session-status/codex-feature.json +1 -1
  37. package/features/session-status/src/adapters/codex/context-usage.mjs +46 -0
  38. package/features/session-status/src/adapters/codex/session-log.mjs +81 -0
  39. package/features/session-status/src/adapters/codex/session-reader.mjs +106 -0
  40. package/features/session-status/src/domain/session-stats.mjs +141 -0
  41. package/features/session-status/src/injection-source.mjs +6 -0
  42. package/features/session-status/src/session-status.mjs +42 -102
  43. package/features/sidebar-focus/DESIGN.md +6 -5
  44. package/features/sidebar-focus/codex-feature.json +2 -1
  45. package/features/sidebar-focus/src/adapters/codex/renderer.mjs +28 -0
  46. package/features/sidebar-focus/src/injection-source.mjs +6 -0
  47. package/features/sidebar-focus/src/sidebar-focus.mjs +19 -66
  48. package/features/sites/DESIGN.md +60 -0
  49. package/features/sites/README.md +27 -0
  50. package/features/sites/codex-feature.json +28 -0
  51. package/features/sites/src/adapters/codex/catalog.mjs +26 -0
  52. package/features/sites/src/adapters/codex/renderer.mjs +83 -0
  53. package/features/sites/src/backend/discovery.mjs +73 -0
  54. package/features/sites/src/backend/project-sites.mjs +34 -0
  55. package/features/sites/src/backend/site-launcher.mjs +54 -0
  56. package/features/sites/src/backend/sites-preview.mjs +53 -0
  57. package/features/sites/src/backend/sites-service.mjs +162 -0
  58. package/features/sites/src/domain/site.mjs +40 -0
  59. package/features/sites/src/frontend/entry.mjs +383 -0
  60. package/features/sites/src/frontend/icons.json +12 -0
  61. package/features/sites/src/frontend/styles.css +72 -0
  62. package/features/sites/src/injection-source.mjs +7 -0
  63. package/features/sites/vendor/lucide-LICENSE +35 -0
  64. package/features/unread-inbox/DESIGN.md +6 -2
  65. package/features/unread-inbox/codex-feature.json +2 -1
  66. package/features/unread-inbox/src/adapters/codex/renderer.mjs +66 -0
  67. package/features/unread-inbox/src/injection-source.mjs +6 -0
  68. package/features/unread-inbox/src/unread-inbox.mjs +34 -89
  69. package/features/usage-meter/DESIGN.md +9 -9
  70. package/features/usage-meter/README.md +2 -1
  71. package/features/usage-meter/codex-feature.json +3 -3
  72. package/features/usage-meter/src/adapters/codex/renderer.mjs +61 -0
  73. package/features/usage-meter/src/injection-source.mjs +6 -0
  74. package/features/usage-meter/src/usage-meter.mjs +27 -66
  75. package/package.json +17 -4
  76. package/packages/cli/src/config.mjs +2 -1
  77. package/packages/codex-adapter/README.md +15 -0
  78. package/packages/codex-adapter/package.json +1 -0
  79. package/packages/codex-adapter/src/renderer/environment.mjs +53 -0
  80. package/packages/runtime-host/README.md +27 -0
  81. package/packages/runtime-host/src/codex-injection.mjs +32 -2393
  82. package/packages/runtime-host/src/codex-runtime.mjs +21 -3
  83. package/packages/runtime-host/src/feature-registry.mjs +28 -3
  84. package/packages/ui/DESIGN.md +31 -0
  85. package/packages/ui/package.json +8 -0
  86. package/packages/ui/src/components/button.mjs +21 -0
  87. package/packages/ui/src/components/composer-menu.mjs +18 -0
  88. package/packages/ui/src/components/dialog.mjs +55 -0
  89. package/packages/ui/src/components/navigation-rail.mjs +77 -0
  90. package/packages/ui/src/components/page.mjs +8 -0
  91. package/packages/ui/src/components/tooltip.mjs +96 -0
  92. package/packages/ui/src/foundation/layers.mjs +14 -0
  93. package/packages/ui/src/foundation/surfaces.mjs +34 -0
  94. package/packages/ui/src/install.mjs +22 -0
  95. package/packages/ui/src/native/navigation-rail.mjs +33 -0
  96. package/packages/ui/src/native/theme.mjs +35 -0
  97. package/packages/{runtime-host/src/codex-design-contract.mjs → ui/src/native/tokens.mjs} +67 -24
  98. package/packages/runtime-host/src/cloud-images.mjs +0 -123
  99. package/packages/runtime-host/src/feature-tooltips.mjs +0 -65
  100. package/packages/runtime-host/src/session-status-reader.mjs +0 -340
  101. /package/features/notes/src/{prompt-schema.mjs → backend/prompt-schema.mjs} +0 -0
@@ -0,0 +1,383 @@
1
+ const nativeSites=createSitesAdapter(signal,config.codex);
2
+ // 复用 Prompts 已验证的原生图标与 Lucide 图标,保持交互一致。
3
+ const ICONS = config.resources.icons;
4
+ const MENU_CLASSES = {"menu": "no-drag z-50 m-px flex select-none flex-col overflow-y-auto bg-surface-elevated-secondary/90 text-default ring-border ring-[0.5px] shadow-xl-spread backdrop-blur-sm rounded-2xl p-[var(--app-menu-gutter,var(--mcx-spacing))] min-w-[var(--app-menu-min-width,220px)]", "item": "no-drag outline-hidden flex min-h-[var(--app-menu-item-height,0px)] shrink-0 items-center justify-center p-[var(--app-menu-item-padding,var(--padding-row-y)_var(--padding-row-x))] text-(length:--app-menu-item-font-size,var(--mcx-text-sm)) leading-(--app-menu-item-line-height,var(--mcx-text-sm--line-height)) rounded-xl text-default group hover:bg-primary-ghost-hover focus:bg-primary-ghost-hover cursor-interaction flex flex-col", "row": "flex w-full min-w-0 items-center gap-[var(--spacing-menu-item-content,calc(var(--mcx-spacing)*1.5))]", "leading": "_leadingIcon_ml6jk_2"};
5
+ const text = (en, zh) => (document.documentElement.lang || navigator.language).startsWith("zh") ? zh : en;
6
+ const host = payload => config.requestHostAction("sites", payload);
7
+ const owned = "data-mcx-sites";
8
+ let disposed = false, state = { sites: [], projects: [], threads: [] }, addButton, rowsRoot, fallback, dialog, menu;
9
+ let rowSignature = "", nativeButton, nativeInner, nativePlus, nativeSiteIcon, nativeMore, timer, revision = 0, opening = false;
10
+ let addedTab, addedOnly = false, nativeExternal, nativeGlobe, nativeActionsClass, tabClasses;
11
+ let currentPage, refreshAt = 0, pendingLayout = null;
12
+ const style = document.createElement("style");
13
+ style.setAttribute(owned, "style");
14
+ style.textContent = config.resources.styles;
15
+ document.head.append(style);
16
+ function element(tag, className, content) { const e = document.createElement(tag); if (className) e.className = className; if (content != null) e.textContent = content; return e; }
17
+ function icon(name) { return new DOMParser().parseFromString(ICONS[name], "image/svg+xml").documentElement.cloneNode(true); }
18
+ function iconButton(label, name, action) { const b = element("button", "mcx-site-icon"); b.type = "button"; b.setAttribute("aria-label", label); b.setAttribute("data-minecodex-tooltip", label); b.append(icon(name)); b.onclick = action; return b; }
19
+
20
+
21
+ function visible(e) { return !!e?.isConnected && e.getBoundingClientRect().width > 0 && e.getBoundingClientRect().height > 0; }
22
+ function button(label, action, primary = false) {
23
+ const b = element("button", nativeButton || ""); b.type = "button";
24
+ b.dataset.color = primary ? "primary" : "secondary"; b.dataset.variant = primary ? "solid" : "outline"; b.dataset.size = "lg"; b.dataset.pill = "";
25
+ b.append(element("span", nativeInner || "", label)); b.addEventListener("click", action); return b;
26
+ }
27
+ function dialogButton(label, action, primary = false, tone = "secondary") {
28
+ const b = config.ui.action(element("button", "", label), primary ? "primary" : tone);
29
+ b.type = "button"; b.addEventListener("click", action); return b;
30
+ }
31
+ function setButtonDisabled(button, disabled) {
32
+ button.disabled = disabled; button.setAttribute("aria-disabled", String(disabled)); button.toggleAttribute("data-disabled", disabled);
33
+ }
34
+ function notice(message) {
35
+ document.querySelector(`[${owned}=notice]`)?.remove();
36
+ const n = element("div", "", message); n.setAttribute(owned, "notice"); n.setAttribute("role", "status"); document.body.append(n);
37
+ setTimeout(() => n.remove(), 7000);
38
+ }
39
+ async function refresh() {
40
+ const data = await host({ operation: "list" });
41
+ if (disposed) return; state = data; revision++; rowSignature = ""; refreshAt = Date.now(); schedule();
42
+ }
43
+
44
+ function cacheNative(page) {
45
+ ({nativeButton,nativeInner,nativePlus,nativeSiteIcon,nativeMore,nativeExternal,nativeGlobe,nativeActionsClass}=nativeSites.templates(page));
46
+ }
47
+ function closeMenu() { const previous = menu?.trigger; previous?.setAttribute("aria-expanded", "false"); previous?.setAttribute("data-state", "closed"); menu?.node.remove(); menu = null; previous?.focus({ preventScroll: true }); }
48
+ function showMenu(trigger, site) {
49
+ closeMenu();
50
+ const m = element("div", MENU_CLASSES.menu); config.ui.surface(m, "dropdown"); m.setAttribute(owned, "menu"); m.setAttribute("role", "menu"); m.tabIndex = -1;
51
+ function menuItem(label, glyph) {
52
+ const item = element("button", MENU_CLASSES.item); item.type = "button"; item.setAttribute("role", "menuitem"); item.tabIndex = -1;
53
+ const row = element("div", MENU_CLASSES.row), leading = element("span", MENU_CLASSES.leading), svg = icon(glyph); svg.setAttribute("aria-hidden", "true");
54
+ leading.append(svg); row.append(leading, element("span", "flex-1 min-w-0 truncate", label)); item.append(row); return item;
55
+ }
56
+ const edit = menuItem(text("Edit", "编辑"), "edit"); edit.onclick = () => { closeMenu(); showEditor(site); };
57
+ const remove = menuItem(text("Remove", "移除"), "remove");
58
+ remove.onclick = async () => { closeMenu(); try { await host({ operation: "remove", id: site.id }); await refresh(); } catch (e) { notice(e.message); } };
59
+ if (site.origin.kind === "external") m.append(edit); m.append(remove); document.body.append(m); const r = trigger.getBoundingClientRect();
60
+ m.style.left = Math.max(8, Math.min(r.right - m.offsetWidth, innerWidth - m.offsetWidth - 8)) + "px";
61
+ m.style.top = Math.max(8, Math.min(r.bottom + 4, innerHeight - m.offsetHeight - 8)) + "px";
62
+ menu = { node: m, trigger }; trigger.setAttribute("aria-expanded", "true"); trigger.setAttribute("data-state", "open"); m.focus();
63
+ m.addEventListener("keydown", e => { if (["ArrowUp", "ArrowDown"].includes(e.key)) { e.preventDefault(); const items = [...m.children]; const current = items.indexOf(document.activeElement); items[current < 0 ? (e.key === "ArrowUp" ? items.length - 1 : 0) : (current + (e.key === "ArrowUp" ? -1 : 1) + items.length) % items.length].focus(); } });
64
+ }
65
+ function siteSecondary(site) {
66
+ const projectId = site.projectId || site.origin?.projectId;
67
+ const project = site.projectName || state.projects.find(p => p.id === projectId)?.name;
68
+ if (project) {
69
+ const port = site.status === "running" && site.url ? new URL(site.url).port : "";
70
+ return port ? `${project}(${port})` : project;
71
+ }
72
+ return site.url ? new URL(site.url).host : text("Not started", "未启动");
73
+ }
74
+ async function startProjectSite(id) {
75
+ notice(text("Starting website…", "正在启动网站…"));
76
+ for (let attempt = 0; attempt < 40 && !disposed; attempt++) {
77
+ const result = await host({ operation: "start", id }); if (!result.pending) return result.url;
78
+ await new Promise(resolve => setTimeout(resolve, 1000));
79
+ }
80
+ throw new Error(text("Website startup timed out.", "网站启动超时。"));
81
+ }
82
+ const previewRequests = new Map();
83
+ function loadPreview(container, url, cachedOnly = false) {
84
+ container.classList.remove("mcx-site-placeholder"); container.setAttribute("aria-busy", "true");
85
+ const skeleton = element("div", "mcx-site-skeleton"); skeleton.setAttribute("aria-hidden", "true"); container.replaceChildren(skeleton);
86
+ const placeholder = () => { container.classList.add("mcx-site-placeholder"); container.removeAttribute("aria-busy"); container.replaceChildren(); if (nativeSiteIcon) container.append(nativeSiteIcon.cloneNode(true)); };
87
+ const key = cachedOnly ? `cached:${url}` : url;
88
+ let request = previewRequests.get(key);
89
+ if (!request) {
90
+ request = (async () => {
91
+ for (let attempt = 0; attempt < 60 && !disposed; attempt++) {
92
+ const result = await host({ operation: "preview", url, cachedOnly });
93
+ if (!result.pending) return result;
94
+ await new Promise(resolve => setTimeout(resolve, 1500));
95
+ }
96
+ return { image: null };
97
+ })().catch(() => ({ image: null })); previewRequests.set(key, request);
98
+ request.then(result => { if (!result.image) previewRequests.delete(key); });
99
+ }
100
+ request.then(({ image }) => {
101
+ if (disposed || !container.isConnected) return;
102
+ if (!image) { placeholder(); return; }
103
+ const img = element("img"); img.alt = ""; img.className = "mcx-site-thumbnail";
104
+ img.onload = () => { if (!disposed && container.isConnected) { container.removeAttribute("aria-busy"); container.replaceChildren(img); } };
105
+ img.onerror = placeholder; img.src = image;
106
+ });
107
+ }
108
+ function row(site, grid) {
109
+ const r = element("div", grid ? "group/site-card relative grid h-[210px] min-w-0 grid-rows-[148px_62px] overflow-hidden rounded-2xl border border-subtle bg-surface-card shadow-sm" : "relative col-span-full grid min-h-[82px] grid-cols-subgrid items-center overflow-hidden rounded-xl px-4 py-4 select-none hover:bg-primary-ghost-hover");
110
+ r.setAttribute(owned, "entry"); r.dataset.siteId = site.id;
111
+ const open = element("button", "absolute inset-0 cursor-interaction rounded-xl bg-transparent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-inset");
112
+ open.type = "button"; open.setAttribute("aria-label", text("Open ", "打开 ") + site.title); open.onclick = () => openSite(site); r.append(open);
113
+ const preview = element("div", "mcx-site-preview " + (grid ? "pointer-events-none" : "relative h-[50px] w-20 shrink-0 overflow-hidden rounded-[6px]"));
114
+ if (nativeSiteIcon) preview.append(nativeSiteIcon.cloneNode(true));
115
+ if (site.url) loadPreview(preview, site.url, site.status === "stopped"); else preview.classList.add("mcx-site-placeholder");
116
+ const info = element("div", grid ? "pointer-events-none relative z-10 flex min-w-0 items-center gap-3 px-4" : "pointer-events-none relative z-10 flex min-w-0 items-center gap-4");
117
+ const names = element("div", "min-w-0 flex-1"); names.append(element("div", "mcx-site-name", site.title), element("div", "mcx-site-meta", siteSecondary(site)));
118
+ if (!grid) info.append(preview); info.append(names);
119
+ const more = button(text("Site options", "站点选项"), () => showMenu(more, site));
120
+ more.classList.add("mcx-site-menu", "justify-self-end"); more.dataset.variant = "ghost"; more.dataset.uniform = ""; more.style.pointerEvents = "auto"; more.setAttribute("aria-label", text("Options for ", "站点选项:") + site.title); more.setAttribute("aria-haspopup", "menu");
121
+ more.firstElementChild.replaceChildren(); if (nativeMore) more.firstElementChild.append(nativeMore.cloneNode(true)); else more.firstElementChild.textContent = "⋯";
122
+ const actions = element("div", nativeActionsClass || "relative z-10 flex items-center gap-2 justify-end ps-4");
123
+ if (grid) actions.className = "pointer-events-auto relative z-10 flex items-center gap-2";
124
+ {
125
+ const external = nativeExternal ? nativeExternal.cloneNode(true) : button("", () => {});
126
+ if (!nativeExternal) { external.dataset.variant = "ghost"; external.dataset.uniform = ""; external.dataset.iconSize = "sm"; external.firstElementChild.replaceChildren(icon("external")); } external.setAttribute("aria-label", text("Open in default browser: ", "在默认浏览器打开:") + site.title);
127
+ external.onclick = async e => {
128
+ e.stopPropagation();
129
+ try {
130
+ if (!window.electronBridge?.sendMessageFromView) throw new Error(text("Native browser opening is unavailable.", "原生浏览器打开通道暂不可用。"));
131
+ const resolved = await host({ operation: "resolve", id: site.id });
132
+ if (resolved.missing) throw new Error(text("The original project is unavailable.", "原项目已不可用。"));
133
+ const url = resolved.site.entryId && resolved.site.status !== "running" ? await startProjectSite(site.id) : resolved.site.url;
134
+ await window.electronBridge.sendMessageFromView({ type: "open-in-browser", url, initiator: "sites_library", openTarget: "external-browser", disposition: "new-tab", source: "manual" });
135
+ } catch (error) { notice(error.message); }
136
+ };
137
+ actions.append(external);
138
+ }
139
+ actions.append(more);
140
+ if (grid) { info.append(actions); r.append(preview, info); }
141
+ else {
142
+ const sharing = element("div", "mcx-site-sharing pointer-events-none relative z-10 flex min-w-0 items-center gap-1.5 ps-4 text-sm leading-5 text-default [@container_(max-width:520px)]:sr-only");
143
+ const glyph = site.hosting === "local" ? icon("monitor") : (nativeGlobe?.cloneNode(true) || icon("globe"));
144
+ if (glyph) { glyph.setAttribute("aria-hidden", "true"); sharing.append(glyph); }
145
+ sharing.append(element("span", "truncate", site.hosting === "local" ? text("Local address", "本机地址") : text("External site", "外部站点")));
146
+ r.append(info, sharing, actions);
147
+ }
148
+ return r;
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);
159
+ }
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));
165
+ });
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));
169
+ }
170
+ function reconcile() {
171
+ if (disposed) return;
172
+ 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);
175
+ if (currentPage !== page.page) { currentPage = page.page; rowSignature = ""; if (Date.now() - refreshAt > 1500) refresh().catch(e => notice(e.message)); }
176
+ if (!addButton?.isConnected && page.create) {
177
+ addButton = button(text("Add site", "添加站点"), () => showDiscovery()); addButton.setAttribute(owned, "add");
178
+ if (nativePlus) addButton.firstElementChild.prepend(nativePlus.cloneNode(true));
179
+ page.create.before(addButton);
180
+ }
181
+ 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]);
184
+ if (signature === rowSignature && rowsRoot?.isConnected && (page.list ? rowsRoot.parentElement === page.list : fallback?.isConnected)) return;
185
+ rowsRoot?.remove(); fallback?.remove(); fallback = null;
186
+ document.querySelectorAll("[data-mcx-sites-empty-suppressed]").forEach(e => e.removeAttribute("data-mcx-sites-empty-suppressed"));
187
+ 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", "");
190
+ if (page.list) page.list.append(rowsRoot);
191
+ else {
192
+ fallback = element("div", "@container pb-3"); fallback.setAttribute(owned, "fallback");
193
+ 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
+ page.content.append(fallback);
196
+ }
197
+ 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
+ }
200
+ function schedule() { clearTimeout(timer); timer = setTimeout(reconcile, 60); }
201
+ function newDialog(title, back) {
202
+ dialog?.close(); dialog?.remove(); closeMenu();
203
+ const previous = document.activeElement; const d = element("dialog"); d.setAttribute(owned, "dialog"); d.setAttribute("aria-labelledby", "mcx-sites-dialog-title");
204
+ const heading = config.ui.heading(d, { title, closeLabel:text("Close", "关闭"), onClose:()=>d.close(),
205
+ back:back ? iconButton(text("Back", "返回"), "back", back) : null });
206
+ heading.querySelector("h2").id = "mcx-sites-dialog-title";
207
+ document.body.append(d); dialog = d;
208
+ d.addEventListener("close", () => { d.remove(); if (dialog === d) dialog = null; previous?.focus({ preventScroll: true }); });
209
+ d.addEventListener("click", e => { if (e.target === d) { const r = d.getBoundingClientRect(); if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) d.close(); } });
210
+ d.showModal(); return d;
211
+ }
212
+ async function showDiscovery() {
213
+ const d = newDialog(text("Add site", "添加站点"));
214
+ const tabs = element("div", "mcx-site-modes"); tabs.setAttribute("role", "tablist"); tabs.setAttribute("aria-label", text("Add site method", "添加方式"));
215
+ const modes = [
216
+ { id: "projects", label: text("Project", "项目") },
217
+ { id: "url", label: text("Custom", "自定义") }
218
+ ].map(mode => {
219
+ const tab = element("button", "mcx-site-mode", mode.label), panel = element("div"); tab.type = "button";
220
+ tab.id = `mcx-site-tab-${mode.id}`; panel.id = `mcx-site-panel-${mode.id}`;
221
+ tab.setAttribute("role", "tab"); tab.setAttribute("aria-controls", panel.id);
222
+ panel.setAttribute("role", "tabpanel"); panel.setAttribute("aria-labelledby", tab.id);
223
+ tabs.append(tab); return { tab, panel };
224
+ });
225
+ d.append(tabs, ...modes.map(mode => mode.panel));
226
+ function select(index) {
227
+ d.dispatchEvent(new Event("mcx-sites-mode-change"));
228
+ modes.forEach(({ tab, panel }, i) => { const active = i === index; tab.setAttribute("aria-selected", String(active)); tab.tabIndex = active ? 0 : -1; panel.hidden = !active; });
229
+ }
230
+ modes.forEach(({ tab }, index) => {
231
+ tab.onclick = () => select(index);
232
+ tab.onkeydown = e => {
233
+ if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return;
234
+ e.preventDefault(); const next = e.key === "Home" ? 0 : e.key === "End" ? modes.length - 1 : (index + (e.key === "ArrowRight" ? 1 : -1) + modes.length) % modes.length;
235
+ select(next); modes[next].tab.focus();
236
+ };
237
+ });
238
+ select(0); mountEditor(d, modes[1].panel);
239
+ await mountDiscovery(d, modes[0].panel);
240
+ }
241
+ async function mountDiscovery(d, panel) {
242
+ const content = element("div", "mcx-site-content"); panel.append(content);
243
+ const results = element("div", "mcx-site-results"); results.setAttribute("aria-live", "polite"); content.append(results);
244
+ const error = element("div"); error.setAttribute("role", "alert"); content.append(error);
245
+ let candidates = [], loaded = false, saving = false;
246
+ const selected = new Set(), completed = new Set();
247
+ const footer = config.ui.footer(element("div", "mcx-site-footer"));
248
+ const cancel = dialogButton(text("Cancel", "取消"), () => d.close(), false, "ghost");
249
+ const add = dialogButton(text("Add", "添加"), async () => {
250
+ if (saving || !selected.size) return;
251
+ saving = true; error.textContent = ""; render();
252
+ try {
253
+ // 逐项保存,失败时保留尚未完成的选择,避免重复提交成功项。
254
+ for (const candidate of [...selected]) {
255
+ await host({ operation: "save", site: { ...candidate, origin: { kind: "project", projectId: candidate.projectId } } });
256
+ completed.add(candidate); selected.delete(candidate);
257
+ }
258
+ await refresh(); d.close();
259
+ } catch (e) { error.textContent = e.message; }
260
+ finally { saving = false; if (d.isConnected) render(); }
261
+ }, true);
262
+ footer.append(cancel, add); panel.append(footer);
263
+ function updateActions() { setButtonDisabled(add, saving || !selected.size); setButtonDisabled(cancel, saving); }
264
+ function render() {
265
+ updateActions(); results.replaceChildren();
266
+ results.setAttribute("aria-busy", String(!loaded));
267
+ if (!loaded) {
268
+ results.append(element("span", "sr-only", text("Looking for websites…", "正在查找网页…")));
269
+ ["64%", "48%", "58%"].forEach(width => {
270
+ const row = element("div", "mcx-site-scan-row"); row.setAttribute("aria-hidden", "true");
271
+ const art = element("div", "mcx-site-candidate-art"); art.append(element("div", "mcx-site-skeleton"));
272
+ const copy = element("div", "mcx-site-scan-copy"), title = element("div", "mcx-site-skeleton"); title.style.width = width;
273
+ copy.append(title, element("div", "mcx-site-skeleton")); row.append(art, copy); results.append(row);
274
+ });
275
+ return;
276
+ }
277
+ if (!candidates.length) results.append(element("div", "mcx-site-empty", text("No project websites found", "尚未发现项目网页")));
278
+ for (const candidate of candidates) {
279
+ const item = element("label", "mcx-site-candidate");
280
+ const art = element("span", "mcx-site-candidate-art"); if (nativeSiteIcon) art.append(nativeSiteIcon.cloneNode(true));
281
+ 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
+ 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);
286
+ checkbox.onchange = () => { if (checkbox.checked) selected.add(candidate); else selected.delete(candidate); updateActions(); };
287
+ item.append(art, info, checkbox); if (candidate.url) loadPreview(art, candidate.url, candidate.status !== "running"); else art.classList.add("mcx-site-placeholder");
288
+ results.append(item);
289
+ }
290
+ }
291
+ async function load() {
292
+ loaded = false; render(); error.textContent = "";
293
+ try { await refresh(); const data = await host({ operation: "discover" }); if (!d.isConnected) return; candidates = data.candidates; }
294
+ catch { error.textContent = text("Couldn't find services. Reopen this dialog to try again.", "暂时无法发现服务,请重新打开弹窗重试。"); }
295
+ finally { loaded = true; if (d.isConnected) render(); }
296
+ }
297
+ await load();
298
+ }
299
+ function showEditor(site, problem) {
300
+ const d = newDialog(text("Edit site", "编辑站点"));
301
+ mountEditor(d, d, site, problem).focus();
302
+ }
303
+ function mountEditor(d, container, site, problem) {
304
+ const form = element("form"), content = element("div", "mcx-site-content"); form.append(content); container.append(form);
305
+ 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
+ const url = field(text("URL", "网址"), "url", site?.url, "url"); url.required = true; url.placeholder = "http://localhost:3000";
307
+ const title = field(text("Name", "名称"), "title", site?.title); title.maxLength = 160; title.required = true;
308
+ let origin = site?.origin || { kind: "external" };
309
+ 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);
311
+ let submitting = false;
312
+ function validFields() {
313
+ if (!title.value.trim() || title.value.trim().length > 160 || !url.value.trim() || !url.validity.valid) return false;
314
+ try { const parsed = new URL(url.value.trim()); return ["http:", "https:"].includes(parsed.protocol) && !!parsed.hostname && !parsed.username && !parsed.password; }
315
+ catch { return false; }
316
+ }
317
+ function updateSave() { setButtonDisabled(save, submitting || !validFields()); }
318
+ form.addEventListener("input", updateSave); form.addEventListener("change", updateSave); updateSave();
319
+ form.onsubmit = async e => {
320
+ e.preventDefault(); if (submitting || !validFields()) { updateSave(); return; }
321
+ 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(); }
323
+ catch (e) { error.textContent = e.message; }
324
+ finally { submitting = false; updateSave(); }
325
+ };
326
+ return url;
327
+ }
328
+ async function until(check, milliseconds = 7000) {
329
+ const end = Date.now() + milliseconds;
330
+ while (!disposed && Date.now() < end) { const value = check(); if (value) return value; await new Promise(r => setTimeout(r, 80)); }
331
+ throw new Error(text("The native view did not become ready. Please try again.", "原生视图未就绪,请重试。"));
332
+ }
333
+
334
+
335
+ async function applyWebLayout(token) {
336
+ try {
337
+ await until(() => pendingLayout !== token || (nativeSites.path() !== "/space/sites" && (nativeSites.fullViewControl(true) || nativeSites.fullViewControl(false))));
338
+ if (pendingLayout !== token) return;
339
+ const hide = [...document.querySelectorAll("button")].find(e => visible(e) && /^(Hide sidebar|隐藏侧栏|隐藏侧边栏|隱藏側邊欄|收起侧栏)$/.test(e.getAttribute("aria-label") || "")); hide?.click();
340
+ // 使用原生完整视图入口,保留原生网页标签和底部聊天输入框。
341
+ nativeSites.fullViewControl(true)?.click();
342
+ } catch (e) { if (pendingLayout === token && !disposed) notice(e.message); }
343
+ finally { if (pendingLayout === token) pendingLayout = null; }
344
+ }
345
+ async function openSite(site) {
346
+ if (opening) return; opening = true;
347
+ try {
348
+ const resolved = await host({ operation: "resolve", id: site.id });
349
+ if (!resolved.missing && resolved.site.entryId && resolved.site.status !== "running") {
350
+ resolved.site.url = await startProjectSite(site.id); resolved.site.status = "running"; await refresh();
351
+ }
352
+ if (resolved.missing) { await refresh(); notice(text("The original project or chat is unavailable. Remove this entry and add it again from an available project.", "原项目或聊天已不可用,请移除此条目后从可用项目重新添加。")); return; }
353
+ await nativeSites.open(resolved.site.origin, resolved.site.url, resolved.cwd);
354
+ const token = {}; pendingLayout = token; await applyWebLayout(token);
355
+ } catch (e) { notice(e.message); }
356
+ finally { opening = false; }
357
+ }
358
+ function onClick(e) {
359
+ if (addedOnly && e.target.closest('[aria-label="Site ownership"] button:not([data-mcx-sites])')) { addedOnly = false; rowSignature = ""; schedule(); }
360
+ if (menu && !menu.node.contains(e.target) && !menu.trigger.contains(e.target)) closeMenu();
361
+ if (e.target.closest(`[${owned}]`)) return;
362
+ const row = e.target.closest("[data-appgen-row], [data-testid=sites-grid] > article");
363
+ if (!row || !nativeSites.pageInfo()) return;
364
+ const b = e.target.closest("button");
365
+ // 只跟随官方打开动作;分享、菜单和 Create 不触发布局变化。
366
+ if (b && !b.hasAttribute("aria-haspopup") && /^(Edit |Open |编辑|打开)/.test(b.getAttribute("aria-label") || "")) {
367
+ const token = {}; pendingLayout = token; void applyWebLayout(token);
368
+ }
369
+ }
370
+ function onKey(e) { if (e.key === "Escape" && menu) { e.preventDefault(); closeMenu(); } else if (e.key === "Tab" && menu) closeMenu(); }
371
+ function onPointer(e) { if (e.target.closest('[role="separator"]')) pendingLayout = null; }
372
+ const observer = new MutationObserver(records => { if (records.some(r => !r.target.closest?.(`[${owned}]`))) schedule(); });
373
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["aria-pressed"] });
374
+ document.addEventListener("input", schedule); document.addEventListener("click", onClick, true); document.addEventListener("keydown", onKey, true); document.addEventListener("pointerdown", onPointer, true);
375
+ refresh().catch(e => notice(e.message)); schedule();
376
+ return () => {
377
+ disposed = true; pendingLayout = null; clearTimeout(timer); observer.disconnect();
378
+ 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
+ closeMenu(); dialog?.close(); document.querySelectorAll(`[${owned}]`).forEach(e => e.remove());
382
+ document.querySelectorAll("[data-mcx-sites-empty-suppressed]").forEach(e => e.removeAttribute("data-mcx-sites-empty-suppressed"));
383
+ };
@@ -0,0 +1,12 @@
1
+ {
2
+ "close": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M10.962 4.29539C11.1669 4.09057 11.4992 4.09076 11.7042 4.29539C11.9092 4.50039 11.9091 4.83255 11.7042 5.03758L8.74229 7.99949L11.7052 10.9624C11.9097 11.1673 11.9097 11.4996 11.7052 11.7046C11.5003 11.9095 11.168 11.9093 10.963 11.7046L8.0001 8.74168L5.03721 11.7046C4.83216 11.9093 4.49994 11.9095 4.29502 11.7046C4.09047 11.4996 4.09045 11.1673 4.29502 10.9624L7.25791 7.99949L4.296 5.03758C4.09101 4.83255 4.09099 4.5004 4.296 4.29539C4.50105 4.09079 4.8333 4.09054 5.03819 4.29539L8.0001 7.2573L10.962 4.29539Z\" fill=\"currentColor\"/></svg>",
3
+ "back": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M9.87117 11.6289C10.0762 11.8339 10.0762 12.166 9.87117 12.3711C9.66614 12.5761 9.334 12.5761 9.12898 12.3711L5.36433 8.60641C5.0296 8.27129 5.0296 7.72863 5.36433 7.39351L9.12898 3.62887C9.334 3.42384 9.66614 3.42384 9.87117 3.62887C10.0762 3.83389 10.0762 4.16603 9.87117 4.37105L6.24226 7.99996L9.87117 11.6289Z\" fill=\"currentColor\"/></svg>",
4
+ "chevron": "<!-- @license lucide-static v0.577.0 - ISC -->\n<svg\n class=\"lucide lucide-chevron-down\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <path d=\"m6 9 6 6 6-6\" />\n</svg>\n",
5
+ "search": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M7.32849 1.91016C10.2877 1.91016 12.6867 4.30938 12.6869 7.26855C12.6869 8.57363 12.2192 9.7688 11.4437 10.6982L14.0433 13.2979C14.2481 13.5028 14.2481 13.8351 14.0433 14.04C13.8384 14.245 13.5062 14.2449 13.3011 14.04L10.6957 11.4346C9.77536 12.1793 8.60462 12.627 7.32849 12.627C4.36931 12.6268 1.97009 10.2278 1.97009 7.26855C1.97027 4.30949 4.36942 1.91033 7.32849 1.91016ZM7.32849 2.96094C4.94932 2.96111 3.02105 4.88939 3.02087 7.26855C3.02087 9.64787 4.94921 11.577 7.32849 11.5771C9.70792 11.5771 11.6371 9.64798 11.6371 7.26855C11.6369 4.88928 9.70781 2.96094 7.32849 2.96094Z\" fill=\"currentColor\"/></svg>",
6
+ "check": "<!-- @license lucide-static v0.577.0 - ISC -->\n<svg\n class=\"lucide lucide-check\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <path d=\"M20 6 9 17l-5-5\" />\n</svg>\n",
7
+ "monitor": "<svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.77\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <rect width=\"20\" height=\"14\" x=\"2\" y=\"3\" rx=\"2\" />\n <line x1=\"8\" x2=\"16\" y1=\"21\" y2=\"21\" />\n <line x1=\"12\" x2=\"12\" y1=\"17\" y2=\"21\" />\n</svg>\n",
8
+ "external": "<svg aria-hidden=\"true\" class=\"_Icon_uttcz_1\" focusable=\"false\" height=\"16\" viewBox=\"0 0 16 16\" width=\"16\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M11.9489 3.47949C12.0996 3.46465 12.2552 3.51279 12.3707 3.62793C12.4862 3.74328 12.5337 3.89898 12.5192 4.0498C12.5205 4.06633 12.525 4.08275 12.525 4.09961V10.667C12.5249 10.9565 12.2901 11.191 12.0006 11.1914C11.7108 11.1914 11.4754 10.9568 11.4752 10.667V5.2666L4.37172 12.376C4.16672 12.5807 3.83352 12.5808 3.62855 12.376C3.42373 12.1711 3.42384 11.8388 3.62855 11.6338L10.733 4.52539H5.33363C5.04368 4.52539 4.80824 4.28995 4.80824 4C4.80824 3.71005 5.04368 3.47461 5.33363 3.47461H11.9C11.9165 3.47462 11.9327 3.47822 11.9489 3.47949Z\" fill=\"currentColor\"></path></svg>",
9
+ "globe": "<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\" class=\"icon-xs shrink-0\" aria-hidden=\"true\"><path d=\"M10 2.125C14.3492 2.125 17.875 5.65076 17.875 10C17.875 14.3492 14.3492 17.875 10 17.875C5.65076 17.875 2.125 14.3492 2.125 10C2.125 5.65076 5.65076 2.125 10 2.125ZM7.88672 10.625C7.94334 12.3161 8.22547 13.8134 8.63965 14.9053C8.87263 15.5194 9.1351 15.9733 9.39453 16.2627C9.65437 16.5524 9.86039 16.625 10 16.625C10.1396 16.625 10.3456 16.5524 10.6055 16.2627C10.8649 15.9733 11.1274 15.5194 11.3604 14.9053C11.7745 13.8134 12.0567 12.3161 12.1133 10.625H7.88672ZM3.40527 10.625C3.65313 13.2734 5.45957 15.4667 7.89844 16.2822C7.7409 15.997 7.5977 15.6834 7.4707 15.3486C6.99415 14.0923 6.69362 12.439 6.63672 10.625H3.40527ZM13.3633 10.625C13.3064 12.439 13.0059 14.0923 12.5293 15.3486C12.4022 15.6836 12.2582 15.9969 12.1006 16.2822C14.5399 15.467 16.3468 13.2737 16.5947 10.625H13.3633ZM12.1006 3.7168C12.2584 4.00235 12.4021 4.31613 12.5293 4.65137C13.0059 5.90775 13.3064 7.56102 13.3633 9.375H16.5947C16.3468 6.72615 14.54 4.53199 12.1006 3.7168ZM10 3.375C9.86039 3.375 9.65437 3.44756 9.39453 3.7373C9.1351 4.02672 8.87263 4.48057 8.63965 5.09473C8.22547 6.18664 7.94334 7.68388 7.88672 9.375H12.1133C12.0567 7.68388 11.7745 6.18664 11.3604 5.09473C11.1274 4.48057 10.8649 4.02672 10.6055 3.7373C10.3456 3.44756 10.1396 3.375 10 3.375ZM7.89844 3.7168C5.45942 4.53222 3.65314 6.72647 3.40527 9.375H6.63672C6.69362 7.56102 6.99415 5.90775 7.4707 4.65137C7.59781 4.31629 7.74073 4.00224 7.89844 3.7168Z\"></path></svg>",
10
+ "edit": "<svg aria-hidden=\"false\" class=\"shrink-0 opacity-75 group-focus:opacity-100 group-hover:opacity-100\" height=\"16\" viewBox=\"0 0 16 16\" width=\"16\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M9.41506 2.54535C10.5566 1.516 12.2884 1.57105 13.3399 2.64105C14.4597 3.72222 14.4959 5.5405 13.3467 6.68988L7.75588 12.2797C7.18583 12.8809 6.45561 13.2881 5.6924 13.4653H5.69045L2.59767 14.1762L2.5967 14.1743C2.4644 14.2058 2.19674 14.2328 1.98342 14.02C1.76937 13.8062 1.79632 13.5371 1.82814 13.4047L2.5381 10.3188C2.71723 9.5224 3.12639 8.8269 3.68752 8.26703L9.30568 2.64887L9.41506 2.54535ZM4.4297 9.01019C3.99658 9.44234 3.69398 9.96492 3.56252 10.5512L3.56154 10.5541L2.99513 13.0063L5.45607 12.4418C6.02206 12.3102 6.56957 12.0068 6.99513 11.558L7.00392 11.5483L11.5254 7.02582L8.96877 4.46918L4.4297 9.01019ZM12.5986 3.38422C11.9258 2.69135 10.7787 2.66298 10.0479 3.39203L10.0469 3.39105L9.71193 3.72602L12.2686 6.28266L12.6045 5.94769C13.3406 5.21146 13.3067 4.06829 12.6113 3.39691L12.5986 3.38422Z\" fill=\"currentColor\"></path></svg>",
11
+ "remove": "<svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.575\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <path d=\"M10 11v6\" />\n <path d=\"M14 11v6\" />\n <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\" />\n <path d=\"M3 6h18\" />\n <path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" />\n</svg>\n"
12
+ }
@@ -0,0 +1,72 @@
1
+ [data-mcx-sites=rows] {display:contents}
2
+ [data-mcx-sites] .mcx-site-preview,[data-mcx-sites] .mcx-site-candidate-art {position:relative}
3
+ [data-mcx-sites] .mcx-site-preview::after,[data-mcx-sites] .mcx-site-candidate-art::after {content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;box-shadow:inset 0 0 0 1px var(--mcx-color-border)}
4
+ [data-mcx-sites] .mcx-site-preview.mcx-site-placeholder,[data-mcx-sites] .mcx-site-candidate-art.mcx-site-placeholder {background:var(--color-background-primary-soft);color:var(--mcx-color-text-primary)}
5
+ .mcx-site-thumbnail {display:block;width:100%;height:100%;object-fit:cover;object-position:top;border-radius:inherit}
6
+ [data-mcx-sites] .mcx-site-skeleton {width:100%;height:100%;flex:none;border-radius:inherit;--loading-results-shimmer-base:color-mix(in srgb,var(--mcx-color-text-primary) 6%,var(--mcx-color-surface));--loading-results-shimmer-highlight:color-mix(in srgb,var(--mcx-color-text-primary) 16%,var(--mcx-color-surface));background-color:var(--loading-results-shimmer-base);background-image:linear-gradient(105deg,transparent 30%,var(--loading-results-shimmer-highlight) 48%,transparent 66%);background-repeat:no-repeat;background-size:240% 100%;animation:mcx-site-shimmer 1.6s ease-in-out infinite}
7
+ [data-mcx-sites] .mcx-site-preview>.mcx-site-skeleton,[data-mcx-sites] .mcx-site-candidate-art>.mcx-site-skeleton {position:absolute;inset:0}
8
+ @keyframes mcx-site-shimmer {from {background-position:150% 0} to {background-position:-150% 0}}
9
+ @media(prefers-reduced-motion:reduce) {[data-mcx-sites] .mcx-site-skeleton {animation:none}}
10
+ :root[data-reduced-motion=true] [data-mcx-sites] .mcx-site-skeleton {animation:none}
11
+ [data-mcx-sites=grid] {display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:20px}
12
+ [data-mcx-sites=table] {display:grid;grid-template-columns:minmax(0,1fr) minmax(120px,160px) auto}
13
+ [data-mcx-sites=entry] {color:var(--mcx-color-text-primary);font-family:var(--mcx-font-sans-default)}
14
+ [data-mcx-sites=entry] .mcx-site-preview {display:flex;align-items:center;justify-content:center;background:var(--mcx-color-surface-elevated-secondary);color:var(--mcx-color-text-tertiary)}
15
+ [data-mcx-sites=entry] .mcx-site-preview svg {width:24px;height:24px}
16
+ [data-mcx-sites=entry] .mcx-site-meta {font-size:var(--mcx-text-sm);color:var(--mcx-color-text-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
17
+ [data-mcx-sites=entry] .mcx-site-name {overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--mcx-text-base)}
18
+ [data-mcx-sites=entry] .mcx-site-menu {position:relative;z-index:2}
19
+ [data-mcx-sites=dialog] {margin:auto;width:min(560px,calc(100vw - 40px));max-height:calc(95vh - 40px);overflow:auto}
20
+ [data-mcx-sites=dialog][open] {display:flex;flex-direction:column}
21
+ [data-mcx-sites=dialog] .mcx-site-content {padding:0;min-height:0;overflow:auto}
22
+ [data-mcx-sites=dialog] [role=tabpanel],[data-mcx-sites=dialog] form {min-height:0;display:flex;flex-direction:column}
23
+ [data-mcx-sites=dialog] h3 {font-size:var(--mcx-text-sm);font-weight:var(--font-weight-medium);color:var(--mcx-color-text-secondary);margin:20px 0 6px}
24
+ [data-mcx-sites=dialog] p {color:var(--mcx-color-text-secondary);font-size:var(--mcx-text-sm);margin:6px 0 16px}
25
+ [data-mcx-sites=dialog] .mcx-site-candidate {padding:12px 0;border-bottom:1px solid var(--mcx-color-border)}
26
+ [data-mcx-sites=dialog] .mcx-site-candidate>div {min-width:0}
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)}
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
+ [data-mcx-sites=dialog] input:focus-visible {border-color:var(--mcx-color-ring);outline:none}
31
+ [data-mcx-sites=dialog] [role=alert] {color:var(--mcx-color-text-danger);font-size:var(--mcx-text-sm);margin-top:12px}
32
+ [data-mcx-sites=menu] {position:fixed;z-index:1000;max-width:calc(100vw - 16px);max-height:calc(100vh - 16px);outline:none}
33
+ [data-mcx-sites=menu] button {width:100%;border:0;text-align:start;font-family:var(--mcx-font-sans-default)}
34
+ [data-mcx-sites=menu] svg {width:16px;height:16px;opacity:.75}
35
+ [data-mcx-sites=menu] button:hover svg,[data-mcx-sites=menu] button:focus svg {opacity:1}
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}
38
+ [data-mcx-sites=entry] .mcx-site-sharing svg {width:16px;height:16px;flex:none}
39
+ @container (max-width:520px) {[data-mcx-sites=table] {grid-template-columns:minmax(0,1fr) auto}}
40
+
41
+ [data-mcx-sites=dialog] .mcx-site-icon {display:flex;align-items:center;justify-content:center;width:28px;height:28px;flex:none;border:0;border-radius:var(--mcx-radius-full);padding:0;background:transparent;color:var(--mcx-color-text-secondary)}
42
+ [data-mcx-sites=dialog] .mcx-site-icon svg {width:16px;height:16px}
43
+ [data-mcx-sites=dialog] .mcx-site-icon:hover {background:var(--mcx-color-background-primary-ghost-hover);color:var(--mcx-color-text-primary)}
44
+ [data-mcx-sites=dialog] .mcx-site-results {max-height:340px;min-height:100px;overflow:auto;margin:0 -8px}
45
+ [data-mcx-sites=dialog] .mcx-site-results h3 {margin:12px 8px 4px;color:var(--mcx-color-text-tertiary);font-weight:500}
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
+ [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
+ [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}
50
+ [data-mcx-sites=dialog] .mcx-site-candidate:has(input:disabled) {cursor:default}
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
+ [data-mcx-sites=dialog] .mcx-site-candidate input.mcx-site-checkbox:focus-visible {outline:2px solid var(--mcx-color-ring);outline-offset:2px}
53
+ [data-mcx-sites=dialog] .mcx-site-candidate .mcx-site-candidate-info>div {white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
54
+ [data-mcx-sites=dialog] .mcx-site-candidate>svg {width:16px;height:16px;flex:none;color:var(--mcx-color-text-secondary)}
55
+ [data-mcx-sites=dialog] .mcx-site-candidate-art {width:96px;height:60px;overflow:hidden;display:flex;align-items:center;justify-content:center;flex:none;background:var(--color-background-primary-soft);border-radius:var(--mcx-radius-md);color:var(--mcx-color-text-secondary)}
56
+ [data-mcx-sites=dialog] .mcx-site-candidate-art svg {width:20px;height:20px}
57
+ [data-mcx-sites=dialog] .mcx-site-modes {display:flex;align-items:center;gap:var(--segmented-control-gap);padding:var(--segmented-control-gutter);flex-shrink:0;margin:0 0 16px;border-radius:var(--control-radius-md);background:var(--color-background-secondary-solid)}
58
+ [data-mcx-sites=dialog] .mcx-site-mode {flex:1;min-width:0;height:var(--control-size-md);padding:0 var(--mcx-control-gutter-md);border:0;border-radius:calc(var(--control-radius-md) - var(--segmented-control-gutter));background:transparent;color:var(--mcx-color-text-secondary);font-family:inherit;font-size:var(--mcx-control-font-size-md);font-weight:var(--segmented-control-font-weight);line-height:1.5;white-space:nowrap}
59
+ [data-mcx-sites=dialog] .mcx-site-mode[aria-selected=false] {color:var(--color-text-secondary-solid)}
60
+ [data-mcx-sites=dialog] .mcx-site-mode[aria-selected=false]:hover {background:var(--color-background-secondary-solid-hover)}
61
+ [data-mcx-sites=dialog] .mcx-site-mode[aria-selected=false]:active {background:var(--color-background-secondary-solid-active)}
62
+ [data-mcx-sites=dialog] .mcx-site-mode[aria-selected=true] {background:var(--segmented-control-thumb-background);box-shadow:var(--segmented-control-thumb-shadow);color:var(--mcx-color-text-primary)}
63
+ [data-mcx-sites=dialog] .mcx-site-mode:focus-visible {outline:2px solid var(--mcx-color-ring);outline-offset:-2px}
64
+ [data-mcx-sites=dialog] [role=tabpanel][hidden] {display:none}
65
+ [data-mcx-sites=dialog] form .mcx-site-content>label:first-child {margin-top:0}
66
+ [data-mcx-sites=dialog] .mcx-site-scan-row {display:flex;align-items:center;gap:12px;padding:10px 8px}
67
+ [data-mcx-sites=dialog] .mcx-site-scan-copy {flex:1;display:flex;flex-direction:column;gap:10px}
68
+ [data-mcx-sites=dialog] .mcx-site-scan-copy .mcx-site-skeleton {height:12px;border-radius:var(--mcx-radius-sm)}
69
+ [data-mcx-sites=dialog] .mcx-site-scan-copy .mcx-site-skeleton:last-child {width:32%;height:10px}
70
+ [data-mcx-sites=dialog] .mcx-site-scan-row:nth-child(2) .mcx-site-skeleton {animation-delay:-.3s}
71
+ [data-mcx-sites=dialog] .mcx-site-scan-row:nth-child(3) .mcx-site-skeleton {animation-delay:-.6s}
72
+ [data-mcx-sites=dialog] .mcx-site-empty {padding:28px 16px;text-align:center;color:var(--mcx-color-text-secondary);font-size:var(--mcx-text-sm)}
@@ -0,0 +1,7 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createSitesAdapter } from "./adapters/codex/renderer.mjs";
3
+
4
+ export async function createPageScriptSource() {
5
+ const source=await readFile(new URL("./frontend/entry.mjs",import.meta.url),"utf8");
6
+ return `const createSitesAdapter = ${createSitesAdapter.toString()};\n${source}`;
7
+ }
@@ -0,0 +1,35 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Lucide Icons and Contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
+
16
+ ---
17
+ The following Lucide icons are derived from the Feather project:
18
+ airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
19
+ The MIT License (MIT) (for the icons listed above)
20
+
21
+ Copyright (c) 2013-present Cole Bemis
22
+ Permission is hereby granted, free of charge, to any person obtaining a copy
23
+ of this software and associated documentation files (the "Software"), to deal
24
+ in the Software without restriction, including without limitation the rights
25
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
26
+ copies of the Software, and to permit persons to whom the Software is
27
+ furnished to do so, subject to the following conditions:
28
+ The above copyright notice and this permission notice shall be included in all
29
+ copies or substantial portions of the Software.
30
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
33
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
34
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
35
+ OUT OF OR IN CONNECTION WITH THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,14 +1,16 @@
1
1
  # 任务切换
2
2
 
3
+ 通用视觉、材质、按钮、Tooltip 与原生 sidebar 层级遵循根 [DESIGN.md](../../DESIGN.md)。本文件只记录功能特有组件、布局、数据语义与通用组件选型。
4
+
3
5
  仅在线程侧栏收起时显示入口;展开时移除入口并关闭浮层。入口随 Codex 标题栏布局切换:
4
6
  - 普通/分栏:在主 Chat 标题前显示原生 list-bullet 图标;整段标题可点击。
5
7
  - 全宽标签页:在 Chat 标签页前注入独立的任务图标按钮。
6
8
  - 移除扩展铃铛,注入入口存在时,用原生完整 SVG 显示无蓝点的 Sidebar 图标;保留 React 原节点,卸载或展开侧栏时移除替代图标并恢复原节点。
7
9
  - 悬停入口150ms展开;鼠标离开入口与浮层250ms后关闭,跨越8px间隙进入浮层会取消关闭。点击入口确保打开;点击外部、Escape 关闭。Enter、空格、ArrowDown 可打开。
8
10
 
9
- 样式使用原生字体、text-secondary、radius-lg、primary-ghost-hover 与 ring token。入口仅 hover 显示背景,不保持选中底色。标题区域 no-drag,其他标题栏区域保留窗口拖拽。
11
+ 入口克隆原生 Chat 标题控件,通用反馈遵循根规范。入口仅 hover 显示背景,不保持选中底色。标题区域 no-drag,其他标题栏区域保留窗口拖拽。
10
12
 
11
- 浮层宽320px(窄窗口缩小),最大高440px(约5条,受视口高度限制);沿用 dropdown 背景与 border,圆角16px、shadow-md,外层上下 padding 6px、左右0;滚动容器左右 padding 6px,使滚动条靠近浮层边缘。位置对齐标题触发区域左侧、底缘下方8px,并受窗口边界约束。
13
+ 浮层宽320px(窄窗口缩小),最大高440px(约5条,受视口高度限制);选择通用 Popover,功能圆角16px,外层上下 padding 6px、左右0;滚动容器左右 padding 6px,使滚动条靠近浮层边缘。位置对齐标题触发区域左侧、底缘下方8px,并受窗口边界约束。
12
14
 
13
15
  列表集合为等待回复/批准、工作中、最近1小时打开过、未读的会话,按 Thread ID 去重。按需要回复、等待批准、未读、执行中、空闲排序,同级按消息时间倒序;排除当前会话。原生流式摘要变化时更新观测消息时间,不因菜单重绘更新时间。最近访问记录限定当前窗口运行期间。每条显示:
14
16
  1. 单行标题(13px/18px),远程标识紧随标题且不显示主机名;右侧状态优先级为待确认标签、原生 loading、相对时间。背景仅 hover 时显示。
@@ -34,3 +36,5 @@
34
36
  入口提醒点:排除当前会话后,有需要回复或批准时为红色 `#FF3B30`,否则存在未读为原生蓝色 `--blue-400`;只有执行中或空闲则无点。点直径6px。无其他命中会话时不打开浮层,包括仅当前会话命中的情况。原生侧栏图标与 MineCodex 提醒随注入/卸载同步恢复。
35
37
 
36
38
  浮层标题颜色与原生 Sidebar 类别标题一致,使用 `--color-text-tertiary`。
39
+
40
+ 当前通过 UI 组件库的 `config.ui.surface(panel, "popover")` 选择原生 Popover;功能继续拥有定位、尺寸与内容。
@@ -12,7 +12,8 @@
12
12
  },
13
13
  "entry": {
14
14
  "kind": "page-script",
15
- "script": "src/unread-inbox.mjs"
15
+ "script": "src/unread-inbox.mjs",
16
+ "sourceFactory": "src/injection-source.mjs"
16
17
  },
17
18
  "placement": {
18
19
  "order": 70