minecodex 2.0.44 → 2.0.46

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 (34) hide show
  1. package/README.md +0 -1
  2. package/apps/dashboard/app.js +1 -1
  3. package/apps/dashboard/assets/preview-content.js +4 -3
  4. package/features/file-navigation/src/adapters/codex/modules.mjs +10 -11
  5. package/features/files-auto-refresh/assets/folder-sync.svg +7 -0
  6. package/features/files-auto-refresh/codex-feature.json +17 -0
  7. package/features/files-auto-refresh/src/adapters/codex/directories.mjs +75 -0
  8. package/features/files-auto-refresh/src/entry.mjs +58 -0
  9. package/features/files-auto-refresh/src/injection-source.mjs +8 -0
  10. package/features/files-auto-refresh/src/refresh.mjs +32 -0
  11. package/features/files-auto-refresh/vendor/lucide-LICENSE +43 -0
  12. package/features/images/src/adapters/codex/native-image-modules.mjs +11 -4
  13. package/features/images/src/frontend/local-image-source.mjs +11 -1
  14. package/features/markdown-reading/src/adapters/codex/table-actions.mjs +16 -17
  15. package/features/markdown-reading/src/adapters/codex/tables.mjs +18 -4
  16. package/features/markdown-reading/src/injection-source.mjs +2 -1
  17. package/features/mermaid-diagrams/src/adapters/codex/native.mjs +14 -6
  18. package/features/model-slider/src/frontend/install.mjs +11 -2
  19. package/features/model-slider/src/injection-source.mjs +2 -1
  20. package/features/quick-chat/assets/close.svg +15 -0
  21. package/features/quick-chat/codex-feature.json +2 -1
  22. package/features/quick-chat/src/adapters/codex/lifecycle.mjs +27 -0
  23. package/features/quick-chat/src/adapters/codex/renderer.mjs +8 -5
  24. package/features/quick-chat/src/adapters/codex/shortcut.mjs +38 -14
  25. package/features/quick-chat/src/frontend/entry.mjs +33 -18
  26. package/features/quick-chat/src/injection-source.mjs +2 -1
  27. package/features/usage-meter/src/adapters/codex/renderer.mjs +14 -4
  28. package/features/usage-meter/src/usage-meter.mjs +24 -19
  29. package/package.json +6 -4
  30. package/packages/cli/src/config.mjs +4 -2
  31. package/packages/cli/src/control-server.mjs +1 -1
  32. package/packages/ui/src/native/tokens.mjs +2 -0
  33. package/features/hide-upsell-banner/codex-feature.json +0 -24
  34. package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +0 -94
@@ -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
  }
@@ -0,0 +1,15 @@
1
+ <!-- @license lucide-static v0.468.0 - ISC; source: lucide-icons/lucide icons/x.svg -->
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ width="24"
5
+ height="24"
6
+ viewBox="0 0 24 24"
7
+ fill="none"
8
+ stroke="currentColor"
9
+ stroke-width="2"
10
+ stroke-linecap="round"
11
+ stroke-linejoin="round"
12
+ >
13
+ <path d="M18 6 6 18" />
14
+ <path d="m6 6 12 12" />
15
+ </svg>
@@ -15,7 +15,8 @@
15
15
  "script": "src/frontend/entry.mjs",
16
16
  "sourceFactory": "src/injection-source.mjs",
17
17
  "resources": {
18
- "history": "assets/history.svg"
18
+ "history": "assets/history.svg",
19
+ "close": "assets/close.svg"
19
20
  }
20
21
  },
