minecodex 2.0.44 → 2.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,7 +21,6 @@
21
21
  | 站点收藏 | 发现本地项目网页,收藏网址并快速打开。 |
22
22
  | 快捷聊天增强 | 恢复快捷键,发送后展开,支持历史与窗口固定。 |
23
23
  | 侧栏聚焦 | 收起其他任务分组,再次点击恢复布局。 |
24
- | 隐藏额度推广 | 隐藏输入框上方的套餐升级推广。 |
25
24
 
26
25
  ## 安装
27
26
 
@@ -240,7 +240,7 @@ function findPluginCard(featureId) {
240
240
 
241
241
  function renderPluginCards(features) {
242
242
  // Task 暂停接入,旧版服务返回的卡片也不展示。
243
- features = features.filter(feature => feature.id !== "unread-inbox");
243
+ features = features.filter(feature => !["unread-inbox", "hide-upsell-banner"].includes(feature.id));
244
244
  for (const child of [...elements.pluginList.children]) {
245
245
  if (!child.dataset.featureId) child.remove();
246
246
  }
@@ -36,9 +36,7 @@ const content = {
36
36
  'sidebar-focus': [
37
37
  ['Codex 左侧栏的分组标题。', '多个侧栏分组展开时,当前分组不容易聚焦。', '点击当前分组,一键收起其他分组;再次点击恢复先前的展开状态。'],
38
38
  ['Group headings in the Codex sidebar.', 'Several expanded groups make the current group harder to focus on.', 'Click a group to collapse other groups. Click again to restore their previous expanded state.']],
39
- 'hide-upsell-banner': [
40
- ['Codex 的额度推广提示区域。', '推广提示占用日常操作空间。', '隐藏额度推广提示,保留真实额度与运行错误反馈。'],
41
- ['Usage promotion banners in Codex.', 'Promotions take space during everyday tasks.', 'Hide usage promotions while retaining real usage and error feedback.']],
39
+
42
40
  };
43
41
  export function previewContent(id, locale) {
44
42
  return content[id]?.[locale === 'zh-CN' ? 0 : 1] ?? (locale === 'zh-CN'
@@ -1,14 +1,9 @@
1
1
  // 依照原生 breadcrumb 的依赖角色查找公开模块,不绑定产物 hash 或短变量名。
2
- export async function loadFileNavigationModules() {
2
+ export async function loadFileNavigationModules({read = async url => { const response = await fetch(url); if (!response.ok) throw new Error('file-navigation-source-unavailable'); return response.text(); }, load = url => import(url)} = {}) {
3
3
  const urls = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(n => n.href || n.src);
4
4
  const breadcrumbUrl = urls.find(url => /\/review-file-source-breadcrumb-[^/]+\.js$/.test(url));
5
5
  const mainUrl = urls.find(url => /\/app-main-[^/]+\.js$/.test(url));
6
6
  if (!breadcrumbUrl || !mainUrl) throw new Error('file-navigation-modules-unavailable');
7
- const read = async url => {
8
- const response = await fetch(url);
9
- if (!response.ok) throw new Error('file-navigation-source-unavailable');
10
- return response.text();
11
- };
12
7
  const imports = (source, url) => {
13
8
  const result = new Map();
14
9
  for (const match of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g))
@@ -21,14 +16,14 @@ export async function loadFileNavigationModules() {
21
16
  const binding = async (map, name) => {
22
17
  const value = map.get(name);
23
18
  if (!value) throw new Error('file-navigation-dependency-unavailable');
24
- return (await import(value.url))[value.name];
19
+ return (await load(value.url))[value.name];
25
20
  };
26
21
  const source = await read(breadcrumbUrl), deps = imports(source, breadcrumbUrl);
27
22
  const treeUrl = [...deps.values()].find(v => /\/base-file-tree-/.test(v.url))?.url;
28
23
  const queryUrl = [...deps.values()].find(v => /\/workspace-directory-entries-query-/.test(v.url))?.url;
29
24
  if (!treeUrl || !queryUrl) throw new Error('file-navigation-contract-changed');
30
25
  const [treeSource, querySource, main] = await Promise.all([read(treeUrl), read(queryUrl), read(mainUrl)]);
31
- const [treeModule, queryModule] = await Promise.all([import(treeUrl), import(queryUrl)]);
26
+ const [treeModule, queryModule] = await Promise.all([load(treeUrl), load(queryUrl)]);
32
27
  for (const module of [treeModule, queryModule])
33
28
  for (const value of Object.values(module))
34
29
  if (typeof value === 'function' && /^function ([\w$]+)\(\)\{return\(\1=/.test(String(value))) value();
@@ -38,16 +33,20 @@ export async function loadFileNavigationModules() {
38
33
  const rootOwner = main.match(/\(0,([\w$]+)\.createRoot\)/)?.[1];
39
34
  const rootFactory = rootOwner && main.match(new RegExp(`\\b${rootOwner}=([\\w$]+)\\(\\)`))?.[1];
40
35
  const requestName = querySource.match(/queryFn:\(\{signal:[\w$]+\}\)=>([\w$]+)\(`workspace-directory-entries`/)?.[1];
41
- const [react, dom, request] = await Promise.all([
36
+ const directoryName = querySource.match(/queryFn:\(\{signal:[\w$]+\}\)=>([\w$]+)\([\w$]+,[\w$]+\)/)?.[1];
37
+ const [react, dom, directoryRequest] = await Promise.all([
42
38
  binding(imports(treeSource, treeUrl), reactFactory),
43
39
  binding(imports(main, mainUrl), rootFactory),
44
- binding(imports(querySource, queryUrl), requestName),
40
+ binding(imports(querySource, queryUrl), requestName ?? directoryName),
45
41
  ]);
42
+ // 新版 query 将目录读取委托给 workspaceFiles.listDirectory,参数和取消信号保持原生归属。
43
+ const request = requestName ? directoryRequest : (_method, {params, signal}) => directoryRequest(params, signal);
44
+ if (typeof directoryRequest !== 'function') throw new Error('file-navigation-directory-unavailable');
46
45
  const React = react(), ReactDOM = dom();
47
46
  if (!Tree || !React.createElement || !ReactDOM.createRoot || typeof request !== 'function')
48
47
  throw new Error('file-navigation-components-unavailable');
49
48
  const sharedUrl = [...imports(treeSource, treeUrl).values()].find(value => /\/app-shared-/.test(value.url))?.url;
50
- const shared = sharedUrl && await import(sharedUrl);
49
+ const shared = sharedUrl && await load(sharedUrl);
51
50
  const folder = shared && Object.values(shared).find(value => value?.name === 'folder-light-16');
52
51
  if (!folder?.canvas?.viewBox || typeof folder.body !== 'string')
53
52
  throw new Error('file-navigation-folder-icon-unavailable');
@@ -75,13 +75,18 @@ export function createNativeImageModules({read = async url => (await fetch(url))
75
75
  const [initialSource, mainSource] = await Promise.all([read(initialUrl), read(mainUrl)]);
76
76
  const initial = await load(initialUrl);
77
77
  const asset = (name, references = initialSource) => {
78
+ const loaded = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(node => node.href || node.src)
79
+ .find(url => new RegExp(`/${name}-[\\w-]+\\.js$`).test(url));
80
+ if (loaded) return loaded;
78
81
  const file = references.match(new RegExp(`(?:["'./])(${name}-[\\w-]+\\.js)`))?.[1];
79
82
  if (!file) throw new Error(`Native Images capability is unavailable: ${name}`);
80
83
  return new URL(file, initialUrl).href;
81
84
  };
82
85
  const module = async (name,references) => {
83
86
  // 26.1013 将图库排布与卡片合入入口,其他能力仍按独立模块发现。
84
- if (["library-masonry", "library-item-layout"].includes(name)
87
+ const loaded = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].some(node =>
88
+ new RegExp(`/${name}-[\\w-]+\\.js$`).test(node.href || node.src));
89
+ if (!loaded && ["library-masonry", "library-item-layout"].includes(name)
85
90
  && !new RegExp(`(?:["'./])(${name}-[\\w-]+\\.js)`).test(references ?? initialSource)) {
86
91
  return {url:initialUrl, source:initialSource, exports:initial};
87
92
  }
@@ -129,7 +134,9 @@ export function createNativeImageModules({read = async url => (await fetch(url))
129
134
  module("generated-image-context-menu"),module("library-selection-toolbar"),
130
135
  ]);
131
136
  // 26.1007 将选择状态与图库动作移到 selectable-library-item;工具栏保留按钮与材质。
132
- const selection=await module("selectable-library-item",toolbar.source);
137
+ const selectionRows=await module("selectable-library-item",toolbar.source);
138
+ const selection = /library-move-dialog-[\w-]+\.js/.test(selectionRows.source)
139
+ ? selectionRows : await module("library-item-menu",selectionRows.source);
133
140
  const [moveDialog,dialogs]=await Promise.all([module("library-move-dialog",selection.source),module("library-item-dialogs",toolbar.source)]);
134
141
  const routerLocal = selection.source.match(/get\(([\w$]+)\)\.prepareNavigation\(/)?.[1];
135
142
  const routerAtom = await binding(selection.source, selection.url, routerLocal);
@@ -143,9 +150,9 @@ export function createNativeImageModules({read = async url => (await fetch(url))
143
150
  const iconLocal = toolbar.source.match(/id:`libraryNext.selection.more`[^;]+?children:\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/)?.[1];
144
151
  const [Dropdown, Menu] = await Promise.all([dropdownLocal, itemsLocal].map(local => binding(selection.source, selection.url, local)));
145
152
  const [Button, MoreIcon] = await Promise.all([buttonLocal, iconLocal].map(local => binding(toolbar.source, toolbar.url, local)));
146
- const checkboxFunction = component(selection.exports,source=>/onCheckedChange:/.test(source)&&/viewMode:/.test(source),"selection checkbox");
153
+ const checkboxFunction = component(selectionRows.exports,source=>/onCheckedChange:/.test(source)&&/viewMode:/.test(source),"selection checkbox");
147
154
  const checkboxLocal = String(checkboxFunction).match(/jsx\)\(([\w$]+),\{className:`cursor-interaction`,/)?.[1];
148
- const Checkbox = await binding(selection.source,selection.url,checkboxLocal);
155
+ const Checkbox = await binding(selectionRows.source,selectionRows.url,checkboxLocal);
149
156
  const rangeSelection = component(selection.exports,source=>/^function [\w$]+\(\{orderedIds:/.test(source)&&/extendRange:/.test(source),"range selection");
150
157
  const selectionLocal = toolbar.source.match(/\.set\(([\w$]+),new Set[,)]/)?.[1];
151
158
  const nativeSelectedIds = await binding(toolbar.source,toolbar.url,selectionLocal);
@@ -1,6 +1,7 @@
1
1
  // 只编排来源与加载状态;图库、标签、菜单及详情由原生 Adapter 承载。
2
2
  export function installLocalImageSource(config, signal, sources, native) {
3
3
  let mounted;
4
+ let failedEntry;
4
5
  let revision = 0;
5
6
  let pendingPrompt;
6
7
  const text = (en, zh) => (document.documentElement.lang || navigator.language).startsWith("zh") ? zh : en;
@@ -71,6 +72,7 @@ export function installLocalImageSource(config, signal, sources, native) {
71
72
  record.entry.content.hidden = record.active || record.contentHidden;
72
73
  record.area.hidden = !record.active;
73
74
  if (record.entry.search) record.entry.search.value = record.active ? record.query : record.searchValue;
75
+ record.selectedKey = key;
74
76
  record.nav.render(key);
75
77
  if (record.active) void fetchPage(record, true);
76
78
  }
@@ -79,9 +81,14 @@ export function installLocalImageSource(config, signal, sources, native) {
79
81
  if (signal.aborted) return;
80
82
  const entry = sources.read();
81
83
  if (mounted && mounted.entry.tabs === entry?.tabs && mounted.entry.accountKey === entry?.accountKey && mounted.entry.mode === entry?.mode) {
82
- if (!mounted.active) mounted.nav?.render(entry.selectedKey);
84
+ if (!mounted.active && mounted.nav && mounted.selectedKey !== entry.selectedKey) {
85
+ mounted.selectedKey = entry.selectedKey;
86
+ mounted.nav.render(entry.selectedKey);
87
+ }
83
88
  return;
84
89
  }
90
+ if (failedEntry?.tabs === entry?.tabs && failedEntry?.accountKey === entry?.accountKey && failedEntry?.mode === entry?.mode) return;
91
+ failedEntry = null;
85
92
  unmount();
86
93
  if (!entry) return;
87
94
  const navigation = document.createElement("div");
@@ -126,6 +133,7 @@ export function installLocalImageSource(config, signal, sources, native) {
126
133
  onRetry:error => {
127
134
  console.error("MineCodex native Images rendering",error);
128
135
  if (mounted !== record) return;
136
+ failedEntry = entry;
129
137
  unmount();
130
138
  config.codex.report("images.local-source", false, "Native image rendering failed");
131
139
  },
@@ -145,6 +153,7 @@ export function installLocalImageSource(config, signal, sources, native) {
145
153
  record.nav = nav;
146
154
  record.view = view;
147
155
  entry.tabs.hidden = true;
156
+ record.selectedKey = entry.selectedKey;
148
157
  nav.render(entry.selectedKey);
149
158
  retry.onclick = () => fetchPage(record, true);
150
159
  record.observer = new IntersectionObserver(entries => {
@@ -169,6 +178,7 @@ export function installLocalImageSource(config, signal, sources, native) {
169
178
  entry.search?.addEventListener("keydown", record.onSearchKey, true);
170
179
  } catch {
171
180
  if (mounted === record) {
181
+ failedEntry = entry;
172
182
  unmount();
173
183
  config.codex.report("images.local-source", false, "Native image capability unavailable");
174
184
  }
@@ -68,23 +68,22 @@ export function installMarkdownTableActions(view, native, codex, controller) {
68
68
  h(
69
69
  Actions,
70
70
  { 'data-visible': item.visible, 'data-markdown-copy': 'exclude' },
71
- item.fit || item.overflow
72
- ? h(
73
- Tooltip,
74
- { tooltipContent: wrap, delayOpen: true },
75
- h(
76
- Button,
77
- {
78
- 'aria-label': wrap,
79
- color: 'blockAction',
80
- radius: 'full',
81
- size: 'inputIcon',
82
- onClick: () => controller.toggle(),
83
- },
84
- h(item.fit ? wrapIcons.disable : wrapIcons.enable),
85
- ),
86
- )
87
- : null,
71
+ h(
72
+ Tooltip,
73
+ { tooltipContent: wrap, delayOpen: true },
74
+ h(
75
+ Button,
76
+ {
77
+ 'aria-label': wrap,
78
+ 'aria-pressed': item.fit,
79
+ color: 'blockAction',
80
+ radius: 'full',
81
+ size: 'inputIcon',
82
+ onClick: () => controller.toggle(),
83
+ },
84
+ h(item.fit ? wrapIcons.disable : wrapIcons.enable),
85
+ ),
86
+ ),
88
87
  h(CopyButton, {
89
88
  ariaLabel: copy,
90
89
  buttonText: copy,
@@ -1,3 +1,13 @@
1
+ // 操作栏锚定阅读区域;表格横向展开与滚动不改变恢复按钮的位置。
2
+ export function markdownTableActionGeometry(first, last, frame, scale, scrollTop, padding, headerInset) {
3
+ const left = padding.left;
4
+ const width = Math.max(0, frame.width / scale - padding.left - padding.right);
5
+ const viewportTop = Math.max(0, (first.top - frame.top) / scale + headerInset - 40);
6
+ const visibleBottom = (Math.min(last.bottom, frame.bottom) - frame.top) / scale;
7
+ return {left, width, top: scrollTop + Math.min(viewportTop, Math.max(0, visibleBottom - 48)),
8
+ inView: last.bottom > frame.top && first.top < frame.bottom};
9
+ }
10
+
1
11
  // 保留原生逐行表格及选区;每张表的行共享横向位置,不移动正文节点。
2
12
  export function installMarkdownTables(view, api, wrapState) {
3
13
  const { Decoration, ViewPlugin, StateEffect, Compartment, WidgetType } = api;
@@ -323,14 +333,18 @@ export function installMarkdownTables(view, api, wrapState) {
323
333
  if (!rows.length) continue;
324
334
  const first = rows[0].getBoundingClientRect();
325
335
  const headerInset = parseFloat(getComputedStyle(rows[0]).paddingTop);
336
+ const padding = getComputedStyle(view.scrollDOM);
337
+ const geometry = markdownTableActionGeometry(first, rows.at(-1).getBoundingClientRect(), frameRect, scale,
338
+ view.scrollDOM.scrollTop, {left:parseFloat(padding.paddingLeft),right:parseFloat(padding.paddingRight)}, headerInset);
326
339
  items.push({
327
340
  id,
328
341
  fit: wrapState.get(),
329
342
  overflow: (scrollLimits.get(id) ?? 0) > 0,
330
- visible: hovered === id,
331
- left: (first.left - frameRect.left) / scale,
332
- top: (first.top - frameRect.top) / scale + view.scrollDOM.scrollTop + headerInset - 40,
333
- width: first.width / scale,
343
+ // 关闭换行后常显恢复入口;键盘用户聚焦任一表格控件也保持显示。
344
+ visible: geometry.inView && (hovered === id || tableId(document.activeElement) === id || !wrapState.get()),
345
+ left: geometry.left,
346
+ top: geometry.top,
347
+ width: geometry.width,
334
348
  source: view.state.sliceDoc(Number(id), Number(table.dataset.to)),
335
349
  });
336
350
  }
@@ -14,7 +14,7 @@ import { loadNativeMarkdownLinks, installMarkdownLinks } from './adapters/codex/
14
14
  import { loadNativeMarkdownToc } from '../../../packages/codex-adapter/src/markdown/toc-native.mjs';
15
15
  import { installMarkdownToc } from './adapters/codex/toc.mjs';
16
16
  import { readFile } from 'node:fs/promises';
17
- import { installMarkdownTables } from './adapters/codex/tables.mjs';
17
+ import { installMarkdownTables, markdownTableActionGeometry } from './adapters/codex/tables.mjs';
18
18
  import { installMarkdownLists } from './adapters/codex/lists.mjs';
19
19
  import { createMarkdownPreviewAdapter } from '../../../packages/codex-adapter/src/markdown/preview.mjs';
20
20
  export async function createPageScriptSource() {
@@ -29,6 +29,7 @@ export async function createPageScriptSource() {
29
29
  installMarkdownLists,
30
30
  loadNativeMarkdownLinks,
31
31
  installMarkdownLinks,
32
+ markdownTableActionGeometry,
32
33
  installMarkdownTables,
33
34
  loadNativeMarkdownToc,
34
35
  installMarkdownToc,
@@ -1,12 +1,10 @@
1
1
  import { saveQvDiagram } from '../../mermaid/export.mjs';
2
2
 
3
3
  // 图表下载协议只由实验性功能加载,阅读增强不依赖此能力。
4
- export async function loadNativeMermaidActions(native) {
4
+ export async function loadNativeMermaidActions(native, {read = async url => { const response = await fetch(url); if (!response.ok) throw new Error('native-diagram-source-unavailable'); return response.text(); }, load = url => import(url)} = {}) {
5
5
  const url = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')]
6
6
  .map(node => node.href || node.src).find(url => /\/app-initial-[^/]+\.js/.test(url));
7
- const response = await fetch(url);
8
- if (!response.ok) throw new Error('native-diagram-source-unavailable');
9
- const source = await response.text();
7
+ const source = await read(url);
10
8
  const section = marker => {
11
9
  const at = source.indexOf(marker);
12
10
  if (at < 0) throw new Error('native-diagram-contract-changed');
@@ -15,7 +13,9 @@ export async function loadNativeMermaidActions(native) {
15
13
  const preview = section('id:`imagePreviewDialog.label`');
16
14
  const downloadSection = preview.slice(preview.indexOf('suggestedFilename:', preview.indexOf('downloadSrc:')));
17
15
  const downloadIcon = downloadSection.match(/children:\(0,[\w$]+\.jsx\)\(([\w$]+),\{className:`icon-xs`\}/)?.[1];
18
- const requestName = section('Failed to download image').match(/await ([\w$]+)\(`save-file`,/)?.[1];
16
+ const download = section('Failed to download image');
17
+ const downloadName = download.match(/^function ([\w$]+)\(/)?.[1];
18
+ const requestName = download.match(/await ([\w$]+)\(`save-file`,/)?.[1];
19
19
  const imports = new Map();
20
20
  for (const match of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g))
21
21
  for (const entry of match[1].split(',')) {
@@ -23,10 +23,18 @@ export async function loadNativeMermaidActions(native) {
23
23
  imports.set(parts.at(-1), { name:parts[0], url:new URL(match[2],url).href });
24
24
  }
25
25
  const binding = async name => {
26
+ const alias = markdownExportAlias(source, name);
27
+ if (alias) return (await load(url))[alias];
26
28
  const item = imports.get(name);
27
29
  if (!item) throw new Error('native-diagram-save-request-unavailable');
28
- return (await import(item.url))[item.name];
30
+ return (await load(item.url))[item.name];
29
31
  };
32
+ // 新版原生下载使用 workspaceFiles.saveStream,沿用原生字节流与保存动作。
33
+ if (download.includes('workspaceFiles.saveStream')) {
34
+ const [DownloadIcon, downloadImage] = await Promise.all([binding(downloadIcon), binding(downloadName)]);
35
+ if (typeof DownloadIcon !== 'function' || typeof downloadImage !== 'function') throw new Error('native-diagram-download-unavailable');
36
+ return {...native, DownloadIcon, downloadImage};
37
+ }
30
38
  const [DownloadIcon, saveFile] = await Promise.all([binding(downloadIcon),binding(requestName)]);
31
39
  return { ...native, DownloadIcon, downloadImage({downloadSrc,suggestedFilename}) {
32
40
  return saveQvDiagram(downloadSrc,saveFile,markdownDownloadsDirectory,suggestedFilename);
@@ -1,3 +1,13 @@
1
+ // 强度提交会重建面板;只延续滑条自身的焦点,不抢走其他控件或已关闭菜单的焦点。
2
+ export function replaceModelSliderShell(parentMenu, existing, shell) {
3
+ const previousRail = existing?.querySelector('[data-codex-model-slider-rail]');
4
+ const restore = Boolean(previousRail && document.activeElement === previousRail);
5
+ existing?.remove();
6
+ parentMenu.append(shell);
7
+ if (restore && parentMenu.isConnected && shell.isConnected)
8
+ shell.querySelector('[data-codex-model-slider-rail]')?.focus({preventScroll:true});
9
+ }
10
+
1
11
  // 专有原生控制器适配与呈现由插件维护;宿主只提供刷新和共用 UI。
2
12
  export function installModelSlider(signal, { feature, ui, createIcon, requestRefresh, locale, codex, requestAction }, rules, createModelControllerAdapter) {
3
13
  const modelSelectorFeature = feature;
@@ -2079,7 +2089,6 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
2079
2089
  return false;
2080
2090
  }
2081
2091
  modelSelectorGenericContainers.get(nativeContainer)?.effectMirror?.disconnect();
2082
- existing?.remove();
2083
2092
  if (!modelSelectorGenericContainers.has(nativeContainer)) {
2084
2093
  modelSelectorGenericContainers.set(nativeContainer, {
2085
2094
  position: nativeContainer.style.position,
@@ -2099,7 +2108,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
2099
2108
  nativeContainer.inert = true;
2100
2109
  shell.setAttribute("data-codex-model-slider-generic", key);
2101
2110
  shell.setAttribute("data-codex-model-slider-structure-key", structureKey);
2102
- parentMenu.append(shell);
2111
+ replaceModelSliderShell(parentMenu, existing, shell);
2103
2112
  modelSelectorGenericContainers.get(nativeContainer).effectMirror = syncClonedPowerEffects(nativeContainer, shell);
2104
2113
  centerGenericModelPicker(parentMenu);
2105
2114
  return true;
@@ -1,10 +1,11 @@
1
1
  import { createModelControllerAdapter } from "./adapters/codex/controller.mjs";
2
2
  import { createModelRules } from "./domain/model-rules.mjs";
3
- import { installModelSlider } from "./frontend/install.mjs";
3
+ import { installModelSlider, replaceModelSliderShell } from "./frontend/install.mjs";
4
4
 
5
5
  // 两部分均显式序列化,Renderer 不捕获 Node imports。
6
6
  export function createModelSliderInstallerSource() {
7
7
  return `function installModelSliderFeature(signal, context) {
8
+ const replaceModelSliderShell = ${replaceModelSliderShell.toString()};
8
9
  return (${installModelSlider.toString()})(signal, context, (${createModelRules.toString()})(), ${createModelControllerAdapter.toString()});
9
10
  }`;
10
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "2.0.44",
3
+ "version": "2.0.45",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,8 +30,6 @@
30
30
  "features/images/src",
31
31
  "features/images/web",
32
32
  "features/images/codex-feature.json",
33
- "features/hide-upsell-banner/src",
34
- "features/hide-upsell-banner/codex-feature.json",
35
33
  "features/session-status/src",
36
34
  "features/session-status/codex-feature.json",
37
35
  "features/session-status/vendor/lucide-LICENSE",
@@ -94,7 +92,7 @@
94
92
  },
95
93
  "scripts": {
96
94
  "start": "node packages/cli/bin/mcx.mjs serve",
97
- "test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs features/hide-upsell-banner/test/*.test.mjs features/session-status/test/*.test.mjs features/usage-meter/test/*.test.mjs apps/dashboard/test/*.test.mjs features/unread-inbox/test/*.test.mjs features/quick-chat/test/*.test.mjs features/composer-expand/test/*.test.mjs features/sidebar-focus/test/*.test.mjs features/sites/test/*.test.mjs packages/ui/test/*.test.mjs packages/codex-adapter/test/*.test.mjs features/markdown-reading/test/*.test.mjs features/file-navigation/test/*.test.mjs packages/plugin/test/*.test.mjs features/mermaid-diagrams/test/*.test.mjs",
95
+ "test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs features/session-status/test/*.test.mjs features/usage-meter/test/*.test.mjs apps/dashboard/test/*.test.mjs features/unread-inbox/test/*.test.mjs features/quick-chat/test/*.test.mjs features/composer-expand/test/*.test.mjs features/sidebar-focus/test/*.test.mjs features/sites/test/*.test.mjs packages/ui/test/*.test.mjs packages/codex-adapter/test/*.test.mjs features/markdown-reading/test/*.test.mjs features/file-navigation/test/*.test.mjs packages/plugin/test/*.test.mjs features/mermaid-diagrams/test/*.test.mjs",
98
96
  "test:cli": "node --test packages/cli/test/*.test.mjs",
99
97
  "test:runtime": "node --test packages/runtime-host/test/*.test.mjs",
100
98
  "check": "node scripts/check-syntax.mjs",
@@ -2,14 +2,13 @@ import { randomBytes } from "node:crypto";
2
2
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
- export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "hide-upsell-banner", "session-status", "usage-meter", "sidebar-focus", "sites", "markdown-reading", "mermaid-diagrams", "file-navigation", "quick-chat", "composer-expand"]);
5
+ export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "session-status", "usage-meter", "sidebar-focus", "sites", "markdown-reading", "mermaid-diagrams", "file-navigation", "quick-chat", "composer-expand"]);
6
6
  export const DEFAULT_CONFIG = Object.freeze({
7
7
  schemaVersion: 1,
8
8
  features: Object.freeze({
9
9
  "model-slider": true,
10
10
  notes: true,
11
11
  images: true,
12
- "hide-upsell-banner": true,
13
12
  "session-status": true,
14
13
  "usage-meter": true,
15
14
  "sidebar-focus": true,
@@ -44,6 +43,8 @@ export function normalizeConfig(value = {}) {
44
43
  };
45
44
  // 暂停的 Task 通知不继承旧配置,也不再提供开启入口。
46
45
  delete features["unread-inbox"];
46
+ // 已移除的额度推广隐藏功能不继承旧启用项。
47
+ delete features["hide-upsell-banner"];
47
48
  // 清理已退役的独立 Quick Chat 修复开关。
48
49
  delete features["quick-chat-repair"];
49
50
  return {
@@ -14,7 +14,7 @@ import { createDataTransfer, MAX_TRANSFER_BYTES } from "./data-transfer/service.
14
14
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
15
15
  const packageMetadata = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
16
16
  const MAX_BODY_BYTES = 64 * 1024;
17
- const DASHBOARD_FEATURE_ORDER = ["model-slider", "notes", "usage-meter", "session-status", "markdown-reading", "mermaid-diagrams", "file-navigation", "images", "sites", "quick-chat", "composer-expand", "sidebar-focus", "hide-upsell-banner"];
17
+ const DASHBOARD_FEATURE_ORDER = ["model-slider", "notes", "usage-meter", "session-status", "markdown-reading", "mermaid-diagrams", "file-navigation", "images", "sites", "quick-chat", "composer-expand", "sidebar-focus"];
18
18
  const CONTENT_TYPES = new Map([
19
19
  [".css", "text/css; charset=utf-8"],
20
20
  [".html", "text/html; charset=utf-8"],
@@ -1,24 +0,0 @@
1
- {
2
- "schemaVersion": 1,
3
- "id": "hide-upsell-banner",
4
- "version": "0.1.11",
5
- "label": {
6
- "en": "Hide Usage Promotions",
7
- "zh-CN": "隐藏额度推广"
8
- },
9
- "description": {
10
- "en": "Hide plan upgrade banners above the message box for a cleaner chat.",
11
- "zh-CN": "隐藏输入框上方的套餐升级推广,让对话更清爽。"
12
- },
13
- "entry": {
14
- "kind": "page-script",
15
- "script": "src/hide-upsell-banner.mjs"
16
- },
17
- "icon": {
18
- "viewBox": "0 0 24 24",
19
- "markup": "<rect width=\"18\" height=\"12\" x=\"3\" y=\"6\" rx=\"2.5\"></rect><path d=\"m5 5 14 14\"></path>"
20
- },
21
- "placement": {
22
- "order": 40
23
- }
24
- }
@@ -1,94 +0,0 @@
1
- // page-script feature body.
2
- // Injected by CodexRuntimeHost into the Codex renderer through
3
- // `new Function("signal", "window", "document", "MutationObserver", source)`,
4
- // so this file is a function body: it runs once with those bindings in scope
5
- // and may return a cleanup function. It hides the "You're out of Codex and
6
- // Work usage" upsell banner that the desktop app renders above the composer
7
- // when the account rate-limit snapshot marks the plan as exhausted. It never
8
- // touches the rate-limit query path: `account/rateLimits/read` and the
9
- // app-server JSON-RPC traffic stay intact.
10
- const HIDDEN_MARKER = "data-codex-upsell-banner-hidden";
11
- const hiddenBanners = new Map();
12
- let disposed = false;
13
-
14
- function isUpsellBanner(element) {
15
- if (!(element instanceof window.HTMLElement)) return false;
16
- // 文件侧栏也是 aside,正文中的用量说明不能使整个预览被隐藏。
17
- if (element.querySelector("[role='tabpanel']")) return false;
18
- const text = (element.textContent ?? "").trim();
19
- if (!text) return false;
20
- const title = (
21
- element.querySelector("h1, h2, h3, [role='heading']")?.textContent
22
- ?? text.split("\n", 1)[0]
23
- ).trim();
24
- return /out of Codex(?: and Work)? usage/i.test(title)
25
- || /out of Codex messages/i.test(title)
26
- || /Codex (?:message )?usage limit reached/i.test(title)
27
- || /Codex.*(?:用量|使用量|使用额度|额度|消息).*(?:用完|用尽|耗尽|已达|达到.*上限|上限)/i.test(title)
28
- || /(?:已达到|达到|已用完|已用尽|耗尽).{0,20}Codex.{0,20}(?:用量|使用量|额度|消息|上限)/i.test(title)
29
- // 通用标题须同时具有原生用量横幅的关闭语义,不能仅按耗尽文案隐藏通知。
30
- || (/^you['’]re out of usage[.!]?$/i.test(title)
31
- && [...element.querySelectorAll("button[aria-label]")].some(button =>
32
- /^(?:Dismiss usage banner|不再显示用量横幅|關閉用量橫幅|關閉用量橫額)$/i.test(button.getAttribute("aria-label") ?? "")));
33
- }
34
-
35
- function hideBanner(element) {
36
- if (!hiddenBanners.has(element)) hiddenBanners.set(element, {
37
- display:element.style.getPropertyValue("display"), priority:element.style.getPropertyPriority("display"),
38
- });
39
- if (!element.hasAttribute(HIDDEN_MARKER)) element.setAttribute(HIDDEN_MARKER, "");
40
- // 标记不代表样式仍有效;只在需要时写入,避免 observer 自触发循环。
41
- if (element.style.display !== "none") element.style.display = "none";
42
- }
43
-
44
- function restoreBanner(element) {
45
- const original = hiddenBanners.get(element);
46
- element.removeAttribute(HIDDEN_MARKER);
47
- if (element.style.display === "none") {
48
- if (original.display) element.style.setProperty("display", original.display, original.priority);
49
- else element.style.removeProperty("display");
50
- }
51
- hiddenBanners.delete(element);
52
- }
53
-
54
- function hideVisibleBanners(root = document) {
55
- for (const element of hiddenBanners.keys()) if (!element.isConnected || !isUpsellBanner(element)) restoreBanner(element);
56
- if (root.matches?.("aside") && isUpsellBanner(root)) hideBanner(root);
57
- for (const element of root.querySelectorAll("aside")) {
58
- if (isUpsellBanner(element)) hideBanner(element);
59
- }
60
- }
61
-
62
- let rescanQueued = false;
63
- function rescanBanners() {
64
- // React can render the banner in stages: an empty <aside> shell first, then
65
- // text and style updates on the same element. A childList-only observer
66
- // misses those later mutations, so every mutation triggers a full rescan
67
- // instead of only inspecting freshly added nodes.
68
- if (rescanQueued) return;
69
- rescanQueued = true;
70
- queueMicrotask(() => {
71
- rescanQueued = false;
72
- if (!disposed) hideVisibleBanners(document);
73
- });
74
- }
75
-
76
- hideVisibleBanners();
77
- const observer = new MutationObserver(() => {
78
- rescanBanners();
79
- });
80
- observer.observe(document.documentElement, {
81
- childList: true,
82
- subtree: true,
83
- attributes: true,
84
- attributeFilter: ["style", "class", "hidden", "aria-label"],
85
- characterData: true,
86
- });
87
-
88
- config.registerSelfCheck?.(() => ({state:hiddenBanners.size ? "available" : "not-applicable",reason:hiddenBanners.size ? null : "no-usage-banner",
89
- checks:[...hiddenBanners.keys()].filter(node=>node.isConnected).map(()=>({name:"usage-banner-hidden",available:true}))}));
90
- return () => {
91
- disposed = true;
92
- observer.disconnect();
93
- for (const element of hiddenBanners.keys()) restoreBanner(element);
94
- };