minecodex 1.0.4 → 1.0.5

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.
@@ -9,6 +9,7 @@ Toolbar 入口,由 `CodexRuntimeHost` 在渲染进程内注入一段脚本。
9
9
  功能范围:
10
10
 
11
11
  - 仅隐藏横幅的 GUI 显示(`aside` 卡片,`display: none`);
12
+ - 只以 `aside` 和中英文额度耗尽标题识别,不依赖易变的卡片 class、升级、购买 Credit 或使用重置文案;
12
13
  - 不拦截、不篡改额度查询接口(`account/rateLimits/read` 与 app-server
13
14
  JSON-RPC 流量保持原样);
14
15
  - 横幅重新渲染时由 `MutationObserver` 持续处理,不会闪烁后残留;
@@ -23,5 +24,5 @@ npm test
23
24
  ## 适配说明
24
25
 
25
26
  这是非官方 CDP / DOM Adapter 的功能声明。Codex 私有 DOM 的适配实现统一留在
26
- `CodexRuntimeHost`;本功能只声明横幅的识别规则(卡片类名 + 文案特征)与
27
+ `CodexRuntimeHost`;本功能只声明横幅的识别规则(`aside` + 耗尽标题)与
27
28
  隐藏动作。
@@ -13,16 +13,15 @@ function isUpsellBanner(element) {
13
13
  if (!(element instanceof window.HTMLElement)) return false;
14
14
  const text = (element.innerText ?? "").trim();
15
15
  if (!text) return false;
16
- const isCard = element.classList.contains("rounded-2xl")
17
- && element.classList.contains("border")
18
- && (element.classList.contains("bg-token-main-surface-primary")
19
- || element.classList.contains("bg-surface"));
20
- if (!isCard) return false;
21
- const titleMatch = /out of Codex and Work usage/i.test(text)
22
- || /out of Codex messages/i.test(text)
23
- || /Codex message usage limit reached/i.test(text);
24
- const actionMatch = /Upgrade to Pro|Add Credits/i.test(text);
25
- return titleMatch && actionMatch;
16
+ const title = (
17
+ element.querySelector("h1, h2, h3, [role='heading']")?.innerText
18
+ ?? text.split("\n", 1)[0]
19
+ ).trim();
20
+ return /out of Codex(?: and Work)? usage/i.test(title)
21
+ || /out of Codex messages/i.test(title)
22
+ || /Codex (?:message )?usage limit reached/i.test(title)
23
+ || /Codex.*(?:用量|使用量|使用额度|额度|消息).*(?:用完|用尽|耗尽|已达|达到.*上限|上限)/i.test(title)
24
+ || /(?:已达到|达到|已用完|已用尽|耗尽).{0,20}Codex.{0,20}(?:用量|使用量|额度|消息|上限)/i.test(title);
26
25
  }
27
26
 
28
27
  function hideBanner(element) {
@@ -79,6 +79,7 @@ HTTP + Images UI
79
79
  - 支持 Codex 的明亮 / 暗黑主题,并监听根节点 theme class 与 style 变化。
80
80
  - 用户修改 accent、ink 或 surface 后,新的 resolved tokens 会同步到 iframe,不需要重新读取或猜测内部配置文件位置。
81
81
  - 侧边栏 Images 入口克隆原生 Plugins 控件的布局与状态,再替换为 Codex 图标体系中的独立 Image 图标;iframe 内不导入 Codex 私有 React 模块或 hashed CSS bundle,以免 Codex 更新后直接失效。
82
+ - 原生左侧栏隐藏时,Images 标题左侧显示同源图标入口;点击由 RuntimeHost 转发给原生侧栏开关,恢复后入口自动隐藏。
82
83
 
83
84
  取证、token Interface 和代码复用边界见 [`docs/CODEX_DESIGN_LANGUAGE.md`](docs/CODEX_DESIGN_LANGUAGE.md)。
84
85
 
@@ -16,7 +16,7 @@
16
16
  "pageScript": {
17
17
  "resource": "src/chatgpt-image-source-page.mjs"
18
18
  },
19
- "hostActions": ["import-generated-image", "create-image-edit-thread"],
19
+ "hostActions": ["import-generated-image", "create-image-edit-thread", "open-sidebar"],
20
20
  "placement": {
21
21
  "after": "sites",
22
22
  "order": 10
@@ -1,4 +1,6 @@
1
1
  const shell = document.querySelector(".library-shell");
2
+ const libraryHeader = document.querySelector(".library-header");
3
+ const sidebarOpenButton = document.querySelector(".sidebar-open-button");
2
4
  const scrollArea = document.querySelector(".library-scroll");
3
5
  const grid = document.querySelector(".image-grid");
4
6
  const pageSentinel = document.querySelector(".page-sentinel");
@@ -141,9 +143,26 @@ function announce(message) {
141
143
  });
142
144
  }
143
145
 