21
22
  "icon": {
@@ -0,0 +1,27 @@
1
+ // 从原生“丢弃 Quick Chat”动作中仅取关闭和草稿清理;不调用会话销毁,保留后台生成。
2
+ export function quickChatLifecycleActions(source) {
3
+ const match = source.match(/let ([\w$]+)=([\w$]+)\.get\(([\w$]+)\);([\w$]+)\(\2\),\2\.set\(\3,null\),\1!=null&&\(([\w$]+)\(\2,\{draftKey:\1\.conversationId,content:null\}\),([\w$]+)\(\2,\1\.conversationId,[`"'][`"']\),\2\.set\(([\w$]+),\1\.conversationId,\[\]\)/);
4
+ if (!match) throw new Error('quick-chat-close-contract-unavailable');
5
+ const exports = source.slice(source.lastIndexOf('export{'));
6
+ const exported = local => {
7
+ const escaped = local.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
8
+ const name = exports.match(new RegExp(`(?:\\{|,)${escaped} as ([\\w$]+)(?:,|\\})`))?.[1];
9
+ if (!name) throw new Error('quick-chat-close-export-unavailable');
10
+ return name;
11
+ };
12
+ const roles = {session:exported(match[3]),close:exported(match[4]),draft:exported(match[5]),text:exported(match[6]),attachments:exported(match[7])};
13
+ const mode = source.match(/([\w$]+)=[\w$]+\([\w$]+,\(\{get:[\w$]+\}\)=>\{[\s\S]{0,700}?conversationDetailMode[\s\S]{0,700}?[`"']STEPS_PROSE[`"'][^{}]*\?[`"']work[`"']:[`"']codex[`"']/)?.[1];
14
+ if (mode) roles.mode=exported(mode);
15
+ return roles;
16
+ }
17
+
18
+ export function quickChatScope(codex, element) {
19
+ for (let fiber=codex.currentFiber(element),depth=0;fiber && depth++<200;fiber=fiber.return) {
20
+ const values=[];
21
+ for (let hook=fiber.memoizedState,count=0;hook && count++<150;hook=hook.next) values.push(hook.memoizedState?.current);
22
+ for (const row of fiber.updateQueue?.memoCache?.data ?? []) values.push(...row);
23
+ const scope=values.find(value=>typeof value?.get==='function' && typeof value.set==='function' && value.scope?.__scopeBrand==='AppScope');
24
+ if (scope) return scope;
25
+ }
26
+ return null;
27
+ }
@@ -18,10 +18,14 @@ export function createQuickChatAdapter(codex) {
18
18
  if (!history) return null;
19
19
  const state = codex.props(editable, props => typeof props.submitDisabled === "boolean" && props.conversationId);
20
20
  const controller = codex.props(editable, props => props.composerController?.eventEmitter)?.composerController;
21
+ const expanded = Boolean(floating.presentation === "expanded" || floating.forceExpanded);
21
22
  const minimize = [...surface.querySelectorAll('button')].find(button => {
22
- const props = codex.props(button, props => props.standalone === true && typeof props.onMinimize === "function" && typeof props.isCompact === "boolean", 5);
23
- return Boolean(props && button.checkVisibility({visibilityProperty:true}) && !button.hasAttribute('data-mcx-quick-chat-history'));
23
+ const props = codex.props(button, props => (props.standalone === true || expanded) && typeof props.onMinimize === "function" && typeof props.isCompact === "boolean"
24
+ && Object.keys(props).every(key => ["standalone","isCompact","onMinimize"].includes(key)), 5);
25
+ return Boolean(props && button.checkVisibility({visibilityProperty:true}) && !button.hasAttribute('data-mcx-quick-chat-history') && !button.hasAttribute('data-mcx-quick-chat-close'));
24
26
  });
27
+ const drag = expanded && [...surface.querySelectorAll('header button[data-quick-chat-drag-handle]')]
28
+ .find(button => button.checkVisibility({visibilityProperty:true}));
25
29
  let receipt = null;
26
30
  for (let fiber = codex.currentFiber(surface), depth = 0; fiber && depth < 80; depth++, fiber = fiber.return) {
27
31
  if (fiber.memoizedProps?.session?.conversationId !== session.conversationId) continue;
@@ -41,13 +45,12 @@ export function createQuickChatAdapter(codex) {
41
45
  const project = [...surface.querySelectorAll('[data-composer-rail-item] button')].find(button =>
42
46
  codex.props(button, props => props.variant === "composer" && typeof props.onProjectChange === "function"));
43
47
  const projectRail = project?.closest('[data-composer-rail-item]') ?? null;
44
- const expanded = Boolean(floating.presentation === "expanded" || floating.forceExpanded);
45
48
  const emitter = controller?.eventEmitter;
46
49
  const subscribable = typeof emitter?.addListener === "function" && typeof emitter?.removeListener === "function";
47
50
  if (subscribable && !subscriptions.has(emitter)) subscriptions.set(emitter, nextSubscription++);
48
- const snapshot = {surface, editable, minimize, projectRail, background:!session.hasConversation && floating.forceExpanded ? background : null, sessionId:session.conversationId, hasConversation:session.hasConversation,
51
+ const snapshot = {surface, editable, minimize, closeAnchor:drag || minimize, projectRail, background:!session.hasConversation && floating.forceExpanded ? background : null, sessionId:session.conversationId, hasConversation:session.hasConversation,
49
52
  canSubmit:Boolean(state && !state.submitDisabled && !state.submitLoading), receiptId:receipt ? receipts.get(receipt) : 0, receiptPresentation:receipt?.presentation ?? null, subscriptionId:subscribable ? subscriptions.get(emitter) : 0,
50
- expanded, showHistory:Boolean(minimize && !expanded), historyVisible:quickChatHistoryVisible(floating.renderTranscript?.()),
53
+ expanded, standalone:Boolean(minimize && codex.props(minimize,props=>props.standalone===true,5)), showHistory:Boolean(minimize && !expanded), showClose:Boolean(minimize && expanded), historyVisible:quickChatHistoryVisible(floating.renderTranscript?.()),
51
54
  expand:() => floating.onPresentationChange("expanded"),
52
55
  // 历史动作通过 forceExpanded 临时展开;不要覆盖最小化后的恢复形态。
53
56
  history:() => history.onSelect()};
@@ -1,3 +1,4 @@
1
+ import {quickChatLifecycleActions, quickChatScope} from "./lifecycle.mjs";
1
2
  // 通过原生能力检查与已有会话分支定位打开动作,不依赖构建哈希或压缩名称。
2
3
  export function quickChatOpenExport(source) {
3
4
  const match = source.match(/function ([\w$]+)\(([\w$]+),[\w$]+=\{\}\)\{if\(!\2\.get\([\w$]+,\{name:[`"']chatgpt\.quick-chat[`"']\}\)\.isCapable\)return;let [\w$]+=\2\.get\([\w$]+\),[\w$]+=\2\.get\([\w$]+\);if\(/);
@@ -83,13 +84,13 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
83
84
  const picker = codex.props(button, p => typeof p.onModeSelect === 'function' && typeof p.mode === 'string', 80);
84
85
  // 原生 ChatGPT 也使用 work 路由,具体产品由 workModeAccess 区分。
85
86
  if (picker?.mode !== 'codex' && !(picker?.mode === 'work' && picker.workModeAccess === 'chatgpt')) continue;
86
- for (let fiber = codex.currentFiber(button), depth = 0; fiber && depth++ < 200; fiber = fiber.return) {
87
- const values = [];
88
- for (let hook = fiber.memoizedState, n = 0; hook && n++ < 150; hook = hook.next) values.push(hook.memoizedState?.current);
89
- for (const row of fiber.updateQueue?.memoCache?.data ?? []) values.push(...row);
90
- const scope = values.find(v => typeof v?.get === 'function' && typeof v.set === 'function' && v.scope?.__scopeBrand === 'AppScope');
91
- if (scope) return scope;
92
- }
87
+ const scope=quickChatScope(codex,button);
88
+ if (scope) return scope;
89
+ }
90
+ // 侧栏可能卸载;从仍挂载的窗口内容取当前 scope,模式由原生状态判定。
91
+ if (bindings?.mode) for (const anchor of document.querySelectorAll('[data-app-navigation-rail], [data-codex-composer-root], [data-chatgpt-composer], main, #root > *, #app > *')) {
92
+ const scope=quickChatScope(codex,anchor);
93
+ if (scope && bindings.mode(scope)==='codex') return scope;
93
94
  }
94
95
  return null;
95
96
  }
@@ -120,12 +121,29 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
120
121
  if (!response.ok) throw error;
121
122
  native=quickChatNativeCommand(await response.text());
122
123
  }
123
- if (native) return {native:true};
124
- const roles = quickChatLifecycleExports(source);
125
- if (!module[roles.openState] && typeof module[roles.initialize] === 'function') module[roles.initialize]();
126
- const open = module[roles.open], create = module[roles.create], openState = module[roles.openState];
127
- if (typeof open !== 'function' || typeof create !== 'function' || !openState) throw new Error('quick-chat-open-action-unavailable');
128
- return {native:false, open, create, isOpen:scope => scope.get(openState)};
124
+ const bindings={native};
125
+ if (!native) {
126
+ const roles = quickChatLifecycleExports(source);
127
+ if (!module[roles.openState] && typeof module[roles.initialize] === 'function') module[roles.initialize]();
128
+ const open = module[roles.open], create = module[roles.create], openState = module[roles.openState];
129
+ if (typeof open !== 'function' || typeof create !== 'function' || !openState) throw new Error('quick-chat-open-action-unavailable');
130
+ Object.assign(bindings,{open,create,isOpen:scope=>scope.get(openState)});
131
+ }
132
+ try {
133
+ const actions=quickChatLifecycleActions(source);
134
+ if (actions.mode && module[actions.mode]) bindings.mode=scope=>scope.get(module[actions.mode]);
135
+ if (['close','draft','text'].every(key=>typeof module[actions[key]]==='function') && module[actions.session] && module[actions.attachments]) {
136
+ bindings.close=(scope,id)=>{
137
+ if (scope.get(module[actions.session])?.conversationId!==id) return false;
138
+ module[actions.draft](scope,{draftKey:id,content:null});
139
+ module[actions.text](scope,id,'');
140
+ scope.set(module[actions.attachments],id,[]);
141
+ module[actions.close](scope);
142
+ return true;
143
+ };
144
+ }
145
+ } catch { codex.report('quick-chat-close',false,'quick-chat-close-contract-unavailable'); }
146
+ return bindings;
129
147
  }
130
148
  const ready = (discover ?? load)().then(value => {
131
149
  if (disposed || signal.aborted) return;
@@ -146,6 +164,12 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
146
164
  }
147
165
  return true;
148
166
  }
167
+ function close(surface,id) {
168
+ const scope=quickChatScope(codex,surface);
169
+ if (!scope || !bindings?.close || disposed || signal.aborted) return false;
170
+ pendingFocus=null;
171
+ return bindings.close(scope,id);
172
+ }
149
173
  function keydown(event) {
150
174
  const primaryModifier = windows ? event.ctrlKey && !event.metaKey : event.metaKey && !event.ctrlKey;
151
175
  if (event.defaultPrevented || event.repeat || event.isComposing
@@ -156,5 +180,5 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
156
180
  document.addEventListener('keydown', keydown, {signal});
157
181
  function dispose() { disposed = true; stopWaiting?.(); document.removeEventListener('keydown', keydown); bindings = null; pendingFocus=null; }
158
182
  signal.addEventListener('abort', dispose, {once:true});
159
- return {ready, dispose, sync:focusNew, activate, available:() => Boolean(bindings && !bindings.native && !disposed && !signal.aborted)};
183
+ return {ready, dispose, sync:focusNew, activate, close, canClose:() => Boolean(bindings?.close && !disposed && !signal.aborted), available:() => Boolean(bindings && !bindings.native && !disposed && !signal.aborted)};
160
184
  }
@@ -6,13 +6,17 @@ let scheduled = false, disposed = false;
6
6
  const text = (en,zh) => (document.documentElement.lang || navigator.language).startsWith('zh') ? zh : en;
7
7
  const style = document.createElement('style');
8
8
  style.textContent = `
9
- [data-mcx-quick-chat-history]{margin-inline-end:8px;flex-shrink:0}
10
- [data-mcx-quick-chat-history]>svg{width:16px;height:16px}
9
+ [data-mcx-quick-chat-history],[data-mcx-quick-chat-close]{margin-inline-end:8px;flex-shrink:0}
10
+ [data-mcx-quick-chat-history]>svg,[data-mcx-quick-chat-close]>svg{width:16px;height:16px}
11
+ [data-mcx-quick-chat-close]{margin-inline-end:0}
11
12
  [data-mcx-quick-chat-minimize]{border-radius:calc(infinity * 1px)}
13
+ /* 单行输入沿用控件中线,消除原生编辑区换行布局留下的底部间距。 */
14
+ [data-mcx-quick-chat-compact] [data-composer-input]:has(>[data-composer-input-layout="single-line"]){margin-block:0;align-self:center}
15
+ [data-mcx-quick-chat-compact] [data-composer-input-layout="single-line"]{margin-block:0;align-self:center}
12
16
  [data-mcx-quick-chat-project-rail]{display:none!important}
13
17
  /* 隐藏唯一的项目栏时,一并收起原生 rail 的圆角留白与负 margin。 */
14
18
  [data-composer-rail]:has(>[data-mcx-quick-chat-project-rail]):not(:has(>:not([data-mcx-quick-chat-project-rail]):not(:empty))){display:none!important}
15
- [data-floating-chat-surface]>:has(>[data-mcx-quick-chat-history]){display:flex;align-items:center}
19
+ [data-floating-chat-surface]>:has(>[data-mcx-quick-chat-history],>[data-mcx-quick-chat-close]){display:flex;align-items:center}
16
20
  `;
17
21
  document.head.append(style);
18
22
  function releaseBackground(instance) {
@@ -21,7 +25,7 @@ function releaseBackground(instance) {
21
25
  instance.background = null;
22
26
  }
23
27
  function releaseProjectRail(instance) { instance.projectRail?.removeAttribute('data-mcx-quick-chat-project-rail'); instance.projectRail = null; }
24
- function remove(instance) { releaseBackground(instance); releaseProjectRail(instance); instance.minimize?.removeAttribute('data-mcx-quick-chat-minimize'); instance.unsubscribe(); instance.button?.remove(); instance.tracker.clear(); instance.historyTracker.clear(); }
28
+ function remove(instance) { instance.surface?.removeAttribute('data-mcx-quick-chat-compact'); releaseBackground(instance); releaseProjectRail(instance); instance.minimize?.removeAttribute('data-mcx-quick-chat-minimize'); instance.unsubscribe(); instance.button?.remove(); instance.tracker.clear(); instance.historyTracker.clear(); }
25
29
  function sync() {
26
30
  if (disposed) return;
27
31
  shortcut.sync();
@@ -32,13 +36,14 @@ function sync() {
32
36
  const selected = instance?.historyTracker.selected(snapshot);
33
37
  if (!instance || instance.sessionId !== snapshot.sessionId || instance.subscriptionId !== snapshot.subscriptionId) {
34
38
  if (instance) remove(instance);
35
- instance = {sessionId:snapshot.sessionId, subscriptionId:snapshot.subscriptionId, tracker:createSubmissionTracker(), historyTracker:createHistoryTracker(), button:null};
39
+ instance = {surface:snapshot.surface, sessionId:snapshot.sessionId, subscriptionId:snapshot.subscriptionId, tracker:createSubmissionTracker(), historyTracker:createHistoryTracker(), button:null};
36
40
  const begin = () => { const current = native.read(snapshot.surface); if (current) instance.tracker.begin(current); };
37
41
  instance.unsubscribe = native.subscribe(snapshot, begin);
38
42
  instances.set(snapshot.surface, instance);
39
43
  }
40
44
  if (selected || instance.tracker.accepted(snapshot)) snapshot.expand();
41
45
  instance.historyTracker.open(snapshot);
46
+ snapshot.surface.toggleAttribute('data-mcx-quick-chat-compact', !snapshot.expanded);
42
47
  if (instance.projectRail !== snapshot.projectRail) {
43
48
  releaseProjectRail(instance);
44
49
  instance.projectRail = snapshot.projectRail;
@@ -54,25 +59,34 @@ function sync() {
54
59
  const minimize = snapshot.minimize;
55
60
  if (instance.minimize !== minimize) {
56
61
  instance.minimize?.removeAttribute('data-mcx-quick-chat-minimize'); instance.minimize = minimize;
57
- minimize?.setAttribute('data-mcx-quick-chat-minimize','');
62
+ if (snapshot.standalone) minimize?.setAttribute('data-mcx-quick-chat-minimize','');
58
63
  }
59
- if (!minimize || !snapshot.showHistory) { instance.button?.remove(); instance.button = null; continue; }
60
- if (!instance.button?.isConnected || instance.button.parentElement !== minimize.parentElement) {
64
+ const kind=snapshot.showHistory ? 'history' : snapshot.showClose && shortcut.canClose() ? 'close' : null;
65
+ const anchor=kind==='close' ? snapshot.closeAnchor : minimize;
66
+ if (!minimize || !anchor || !kind) { instance.button?.remove(); instance.button = null; continue; }
67
+ if (!instance.button?.isConnected || instance.button.parentElement !== anchor.parentElement || instance.button.nextElementSibling!==anchor || instance.buttonKind!==kind) {
61
68
  instance.button?.remove();
62
69
  const button = minimize.cloneNode(false);
63
70
  for (const attr of ['id','disabled','aria-disabled','aria-describedby','aria-expanded','title']) button.removeAttribute(attr);
64
71
  button.removeAttribute('data-mcx-quick-chat-minimize');
65
72
  button.type = 'button';
66
- button.setAttribute('data-mcx-quick-chat-history', '');
67
- button.innerHTML = config.resources.history;
73
+ button.setAttribute(`data-mcx-quick-chat-${kind}`, '');
74
+ button.innerHTML = config.resources[kind];
68
75
  const icon = button.querySelector('svg');
69
76
  if (icon) { icon.setAttribute('width','16'); icon.setAttribute('height','16'); icon.setAttribute('aria-hidden','true'); }
70
- button.addEventListener('pointerdown', event => event.preventDefault(), {signal});
71
- button.addEventListener('click', () => { instance.tracker.clear(); native.read(snapshot.surface)?.history(); }, {signal});
72
- minimize.before(button); instance.button = button;
77
+ button.addEventListener('pointerdown', event => {event.preventDefault();event.stopPropagation();}, {signal});
78
+ button.addEventListener('click', event => {
79
+ event.preventDefault(); event.stopPropagation();
80
+ instance.tracker.clear(); instance.historyTracker.clear();
81
+ const current=native.read(snapshot.surface);
82
+ if (!current || current.sessionId!==instance.sessionId) return;
83
+ if (kind==='history') current.history();
84
+ else if (!shortcut.close(snapshot.surface,current.sessionId)) config.codex.report('quick-chat-close',false,'quick-chat-close-action-unavailable');
85
+ }, {signal});
86
+ anchor.before(button); instance.button = button; instance.buttonKind=kind;
73
87
  }
74
88
  if (instance.button.className !== minimize.className) instance.button.className = minimize.className;
75
- const label = text('Chat history', '聊天历史');
89
+ const label = kind==='history' ? text('Chat history', '聊天历史') : text('Close Quick Chat', '关闭快捷聊天');
76
90
  if (instance.button.getAttribute('aria-label') !== label) {
77
91
  instance.button.setAttribute('aria-label',label); instance.button.setAttribute('data-minecodex-tooltip',label);
78
92
  }
@@ -91,14 +105,15 @@ document.addEventListener('submit',begin,{capture:true,signal});
91
105
  const observer = new MutationObserver(schedule);
92
106
  observer.observe(document.documentElement,{subtree:true,childList:true,attributes:true,attributeFilter:['aria-hidden','inert','hidden','lang']});
93
107
  sync();
108
+ void shortcut.ready.then(schedule);
94
109
  config.registerSelfCheck?.(() => {
95
- const snapshots = native.surfaces(), anchors = snapshots.filter(item => item.showHistory);
96
- const history = anchors.some(item => instances.get(item.surface)?.button?.isConnected), entry = sidebar.status();
110
+ const snapshots = native.surfaces(), anchors = snapshots.filter(item => item.showHistory || item.showClose);
111
+ const controls = anchors.every(item => instances.get(item.surface)?.button?.isConnected), entry = sidebar.status();
97
112
  const applicable = anchors.length > 0 || entry.applicable;
98
- const available = (!anchors.length || history) && (!entry.applicable || entry.available);
113
+ const available = (!anchors.length || controls) && (!entry.applicable || entry.available);
99
114
  return {state:applicable ? available ? 'available' : 'unavailable' : 'not-applicable',
100
115
  reason:applicable ? available ? null : 'quick-chat-controls-unavailable' : 'no-classic-quick-chat-floating-controls',
101
- checks:[...(anchors.length ? [{name:'quick-chat-history',available:history}] : []), ...(entry.applicable ? [{name:'quick-chat-sidebar-entry',available:entry.available}] : [])],
116
+ checks:[...(anchors.length ? [{name:'quick-chat-controls',available:controls}] : []), ...(entry.applicable ? [{name:'quick-chat-sidebar-entry',available:entry.available}] : [])],
102
117
  scopes:snapshots.map(item=>({name:'quick-chat',element:item.surface}))};
103
118
  });
104
119
  return () => { disposed = true; shortcut.dispose(); sidebar.dispose(); observer.disconnect(); for (const instance of instances.values()) remove(instance); instances.clear(); style.remove(); };
@@ -1,3 +1,4 @@
1
+ import {quickChatLifecycleActions, quickChatScope} from "./adapters/codex/lifecycle.mjs";
1
2
  import {quickChatHistoryVisible} from "./adapters/codex/history.mjs";
2
3
  import { readFile } from "node:fs/promises";
3
4
  import { createQuickChatAdapter } from "./adapters/codex/renderer.mjs";
@@ -9,7 +10,7 @@ import { createHistoryTracker } from "./domain/history.mjs";
9
10
  export async function createPageScriptSource() {
10
11
  const source = await readFile(new URL("./frontend/entry.mjs", import.meta.url), "utf8");
11
12
  const functions = [quickChatHistoryVisible, loadQuickChatSidebarIcon, createQuickChatSidebarAdapter, installQuickChatSidebar,
12
- quickChatOpenExport, quickChatLifecycleExports, quickChatNativeCommand, installQuickChatShortcut, createQuickChatAdapter,
13
+ quickChatLifecycleActions, quickChatScope, quickChatOpenExport, quickChatLifecycleExports, quickChatNativeCommand, installQuickChatShortcut, createQuickChatAdapter,
13
14
  createSubmissionTracker, createHistoryTracker];
14
15
  return functions.map(fn => `const ${fn.name}=${fn.toString()};`).join('\n') + '\n' + source;
15
16
  }
@@ -44,6 +44,13 @@ function findAvatar(rail) {
44
44
  ?? buttons.findLast(button => button.querySelector('img[alt], img[src], [data-avatar]'))
45
45
  ?? null;
46
46
  }
47
+ function findUsage(rail) {
48
+ // 原生图表的轨道 token 是语义锚点,避免依赖构建生成的 class 或界面语言。
49
+ return [...(rail?.querySelectorAll("button") ?? [])].find(button =>
50
+ !button.hasAttribute?.("data-minecodex-usage-meter") &&
51
+ (button.querySelector?.('[stroke="var(--color-usage-meter-track)"]') ||
52
+ /^(Usage|用量|使用量|使用情况|使用情況)$/.test(button.getAttribute("aria-label") ?? ""))) ?? null;
53
+ }
47
54
  function avatarAnchor(avatar, rail) {
48
55
  // 头像内部是横向行;在最近的原生纵向容器中插入独立一项。
49
56
  let anchor = avatar;
@@ -58,13 +65,16 @@ function avatarAnchor(avatar, rail) {
58
65
  unsubscribe?.(); client=context.client;
59
66
  unsubscribe=client?.getQueryCache().subscribe(event=>{ if (event.query?.queryKey?.[0] === "rate-limit-status") invalidate(); });
60
67
  }
61
- const windows=currentWindows(context), avatar=findAvatar(rail);
62
- const available=Boolean(avatar && context.router && windows);
68
+ const windows=currentWindows(context), avatar=findAvatar(rail), usage=findUsage(rail);
69
+ const available=Boolean((usage || avatar) && context.router && windows);
63
70
  codex.report("usage-meter",available,available ? null : "account-quota-or-anchor-unavailable");
64
- return { rail,avatar,windows,available,anchorAvailable:Boolean(avatar && context.router),
71
+ return { rail,avatar,usage,windows,available,anchorAvailable:Boolean((usage || avatar) && context.router),
65
72
  applicable:context.auth?.isLoading ? null : context.auth ? ["chatgpt","chatgptAuthTokens"].includes(context.auth.authMethod) : null };
66
73
  }
67
74
  function open(rail) { const {router}=nativeContext(rail); return router?.navigate("/settings/usage?tab=overview",{state:router.state?.location?.state}); }
68
75
  signal.addEventListener("abort",()=>unsubscribe?.(),{once:true});
69
- return { read,open,anchor:avatarAnchor };
76
+ return { read,open,anchor:avatarAnchor,
77
+ conceal: usage => usage?.setAttribute("data-minecodex-native-usage", ""),
78
+ reveal: usage => usage?.removeAttribute("data-minecodex-native-usage") };
79
+
70
80
  }
@@ -17,9 +17,10 @@ let hoverTimer = null;
17
17
  const style = document.createElement("style");
18
18
  style.id = STYLE_ID;
19
19
  style.textContent = `
20
- [${MARKER}] { position:relative; display:flex; align-items:center; justify-content:center; flex:none; color:var(--mcx-color-text-tertiary); -webkit-app-region:no-drag; }
20
+ [${MARKER}] { position:relative; display:flex; align-items:center; justify-content:center; flex:none; color:var(--mcx-color-usage-meter-fill,var(--mcx-color-text-tertiary)); -webkit-app-region:no-drag; }
21
+ [data-minecodex-native-usage] { display:none !important; }
21
22
  [${MARKER}] .mcx-usage-ring { display:block; width:24px; height:24px; overflow:visible; }
22
- [${MARKER}] .mcx-usage-number { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:inherit; font-family:var(--mcx-font-sans-default); font-size:calc(var(--mcx-text-xs) * 2 / 3); line-height:1; font-weight:var(--font-weight-medium,500); letter-spacing:0; font-variant-numeric:tabular-nums; pointer-events:none; }
23
+ [${MARKER}] .mcx-usage-number { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:var(--mcx-color-usage-meter-fill,var(--mcx-color-text-tertiary)); font-family:var(--mcx-font-sans-default); font-size:calc(var(--mcx-text-xs) * 2 / 3); line-height:1; font-weight:var(--font-weight-medium,500); letter-spacing:0; font-variant-numeric:tabular-nums; pointer-events:none; }
23
24
  #${TIP_ID} { width:max-content; max-width:calc(100vw - 16px); }
24
25
  #${TIP_ID}[hidden] { display:none; }
25
26
  #${TIP_ID} dl { display:grid; grid-template-columns:auto auto; gap:2px 16px; margin:0; }
@@ -31,13 +32,14 @@ style.textContent = `
31
32
  document.head.append(style);
32
33
 
33
34
  function svgNode(name, attributes) {
35
+ if (name === "path" || name === "circle") attributes = { ...attributes, "vector-effect": "non-scaling-stroke" };
34
36
  const node = document.createElementNS("http://www.w3.org/2000/svg", name);
35
37
  for (const [key, value] of Object.entries(attributes)) node.setAttribute(key, String(value));
36
38
  return node;
37
39
  }
38
40
 
39
41
  function quotaArc(start, end, clockwise) {
40
- const radius = 43 / 3;
42
+ const radius = 44 / 3;
41
43
  const point = angle => {
42
44
  const radians = (angle - 90) * Math.PI / 180;
43
45
  return [16 + radius * Math.cos(radians), 16 + radius * Math.sin(radians)];
@@ -52,8 +54,8 @@ function draw(windows) {
52
54
  if (windows.fiveHour != null) {
53
55
  // 左下、右下各留 24° 圆头中心间距;两段都从左下切口扣减。
54
56
  for (const [key, start, end, clockwise] of [["fiveHour", 237, 483, true], ["weekly", 213, 147, false]]) {
55
- const group = svgNode("g", { "data-window": key, fill: "none", stroke: "currentColor", "stroke-width": 10 / 3, "stroke-linecap": "round" });
56
- group.append(svgNode("path", { d: quotaArc(start, end, clockwise), opacity: 0.18 }));
57
+ const group = svgNode("g", { "data-window": key, fill: "none", stroke: "currentColor", "stroke-width": 2, "stroke-linecap": "round" });
58
+ group.append(svgNode("path", { d: quotaArc(start, end, clockwise), stroke: "var(--mcx-color-usage-meter-track,color-mix(in srgb,currentColor 18%,transparent))" }));
57
59
  if (windows[key] > 0) {
58
60
  const remainingStart = start + (end - start) * (1 - windows[key] / 100);
59
61
  group.append(svgNode("path", { d: quotaArc(remainingStart, end, clockwise) }));
@@ -61,9 +63,9 @@ function draw(windows) {
61
63
  svg.append(group);
62
64
  }
63
65
  } else {
64
- const key = "weekly", radius = 43 / 3;
65
- const group = svgNode("g", { "data-window": key, fill: "none", stroke: "currentColor", "stroke-width": 10 / 3 });
66
- group.append(svgNode("circle", { cx: 16, cy: 16, r: radius, opacity: 0.18 }));
66
+ const key = "weekly", radius = 44 / 3;
67
+ const group = svgNode("g", { "data-window": key, fill: "none", stroke: "currentColor", "stroke-width": 2 });
68
+ group.append(svgNode("circle", { cx: 16, cy: 16, r: radius, stroke: "var(--mcx-color-usage-meter-track,color-mix(in srgb,currentColor 18%,transparent))" }));
67
69
  if (windows[key] > 0) group.append(svgNode("circle", {
68
70
  cx: 16, cy: 16, r: radius, pathLength: 100,
69
71
  "stroke-dasharray": `${windows[key]} 100`, "stroke-linecap": "round", transform: "rotate(-90 16 16)",
@@ -127,6 +129,7 @@ function hideTooltip() {
127
129
  }
128
130
  function removeControl() {
129
131
  hideTooltip();
132
+ nativeUsage.reveal(record?.usage);
130
133
  record?.button.remove();
131
134
  record?.tooltip.remove();
132
135
  record = null;
@@ -136,21 +139,23 @@ function removeControl() {
136
139
 
137
140
 
138
141
 
139
- function mount(avatar, rail) {
140
- // 克隆 Avatar 热区的外观,不携带原生账号菜单行为。
141
- const button = avatar.cloneNode(false);
142
+ function mount(avatar, rail, usage) {
143
+ // 复用原生 Usage 热区;旧版宿主才使用 Avatar 模板。
144
+ const button = (usage || avatar).cloneNode(false);
142
145
  for (const attribute of [...button.attributes]) {
143
- if (attribute.name.startsWith("aria-") || ["id", "title", "data-state", "data-sidebar-destination", "data-testid"].includes(attribute.name)) button.removeAttribute(attribute.name);
146
+ if (attribute.name.startsWith("aria-") || ["id", "title", "data-state", "data-sidebar-destination", "data-testid", "data-minecodex-native-usage"].includes(attribute.name)) button.removeAttribute(attribute.name);
144
147
  }
145
148
  button.setAttribute(MARKER, "");
149
+ config.ui.theme.bindScope(button, ["--color-usage-meter-fill", "--color-usage-meter-track"]);
146
150
  const tooltip = document.createElement("div");
147
151
  tooltip.id = TIP_ID;
148
152
  config.ui.tooltipSurface(tooltip);
149
153
  tooltip.setAttribute("role", "tooltip");
150
154
  tooltip.hidden = true;
151
- nativeUsage.anchor(avatar, rail).before(button);
155
+ (usage || nativeUsage.anchor(avatar, rail)).before(button);
156
+ nativeUsage.conceal(usage);
152
157
  document.body.append(tooltip);
153
- record = { button, tooltip, rail, avatar, signature: null };
158
+ record = { button, tooltip, rail, avatar, usage, signature: null };
154
159
  button.addEventListener("pointerenter", () => { hoverTimer = setTimeout(showTooltip, 350); });
155
160
  button.addEventListener("pointerleave", hideTooltip);
156
161
  button.addEventListener("focus", showTooltip);
@@ -165,10 +170,10 @@ function mount(avatar, rail) {
165
170
  function sync() {
166
171
  queued = false;
167
172
  if (signal.aborted) return;
168
- const { rail,avatar,windows,available } = nativeUsage.read();
173
+ const { rail,avatar,usage,windows,available } = nativeUsage.read();
169
174
  if (!available) { removeControl(); return; }
170
- if (record?.avatar !== avatar || !record?.button.isConnected) { removeControl(); mount(avatar, rail); }
171
- else if (record.button.nextElementSibling !== nativeUsage.anchor(avatar, rail)) nativeUsage.anchor(avatar, rail).before(record.button);
175
+ if (record?.avatar !== avatar || record?.usage !== usage || !record?.button.isConnected) { removeControl(); mount(avatar, rail, usage); }
176
+ else if (record.button.nextElementSibling !== (usage || nativeUsage.anchor(avatar, rail))) (usage || nativeUsage.anchor(avatar, rail)).before(record.button);
172
177
  const signature = JSON.stringify(windows);
173
178
  if (record.signature !== signature) { record.signature = signature; draw(windows); }
174
179
  }
@@ -196,8 +201,8 @@ config.registerSelfCheck?.(() => {
196
201
  const snapshot=nativeUsage.read();
197
202
  const available=Boolean(snapshot.available && record?.button.isConnected);
198
203
  return {state:available ? "available" : snapshot.applicable === false ? "not-applicable" : !snapshot.anchorAvailable && snapshot.rail ? "unavailable" : "unknown",reason:available ? null : "quota-data-or-anchor-unavailable",
199
- checks:[{name:"account-anchor",available:Boolean(snapshot.avatar)},{name:"account-quota",available:Boolean(snapshot.windows)},{name:"usage-meter-mount",available:Boolean(record?.button.isConnected)}],
200
- scopes:[{name:"account",element:snapshot.avatar}]};
204
+ checks:[{name:"usage-anchor",available:Boolean(snapshot.usage || snapshot.avatar)},{name:"account-quota",available:Boolean(snapshot.windows)},{name:"usage-meter-mount",available:Boolean(record?.button.isConnected)}],
205
+ scopes:[{name:"account",element:snapshot.usage || snapshot.avatar}]};
201
206
  });
202
207
 
203
208
  return cleanup;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "2.0.44",
3
+ "version": "2.0.46",
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",
@@ -73,6 +71,10 @@
73
71
  "features/markdown-reading/codex-feature.json",
74
72
  "features/markdown-reading/vendor/lucide-LICENSE",
75
73
  "features/markdown-reading/assets",
74
+ "features/files-auto-refresh/src",
75
+ "features/files-auto-refresh/codex-feature.json",
76
+ "features/files-auto-refresh/assets",
77
+ "features/files-auto-refresh/vendor/lucide-LICENSE",
76
78
  "features/file-navigation/src",
77
79
  "features/file-navigation/codex-feature.json",
78
80
  "features/file-navigation/assets",
@@ -94,7 +96,7 @@
94
96
  },
95
97
  "scripts": {
96
98
  "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",
99
+ "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 features/files-auto-refresh/test/*.test.mjs packages/plugin/test/*.test.mjs features/mermaid-diagrams/test/*.test.mjs",
98
100
  "test:cli": "node --test packages/cli/test/*.test.mjs",
99
101
  "test:runtime": "node --test packages/runtime-host/test/*.test.mjs",
100
102
  "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", "files-auto-refresh", "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,
@@ -17,6 +16,7 @@ export const DEFAULT_CONFIG = Object.freeze({
17
16
  "markdown-reading": true,
18
17
  "mermaid-diagrams": false,
19
18
  "file-navigation": true,
19
+ "files-auto-refresh": true,
20
20
  "quick-chat": true,
21
21
  "composer-expand": true,
22
22
  }),
@@ -44,6 +44,8 @@ export function normalizeConfig(value = {}) {
44
44
  };
45
45
  // 暂停的 Task 通知不继承旧配置,也不再提供开启入口。
46
46
  delete features["unread-inbox"];
47
+ // 已移除的额度推广隐藏功能不继承旧启用项。
48
+ delete features["hide-upsell-banner"];
47
49
  // 清理已退役的独立 Quick Chat 修复开关。
48
50
  delete features["quick-chat-repair"];
49
51
  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", "files-auto-refresh", "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"],
@@ -102,6 +102,8 @@ export const CODEX_THEME_TOKENS = Object.freeze([
102
102
  "--color-text-primary",
103
103
  "--color-text-secondary",
104
104
  "--color-text-tertiary",
105
+ "--color-usage-meter-fill",
106
+ "--color-usage-meter-track",
105
107
  "--color-border-overlay",
106
108
  "--color-ring",
107
109
  "--color-dialog-overlay",
@@ -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
- }