minecodex 2.0.27 → 2.0.29

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 (31) hide show
  1. package/README.md +2 -0
  2. package/features/images/src/adapters/codex/image-sources.mjs +3 -1
  3. package/features/images/src/adapters/codex/native-image-modules.mjs +24 -19
  4. package/features/model-slider/src/frontend/install.mjs +12 -6
  5. package/features/notes/src/frontend/entry.mjs +3 -0
  6. package/features/quick-chat/assets/history.svg +17 -0
  7. package/features/quick-chat/assets/pin.svg +1 -0
  8. package/features/quick-chat/codex-feature.json +10 -0
  9. package/features/quick-chat/src/adapters/codex/pin.mjs +34 -0
  10. package/features/quick-chat/src/adapters/codex/renderer.mjs +63 -0
  11. package/features/quick-chat/src/adapters/codex/shortcut.mjs +83 -0
  12. package/features/quick-chat/src/domain/history.mjs +12 -0
  13. package/features/quick-chat/src/domain/submission.mjs +15 -0
  14. package/features/quick-chat/src/frontend/entry.mjs +104 -0
  15. package/features/quick-chat/src/frontend/pin.mjs +54 -0
  16. package/features/quick-chat/src/injection-source.mjs +11 -0
  17. package/features/quick-chat/vendor/lucide-LICENSE +15 -0
  18. package/features/quick-chat-repair/assets/history.svg +17 -0
  19. package/features/quick-chat-repair/codex-feature.json +10 -0
  20. package/features/quick-chat-repair/src/adapters/codex/presence.mjs +74 -0
  21. package/features/quick-chat-repair/src/frontend/entry.mjs +12 -0
  22. package/features/quick-chat-repair/src/injection-source.mjs +5 -0
  23. package/features/quick-chat-repair/vendor/lucide-LICENSE +15 -0
  24. package/features/session-status/src/adapters/codex/session-log.mjs +1 -1
  25. package/features/session-status/src/domain/session-stats.mjs +29 -4
  26. package/features/session-status/src/session-status.mjs +3 -2
  27. package/package.json +11 -3
  28. package/packages/cli/src/config.mjs +3 -1
  29. package/packages/cli/src/control-server.mjs +1 -1
  30. package/packages/ui/src/foundation/surfaces.mjs +3 -2
  31. package/packages/ui/src/native/tokens.mjs +1 -0
package/README.md CHANGED
@@ -18,6 +18,8 @@ MineCodex 给 Codex Desktop 增加一组轻量、本地优先的扩展功能。
18
18
  | **文件级联浏览** · Cascading File Browser | 点击文件顶部路径,分列浏览文件夹并快速切换文件。 |
19
19
  | **图片集合** · Image Collection | 集中浏览生成的图片,查看提示词并复用创作思路。 |
20
20
  | **站点收藏** · Saved Sites | 发现项目正在运行的网页,收藏常用网址,随时打开。 |
21
+ | **Quick Chat 退出修复** | 修复关闭快捷聊天后透明层残留、主页面无法点击的问题;可独立开关。 |
22
+ | **快捷聊天增强** · Quick Chat | 补回 Codex 模式的 ⌘⌥N,恢复最小化输入框;发送后展开聊天,可固定内容窗口并查看原生聊天历史。 |
21
23
  | **侧栏聚焦** · Sidebar Focus | 一键收起侧栏中的其他分组,再次点击恢复原来的展开状态。 |
22
24
  | **隐藏额度推广** · Hide Usage Promotions | 隐藏输入框上方的套餐升级推广,让对话更清爽。 |
23
25
 
@@ -15,7 +15,9 @@ export function createImageSourcesAdapter(codex) {
15
15
  !node.closest("[data-minecodex-image-navigation]")
16
16
  && codex.props(node, isImageNavigation));
17
17
  if (!tabs) return null;
18
- const navigation = tabs.parentElement?.parentElement;
18
+ // 26.1007 把来源工具栏包进独立 inset;图库仍是同级的 flex 内容区。
19
+ let navigation = tabs.parentElement?.parentElement;
20
+ while (navigation && !navigation.nextElementSibling) navigation = navigation.parentElement;
19
21
  const content = navigation?.nextElementSibling;
20
22
  const shell = navigation?.parentElement;
21
23
  if (!content || !shell) return null;
@@ -18,7 +18,7 @@ export function discoverFilePreviewComposerBindings(source) {
18
18
  if(found.length!==1)throw new Error("Native file preview Composer contract changed");
19
19
  return found[0];
20
20
  };
21
- const scopeBody=requireBody(/draftId:[\w$]+,imageDataUrl:/);
21
+ const scopeBody=requireBody(/draftId:[\w$]+,(?:imageSrc|imageDataUrl):/);
22
22
  const homeBody=requireBody(/homeComposerController:[\w$]+,homeComposerMode:/);
23
23
  const composerBody=requireBody(/lockedLayout:`single-line`/);
24
24
  const localBody=requireBody(/prepareLocalConversationAttachments:/);
