minecodex 2.0.35 → 2.0.36

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.
@@ -1,28 +1,35 @@
1
- export function createImageSourcesAdapter(codex) {
2
- const isImageNavigation = props => props?.variant === "browse"
3
- && props.tabs?.some(item => item.key === "generated")
4
- && props.tabs?.some(item => item.key === "uploaded");
1
+ export function createImageSourcesAdapter(codex, signal, invalidate) {
2
+ const account = codex.account(signal, invalidate);
3
+ const visible = node => node?.isConnected && node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0;
4
+ const isImageNavigation = (props, plus) => props?.variant === "browse"
5
+ && (plus ? ["images", "all", "folders"] : ["generated", "uploaded"]).every(key => props.tabs?.some(item => item.key === key));
5
6
 
6
7
  function applicability() {
7
8
  const props=codex.props(codex.rail(),value=>Boolean((value.router ?? value.value?.router)?.state?.location));
8
9
  const location=(props?.router ?? props?.value?.router)?.state?.location;
9
- if (!location) return "unknown";
10
- return location.pathname === "/space/files" && new URLSearchParams(location.search).get("tab") === "images"
10
+ const current = account.read();
11
+ if (!location || current.status !== "available") return "unknown";
12
+ return (current.planType === "plus" ? location.pathname === "/library"
13
+ : location.pathname === "/space/files" && new URLSearchParams(location.search).get("tab") === "images")
11
14
  ? "applicable" : "not-applicable";
12
15
  }
13
16
  return {applicability,read() {
17
+ if (applicability() !== "applicable") return null;
18
+ const current = account.read(), plus = current.planType === "plus";
14
19
  const tabs = [...document.querySelectorAll('[role="tablist"]')].find(node =>
15
- !node.closest("[data-minecodex-image-navigation]")
16
- && codex.props(node, isImageNavigation));
20
+ (visible(node) || node.hidden && visible(node.parentElement?.querySelector('[data-minecodex-image-navigation]')))
21
+ && !node.closest("[data-minecodex-image-navigation]")
22
+ && codex.props(node, props => isImageNavigation(props, plus)));
17
23
  if (!tabs) return null;
18
24
  // 26.1007 把来源工具栏包进独立 inset;图库仍是同级的 flex 内容区。
19
- let navigation = tabs.parentElement?.parentElement;
25
+ let navigation = plus ? tabs.parentElement : tabs.parentElement?.parentElement;
20
26
  while (navigation && !navigation.nextElementSibling) navigation = navigation.parentElement;
21
27
  const content = navigation?.nextElementSibling;
22
28
  const shell = navigation?.parentElement;
23
- if (!content || !shell) return null;
24
- const props = codex.props(tabs, isImageNavigation);
25
- const search = shell.querySelector('input[type="search"],input[placeholder="Search"]');
26
- return {tabs, navigation, content, shell, search, selectedKey:props.selectedKey};
29
+ if (!content || !shell || !(visible(content) || content.hidden
30
+ && content.nextElementSibling?.id === "minecodex-image-gen-content" && visible(content.nextElementSibling))) return null;
31
+ const props = codex.props(tabs, props => isImageNavigation(props, plus));
32
+ const search = shell.querySelector('input[type="search"],input[placeholder="Search"],input[placeholder="Search library"],input[placeholder="搜索资料库"],input[placeholder="搜索库"],input[placeholder="搜尋資料庫"]');
33
+ return {tabs, navigation, content, shell, search, selectedKey:props.selectedKey, accountKey:current.key, mode:plus ? "library" : "space"};
27
34
  }};
28
35
  }
