minecodex 0.1.11 → 0.1.13
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 +33 -32
- package/features/model-slider/THIRD_PARTY_NOTICES.md +14 -0
- package/features/model-slider/assets/providers/xiaomimimo.svg +1 -1
- package/features/model-slider/assets/providers/zhipu.svg +1 -1
- package/package.json +1 -1
- package/packages/runtime-host/src/codex-runtime.mjs +181 -28
package/README.md
CHANGED
|
@@ -1,31 +1,32 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/readme/hero.svg" width="100%" alt="MineCodex:给 Codex Desktop 增加轻量、本地优先的插件,包含模型滑条、笔记与图片库">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# MineCodex
|
|
2
6
|
|
|
3
|
-
MineCodex 给 Codex Desktop
|
|
7
|
+
MineCodex 给 Codex Desktop 增加一组轻量、本地优先的扩展功能。所有数据均保存在本地,不经过任何第三方服务器,也不会改变 Codex 的原有使用习惯。
|
|
8
|
+
|
|
9
|
+
## 插件列表
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
| 插件 | 说明 |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| **Model Slider** | 更清晰、更顺滑的模型切换面板:具备品牌标识、常用收藏与流畅交互 |
|
|
14
|
+
| **Notes** | 跨会话管理待办事项、常用提示词与文件,可随时从 Codex 工具栏快速复用 |
|
|
15
|
+
| **Images** | 集中浏览生成的图片、Prompt 和来源任务,轻松保存原图或快速返回原会话 |
|
|
16
|
+
| **Hide Upsell Banner** | 隐藏输入框上方的用量提示横幅,仅优化界面展示,不影响正常的用量统计 |
|
|
8
17
|
|
|
9
18
|
## 安装
|
|
10
19
|
|
|
11
|
-
MineCodex
|
|
12
|
-
`/Applications/ChatGPT.app` 或 `~/Applications/ChatGPT.app` 查找 Codex Desktop;
|
|
13
|
-
如果两处都找不到,安装会停止并给出提示。
|
|
20
|
+
MineCodex 目前仅支持 macOS 系统,需要 Node.js 22.5 或更高版本。安装程序会自动扫描 `/Applications/ChatGPT.app` 或 `~/Applications/ChatGPT.app` 寻找 Codex Desktop;如果两处均未找到,安装将暂停并给出说明。
|
|
14
21
|
|
|
15
22
|
```bash
|
|
16
23
|
npm install -g minecodex
|
|
17
24
|
mcx install
|
|
18
25
|
```
|
|
19
26
|
|
|
20
|
-
`mcx install`
|
|
21
|
-
MineCodex 将在下次重启 Codex 时启用,并询问是否现在重启;默认答案是 **No**。
|
|
22
|
-
安装过程只有在用户明确确认,或之后主动运行 `mcx restart` 时,才会关闭并重新打开
|
|
23
|
-
Codex。安装后,macOS 的“登录项与扩展”中会显示名为 **MineCodex** 的后台项目。
|
|
24
|
-
这个后台项目负责插件与本地控制台:用户退出 Codex 后,它会保持停止;用户后来从
|
|
25
|
-
Dock 主动打开 Codex 时,这次新启动会被视为明确的启动意图,并切换为可注入的托管
|
|
26
|
-
实例。由于普通 Dock 启动本身没有 CDP,首次切换时 Codex 可能会短暂重开一次。
|
|
27
|
+
运行 `mcx install` 不会强制关闭或启动 Codex,也不会影响当前打开的窗口。安装完成后,终端会说明更改将在下一次重启 Codex 时生效,并询问是否立即重启;默认选项为 **No**。只有在您明确确认,或后续手动运行 `mcx restart` 时,才会重新打开 Codex。安装完成后,macOS 的“登录项与扩展”中会添加名为 **MineCodex** 的后台项目。
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
打开本地控制台:
|
|
29
30
|
|
|
30
31
|
```bash
|
|
31
32
|
mcx open
|
|
@@ -33,19 +34,22 @@ mcx open
|
|
|
33
34
|
|
|
34
35
|
常用命令:
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
mcx
|
|
39
|
-
mcx
|
|
40
|
-
mcx
|
|
41
|
-
mcx
|
|
42
|
-
mcx
|
|
43
|
-
|
|
37
|
+
| 命令 | 作用 |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `mcx install` | 安装或修复 MineCodex |
|
|
40
|
+
| `mcx uninstall [--purge]` | 移除扩展;默认保留个人数据,带 `--purge` 参数才会清理 |
|
|
41
|
+
| `mcx update` | 更新到最新的稳定版本 |
|
|
42
|
+
| `mcx open` | 打开 MineCodex 控制台 |
|
|
43
|
+
| `mcx status [--json]` | 查看安装与插件运行状态 |
|
|
44
|
+
| `mcx restart` | 重启 Codex 并应用插件开启/关闭状态 |
|
|
45
|
+
|
|
46
|
+
关闭某项功能仅停用对应的扩展服务,不会删除任何个人数据;修改设置后运行 `mcx restart` 即可应用更改。
|
|
47
|
+
|
|
48
|
+
运行 `mcx uninstall` 会移除名为 MineCodex 的后台项目,并默认保留配置设置、Notes 笔记和 Images 图片索引;只有显式加上 `--purge` 参数,才会清理 MineCodex 产生的本地配置。MineCodex 绝不会删除 `~/.codex/generated_images` 中的原始图片或 Notes 引用的本地文件。
|
|
49
|
+
|
|
50
|
+
## 工作原理
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
`mcx uninstall` 会移除名为 MineCodex 的后台项目,并默认保留 MineCodex 设置、
|
|
47
|
-
Notes 数据和 Images 索引;只有显式传入 `--purge` 才会删除 MineCodex 自有数据。
|
|
48
|
-
MineCodex 永远不会删除 `~/.codex/generated_images` 中的原图或 Notes 引用的源文件。
|
|
52
|
+
MineCodex 统一管理 Codex 内的菜单入口、主题风格与界面适配。每个插件(Model Slider、Notes、Images)独立管理各自的数据与操作页面;Hide Upsell Banner 专注于界面展示的优化,不影响实际的用量计算。所有功能按需开启,关闭后即停止运行。
|
|
49
53
|
|
|
50
54
|
## 开发
|
|
51
55
|
|
|
@@ -57,9 +61,6 @@ npm run pack:check
|
|
|
57
61
|
npm run pack:audit
|
|
58
62
|
```
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
Codex 中的入口、主题与交互适配。
|
|
64
|
+
## 许可
|
|
62
65
|
|
|
63
|
-
MineCodex
|
|
64
|
-
第三方图标、品牌和兼容资产遵循各自的权利说明,见
|
|
65
|
-
[`features/model-slider/THIRD_PARTY_NOTICES.md`](features/model-slider/THIRD_PARTY_NOTICES.md)。
|
|
66
|
+
MineCodex 是非官方独立扩展项目,与 OpenAI 无关联。项目自有代码以 MIT License 授权;第三方图标、品牌和兼容资产遵循各自的权利说明,见 [`features/model-slider/THIRD_PARTY_NOTICES.md`](features/model-slider/THIRD_PARTY_NOTICES.md)。
|
|
@@ -8,6 +8,20 @@ licensed under the MIT License. Product names and marks remain the property of t
|
|
|
8
8
|
|
|
9
9
|
The upstream license text is preserved in `vendor/cc-switch-LICENSE`.
|
|
10
10
|
|
|
11
|
+
## Zhipu official favicon
|
|
12
|
+
|
|
13
|
+
`assets/providers/zhipu.svg` wraps the official Z.ai (Zhipu) favicon
|
|
14
|
+
downloaded from <https://www.zhipuai.cn/favicon.png> in an SVG container.
|
|
15
|
+
The favicon and Z.ai marks remain the property of Zhipu AI; the wrapper is
|
|
16
|
+
distributed under MineCodex's MIT License.
|
|
17
|
+
|
|
18
|
+
## Simple Icons (Xiaomi)
|
|
19
|
+
|
|
20
|
+
`assets/providers/xiaomimimo.svg` uses the Xiaomi mark from
|
|
21
|
+
[`simple-icons/simple-icons`](https://github.com/simple-icons/simple-icons)
|
|
22
|
+
(`icons/xiaomi.svg`), licensed under CC0 1.0. The Xiaomi brand mark remains
|
|
23
|
+
the property of Xiaomi Corporation.
|
|
24
|
+
|
|
11
25
|
## Lucide
|
|
12
26
|
|
|
13
27
|
Lucide icons distributed by MineCodex include the Model Slider UI icons, Notes action and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg
|
|
1
|
+
<svg role="img" viewBox="0 0 24 24" fill="#FF6900" xmlns="http://www.w3.org/2000/svg"><title>Xiaomi</title><path d="M12 0C8.016 0 4.756.255 2.493 2.516.23 4.776 0 8.033 0 12.012c0 3.98.23 7.235 2.494 9.497C4.757 23.77 8.017 24 12 24c3.983 0 7.243-.23 9.506-2.491C23.77 19.247 24 15.99 24 12.012c0-3.984-.233-7.243-2.502-9.504C19.234.252 15.978 0 12 0zM4.906 7.405h5.624c1.47 0 3.007.068 3.764.827.746.746.827 2.233.83 3.676v4.54a.15.15 0 0 1-.152.147h-1.947a.15.15 0 0 1-.152-.148V11.83c-.002-.806-.048-1.634-.464-2.051-.358-.36-1.026-.441-1.72-.458H7.158a.15.15 0 0 0-.151.147v6.98a.15.15 0 0 1-.152.148H4.906a.15.15 0 0 1-.15-.148V7.554a.15.15 0 0 1 .15-.149zm12.131 0h1.949a.15.15 0 0 1 .15.15v8.892a.15.15 0 0 1-.15.148h-1.949a.15.15 0 0 1-.151-.148V7.554a.15.15 0 0 1 .151-.149zM8.92 10.948h2.046c.083 0 .15.066.15.147v5.352a.15.15 0 0 1-.15.148H8.92a.15.15 0 0 1-.152-.148v-5.352a.15.15 0 0 1 .152-.147Z" fill="#FF6900"/></svg>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0
|
|
1
|
+
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 96 96" width="1em" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>Zhipu</title><image href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAOgSURBVHgB7Z3LcRpBEIYb2QcfCWGVARkIIhAZQAaQAUsE4AggAysDUAZkAI6Avfq0noaljGERjxn6MdNf1V/S6qLZ/ufRM10MDbhO06nr9ObUrp6bYFxi5bRx+nD6rH5/CAzyyGnrVJoe1swpgzvpgQU+pNZVTG9iBMDe4Fg1Agu+XBP6ghoZu3pwQgb7eUpD42MQrq+7TPJbZcAU9immQcMPpz9Oywb86/0GLYXT6wtYz+dit8FFA97B4OINDcjA4KKNa0AJBheFGcDMCxismAHMmAHMmAHMmAHMfAdPms3mTpwURbHTMZTt2mw24IPXyV6/3y+5abfbZ+1ar9clBbPZzPdkVLcBeZ6ftWk0GpUUoMlZlqVrAAbgtD0YECparZZv8EvVi3Cn0/nvGef8xWIBFIzHY1itVhAClSOgbuqZTCYlBXUjz0P6DHC9nK0d2+02xLyv14C6hQ+fqbKe4XAYMvi6DMCeX9f78O8UBEg5z+R9HM294XEpJ7j1ACj+Ny76vpuuOkqtokw5e73es95DXmBvFdW8P51On/kesoJ6qyh3u09+F1mBvUV49kNF4JRTvwGUKWfdZi95A+bzeUlB3WYveQMGg0FJQaBTzrgMwIDgMQAFT0w59RqgqMASnwHKCixxGaCtwBKVAe6MKbaUU5cBVCknwW5XnwGKCyz6DVBeYNFvgOYCi3oDIk85ZRsQSYFFrwGRFFh0GhBRgUWfAd1ut6RCyLwvx4AICyy6DIiwwKLHgEgLLDoMiLjAosOAiAss8g1IcLcrx4AECixyDUikwCLXgEQKLDINSKjAIs+AxAos8gxIrMAiywBLORkNSLTAIseARAssMgxIuMDCb0DiBZa75H1f0CkuIDCZTIACvK+h7mOjVB+drbun6BGCOspZYMHRQJXyIri5DBCzcMHnLLBQbvYOiDKAu8BCNfKOEWUAZ4GFauSdIsYAzt0u5cg7RYQB3AUW6nn/mBAGeF1Zxn1FGN6Ugmmvdh52j7PAgqOBmxAjwOu+IKre53Mx6zPu9wmJfX8AM3Z3NDNmADNmADNmADNmADNogP+BtvEwZgAvKzTgEwwufqMBSzC4+IU7YdzPr6ufBi2vhzXgJxjUzJ02hwfs/fg126WJRGuo+RbbvsCGxqoeXCAX1MhYlcMVcgGNTDb4B/qwn6e0vJh0bauY3kUG+5Va04tKDHwOX6T4DbhO5tR2eq9+b4FxiaLSEvYnDB9w5ajnLzy3z1aOtKIfAAAAAElFTkSuQmCC" x="0" y="0" width="96" height="96" preserveAspectRatio="xMidYMid meet"/></svg>
|
package/package.json
CHANGED
|
@@ -439,6 +439,20 @@ export function createInjectionSource(features, {
|
|
|
439
439
|
}) ?? null;
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
+
function nativeSidePanelButton() {
|
|
443
|
+
return Array.from(document.querySelectorAll("button")).find((button) => {
|
|
444
|
+
if (button.getAttribute("aria-label") !== "Toggle side panel") return false;
|
|
445
|
+
return isVisibleToolbarControl(button);
|
|
446
|
+
}) ?? null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function nativeTemporaryChatButton() {
|
|
450
|
+
return Array.from(document.querySelectorAll("button")).find((button) => {
|
|
451
|
+
if (!/temporary chat/i.test(button.getAttribute("aria-label") ?? "")) return false;
|
|
452
|
+
return isVisibleToolbarControl(button);
|
|
453
|
+
}) ?? null;
|
|
454
|
+
}
|
|
455
|
+
|
|
442
456
|
function toolbarControlRoot(button) {
|
|
443
457
|
return button?.parentElement?.parentElement ?? button?.parentElement ?? button ?? null;
|
|
444
458
|
}
|
|
@@ -463,12 +477,28 @@ export function createInjectionSource(features, {
|
|
|
463
477
|
if (!group) {
|
|
464
478
|
group = document.createElement("div");
|
|
465
479
|
group.setAttribute("data-codex-personal-summary-toolbar-group", "fallback");
|
|
466
|
-
|
|
480
|
+
// The host toolbar strip is pointer-events:none; without this the
|
|
481
|
+
// fallback entries would be visible but unclickable.
|
|
482
|
+
group.className = "ms-auto flex shrink-0 items-center gap-1.5 pointer-events-auto";
|
|
483
|
+
// Same structure as the native tool group (e.g. the temporary-chat
|
|
484
|
+
// group in Chat mode): an in-flow ms-auto strip inside the flexible
|
|
485
|
+
// header area. It sits at the right edge while the side panel is
|
|
486
|
+
// closed and, when the side panel widens its host container, moves
|
|
487
|
+
// together with the native layout exactly like Chat mode does.
|
|
467
488
|
toolbarHost.append(group);
|
|
468
489
|
}
|
|
469
490
|
return group;
|
|
470
491
|
}
|
|
471
492
|
|
|
493
|
+
function toolbarEntryFitsHeader(entryRoot) {
|
|
494
|
+
const header = entryRoot.closest("header");
|
|
495
|
+
if (!header) return true;
|
|
496
|
+
const headerRect = header.getBoundingClientRect();
|
|
497
|
+
const entryRect = entryRoot.getBoundingClientRect();
|
|
498
|
+
if (entryRect.width <= 0 || entryRect.height <= 0) return false;
|
|
499
|
+
return entryRect.right <= headerRect.right + 1 && entryRect.left >= headerRect.left - 1;
|
|
500
|
+
}
|
|
501
|
+
|
|
472
502
|
function isNativeSummaryButton(element) {
|
|
473
503
|
if (!(element instanceof Element)) return false;
|
|
474
504
|
const button = element.closest("button");
|
|
@@ -613,10 +643,42 @@ export function createInjectionSource(features, {
|
|
|
613
643
|
}
|
|
614
644
|
}
|
|
615
645
|
|
|
646
|
+
function nativeCssModuleClass(tokenName, root = document) {
|
|
647
|
+
const pattern = new RegExp(`^_${tokenName}_[A-Za-z0-9_-]+$`);
|
|
648
|
+
const live = Array.from(root.querySelectorAll(`[class*="_${tokenName}_"]`))
|
|
649
|
+
.flatMap((element) => Array.from(element.classList))
|
|
650
|
+
.find((name) => pattern.test(name));
|
|
651
|
+
if (live) return live;
|
|
652
|
+
const selectorPattern = new RegExp(`\\.(_${tokenName}_[A-Za-z0-9_-]+)`);
|
|
653
|
+
const find = (rules) => {
|
|
654
|
+
for (const rule of rules ?? []) {
|
|
655
|
+
const match = String(rule.selectorText ?? "").match(selectorPattern);
|
|
656
|
+
if (match?.[1]) return match[1];
|
|
657
|
+
try {
|
|
658
|
+
const nested = find(rule.cssRules);
|
|
659
|
+
if (nested) return nested;
|
|
660
|
+
} catch {
|
|
661
|
+
// Ignore inaccessible nested rules and continue with Codex's readable sheets.
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return null;
|
|
665
|
+
};
|
|
666
|
+
for (const sheet of document.styleSheets) {
|
|
667
|
+
try {
|
|
668
|
+
const match = find(sheet.cssRules);
|
|
669
|
+
if (match) return match;
|
|
670
|
+
} catch {
|
|
671
|
+
// Ignore cross-origin stylesheets.
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
|
|
616
677
|
function createNativeFastIcon(active) {
|
|
617
678
|
const definition = modelSelectorNativeFastIcons?.[active ? "active" : "inactive"];
|
|
618
679
|
const icon = createModelSelectorIcon(definition, { brand: true });
|
|
619
|
-
|
|
680
|
+
const iconClass = nativeCssModuleClass("FastModeIcon");
|
|
681
|
+
if (icon && iconClass) icon.classList.add(iconClass);
|
|
620
682
|
return icon;
|
|
621
683
|
}
|
|
622
684
|
|
|
@@ -687,10 +749,52 @@ export function createInjectionSource(features, {
|
|
|
687
749
|
for (let depth = 0; fiber && depth < 40; depth += 1, fiber = fiber.return) {
|
|
688
750
|
const props = fiber.memoizedProps;
|
|
689
751
|
if (Array.isArray(props?.models) && typeof props.onSelectReasoningEffort === "function") return props;
|
|
752
|
+
if (
|
|
753
|
+
props?.models
|
|
754
|
+
&& typeof props.models === "object"
|
|
755
|
+
&& !Array.isArray(props.models)
|
|
756
|
+
&& typeof props.onModelChange === "function"
|
|
757
|
+
&& props.selectedModel
|
|
758
|
+
) {
|
|
759
|
+
return chatComposerModelController(props);
|
|
760
|
+
}
|
|
690
761
|
}
|
|
691
762
|
return null;
|
|
692
763
|
}
|
|
693
764
|
|
|
765
|
+
function chatComposerModelController(props) {
|
|
766
|
+
const selected = props.selectedModel ?? {};
|
|
767
|
+
if (!Array.isArray(props.models?.versionOptions) || !selected.versionId) return null;
|
|
768
|
+
const version = props.models.versionOptions.find(
|
|
769
|
+
(candidate) => String(candidate.id ?? "") === String(selected.versionId ?? ""),
|
|
770
|
+
);
|
|
771
|
+
if (!version) return null;
|
|
772
|
+
const options = Array.isArray(version?.options) ? version.options : [];
|
|
773
|
+
const effortFor = (option) => String(option?.title ?? option?.lane ?? "").toLowerCase();
|
|
774
|
+
const efforts = options.map(effortFor).filter(Boolean);
|
|
775
|
+
const selectedOption = options.find(
|
|
776
|
+
(option) => String(option.thinkingEffort ?? "") === String(selected.thinkingEffort ?? ""),
|
|
777
|
+
);
|
|
778
|
+
const selectedEffort = selectedOption ? effortFor(selectedOption) : (efforts[0] ?? "instant");
|
|
779
|
+
const models = [{
|
|
780
|
+
model: String(selected.slug ?? ""),
|
|
781
|
+
displayName: String(version?.label ?? selected.slug ?? ""),
|
|
782
|
+
defaultReasoningEffort: selectedEffort,
|
|
783
|
+
supportedReasoningEfforts: efforts.map((effort) => ({ reasoningEffort: effort })),
|
|
784
|
+
}];
|
|
785
|
+
return {
|
|
786
|
+
models,
|
|
787
|
+
onSelectReasoningEffort(effort) {
|
|
788
|
+
const target = options.find((option) => effortFor(option) === String(effort).toLowerCase());
|
|
789
|
+
if (!target) return;
|
|
790
|
+
const next = { ...selected, slug: String(target.slug ?? selected.slug ?? "") };
|
|
791
|
+
if (target.thinkingEffort) next.thinkingEffort = target.thinkingEffort;
|
|
792
|
+
else delete next.thinkingEffort;
|
|
793
|
+
props.onModelChange(next);
|
|
794
|
+
},
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
694
798
|
function nativeCatalogModelFor(identity) {
|
|
695
799
|
const models = nativeComposerModelController()?.models ?? [];
|
|
696
800
|
const exact = models.find((model) => String(model.model ?? "").toLowerCase() === identity.raw.toLowerCase());
|
|
@@ -1106,11 +1210,11 @@ export function createInjectionSource(features, {
|
|
|
1106
1210
|
const observedIdentity = modelIdentity(observedRaw);
|
|
1107
1211
|
const nativeDescriptor = nativeModelDescriptorForItem(item, observedRaw);
|
|
1108
1212
|
const catalogModel = catalogModelFor(observedIdentity, selector);
|
|
1109
|
-
const raw =
|
|
1213
|
+
const raw = catalogModel?.slug ?? nativeDescriptor?.slug ?? observedRaw;
|
|
1110
1214
|
item.dataset.codexModelSliderRaw = raw;
|
|
1111
1215
|
item.dataset.codexModelSliderItem = "";
|
|
1112
1216
|
item.dataset.codexModelSliderOrder = String(index);
|
|
1113
|
-
item.dataset.codexModelSliderIdentitySource =
|
|
1217
|
+
item.dataset.codexModelSliderIdentitySource = catalogModel ? "catalog" : nativeDescriptor ? "native" : "visible";
|
|
1114
1218
|
if (item.querySelector("[data-codex-model-slider-row]")) return;
|
|
1115
1219
|
|
|
1116
1220
|
const identity = modelIdentity(raw);
|
|
@@ -1397,12 +1501,17 @@ export function createInjectionSource(features, {
|
|
|
1397
1501
|
if (!track) return;
|
|
1398
1502
|
track.querySelectorAll('[class*="_FastParticleClip_"]').forEach((element) => element.remove());
|
|
1399
1503
|
if (!enabled || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
1504
|
+
const clipClass = nativeCssModuleClass("FastParticleClip");
|
|
1505
|
+
const particlesClass = nativeCssModuleClass("FastTrackParticles");
|
|
1506
|
+
const pathClass = nativeCssModuleClass("FastTrackParticlePath");
|
|
1507
|
+
const particleClass = nativeCssModuleClass("TrackParticle");
|
|
1508
|
+
if (!clipClass || !particlesClass || !pathClass || !particleClass) return;
|
|
1400
1509
|
const clip = document.createElement("span");
|
|
1401
|
-
clip.className =
|
|
1510
|
+
clip.className = clipClass;
|
|
1402
1511
|
clip.setAttribute("aria-hidden", "true");
|
|
1403
1512
|
clip.style.opacity = "1";
|
|
1404
1513
|
const particles = document.createElement("span");
|
|
1405
|
-
particles.className =
|
|
1514
|
+
particles.className = particlesClass;
|
|
1406
1515
|
particles.setAttribute("aria-hidden", "true");
|
|
1407
1516
|
particles.setAttribute("data-animation-active", "true");
|
|
1408
1517
|
const travelDuration = 1.9;
|
|
@@ -1415,12 +1524,12 @@ export function createInjectionSource(features, {
|
|
|
1415
1524
|
const speed = 1 + (nativeParticleSeed(index, 21) - 0.5) * 0.4;
|
|
1416
1525
|
const duration = travelDuration / speed;
|
|
1417
1526
|
const path = document.createElement("span");
|
|
1418
|
-
path.className =
|
|
1527
|
+
path.className = pathClass;
|
|
1419
1528
|
path.style.animationDelay = `${index * stagger - duration * remainingTravel}s`;
|
|
1420
1529
|
path.style.animationDuration = `${duration}s`;
|
|
1421
1530
|
path.style.top = `${12 + nativeParticleSeed(index, 23) * 76}%`;
|
|
1422
1531
|
const particle = document.createElement("span");
|
|
1423
|
-
particle.className =
|
|
1532
|
+
particle.className = particleClass;
|
|
1424
1533
|
particle.style.opacity = String(0.4 + nativeParticleSeed(index, 11) * 0.6);
|
|
1425
1534
|
particle.style.transform = `translate(-50%, -50%) scale(${0.5 + nativeParticleSeed(index, 12) * 0.45})`;
|
|
1426
1535
|
path.append(particle);
|
|
@@ -1509,26 +1618,37 @@ export function createInjectionSource(features, {
|
|
|
1509
1618
|
function createGenericModelSlider(parentMenu, selector, identity, model, selectedEffort) {
|
|
1510
1619
|
const efforts = model.supportedReasoningLevels.map((level) => level.effort);
|
|
1511
1620
|
const selectedIndex = Math.max(0, efforts.indexOf(selectedEffort));
|
|
1621
|
+
const classes = Object.fromEntries([
|
|
1622
|
+
"Menu",
|
|
1623
|
+
"ViewToggle",
|
|
1624
|
+
"ViewToggleContent",
|
|
1625
|
+
"ViewToggleIcon",
|
|
1626
|
+
"ViewControls",
|
|
1627
|
+
"FastModeToggle",
|
|
1628
|
+
"FastModeToggleContent",
|
|
1629
|
+
"SimpleView",
|
|
1630
|
+
].map((tokenName) => [tokenName, nativeCssModuleClass(tokenName, parentMenu)]));
|
|
1631
|
+
if (Object.values(classes).some((name) => !name)) return null;
|
|
1512
1632
|
const shell = document.createElement("div");
|
|
1513
1633
|
shell.setAttribute("data-codex-model-slider-generic", `${identity.raw}:${selectedEffort}`);
|
|
1514
|
-
shell.className =
|
|
1634
|
+
shell.className = classes.Menu;
|
|
1515
1635
|
shell.setAttribute("data-view", "simple");
|
|
1516
1636
|
shell.setAttribute("data-reduced-motion", String(matchMedia("(prefers-reduced-motion: reduce)").matches));
|
|
1517
1637
|
shell.setAttribute("data-transitions-ready", "false");
|
|
1518
1638
|
const nativeRow = nativeModelPickerRow(parentMenu);
|
|
1519
1639
|
const modelButton = document.createElement("div");
|
|
1520
|
-
modelButton.className =
|
|
1640
|
+
modelButton.className = classes.ViewToggle;
|
|
1521
1641
|
modelButton.setAttribute("role", "button");
|
|
1522
1642
|
modelButton.setAttribute("tabindex", "0");
|
|
1523
1643
|
modelButton.setAttribute("data-codex-model-slider-model-button", "");
|
|
1524
1644
|
modelButton.setAttribute("aria-label", `Model ${modelDisplayLabel(identity, selector)}`);
|
|
1525
1645
|
const buttonContent = document.createElement("span");
|
|
1526
|
-
buttonContent.className =
|
|
1646
|
+
buttonContent.className = classes.ViewToggleContent;
|
|
1527
1647
|
const buttonLabel = document.createElement("span");
|
|
1528
1648
|
buttonLabel.textContent = modelDisplayLabel(identity, selector);
|
|
1529
1649
|
const nativeChevron = nativeRow?.querySelector("svg")?.cloneNode(true);
|
|
1530
1650
|
if (nativeChevron) {
|
|
1531
|
-
nativeChevron.className.baseVal =
|
|
1651
|
+
nativeChevron.className.baseVal = classes.ViewToggleIcon;
|
|
1532
1652
|
nativeChevron.setAttribute("aria-hidden", "true");
|
|
1533
1653
|
buttonContent.append(buttonLabel, nativeChevron);
|
|
1534
1654
|
} else {
|
|
@@ -1544,7 +1664,7 @@ export function createInjectionSource(features, {
|
|
|
1544
1664
|
});
|
|
1545
1665
|
|
|
1546
1666
|
const viewControls = document.createElement("div");
|
|
1547
|
-
viewControls.className =
|
|
1667
|
+
viewControls.className = classes.ViewControls;
|
|
1548
1668
|
viewControls.setAttribute("data-expanded", "false");
|
|
1549
1669
|
viewControls.setAttribute("data-ultra-warning-visible", "false");
|
|
1550
1670
|
const controller = nativeComposerModelController();
|
|
@@ -1560,7 +1680,7 @@ export function createInjectionSource(features, {
|
|
|
1560
1680
|
if (supportsFast) {
|
|
1561
1681
|
const fastButton = document.createElement("button");
|
|
1562
1682
|
fastButton.type = "button";
|
|
1563
|
-
fastButton.className =
|
|
1683
|
+
fastButton.className = classes.FastModeToggle;
|
|
1564
1684
|
fastButton.setAttribute("data-codex-model-slider-fast", "");
|
|
1565
1685
|
fastButton.setAttribute("aria-pressed", String(fastEnabled));
|
|
1566
1686
|
fastButton.setAttribute("data-fast-mode-enabled", String(fastEnabled));
|
|
@@ -1571,7 +1691,7 @@ export function createInjectionSource(features, {
|
|
|
1571
1691
|
: (locale === "zh-CN" ? "开启 Fast 模式" : "Enable Fast mode"));
|
|
1572
1692
|
const fastIcon = createNativeFastIcon(fastEnabled);
|
|
1573
1693
|
const fastContent = document.createElement("span");
|
|
1574
|
-
fastContent.className =
|
|
1694
|
+
fastContent.className = classes.FastModeToggleContent;
|
|
1575
1695
|
fastContent.setAttribute("data-codex-model-slider-fast-content", "");
|
|
1576
1696
|
if (fastIcon) fastContent.append(fastIcon);
|
|
1577
1697
|
fastButton.append(fastContent);
|
|
@@ -1677,7 +1797,7 @@ export function createInjectionSource(features, {
|
|
|
1677
1797
|
selectNativeEffort(parentMenu, efforts[pendingIndex]);
|
|
1678
1798
|
});
|
|
1679
1799
|
const simpleView = document.createElement("div");
|
|
1680
|
-
simpleView.className =
|
|
1800
|
+
simpleView.className = classes.SimpleView;
|
|
1681
1801
|
simpleView.append(nativeSlider ?? rail);
|
|
1682
1802
|
shell.append(viewControls, simpleView);
|
|
1683
1803
|
return shell;
|
|
@@ -1715,12 +1835,13 @@ export function createInjectionSource(features, {
|
|
|
1715
1835
|
existing?.remove();
|
|
1716
1836
|
const nativeContainer = parentMenu.firstElementChild;
|
|
1717
1837
|
if (!nativeContainer) return false;
|
|
1838
|
+
const shell = createGenericModelSlider(parentMenu, selector, identity, model, selectedEffort);
|
|
1839
|
+
if (!shell) return false;
|
|
1718
1840
|
if (!modelSelectorGenericContainers.has(nativeContainer)) {
|
|
1719
1841
|
modelSelectorGenericContainers.set(nativeContainer, nativeContainer.style.display);
|
|
1720
1842
|
}
|
|
1721
1843
|
parentMenu.setAttribute("data-codex-model-slider-generic-menu", "");
|
|
1722
1844
|
nativeContainer.style.display = "none";
|
|
1723
|
-
const shell = createGenericModelSlider(parentMenu, selector, identity, model, selectedEffort);
|
|
1724
1845
|
shell.setAttribute("data-codex-model-slider-generic", key);
|
|
1725
1846
|
parentMenu.append(shell);
|
|
1726
1847
|
return true;
|
|
@@ -2056,7 +2177,10 @@ export function createInjectionSource(features, {
|
|
|
2056
2177
|
}
|
|
2057
2178
|
|
|
2058
2179
|
function currentComposer() {
|
|
2059
|
-
|
|
2180
|
+
const marked = document.querySelector('[data-codex-composer="true"][contenteditable="true"]');
|
|
2181
|
+
if (marked) return marked;
|
|
2182
|
+
return Array.from(document.querySelectorAll('[contenteditable="true"][role="textbox"]'))
|
|
2183
|
+
.find((element) => isVisibleToolbarControl(element)) ?? null;
|
|
2060
2184
|
}
|
|
2061
2185
|
|
|
2062
2186
|
function rememberComposerSelection() {
|
|
@@ -2379,9 +2503,16 @@ export function createInjectionSource(features, {
|
|
|
2379
2503
|
const entryRect = entry.getBoundingClientRect();
|
|
2380
2504
|
const scrollRect = mainContentViewport()?.querySelector(".thread-scroll-container")
|
|
2381
2505
|
?.getBoundingClientRect();
|
|
2506
|
+
// The native conversation header is a fixed overlay that starts above the
|
|
2507
|
+
// thread scroll container (scroll top is often 0 while the header still
|
|
2508
|
+
// occupies the top toolbar strip). Anchor below the header so the pinned
|
|
2509
|
+
// surface never covers the toolbar entry used to close it again.
|
|
2510
|
+
const header = entry.closest("header");
|
|
2511
|
+
const headerBottom = header?.getBoundingClientRect().bottom ?? 0;
|
|
2512
|
+
const contentTop = Math.max(scrollRect?.top ?? 0, headerBottom);
|
|
2382
2513
|
const top = displayMode === "overlay"
|
|
2383
2514
|
? entryRect.bottom + layout.popoverOffset
|
|
2384
|
-
: (
|
|
2515
|
+
: (contentTop || 46) + layout.verticalInset;
|
|
2385
2516
|
const right = displayMode === "overlay"
|
|
2386
2517
|
? Math.max(layout.popoverOffset, innerWidth - entryRect.right)
|
|
2387
2518
|
: Math.max(layout.panelInset, innerWidth - target.right + layout.panelInset);
|
|
@@ -3169,15 +3300,23 @@ export function createInjectionSource(features, {
|
|
|
3169
3300
|
|
|
3170
3301
|
function ensureToolbarEntries() {
|
|
3171
3302
|
if (!toolbarFeatures.length) return;
|
|
3172
|
-
const
|
|
3173
|
-
if (!bottomPanelButton) return;
|
|
3303
|
+
const temporaryChatButton = nativeTemporaryChatButton();
|
|
3174
3304
|
const summaryButton = nativeSummaryButton();
|
|
3175
|
-
const
|
|
3176
|
-
const
|
|
3177
|
-
const
|
|
3178
|
-
|
|
3305
|
+
const bottomPanelButton = nativeBottomPanelButton();
|
|
3306
|
+
const sidePanelButton = nativeSidePanelButton();
|
|
3307
|
+
const anchorButton = temporaryChatButton ?? summaryButton ?? bottomPanelButton ?? sidePanelButton;
|
|
3308
|
+
if (!anchorButton) return;
|
|
3309
|
+
const anchorRoot = toolbarControlRoot(anchorButton);
|
|
3310
|
+
// The side-panel toggle sits in a narrow fixed container that travels
|
|
3311
|
+
// with the native panel. Entries anchored to it must live in the
|
|
3312
|
+
// fallback toolbar group, otherwise the Notes entry would be dragged
|
|
3313
|
+
// into the side panel and hidden. The Notes entry is only grouped with
|
|
3314
|
+
// the native summary button when that button is the anchor.
|
|
3315
|
+
const useFallbackAnchor = anchorButton === sidePanelButton;
|
|
3316
|
+
const summaryGroup = useFallbackAnchor ? null : nativeSummaryToolbarGroup(anchorButton);
|
|
3317
|
+
let targetGroup = summaryGroup ?? ensureFallbackSummaryToolbarGroup(anchorButton);
|
|
3179
3318
|
if (!targetGroup) return;
|
|
3180
|
-
let cursor =
|
|
3319
|
+
let cursor = useFallbackAnchor ? null : anchorRoot;
|
|
3181
3320
|
for (const feature of toolbarFeatures) {
|
|
3182
3321
|
const entries = Array.from(document.querySelectorAll(`[${entryMarker}="${feature.id}"]`));
|
|
3183
3322
|
for (const duplicate of entries.slice(1)) {
|
|
@@ -3185,7 +3324,7 @@ export function createInjectionSource(features, {
|
|
|
3185
3324
|
}
|
|
3186
3325
|
const entry = entries[0]?.isConnected
|
|
3187
3326
|
? entries[0]
|
|
3188
|
-
: createToolbarEntry(feature,
|
|
3327
|
+
: createToolbarEntry(feature, anchorButton);
|
|
3189
3328
|
const entryRoot = entry?.closest("[data-codex-personal-toolbar-entry]") ?? entry;
|
|
3190
3329
|
if (!entryRoot) continue;
|
|
3191
3330
|
if (cursor) {
|
|
@@ -3193,10 +3332,24 @@ export function createInjectionSource(features, {
|
|
|
3193
3332
|
} else if (targetGroup.lastElementChild !== entryRoot) {
|
|
3194
3333
|
targetGroup.append(entryRoot);
|
|
3195
3334
|
}
|
|
3335
|
+
// Narrow native anchor containers (e.g. the Work-mode side-panel
|
|
3336
|
+
// button) can push the entry outside the header so it becomes
|
|
3337
|
+
// unreachable. Move it into the fallback toolbar group instead.
|
|
3338
|
+
if (summaryGroup && !toolbarEntryFitsHeader(entryRoot)) {
|
|
3339
|
+
const fallback = ensureFallbackSummaryToolbarGroup(anchorButton);
|
|
3340
|
+
if (fallback && fallback !== summaryGroup) {
|
|
3341
|
+
fallback.append(entryRoot);
|
|
3342
|
+
targetGroup = fallback;
|
|
3343
|
+
cursor = null;
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3196
3346
|
cursor = entryRoot;
|
|
3197
3347
|
}
|
|
3198
3348
|
if (summaryGroup) {
|
|
3199
|
-
document.querySelectorAll("[data-codex-personal-summary-toolbar-group]").forEach((group) =>
|
|
3349
|
+
document.querySelectorAll("[data-codex-personal-summary-toolbar-group]").forEach((group) => {
|
|
3350
|
+
const used = [...group.querySelectorAll(`[${entryMarker}]`)].some((entry) => entry.isConnected);
|
|
3351
|
+
if (!used) group.remove();
|
|
3352
|
+
});
|
|
3200
3353
|
}
|
|
3201
3354
|
}
|
|
3202
3355
|
|