minecodex 2.0.28 → 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.
- package/README.md +1 -1
- package/features/images/src/adapters/codex/image-sources.mjs +3 -1
- package/features/images/src/adapters/codex/native-image-modules.mjs +24 -19
- package/features/model-slider/src/frontend/install.mjs +7 -6
- package/features/quick-chat/assets/pin.svg +1 -0
- package/features/quick-chat/codex-feature.json +2 -2
- package/features/quick-chat/src/adapters/codex/pin.mjs +34 -0
- package/features/quick-chat/src/adapters/codex/shortcut.mjs +83 -0
- package/features/quick-chat/src/frontend/entry.mjs +12 -5
- package/features/quick-chat/src/frontend/pin.mjs +54 -0
- package/features/quick-chat/src/injection-source.mjs +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ MineCodex 给 Codex Desktop 增加一组轻量、本地优先的扩展功能。
|
|
|
19
19
|
| **图片集合** · Image Collection | 集中浏览生成的图片,查看提示词并复用创作思路。 |
|
|
20
20
|
| **站点收藏** · Saved Sites | 发现项目正在运行的网页,收藏常用网址,随时打开。 |
|
|
21
21
|
| **Quick Chat 退出修复** | 修复关闭快捷聊天后透明层残留、主页面无法点击的问题;可独立开关。 |
|
|
22
|
-
| **快捷聊天增强** · Quick Chat |
|
|
22
|
+
| **快捷聊天增强** · Quick Chat | 补回 Codex 模式的 ⌘⌥N,恢复最小化输入框;发送后展开聊天,可固定内容窗口并查看原生聊天历史。 |
|
|
23
23
|
| **侧栏聚焦** · Sidebar Focus | 一键收起侧栏中的其他分组,再次点击恢复原来的展开状态。 |
|
|
24
24
|
| **隐藏额度推广** · Hide Usage Promotions | 隐藏输入框上方的套餐升级推广,让对话更清爽。 |
|
|
25
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
|
-
|
|
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$]+,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 =
|
|
132
|
-
const iconLocal =
|
|
133
|
-
const [Dropdown, Menu
|
|
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(
|
|
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 =
|
|
139
|
-
const
|
|
140
|
-
const
|
|
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(
|
|
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 =
|
|
158
|
-
const
|
|
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 [
|
|
184
|
-
|
|
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 {
|
|
@@ -727,7 +727,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
727
727
|
.reduce((sum, value) => sum + (Number.parseFloat(value) || 0), 0);
|
|
728
728
|
const last = items[Math.max(1, maxVisibleItems) - 1];
|
|
729
729
|
const resultsHeight = last ? last.offsetTop + last.offsetHeight - content.offsetTop : content.offsetHeight;
|
|
730
|
-
const available = modelSelectorControllerMenu.parentMenu.getBoundingClientRect().bottom - 8;
|
|
730
|
+
const available = (modelSelectorControllerMenu.parentMenu.getBoundingClientRect().bottom - 8) / (menu.currentCSSZoom || 1);
|
|
731
731
|
menu.style.height = `${Math.ceil(Math.min(chrome + header.offsetHeight + resultsHeight, available))}px`;
|
|
732
732
|
menu.style.overflowY = "auto";
|
|
733
733
|
return;
|
|
@@ -1008,7 +1008,8 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
1008
1008
|
}
|
|
1009
1009
|
|
|
1010
1010
|
function animateControllerModelMenu(record, opening, onFinish) {
|
|
1011
|
-
|
|
1011
|
+
// height 写入 CSS 动画,必须使用布局尺寸,不能重复计入原生 zoom 和开场 scale。
|
|
1012
|
+
const height = Number.parseFloat(getComputedStyle(record.menu).height);
|
|
1012
1013
|
clearTimeout(record.motionTimer);
|
|
1013
1014
|
record.animations?.forEach((animation) => animation.cancel());
|
|
1014
1015
|
record.animations = [];
|
|
@@ -1139,14 +1140,14 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
1139
1140
|
resetKeyboard();
|
|
1140
1141
|
const record = modelSelectorControllerMenu;
|
|
1141
1142
|
if (!record || record.menu !== menu || record.closing) return;
|
|
1142
|
-
const previousHeight =
|
|
1143
|
+
const previousHeight = Number.parseFloat(getComputedStyle(menu).height);
|
|
1143
1144
|
clearTimeout(record.motionTimer);
|
|
1144
1145
|
record.animations?.forEach((animation) => animation.cancel());
|
|
1145
1146
|
record.animations = [];
|
|
1146
1147
|
menu.scrollTop = 0;
|
|
1147
1148
|
renderEnhancedModelMenu(menu, selector);
|
|
1148
1149
|
sizeModelMenuViewport(menu, selector.maxVisibleItems);
|
|
1149
|
-
record.listHeight =
|
|
1150
|
+
record.listHeight = Number.parseFloat(getComputedStyle(menu).height);
|
|
1150
1151
|
if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
1151
1152
|
const motion = menu.animate([{ height: `${previousHeight}px` }, { height: `${record.listHeight}px` }],
|
|
1152
1153
|
{ duration: 320, easing: "cubic-bezier(.19,1,.22,1)" });
|
|
@@ -1242,7 +1243,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
1242
1243
|
placeholderText,
|
|
1243
1244
|
position: parentMenu.style.position,
|
|
1244
1245
|
overflow: parentMenu.style.overflow,
|
|
1245
|
-
sliderHeight:
|
|
1246
|
+
sliderHeight: Number.parseFloat(getComputedStyle(parentMenu).height),
|
|
1246
1247
|
};
|
|
1247
1248
|
parentMenu.style.position = "relative";
|
|
1248
1249
|
parentMenu.style.overflow = "visible";
|
|
@@ -1251,7 +1252,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
1251
1252
|
modelButton.setAttribute("aria-expanded", "true");
|
|
1252
1253
|
search.focus({ preventScroll: true });
|
|
1253
1254
|
const record = modelSelectorControllerMenu;
|
|
1254
|
-
record.listHeight =
|
|
1255
|
+
record.listHeight = Number.parseFloat(getComputedStyle(menu).height);
|
|
1255
1256
|
animateControllerModelMenu(record, true, () => {
|
|
1256
1257
|
if (modelSelectorControllerMenu !== record || record.closing) return;
|
|
1257
1258
|
record.animations.forEach((animation) => animation.cancel());
|
|
@@ -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>
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"id": "quick-chat",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"label": { "en": "Quick Chat", "zh-CN": "快捷聊天增强" },
|
|
6
|
-
"description": { "en": "
|
|
7
|
-
"entry": { "kind": "page-script", "script": "src/frontend/entry.mjs", "sourceFactory": "src/injection-source.mjs", "resources": { "history": "assets/history.svg" } },
|
|
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
8
|
"icon": { "resource": "assets/history.svg" },
|
|
9
9
|
"placement": { "order": 75 }
|
|
10
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,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
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const native = createQuickChatAdapter(config.codex);
|
|
2
|
+
const shortcut = installQuickChatShortcut(signal, config.codex);
|
|
3
|
+
const pin = installQuickChatPin(signal, config);
|
|
2
4
|
const instances = new Map();
|
|
3
5
|
let scheduled = false, disposed = false;
|
|
4
6
|
const text = (en,zh) => (document.documentElement.lang || navigator.language).startsWith('zh') ? zh : en;
|
|
@@ -22,6 +24,7 @@ function releaseProjectRail(instance) { instance.projectRail?.removeAttribute('d
|
|
|
22
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(); }
|
|
23
25
|
function sync() {
|
|
24
26
|
if (disposed) return;
|
|
27
|
+
pin.sync();
|
|
25
28
|
const snapshots = native.surfaces(), active = new Set();
|
|
26
29
|
for (const snapshot of snapshots) {
|
|
27
30
|
active.add(snapshot.surface);
|
|
@@ -89,9 +92,13 @@ const observer = new MutationObserver(schedule);
|
|
|
89
92
|
observer.observe(document.documentElement,{subtree:true,childList:true,attributes:true,attributeFilter:['aria-hidden','inert','hidden','lang']});
|
|
90
93
|
sync();
|
|
91
94
|
config.registerSelfCheck?.(() => {
|
|
92
|
-
const snapshots = native.surfaces(), anchors = snapshots.filter(item => item.showHistory)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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}))};
|
|
96
103
|
});
|
|
97
|
-
return () => { disposed = true; observer.disconnect(); for (const instance of instances.values()) remove(instance); instances.clear(); style.remove(); };
|
|
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
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { createQuickChatAdapter } from "./adapters/codex/renderer.mjs";
|
|
3
|
+
import { quickChatOpenExport, installQuickChatShortcut } from "./adapters/codex/shortcut.mjs";
|
|
3
4
|
import { createSubmissionTracker } from "./domain/submission.mjs";
|
|
5
|
+
import { createQuickChatPinAdapter } from "./adapters/codex/pin.mjs";
|
|
6
|
+
import { installQuickChatPin } from "./frontend/pin.mjs";
|
|
4
7
|
import { createHistoryTracker } from "./domain/history.mjs";
|
|
5
8
|
export async function createPageScriptSource() {
|
|
6
9
|
const source = await readFile(new URL("./frontend/entry.mjs", import.meta.url), "utf8");
|
|
7
|
-
return `const createQuickChatAdapter=${createQuickChatAdapter.toString()};\nconst createSubmissionTracker=${createSubmissionTracker.toString()};\nconst createHistoryTracker=${createHistoryTracker.toString()};\n${source}`;
|
|
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}`;
|
|
8
11
|
}
|