diff --git a/docs/images/pr-163-after.png b/docs/images/pr-163-after.png
new file mode 100644
index 00000000..f9eafa4a
Binary files /dev/null and b/docs/images/pr-163-after.png differ
diff --git a/docs/images/pr-163-before.png b/docs/images/pr-163-before.png
new file mode 100644
index 00000000..172f2812
Binary files /dev/null and b/docs/images/pr-163-before.png differ
diff --git a/macos/CHANGELOG.md b/macos/CHANGELOG.md
index 0e5d740a..bc2fabf2 100644
--- a/macos/CHANGELOG.md
+++ b/macos/CHANGELOG.md
@@ -5,6 +5,8 @@
### 改进
- 菜单栏在 `paused` 状态显示「继续显示皮肤」(调用现有应用入口),与 Windows 托盘暂停/继续文案和语义对齐;运行中仍为「重新应用皮肤」+「暂停皮肤」。
+- 收紧 macOS 首页布局:项目选择器从中英文标题中独立显示,四个首页入口固定为紧凑的 2×2 网格(Codex 异步减少原生建议时自动补齐),并将撰写器最大宽度统一为 920px;其他语言继续保留原生完整标题。
+- 沉浸任务页右侧的输出、子智能体、浏览器和来源浮层改用半透明模糊材质,不再以原生实色下拉背景遮住主题图片;粘性分组标题直接继承浮层磨砂材质,不再叠加暗色底条。
## 1.2.0 — 2026-07-17
diff --git a/macos/assets/dream-skin.css b/macos/assets/dream-skin.css
index 07976b19..241d9b06 100644
--- a/macos/assets/dream-skin.css
+++ b/macos/assets/dream-skin.css
@@ -540,7 +540,7 @@ html.codex-dream-skin [role="main"] {
}
html.codex-dream-skin .dream-skin-home {
- --thread-content-max-width: min(1180px, calc(100cqw - 44px)) !important;
+ --thread-content-max-width: min(920px, calc(100cqw - 48px)) !important;
overflow-x: hidden !important;
}
@@ -550,8 +550,8 @@ html.codex-dream-skin .dream-skin-home {
}
.dream-skin-home > div:first-child > div:first-child {
- flex: 0 0 440px !important;
- min-height: 440px !important;
+ flex: 0 0 520px !important;
+ min-height: 520px !important;
align-items: flex-start !important;
padding-bottom: 0 !important;
}
@@ -561,13 +561,13 @@ html.codex-dream-skin .dream-skin-home {
isolation: isolate;
width: calc(100% - 44px) !important;
max-width: none !important;
- height: clamp(248px, 31cqw, 376px) !important;
- min-height: 248px !important;
+ height: clamp(260px, 26cqw, 320px) !important;
+ min-height: 260px !important;
flex: 0 1 auto !important;
overflow: visible !important;
padding: 0 !important;
border: 1px solid rgb(var(--ds-accent-rgb) / .30) !important;
- border-radius: 22px !important;
+ border-radius: 16px !important;
background-color: var(--ds-panel) !important;
background-image: var(--dream-skin-art) !important;
background-repeat: no-repeat !important;
@@ -582,7 +582,7 @@ html.codex-dream-skin .dream-skin-home {
inset: 0 auto 0 0;
z-index: 0;
width: 62%;
- border-radius: 21px 0 0 21px;
+ border-radius: 15px 0 0 15px;
pointer-events: none;
background: var(--ds-hero-scrim);
}
@@ -603,7 +603,7 @@ html.codex-dream-skin:is([data-dream-art-safe="none"], [data-dream-art-safe-area
.dream-skin-home > div:first-child > div:first-child > div:first-child::before {
inset: 0;
width: auto;
- border-radius: 21px;
+ border-radius: 15px;
}
.dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child {
@@ -626,7 +626,7 @@ html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-ar
}
.dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child {
- width: min(46%, 520px) !important;
+ width: min(640px, 100%) !important;
align-items: flex-start !important;
gap: 0 !important;
}
@@ -639,7 +639,6 @@ html.codex-dream-skin:is([data-dream-art-safe="right"], [data-dream-art-safe-are
html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"])
.dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child {
- width: min(58%, 640px) !important;
align-items: center !important;
text-align: center !important;
}
@@ -648,6 +647,7 @@ html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-ar
.dream-skin-home [data-feature="game-source"] {
display: block !important;
+ position: relative !important;
max-width: 100% !important;
color: var(--ds-text) !important;
font-size: clamp(19px, 1.85vw, 27px) !important;
@@ -659,14 +659,26 @@ html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-ar
pointer-events: auto !important;
}
-.dream-skin-home [data-feature="game-source"]::before {
- content: none;
+.dream-skin-home [data-feature="game-source"][data-dream-skin-question] {
+ width: min(640px, 100%) !important;
+ padding-top: 58px !important;
+ font-size: 0 !important;
+}
+
+.dream-skin-home [data-feature="game-source"][data-dream-skin-question]::before {
+ content: attr(data-dream-skin-question);
+ display: block;
+ color: var(--ds-text);
+ font-size: clamp(38px, 3.6cqw, 52px);
+ line-height: 1.08;
+ font-weight: 680;
+ letter-spacing: -.035em;
}
.dream-skin-home [data-feature="game-source"]::after {
content: var(--dream-skin-tagline, "把喜欢的画面变成可交互的 Codex 工作台。");
display: block;
- margin-top: 13px;
+ margin-top: 14px;
color: rgb(var(--ds-text-rgb) / .76);
font-size: 13px;
line-height: 1.5;
@@ -674,49 +686,143 @@ html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-ar
letter-spacing: 0;
}
-.dream-skin-home [data-feature="game-source"] button {
- margin: 0 5px;
- padding: 2px 9px 3px;
+.dream-skin-home [data-feature="game-source"][data-dream-skin-question] button {
+ position: absolute !important;
+ left: 0;
+ top: 0;
+ min-height: 44px;
+ margin: 0;
+ padding: 0 14px;
border: 1px solid rgb(var(--ds-accent-rgb) / .36);
- border-radius: 999px;
- background: rgb(var(--ds-accent-rgb) / .10);
- color: var(--ds-accent) !important;
- text-decoration-color: rgb(var(--ds-accent-rgb) / .56) !important;
- text-underline-offset: 5px;
+ border-radius: 12px;
+ background: rgb(var(--ds-panel-rgb) / .54);
+ color: var(--ds-text) !important;
+ font-size: 14px !important;
+ line-height: 1;
+ text-decoration: none !important;
+ box-shadow: inset 0 1px rgb(var(--ds-text-rgb) / .08);
+ backdrop-filter: blur(8px) saturate(104%);
}
-.dream-skin-home [data-feature="game-source"] button::before {
+.dream-skin-home [data-feature="game-source"][data-dream-skin-question] button::before {
content: var(--dream-skin-project-prefix, "选择项目 · ");
- font-size: .46em;
- font-weight: 700;
+ margin-right: 8px;
+ color: var(--ds-muted);
+ font-size: 11px;
+ font-weight: 650;
vertical-align: middle;
- opacity: .86;
+ opacity: 1;
+}
+
+html.codex-dream-skin:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"])
+ .dream-skin-home [data-feature="game-source"][data-dream-skin-question] button {
+ left: auto;
+ right: 0;
+}
+
+html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"])
+ .dream-skin-home [data-feature="game-source"][data-dream-skin-question] button {
+ left: 50%;
+ transform: translateX(-50%);
}
.dream-skin-home > div:first-child > div:first-child > div:first-child > div:nth-child(2) {
- left: 14px !important;
- right: 14px !important;
+ left: 38px !important;
+ right: auto !important;
top: 100% !important;
- margin-top: 13px !important;
+ width: min(640px, calc(100% - 76px)) !important;
+ margin-top: 14px !important;
+}
+
+html.codex-dream-skin:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"])
+ .dream-skin-home > div:first-child > div:first-child > div:first-child > div:nth-child(2) {
+ left: auto !important;
+ right: 38px !important;
+}
+
+html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"])
+ .dream-skin-home > div:first-child > div:first-child > div:first-child > div:nth-child(2) {
+ left: 50% !important;
+ right: auto !important;
+ transform: translateX(-50%);
+}
+
+html.codex-dream-skin main.main-surface.dream-skin-home-shell [role="main"].dream-skin-home
+ [class~="absolute"][class~="top-full"]:has(> [data-dream-skin-generated-home-suggestions]) {
+ left: 50% !important;
+ right: auto !important;
+ top: 100% !important;
+ width: calc(100% - 52px) !important;
+ max-width: 888px;
+ margin-top: 76px !important;
+ transform: translateX(-50%) !important;
+}
+
+.dream-skin-home .group\/home-suggestions {
+ display: block !important;
+ overflow: visible !important;
+}
+
+.dream-skin-home div:has(> .group\/home-suggestions) {
+ margin-inline: 0 !important;
+}
+
+html.codex-dream-skin[data-dream-art-wide="true"]
+ .dream-skin-home div:has(> .group\/home-suggestions) {
+ transform: translateY(-300px);
+}
+
+.dream-skin-home .group\/home-suggestions > div {
+ width: 100% !important;
+ padding-inline: 0 !important;
+}
+
+.dream-skin-home [data-dream-skin-home-grid] {
+ display: grid !important;
+ width: 100% !important;
+ padding-inline: 0 !important;
+ grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
+ gap: 12px !important;
+}
+
+.dream-skin-home [data-dream-skin-home-grid] > * {
+ display: block !important;
}
-.dream-skin-home .group\/home-suggestions { overflow: visible !important; }
+.dream-skin-home .group\/home-suggestions[data-dream-skin-home-suggestions]
+ [data-dream-skin-home-grid] > [data-dream-skin-native-home-card="hidden"] {
+ display: none !important;
+}
+
+.dream-skin-home .group\/home-suggestions[data-dream-skin-home-suggestions]
+ > .group\/home-suggestions-header {
+ display: none !important;
+}
+
+.dream-skin-home .group\/home-suggestions[data-dream-skin-empty-home-suggestions] {
+ display: none !important;
+}
.dream-skin-home .group\/home-suggestions button {
position: relative !important;
- min-height: 118px !important;
- padding: 14px 13px 12px !important;
- align-items: stretch !important;
+ display: flex !important;
+ flex-direction: row !important;
+ width: 100% !important;
+ height: 100% !important;
+ min-height: 72px !important;
+ padding: 12px 15px !important;
+ align-items: center !important;
justify-content: flex-start !important;
- text-align: center !important;
+ gap: 12px !important;
+ text-align: left !important;
border: 1px solid rgb(var(--ds-muted-rgb) / .18) !important;
- border-radius: 18px !important;
- background: rgb(var(--ds-panel-rgb) / .90) !important;
+ border-radius: 14px !important;
+ background: rgb(var(--ds-panel-rgb) / .70) !important;
color: var(--ds-text) !important;
font-weight: 600 !important;
- line-height: 1.45 !important;
- box-shadow: 0 8px 22px rgb(var(--ds-bg-rgb) / .18) !important;
- backdrop-filter: blur(10px) saturate(105%) !important;
+ line-height: 1.35 !important;
+ box-shadow: 0 6px 18px rgb(var(--ds-bg-rgb) / .14) !important;
+ backdrop-filter: blur(8px) saturate(104%) !important;
transform: translateY(0);
transition: transform .18s cubic-bezier(.22, 1, .36, 1),
box-shadow .18s cubic-bezier(.22, 1, .36, 1),
@@ -728,37 +834,85 @@ html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-ar
}
.dream-skin-home .group\/home-suggestions button:hover {
- transform: translateY(-2px) !important;
+ transform: translateY(-1px) !important;
border-color: rgb(var(--ds-accent-rgb) / .42) !important;
- box-shadow: 0 12px 28px rgb(var(--ds-bg-rgb) / .24) !important;
+ background: rgb(var(--ds-panel-rgb) / .82) !important;
+ box-shadow: 0 9px 22px rgb(var(--ds-bg-rgb) / .20) !important;
}
.dream-skin-home .group\/home-suggestions button > span:first-child > span:first-child {
- width: 38px;
- height: 38px;
+ width: 34px;
+ height: 34px;
display: grid !important;
place-items: center;
- margin: 0 auto;
+ flex: 0 0 34px;
+ margin: 0;
border: 1px solid rgb(var(--ds-accent-rgb) / .24);
- border-radius: 50%;
+ border-radius: 10px;
color: var(--ds-accent) !important;
- background: rgb(var(--ds-accent-rgb) / .12);
- box-shadow: 0 0 0 6px rgb(var(--ds-accent-rgb) / .05);
+ background: rgb(var(--ds-accent-rgb) / .10);
+ box-shadow: inset 0 1px rgb(var(--ds-text-rgb) / .06);
}
.dream-skin-home .group\/home-suggestions button svg {
- width: 21px !important;
- height: 21px !important;
+ width: 18px !important;
+ height: 18px !important;
color: var(--ds-accent) !important;
}
-.dream-skin-home .group\/home-suggestions button > span:first-child { justify-content: center !important; }
-.dream-skin-home .group\/home-suggestions button > span:last-child { align-items: center !important; text-align: center !important; }
+.dream-skin-home .group\/home-suggestions .dream-skin-home-action-icon {
+ width: 34px !important;
+ height: 34px !important;
+ display: grid !important;
+ place-items: center;
+ flex: 0 0 34px !important;
+ border: 1px solid rgb(var(--ds-accent-rgb) / .24);
+ border-radius: 10px;
+ color: var(--ds-accent) !important;
+ background: rgb(var(--ds-accent-rgb) / .10);
+ box-shadow: inset 0 1px rgb(var(--ds-text-rgb) / .06);
+}
+
+.dream-skin-home .group\/home-suggestions .dream-skin-home-action-copy {
+ display: flex !important;
+ flex-direction: column !important;
+}
+
+.dream-skin-home .group\/home-suggestions .dream-skin-home-action-label {
+ display: block !important;
+ min-width: 0;
+ overflow: hidden;
+ color: var(--ds-text) !important;
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 20px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.dream-skin-home .group\/home-suggestions button > span:first-child:not(.dream-skin-home-action-icon) {
+ width: auto !important;
+ flex: 0 0 auto !important;
+ justify-content: flex-start !important;
+}
+.dream-skin-home .group\/home-suggestions button > span:last-child {
+ min-width: 0 !important;
+ min-height: 0 !important;
+ width: auto !important;
+ flex: 1 1 auto !important;
+ margin: 0 !important;
+ justify-content: center !important;
+ align-items: flex-start !important;
+ text-align: left !important;
+ white-space: nowrap !important;
+ overflow: hidden !important;
+ text-overflow: ellipsis !important;
+}
html.codex-dream-skin .composer-surface-chrome {
overflow: visible !important;
border: 1px solid rgb(var(--ds-muted-rgb) / .18) !important;
- border-radius: 22px !important;
+ border-radius: 16px !important;
background: rgb(var(--ds-panel-rgb) / .94) !important;
box-shadow: 0 10px 28px rgb(var(--ds-bg-rgb) / .24) !important;
backdrop-filter: blur(16px) saturate(108%) !important;
@@ -837,6 +991,25 @@ html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface.dream-skin-h
content: none !important;
}
+html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface.dream-skin-home-shell
+ .dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child {
+ justify-content: center !important;
+}
+
+html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface.dream-skin-home-shell
+ .dream-skin-home > div:first-child > div:first-child > div:first-child
+ > div:first-child > div:first-child {
+ width: min(calc(var(--thread-content-max-width) - 32px), calc(100% - 76px)) !important;
+ align-items: flex-start !important;
+ text-align: left !important;
+ transform: translateY(90px);
+}
+
+html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface.dream-skin-home-shell
+ .dream-skin-home [data-feature="game-source"][data-dream-skin-question] {
+ width: 100% !important;
+}
+
html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface.dream-skin-home-shell
.dream-skin-home .group\/home-suggestions button {
background: rgb(var(--ds-panel-rgb) / .56) !important;
@@ -878,8 +1051,9 @@ html.codex-dream-skin .dream-skin-home-utility {
top: 0 !important;
width: 100% !important;
margin-inline: 0 !important;
- padding-inline: 18px !important;
- border-radius: 22px 22px 0 0 !important;
+ min-height: 48px !important;
+ padding-inline: 14px !important;
+ border-radius: 16px 16px 0 0 !important;
background: rgb(var(--ds-panel-rgb) / .94) !important;
box-shadow:
inset 1px 0 rgb(var(--ds-muted-rgb) / .18),
@@ -891,7 +1065,7 @@ html.codex-dream-skin .dream-skin-home-utility {
html.codex-dream-skin .dream-skin-home:has(.dream-skin-home-utility)
.composer-surface-chrome {
border: 0 !important;
- border-radius: 0 0 22px 22px !important;
+ border-radius: 0 0 16px 16px !important;
box-shadow:
0 10px 28px rgb(var(--ds-bg-rgb) / .24),
inset 1px 0 rgb(var(--ds-muted-rgb) / .18),
@@ -982,6 +1156,29 @@ html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-
background: transparent !important;
}
+/* The native thread detail panel uses the opaque dropdown token for both its
+ shell and sticky headings. Give this right-edge surface the same translucent
+ treatment as the immersive composer without changing unrelated dropdowns. */
+html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]
+ main.main-surface:not(.dream-skin-home-shell)
+ [class~="top-(--thread-floating-content-top-inset)"][class~="right-0"][class~="z-40"]
+ [class~="rounded-3xl"][class~="bg-token-dropdown-background"] {
+ background: var(--ds-immersive-composer) !important;
+ box-shadow:
+ 0 12px 32px rgb(var(--ds-bg-rgb) / .22),
+ inset 0 0 0 1px var(--ds-immersive-line) !important;
+ backdrop-filter: blur(16px) saturate(108%) !important;
+}
+
+html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]
+ main.main-surface:not(.dream-skin-home-shell)
+ [class~="top-(--thread-floating-content-top-inset)"][class~="right-0"][class~="z-40"]
+ [class~="rounded-3xl"][class~="bg-token-dropdown-background"]
+ header[class~="bg-token-dropdown-background"] {
+ background: transparent !important;
+ backdrop-filter: none !important;
+}
+
html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface
.app-shell-main-content-frame {
border-top: 0 !important;
@@ -1053,21 +1250,50 @@ html.codex-dream-skin [data-message-author-role] { border-radius: 16px; }
.dream-skin-brand { left: 15px; }
.dream-skin-brand b { font-size: 12px; }
.dream-skin-home > div:first-child > div:first-child {
- flex-basis: 408px !important;
- min-height: 408px !important;
+ flex-basis: 500px !important;
+ min-height: 500px !important;
}
.dream-skin-home > div:first-child > div:first-child > div:first-child {
- height: 232px !important;
- min-height: 232px !important;
+ height: 260px !important;
+ min-height: 260px !important;
}
.dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child { padding: 0 26px; }
- .dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child {
- width: min(58%, 460px) !important;
- }
.dream-skin-home [data-feature="game-source"] { font-size: 18px !important; }
- .dream-skin-home [data-feature="game-source"]::before { margin-bottom: 7px; font-size: 10px; }
+ .dream-skin-home [data-feature="game-source"][data-dream-skin-question]::before { font-size: 36px; }
.dream-skin-home [data-feature="game-source"]::after { margin-top: 9px; font-size: 11px; }
- .dream-skin-home .group\/home-suggestions button { min-height: 112px !important; font-size: 12px !important; }
+ html.codex-dream-skin[data-dream-art-wide="true"]
+ .dream-skin-home div:has(> .group\/home-suggestions) { transform: translateY(-250px); }
+ .dream-skin-home .group\/home-suggestions button { min-height: 68px !important; font-size: 12px !important; }
+}
+
+@media (max-width: 700px) {
+ .dream-skin-home { --thread-content-max-width: calc(100cqw - 28px) !important; }
+ .dream-skin-home > div:first-child > div:first-child {
+ flex-basis: 580px !important;
+ min-height: 580px !important;
+ }
+ .dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child {
+ padding: 0 20px;
+ }
+ .dream-skin-home > div:first-child > div:first-child > div:first-child > div:nth-child(2) {
+ left: 20px !important;
+ right: auto !important;
+ width: calc(100% - 40px) !important;
+ transform: none;
+ }
+ html.codex-dream-skin:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"])
+ .dream-skin-home > div:first-child > div:first-child > div:first-child > div:nth-child(2),
+ html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"])
+ .dream-skin-home > div:first-child > div:first-child > div:first-child > div:nth-child(2) {
+ left: 20px !important;
+ right: auto !important;
+ transform: none;
+ }
+ .dream-skin-home [data-dream-skin-home-grid] {
+ grid-template-columns: minmax(0, 1fr) !important;
+ }
+ html.codex-dream-skin[data-dream-art-wide="true"]
+ .dream-skin-home div:has(> .group\/home-suggestions) { transform: translateY(-180px); }
}
@media (prefers-reduced-motion: reduce) {
diff --git a/macos/assets/renderer-inject.js b/macos/assets/renderer-inject.js
index fe427679..91393d78 100644
--- a/macos/assets/renderer-inject.js
+++ b/macos/assets/renderer-inject.js
@@ -4,6 +4,33 @@
const STYLE_ID = "codex-dream-skin-style";
const CHROME_ID = "codex-dream-skin-chrome";
const SHELL_ATTR = "data-dream-shell";
+ const HOME_QUESTION_ATTR = "data-dream-skin-question";
+ const HOME_SUGGESTIONS_ATTR = "data-dream-skin-home-suggestions";
+ const HOME_CARD_ATTR = "data-dream-skin-home-card";
+ const HOME_NATIVE_CARD_ATTR = "data-dream-skin-native-home-card";
+ const HOME_GENERATED_SUGGESTIONS_ATTR = "data-dream-skin-generated-home-suggestions";
+ const HOME_EMPTY_SUGGESTIONS_ATTR = "data-dream-skin-empty-home-suggestions";
+ const HOME_GRID_ATTR = "data-dream-skin-home-grid";
+ const HOME_SUGGESTIONS = {
+ zh: [
+ ["explore", "探索并理解代码", "探索并理解这个项目的代码结构、核心模块和关键执行链路。"],
+ ["build", "构建新功能、应用或工具", "在这个项目中构建一个新功能、应用或工具。"],
+ ["review", "审查代码并提出修改建议", "审查这个项目的代码,并提出具体、可执行的修改建议。"],
+ ["fix", "修复问题和失败", "定位并修复这个项目中的问题和失败。"],
+ ],
+ en: [
+ ["explore", "Explore and understand the code", "Explore this project's code structure, core modules, and execution flow."],
+ ["build", "Build a new feature, app, or tool", "Build a new feature, app, or tool in this project."],
+ ["review", "Review code and suggest improvements", "Review this project's code and suggest specific, actionable improvements."],
+ ["fix", "Fix issues and failures", "Find and fix issues or failures in this project."],
+ ],
+ };
+ const HOME_SUGGESTION_ICONS = {
+ explore: '',
+ build: '',
+ review: '',
+ fix: '',
+ };
const ART_ATTRS = [
"data-dream-art-wide", "data-dream-art-safe", "data-dream-task-mode",
"data-dream-art-safe-area", "data-dream-art-task-mode", "data-dream-art-aspect",
@@ -44,6 +71,7 @@
rootPasses: 0,
routePasses: 0,
layoutReads: 0,
+ rootClassWrites: 0,
attributeWrites: 0,
styleWrites: 0,
textWrites: 0,
@@ -78,6 +106,24 @@
try { previous.mediaQuery.removeEventListener("change", previous.mediaHandler); } catch {}
}
+ const clearHomeEnhancements = () => {
+ document.querySelectorAll(`[${HOME_QUESTION_ATTR}]`).forEach((node) =>
+ node.removeAttribute(HOME_QUESTION_ATTR));
+ document.querySelectorAll(`[${HOME_CARD_ATTR}]`).forEach((node) => node.remove());
+ document.querySelectorAll(`[${HOME_NATIVE_CARD_ATTR}]`).forEach((node) =>
+ node.removeAttribute(HOME_NATIVE_CARD_ATTR));
+ document.querySelectorAll(`[${HOME_SUGGESTIONS_ATTR}]`).forEach((node) =>
+ node.removeAttribute(HOME_SUGGESTIONS_ATTR));
+ document.querySelectorAll(`[${HOME_GRID_ATTR}]`).forEach((node) =>
+ node.removeAttribute(HOME_GRID_ATTR));
+ document.querySelectorAll(`[${HOME_EMPTY_SUGGESTIONS_ATTR}]`).forEach((node) =>
+ node.removeAttribute(HOME_EMPTY_SUGGESTIONS_ATTR));
+ document.querySelectorAll(`[${HOME_GENERATED_SUGGESTIONS_ATTR}]`).forEach((node) =>
+ node.remove());
+ };
+
+ clearHomeEnhancements();
+
const cssString = (value) => JSON.stringify(String(value ?? ""));
const setStyleProperty = (root, name, value) => {
@@ -102,6 +148,162 @@
}
};
+ const fillComposer = (text) => {
+ const editor = document.querySelector('.ProseMirror[contenteditable="true"]');
+ if (!editor) return;
+ editor.focus();
+ const selection = window.getSelection?.();
+ if (selection && typeof document.createRange === "function") {
+ const range = document.createRange();
+ range.selectNodeContents(editor);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ let inserted = false;
+ try {
+ inserted = document.execCommand?.("insertText", false, text) === true;
+ } catch {}
+ if (!inserted) {
+ editor.textContent = text;
+ const InputEventConstructor = window.InputEvent || window.Event;
+ if (InputEventConstructor) {
+ editor.dispatchEvent(new InputEventConstructor("input", {
+ bubbles: true,
+ inputType: "insertText",
+ data: text,
+ }));
+ }
+ }
+ };
+
+ const suggestionGridFor = (group) => {
+ if (!group) return null;
+ for (const child of group.children ?? []) {
+ if (child.classList?.contains("group/home-suggestions-header")) continue;
+ if (child.classList?.contains("grid")) return child;
+ const nested = child.querySelector?.('[class~="grid"]');
+ if (nested) return nested;
+ }
+ const legacyContainer = group.children?.[0] ?? null;
+ if (legacyContainer?.classList?.contains("group/home-suggestions-header")) return null;
+ return legacyContainer?.children?.[0] ?? null;
+ };
+
+ const findCurrentSuggestionPortal = (home, question) => {
+ for (let node = question; node && node !== home; node = node.parentElement) {
+ const sibling = node.nextElementSibling;
+ if (sibling?.classList?.contains("absolute") &&
+ sibling.classList.contains("top-full")) return sibling;
+ }
+ return null;
+ };
+
+ const resolveHomeSuggestions = (home, question, locale) => {
+ const generated = [...document.querySelectorAll(`[${HOME_GENERATED_SUGGESTIONS_ATTR}]`)];
+ const portal = locale ? findCurrentSuggestionPortal(home, question) : null;
+ const nativeGroups = home
+ ? [...home.querySelectorAll('[class~="group/home-suggestions"]')]
+ .filter((candidate) => candidate.getAttribute(HOME_GENERATED_SUGGESTIONS_ATTR) === null)
+ : [];
+ const native = nativeGroups.find((candidate) => suggestionGridFor(candidate)) ?? null;
+ for (const candidate of document.querySelectorAll(`[${HOME_EMPTY_SUGGESTIONS_ATTR}]`)) {
+ if (native || !portal || !nativeGroups.includes(candidate)) {
+ candidate.removeAttribute(HOME_EMPTY_SUGGESTIONS_ATTR);
+ }
+ }
+ if (native) {
+ for (const candidate of generated) candidate.remove();
+ return native;
+ }
+
+ if (!portal) {
+ for (const candidate of generated) candidate.remove();
+ return null;
+ }
+
+ for (const candidate of nativeGroups) setAttribute(candidate, HOME_EMPTY_SUGGESTIONS_ATTR, "");
+ let group = generated.find((candidate) => portal.contains(candidate)) ?? null;
+ for (const candidate of generated) {
+ if (candidate !== group) candidate.remove();
+ }
+ if (!group) {
+ group = document.createElement("section");
+ group.classList.add("group/home-suggestions");
+ group.setAttribute(HOME_GENERATED_SUGGESTIONS_ATTR, "");
+ const container = document.createElement("div");
+ container.appendChild(document.createElement("div"));
+ group.appendChild(container);
+ portal.appendChild(group);
+ }
+ return group;
+ };
+
+ // Codex can replace the four curated actions with fewer generated suggestions.
+ // Keep matching native buttons and their React handlers, then fill only the gaps.
+ const syncHomeSuggestions = (group, locale) => {
+ const suggestions = HOME_SUGGESTIONS[locale] ?? null;
+ for (const card of document.querySelectorAll(`[${HOME_CARD_ATTR}]`)) {
+ if (!group || !suggestions || !group.contains(card)) card.remove();
+ }
+ for (const card of document.querySelectorAll(`[${HOME_NATIVE_CARD_ATTR}]`)) {
+ if (!group || !suggestions || !group.contains(card)) {
+ card.removeAttribute(HOME_NATIVE_CARD_ATTR);
+ }
+ }
+ for (const candidate of document.querySelectorAll(`[${HOME_SUGGESTIONS_ATTR}]`)) {
+ if (candidate !== group || !suggestions) candidate.removeAttribute(HOME_SUGGESTIONS_ATTR);
+ }
+ for (const candidate of document.querySelectorAll(`[${HOME_GRID_ATTR}]`)) {
+ if (!group?.contains(candidate) || !suggestions) candidate.removeAttribute(HOME_GRID_ATTR);
+ }
+ if (!group || !suggestions) return;
+
+ setAttribute(group, HOME_SUGGESTIONS_ATTR, locale);
+ const suggestionByLabel = new Map(suggestions.map((suggestion) => [suggestion[1], suggestion]));
+ const nativeById = new Map();
+ const grid = suggestionGridFor(group);
+ if (!grid) return;
+ setAttribute(grid, HOME_GRID_ATTR, "");
+ for (const button of grid.querySelectorAll("button")) {
+ let wrapper = button;
+ while (wrapper.parentElement && wrapper.parentElement !== grid) {
+ wrapper = wrapper.parentElement;
+ }
+ if (wrapper.parentElement !== grid || wrapper.getAttribute(HOME_CARD_ATTR) !== null) continue;
+ const label = String(button.innerText || button.textContent || "").replace(/\s+/g, " ").trim();
+ const suggestion = suggestionByLabel.get(label);
+ if (suggestion && !nativeById.has(suggestion[0])) {
+ setAttribute(wrapper, HOME_NATIVE_CARD_ATTR, suggestion[0]);
+ nativeById.set(suggestion[0], wrapper);
+ } else {
+ setAttribute(wrapper, HOME_NATIVE_CARD_ATTR, "hidden");
+ }
+ }
+
+ const syntheticById = new Map([...group.querySelectorAll(`[${HOME_CARD_ATTR}]`)]
+ .map((card) => [card.getAttribute(HOME_CARD_ATTR), card]));
+ for (const [id, label, prompt] of suggestions) {
+ const existing = syntheticById.get(id);
+ if (nativeById.has(id)) {
+ existing?.remove();
+ } else if (!existing || existing.parentElement !== grid) {
+ existing?.remove();
+ const card = document.createElement("div");
+ card.setAttribute(HOME_CARD_ATTR, id);
+ const button = document.createElement("button");
+ button.type = "button";
+ button.setAttribute("aria-label", label);
+ button.innerHTML = `${HOME_SUGGESTION_ICONS[id]}` +
+ `${label}`;
+ button.addEventListener("click", () => fillComposer(prompt));
+ card.appendChild(button);
+ grid.appendChild(card);
+ }
+ syntheticById.delete(id);
+ }
+ for (const card of syntheticById.values()) card.remove();
+ };
+
const parseRgb = (value) => {
if (!value || value === "transparent") return null;
const hex = String(value).trim().match(/^#([0-9a-f]{6})$/i);
@@ -552,7 +754,10 @@
setStyleProperty(root, "--dream-skin-art", `url("${artUrl}")`);
applyTheme(root, shell);
applyArtMetadata(root);
- root.classList.add("codex-dream-skin");
+ if (!root.classList.contains("codex-dream-skin")) {
+ root.classList.add("codex-dream-skin");
+ metrics.rootClassWrites += 1;
+ }
return shell;
};
@@ -563,14 +768,43 @@
shell ||= root.getAttribute(SHELL_ATTR) || resolvedShell();
const shellMain = document.querySelector("main.main-surface") || document.querySelector("main");
const homeIndicator = document.querySelector('[data-testid="home-icon"]');
+ const homeQuestionSignal = document.querySelector('[data-feature="game-source"]');
const home = homeIndicator?.closest('[role="main"]') ||
+ homeQuestionSignal?.closest?.('[role="main"]') ||
[...document.querySelectorAll('[role="main"]')].find((candidate) =>
- candidate.querySelector('[data-feature="game-source"]') &&
- candidate.querySelector('.group\\\\/home-suggestions')) || null;
+ candidate.querySelector('[data-feature="game-source"]')) || null;
for (const candidate of document.querySelectorAll('[role="main"].dream-skin-home')) {
if (candidate !== home) candidate.classList.remove("dream-skin-home");
}
if (home) home.classList.add("dream-skin-home");
+ const homeQuestion = home?.querySelector('[data-feature="game-source"]') ?? null;
+ let homeLocale = null;
+ for (const candidate of document.querySelectorAll(`[${HOME_QUESTION_ATTR}]`)) {
+ if (candidate !== homeQuestion) candidate.removeAttribute(HOME_QUESTION_ATTR);
+ }
+ if (homeQuestion) {
+ const questionClone = homeQuestion.cloneNode(true);
+ questionClone.querySelectorAll("button").forEach((button) => button.remove());
+ const nativeQuestion = questionClone.textContent.replace(/\s+/g, " ").trim();
+ let compactQuestion = null;
+ if (/^我们应该(?:在\s*中)?构建什么[??]?$/.test(nativeQuestion)) {
+ compactQuestion = "我们应该构建什么?";
+ homeLocale = "zh";
+ } else if (/^我们应该(?:在\s*中)?做些什么[??]?$/.test(nativeQuestion)) {
+ compactQuestion = "我们应该做些什么?";
+ homeLocale = "zh";
+ } else if (/^what should we build(?: in)?\s*[??]?$/i.test(nativeQuestion)) {
+ compactQuestion = "What should we build?";
+ homeLocale = "en";
+ } else if (/^what should we work on(?: in)?\s*[??]?$/i.test(nativeQuestion)) {
+ compactQuestion = "What should we work on?";
+ homeLocale = "en";
+ }
+ if (compactQuestion) setAttribute(homeQuestion, HOME_QUESTION_ATTR, compactQuestion);
+ else homeQuestion.removeAttribute(HOME_QUESTION_ATTR);
+ }
+ const homeSuggestions = resolveHomeSuggestions(home, homeQuestion, homeLocale);
+ syncHomeSuggestions(homeSuggestions, homeLocale);
const homeUtilityBars = new Set(home
? home.querySelectorAll('[class*="_homeUtilityBar_"]')
: []);
@@ -656,6 +890,7 @@
document.querySelectorAll(".dream-skin-home").forEach((node) => node.classList.remove("dream-skin-home"));
document.querySelectorAll(".dream-skin-home-shell").forEach((node) => node.classList.remove("dream-skin-home-shell"));
document.querySelectorAll(".dream-skin-home-utility").forEach((node) => node.classList.remove("dream-skin-home-utility"));
+ clearHomeEnhancements();
document.getElementById(STYLE_ID)?.remove();
document.getElementById(CHROME_ID)?.remove();
state?.observer?.disconnect();
diff --git a/macos/scripts/injector.mjs b/macos/scripts/injector.mjs
index a37b67e2..de142ecf 100644
--- a/macos/scripts/injector.mjs
+++ b/macos/scripts/injector.mjs
@@ -203,13 +203,15 @@ function validatedDebuggerUrl(target, port) {
return url.href;
}
-function isValidCdpPageTarget(item, port) {
+export function isValidCdpPageTarget(item, port) {
if (
item?.type !== "page" || !item.url?.startsWith("app://")
|| typeof item.id !== "string" || !CDP_ID_PATTERN.test(item.id)
|| !item.webSocketDebuggerUrl
) return false;
try {
+ const targetUrl = new URL(item.url);
+ if (targetUrl.searchParams.get("initialRoute") === "/avatar-overlay") return false;
const debuggerUrl = new URL(validatedDebuggerUrl(item, port));
return debuggerUrl.pathname === `/devtools/page/${item.id}`;
} catch {
@@ -217,7 +219,7 @@ function isValidCdpPageTarget(item, port) {
}
}
-class CdpSession {
+export class CdpSession {
constructor(target, port) {
this.target = target;
this.ws = new WebSocket(validatedDebuggerUrl(target, port));
@@ -228,27 +230,32 @@ class CdpSession {
}
async open() {
- await new Promise((resolve, reject) => {
- const timeout = setTimeout(() => {
- try { this.ws.close(); } catch {}
- reject(new Error("CDP WebSocket open timed out"));
- }, 5000);
- this.ws.addEventListener("open", () => { clearTimeout(timeout); resolve(); }, { once: true });
- this.ws.addEventListener("error", () => { clearTimeout(timeout); reject(new Error("CDP WebSocket open failed")); }, { once: true });
- });
- this.ws.addEventListener("message", (event) => this.onMessage(event));
- this.ws.addEventListener("error", () => this.close());
- this.ws.addEventListener("close", () => {
- this.closed = true;
- for (const waiter of this.pending.values()) {
- clearTimeout(waiter.timeout);
- waiter.reject(new Error("CDP socket closed"));
- }
- this.pending.clear();
- });
- await this.send("Runtime.enable");
- await this.send("Page.enable");
- return this;
+ try {
+ await new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ try { this.ws.close(); } catch {}
+ reject(new Error("CDP WebSocket open timed out"));
+ }, 5000);
+ this.ws.addEventListener("open", () => { clearTimeout(timeout); resolve(); }, { once: true });
+ this.ws.addEventListener("error", () => { clearTimeout(timeout); reject(new Error("CDP WebSocket open failed")); }, { once: true });
+ });
+ this.ws.addEventListener("message", (event) => this.onMessage(event));
+ this.ws.addEventListener("error", () => this.close());
+ this.ws.addEventListener("close", () => {
+ this.closed = true;
+ for (const waiter of this.pending.values()) {
+ clearTimeout(waiter.timeout);
+ waiter.reject(new Error("CDP socket closed"));
+ }
+ this.pending.clear();
+ });
+ await this.send("Runtime.enable");
+ await this.send("Page.enable");
+ return this;
+ } catch (error) {
+ this.close();
+ throw error;
+ }
}
onMessage(event) {
@@ -840,8 +847,10 @@ async function verifySession(session, expectedThemeId = null, expectedRevision =
document.querySelector('.group\\\\/home-suggestions');
const homeRoute = homeSignal?.closest('[role="main"]') ?? null;
const home = document.querySelector('[role="main"].dream-skin-home');
- const suggestions = home?.querySelector('.group\\\\/home-suggestions') ?? null;
- const cardButtons = suggestions ? [...suggestions.querySelectorAll('button')] : [];
+ const homeQuestion = home?.querySelector('[data-feature="game-source"]') ?? null;
+ const suggestions = home?.querySelector('[data-dream-skin-home-suggestions]') ?? null;
+ const suggestionGrid = suggestions?.querySelector('[data-dream-skin-home-grid]') ?? null;
+ const cardButtons = suggestionGrid ? [...suggestionGrid.querySelectorAll('button')] : [];
const cardBoxes = cardButtons.map(box);
const visibleCards = cardBoxes.filter((item) => item?.visible);
const suggestionLabels = cardButtons.flatMap((button) => {
@@ -857,14 +866,15 @@ async function verifySession(session, expectedThemeId = null, expectedRevision =
}));
});
const visibleSuggestionLabels = suggestionLabels.filter((item) => item?.visible);
- const suggestionLabelColorsMatch = visibleSuggestionLabels.every((item) =>
- item.color === item.expectedColor);
const hero = box(home?.firstElementChild?.firstElementChild?.firstElementChild);
- const projectButton = box(home?.querySelector('.group\\\\/project-selector > button'));
+ const projectButton = box(homeQuestion?.querySelector('button') ??
+ home?.querySelector('.group\\\\/project-selector > button'));
const shell = box(document.querySelector('main.main-surface'));
const composer = box(document.querySelector('.composer-surface-chrome'));
const sidebar = box(document.querySelector('aside.app-shell-left-panel'));
const chrome = document.getElementById('codex-dream-skin-chrome');
+ const suggestionLabelColorsMatch = visibleSuggestionLabels.every((item) =>
+ item.color === item.expectedColor);
const result = {
installed: document.documentElement.classList.contains('codex-dream-skin'),
version: window.__CODEX_DREAM_SKIN_STATE__?.version ?? null,
@@ -875,7 +885,9 @@ async function verifySession(session, expectedThemeId = null, expectedRevision =
chromePointerEvents: getComputedStyle(chrome || document.body).pointerEvents,
homeRoute: Boolean(homeRoute),
homePresent: Boolean(home),
+ homeEnhanced: Boolean(homeQuestion?.hasAttribute('data-dream-skin-question')),
hero,
+ suggestions: box(suggestions),
cards: cardBoxes,
visibleCardCount: visibleCards.length,
suggestionLabels,
@@ -900,7 +912,11 @@ async function verifySession(session, expectedThemeId = null, expectedRevision =
// Project selector markup varies across Codex builds — soft requirement.
const homePass = !result.homeRoute || (
result.homePresent && result.hero?.visible && result.hero.width >= 280 &&
- result.hero.height >= 120 && (result.visibleCardCount === 0 || (
+ result.hero.height >= 120 && (!result.homeEnhanced || (
+ result.visibleCardCount === 4 &&
+ result.suggestions?.visible && result.composer?.visible &&
+ Math.abs(result.suggestions.x - result.composer.x) <= 2 &&
+ Math.abs(result.suggestions.width - result.composer.width) <= 2 &&
visibleSuggestionLabels.length >= result.visibleCardCount &&
result.suggestionLabelColorsMatch
))
@@ -911,7 +927,7 @@ async function verifySession(session, expectedThemeId = null, expectedRevision =
result.softNotes = {
projectButtonOptional: !result.projectButton?.visible,
composerOptionalOnNonTaskRoutes: !result.composer?.visible,
- suggestionCardsOptional: result.homeRoute && result.visibleCardCount === 0,
+ suggestionCardsOptional: result.homeRoute && !result.homeEnhanced,
};
return result;
})()`);
@@ -1102,20 +1118,46 @@ async function runOneShot(options) {
export function earlyPayloadFor(payload, revision) {
return `(() => {
+ const isAuxiliaryRoute = () =>
+ /(?:^|[?&])initialRoute=(?:%2f|\\/)avatar-overlay(?:&|$)/i.test(
+ window.location?.search || "",
+ );
+ if (window.top !== window || window.location?.protocol !== "app:" || isAuxiliaryRoute()) return;
const generationKey = "__CODEX_DREAM_SKIN_EARLY_GENERATION__";
const appliedKey = "__CODEX_DREAM_SKIN_EARLY_APPLIED__";
+ const bootstrapKey = "__CODEX_DREAM_SKIN_EARLY_BOOTSTRAP__";
const generation = ${JSON.stringify(revision)};
+ const previousBootstrap = window[bootstrapKey];
+ if (typeof previousBootstrap?.stop === "function") previousBootstrap.stop();
window[generationKey] = generation;
- let observer = null;
+ let interval = null;
let timeout = null;
+ let controller = null;
+ const onReady = () => install();
const stop = () => {
- observer?.disconnect();
- observer = null;
+ if (interval) clearInterval(interval);
+ interval = null;
if (timeout) clearTimeout(timeout);
timeout = null;
+ document.removeEventListener?.("DOMContentLoaded", onReady);
+ if (window[bootstrapKey] === controller) delete window[bootstrapKey];
};
+ controller = { stop };
+ window[bootstrapKey] = controller;
const install = () => {
if (window[generationKey] !== generation) { stop(); return true; }
+ if (isAuxiliaryRoute()) { stop(); return true; }
+ const state = window.__CODEX_DREAM_SKIN_STATE__;
+ if (
+ window[appliedKey] === generation &&
+ state?.revision === generation &&
+ typeof state.ensure === "function" &&
+ typeof state.cleanup === "function" &&
+ !window.__CODEX_DREAM_SKIN_DISABLED__
+ ) {
+ stop();
+ return true;
+ }
if (!document.documentElement) return false;
const shell = document.querySelector('main.main-surface');
const sidebar = document.querySelector('aside.app-shell-left-panel');
@@ -1126,10 +1168,8 @@ export function earlyPayloadFor(payload, revision) {
return true;
};
if (install()) return;
- if (typeof MutationObserver === "function" && document.documentElement) {
- observer = new MutationObserver(install);
- observer.observe(document.documentElement, { childList: true, subtree: true });
- }
+ document.addEventListener?.("DOMContentLoaded", onReady);
+ interval = setInterval(install, 250);
timeout = setTimeout(stop, 10000);
})()`;
}
diff --git a/macos/tests/injector-bootstrap.test.mjs b/macos/tests/injector-bootstrap.test.mjs
index 21330596..59be9bff 100644
--- a/macos/tests/injector-bootstrap.test.mjs
+++ b/macos/tests/injector-bootstrap.test.mjs
@@ -1,23 +1,34 @@
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import path from "node:path";
+import test from "node:test";
import vm from "node:vm";
import { fileURLToPath } from "node:url";
-import { earlyPayloadFor } from "../scripts/injector.mjs";
+import * as injector from "../scripts/injector.mjs";
+
+const { earlyPayloadFor } = injector;
const here = path.dirname(fileURLToPath(import.meta.url));
const injectorPath = path.resolve(here, "../scripts/injector.mjs");
const source = await fs.readFile(injectorPath, "utf8");
-function createFixture() {
+function createFixture({ topFrame = true, protocol = "app:" } = {}) {
const observers = [];
const timers = new Map();
+ const intervals = new Map();
+ const listeners = new Map();
let nextTimer = 1;
const markers = { shell: false, sidebar: false };
+ const window = { installs: [], location: { protocol, search: "" } };
+ window.top = topFrame ? window : {};
const context = {
- window: { installs: [] },
+ window,
document: {
documentElement: {},
+ addEventListener(type, listener) { listeners.set(type, listener); },
+ removeEventListener(type, listener) {
+ if (listeners.get(type) === listener) listeners.delete(type);
+ },
querySelector(selector) {
if (selector === "main.main-surface") return markers.shell ? {} : null;
if (selector === "aside.app-shell-left-panel") return markers.sidebar ? {} : null;
@@ -39,29 +50,178 @@ function createFixture() {
return id;
},
clearTimeout(id) { timers.delete(id); },
+ setInterval(callback) {
+ const id = nextTimer++;
+ intervals.set(id, callback);
+ return id;
+ },
+ clearInterval(id) { intervals.delete(id); },
+ };
+ const tick = () => {
+ for (const observer of observers) {
+ if (observer.connected) observer.callback([]);
+ }
+ for (const callback of [...intervals.values()]) callback();
};
- return { context, markers, observers };
+ return { context, intervals, listeners, markers, observers, tick, timers };
}
-const guarded = createFixture();
-vm.runInNewContext(earlyPayloadFor('window.installs.push("guarded")', "guarded"), guarded.context);
-assert.deepEqual(guarded.context.window.installs, [], "Auxiliary app targets must remain untouched.");
-guarded.markers.shell = true;
-guarded.observers[0].callback([]);
-assert.deepEqual(guarded.context.window.installs, [], "A main surface without the Codex sidebar is not sufficient.");
+test("early injection uses one bounded bootstrap and installs only in the top-level app shell", () => {
+ const guarded = createFixture();
+ vm.runInNewContext(earlyPayloadFor('window.installs.push("guarded")', "guarded"), guarded.context);
+ assert.equal(guarded.observers.length, 0, "Bootstrap must not retain a broad subtree MutationObserver.");
+ assert.equal(guarded.intervals.size, 1, "A missing shell should have one bounded polling bootstrap.");
+ guarded.markers.shell = true;
+ guarded.tick();
+ assert.deepEqual(guarded.context.window.installs, [], "A main surface without the Codex sidebar is not sufficient.");
+ guarded.markers.sidebar = true;
+ guarded.tick();
+ assert.deepEqual(guarded.context.window.installs, ["guarded"]);
+ assert.equal(guarded.intervals.size, 0, "A successful install must clear its bootstrap interval.");
-const generations = createFixture();
-vm.runInNewContext(earlyPayloadFor('window.installs.push("old")', "old"), generations.context);
-vm.runInNewContext(earlyPayloadFor('window.installs.push("new")', "new"), generations.context);
-generations.markers.shell = true;
-generations.markers.sidebar = true;
-for (const observer of generations.observers) observer.callback([]);
-assert.deepEqual(
- generations.context.window.installs,
- ["new"],
- "A stale early script must yield to the newest watcher generation.",
-);
-assert.equal(generations.context.window.__CODEX_DREAM_SKIN_EARLY_APPLIED__, "new");
+ for (const fixture of [
+ createFixture({ topFrame: false }),
+ createFixture({ protocol: "https:" }),
+ ]) {
+ vm.runInNewContext(earlyPayloadFor('window.installs.push("unexpected")', "ignored"), fixture.context);
+ assert.deepEqual(fixture.context.window.installs, []);
+ assert.equal(fixture.observers.length, 0);
+ assert.equal(fixture.intervals.size, 0);
+ assert.equal(fixture.timers.size, 0);
+ }
+});
+
+test("same-revision Runtime.evaluate replaces the previous bootstrap instead of stacking work", () => {
+ const generations = createFixture();
+ for (let index = 0; index < 40; index += 1) {
+ vm.runInNewContext(
+ earlyPayloadFor('window.installs.push("same")', "same"),
+ generations.context,
+ );
+ }
+ assert.equal(generations.observers.length, 0);
+ assert.equal(generations.intervals.size, 1, "Only the newest bootstrap interval may remain active.");
+ assert.equal(generations.timers.size, 1, "Only the newest bootstrap timeout may remain active.");
+ assert.equal(generations.listeners.size, 1, "Only the newest DOM-ready listener may remain active.");
+ generations.markers.shell = true;
+ generations.markers.sidebar = true;
+ generations.tick();
+ assert.deepEqual(generations.context.window.installs, ["same"]);
+ assert.equal(generations.context.window.__CODEX_DREAM_SKIN_EARLY_APPLIED__, "same");
+});
+
+test("same-revision reconnect skips a healthy renderer but restores a missing state", () => {
+ const ready = createFixture();
+ ready.markers.shell = true;
+ ready.markers.sidebar = true;
+ const payload = `
+ window.installs.push("same");
+ window.__CODEX_DREAM_SKIN_STATE__ = {
+ revision: "same",
+ ensure() {},
+ cleanup() {},
+ };
+ `;
+ vm.runInNewContext(earlyPayloadFor(payload, "same"), ready.context);
+ vm.runInNewContext(earlyPayloadFor(payload, "same"), ready.context);
+ assert.deepEqual(
+ ready.context.window.installs,
+ ["same"],
+ "A reconnect must not rebuild a healthy renderer at the same revision.",
+ );
+
+ delete ready.context.window.__CODEX_DREAM_SKIN_STATE__;
+ vm.runInNewContext(earlyPayloadFor(payload, "same"), ready.context);
+ assert.deepEqual(
+ ready.context.window.installs,
+ ["same", "same"],
+ "A stale applied marker must not block recovery after renderer state cleanup.",
+ );
+});
+
+test("an early bootstrap aborts if its target navigates to the avatar overlay", () => {
+ const navigated = createFixture();
+ vm.runInNewContext(
+ earlyPayloadFor('window.installs.push("unexpected")', "navigated"),
+ navigated.context,
+ );
+ navigated.context.window.location.search = "?initialRoute=%2Favatar-overlay";
+ navigated.markers.shell = true;
+ navigated.markers.sidebar = true;
+ navigated.tick();
+ assert.deepEqual(navigated.context.window.installs, []);
+ assert.equal(navigated.intervals.size, 0);
+ assert.equal(navigated.timers.size, 0);
+});
+
+test("an unused early bootstrap releases its timer and listener at the deadline", () => {
+ const expired = createFixture();
+ vm.runInNewContext(
+ earlyPayloadFor('window.installs.push("unexpected")', "expired"),
+ expired.context,
+ );
+ assert.equal(expired.intervals.size, 1);
+ assert.equal(expired.listeners.size, 1);
+ const [timeout] = expired.timers.values();
+ timeout();
+ assert.equal(expired.intervals.size, 0);
+ assert.equal(expired.listeners.size, 0);
+ assert.equal(expired.context.window.__CODEX_DREAM_SKIN_EARLY_BOOTSTRAP__, undefined);
+});
+
+test("auxiliary app routes are rejected before a CDP session is opened", () => {
+ assert.equal(typeof injector.isValidCdpPageTarget, "function");
+ const target = {
+ type: "page",
+ id: "target-1",
+ url: "app://-/index.html",
+ webSocketDebuggerUrl: "ws://127.0.0.1:9341/devtools/page/target-1",
+ };
+ assert.equal(injector.isValidCdpPageTarget(target, 9341), true);
+ assert.equal(injector.isValidCdpPageTarget({
+ ...target,
+ url: "app://-/index.html?initialRoute=%2Favatar-overlay",
+ }, 9341), false);
+});
+
+test("a failed CDP domain handshake closes the just-opened socket", async () => {
+ assert.equal(typeof injector.CdpSession, "function");
+ const previousWebSocket = globalThis.WebSocket;
+ let socket = null;
+ class FakeWebSocket {
+ constructor() {
+ socket = this;
+ this.closed = false;
+ this.listeners = new Map();
+ queueMicrotask(() => this.emit("open"));
+ }
+ addEventListener(type, listener) {
+ const listeners = this.listeners.get(type) ?? [];
+ listeners.push(listener);
+ this.listeners.set(type, listeners);
+ }
+ emit(type, event = {}) {
+ for (const listener of this.listeners.get(type) ?? []) listener(event);
+ }
+ close() {
+ this.closed = true;
+ this.emit("close");
+ }
+ }
+ globalThis.WebSocket = FakeWebSocket;
+ try {
+ const session = new injector.CdpSession({
+ id: "target-1",
+ webSocketDebuggerUrl: "ws://127.0.0.1:9341/devtools/page/target-1",
+ }, 9341);
+ session.send = async () => { throw new Error("Runtime.enable timed out"); };
+ await assert.rejects(session.open(), /Runtime\.enable timed out/);
+ assert.equal(socket.closed, true);
+ assert.equal(session.closed, true);
+ } finally {
+ globalThis.WebSocket = previousWebSocket;
+ }
+});
const discoveryStart = source.indexOf("record.earlyScriptId = await registerEarly");
const probeStart = source.indexOf("const probe = await waitForCodexProbe", discoveryStart);
@@ -81,5 +241,3 @@ assert.match(
/const suggestionLabelColorsMatch = visibleSuggestionLabels\.every\([\s\S]{0,2500}visibleSuggestionLabels\.length >= result\.visibleCardCount[\s\S]{0,160}result\.suggestionLabelColorsMatch/,
"Live verification must reject visible home suggestion labels that diverge from the themed card color.",
);
-
-console.log("PASS: early injection is shell-guarded, generation-safe, and removed on shutdown.");
diff --git a/macos/tests/renderer-inject.test.mjs b/macos/tests/renderer-inject.test.mjs
index 7780a89f..ed1443b9 100644
--- a/macos/tests/renderer-inject.test.mjs
+++ b/macos/tests/renderer-inject.test.mjs
@@ -85,9 +85,49 @@ assert.match(
/\[class\*="_homeUtilityBar_"\][\s\S]{0,500}dream-skin-home-utility/,
"The renderer should give the current native home utility bar a stable theme class.",
);
+assert.match(
+ template,
+ /\[class~="group\/home-suggestions"\]/,
+ "The renderer should select the slash-containing native class without fragile CSS escaping.",
+);
+assert.match(
+ css,
+ /data-dream-skin-question\]::before\s*\{[\s\S]{0,120}content:\s*attr\(data-dream-skin-question\);/,
+ "The compact question should render from the localized DOM attribute instead of hard-coded visible markup.",
+);
+assert.match(
+ css,
+ /data-dream-skin-home-grid\]\s*\{[\s\S]{0,160}grid-template-columns:\s*repeat\(2,\s*minmax\(0,\s*1fr\)\) !important;/,
+ "The marked suggestion grid should use two columns on desktop.",
+);
+assert.match(
+ css,
+ /\[class~="absolute"\]\[class~="top-full"\]:has\(> \[data-dream-skin-generated-home-suggestions\]\)[\s\S]{0,260}width:\s*calc\(100% - 52px\) !important;[\s\S]{0,160}transform:\s*translateX\(-50%\) !important;/,
+ "Current-renderer fallback suggestions should align with the composer without changing legacy layout.",
+);
+assert.match(
+ css,
+ /group\\\/home-suggestions button\s*\{[\s\S]{0,160}min-height:\s*72px !important;/,
+ "Desktop home suggestions should not expand into oversized feature cards.",
+);
+assert.match(
+ css,
+ /data-dream-skin-home-grid\]\s*>\s*\*\s*\{[\s\S]{0,80}display:\s*block !important;/,
+ "All four native suggestions must remain visible despite Codex container-query hiding rules.",
+);
assert.match(
css,
- /\.dream-skin-home:has\(\.dream-skin-home-utility\)[\s\S]{0,120}\.composer-surface-chrome\s*\{[\s\S]{0,180}border-radius:\s*0 0 22px 22px !important;/,
+ /data-dream-skin-native-home-card="hidden"\][\s\S]{0,80}display:\s*none !important;/,
+ "Native suggestions outside the fixed four-card set should not displace the requested actions.",
+);
+assert.match(
+ css,
+ /@media \(max-width:\s*700px\)[\s\S]{0,1400}data-dream-skin-home-grid\]\s*\{[\s\S]{0,100}grid-template-columns:\s*minmax\(0,\s*1fr\) !important;/,
+ "The four-action grid must collapse to one column on narrow windows.",
+);
+assert.match(
+ css,
+ /\.dream-skin-home:has\(\.dream-skin-home-utility\)[\s\S]{0,120}\.composer-surface-chrome\s*\{[\s\S]{0,180}border-radius:\s*0 0 16px 16px !important;/,
"The home utility bar and composer should render as one continuous control.",
);
assert.match(
@@ -130,6 +170,16 @@ assert.match(
/\[class~="bg-token-main-surface-primary"\]\[class~="h-full"\]\[class~="w-full"\][\s\S]{0,100}background:\s*transparent !important;/,
"Full-size utility route wrappers should not hide the selected artwork.",
);
+assert.match(
+ css,
+ /\[class~="top-\(--thread-floating-content-top-inset\)"\]\[class~="right-0"\]\[class~="z-40"\][\s\S]{0,220}\[class~="rounded-3xl"\]\[class~="bg-token-dropdown-background"\]\s*\{[\s\S]{0,220}background:\s*var\(--ds-immersive-composer\) !important;[\s\S]{0,220}backdrop-filter:\s*blur\(16px\) saturate\(108%\) !important;/,
+ "The native thread detail panel should reveal wide task artwork through a blurred theme surface.",
+);
+assert.match(
+ css,
+ /\[class~="rounded-3xl"\]\[class~="bg-token-dropdown-background"\][\s\S]{0,220}header\[class~="bg-token-dropdown-background"\]\s*\{[\s\S]{0,180}background:\s*transparent !important;[\s\S]{0,120}backdrop-filter:\s*none !important;/,
+ "Sticky thread detail headings should inherit the panel glass instead of adding dark overlay strips.",
+);
function createStyleDeclaration() {
const values = new Map();
@@ -141,11 +191,14 @@ function createStyleDeclaration() {
};
}
-function createClassList(initial = []) {
+function createClassList(initial = [], onAdd = null) {
const values = new Set(initial);
return {
values,
- add(...names) { for (const name of names) values.add(name); },
+ add(...names) {
+ onAdd?.(names);
+ for (const name of names) values.add(name);
+ },
remove(...names) { for (const name of names) values.delete(name); },
contains(name) { return values.has(name); },
toggle(name, enabled) {
@@ -159,6 +212,10 @@ function createFixture(theme, {
nativeShell = "light",
analysisFixture = null,
analysisCache = null,
+ homeQuestionText = null,
+ nativeSuggestionLabels = [],
+ homeMarkup = "legacy",
+ currentSuggestionShell = "none",
} = {}) {
let fixtureShell = nativeShell;
const nodes = new Map();
@@ -170,9 +227,10 @@ function createFixture(theme, {
let nextTimer = 1;
let nextBlob = 1;
const rootStyle = createStyleDeclaration();
+ const rootClassWrites = { addCalls: 0 };
const root = {
className: nativeShell === "dark" ? "electron-dark" : "electron-light",
- classList: createClassList(),
+ classList: createClassList([], () => { rootClassWrites.addCalls += 1; }),
style: rootStyle,
appendChild(node) {
node.parentElement = root;
@@ -198,38 +256,170 @@ function createFixture(theme, {
return { ...shellBox };
},
};
-
- const createElement = (tagName) => {
- if (tagName === "canvas" && analysisFixture) {
- return {
- width: 0,
- height: 0,
- getContext() {
- return {
- drawImage() {},
- getImageData() { return { data: analysisFixture.pixels }; },
- };
- },
- };
- }
+ const createBasicElement = (tagName) => {
const childNodes = new Map();
+ const elementAttributes = new Map();
+ const listeners = new Map();
const element = {
+ tagName: String(tagName).toUpperCase(),
id: "",
dataset: {},
style: createStyleDeclaration(),
classList: createClassList(),
parentElement: null,
+ children: [],
textContent: "",
+ innerText: "",
innerHTML: "",
- setAttribute() {},
+ get nextElementSibling() {
+ const siblings = element.parentElement?.children ?? [];
+ const index = siblings.indexOf(element);
+ return index >= 0 ? siblings[index + 1] ?? null : null;
+ },
+ setAttribute(name, value) { elementAttributes.set(name, String(value)); },
+ getAttribute(name) { return elementAttributes.get(name) ?? null; },
+ removeAttribute(name) { elementAttributes.delete(name); },
+ appendChild(node) {
+ node.parentElement = element;
+ element.children.push(node);
+ return node;
+ },
+ contains(node) {
+ return node === element || element.children.some((child) => child.contains?.(node));
+ },
+ addEventListener(type, listener) { listeners.set(type, listener); },
+ click() { listeners.get("click")?.({ preventDefault() {} }); },
querySelector(selector) {
+ if (/^\[[^\]]+\]$/.test(selector)) return element.querySelectorAll(selector)[0] ?? null;
if (!childNodes.has(selector)) childNodes.set(selector, { textContent: "" });
return childNodes.get(selector);
},
- remove() { if (element.id) nodes.delete(element.id); },
+ querySelectorAll(selector) {
+ const attr = /^\[([^\]=]+)\]$/.exec(selector)?.[1] ?? null;
+ const classToken = /^\[class~="([^"]+)"\]$/.exec(selector)?.[1] ?? null;
+ const matches = (node) => selector === "button"
+ ? node.tagName === "BUTTON"
+ : attr !== null
+ ? node.getAttribute?.(attr) !== null
+ : classToken !== null && node.classList?.contains(classToken);
+ const result = [];
+ const visit = (node) => {
+ if (matches(node)) result.push(node);
+ for (const child of node.children ?? []) visit(child);
+ };
+ for (const child of element.children) visit(child);
+ return result;
+ },
+ remove() {
+ if (element.id) nodes.delete(element.id);
+ if (element.parentElement?.children) {
+ const index = element.parentElement.children.indexOf(element);
+ if (index >= 0) element.parentElement.children.splice(index, 1);
+ }
+ element.parentElement = null;
+ },
};
return element;
};
+ const homeQuestionAttributes = new Map();
+ const homeQuestion = homeQuestionText === null ? null : createBasicElement("div");
+ if (homeQuestion) {
+ homeQuestion.textContent = homeQuestionText;
+ homeQuestion.getAttribute = (name) => homeQuestionAttributes.get(name) ?? null;
+ homeQuestion.setAttribute = (name, value) => homeQuestionAttributes.set(name, String(value));
+ homeQuestion.removeAttribute = (name) => homeQuestionAttributes.delete(name);
+ homeQuestion.cloneNode = () => ({
+ textContent: homeQuestionText,
+ querySelectorAll(selector) {
+ return selector === "button" ? [{ remove() {} }] : [];
+ },
+ });
+ }
+ const suggestionGroup = createBasicElement("section");
+ const suggestionContainer = createBasicElement("div");
+ const suggestionGrid = createBasicElement("div");
+ suggestionGroup.appendChild(suggestionContainer);
+ suggestionContainer.appendChild(suggestionGrid);
+ for (const label of nativeSuggestionLabels) {
+ const wrapper = createBasicElement("div");
+ const button = createBasicElement("button");
+ button.textContent = label;
+ button.innerText = label;
+ wrapper.appendChild(button);
+ suggestionGrid.appendChild(wrapper);
+ }
+ const currentHomeHero = createBasicElement("div");
+ const currentSuggestionPortal = createBasicElement("div");
+ currentSuggestionPortal.classList.add("absolute", "top-full");
+ const currentNativeSuggestionGroup = createBasicElement("section");
+ currentNativeSuggestionGroup.classList.add("group/home-suggestions");
+ const currentNativeSuggestionHeader = createBasicElement("div");
+ currentNativeSuggestionHeader.classList.add("group/home-suggestions-header");
+ const currentNativeHeaderButton = createBasicElement("button");
+ currentNativeHeaderButton.textContent = "Dismiss suggestions";
+ currentNativeSuggestionHeader.appendChild(currentNativeHeaderButton);
+ currentNativeSuggestionGroup.appendChild(currentNativeSuggestionHeader);
+ const currentNativeSuggestionGrid = createBasicElement("div");
+ currentNativeSuggestionGrid.classList.add("grid");
+ if (currentSuggestionShell === "grid") {
+ const motion = createBasicElement("div");
+ motion.appendChild(currentNativeSuggestionGrid);
+ currentNativeSuggestionGroup.appendChild(motion);
+ for (const label of nativeSuggestionLabels) {
+ const wrapper = createBasicElement("div");
+ const button = createBasicElement("button");
+ button.textContent = label;
+ button.innerText = label;
+ wrapper.appendChild(button);
+ currentNativeSuggestionGrid.appendChild(wrapper);
+ }
+ }
+ if (homeQuestion && homeMarkup === "current") {
+ currentHomeHero.appendChild(homeQuestion);
+ currentHomeHero.appendChild(currentSuggestionPortal);
+ if (currentSuggestionShell !== "none") {
+ currentSuggestionPortal.appendChild(currentNativeSuggestionGroup);
+ }
+ }
+ const home = homeQuestion ? {
+ classList: createClassList(),
+ querySelector(selector) {
+ if (selector === '[data-feature="game-source"]') return homeQuestion;
+ return null;
+ },
+ querySelectorAll(selector) {
+ if (selector === '[class~="group/home-suggestions"]') {
+ return homeMarkup === "legacy"
+ ? [suggestionGroup]
+ : currentSuggestionPortal.querySelectorAll(selector);
+ }
+ return [];
+ },
+ } : null;
+ const homeIndicator = home && homeMarkup === "legacy" ? {
+ closest(selector) { return selector === '[role="main"]' ? home : null; },
+ } : null;
+
+ const createElement = (tagName) => {
+ if (tagName === "canvas" && analysisFixture) {
+ return {
+ width: 0,
+ height: 0,
+ getContext() {
+ return {
+ drawImage() {},
+ getImageData() { return { data: analysisFixture.pixels }; },
+ };
+ },
+ };
+ }
+ return createBasicElement(tagName);
+ };
+
+ const composer = createBasicElement("div");
+ composer.focusCount = 0;
+ composer.focus = () => { composer.focusCount += 1; };
+ const composerWrites = [];
const document = {
documentElement: root,
@@ -239,9 +429,49 @@ function createFixture(theme, {
getElementById(id) { return nodes.get(id) ?? null; },
querySelector(selector) {
if (selector === "main.main-surface" || selector === "main") return shellMain;
+ if (selector === '[data-testid="home-icon"]') return homeIndicator;
+ if (selector === '.ProseMirror[contenteditable="true"]') return composer;
return null;
},
- querySelectorAll() { return []; },
+ querySelectorAll(selector) {
+ if (selector === '[role="main"]') return home ? [home] : [];
+ if (selector === '[role="main"].dream-skin-home' || selector === ".dream-skin-home") {
+ return home?.classList.contains("dream-skin-home") ? [home] : [];
+ }
+ if (selector === "[data-dream-skin-question]") {
+ return homeQuestionAttributes.has("data-dream-skin-question") ? [homeQuestion] : [];
+ }
+ if (selector === "[data-dream-skin-home-card]" ||
+ selector === "[data-dream-skin-native-home-card]") {
+ return homeMarkup === "legacy"
+ ? suggestionGroup.querySelectorAll(selector)
+ : currentSuggestionPortal.querySelectorAll(selector);
+ }
+ if (selector === "[data-dream-skin-home-suggestions]") {
+ if (homeMarkup === "legacy") {
+ return suggestionGroup.getAttribute("data-dream-skin-home-suggestions") !== null
+ ? [suggestionGroup] : [];
+ }
+ return currentSuggestionPortal.querySelectorAll(selector);
+ }
+ if (selector === "[data-dream-skin-generated-home-suggestions]") {
+ return currentSuggestionPortal.querySelectorAll(selector);
+ }
+ if (selector === "[data-dream-skin-empty-home-suggestions]") {
+ return currentSuggestionPortal.querySelectorAll(selector);
+ }
+ if (selector === ".dream-skin-home-shell") {
+ return shellMain.classList.contains("dream-skin-home-shell") ? [shellMain] : [];
+ }
+ return [];
+ },
+ execCommand(command, _showUi, value) {
+ composerWrites.push({ command, value });
+ if (command !== "insertText") return false;
+ composer.textContent = value;
+ composer.innerText = value;
+ return true;
+ },
};
const mediaQuery = {
matches: false,
@@ -331,9 +561,12 @@ function createFixture(theme, {
return {
attributes,
body,
- bodyAttributes,
+ composer,
+ composerWrites,
context,
flushTimers,
+ home,
+ homeQuestionAttributes,
nodes,
observers,
payload: payloadFor(theme),
@@ -341,8 +574,15 @@ function createFixture(theme, {
revokedUrls,
resizeObservers,
root,
+ rootClassWrites,
rootStyle,
shellBox,
+ suggestionGrid,
+ suggestionGroup,
+ currentSuggestionPortal,
+ currentNativeHeaderButton,
+ currentNativeSuggestionGrid,
+ currentNativeSuggestionGroup,
timers,
window,
setNativeShell(value) { fixtureShell = value; },
@@ -365,10 +605,22 @@ const defaultMetrics = defaults.window.__CODEX_DREAM_SKIN_STATE__.metrics;
assert.equal(defaultMetrics.rootPasses, 1);
assert.equal(defaultMetrics.routePasses, 1);
assert.equal(defaultMetrics.layoutReads, 1);
+assert.equal(defaults.rootClassWrites.addCalls, 1);
+defaults.window.__CODEX_DREAM_SKIN_STATE__.ensure({ root: true, route: false, layout: false });
+assert.equal(
+ defaults.rootClassWrites.addCalls,
+ 1,
+ "A repeated root pass must not rewrite the observed skin class and retrigger itself.",
+);
+const rootPassesBeforeSubtreeMutations = defaultMetrics.rootPasses;
for (let index = 0; index < 50; index += 1) defaults.observers[0].callback([]);
assert.equal(defaults.timers.size, 1, "Mutation bursts should coalesce into one scheduled ensure.");
defaults.flushTimers(64);
-assert.equal(defaultMetrics.rootPasses, 1, "Subtree mutations must not recompute root theme tokens.");
+assert.equal(
+ defaultMetrics.rootPasses,
+ rootPassesBeforeSubtreeMutations,
+ "Subtree mutations must not recompute root theme tokens.",
+);
assert.equal(defaultMetrics.routePasses, 2);
assert.equal(defaultMetrics.layoutReads, 1, "Subtree mutations must not force shell layout reads.");
assert.equal(defaults.resizeObservers.length, 1);
@@ -382,6 +634,226 @@ const defaultChrome = defaults.nodes.get("codex-dream-skin-chrome");
assert.equal(defaultChrome.style.values.get("left"), "196px");
assert.equal(defaultChrome.style.values.get("width"), "1084px");
+const homeTheme = {
+ id: "home-question-contract",
+ appearance: "auto",
+ art: { safeArea: "auto", taskMode: "auto" },
+};
+const chineseHome = createFixture(homeTheme, {
+ homeQuestionText: "我们应该在 中构建什么?",
+ nativeSuggestionLabels: ["探索并理解代码", "构建新功能、应用或工具", "审查代码并提出修改建议"],
+});
+vm.runInNewContext(chineseHome.payload, chineseHome.context);
+assert.equal(
+ chineseHome.homeQuestionAttributes.get("data-dream-skin-question"),
+ "我们应该构建什么?",
+ "The Chinese home question should be compacted after excluding the native project button.",
+);
+assert.equal(chineseHome.home.classList.contains("dream-skin-home"), true);
+assert.equal(chineseHome.suggestionGroup.getAttribute("data-dream-skin-home-suggestions"), "zh");
+assert.deepEqual(
+ chineseHome.suggestionGrid.children.slice(0, 3)
+ .map((wrapper) => wrapper.getAttribute("data-dream-skin-native-home-card")),
+ ["explore", "build", "review"],
+);
+let syntheticCards = chineseHome.suggestionGroup.querySelectorAll("[data-dream-skin-home-card]");
+assert.equal(syntheticCards.length, 1);
+assert.equal(syntheticCards[0].getAttribute("data-dream-skin-home-card"), "fix");
+chineseHome.window.__CODEX_DREAM_SKIN_STATE__.ensure({ root: false, route: true });
+assert.equal(
+ chineseHome.suggestionGroup.querySelectorAll("[data-dream-skin-home-card]").length,
+ 1,
+ "Repeated synchronization must not duplicate the fallback fourth card.",
+);
+syntheticCards[0].remove();
+chineseHome.window.__CODEX_DREAM_SKIN_STATE__.ensure({ root: false, route: true });
+syntheticCards = chineseHome.suggestionGroup.querySelectorAll("[data-dream-skin-home-card]");
+assert.equal(syntheticCards.length, 1, "A React rerender must not permanently remove the fourth card.");
+syntheticCards[0].querySelectorAll("button")[0].click();
+assert.equal(chineseHome.composer.focusCount, 1);
+assert.deepEqual(chineseHome.composerWrites, [{
+ command: "insertText",
+ value: "定位并修复这个项目中的问题和失败。",
+}]);
+
+const currentChineseHome = createFixture(homeTheme, {
+ homeMarkup: "current",
+ homeQuestionText: "我们应该在 中做些什么?",
+});
+vm.runInNewContext(currentChineseHome.payload, currentChineseHome.context);
+assert.equal(
+ currentChineseHome.home.classList.contains("dream-skin-home"),
+ true,
+ "The current Codex home route must be recognized without the retired native suggestion group.",
+);
+assert.equal(
+ currentChineseHome.homeQuestionAttributes.get("data-dream-skin-question"),
+ "我们应该做些什么?",
+ "The current Chinese home question should keep its native wording while moving the project selector.",
+);
+const currentChineseSuggestions = currentChineseHome.currentSuggestionPortal
+ .querySelector("[data-dream-skin-home-suggestions]");
+assert.ok(
+ currentChineseSuggestions,
+ "An empty current Codex suggestion portal should receive the Dream Skin fallback group.",
+);
+assert.equal(
+ currentChineseSuggestions.querySelectorAll("[data-dream-skin-home-card]").length,
+ 4,
+ "The current Codex home route should always receive the complete four-card set.",
+);
+currentChineseHome.window.__CODEX_DREAM_SKIN_STATE__.ensure({ root: false, route: true });
+assert.equal(
+ currentChineseHome.currentSuggestionPortal
+ .querySelectorAll("[data-dream-skin-home-card]").length,
+ 4,
+ "Repeated synchronization must not duplicate current-renderer fallback cards.",
+);
+
+const currentHeaderOnlyHome = createFixture(homeTheme, {
+ homeMarkup: "current",
+ homeQuestionText: "我们应该在 中做些什么?",
+ currentSuggestionShell: "header-only",
+});
+vm.runInNewContext(currentHeaderOnlyHome.payload, currentHeaderOnlyHome.context);
+const headerOnlyGeneratedGroup = currentHeaderOnlyHome.currentSuggestionPortal
+ .querySelector("[data-dream-skin-generated-home-suggestions]");
+assert.ok(
+ headerOnlyGeneratedGroup,
+ "A native header without a card grid must not replace the generated four-card group.",
+);
+assert.equal(
+ currentHeaderOnlyHome.currentNativeSuggestionGroup
+ .getAttribute("data-dream-skin-empty-home-suggestions"),
+ "",
+ "An unusable native shell must stay hidden while the fallback grid is active.",
+);
+assert.equal(
+ headerOnlyGeneratedGroup.querySelectorAll("[data-dream-skin-home-card]").length,
+ 4,
+);
+assert.equal(
+ currentHeaderOnlyHome.currentNativeHeaderButton
+ .getAttribute("data-dream-skin-native-home-card"),
+ null,
+ "Native suggestion controls must never be classified as home cards.",
+);
+
+const currentNativeHome = createFixture(homeTheme, {
+ homeMarkup: "current",
+ homeQuestionText: "我们应该在 中做些什么?",
+ currentSuggestionShell: "grid",
+ nativeSuggestionLabels: ["探索并理解代码"],
+});
+vm.runInNewContext(currentNativeHome.payload, currentNativeHome.context);
+assert.equal(
+ currentNativeHome.currentNativeHeaderButton.getAttribute("data-dream-skin-native-home-card"),
+ null,
+);
+assert.equal(
+ currentNativeHome.currentNativeSuggestionGrid
+ .querySelectorAll("[data-dream-skin-home-card]").length,
+ 3,
+ "A current native grid should preserve matching React cards and fill only the missing actions.",
+);
+assert.equal(
+ currentNativeHome.currentNativeSuggestionGrid.children[0]
+ .getAttribute("data-dream-skin-native-home-card"),
+ "explore",
+);
+assert.equal(
+ currentNativeHome.currentSuggestionPortal
+ .querySelector("[data-dream-skin-generated-home-suggestions]"),
+ null,
+ "A usable current native grid should supersede the generated group.",
+);
+assert.equal(
+ currentNativeHome.currentNativeSuggestionGroup
+ .getAttribute("data-dream-skin-empty-home-suggestions"),
+ null,
+);
+
+const currentEnglishHome = createFixture(homeTheme, {
+ homeMarkup: "current",
+ homeQuestionText: "What should we work on in ",
+});
+vm.runInNewContext(currentEnglishHome.payload, currentEnglishHome.context);
+assert.equal(
+ currentEnglishHome.homeQuestionAttributes.get("data-dream-skin-question"),
+ "What should we work on?",
+ "The current English home question should retain the updated work-on wording.",
+);
+assert.equal(
+ currentEnglishHome.currentSuggestionPortal
+ .querySelectorAll("[data-dream-skin-home-card]").length,
+ 4,
+);
+
+const currentUnsupportedHome = createFixture(homeTheme, {
+ homeMarkup: "current",
+ homeQuestionText: "¿En qué deberíamos trabajar?",
+});
+vm.runInNewContext(currentUnsupportedHome.payload, currentUnsupportedHome.context);
+assert.equal(currentUnsupportedHome.home.classList.contains("dream-skin-home"), true);
+assert.equal(currentUnsupportedHome.homeQuestionAttributes.has("data-dream-skin-question"), false);
+assert.equal(
+ currentUnsupportedHome.currentSuggestionPortal
+ .querySelector("[data-dream-skin-generated-home-suggestions]"),
+ null,
+ "Unknown current locales must preserve the native empty state without English fallback cards.",
+);
+assert.equal(currentChineseHome.window.__CODEX_DREAM_SKIN_STATE__.cleanup(), true);
+assert.equal(
+ currentChineseHome.currentSuggestionPortal
+ .querySelector("[data-dream-skin-generated-home-suggestions]"),
+ null,
+ "Cleanup must remove the generated current-renderer suggestion group.",
+);
+
+const englishHome = createFixture(homeTheme, {
+ homeQuestionText: "What should we build in ?",
+ nativeSuggestionLabels: [
+ "Explore and understand the code",
+ "Build a new feature, app, or tool",
+ "Review code and suggest improvements",
+ "Fix issues and failures",
+ ],
+});
+vm.runInNewContext(englishHome.payload, englishHome.context);
+assert.equal(
+ englishHome.homeQuestionAttributes.get("data-dream-skin-question"),
+ "What should we build?",
+ "The English home question should be compacted after excluding the native project button.",
+);
+assert.equal(
+ englishHome.suggestionGroup.querySelectorAll("[data-dream-skin-home-card]").length,
+ 0,
+ "Four matching native English suggestions should keep their original React click handlers.",
+);
+
+const unsupportedHome = createFixture(homeTheme, {
+ homeQuestionText: "¿Qué deberíamos construir en ?",
+ nativeSuggestionLabels: ["Explorar el código"],
+});
+vm.runInNewContext(unsupportedHome.payload, unsupportedHome.context);
+assert.equal(
+ unsupportedHome.homeQuestionAttributes.has("data-dream-skin-question"),
+ false,
+ "Unknown locales must keep the complete native question instead of rendering text with the project name removed.",
+);
+assert.equal(unsupportedHome.suggestionGroup.getAttribute("data-dream-skin-home-suggestions"), null);
+assert.equal(
+ unsupportedHome.suggestionGrid.children[0].getAttribute("data-dream-skin-native-home-card"),
+ null,
+ "Unknown locales must preserve native suggestions without filtering them.",
+);
+assert.equal(chineseHome.window.__CODEX_DREAM_SKIN_STATE__.cleanup(), true);
+assert.equal(chineseHome.suggestionGroup.querySelectorAll("[data-dream-skin-home-card]").length, 0);
+assert.equal(
+ chineseHome.suggestionGrid.children[0].getAttribute("data-dream-skin-native-home-card"),
+ null,
+);
+
// Auto appearance must continue following the native shell after the skin is
// already installed. The fixture makes the injected root color-scheme win
// whenever our class remains on , so a temporary native probe is needed