@@ -4,6 +4,7 @@ import {createNativeImageModules} from "./native-image-modules.mjs";
4
4
  export function createNativeImageSource(codex, { requestHostAction, libraryApi, createCloudImageActions, promptGlyphSource, signal, modules = createNativeImageModules() } = {}) {
5
5
  const connect = modules.gallery;
6
6
  const opened = new Map();
7
+ const account = codex.account(signal);
7
8
 
8
9
  function uploadFile({base64,name}) {
9
10
  const decoded=atob(base64),bytes=new Uint8Array(decoded.length);
@@ -42,11 +43,11 @@ export function createNativeImageSource(codex, { requestHostAction, libraryApi,
42
43
  }
43
44
 
44
45
  function navigationContract(anchor) {
46
+ const plus = account.read().planType === "plus";
45
47
  for (let fiber = codex.currentFiber(anchor); fiber; fiber = fiber.return) {
46
48
  const props = fiber.memoizedProps;
47
49
  if (typeof fiber.type === "function" && props?.variant === "browse"
48
- && props.tabs?.some(item => item.key === "generated")
49
- && props.tabs?.some(item => item.key === "uploaded")) return {type:fiber.type, props};
50
+ && (plus ? ["images", "all", "folders"] : ["generated", "uploaded"]).every(key => props.tabs?.some(item => item.key === key))) return {type:fiber.type, props};
50
51
  }
51
52
  throw new Error("Native image navigation contract changed");
52
53
  }
@@ -61,7 +62,9 @@ export function createNativeImageSource(codex, { requestHostAction, libraryApi,
61
62
  const props = {
62
63
  ...contract.props,
63
64
  selectedKey,
64
- tabs:[...contract.props.tabs, {key:"imagegen", name:"Image Gen"}],
65
+ tabs:account.read().planType === "plus"
66
+ ? contract.props.tabs.flatMap(tab => tab.key === "images" ? [tab, {key:"imagegen", name:"Image Gen"}] : [tab])
67
+ : [...contract.props.tabs, {key:"imagegen", name:"Image Gen"}],
65
68
  onSelect:key => {
66
69
  onSelect(key);
67
70
  if (key !== "imagegen") navigationContract(anchor).props.onSelect(key);
@@ -78,7 +78,7 @@ export function installLocalImageSource(config, signal, sources, native) {
78
78
  async function scan() {
79
79
  if (signal.aborted) return;
80
80
  const entry = sources.read();
81
- if (mounted && mounted.entry.tabs === entry?.tabs) {
81
+ if (mounted && mounted.entry.tabs === entry?.tabs && mounted.entry.accountKey === entry?.accountKey && mounted.entry.mode === entry?.mode) {
82
82
  if (!mounted.active) mounted.nav?.render(entry.selectedKey);
83
83
  return;
84
84
  }
@@ -176,11 +176,12 @@ export function installLocalImageSource(config, signal, sources, native) {
176
176
  }
177
177
 
178
178
  let scheduled = false;
179
- const observer = new MutationObserver(() => {
179
+ function schedule() {
180
180
  if (scheduled) return;
181
181
  scheduled = true;
182
182
  queueMicrotask(() => {scheduled = false; void scan();});
183
- });
183
+ }
184
+ const observer = new MutationObserver(schedule);
184
185
  observer.observe(document.body, {childList:true, subtree:true, attributes:true, attributeFilter:["aria-selected"]});
185
186
  void scan();
186
187
  let cleaned = false;
@@ -188,6 +189,7 @@ export function installLocalImageSource(config, signal, sources, native) {
188
189
  signal.addEventListener("abort", cleanup, {once:true});
189
190
  return {
190
191
  cleanup,
192
+ refresh:schedule,
191
193
  selfCheck() {
192
194
  const entry=sources.read(),scope=sources.applicability();
193
195
  if (!entry) return {state:scope === "applicable" ? "unavailable" : scope,reason:scope === "not-applicable" ? "outside-image-sources" : "image-sources-anchor-unavailable"};
@@ -259,7 +259,7 @@ function localPromptRequested(){openPendingLocalPrompt();void scan();}
259
259
  document.addEventListener("minecodex:image-prompt",localPromptRequested);
260
260
  const localeObserver = new MutationObserver(() => mounted?.refreshLocale());
261
261
  localeObserver.observe(document.documentElement, { attributes:true, attributeFilter:["lang"] });
262
- localImageSource = installLocalImageSource(config, signal, createImageSourcesAdapter(config.codex), createNativeImageSource(config.codex, {
262
+ localImageSource = installLocalImageSource(config, signal, createImageSourcesAdapter(config.codex, signal, () => localImageSource?.refresh()), createNativeImageSource(config.codex, {
263
263
  requestHostAction:config.requestHostAction, libraryApi, createCloudImageActions, promptGlyphSource, signal,
264
264
  }));
265
265
  config.registerSelfCheck?.(() => {
@@ -1,7 +1,8 @@
1
1
  // 专有路由、页面锚点和原生网页打开协议不跨出此适配器。
2
- export function createSitesAdapter(signal,codex) {
2
+ export function createSitesAdapter(signal,codex,invalidate) {
3
3
  let nativeButton,nativeInner,nativePlus,nativeSiteIcon,nativeMore,nativeExternal,nativeGlobe,nativeActionsClass;
4
4
  const owned="data-mcx-sites";
5
+ const account=codex.account(signal,invalidate);
5
6
  const text=(en,zh)=>(document.documentElement.lang || navigator.language).startsWith("zh") ? zh : en;
6
7
  const visible=e=>!!e?.isConnected && e.getBoundingClientRect().width>0 && e.getBoundingClientRect().height>0;
7
8
  async function until(check) {
@@ -19,20 +20,73 @@ function router() {
19
20
  return p?.router || p?.value?.router;
20
21
  }
21
22
  function pageInfo() {
22
- if (router()?.state?.location?.pathname !== "/space/sites") return null;
23
- const input = [...document.querySelectorAll("input")].find(e => /Search sites|搜索站点|搜尋網站/.test(e.placeholder));
23
+ if (applicability() !== "applicable") return null;
24
+ const plus=account.read().planType === "plus";
25
+ const input = [...document.querySelectorAll("input")].find(e => visible(e) && /Search sites|搜索站点|搜尋網站/.test(e.placeholder));
24
26
  if (!input) return null;
25
27
  let page = input.parentElement;
26
28
  while (page && !page.classList.contains("min-h-full")) page = page.parentElement;
27
29
  if (!page || page.children.length < 2) return null;
28
- const content = page.children[1], list = page.querySelector('[data-testid="sites-table"], [data-testid="sites-grid"]');
30
+ const content = plus ? page.lastElementChild : page.children[1], list = page.querySelector('[data-testid="sites-table"], [data-testid="sites-grid"]');
31
+ if (!visible(content)) return null;
29
32
  const native = nativeProps(list || content.querySelector("button") || content.querySelector("p") || content.lastElementChild?.lastElementChild, p => "viewMode" in p && "shared" in p);
30
33
  const grid = native ? native.viewMode === "grid" : !!page.querySelector('button[aria-label="Grid view"][aria-pressed="true"],button[aria-label="网格视图"][aria-pressed="true"]');
31
- const shared = native ? !!native.shared : [...page.querySelectorAll("button[aria-pressed=true]")].some(b => /Shared with you|与你共享|與你共用/.test(b.textContent));
32
- const create = [...page.firstElementChild.querySelectorAll("button")].find(b => /^(New|新建|新增)$/.test(b.textContent.trim()) && !b.hasAttribute(owned));
34
+ const ownershipTabs=[...page.querySelectorAll('[role="tablist"]')].find(node=>!node.closest(`[${owned}]`)&&nativeProps(node,p=>p.tabs?.some(tab=>tab.key==="owned")&&p.tabs?.some(tab=>tab.key==="shared")));
35
+ const ownership=ownershipTabs&&nativeProps(ownershipTabs,p=>p.tabs?.some(tab=>tab.key==="owned")&&p.tabs?.some(tab=>tab.key==="shared"));
36
+ const shared = ownership ? ownership.selectedKey === "shared" : native ? !!native.shared : [...page.querySelectorAll("button[aria-pressed=true]")].some(b => /Shared with you|与你共享|與你共用/.test(b.textContent));
37
+ const create = [...(plus ? page : page.firstElementChild).querySelectorAll("button")].find(b => visible(b) && (plus ? /^(Create Site|创建站点|建立網站)$/ : /^(New|新建|新增)$/).test(b.textContent.trim()) && !b.hasAttribute(owned));
33
38
  codex.report("sites-page",true);
34
- return { page, input, content, list, grid, shared, create, empty: list ? null : [...content.children].findLast(e => !e.hasAttribute(owned)) };
39
+ return { page, input, content, list, grid, shared, create, plus, ownershipTabs:plus?(ownershipTabs??null):null, accountKey:account.read().key, empty: list ? null : [...content.children].findLast(e => !e.hasAttribute(owned)) };
35
40
  }
41
+ function mountOwnership(page,onSelect) {
42
+ const tabs=page.ownershipTabs,original=tabs?[...tabs.querySelectorAll('[role="tab"]')]:[];
43
+ let wrapper;
44
+ const hidden=tabs?.hidden,node=tabs?tabs.cloneNode(false):document.createElement("div");
45
+ if (!tabs) {
46
+ // 26.1007:共享能力确认后可能移除原标签组;保留已测 browse 分支。
47
+ node.className="flex min-w-0 items-center gap-1 hide-scrollbar overflow-x-auto overflow-y-hidden";
48
+ node.setAttribute("role","tablist");node.setAttribute("aria-label",text("Site views","站点视图"));
49
+ const native=document.createElement("button");native.type="button";native.setAttribute("role","tab");native.setAttribute("aria-selected","true");
50
+ native.textContent=text("Owned by you","你拥有的");
51
+ native.className="cursor-interaction items-center text-sm select-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 flex h-9 min-w-0 rounded-full border border-transparent px-4 py-0 font-medium ring-inset bg-primary-ghost-hover text-default";
52
+ native.onclick=()=>{const r=router(),location=r.state.location,search=new URLSearchParams(location.search);search.delete("view");search.delete("search");r.navigate({pathname:location.pathname,search:search.size?`?${search}`:""},{state:location.state});};
53
+ original.push(native);
54
+ wrapper=page.content.cloneNode(false);
55
+ wrapper.classList.remove("min-h-0","flex-1","flex-col","pb-4","bg-surface");wrapper.classList.add("min-h-15","items-center");
56
+ wrapper.setAttribute(owned,"ownership-wrapper");wrapper.append(node);page.content.before(wrapper);
57
+ }
58
+ const buttons=original.map(button=>button.cloneNode(true));
59
+ let selected="native";
60
+ node.removeAttribute("id");node.setAttribute(owned,"ownership");
61
+ const added=original[0].cloneNode(false);added.textContent=text("Added by you","由你添加");buttons.push(added);
62
+ buttons.forEach((button,index)=>{
63
+ button.removeAttribute("id");button.removeAttribute("aria-controls");
64
+ button.onclick=()=>{selected=index===original.length?"added":"native";if(selected==="native")original[index].click();sync();onSelect(selected);};
65
+ node.append(button);
66
+ });
67
+ function sync() {
68
+ const active=original.find(button=>button.getAttribute("aria-selected")==="true"),inactive=original.find(button=>button.getAttribute("aria-selected")!=="true");
69
+ buttons.forEach((button,index)=>{
70
+ const chosen=selected==="added"?index===original.length:original[index]?.getAttribute("aria-selected")==="true";
71
+ button.className=(chosen?active:inactive)?.className || original[0].className.replace("bg-primary-ghost-hover text-default","text-secondary hover:bg-background-primary-ghost-hover/50 hover:text-default");
72
+ button.setAttribute("aria-selected",String(chosen));button.tabIndex=chosen?0:-1;
73
+ });
74
+ }
75
+ node.onkeydown=event=>{
76
+ if (!["ArrowLeft","ArrowRight","Home","End"].includes(event.key))return;
77
+ event.preventDefault();const index=buttons.indexOf(document.activeElement),last=buttons.length-1;
78
+ const next=event.key==="Home"?0:event.key==="End"?last:(index+(event.key==="ArrowRight"?1:last))%buttons.length;
79
+ buttons[next].click();buttons[next].focus();
80
+ };
81
+ if (tabs) {tabs.after(node);tabs.hidden=true;}sync();
82
+ return {node,tabs,sync,selection:()=>selected,selectAdded:()=>added.click(),dispose(){wrapper?.remove();node.remove();if(tabs)tabs.hidden=hidden;}};
83
+ }
84
+ function applicability() {
85
+ const current=account.read(),path=router()?.state?.location?.pathname;
86
+ if (!path || current.status !== "available") return "unknown";
87
+ return path === (current.planType === "plus" ? "/sites" : "/space/sites") ? "applicable" : "not-applicable";
88
+ }
89
+ function isSitesPath() { return ["/sites","/space/sites"].includes(router()?.state?.location?.pathname); }
36
90
  function browserMenu() {
37
91
  for (const menu of document.querySelectorAll('[role="menu"]')) {
38
92
  if (!visible(menu)) continue;
@@ -85,7 +139,7 @@ async function open(origin,url,cwd) {
85
139
  function templates(page) {
86
140
  const create = page.create;
87
141
  if (create) {
88
- const template = page.page.querySelector('button[data-size="lg"][data-color="secondary"]');
142
+ const template = page.page.querySelector('button[data-size="lg"][data-color="secondary"]') || create;
89
143
  if (template) { nativeButton = template.className; nativeInner = template.firstElementChild?.className || ""; }
90
144
  nativePlus = create.querySelector("svg")?.cloneNode(true);
91
145
  // New 的图标在桌面端隐藏,克隆后只保留图标自身样式。
@@ -100,5 +154,5 @@ function templates(page) {
100
154
  return {nativeButton,nativeInner,nativePlus,nativeSiteIcon,nativeMore,nativeExternal,nativeGlobe,nativeActionsClass};
101
155
  }
102
156
 
103
- return {templates,pageInfo,browserMenu,fullViewControl,open,path:()=>router()?.state?.location?.pathname};
157
+ return {templates,pageInfo,mountOwnership,browserMenu,fullViewControl,open,applicability,isSitesPath,accountKey:()=>account.read().key,path:()=>router()?.state?.location?.pathname};
104
158
  }
@@ -1,4 +1,4 @@
1
- const nativeSites=createSitesAdapter(signal,config.codex);
1
+ const nativeSites=createSitesAdapter(signal,config.codex,schedule);
2
2
  // 复用 Prompts 已验证的原生图标与 Lucide 图标,保持交互一致。
3
3
  const ICONS = config.resources.icons;
4
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"};
@@ -8,7 +8,11 @@ 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
10
  let nativeExternal, nativeGlobe, nativeActionsClass;
11
- let currentPage, refreshAt = 0, pendingLayout = null;
11
+ let currentPage, currentAccount, refreshAt = 0, pendingLayout = null;
12
+ let ownership;
13
+ const nativeHidden=new Map();
14
+ function restoreNativeContent() { for (const [node,hidden] of nativeHidden) node.hidden=hidden; nativeHidden.clear(); }
15
+ function restoreOwnership() { restoreNativeContent();ownership?.dispose();ownership=null; }
12
16
  const style = document.createElement("style");
13
17
  style.setAttribute(owned, "style");
14
18
  style.textContent = config.resources.styles;
@@ -106,7 +110,7 @@ function loadPreview(container, url, cachedOnly = false) {
106
110
  });
107
111
  }
108
112
  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");
113
+ 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" : "group/site-row 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
114
  r.setAttribute(owned, "entry"); r.dataset.siteId = site.id;
111
115
  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
116
  open.type = "button"; open.setAttribute("aria-label", text("Open ", "打开 ") + site.title); open.onclick = () => openSite(site); r.append(open);
@@ -210,32 +214,54 @@ function showBrowserAdd({ site, project, preferProject }) {
210
214
  }
211
215
  function reconcile() {
212
216
  if (disposed) return;
217
+ if (currentAccount && nativeSites.accountKey() !== currentAccount) { closeMenu();dialog?.close(); }
213
218
  updateBrowserMenu();
214
219
  const page = nativeSites.pageInfo();
215
- if (!page) { addButton?.remove(); addButton = null; rowsRoot?.remove(); rowsRoot = null; fallback?.remove(); fallback = null; rowSignature = ""; currentPage = null; closeMenu(); return; }
220
+ if (!page) { restoreOwnership();addButton?.remove(); addButton = null; rowsRoot?.remove(); rowsRoot = null; fallback?.remove(); fallback = null; rowSignature = ""; currentPage = null; closeMenu(); document.querySelectorAll("[data-mcx-sites-empty-suppressed]").forEach(e => e.removeAttribute("data-mcx-sites-empty-suppressed")); return; }
216
221
  cacheNative(page);
217
- if (currentPage !== page.page) { currentPage = page.page; rowSignature = ""; if (Date.now() - refreshAt > 1500) refresh().catch(e => notice(e.message)); }
222
+ if (currentPage !== page.page || currentAccount !== page.accountKey) {
223
+ restoreOwnership();
224
+ if (currentAccount && currentAccount !== page.accountKey) { closeMenu(); dialog?.close(); }
225
+ addButton?.remove(); addButton=null; rowsRoot?.remove(); rowsRoot=null; fallback?.remove(); fallback=null;
226
+ currentPage = page.page; currentAccount=page.accountKey; rowSignature = "";
227
+ if (Date.now() - refreshAt > 1500) refresh().catch(e => notice(e.message));
228
+ }
229
+ if (page.plus && (!ownership || ownership.tabs !== page.ownershipTabs || !ownership.node.isConnected)) {
230
+ const added=ownership?.selection()==="added";
231
+ restoreOwnership();ownership=nativeSites.mountOwnership(page,()=>{restoreNativeContent();rowSignature="";schedule();});
232
+ if (added) ownership.selectAdded();
233
+ }
234
+ ownership?.sync();
235
+ const added=ownership?.selection()==="added";
236
+ if (added) {
237
+ for (const child of page.content.children) if (!child.hasAttribute(owned)) {
238
+ if (!nativeHidden.has(child)) nativeHidden.set(child,child.hidden);
239
+ child.hidden=true;
240
+ }
241
+ } else restoreNativeContent();
218
242
  if (!addButton?.isConnected && page.create) {
219
243
  addButton = button(text("Add site", "添加站点"), () => showDiscovery()); addButton.setAttribute(owned, "add");
220
244
  if (nativePlus) addButton.firstElementChild.prepend(nativePlus.cloneNode(true));
221
245
  page.create.before(addButton);
222
246
  }
223
247
  const query = page.input.value.trim().toLowerCase();
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]);
226
- if (signature === rowSignature && rowsRoot?.isConnected && (page.list ? rowsRoot.parentElement === page.list : fallback?.isConnected)) return;
248
+ const sites = ownership && !added || !ownership && page.shared ? [] : state.sites.filter(s => [s.title, s.url || "", state.projects.find(p => p.id === s.origin.projectId)?.name || ""].some(v => v.toLowerCase().includes(query)));
249
+ const list=added?null:page.list;
250
+ const signature = JSON.stringify([revision, query, page.shared, page.grid,added]);
251
+ if (signature === rowSignature && rowsRoot?.isConnected && (list ? rowsRoot.parentElement === list : fallback?.isConnected)) return;
227
252
  rowsRoot?.remove(); fallback?.remove(); fallback = null;
228
253
  document.querySelectorAll("[data-mcx-sites-empty-suppressed]").forEach(e => e.removeAttribute("data-mcx-sites-empty-suppressed"));
229
254
  rowsRoot = element("div"); rowsRoot.setAttribute(owned, "rows"); rowSignature = signature;
230
- if (!sites.length) { rowsRoot = null; return; }
231
- if (page.empty) page.empty.setAttribute("data-mcx-sites-empty-suppressed", "");
232
- if (page.list) page.list.append(rowsRoot);
255
+ if (!sites.length && !added) { rowsRoot = null; return; }
256
+ if (page.empty && !added) page.empty.setAttribute("data-mcx-sites-empty-suppressed", "");
257
+ if (list) list.append(rowsRoot);
233
258
  else {
234
259
  fallback = element("div", "@container pb-3"); fallback.setAttribute(owned, "fallback");
235
260
  const layout = element("div"); layout.setAttribute(owned, page.grid ? "grid" : "table"); fallback.append(layout); layout.append(rowsRoot);
236
261
  page.content.append(fallback);
237
262
  }
238
263
  rowsRoot.append(...sites.map(s => row(s, page.grid)));
264
+ if (!sites.length) rowsRoot.append(element("p","text-sm text-secondary",text("No added sites found.","没有找到你添加的站点。")));
239
265
  }
240
266
  function schedule() { clearTimeout(timer); timer = setTimeout(reconcile, 60); }
241
267
  function newDialog(title, back) {
@@ -375,7 +401,7 @@ async function until(check, milliseconds = 7000) {
375
401
 
376
402
  async function applyWebLayout(token) {
377
403
  try {
378
- await until(() => pendingLayout !== token || (nativeSites.path() !== "/space/sites" && (nativeSites.fullViewControl(true) || nativeSites.fullViewControl(false))));
404
+ await until(() => pendingLayout !== token || (!nativeSites.isSitesPath() && (nativeSites.fullViewControl(true) || nativeSites.fullViewControl(false))));
379
405
  if (pendingLayout !== token) return;
380
406
  const hide = [...document.querySelectorAll("button")].find(e => visible(e) && /^(Hide sidebar|隐藏侧栏|隐藏侧边栏|隱藏側邊欄|收起侧栏)$/.test(e.getAttribute("aria-label") || "")); hide?.click();
381
407
  // 使用原生完整视图入口,保留原生网页标签和底部聊天输入框。
@@ -414,9 +440,8 @@ observer.observe(document.body, { childList: true, subtree: true, attributes: tr
414
440
  document.addEventListener("input", schedule); document.addEventListener("click", onClick, true); document.addEventListener("keydown", onKey, true); document.addEventListener("pointerdown", onPointer, true);
415
441
  refresh().catch(e => notice(e.message)); schedule();
416
442
  config.registerSelfCheck?.(() => {
417
- const path=nativeSites.path();
418
- if (!path) return {state:"unknown",reason:"navigation-context-unavailable"};
419
- if (path !== "/space/sites") return {state:"not-applicable",reason:"outside-sites-page"};
443
+ const scope=nativeSites.applicability();
444
+ if (scope !== "applicable") return {state:scope,reason:scope === "unknown" ? "account-or-navigation-unavailable" : "outside-sites-page"};
420
445
  const page=nativeSites.pageInfo(),available=Boolean(page?.create && addButton?.isConnected);
421
446
  return {state:available ? "available" : "unavailable",reason:available ? null : "sites-anchor-unavailable",
422
447
  checks:[{name:"sites-page",available:Boolean(page)},{name:"sites-create-anchor",available}],scopes:[{name:"sites-page",element:page?.page},{name:"sites-dialog",element:dialog},{name:"sites-menu",element:menu}]};
@@ -425,5 +450,6 @@ return () => {
425
450
  disposed = true; pendingLayout = null; clearTimeout(timer); observer.disconnect();
426
451
  document.removeEventListener("input", schedule); document.removeEventListener("click", onClick, true); document.removeEventListener("keydown", onKey, true); document.removeEventListener("pointerdown", onPointer, true);
427
452
  closeMenu(); dialog?.close(); document.querySelectorAll(`[${owned}]`).forEach(e => e.remove());
453
+ restoreOwnership();
428
454
  document.querySelectorAll("[data-mcx-sites-empty-suppressed]").forEach(e => e.removeAttribute("data-mcx-sites-empty-suppressed"));
429
455
  };
@@ -36,10 +36,31 @@ function svgNode(name, attributes) {
36
36
  return node;
37
37
  }
38
38
 
39
+ function quotaArc(start, end, clockwise) {
40
+ const radius = 43 / 3;
41
+ const point = angle => {
42
+ const radians = (angle - 90) * Math.PI / 180;
43
+ return [16 + radius * Math.cos(radians), 16 + radius * Math.sin(radians)];
44
+ };
45
+ const from = point(start), to = point(end);
46
+ return `M ${from[0]} ${from[1]} A ${radius} ${radius} 0 ${Math.abs(end - start) > 180 ? 1 : 0} ${clockwise ? 1 : 0} ${to[0]} ${to[1]}`;
47
+ }
48
+
39
49
  function draw(windows) {
40
50
  const svg = svgNode("svg", { viewBox: "0 0 32 32", class: "mcx-usage-ring", "aria-hidden": "true" });
41
- for (const [key, radius] of [["weekly", 43 / 3], ["fiveHour", 10.5]]) {
42
- if (windows[key] == null) continue;
51
+ if (windows.fiveHour != null) {
52
+ // 左下、右下各留 24° 圆头中心间距;两段都从左下切口扣减。
53
+ for (const [key, start, end, clockwise] of [["fiveHour", 237, 483, true], ["weekly", 213, 147, false]]) {
54
+ const group = svgNode("g", { "data-window": key, fill: "none", stroke: "currentColor", "stroke-width": 10 / 3, "stroke-linecap": "round" });
55
+ group.append(svgNode("path", { d: quotaArc(start, end, clockwise), opacity: 0.18 }));
56
+ if (windows[key] > 0) {
57
+ const remainingStart = start + (end - start) * (1 - windows[key] / 100);
58
+ group.append(svgNode("path", { d: quotaArc(remainingStart, end, clockwise) }));
59
+ }
60
+ svg.append(group);
61
+ }
62
+ } else {
63
+ const key = "weekly", radius = 43 / 3;
43
64
  const group = svgNode("g", { "data-window": key, fill: "none", stroke: "currentColor", "stroke-width": 10 / 3 });
44
65
  group.append(svgNode("circle", { cx: 16, cy: 16, r: radius, opacity: 0.18 }));
45
66
  if (windows[key] > 0) group.append(svgNode("circle", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "2.0.35",
3
+ "version": "2.0.36",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -101,7 +101,42 @@ export function createCodexEnvironment() {
101
101
  const scope = node && props(node, value => Boolean((value.router ?? value.value?.router)?.state?.location));
102
102
  return (scope?.router ?? scope?.value?.router)?.state?.location?.pathname ?? null;
103
103
  }
104
- return { composer, composerSurface, rail, railSlot, route, navigation, mcpAppSource, currentThreadId:element => composer(element)?.threadId ?? null,
104
+ function account(signal, invalidate) {
105
+ let client, unsubscribe;
106
+ const cleanup = () => { unsubscribe?.(); unsubscribe = null; client = null; };
107
+ signal?.addEventListener("abort", cleanup, {once:true});
108
+ return {read() {
109
+ let auth, nextClient;
110
+ props(rail(), value => {
111
+ for (const candidate of [value.auth, value.value]) {
112
+ if (candidate && "authMethod" in Object(candidate) && "accountId" in Object(candidate)) auth ||= candidate;
113
+ }
114
+ for (const candidate of [value.queryClient, value.client, value.value]) {
115
+ if (typeof candidate?.getQueryData === "function" && typeof candidate?.getQueryCache === "function") nextClient ||= candidate;
116
+ }
117
+ return Boolean(auth && nextClient);
118
+ });
119
+ if (nextClient !== client) {
120
+ cleanup(); client = nextClient;
121
+ if (invalidate && !signal?.aborted) unsubscribe = client?.getQueryCache().subscribe(event => {
122
+ if (event.query?.queryKey?.[0] === "accounts") invalidate();
123
+ });
124
+ }
125
+ const unknown = {status:"unknown", planType:null, source:null, key:null};
126
+ if (signal?.aborted || !auth || auth.isLoading || !auth.userId || !auth.accountId
127
+ || !["chatgpt", "chatgptAuthTokens"].includes(auth.authMethod)) return unknown;
128
+ const data = client?.getQueryData(["accounts", "check"]);
129
+ const current = data?.accounts?.find(value => value.id === auth.accountId);
130
+ // 已有账号清单却找不到当前身份时,不借用上一个账号的登录套餐。
131
+ if (data && !current) return unknown;
132
+ const cachedPlan = typeof current?.plan_type === "string" ? current.plan_type : null;
133
+ const loginPlan = typeof auth.planAtLogin === "string" ? auth.planAtLogin : null;
134
+ const planType = cachedPlan ?? loginPlan;
135
+ if (!planType || planType === "unknown") return unknown;
136
+ return {status:"available", planType, source:cachedPlan ? "accounts/check" : "planAtLogin", key:JSON.stringify([auth.userId, auth.accountId])};
137
+ }};
138
+ }
139
+ return { composer, composerSurface, rail, railSlot, route, navigation, account, mcpAppSource, currentThreadId:element => composer(element)?.threadId ?? null,
105
140
  // 仅供 UI/插件 native Adapter 使用,交互层不读取 React props。
106
141
  currentFiber, props, report, diagnostics:() => [...capabilities.values()].map(item => ({...item})) };
107
142
  }