@@ -36,7 +36,7 @@ export function discoverFilePreviewComposerBindings(source) {
36
36
  const roles={
37
37
  useComposerController:alias(scopeBody,controller,true),
38
38
  useHomeComposer:homeBody.match(/let [\w$]+=([\w$]+)\([\w$]+\),[\w$]+=[\w$]+\?\.composerProps/)?.[1],
39
- Composer:composerBody.match(/jsx\)\(([\w$]+),\{\.\.\.[\w$]+,className:`mx-auto w-full max-w-2xl px-toolbar`/)?.[1],
39
+ Composer:composerBody.match(/jsx\)\(([\w$]+),\{\.\.\.[\w$]+,[^;]+?lockedLayout:`single-line`/)?.[1],
40
40
  ComposerSurface:composerBody.match(/jsx\)\(([\w$]+),\{placement:/)?.[1],
41
41
  ScopeProvider:scopeProvider,
42
42
  composerScope:alias(scopeBody,scopes[0][2]),
@@ -115,33 +115,34 @@ export function createNativeImageModules({read = async url => (await fetch(url))
115
115
  async function gallery() {
116
116
  if (!connecting) connecting = (async () => {
117
117
  const {React,ReactDOM,module,component,binding,initialUrl,initialSource,initial}=await bootstrap();
118
- const [masonry,layout,sourceImage,menu,selection]=await Promise.all([
118
+ const [masonry,layout,sourceImage,menu,toolbar]=await Promise.all([
119
119
  module("library-masonry"),module("library-item-layout"),module("generated-image-source"),
120
120
  module("generated-image-context-menu"),module("library-selection-toolbar"),
121
121
  ]);
122
- const [moveDialog,dialogs]=await Promise.all([module("library-move-dialog",selection.source),module("library-item-dialogs",selection.source)]);
122
+ // 26.1007 将选择状态与图库动作移到 selectable-library-item;工具栏保留按钮与材质。
123
+ const selection=await module("selectable-library-item",toolbar.source);
124
+ const [moveDialog,dialogs]=await Promise.all([module("library-move-dialog",selection.source),module("library-item-dialogs",toolbar.source)]);
123
125
  const routerLocal = selection.source.match(/get\(([\w$]+)\)\.prepareNavigation\(/)?.[1];
124
126
  const routerAtom = await binding(selection.source, selection.url, routerLocal);
125
127
  const switchLocal = discoverImageActionBinding(selection.source, "switchMode");
126
128
  const switchMode = await binding(selection.source, selection.url, switchLocal);
127
- const draftLocal = selection.source.match(/let u=([\w$]+)\(e,\[\]\);e.set/)?.[1];
129
+ const draftLocal = selection.source.match(/let [\w$]+=([\w$]+)\([\w$]+,\[\]\);[\w$]+\.set/)?.[1];
128
130
  const createChatDraft = await binding(selection.source, selection.url, draftLocal);
129
131
  const dropdownLocal = selection.source.match(/\(0,[\w$]+\.jsxs?\)\(([\w$]+),\{contentWidth:`menu`/)?.[1];
130
132
  const itemsLocal = selection.source.match(/\(0,[\w$]+\.jsxs?\)\(([\w$]+)\.Item,\{/)?.[1];
131
- const buttonLocal = selection.source.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{ref:[^,]+,color:`mediaOverlay`/)?.[1];
132
- const iconLocal = selection.source.match(/color:`mediaOverlay`[^;]+?children:\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/)?.[1];
133
- const [Dropdown, Menu, Button, MoreIcon] = await Promise.all([dropdownLocal, itemsLocal, buttonLocal, iconLocal].map(local => binding(selection.source, selection.url, local)));
133
+ const buttonLocal = toolbar.source.match(/jsx\)\(([\w$]+),\{color:`mediaSolid`/)?.[1];
134
+ const iconLocal = toolbar.source.match(/id:`libraryNext.selection.more`[^;]+?children:\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/)?.[1];
135
+ const [Dropdown, Menu] = await Promise.all([dropdownLocal, itemsLocal].map(local => binding(selection.source, selection.url, local)));
136
+ const [Button, MoreIcon] = await Promise.all([buttonLocal, iconLocal].map(local => binding(toolbar.source, toolbar.url, local)));
134
137
  const checkboxFunction = component(selection.exports,source=>/onCheckedChange:/.test(source)&&/viewMode:/.test(source),"selection checkbox");
135
- const checkboxLocal = String(checkboxFunction).match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{className:`cursor-interaction`,variant:/)?.[1];
138
+ const checkboxLocal = String(checkboxFunction).match(/jsx\)\(([\w$]+),\{className:`cursor-interaction`,/)?.[1];
136
139
  const Checkbox = await binding(selection.source,selection.url,checkboxLocal);
137
140
  const rangeSelection = component(selection.exports,source=>/^function [\w$]+\(\{orderedIds:/.test(source)&&/extendRange:/.test(source),"range selection");
138
- const selectionLocal = selection.source.match(/\.set\(([\w$]+),new Set\)/)?.[1];
139
- const selectionExport = selection.source.match(/export\{([^}]+)\}/)?.[1].split(",").map(value=>value.trim().split(/\s+as\s+/)).find(parts=>parts[0]===selectionLocal)?.at(-1);
140
- const nativeSelectedIds = selection.exports[selectionExport];
141
- if (!nativeSelectedIds) throw new Error("Native selection state contract changed");
142
- const clearPrefix = selection.source.slice(selection.source.indexOf("id:`libraryNext.selection.clear`"));
141
+ const selectionLocal = toolbar.source.match(/\.set\(([\w$]+),new Set[,)]/)?.[1];
142
+ const nativeSelectedIds = await binding(toolbar.source,toolbar.url,selectionLocal);
143
+ const clearPrefix = toolbar.source.slice(toolbar.source.indexOf("id:`libraryNext.selection.clear`"));
143
144
  const clearLocal = clearPrefix.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/)?.[1];
144
- const ClearIcon = await binding(selection.source,selection.url,clearLocal);
145
+ const ClearIcon = await binding(toolbar.source,toolbar.url,clearLocal);
145
146
  const dialogLocal = dialogs.source.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{open:[^,]+,showDialogClose:!1/)?.[1];
146
147
  const contentLocal = dialogs.source.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{as:`form`,onSubmit:/)?.[1];
147
148
  const [Dialog,DialogContent] = await Promise.all([dialogLocal,contentLocal].map(local=>binding(dialogs.source,dialogs.url,local)));
@@ -154,8 +155,9 @@ export function createNativeImageModules({read = async url => (await fetch(url))
154
155
  const attachLocal = discoverImageActionBinding(imageActionSource, "attachChatImages");
155
156
  const downloadLocal = imageActionSource.match(/await ([\w$]+)\(\{downloadSrc:/)?.[1];
156
157
  const [imageFile,attachChatImages,downloadImage] = await Promise.all([imageFileLocal,attachLocal,downloadLocal].map(local=>binding(menuCoreSource,menuCoreUrl,local)));
157
- const toolbarClass = selection.source.match(/className:`(pointer-events-auto flex h-toolbar[^`]+)`/)?.[1];
158
- const toolbarPositionClass = selection.source.match(/className:`(pointer-events-none fixed inset-x-4 bottom-6[^`]+)`/)?.[1];
158
+ const toolbarClass = toolbar.source.match(/=\`(_Toolbar_[^`]+)\`/)?.[1];
159
+ const toolbarContainer = toolbar.source.match(/=\`(_Container_[^`]+)\`/)?.[1];
160
+ const toolbarPositionClass = toolbarContainer && `${toolbarContainer} fixed`;
159
161
  if (!toolbarClass || !toolbarPositionClass) throw new Error("Native selection toolbar layout changed");
160
162
  const libraryIcon = async label => {
161
163
  const marker = selection.source.indexOf(`defaultMessage:\`${label}\``);
@@ -180,8 +182,11 @@ export function createNativeImageModules({read = async url => (await fetch(url))
180
182
  const favoriteIconOwner = favoritePrefix.slice(0,favoriteIconLocal).match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{$/)?.[1];
181
183
  const [FavoriteIcon, savedSmall, savedMedium, emptySmall, emptyMedium] = await Promise.all([favoriteIconOwner,...favoriteAsset.slice(1)].map(local => binding(selection.source,selection.url,local)));
182
184
  const favoriteIcon = saved => React.createElement(FavoriteIcon,{icon:{assets:saved?{16:savedSmall,20:savedMedium}:{16:emptySmall,20:emptyMedium}}});
183
- const [chatIcon, downloadIcon, folderIcon, deleteIcon] = await Promise.all([
184
- libraryIcon("Chat about this"), libraryIcon("Download"), libraryIcon("Add to folder"), libraryIcon("{kind, select, directory {Delete folder} other {Delete}}"),
185
+ const chatLocal = toolbar.source.match(/children:[\w$]+\?[^;]+?:\(0,[\w$]+\.jsxs\)\([\w$]+\.Fragment,\{children:\[\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\),\(0,[\w$]+\.jsx\)\([\w$]+,\{id:`libraryNext.selection.chat`/)?.[1];
186
+ const ChatIcon = await binding(toolbar.source,toolbar.url,chatLocal);
187
+ const chatIcon = React.createElement(ChatIcon,{});
188
+ const [downloadIcon, folderIcon, deleteIcon] = await Promise.all([
189
+ libraryIcon("Download"), libraryIcon("Move to folder"), libraryIcon("{kind, select, directory {Delete folder} other {Delete}}"),
185
190
  ]);
186
191
  if (!React.createElement || !ReactDOM.createRoot || !Menu.Item) throw new Error("Native Images rendering contract changed");
187
192
  return {
@@ -35,6 +35,11 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
35
35
  position: sticky; top: 0; z-index: 1; flex-shrink: 0; padding: 2px 8px 8px;
36
36
  background: var(--mcx-color-surface-elevated-secondary);
37
37
  }
38
+ /* sticky 保留菜单的 6px 内边距,补齐顶部遮挡而不改变布局高度。 */
39
+ [data-codex-model-slider-search-header]::before {
40
+ content: ""; position: absolute; top: -6px; left: -6px; right: -6px; height: 6px;
41
+ background: inherit; pointer-events: none;
42
+ }
38
43
  [data-codex-model-slider-search] {
39
44
  width: 100%; height: 28px; border: 0; outline: none; padding: 0;
40
45
  background: transparent; color: var(--mcx-color-text-primary);
@@ -722,7 +727,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
722
727
  .reduce((sum, value) => sum + (Number.parseFloat(value) || 0), 0);
723
728
  const last = items[Math.max(1, maxVisibleItems) - 1];
724
729
  const resultsHeight = last ? last.offsetTop + last.offsetHeight - content.offsetTop : content.offsetHeight;
725
- const available = modelSelectorControllerMenu.parentMenu.getBoundingClientRect().bottom - 8;
730
+ const available = (modelSelectorControllerMenu.parentMenu.getBoundingClientRect().bottom - 8) / (menu.currentCSSZoom || 1);
726
731
  menu.style.height = `${Math.ceil(Math.min(chrome + header.offsetHeight + resultsHeight, available))}px`;
727
732
  menu.style.overflowY = "auto";
728
733
  return;
@@ -1003,7 +1008,8 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
1003
1008
  }
1004
1009
 
1005
1010
  function animateControllerModelMenu(record, opening, onFinish) {
1006
- const height = record.menu.getBoundingClientRect().height;
1011
+ // height 写入 CSS 动画,必须使用布局尺寸,不能重复计入原生 zoom 和开场 scale。
1012
+ const height = Number.parseFloat(getComputedStyle(record.menu).height);
1007
1013
  clearTimeout(record.motionTimer);
1008
1014
  record.animations?.forEach((animation) => animation.cancel());
1009
1015
  record.animations = [];
@@ -1134,14 +1140,14 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
1134
1140
  resetKeyboard();
1135
1141
  const record = modelSelectorControllerMenu;
1136
1142
  if (!record || record.menu !== menu || record.closing) return;
1137
- const previousHeight = menu.getBoundingClientRect().height;
1143
+ const previousHeight = Number.parseFloat(getComputedStyle(menu).height);
1138
1144
  clearTimeout(record.motionTimer);
1139
1145
  record.animations?.forEach((animation) => animation.cancel());
1140
1146
  record.animations = [];
1141
1147
  menu.scrollTop = 0;
1142
1148
  renderEnhancedModelMenu(menu, selector);
1143
1149
  sizeModelMenuViewport(menu, selector.maxVisibleItems);
1144
- record.listHeight = menu.getBoundingClientRect().height;
1150
+ record.listHeight = Number.parseFloat(getComputedStyle(menu).height);
1145
1151
  if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
1146
1152
  const motion = menu.animate([{ height: `${previousHeight}px` }, { height: `${record.listHeight}px` }],
1147
1153
  { duration: 320, easing: "cubic-bezier(.19,1,.22,1)" });
@@ -1237,7 +1243,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
1237
1243
  placeholderText,
1238
1244
  position: parentMenu.style.position,
1239
1245
  overflow: parentMenu.style.overflow,
1240
- sliderHeight: parentMenu.getBoundingClientRect().height,
1246
+ sliderHeight: Number.parseFloat(getComputedStyle(parentMenu).height),
1241
1247
  };
1242
1248
  parentMenu.style.position = "relative";
1243
1249
  parentMenu.style.overflow = "visible";
@@ -1246,7 +1252,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
1246
1252
  modelButton.setAttribute("aria-expanded", "true");
1247
1253
  search.focus({ preventScroll: true });
1248
1254
  const record = modelSelectorControllerMenu;
1249
- record.listHeight = menu.getBoundingClientRect().height;
1255
+ record.listHeight = Number.parseFloat(getComputedStyle(menu).height);
1250
1256
  animateControllerModelMenu(record, true, () => {
1251
1257
  if (modelSelectorControllerMenu !== record || record.closing) return;
1252
1258
  record.animations.forEach((animation) => animation.cancel());
@@ -377,6 +377,9 @@ function sync() {
377
377
  close();
378
378
  if (!button) {
379
379
  button = add.cloneNode(false);
380
+ // Add 的附件加载状态不属于只填入文字的提示词入口。
381
+ button.removeAttribute("disabled");
382
+ button.removeAttribute("aria-disabled");
380
383
  button.removeAttribute("title");
381
384
  button.removeAttribute("aria-describedby");
382
385
  button.removeAttribute("id");
@@ -0,0 +1,17 @@
1
+ <!-- @license lucide-static v0.468.0 - ISC -->
2
+ <svg
3
+ class="lucide lucide-history"
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ width="24"
6
+ height="24"
7
+ viewBox="0 0 24 24"
8
+ fill="none"
9
+ stroke="currentColor"
10
+ stroke-width="2"
11
+ stroke-linecap="round"
12
+ stroke-linejoin="round"
13
+ >
14
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
15
+ <path d="M3 3v5h5" />
16
+ <path d="M12 7v5l4 2" />
17
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></svg>
@@ -0,0 +1,10 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "quick-chat",
4
+ "version": "0.1.0",
5
+ "label": { "en": "Quick Chat", "zh-CN": "快捷聊天增强" },
6
+ "description": { "en": "Restore Quick Chat shortcuts, expand after sending, pin classic chats, and open native chat history.", "zh-CN": "恢复 Quick Chat 快捷键,发送后展开聊天,可固定经典聊天窗口并查看原生历史。" },
7
+ "entry": { "kind": "page-script", "script": "src/frontend/entry.mjs", "sourceFactory": "src/injection-source.mjs", "resources": { "history": "assets/history.svg", "pin": "assets/pin.svg" } },
8
+ "icon": { "resource": "assets/history.svg" },
9
+ "placement": { "order": 75 }
10
+ }
@@ -0,0 +1,34 @@
1
+ export function createQuickChatPinAdapter(codex) {
2
+ const layerSelector = '.fixed.inset-0.zoom-adjusted-viewport.pointer-events-auto';
3
+ function historyVisible(node) {
4
+ if (Array.isArray(node)) return node.some(historyVisible);
5
+ const props = node?.props;
6
+ if (!props || props.hidden || props.inert || props['aria-hidden']) return false;
7
+ if (Array.isArray(props.conversations) && typeof props.onConversationSelect === 'function' && typeof props.onNewChat === 'function') return true;
8
+ return historyVisible(props.children);
9
+ }
10
+ function surfaces() {
11
+ const inCodex = [...document.querySelectorAll('button[aria-haspopup="menu"]')].some(button => button.checkVisibility({visibilityProperty:true})
12
+ && codex.props(button, p => typeof p.onModeSelect === 'function' && typeof p.mode === 'string', 80)?.mode === 'codex');
13
+ if (!inCodex) return [];
14
+ return [...document.querySelectorAll('[data-floating-chat-surface]')].flatMap(surface => {
15
+ const layer = surface.closest(layerSelector);
16
+ if (!layer || layer.closest('[hidden], [inert], [aria-hidden="true"]')) return [];
17
+ if (!codex.props(surface, p => p.name === 'QuickChatWindow', 120)) return [];
18
+ const presence = codex.props(surface, p => typeof p.isPresent === 'boolean' && typeof p.onExitComplete === 'function', 120);
19
+ if (presence?.isPresent === false) return [];
20
+ const editable = surface.querySelector('[data-chatgpt-composer] [contenteditable="true"]');
21
+ const composer = editable && codex.props(editable, p => typeof p.isWorkConversation === 'boolean' && p.conversationId);
22
+ const session = codex.props(surface, p => p.session?.conversationId)?.session;
23
+ const floating = codex.props(surface, p => typeof p.onMinimizedChange === 'function' && typeof p.renderMenu === 'function');
24
+ if (!composer || composer.isWorkConversation || composer.annotationSubmitTarget || composer.isWidgetOverlay || !session?.hasConversation || !floating) return [];
25
+ const anchor = surface.querySelector('header button[data-quick-chat-drag-handle]');
26
+ const expanded = floating.presentation === 'expanded' || floating.forceExpanded;
27
+ const history = historyVisible(floating.renderTranscript?.());
28
+ return [{surface, layer, sessionId:session.conversationId, anchor, minimized:Boolean(floating.isMinimized), expanded:Boolean(expanded), history,
29
+ expand:() => floating.onPresentationChange('expanded'),
30
+ showPin:Boolean(anchor && expanded && !floating.isMinimized && !history)}];
31
+ });
32
+ }
33
+ return {surfaces};
34
+ }
@@ -0,0 +1,63 @@
1
+ export function createQuickChatAdapter(codex) {
2
+ const receipts = new WeakMap();
3
+ let nextReceipt = 1;
4
+ const surfaceSelector = '[data-floating-chat-surface]';
5
+ function historyList(node) {
6
+ if (Array.isArray(node)) return node.some(historyList);
7
+ const props = node?.props;
8
+ if (!props || props.hidden || props.inert || props['aria-hidden']) return false;
9
+ if (Array.isArray(props.conversations) && typeof props.onConversationSelect === 'function' && typeof props.onNewChat === 'function') return true;
10
+ return historyList(props.children);
11
+ }
12
+ function read(surface) {
13
+ if (!surface?.isConnected || surface.closest('[hidden], [inert], [aria-hidden="true"]')) return null;
14
+ const editable = surface.querySelector('[data-chatgpt-composer] [contenteditable="true"]');
15
+ if (!editable) return null;
16
+ const composer = codex.props(editable, props => typeof props.isWorkConversation === "boolean" && props.conversationId);
17
+ const session = codex.props(surface, props => props.session?.conversationId)?.session;
18
+ const floating = codex.props(surface, props => typeof props.onPresentationChange === "function" && typeof props.renderMenu === "function");
19
+ // Work 的页面横条没有经典 Chat 的历史动作,不能接入此功能。
20
+ if (!composer || composer.isWorkConversation || composer.annotationSubmitTarget || composer.isWidgetOverlay || !session || !floating) return null;
21
+ const history = floating.renderMenu({})?.props?.items?.find(item => item.id === "history" && typeof item.onSelect === "function");
22
+ if (!history) return null;
23
+ const state = codex.props(editable, props => typeof props.submitDisabled === "boolean" && props.conversationId);
24
+ const controller = codex.props(editable, props => props.composerController?.eventEmitter)?.composerController;
25
+ const minimize = [...surface.querySelectorAll('button')].find(button => {
26
+ const props = codex.props(button, props => props.standalone === true && typeof props.onMinimize === "function" && typeof props.isCompact === "boolean", 5);
27
+ return Boolean(props && button.checkVisibility({visibilityProperty:true}) && !button.hasAttribute('data-mcx-quick-chat-history'));
28
+ });
29
+ let receipt = null;
30
+ for (let fiber = codex.currentFiber(surface), depth = 0; fiber && depth < 80; depth++, fiber = fiber.return) {
31
+ if (fiber.memoizedProps?.session?.conversationId !== session.conversationId) continue;
32
+ for (let hook = fiber.memoizedState, count = 0; hook && count < 140; hook = hook.next, count++) {
33
+ const value = hook.memoizedState;
34
+ if (value?.conversationId === session.conversationId && "owner" in value && typeof value.presentation === "string") receipt = value;
35
+ }
36
+ if (receipt) break;
37
+ }
38
+ // 发送确认回调写入新记录;拒绝发送不会写入。记录身份只映射为窗口内序号。
39
+ if (receipt && !receipts.has(receipt)) receipts.set(receipt, nextReceipt++);
40
+ const frame = surface.closest('[role="dialog"]');
41
+ const background = frame && [...frame.children].find(node => codex.props(node, props => props.asChild === true && typeof props.unstyled === "boolean", 8));
42
+ const project = [...surface.querySelectorAll('[data-composer-rail-item] button')].find(button =>
43
+ codex.props(button, props => props.variant === "composer" && typeof props.onProjectChange === "function"));
44
+ const projectRail = project?.closest('[data-composer-rail-item]') ?? null;
45
+ const expanded = Boolean(floating.presentation === "expanded" || floating.forceExpanded);
46
+ return {surface, editable, minimize, projectRail, background:!session.hasConversation && floating.forceExpanded ? background : null, sessionId:session.conversationId, hasConversation:session.hasConversation,
47
+ canSubmit:Boolean(state && !state.submitDisabled && !state.submitLoading), receiptId:receipt ? receipts.get(receipt) : 0, receiptPresentation:receipt?.presentation ?? null, emitter:controller?.eventEmitter,
48
+ expanded, showHistory:Boolean(minimize && !expanded), historyVisible:historyList(floating.renderTranscript?.()),
49
+ expand:() => floating.onPresentationChange("expanded"),
50
+ // 历史动作通过 forceExpanded 临时展开;不要覆盖最小化后的恢复形态。
51
+ history:() => history.onSelect()};
52
+ }
53
+ function surfaces() { return [...document.querySelectorAll(surfaceSelector)].map(read).filter(Boolean); }
54
+ function subscribe(snapshot, listener) {
55
+ const emitter = snapshot.emitter;
56
+ if (!emitter?.addListener || !emitter?.removeListener) return () => {};
57
+ // 不改变原生 emit 的 handled 结果,也不接管发送。
58
+ const submit = () => { listener(); return false; };
59
+ emitter.addListener("submit", submit);
60
+ return () => emitter.removeListener("submit", submit);
61
+ }
62
+ return {surfaces, read, subscribe};
63
+ }
@@ -0,0 +1,83 @@
1
+ // 通过原生能力检查与已有会话分支定位打开动作,不依赖构建哈希或压缩名称。
2
+ export function quickChatOpenExport(source) {
3
+ 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\(/);
4
+ if (!match) throw new Error('quick-chat-open-contract-unavailable');
5
+ const escaped = match[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6
+ const name = source.slice(source.lastIndexOf('export{')).match(new RegExp(`(?:\\{|,)${escaped} as ([\\w$]+)(?:,|\\})`))?.[1];
7
+ if (!name) throw new Error('quick-chat-open-export-unavailable');
8
+ return name;
9
+ }
10
+
11
+ export function installQuickChatShortcut(signal, codex, discover = null) {
12
+ let bindings = null, disposed = false;
13
+ function activateExisting() {
14
+ // 原生恢复按钮同时恢复 presentation、取消最小化并聚焦输入框。
15
+ for (const button of document.querySelectorAll('button')) {
16
+ if (!button.checkVisibility({visibilityProperty:true})) continue;
17
+ const floating = codex.props(button, p => p.unifiedDocking === true && typeof p.onMinimizedChange === 'function' && typeof p.onPresentationChange === 'function');
18
+ if (!floating?.isMinimized || floating.isHidden) continue;
19
+ const restore = codex.props(button, p => p.uniform === true && p.size === 'composer' && typeof p.onClick === 'function'
20
+ && typeof p.onPointerEnter === 'function' && typeof p.onPointerDown === 'function' && typeof p.onKeyDown === 'function');
21
+ if (!restore) continue;
22
+ button.click();
23
+ return true;
24
+ }
25
+ for (const surface of document.querySelectorAll('[data-floating-chat-surface]')) {
26
+ if (surface.closest('[hidden], [inert], [aria-hidden="true"]')) continue;
27
+ const floating = codex.props(surface, p => p.unifiedDocking === true && typeof p.onMinimizedChange === 'function' && typeof p.onPresentationChange === 'function');
28
+ if (!floating || floating.isMinimized || floating.isHidden) continue;
29
+ const editable = surface.querySelector('[data-chatgpt-composer] [contenteditable="true"]');
30
+ if (!editable?.checkVisibility({visibilityProperty:true})) continue;
31
+ editable.focus({preventScroll:true});
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+ function context() {
37
+ for (const button of document.querySelectorAll('button[aria-haspopup="menu"]')) {
38
+ if (!button.checkVisibility({visibilityProperty:true})) continue;
39
+ const mode = codex.props(button, p => typeof p.onModeSelect === 'function' && typeof p.mode === 'string', 80)?.mode;
40
+ if (mode !== 'codex') continue;
41
+ for (let fiber = codex.currentFiber(button), depth = 0; fiber && depth++ < 200; fiber = fiber.return) {
42
+ const values = [];
43
+ for (let hook = fiber.memoizedState, n = 0; hook && n++ < 150; hook = hook.next) values.push(hook.memoizedState?.current);
44
+ for (const row of fiber.updateQueue?.memoCache?.data ?? []) values.push(...row);
45
+ const scope = values.find(v => typeof v?.get === 'function' && typeof v.set === 'function' && v.scope?.__scopeBrand === 'AppScope');
46
+ if (scope) return scope;
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ async function load() {
52
+ const url = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(e => e.href || e.src).find(url => /\/app-initial-[^/]+\.js(?:$|\?)/.test(url));
53
+ if (!url) throw new Error('quick-chat-module-unavailable');
54
+ const source = await (await fetch(url, {signal})).text();
55
+ const module = await import(url);
56
+ const commands = Object.values(module).find(v => Array.isArray(v) && v.some(c => c?.id === 'temporaryChat') && v.some(c => c?.id === 'newTask'));
57
+ if (!commands) throw new Error('quick-chat-command-catalog-unavailable');
58
+ if (commands.some(c => c.id === 'quickChat')) return {native:true};
59
+ const open = module[quickChatOpenExport(source)];
60
+ if (typeof open !== 'function') throw new Error('quick-chat-open-action-unavailable');
61
+ return {native:false, open};
62
+ }
63
+ const ready = (discover ?? load)().then(value => {
64
+ if (disposed || signal.aborted) return;
65
+ bindings = value;
66
+ codex.report('quick-chat-shortcut', true);
67
+ }).catch(() => { if (!disposed && !signal.aborted) codex.report('quick-chat-shortcut', false, 'native-shortcut-contract-unavailable'); });
68
+ function keydown(event) {
69
+ if (disposed || signal.aborted || bindings?.native || event.defaultPrevented || event.repeat || event.isComposing
70
+ || !event.metaKey || !event.altKey || event.ctrlKey || event.shiftKey || event.code !== 'KeyN') return;
71
+ if (!activateExisting()) {
72
+ const scope = bindings && context();
73
+ if (!scope) return;
74
+ bindings.open(scope);
75
+ }
76
+ event.preventDefault();
77
+ event.stopPropagation();
78
+ }
79
+ document.addEventListener('keydown', keydown, {signal});
80
+ function dispose() { disposed = true; document.removeEventListener('keydown', keydown); bindings = null; }
81
+ signal.addEventListener('abort', dispose, {once:true});
82
+ return {ready, dispose};
83
+ }
@@ -0,0 +1,12 @@
1
+ export function createHistoryTracker() {
2
+ let origin = null;
3
+ return {
4
+ open(snapshot) { if (snapshot.historyVisible) origin = snapshot.sessionId; },
5
+ selected(snapshot) {
6
+ if (origin === null || origin === snapshot.sessionId) return false;
7
+ origin = null;
8
+ return snapshot.hasConversation;
9
+ },
10
+ clear() { origin = null; },
11
+ };
12
+ }
@@ -0,0 +1,15 @@
1
+ // 发送确认会写入同会话的新呈现记录;拒绝发送不会更新该记录。
2
+ export function createSubmissionTracker() {
3
+ let pending = null;
4
+ return {
5
+ begin(snapshot) { pending = snapshot.canSubmit ? {sessionId:snapshot.sessionId, receiptId:snapshot.receiptId} : null; },
6
+ accepted(snapshot) {
7
+ if (!pending) return false;
8
+ if (pending.sessionId !== snapshot.sessionId) { pending = null; return false; }
9
+ if (!snapshot.hasConversation || !snapshot.receiptId || snapshot.receiptId === pending.receiptId) return false;
10
+ pending = null;
11
+ return snapshot.receiptPresentation !== "minimized";
12
+ },
13
+ clear() { pending = null; },
14
+ };
15
+ }
@@ -0,0 +1,104 @@
1
+ const native = createQuickChatAdapter(config.codex);
2
+ const shortcut = installQuickChatShortcut(signal, config.codex);
3
+ const pin = installQuickChatPin(signal, config);
4
+ const instances = new Map();
5
+ let scheduled = false, disposed = false;
6
+ const text = (en,zh) => (document.documentElement.lang || navigator.language).startsWith('zh') ? zh : en;
7
+ const style = document.createElement('style');
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}
11
+ [data-mcx-quick-chat-minimize]{border-radius:calc(infinity * 1px)}
12
+ [data-mcx-quick-chat-project-rail]{display:none!important}
13
+ /* 隐藏唯一的项目栏时,一并收起原生 rail 的圆角留白与负 margin。 */
14
+ [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}
16
+ `;
17
+ document.head.append(style);
18
+ function releaseBackground(instance) {
19
+ if (!instance.background) return;
20
+ instance.background.removeAttribute('data-mcx-ui-surface'); instance.background.removeAttribute('data-mcx-ui-variant');
21
+ instance.background = null;
22
+ }
23
+ 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(); }
25
+ function sync() {
26
+ if (disposed) return;
27
+ pin.sync();
28
+ const snapshots = native.surfaces(), active = new Set();
29
+ for (const snapshot of snapshots) {
30
+ active.add(snapshot.surface);
31
+ let instance = instances.get(snapshot.surface);
32
+ const selected = instance?.historyTracker.selected(snapshot);
33
+ if (!instance || instance.sessionId !== snapshot.sessionId || instance.emitter !== snapshot.emitter) {
34
+ if (instance) remove(instance);
35
+ instance = {sessionId:snapshot.sessionId, emitter:snapshot.emitter, tracker:createSubmissionTracker(), historyTracker:createHistoryTracker(), button:null};
36
+ const begin = () => { const current = native.read(snapshot.surface); if (current) instance.tracker.begin(current); };
37
+ instance.unsubscribe = native.subscribe(snapshot, begin);
38
+ instances.set(snapshot.surface, instance);
39
+ }
40
+ if (selected || instance.tracker.accepted(snapshot)) snapshot.expand();
41
+ instance.historyTracker.open(snapshot);
42
+ if (instance.projectRail !== snapshot.projectRail) {
43
+ releaseProjectRail(instance);
44
+ instance.projectRail = snapshot.projectRail;
45
+ instance.projectRail?.setAttribute('data-mcx-quick-chat-project-rail','');
46
+ }
47
+ if (instance.background !== snapshot.background) {
48
+ releaseBackground(instance);
49
+ if (snapshot.background) {
50
+ instance.background = snapshot.background;
51
+ config.ui.surface(snapshot.background,'floating-panel');
52
+ }
53
+ }
54
+ const minimize = snapshot.minimize;
55
+ if (instance.minimize !== minimize) {
56
+ instance.minimize?.removeAttribute('data-mcx-quick-chat-minimize'); instance.minimize = minimize;
57
+ minimize?.setAttribute('data-mcx-quick-chat-minimize','');
58
+ }
59
+ if (!minimize || !snapshot.showHistory) { instance.button?.remove(); instance.button = null; continue; }
60
+ if (!instance.button?.isConnected || instance.button.parentElement !== minimize.parentElement) {
61
+ instance.button?.remove();
62
+ const button = minimize.cloneNode(false);
63
+ for (const attr of ['id','disabled','aria-disabled','aria-describedby','aria-expanded','title']) button.removeAttribute(attr);
64
+ button.removeAttribute('data-mcx-quick-chat-minimize');
65
+ button.type = 'button';
66
+ button.setAttribute('data-mcx-quick-chat-history', '');
67
+ button.innerHTML = config.resources.history;
68
+ const icon = button.querySelector('svg');
69
+ 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;
73
+ }
74
+ if (instance.button.className !== minimize.className) instance.button.className = minimize.className;
75
+ const label = text('Chat history', '聊天历史');
76
+ if (instance.button.getAttribute('aria-label') !== label) {
77
+ instance.button.setAttribute('aria-label',label); instance.button.setAttribute('data-minecodex-tooltip',label);
78
+ }
79
+ }
80
+ for (const [surface, instance] of instances) if (!active.has(surface)) { remove(instance); instances.delete(surface); }
81
+ }
82
+ function schedule() { if (disposed || scheduled) return; scheduled = true; queueMicrotask(() => { scheduled = false; sync(); }); }
83
+ function begin(event) {
84
+ const snapshot = native.surfaces().find(item => item.surface.contains(event.target));
85
+ if (!snapshot) return;
86
+ if (event.type === 'click' && !event.target.closest('button[type="submit"]')) { instances.get(snapshot.surface)?.tracker.clear(); return; }
87
+ instances.get(snapshot.surface)?.tracker.begin(snapshot);
88
+ }
89
+ document.addEventListener('click',begin,{capture:true,signal});
90
+ document.addEventListener('submit',begin,{capture:true,signal});
91
+ const observer = new MutationObserver(schedule);
92
+ observer.observe(document.documentElement,{subtree:true,childList:true,attributes:true,attributeFilter:['aria-hidden','inert','hidden','lang']});
93
+ sync();
94
+ 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), pinned = pin.status();
97
+ const applicable = anchors.length > 0 || pinned.applicable;
98
+ const available = (!anchors.length || history) && (!pinned.applicable || pinned.available);
99
+ return {state:applicable ? available ? 'available' : 'unavailable' : 'not-applicable',
100
+ reason:applicable ? available ? null : 'quick-chat-controls-unavailable' : 'no-classic-quick-chat-floating-controls',
101
+ checks:[...(anchors.length ? [{name:'quick-chat-history',available:history}] : []), ...(pinned.applicable ? [{name:'quick-chat-pin',available:pinned.available}] : [])],
102
+ scopes:snapshots.map(item=>({name:'quick-chat',element:item.surface}))};
103
+ });
104
+ return () => { disposed = true; shortcut.dispose(); pin.dispose(); observer.disconnect(); for (const instance of instances.values()) remove(instance); instances.clear(); style.remove(); };
@@ -0,0 +1,54 @@
1
+ export function installQuickChatPin(signal, config) {
2
+ const native = createQuickChatPinAdapter(config.codex), instances = new Map();
3
+ const style = document.createElement('style');
4
+ // 保留原生可交互子元素的 pointer-events-auto,仅让透明背景穿透。
5
+ style.textContent = '[data-mcx-quick-chat-pinned-layer]{pointer-events:none!important}';
6
+ document.head.append(style);
7
+ const text = (en,zh) => (document.documentElement.lang || navigator.language).startsWith('zh') ? zh : en;
8
+ function render(instance) {
9
+ if (instance.pinned) instance.layer.setAttribute('data-mcx-quick-chat-pinned-layer','');
10
+ else instance.layer.removeAttribute('data-mcx-quick-chat-pinned-layer');
11
+ const button = instance.button;
12
+ if (!button) return;
13
+ button.setAttribute('aria-pressed',String(instance.pinned));
14
+ const label = instance.pinned ? text('Unpin chat','取消固定聊天') : text('Pin chat','固定聊天');
15
+ button.setAttribute('aria-label',label); button.setAttribute('data-minecodex-tooltip',label);
16
+ button.querySelector('svg path:last-child')?.setAttribute('fill',instance.pinned ? 'currentColor' : 'none');
17
+ }
18
+ function release(instance) { instance.layer.removeAttribute('data-mcx-quick-chat-pinned-layer'); instance.button?.remove(); }
19
+ function sync() {
20
+ const live = new Set();
21
+ for (const snapshot of native.surfaces()) {
22
+ live.add(snapshot.sessionId);
23
+ let instance = instances.get(snapshot.sessionId);
24
+ if (!instance) { instance={pinned:false,layer:snapshot.layer,button:null}; instances.set(snapshot.sessionId,instance); }
25
+ if (instance.layer !== snapshot.layer) { release(instance); instance.layer=snapshot.layer; instance.button=null; }
26
+ if (snapshot.minimized || snapshot.history) instance.pinned=false;
27
+ if (instance.pinned && !snapshot.minimized && !snapshot.expanded && !snapshot.history) snapshot.expand();
28
+ if (!snapshot.showPin) { instance.button?.remove(); instance.button=null; render(instance); continue; }
29
+ const anchor = snapshot.anchor;
30
+ if (!instance.button?.isConnected || instance.button.parentElement !== anchor.parentElement) {
31
+ instance.button?.remove();
32
+ const button = document.createElement('button');
33
+ button.type='button'; button.setAttribute('data-mcx-quick-chat-pin',''); button.innerHTML=config.resources.pin;
34
+ const icon=button.querySelector('svg'), original=anchor.querySelector('svg');
35
+ for (const attr of ['width','height','class']) if (original?.hasAttribute(attr)) icon?.setAttribute(attr,original.getAttribute(attr));
36
+ icon?.setAttribute('aria-hidden','true');
37
+ button.addEventListener('pointerdown',event=>event.stopPropagation(),{signal});
38
+ button.addEventListener('click',event=>{event.stopPropagation();instance.pinned=!instance.pinned;render(instance);},{signal});
39
+ anchor.before(button); instance.button=button;
40
+ }
41
+ if (instance.button.className !== anchor.className) instance.button.className=anchor.className;
42
+ render(instance);
43
+ }
44
+ for (const [id,instance] of instances) if (!live.has(id)) { release(instance); instances.delete(id); }
45
+ }
46
+ function escape(event) {
47
+ if (event.key !== 'Escape' || event.defaultPrevented || event.repeat || event.isComposing) return;
48
+ for (const instance of instances.values()) if (instance.layer.contains(event.target)) { instance.pinned=false; render(instance); }
49
+ }
50
+ document.addEventListener('keydown',escape,{capture:true,signal});
51
+ function dispose() { document.removeEventListener('keydown',escape,true); for (const instance of instances.values()) release(instance); instances.clear(); style.remove(); }
52
+ function status() { const applicable=native.surfaces().some(snapshot=>snapshot.showPin); return {applicable,available:applicable && [...instances.values()].some(instance=>instance.button?.isConnected)}; }
53
+ return {sync,dispose,status};
54
+ }
@@ -0,0 +1,11 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createQuickChatAdapter } from "./adapters/codex/renderer.mjs";
3
+ import { quickChatOpenExport, installQuickChatShortcut } from "./adapters/codex/shortcut.mjs";
4
+ import { createSubmissionTracker } from "./domain/submission.mjs";
5
+ import { createQuickChatPinAdapter } from "./adapters/codex/pin.mjs";
6
+ import { installQuickChatPin } from "./frontend/pin.mjs";
7
+ import { createHistoryTracker } from "./domain/history.mjs";
8
+ export async function createPageScriptSource() {
9
+ const source = await readFile(new URL("./frontend/entry.mjs", import.meta.url), "utf8");
10
+ return `const createQuickChatPinAdapter=${createQuickChatPinAdapter.toString()};\nconst installQuickChatPin=${installQuickChatPin.toString()};\nconst quickChatOpenExport=${quickChatOpenExport.toString()};\nconst installQuickChatShortcut=${installQuickChatShortcut.toString()};\nconst createQuickChatAdapter=${createQuickChatAdapter.toString()};\nconst createSubmissionTracker=${createSubmissionTracker.toString()};\nconst createHistoryTracker=${createHistoryTracker.toString()};\n${source}`;
11
+ }
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,17 @@
1
+ <!-- @license lucide-static v0.468.0 - ISC -->
2
+ <svg
3
+ class="lucide lucide-history"
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ width="24"
6
+ height="24"
7
+ viewBox="0 0 24 24"
8
+ fill="none"
9
+ stroke="currentColor"
10
+ stroke-width="2"
11
+ stroke-linecap="round"
12
+ stroke-linejoin="round"
13
+ >
14
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
15
+ <path d="M3 3v5h5" />
16
+ <path d="M12 7v5l4 2" />
17
+ </svg>
@@ -0,0 +1,10 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "quick-chat-repair",
4
+ "version": "0.1.0",
5
+ "label": {"en":"Quick Chat Exit Repair", "zh-CN":"Quick Chat 退出修复"},
6
+ "description": {"en":"Fix the native transparent layer that blocks clicks after closing Quick Chat.", "zh-CN":"修复关闭 Quick Chat 后透明层残留、主页面无法点击的问题。"},
7
+ "entry": {"kind":"page-script", "script":"src/frontend/entry.mjs", "sourceFactory":"src/injection-source.mjs"},
8
+ "icon": {"resource":"assets/history.svg"},
9
+ "placement": {"order":76}
10
+ }
@@ -0,0 +1,74 @@
1
+ export function createQuickChatExitAdapter(codex) {
2
+ const completed = new WeakSet();
3
+ const selector = '.fixed.inset-0.zoom-adjusted-viewport.pointer-events-auto';
4
+ function currentLayer(layer) {
5
+ const attached = codex.currentFiber(layer);
6
+ let host = attached;
7
+ while (host?.return) host = host.return;
8
+ const current = host?.stateNode?.current;
9
+ if (!current) return attached;
10
+ // Host props 可原样复用,DOM 上的 Fiber 因此可能仍指向上一棵树。
11
+ const pending = [current];
12
+ let visited = 0;
13
+ while (pending.length && visited++ < 10000) {
14
+ const fiber = pending.pop();
15
+ if (fiber.stateNode === layer) return fiber;
16
+ for (let child = fiber.child; child; child = child.sibling) pending.push(child);
17
+ }
18
+ return null;
19
+ }
20
+ function presence(fiber) {
21
+ for (let dependency = fiber.dependencies?.firstContext; dependency; dependency = dependency.next) {
22
+ const context = dependency.memoizedValue;
23
+ if (context?.isPresent === false && typeof context.register === 'function' && typeof context.onExitComplete === 'function') return context;
24
+ }
25
+ return null;
26
+ }
27
+ function completion(fiber) {
28
+ const props = fiber.memoizedProps;
29
+ if (props?.propagate !== true || fiber.child || typeof fiber.type !== 'function') return null;
30
+ const source = Function.prototype.toString.call(fiber.type);
31
+ if (!source.includes('presenceAffectsLayout:') || !source.includes('propagate:') || !source.includes('.useState)')) return null;
32
+ const context = presence(fiber);
33
+ if (!context) return null;
34
+ for (let hook = fiber.memoizedState, count = 0; hook && count < 40; hook = hook.next, count++) {
35
+ const value = hook.memoizedState;
36
+ if (!Array.isArray(value) || typeof value[0] !== 'function') continue;
37
+ const deps = value[1];
38
+ if (!Array.isArray(deps) || deps.length !== 3 || typeof deps[0] !== 'string' || deps[1] !== context.onExitComplete || deps[2] !== true) continue;
39
+ // 仅补齐仍未完成的 usePresence 登记,不绕过父层或其他子动画。
40
+ for (let parent = fiber.return, depth = 0; parent && depth < 40; parent = parent.return, depth++) {
41
+ if (parent.memoizedProps?.value !== context) continue;
42
+ for (let owner = parent.return, level = 0; owner && level < 5; owner = owner.return, level++) {
43
+ for (let state = owner.memoizedState, n = 0; state && n < 20; state = state.next, n++) {
44
+ const map = state.memoizedState?.current;
45
+ if (map instanceof Map && map.get(deps[0]) === false) return value[0];
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+ function repair(layer) {
54
+ if (!layer.isConnected) return 0;
55
+ let root = currentLayer(layer);
56
+ for (let depth = 0; root && depth < 40; root = root.return, depth++) {
57
+ if (typeof root.memoizedProps?.isPresent === 'boolean' && typeof root.memoizedProps?.onExitComplete === 'function') break;
58
+ }
59
+ if (!root || root.memoizedProps.isPresent !== false) return 0;
60
+ const pending = [root];
61
+ const completions = [];
62
+ let quickChat = false, count = 0, visited = 0;
63
+ while (pending.length && visited++ < 1000) {
64
+ const fiber = pending.pop(), finish = completion(fiber);
65
+ if (fiber.memoizedProps?.name === 'QuickChatWindow') quickChat = true;
66
+ if (finish && !completed.has(finish)) completions.push(finish);
67
+ for (let child = fiber.child; child; child = child.sibling) pending.push(child);
68
+ }
69
+ if (!quickChat || pending.length) return 0;
70
+ for (const finish of completions) { completed.add(finish); finish(); count++; }
71
+ return count;
72
+ }
73
+ return { selector, repair, reconcile:() => [...document.querySelectorAll(selector)].reduce((count, layer) => count + repair(layer), 0) };
74
+ }
@@ -0,0 +1,12 @@
1
+ const native = createQuickChatExitAdapter(config.codex);
2
+ let scheduled = false, disposed = false;
3
+ function schedule() {
4
+ if (scheduled || disposed) return;
5
+ scheduled = true;
6
+ queueMicrotask(() => { scheduled = false; if (!disposed) native.reconcile(); });
7
+ }
8
+ const observer = new MutationObserver(schedule);
9
+ observer.observe(document.documentElement, {subtree:true, childList:true, attributes:true, attributeFilter:['style']});
10
+ schedule();
11
+ config.registerSelfCheck?.(() => ({state:'available', checks:[{name:'native-exit-observer', available:!disposed}], scopes:[]}));
12
+ return () => { disposed = true; observer.disconnect(); };
@@ -0,0 +1,5 @@
1
+ import {readFile} from 'node:fs/promises';
2
+ import {createQuickChatExitAdapter} from './adapters/codex/presence.mjs';
3
+ export async function createPageScriptSource() {
4
+ return `const createQuickChatExitAdapter=${createQuickChatExitAdapter.toString()};\n${await readFile(new URL('./frontend/entry.mjs', import.meta.url), 'utf8')}`;
5
+ }
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -68,7 +68,7 @@ export function sessionFacts(record) {
68
68
  if (record?.type === "event_msg") {
69
69
  if (p.type === "context_compacted") emit("context-reset");
70
70
  if (p.type === "task_started" && p.turn_id) emit("turn-started", { id:p.turn_id });
71
- if (["task_complete", "task_interrupted", "turn_aborted"].includes(p.type)) emit("turn-ended", { id:p.turn_id });
71
+ if (["task_complete", "task_interrupted", "turn_aborted"].includes(p.type)) emit("turn-ended", { id:p.turn_id, completed:p.type === "task_complete" });
72
72
  if (p.type === "task_complete" && Number.isFinite(p.time_to_first_token_ms)) emit("first-token-measured", { elapsed:p.time_to_first_token_ms });
73
73
  if (p.type === "item_completed") {
74
74
  if (p.item?.type === "ContextCompaction") emit("compaction-finished");
@@ -9,6 +9,7 @@ export function createSessionState() {
9
9
  turns: new Set(),
10
10
  currentTurnId: null,
11
11
  runningTurns: new Set(),
12
+ completedTurns: new Set(),
12
13
  responses: new Set(),
13
14
  hasResponseUsage: false,
14
15
  total: null,
@@ -66,8 +67,14 @@ export function applySessionStatusEvent(state, event) {
66
67
  if (Number.isFinite(event.contextWindow)) state.contextWindow=event.contextWindow;
67
68
  state.baseInstructionTokens=event.instructionTokens; break;
68
69
  case "context-reset": state.compactionEvents++; state.last=null; reset(); break;
69
- case "turn-started": reset(); state.currentTurnId=event.id; state.turns.add(event.id); state.runningTurns.add(event.id); break;
70
- case "turn-ended": reset(); state.currentTurnId=null; if (event.id) state.runningTurns.delete(event.id); else state.runningTurns.clear(); break;
70
+ case "turn-started": reset(); state.currentTurnId=event.id; state.turns.add(event.id); state.runningTurns.add(event.id); state.completedTurns.delete(event.id); break;
71
+ case "turn-ended": {
72
+ const id = event.id ?? state.currentTurnId;
73
+ if (event.completed && id) state.completedTurns.add(id);
74
+ reset(); state.currentTurnId=null;
75
+ if (id) state.runningTurns.delete(id); else state.runningTurns.clear();
76
+ break;
77
+ }
71
78
  case "first-token-measured": state.ttftTotalMs+=event.elapsed; state.ttftSamples++; break;
72
79
  case "compaction-finished": state.compactionCompleted++; break;
73
80
  case "output-observed":
@@ -103,8 +110,7 @@ export function applySessionStatusEvent(state, event) {
103
110
  if (state.total || state.turns.size) state.updatedAt=event.at ?? state.updatedAt;
104
111
  }
105
112
 
106
- export function sessionStatusSnapshot(state, threadId, measurements = new Map()) {
107
- const sample = state.speedMeasurements.at(-1);
113
+ function responseSpeed(sample, measurements) {
108
114
  const measurement = measurements.get(sample?.id);
109
115
  const source = measurement?.source ?? "codex";
110
116
  const elapsed = measurement ? measurement.elapsed : sample?.start == null ? null
@@ -114,6 +120,23 @@ export function sessionStatusSnapshot(state, threadId, measurements = new Map())
114
120
  const output = measurement?.output ?? sample?.output;
115
121
  const unavailableReason = measurement?.reason ?? (Number.isFinite(elapsed) && elapsed > 0 && elapsed < 1000
116
122
  ? "decode_window_too_short" : "timing_missing");
123
+ return { source, elapsed, valid, output, unavailableReason };
124
+ }
125
+
126
+ export function sessionStatusSnapshot(state, threadId, measurements = new Map()) {
127
+ const { source, elapsed, valid, output, unavailableReason } = responseSpeed(state.speedMeasurements.at(-1), measurements);
128
+ // 窗口先按完成回合截取;缺少计时不能让更早回合补进最新十轮。
129
+ const recentTurns = new Map([...state.completedTurns].slice(-10).map(id => [id, { output:0, elapsed:0 }]));
130
+ for (const sample of state.speedMeasurements) {
131
+ const turn = recentTurns.get(sample.turnId);
132
+ if (!turn) continue;
133
+ const speed = responseSpeed(sample, measurements);
134
+ if (!speed.valid) continue;
135
+ turn.output += speed.output;
136
+ turn.elapsed += speed.elapsed;
137
+ }
138
+ const turnSpeeds = [...recentTurns.values()].filter(turn => turn.elapsed > 0)
139
+ .map(turn => turn.output / (turn.elapsed / 1000));
117
140
  const total = state.total;
118
141
  const input = total?.input ?? 0;
119
142
  const cached = total?.cached ?? 0;
@@ -137,6 +160,8 @@ export function sessionStatusSnapshot(state, threadId, measurements = new Map())
137
160
  speedSource: source,
138
161
  speedUnavailableReason: valid ? null : unavailableReason,
139
162
  observedModelMs: valid ? elapsed : 0,
163
+ averageTps: turnSpeeds.length ? Math.round(turnSpeeds.reduce((sum, speed) => sum + speed, 0) / turnSpeeds.length) : null,
164
+ averageTpsTurns: turnSpeeds.length,
140
165
  toolMs: state.toolMs,
141
166
  averageFirstTokenMs: state.ttftSamples > 0 ? Math.round(state.ttftTotalMs / state.ttftSamples) : null,
142
167
  contextUsedTokens: contextUsed,
@@ -4,7 +4,7 @@ const STYLE_ID = "minecodex-session-status-style";
4
4
  const copyByLocale = { "zh-CN": {
5
5
  turns: "回合", steps: "步", speed: "TPS", cache: "缓存", cacheDetail: "缓存命中",
6
6
  statistics: "会话统计", usage: "Token 用量", context: "上下文详情", total: "总量",
7
- firstToken: "平均首字延迟", input: "输入", cached: "缓存输入", output: "输出",
7
+ firstToken: "平均首字延迟", averageSpeed: "平均 TPS", input: "输入", cached: "缓存输入", output: "输出",
8
8
  sum: "合计", capacity: "窗口容量", contextShare: "上下文占用", compactions: "压缩次数",
9
9
  instructions: "指令", messages: "消息", toolHistory: "工具交互", other: "未分类",
10
10
  speedHelp: "关于 TPS", speedInfo: "根据会话日志估算生成速度,包含推理输出。",
@@ -15,7 +15,7 @@ const copyByLocale = { "zh-CN": {
15
15
  }, en: {
16
16
  turns: "turns", steps: "steps", speed: "TPS", cache: "Cache", cacheDetail: "Cache hit",
17
17
  statistics: "Session stats", usage: "Token usage", context: "Context", total: "Total",
18
- firstToken: "Avg first token", input: "Input", cached: "Cached input", output: "Output",
18
+ firstToken: "Avg first token", averageSpeed: "Avg TPS", input: "Input", cached: "Cached input", output: "Output",
19
19
  sum: "Sum", capacity: "Window size", contextShare: "Context used", compactions: "Compactions",
20
20
  instructions: "Instructions", messages: "Messages", toolHistory: "Tool activity", other: "Unclassified",
21
21
  speedHelp: "About TPS", speedInfo: "Generation speed estimated from session logs, including reasoning output.",
@@ -377,6 +377,7 @@ function showPanel(kind) {
377
377
  row(dl, copy.turns, number(snapshot.turns));
378
378
  row(dl, copy.steps, number(snapshot.steps));
379
379
  row(dl, copy.firstToken, duration(snapshot.averageFirstTokenMs));
380
+ row(dl, copy.averageSpeed, snapshot.averageTps == null ? "—" : `${number(snapshot.averageTps)} tok/s`);
380
381
  const speedLabel = row(dl, copy.speed, snapshot.observedTps == null ? "—" : `${number(snapshot.observedTps)} tok/s`);
381
382
  speedLabel.className = "tps-label";
382
383
  tpsInfo.setAttribute("aria-label", copy.speedHelp);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "2.0.27",
3
+ "version": "2.0.29",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,6 +44,10 @@
44
44
  "README.md",
45
45
  "features/usage-meter/src",
46
46
  "features/usage-meter/codex-feature.json",
47
+ "features/quick-chat/src",
48
+ "features/quick-chat/assets",
49
+ "features/quick-chat/vendor/lucide-LICENSE",
50
+ "features/quick-chat/codex-feature.json",
47
51
  "features/sidebar-focus/src",
48
52
  "features/sidebar-focus/codex-feature.json",
49
53
  "features/sidebar-focus/vendor/lucide-LICENSE",
@@ -72,14 +76,18 @@
72
76
  "apps/dashboard/transport.js",
73
77
  "packages/plugin/src",
74
78
  "packages/plugin/assets",
75
- "packages/plugin/plugin.json"
79
+ "packages/plugin/plugin.json",
80
+ "features/quick-chat-repair/src",
81
+ "features/quick-chat-repair/assets",
82
+ "features/quick-chat-repair/vendor/lucide-LICENSE",
83
+ "features/quick-chat-repair/codex-feature.json"
76
84
  ],
77
85
  "engines": {
78
86
  "node": ">=22.5.0"
79
87
  },
80
88
  "scripts": {
81
89
  "start": "node packages/cli/bin/mcx.mjs serve",
82
- "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/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",
90
+ "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/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/quick-chat-repair/test/*.test.mjs",
83
91
  "test:cli": "node --test packages/cli/test/*.test.mjs",
84
92
  "test:runtime": "node --test packages/runtime-host/test/*.test.mjs",
85
93
  "check": "node scripts/check-syntax.mjs",
@@ -2,7 +2,7 @@ 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", "file-navigation"]);
5
+ export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "hide-upsell-banner", "session-status", "usage-meter", "sidebar-focus", "sites", "markdown-reading", "file-navigation", "quick-chat", "quick-chat-repair"]);
6
6
  export const DEFAULT_CONFIG = Object.freeze({
7
7
  schemaVersion: 1,
8
8
  features: Object.freeze({
@@ -16,6 +16,8 @@ export const DEFAULT_CONFIG = Object.freeze({
16
16
  sites: true,
17
17
  "markdown-reading": true,
18
18
  "file-navigation": true,
19
+ "quick-chat": true,
20
+ "quick-chat-repair": true,
19
21
  }),
20
22
  });
21
23
 
@@ -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", "file-navigation", "images", "sites", "sidebar-focus", "hide-upsell-banner"];
17
+ const DASHBOARD_FEATURE_ORDER = ["model-slider", "notes", "usage-meter", "session-status", "markdown-reading", "file-navigation", "images", "sites", "quick-chat", "quick-chat-repair", "sidebar-focus", "hide-upsell-banner"];
18
18
  const CONTENT_TYPES = new Map([
19
19
  [".css", "text/css; charset=utf-8"],
20
20
  [".html", "text/html; charset=utf-8"],
@@ -13,15 +13,16 @@ export function installSurfaces(signal, ui) {
13
13
  [data-mcx-ui-surface=popover] { box-shadow:0 0 0 var(--mcx-popover-overlay-ring-width,.5px) var(--mcx-color-border-overlay),var(--mcx-shadow-popover-overlay,var(--mcx-shadow-lg)); }
14
14
  [data-mcx-ui-surface=popover][data-mcx-ui-variant=translucent] { background:color-mix(in oklab,var(--mcx-color-surface-elevated-secondary) 90%,transparent); }
15
15
  [data-mcx-ui-surface=composer-menu] { border:var(--mcx-suggestion-menu-border-width,1px) solid var(--mcx-color-border); border-radius:var(--mcx-radius-suggestion-menu,var(--mcx-radius-2xl)); box-shadow:var(--mcx-suggestion-menu-shadow,none); }
16
+ [data-mcx-ui-surface=floating-panel] { box-shadow:0 0 0 .5px var(--mcx-color-border),var(--mcx-shadow-2xl); }
16
17
  [data-mcx-ui-surface=page] { border-radius:var(--mcx-radius-xl-base); background:var(--mcx-color-surface); box-shadow:none; }
17
18
  [data-mcx-ui-surface=page][data-mcx-ui-variant=transparent] { background:transparent; }
18
19
  `;
19
20
  document.head.append(style);
20
21
  signal.addEventListener("abort", () => style.remove(), { once:true });
21
22
  function surface(node, kind = "popover", { variant, opaque = true } = {}) {
22
- const defaults = { dialog:"opaque", dropdown:"menu", popover:opaque ? "opaque" : "translucent", "composer-menu":"floating", page:"opaque" };
23
+ const defaults = { dialog:"opaque", dropdown:"menu", popover:opaque ? "opaque" : "translucent", "composer-menu":"floating", page:"opaque", "floating-panel":"opaque" };
23
24
  if (!(kind in defaults)) throw new Error(`Unknown UI surface: ${kind}`);
24
- if (kind !== "dialog" && kind !== "page") ui.layer(node, "floating");
25
+ if (kind !== "dialog" && kind !== "page" && kind !== "floating-panel") ui.layer(node, "floating");
25
26
  node.setAttribute("data-mcx-ui-surface", kind);
26
27
  node.setAttribute("data-mcx-ui-variant", variant ?? defaults[kind]);
27
28
  return node;
@@ -96,6 +96,7 @@ export const CODEX_THEME_TOKENS = Object.freeze([
96
96
  "--radius-3xl",
97
97
  "--shadow-md",
98
98
  "--shadow-lg",
99
+ "--shadow-2xl",
99
100
  "--shadow-xl",
100
101
  "--color-surface",
101
102
  "--color-text-primary",