146
+ function applySidebarState(payload) {
147
+ const available = Boolean(payload?.available);
148
+ const open = Boolean(payload?.open);
149
+ const label = typeof payload?.label === "string" && payload.label.trim()
150
+ ? payload.label.trim()
151
+ : "Show sidebar";
152
+ const visible = available && !open;
153
+ const leading = Number(payload?.leading);
154
+ sidebarOpenButton.hidden = !visible;
155
+ sidebarOpenButton.setAttribute("aria-label", label);
156
+ sidebarOpenButton.title = label;
157
+ libraryHeader.style.paddingLeft = visible && Number.isFinite(leading)
158
+ ? `${Math.max(8, leading)}px`
159
+ : "";
160
+ }
161
+
144
162
  window.addEventListener("message", (event) => {
145
163
  if (event.source !== window.parent) return;
146
164
  if (event.data?.type === "codex-personal:theme") applyHostTheme(event.data);
165
+ if (event.data?.type === "codex-personal:sidebar-state") applySidebarState(event.data);
147
166
  if (["codex-personal:surface-active", "codex-personal:surface-visibility"].includes(event.data?.type)) {
148
167
  setHostSurfaceActive(event.data.active ?? event.data.visible);
149
168
  }
@@ -974,6 +993,17 @@ densitySlider.addEventListener("input", () => {
974
993
  });
975
994
 
976
995
  groupButton.addEventListener("click", () => toggleThreadGrouping().catch(console.error));
996
+ sidebarOpenButton.addEventListener("click", async () => {
997
+ sidebarOpenButton.disabled = true;
998
+ try {
999
+ await requestHostAction("open-sidebar", {});
1000
+ applySidebarState({ available: true, open: true, label: sidebarOpenButton.title });
1001
+ } catch (error) {
1002
+ announce(error.message);
1003
+ } finally {
1004
+ sidebarOpenButton.disabled = false;
1005
+ }
1006
+ });
977
1007
 
978
1008
  document.addEventListener("click", (event) => {
979
1009
  if (!event.target.closest(".source-filter")) closeToolbarPopups();
@@ -11,6 +11,11 @@
11
11
  <section class="library-pane">
12
12
  <header class="library-header">
13
13
  <div class="identity">
14
+ <button class="icon-button sidebar-open-button" type="button" aria-label="Show sidebar" title="Show sidebar" hidden>
15
+ <svg class="sidebar-toggle-icon" viewBox="0 0 20 20" aria-hidden="true">
16
+ <path d="M6.83496 3.99992C6.38353 4.00411 6.01421 4.0122 5.69824 4.03801C5.31232 4.06954 5.03904 4.12266 4.82227 4.20012L4.62207 4.28606C4.18264 4.50996 3.81498 4.85035 3.55859 5.26848L3.45605 5.45207C3.33013 5.69922 3.25006 6.01354 3.20801 6.52824C3.16533 7.05065 3.16504 7.71885 3.16504 8.66301V11.3271C3.16504 12.2712 3.16533 12.9394 3.20801 13.4618C3.25006 13.9766 3.33013 14.2909 3.45605 14.538L3.55859 14.7216C3.81498 15.1397 4.18266 15.4801 4.62207 15.704L4.82227 15.79C5.03904 15.8674 5.31234 15.9205 5.69824 15.9521C6.01398 15.9779 6.383 15.986 6.83398 15.9902L6.83496 3.99992ZM18.165 11.3271C18.165 12.2493 18.1653 12.9811 18.1172 13.5702C18.0745 14.0924 17.9916 14.5472 17.8125 14.9648L17.7295 15.1415C17.394 15.8 16.8834 16.3511 16.2568 16.7353L15.9814 16.8896C15.5157 17.1268 15.0069 17.2285 14.4102 17.2773C13.821 17.3254 13.0893 17.3251 12.167 17.3251H7.83301C6.91071 17.3251 6.17898 17.3254 5.58984 17.2773C5.06757 17.2346 4.61294 17.1508 4.19531 16.9716L4.01855 16.8896C3.36014 16.5541 2.80898 16.0434 2.4248 15.4169L2.27051 15.1415C2.03328 14.6758 1.93158 14.167 1.88281 13.5702C1.83468 12.9811 1.83496 12.2493 1.83496 11.3271V8.66301C1.83496 7.74072 1.83468 7.00898 1.88281 6.41985C1.93157 5.82309 2.03329 5.31432 2.27051 4.84856L2.4248 4.57317C2.80898 3.94666 3.36012 3.436 4.01855 3.10051L4.19531 3.0175C4.61285 2.83843 5.06771 2.75548 5.58984 2.71281C6.17898 2.66468 6.91071 2.66496 7.83301 2.66496H12.167C13.0893 2.66496 13.821 2.66468 14.4102 2.71281C15.0069 2.76157 15.5157 2.86329 15.9814 3.10051L16.2568 3.25481C16.8833 3.63898 17.394 4.19012 17.7295 4.84856L17.8125 5.02531C17.9916 5.44285 18.0745 5.89771 18.1172 6.41985C18.1653 7.00898 18.165 7.74072 18.165 8.66301V11.3271ZM8.16406 15.995H12.167C13.1112 15.995 13.7794 15.9947 14.3018 15.9521C14.8164 15.91 15.1308 15.8299 15.3779 15.704L15.5615 15.6015C15.9797 15.3451 16.32 14.9774 16.5439 14.538L16.6299 14.3378C16.7074 14.121 16.7605 13.8478 16.792 13.4618C16.8347 12.9394 16.835 12.2712 16.835 11.3271V8.66301C16.835 7.71885 16.8347 7.05065 16.792 6.52824C16.7605 6.14232 16.7073 5.86904 16.6299 5.65227L16.5439 5.45207C16.32 5.01264 15.9796 4.64498 15.5615 4.3886L15.3779 4.28606C15.1308 4.16013 14.8165 4.08006 14.3018 4.03801C13.7794 3.99533 13.1112 3.99504 12.167 3.99504H8.16406C8.16407 3.99667 8.16504 3.99829 8.16504 3.99992L8.16406 15.995Z"></path>
17
+ </svg>
18
+ </button>
14
19
  <h1>Images</h1>
15
20
  </div>
16
21
  <button class="icon-button rescan-button" type="button" aria-label="Refresh images" title="Refresh images">
@@ -240,6 +240,15 @@ button { color: inherit; }
240
240
  gap: 7px;
241
241
  }
242
242
 
243
+ .sidebar-open-button { flex: 0 0 auto; }
244
+ .sidebar-open-button[hidden] { display: none; }
245
+
246
+ .sidebar-toggle-icon {
247
+ width: 16px;
248
+ height: 16px;
249
+ fill: currentColor;
250
+ }
251
+
243
252
  h1,
244
253
  h2,
245
254
  h3,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -144,6 +144,13 @@ export function createInjectionSource(features, {
144
144
  scheduled: ["scheduled", "计划任务"],
145
145
  plugins: ["plugins", "插件"],
146
146
  };
147
+ const nativeToolbarLabels = {
148
+ summary: ["Toggle summary", "切换摘要"],
149
+ pinnedSummary: ["Toggle pinned summary", "切换固定摘要"],
150
+ bottomPanel: ["Toggle bottom panel", "切换底部面板"],
151
+ sidePanel: ["Toggle side panel", "切换侧边面板"],
152
+ temporaryChat: ["temporary chat", "临时聊天", "临时对话"],
153
+ };
147
154
  const themeTokenNames = [
148
155
  "--codex-base-accent",
149
156
  "--codex-base-ink",
@@ -170,6 +177,7 @@ export function createInjectionSource(features, {
170
177
  "--color-background-elevated-primary-opaque",
171
178
  "--color-background-primary-ghost-hover",
172
179
  "--color-chart-blue",
180
+ "--color-chart-purple",
173
181
  "--color-background-panel",
174
182
  "--color-background-control",
175
183
  "--color-background-primary-solid",
@@ -328,9 +336,11 @@ export function createInjectionSource(features, {
328
336
  [data-codex-model-slider-menu] { width: 264px !important; min-width: 264px; overflow-x: hidden; }
329
337
  [data-codex-model-slider-controller-menu] {
330
338
  position: absolute !important;
331
- right: 0;
339
+ left: 50%;
332
340
  bottom: 0;
341
+ transform: translateX(-50%) !important;
333
342
  z-index: 10;
343
+ padding: 6px;
334
344
  background-color: var(--color-surface-elevated-secondary);
335
345
  -webkit-backdrop-filter: none;
336
346
  backdrop-filter: none;
@@ -340,10 +350,10 @@ export function createInjectionSource(features, {
340
350
  overflow-y: auto;
341
351
  }
342
352
  [data-codex-model-slider-trigger] [class*="_ModelPickerTriggerModelLabel_"] {
343
- max-width: none; flex-shrink: 0; overflow: visible; text-overflow: clip;
353
+ min-width: 0; max-width: 110px; flex-shrink: 1; overflow: hidden; text-overflow: ellipsis;
344
354
  }
345
355
  [data-codex-model-slider-trigger] [class*="_ModelPickerTriggerModelText_"] {
346
- overflow: visible; text-overflow: clip;
356
+ min-width: 0; max-width: 110px; overflow: hidden; text-overflow: ellipsis;
347
357
  }
348
358
  html[data-codex-model-slider-catalog-ready]
349
359
  [data-codex-intelligence-trigger]
@@ -351,11 +361,37 @@ export function createInjectionSource(features, {
351
361
  display: none !important;
352
362
  }
353
363
  [data-codex-model-slider-trigger-label] {
354
- display: inline; overflow: visible; text-overflow: clip; white-space: nowrap;
364
+ display: inline-block; min-width: 0; max-width: 110px;
365
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
366
+ }
367
+ /* 展示真实模型名/思考强度时:芯片 HUG 内容,长名称不被截断;
368
+ 仅 "Select model / Select effort" 占位态沿用原生宽度。 */
369
+ [data-codex-model-slider-trigger]:not([data-codex-model-slider-trigger-placeholder]) [data-codex-model-slider-trigger-label] {
370
+ max-width: none; overflow: visible; text-overflow: clip; flex-shrink: 0;
371
+ }
372
+ [data-codex-model-slider-trigger]:not([data-codex-model-slider-trigger-placeholder]) [class*="_ModelPickerTriggerModelLabel_"] {
373
+ max-width: none; overflow: visible; flex-shrink: 0;
374
+ }
375
+ [data-codex-model-slider-trigger]:not([data-codex-model-slider-trigger-placeholder]) [class*="_ModelPickerTriggerContent_"] {
376
+ max-width: none !important;
377
+ }
378
+ [data-codex-model-slider-trigger][data-codex-model-slider-trigger-placeholder] [class*="_ModelPickerTriggerContent_"] {
379
+ width: 160px; min-width: 160px !important; max-width: 160px;
355
380
  }
356
381
  [data-codex-model-slider-trigger] [class*="_ModelPickerTriggerEffortLabel_"] {
357
382
  display: inline-flex; align-items: center; align-self: center;
358
383
  }
384
+ /* 无 Fast 模式时,模型按钮 wrapper 居中,不再按有 Fast 的左对齐策略排布。 */
385
+ [data-codex-model-slider-has-fast="false"] [data-codex-model-slider-model-button] {
386
+ box-sizing: border-box; width: 100%; max-width: 100%; margin-inline: auto;
387
+ flex: 0 1 100%; justify-content: center;
388
+ }
389
+ [data-codex-model-slider-has-fast="false"] [data-codex-model-slider-model-button] > span {
390
+ width: 100%; min-width: 0;
391
+ }
392
+ [data-codex-model-slider-has-fast="false"] > :has(> [data-codex-model-slider-model-button])::before {
393
+ display: none !important; content: none !important;
394
+ }
359
395
  html[data-codex-model-slider-selecting-effort] [role="menu"][data-state="open"]:not(:has([data-reasoning-slider])) {
360
396
  visibility: hidden !important; opacity: 0 !important; animation: none !important; pointer-events: none !important;
361
397
  }
@@ -365,6 +401,10 @@ export function createInjectionSource(features, {
365
401
  [data-codex-model-slider-copy] { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 1px; }
366
402
  [data-codex-model-slider-name] { overflow: hidden; color: var(--color-text-foreground); text-overflow: ellipsis; white-space: nowrap; }
367
403
  [data-codex-model-slider-provider] { overflow: hidden; color: var(--color-text-foreground-tertiary); font-size: 11px; line-height: 14px; text-overflow: ellipsis; white-space: nowrap; }
404
+ [data-codex-model-slider-model-label] {
405
+ display: -webkit-box; min-width: 0; overflow: hidden; overflow-wrap: anywhere;
406
+ -webkit-box-orient: vertical; -webkit-line-clamp: 2; white-space: normal;
407
+ }
368
408
  [data-codex-model-slider-item] { position: relative; }
369
409
  [data-codex-model-slider-item][data-codex-model-slider-selected] { background: var(--color-token-list-hover-background); }
370
410
  [data-codex-model-slider-star] {
@@ -381,8 +421,14 @@ export function createInjectionSource(features, {
381
421
  content: ""; position: absolute; top: 0; right: 0; bottom: 0; width: 40px; z-index: 1;
382
422
  }
383
423
  [data-codex-model-slider-star] svg { position: relative; z-index: 2; width: 16px; height: 16px; pointer-events: none; }
384
- [data-codex-model-slider-divider] { height: 0.5px; margin: 4px 8px; background: var(--color-token-border, var(--color-border)); }
385
- [data-codex-model-slider-generic-menu] { overflow-x: hidden; }
424
+ [data-codex-model-slider-divider] {
425
+ height: 0; border-top: 1px solid var(--color-token-border, var(--color-border));
426
+ margin: 6px 8px;
427
+ }
428
+ [data-codex-model-slider-generic-menu] {
429
+ overflow-x: hidden;
430
+ transform: translateX(var(--codex-model-slider-menu-center-shift, 0px)) !important;
431
+ }
386
432
  [data-codex-model-slider-generic] { box-sizing: border-box; width: 100%; }
387
433
  [data-codex-model-slider-fast] { margin-left: auto; border: 0; background: transparent; cursor: pointer; }
388
434
  [data-codex-model-slider-fast]:focus-visible { outline: 2px solid var(--color-border-focus); outline-offset: 1px; }
@@ -474,11 +520,20 @@ export function createInjectionSource(features, {
474
520
  && rect.height > 0;
475
521
  }
476
522
 
523
+ function hasNativeToolbarLabel(button, key) {
524
+ const label = String(button?.getAttribute("aria-label") ?? "").trim().toLowerCase();
525
+ return nativeToolbarLabels[key].some((candidate) => (
526
+ key === "temporaryChat"
527
+ ? label.includes(candidate.toLowerCase())
528
+ : label === candidate.toLowerCase()
529
+ ));
530
+ }
531
+
477
532
  function nativeSummaryButton() {
478
533
  return Array.from(document.querySelectorAll("button")).find((button) => (
479
534
  (
480
- button.getAttribute("aria-label") === "Toggle summary"
481
- || button.getAttribute("aria-label") === "Toggle pinned summary"
535
+ hasNativeToolbarLabel(button, "summary")
536
+ || hasNativeToolbarLabel(button, "pinnedSummary")
482
537
  )
483
538
  && isVisibleToolbarControl(button)
484
539
  )) ?? null;
@@ -486,32 +541,34 @@ export function createInjectionSource(features, {
486
541
 
487
542
  function nativeBottomPanelButton() {
488
543
  return Array.from(document.querySelectorAll("button")).find((button) => {
489
- if (button.getAttribute("aria-label") !== "Toggle bottom panel") return false;
544
+ if (!hasNativeToolbarLabel(button, "bottomPanel")) return false;
490
545
  return isVisibleToolbarControl(button);
491
546
  }) ?? null;
492
547
  }
493
548
 
494
549
  function nativeSidePanelButton() {
495
550
  return Array.from(document.querySelectorAll("button")).find((button) => {
496
- if (button.getAttribute("aria-label") !== "Toggle side panel") return false;
551
+ if (!hasNativeToolbarLabel(button, "sidePanel")) return false;
497
552
  return isVisibleToolbarControl(button);
498
553
  }) ?? null;
499
554
  }
500
555
 
501
556
  function nativeTemporaryChatButton() {
502
557
  return Array.from(document.querySelectorAll("button")).find((button) => {
503
- if (!/temporary chat/i.test(button.getAttribute("aria-label") ?? "")) return false;
558
+ if (!hasNativeToolbarLabel(button, "temporaryChat")) return false;
504
559
  return isVisibleToolbarControl(button);
505
560
  }) ?? null;
506
561
  }
507
562
 
508
563
  function structuralToolbarAnchor() {
509
564
  const buttons = Array.from(document.querySelectorAll("button"));
510
- return buttons.find((button) => /temporary chat/i.test(button.getAttribute("aria-label") ?? ""))
511
- ?? buttons.find((button) => new Set(["Toggle summary", "Toggle pinned summary"])
512
- .has(button.getAttribute("aria-label")))
513
- ?? buttons.find((button) => button.getAttribute("aria-label") === "Toggle bottom panel")
514
- ?? buttons.find((button) => button.getAttribute("aria-label") === "Toggle side panel")
565
+ return buttons.find((button) => hasNativeToolbarLabel(button, "temporaryChat"))
566
+ ?? buttons.find((button) => (
567
+ hasNativeToolbarLabel(button, "summary")
568
+ || hasNativeToolbarLabel(button, "pinnedSummary")
569
+ ))
570
+ ?? buttons.find((button) => hasNativeToolbarLabel(button, "bottomPanel"))
571
+ ?? buttons.find((button) => hasNativeToolbarLabel(button, "sidePanel"))
515
572
  ?? null;
516
573
  }
517
574
 
@@ -605,8 +662,8 @@ export function createInjectionSource(features, {
605
662
  function isNativeSummaryButton(element) {
606
663
  if (!(element instanceof Element)) return false;
607
664
  const button = element.closest("button");
608
- return button?.getAttribute("aria-label") === "Toggle summary"
609
- || button?.getAttribute("aria-label") === "Toggle pinned summary";
665
+ return hasNativeToolbarLabel(button, "summary")
666
+ || hasNativeToolbarLabel(button, "pinnedSummary");
610
667
  }
611
668
 
612
669
  function mainContentViewport() {
@@ -1728,6 +1785,12 @@ export function createInjectionSource(features, {
1728
1785
  function closeControllerModelMenu() {
1729
1786
  const record = modelSelectorControllerMenu;
1730
1787
  if (!record) return false;
1788
+ const placeholder = document.querySelector(
1789
+ '[data-codex-intelligence-trigger] [class*="_ModelPickerTriggerPlaceholder_"]',
1790
+ );
1791
+ if (placeholder && record.placeholderText != null) {
1792
+ placeholder.textContent = record.placeholderText;
1793
+ }
1731
1794
  record.button.setAttribute("aria-expanded", "false");
1732
1795
  record.parentMenu.style.position = record.position;
1733
1796
  record.parentMenu.style.overflow = record.overflow;
@@ -1809,10 +1872,17 @@ export function createInjectionSource(features, {
1809
1872
  });
1810
1873
  renderEnhancedModelMenu(menu, selector);
1811
1874
 
1875
+ const placeholder = document.querySelector(
1876
+ '[data-codex-intelligence-trigger] [class*="_ModelPickerTriggerPlaceholder_"]',
1877
+ );
1878
+ const placeholderText = placeholder?.textContent ?? null;
1879
+ if (placeholder) placeholder.textContent = locale === "zh-CN" ? "选择模型" : "Select model";
1880
+
1812
1881
  modelSelectorControllerMenu = {
1813
1882
  menu,
1814
1883
  parentMenu,
1815
1884
  button: modelButton,
1885
+ placeholderText,
1816
1886
  position: parentMenu.style.position,
1817
1887
  overflow: parentMenu.style.overflow,
1818
1888
  };
@@ -2083,6 +2153,8 @@ export function createInjectionSource(features, {
2083
2153
  "ViewToggle",
2084
2154
  "ViewToggleContent",
2085
2155
  "ViewToggleIcon",
2156
+ "ViewToggleModelLabel",
2157
+ "ViewToggleEffortLabel",
2086
2158
  "ViewControls",
2087
2159
  "FastModeToggle",
2088
2160
  "FastModeToggleContent",
@@ -2096,6 +2168,11 @@ export function createInjectionSource(features, {
2096
2168
  shell.setAttribute("data-reduced-motion", String(matchMedia("(prefers-reduced-motion: reduce)").matches));
2097
2169
  shell.setAttribute("data-transitions-ready", "false");
2098
2170
  const nativeRow = nativeModelPickerRow(parentMenu);
2171
+ const controller = nativeComposerModelController();
2172
+ const fastOption = controller?.serviceTierOptions?.find((option) => option?.value === "priority") ?? null;
2173
+ const nativeModel = nativeCatalogModelFor(identity);
2174
+ const supportsFast = Boolean(nativeModel?.slug.toLowerCase().startsWith("gpt-") && fastOption);
2175
+ shell.setAttribute("data-codex-model-slider-has-fast", String(supportsFast));
2099
2176
  const modelButton = document.createElement("div");
2100
2177
  modelButton.className = classes.ViewToggle;
2101
2178
  modelButton.setAttribute("role", "button");
@@ -2107,14 +2184,31 @@ export function createInjectionSource(features, {
2107
2184
  const buttonContent = document.createElement("span");
2108
2185
  buttonContent.className = classes.ViewToggleContent;
2109
2186
  const buttonLabel = document.createElement("span");
2187
+ buttonLabel.className = classes.ViewToggleModelLabel;
2188
+ buttonLabel.setAttribute("data-codex-model-slider-model-label", "");
2110
2189
  buttonLabel.textContent = modelDisplayLabel(identity, selector);
2111
- const nativeChevron = nativeRow?.querySelector("svg")?.cloneNode(true);
2190
+ const effortText = selectedEffortVisible ? document.createElement("span") : null;
2191
+ if (effortText) {
2192
+ effortText.className = classes.ViewToggleEffortLabel;
2193
+ effortText.textContent = effortLabel(selectedEffort);
2194
+ effortText.setAttribute("data-accent", "true");
2195
+ effortText.setAttribute("data-maximum", String(selectedEffort === "ultra"));
2196
+ }
2197
+ // 0.152.1 起原生头行 aria-label 为 "Select model",nativeModelPickerRow
2198
+ // 不再命中;优先从 view-toggle 行取 chevron,旧结构走原路径兜底。
2199
+ const nativeChevron = (
2200
+ parentMenu?.querySelector("[data-model-picker-view-toggle] svg")
2201
+ ?? nativeRow?.querySelector("svg")
2202
+ )?.cloneNode(true);
2112
2203
  if (nativeChevron) {
2113
2204
  nativeChevron.className.baseVal = classes.ViewToggleIcon;
2114
2205
  nativeChevron.setAttribute("aria-hidden", "true");
2115
- buttonContent.append(buttonLabel, nativeChevron);
2206
+ buttonContent.append(buttonLabel);
2207
+ if (effortText) buttonContent.append(effortText);
2208
+ buttonContent.append(nativeChevron);
2116
2209
  } else {
2117
2210
  buttonContent.append(buttonLabel);
2211
+ if (effortText) buttonContent.append(effortText);
2118
2212
  }
2119
2213
  modelButton.append(buttonContent);
2120
2214
  const openModels = () => openControllerModelMenu(parentMenu, modelButton, selector);
@@ -2129,10 +2223,6 @@ export function createInjectionSource(features, {
2129
2223
  viewControls.className = classes.ViewControls;
2130
2224
  viewControls.setAttribute("data-expanded", "false");
2131
2225
  viewControls.setAttribute("data-ultra-warning-visible", "false");
2132
- const controller = nativeComposerModelController();
2133
- const fastOption = controller?.serviceTierOptions?.find((option) => option?.value === "priority") ?? null;
2134
- const nativeModel = nativeCatalogModelFor(identity);
2135
- const supportsFast = Boolean(nativeModel?.slug.toLowerCase().startsWith("gpt-") && fastOption);
2136
2226
  const fastEnabled = Boolean(
2137
2227
  supportsFast && (
2138
2228
  controller.selectedServiceTier?.id === (fastOption.tier?.id ?? "priority")
@@ -2283,6 +2373,20 @@ export function createInjectionSource(features, {
2283
2373
  return shell;
2284
2374
  }
2285
2375
 
2376
+ function centerGenericModelPicker(parentMenu) {
2377
+ const trigger = document.querySelector("[data-codex-intelligence-trigger]");
2378
+ if (!trigger) return;
2379
+ const menuRect = parentMenu.getBoundingClientRect();
2380
+ const triggerRect = trigger.getBoundingClientRect();
2381
+ if (!(menuRect.width > 0 && triggerRect.width > 0)) return;
2382
+ const previousShift = Number.parseFloat(
2383
+ parentMenu.style.getPropertyValue("--codex-model-slider-menu-center-shift"),
2384
+ ) || 0;
2385
+ const unshiftedCenter = menuRect.left + (menuRect.width / 2) - previousShift;
2386
+ const nextShift = triggerRect.left + (triggerRect.width / 2) - unshiftedCenter;
2387
+ parentMenu.style.setProperty("--codex-model-slider-menu-center-shift", `${nextShift}px`);
2388
+ }
2389
+
2286
2390
  function enhanceGenericModelPicker(parentMenu, selector) {
2287
2391
  const identity = modelIdentity(nativeModelValue(parentMenu));
2288
2392
  const model = modelDefinitionFor(identity, selector);
@@ -2334,7 +2438,10 @@ export function createInjectionSource(features, {
2334
2438
  const canUpgradeFallback = Boolean(
2335
2439
  existing?.querySelector("[data-codex-model-slider-fallback]") && nativeTemplateReady,
2336
2440
  );
2337
- if (existing?.getAttribute("data-codex-model-slider-generic") === key && !canUpgradeFallback) return true;
2441
+ if (existing?.getAttribute("data-codex-model-slider-generic") === key && !canUpgradeFallback) {
2442
+ centerGenericModelPicker(parentMenu);
2443
+ return true;
2444
+ }
2338
2445
  existing?.remove();
2339
2446
  const nativeContainer = parentMenu.firstElementChild;
2340
2447
  if (!nativeContainer) return false;
@@ -2347,12 +2454,14 @@ export function createInjectionSource(features, {
2347
2454
  nativeContainer.style.display = "none";
2348
2455
  shell.setAttribute("data-codex-model-slider-generic", key);
2349
2456
  parentMenu.append(shell);
2457
+ centerGenericModelPicker(parentMenu);
2350
2458
  return true;
2351
2459
  }
2352
2460
 
2353
2461
  function restoreGenericModelPicker(parentMenu) {
2354
2462
  parentMenu.querySelector(":scope > [data-codex-model-slider-generic]")?.remove();
2355
2463
  parentMenu.removeAttribute("data-codex-model-slider-generic-menu");
2464
+ parentMenu.style.removeProperty("--codex-model-slider-menu-center-shift");
2356
2465
  const nativeContainer = parentMenu.firstElementChild;
2357
2466
  if (nativeContainer && modelSelectorGenericContainers.has(nativeContainer)) {
2358
2467
  nativeContainer.style.display = modelSelectorGenericContainers.get(nativeContainer);
@@ -2361,10 +2470,10 @@ export function createInjectionSource(features, {
2361
2470
  }
2362
2471
 
2363
2472
  function enhanceNativeModelPicker(parentMenu, selector) {
2473
+ if (enhanceGenericModelPicker(parentMenu, selector)) return;
2364
2474
  const toggle = parentMenu.querySelector("[data-model-picker-view-toggle]");
2365
2475
  if (toggle) {
2366
- // 新版 Codex 保留原生 Slider;MCX 只精简模型名并接管模型入口。
2367
- restoreGenericModelPicker(parentMenu);
2476
+ // 私有 DOM 无法满足 MCX shell 契约时,保留原生菜单作为降级路径。
2368
2477
  const raw = nativeModelValue(parentMenu);
2369
2478
  const textNode = firstVisibleTextNode(toggle);
2370
2479
  if (raw && textNode) {
@@ -2393,7 +2502,26 @@ export function createInjectionSource(features, {
2393
2502
  }
2394
2503
  return;
2395
2504
  }
2396
- if (enhanceGenericModelPicker(parentMenu, selector)) return;
2505
+ }
2506
+
2507
+ // 原生 ModelText 重建前的空窗期:立即插回 MCX 标签,避免关盒后芯片
2508
+ // 长时间失去模型名。
2509
+ function restoreTriggerLabelWithoutNative(trigger, selector) {
2510
+ if (trigger.getAttribute("data-state") !== "closed") return;
2511
+ if (trigger.querySelector("[data-codex-model-slider-trigger-label]")) return;
2512
+ const raw = trigger.getAttribute("data-codex-model-slider-trigger-raw");
2513
+ if (!raw) return;
2514
+ const anchor = trigger.querySelector('[class*="_ModelPickerTriggerLabel_"]')
2515
+ ?? trigger.querySelector('[class*="_ModelPickerTriggerContent_"]');
2516
+ if (!anchor) return;
2517
+ const displayLabel = document.createElement("span");
2518
+ displayLabel.setAttribute("data-codex-model-slider-trigger-label", "");
2519
+ displayLabel.textContent = modelDisplayLabel(modelIdentity(raw), selector);
2520
+ anchor.prepend(displayLabel);
2521
+ }
2522
+
2523
+ function isComposerModelPlaceholder(value) {
2524
+ return /^(select\s+(model|effort)|选择\s*(模型|思考强度))$/i.test(String(value ?? "").trim());
2397
2525
  }
2398
2526
 
2399
2527
  function enhanceComposerModelTrigger(selector) {
@@ -2408,7 +2536,15 @@ export function createInjectionSource(features, {
2408
2536
  const nativeLabels = Array.from(trigger.querySelectorAll(
2409
2537
  '[class*="_ModelPickerTriggerModelText_"]:not([data-codex-model-slider-trigger-label])',
2410
2538
  ));
2411
- if (!nativeLabels.length) return;
2539
+ if (!nativeLabels.length) {
2540
+ trigger.setAttribute("data-codex-model-slider-trigger", "");
2541
+ trigger.toggleAttribute(
2542
+ "data-codex-model-slider-trigger-placeholder",
2543
+ isComposerModelPlaceholder(trigger.innerText),
2544
+ );
2545
+ restoreTriggerLabelWithoutNative(trigger, selector);
2546
+ return;
2547
+ }
2412
2548
  const visibleValue = String(nativeLabels.at(-1)?.textContent ?? "").trim();
2413
2549
  const previousRaw = trigger.getAttribute("data-codex-model-slider-trigger-raw") ?? "";
2414
2550
  const previousLabel = previousRaw
@@ -2419,16 +2555,30 @@ export function createInjectionSource(features, {
2419
2555
  const label = modelDisplayLabel(modelIdentity(raw), selector);
2420
2556
  trigger.setAttribute("data-codex-model-slider-trigger", "");
2421
2557
  trigger.setAttribute("data-codex-model-slider-trigger-raw", raw);
2558
+ // 区分占位态(Select model / Select effort)与真实模型态:占位态沿用
2559
+ // 原生宽度契约,真实模型态让芯片 HUG 完整名称,避免第三方长名被截断。
2560
+ const placeholder = isComposerModelPlaceholder(label);
2561
+ trigger.toggleAttribute("data-codex-model-slider-trigger-placeholder", placeholder);
2422
2562
  for (const nativeLabel of nativeLabels) {
2423
2563
  let displayLabel = nativeLabel.nextElementSibling;
2424
2564
  if (!displayLabel?.hasAttribute("data-codex-model-slider-trigger-label")) {
2425
- displayLabel = document.createElement("span");
2426
- displayLabel.className = nativeLabel.className;
2427
- displayLabel.setAttribute("data-codex-model-slider-trigger-label", "");
2565
+ // 空窗期回插等场景会在别处留下现成的 MCX 标签,优先移动复用,
2566
+ // 避免恢复后出现两个可见名称把芯片撑宽。
2567
+ displayLabel = trigger.querySelector("[data-codex-model-slider-trigger-label]");
2568
+ if (!displayLabel) {
2569
+ displayLabel = document.createElement("span");
2570
+ displayLabel.className = nativeLabel.className;
2571
+ displayLabel.setAttribute("data-codex-model-slider-trigger-label", "");
2572
+ }
2428
2573
  nativeLabel.after(displayLabel);
2429
2574
  }
2430
2575
  if (displayLabel.textContent !== label) displayLabel.textContent = label;
2431
2576
  }
2577
+ // 原生开合的临时 min-width 钉定与弹层水平居中都基于对 Measurement
2578
+ // 探针的测量;把探针文本同步为 MCX 显示标签,原生机制即可测得
2579
+ // 真实显示宽度(含 110px 名称上限与 160px Content 上限)。
2580
+ const measurement = trigger.querySelector('[class*="_ModelPickerTriggerMeasurement_"]');
2581
+ if (measurement && measurement.textContent !== label) measurement.textContent = label;
2432
2582
  }
2433
2583
 
2434
2584
  function ensureModelSelector() {
@@ -2568,6 +2718,33 @@ export function createInjectionSource(features, {
2568
2718
  }, record.origin);
2569
2719
  }
2570
2720
 
2721
+ function nativeSidebarTrigger() {
2722
+ const triggers = Array.from(document.querySelectorAll(
2723
+ 'button[data-app-shell-sidebar-trigger="true"]',
2724
+ ));
2725
+ return triggers.find((button) => isVisibleToolbarControl(button)) ?? triggers[0] ?? null;
2726
+ }
2727
+
2728
+ function sidebarState() {
2729
+ const trigger = nativeSidebarTrigger();
2730
+ const expanded = trigger?.getAttribute("aria-expanded");
2731
+ const triggerRect = trigger?.getBoundingClientRect();
2732
+ return {
2733
+ available: Boolean(trigger),
2734
+ open: expanded === null ? sidebarRight() > 0 : expanded === "true",
2735
+ label: trigger?.getAttribute("aria-label") ?? "Show sidebar",
2736
+ leading: triggerRect?.left ?? 16,
2737
+ };
2738
+ }
2739
+
2740
+ function postSidebarState(record, state = sidebarState()) {
2741
+ if (!record || record.kind !== "page") return;
2742
+ record.frame.contentWindow?.postMessage({
2743
+ type: "codex-personal:sidebar-state",
2744
+ ...state,
2745
+ }, record.origin);
2746
+ }
2747
+
2571
2748
  function registerSurface(key, feature, kind, surfaceUrl, element, frame, detailId = null) {
2572
2749
  const record = {
2573
2750
  key,
@@ -2587,6 +2764,7 @@ export function createInjectionSource(features, {
2587
2764
  frame.addEventListener("load", () => {
2588
2765
  queueTheme();
2589
2766
  postSurfaceActive(record, activePageFeatureId === record.featureId && !record.element.hidden);
2767
+ postSidebarState(record);
2590
2768
  });
2591
2769
  requestSurfaceLoad(record);
2592
2770
  return record;
@@ -2695,8 +2873,16 @@ export function createInjectionSource(features, {
2695
2873
  }
2696
2874
 
2697
2875
  function updatePageSurfacePositions() {
2698
- const left = `${Math.max(0, sidebarRight())}px`;
2699
- for (const surface of pageSurfaces.values()) surface.style.left = left;
2876
+ const state = sidebarState();
2877
+ const left = `${state.open ? Math.max(0, sidebarRight()) : 0}px`;
2878
+ for (const [featureId, surface] of pageSurfaces) {
2879
+ surface.style.left = left;
2880
+ postSidebarState(surfaceRecords.get(surfaceKey(featureId, "page")), state);
2881
+ }
2882
+ }
2883
+
2884
+ function syncAfterNativeSidebarToggle() {
2885
+ queueMicrotask(updatePageSurfacePositions);
2700
2886
  }
2701
2887
 
2702
2888
  function threadIdentity() {
@@ -4010,6 +4196,15 @@ export function createInjectionSource(features, {
4010
4196
  });
4011
4197
  return;
4012
4198
  }
4199
+ if (action === "open-sidebar") {
4200
+ const trigger = nativeSidebarTrigger();
4201
+ if (!trigger) throw new Error("The native sidebar control was not found");
4202
+ const state = sidebarState();
4203
+ if (!state.open) trigger.click();
4204
+ queueMicrotask(() => postSidebarState(record, { ...state, open: true }));
4205
+ respondToSurface(record, requestId, { ok: true, result: { opened: true } });
4206
+ return;
4207
+ }
4013
4208
  if (action === "resolve-file-paths") {
4014
4209
  const getPathForFile = window.electronBridge?.getPathForFile;
4015
4210
  if (typeof getPathForFile !== "function") {
@@ -4068,6 +4263,9 @@ export function createInjectionSource(features, {
4068
4263
  document.addEventListener(
4069
4264
  "click",
4070
4265
  (event) => {
4266
+ if (event.target.closest?.('[data-app-shell-sidebar-trigger="true"]')) {
4267
+ syncAfterNativeSidebarToggle();
4268
+ }
4071
4269
  if (
4072
4270
  modelSelectorControllerMenu
4073
4271
  && !modelSelectorControllerMenu.menu.contains(event.target)
@@ -4133,6 +4331,7 @@ export function createInjectionSource(features, {
4133
4331
  if (event.data.type === "codex-personal:ready") {
4134
4332
  record.ready = true;
4135
4333
  queueTheme();
4334
+ postSidebarState(record);
4136
4335
  if (record.kind === "modal") {
4137
4336
  record.frame.focus();
4138
4337
  record.frame.contentWindow?.postMessage({
@@ -4170,7 +4369,24 @@ export function createInjectionSource(features, {
4170
4369
  updatePageSurfacePositions();
4171
4370
  updateResponsiveSummaryPresentation();
4172
4371
  }, { signal: lifetime.signal });
4173
- const ensureObserver = new MutationObserver(queueEnsure);
4372
+ const ensureObserver = new MutationObserver((mutations) => {
4373
+ queueEnsure();
4374
+ const sidebarTriggerChanged = mutations.some((mutation) => {
4375
+ if (
4376
+ mutation.type === "attributes"
4377
+ && mutation.target.matches?.('[data-app-shell-sidebar-trigger="true"]')
4378
+ ) return true;
4379
+ return mutation.type === "childList"
4380
+ && [...mutation.addedNodes, ...mutation.removedNodes].some((node) => (
4381
+ node instanceof Element
4382
+ && (
4383
+ node.matches?.('[data-app-shell-sidebar-trigger="true"]')
4384
+ || node.querySelector?.('[data-app-shell-sidebar-trigger="true"]')
4385
+ )
4386
+ ));
4387
+ });
4388
+ if (sidebarTriggerChanged) updatePageSurfacePositions();
4389
+ });
4174
4390
  ensureObserver.observe(document.documentElement, {
4175
4391
  childList: true,
4176
4392
  subtree: true,
@@ -10,6 +10,7 @@ const HOST_ACTIONS = new Set([
10
10
  "attach-file",
11
11
  "open-detail-tab",
12
12
  "open-editor-modal",
13
+ "open-sidebar",
13
14
  "resolve-file-paths",
14
15
  "import-generated-image",
15
16
  "create-image-edit-thread",