diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 247d0930..d995808d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,9 @@ jobs: with: node-version: "22" + - name: Check tracked files for secrets + run: node tools/check-secrets.mjs + - name: Check shell syntax shell: bash run: | diff --git a/.gitignore b/.gitignore index 993ef6a9..7967cc38 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,38 @@ .DS_Store .idea/ .vscode/ + +# Local credentials and signing material. Commit only explicit examples. +.env +.env.* +!.env.example +!**/.env.example +*.pem +*.key +*.p12 +*.pfx +*.jks +*.keystore +id_rsa* +id_dsa* +id_ecdsa* +id_ed25519* +.netrc +.npmrc +.pypirc +credentials.json +secrets.json +auth.json +service-account*.json +*.secrets.* + +# Local build tools, generated artifacts, and smoke fixtures. +.release-tools/ +.tmp-theme-repair/ README.preview.html docs/readme-preview.html release/ +.tools/ # Keep the large captured DOM/screenshot fixtures local, but version the # canonical renderer sources that generate both platform assets. runtime/* diff --git a/README.md b/README.md index b0552712..59e9bf36 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # Codex Dream Skin +当前 fork 版本为 `v1.6.0`,基于上游 `v1.5.6` 继续维护 Windows 兼容性: + +- Windows PowerShell 5.1 托盘菜单兼容修复(现已被上游吸收)。 +- 左侧栏与菜单栏左段采用连续的深色背景块;首页右侧保持无遮罩,对话页菜单栏右段与主窗口遮罩连续。 +- 适配 Codex 26.721+ 首页结构,收紧首页布局并移除三个多余的装饰文字。 +
中文 · English
@@ -207,6 +213,8 @@ macOS 菜单栏和 Windows 托盘都有「主题库 Gallery」和「在线 Studi | Apple Silicon / Intel Mac | [`macos/`](./macos/) | 双击 `Install Codex Dream Skin.command` | | Windows | [`windows/`](./windows/) | `scripts/install-dream-skin.ps1` → `start-dream-skin.ps1` | +Windows 用户请先阅读 [`windows/README.md`](./windows/README.md)。安装脚本运行时必须完全退出 Codex;源码更新后也需要退出托盘和 Codex,再重新运行安装器,已保存主题和导入图片不会被删除。 + 更细的说明: - Mac:[`macos/README.md`](./macos/README.md) diff --git a/TASK_PROGRESS.md b/TASK_PROGRESS.md index 9ca556ac..6290abd7 100644 --- a/TASK_PROGRESS.md +++ b/TASK_PROGRESS.md @@ -1,5 +1,37 @@ # Task Progress +## Fork Sync With Upstream v1.5.6 (2026-07-28 CST) + +- [complete] Configured `upstream` as `https://github.com/Fei-Away/Codex-Dream-Skin.git` and fetched `upstream/main@611c101` (`v1.5.6`). +- [complete] Audited the three fork-only commits. The PowerShell 5.1 empty tray collection fix is already present upstream; the two local documentation screenshots remain fork-only; the light-shell native menu readability fix still needs migration to the shared runtime CSS. +- [complete] Merged `upstream/main` into `fix/windows-tray-menu-readability` without rewriting the fork's existing commits. Documentation and tray conflicts use current upstream behavior; the light-shell native menu readability fix now lives in shared `runtime/dream-skin.css` and is synchronized to both platform assets with a Windows regression assertion. +- [verified] Runtime sync check, Windows injector/renderer syntax, 16 focused portable Node regressions, and the complete Windows PowerShell regression suite pass. The suite's forced cleanup warnings are intentional failure-injection fixtures; all five final groups report `PASS`. +- [complete] Reviewed the final fork-only delta against `upstream/main` and created local merge commit `13d1237`. The fork's own delta is whitespace-clean; repository-wide `git diff --cached --check` only reported an upstream-preserved trailing space in the official Inno Setup translation file. +- [pending] Do not push until the user finishes testing the v1.5.6 Release and asks to publish the branch. +- [fact] No push, pull request, tag, Release, or local Codex installation/update has been performed in this task. + +## Codex 26.721+ Home Layout Follow-up (2026-07-28 CST) + +- [reported] The user confirmed upstream v1.5.6 installs and runs successfully, but the new-task/home page layout differs from the earlier presentation. +- [reproduced] Real Windows Codex home capture at 1920x1040 shows the 26.721+ sibling content layout is visible, but the title overlaps right-side subject art with weak contrast, four suggestion cards span too widely, and the combined project/composer surface is oversized and pinned near the bottom edge. Normal task layout remains healthy. +- [reported] The user also identified three redundant skin-only labels: the home lower-right quote and the task page upper-left brand / upper-right online-status text. Remove these decorative pseudo-elements while preserving native controls and theme identity in the tray/library. +- [decision] Drop the fork's shell-independent light application-menu backdrop and retain upstream's left-sidebar dark overlay plus dark-shell-only native menu treatment; the user prefers the upstream visual hierarchy. +- [complete] Added a 26.721+-scoped shared-runtime layout refinement for the sibling content wrapper, removed the three redundant decorative labels, and retained upstream's dark-sidebar/dark-shell menu strategy without restoring the obsolete nested `:has()` rules that caused #244. +- [complete] Bumped the six release sources and four current-version assertions to fork version `1.5.7`; added matching Windows/macOS changelog entries while retaining the truthful upstream base of `v1.5.6`. +- [verified] Shared runtime assets are synchronized. Windows injector/renderer syntax, 18 portable Node regressions, the complete Windows PowerShell suite, and installer static contracts pass. Forced cleanup warnings are intentional failure-injection fixtures. +- [superseded] The initial 24,462,494-byte local `v1.5.7` installer was overwritten after the login-startup defect was reproduced; it is no longer the deliverable. +- [fact] The installer is intentionally unsigned, matching upstream's current first-run warning behavior. No installation, push, tag, PR, or public Release was performed. + +## Windows Login Startup Repair (2026-07-28 CST) + +- [reproduced] Installed engine and installer payload both report `1.5.7`, but the Inno-created startup shortcut targets `C:\Windows\Sysnative\WindowsPowerShell\...`; that alias is unavailable to 64-bit Explorer while the real `System32` executable exists. The tray mutex is not held, the recorded injector PID is dead, and its log says the original CDP browser identity closed. +- [root cause] All persisted Inno launch surfaces used the process-only `Sysnative` alias. In addition, the Release Start-menu entry and login task launched only the tray, even though an ordinary Codex launch cannot expose the verified CDP endpoint required for injection. +- [decision] Keep fork version `1.5.7` at the user's explicit request because it has not been published. Rebuild and overwrite only the local installer, then report a new checksum. +- [complete] Added a login orchestrator that starts the tray and a verified skin session while respecting pause, split the Release Start-menu skin/tray entries, and use the real `System32` path for every persisted entry. Ordinary Codex launches are never silently killed; an already-open non-CDP session still requires explicit restart consent. +- [verified] Installer static contracts, PowerShell parsing, 18 portable Node regressions, the complete Windows PowerShell suite, and `git diff --check` pass. Forced cleanup warnings remain intentional failure-injection fixtures. +- [complete] Rebuilt and overwrote local `release/CodexDreamSkin-Setup-v1.5.7.exe` with pinned Inno Setup 6.7.1 and bundled Node 22.23.1. It is 24,463,555 bytes and reports product version `1.5.7`; the prior checksum sidecar was deleted at the user's request. +- [fact] The repaired installer has not been installed, pushed, tagged, or publicly released. + Updated: 2026-07-25 08:31 HKT (Asia/Hong_Kong) ## v1.5.1 Version Release (2026-07-25 08:28 HKT) diff --git a/docs/images/funingna-codex-16x9.png b/docs/images/funingna-codex-16x9.png new file mode 100644 index 00000000..11b2a71c Binary files /dev/null and b/docs/images/funingna-codex-16x9.png differ diff --git a/docs/images/keqing-codex-16x9.png b/docs/images/keqing-codex-16x9.png new file mode 100644 index 00000000..a28881e7 Binary files /dev/null and b/docs/images/keqing-codex-16x9.png differ diff --git a/docs/images/screenshot-demo-art.png b/docs/images/screenshot-demo-art.png deleted file mode 100644 index a4f33430..00000000 Binary files a/docs/images/screenshot-demo-art.png and /dev/null differ diff --git a/macos/CHANGELOG.md b/macos/CHANGELOG.md index 6f9d123e..d6723ddf 100644 --- a/macos/CHANGELOG.md +++ b/macos/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## 1.6.0 — 2026-07-29 + +- 共享 renderer 新增首页样式属性和 v1.5.8 简洁首页 CSS 门控,默认仍保持 v1.5.9 标题与玻璃建议卡视觉。 +- Windows 托盘首页样式偏好通过共享载荷同步到渲染器;macOS 本版保持默认 v1.5.9 首页,不新增菜单项。 + +## 1.5.9 — 2026-07-29 + +- 在 Codex 26.721.4979 的双层首页建议 DOM 上恢复 v1.5.6/v1.5.7 的主题标题、主题副标题及玻璃建议卡视觉。 +- 保留 v1.5.8 的当前首页与对话页兼容性、跨平台选择器同步和路由状态刷新修复。 + +## 1.5.8 — 2026-07-28 + +### 内部 + +- 同步修复宽图首页主表面仍绘制深色渐变的问题:首页右侧恢复透明,对话页任务渐变保持不变。 +- 修复共享 Safe CSS 校验器无法解析已登记的 `backdrop-filter: blur(var(--ds-theme-surface-blur))`;未知变量、fallback 和越界 blur 继续 fail closed。 +- 同步共享渲染器的侧栏宽度桥接与清理逻辑。该逻辑不执行同步布局读取;macOS 没有 Windows 原生应用菜单栏匹配项,因此视觉效果保持不变。 + +## 1.5.7 — 2026-07-28 + +### 修复 + +- 适配 Codex 26.721+ 的新首页兄弟节点结构,限制标题区、建议卡片、项目选择与输入框的最大宽度,避免宽屏下内容过度拉伸、标题与主题主体重叠。 +- 移除首页右下角引语、任务页左上角主题标签和右上角在线状态三个皮肤装饰文字。 +- 采用上游的左侧栏深色蒙版与仅深色外壳生效的应用菜单处理,不再给浅色原生菜单栏额外增加独立背景。 + ## 1.5.6 — 2026-07-26 ### 安全 diff --git a/macos/VERSION b/macos/VERSION index eac1e0ad..dc1e644a 100644 --- a/macos/VERSION +++ b/macos/VERSION @@ -1 +1 @@ -1.5.6 +1.6.0 diff --git a/macos/assets/dream-skin.css b/macos/assets/dream-skin.css index bd112ec0..735038a6 100644 --- a/macos/assets/dream-skin.css +++ b/macos/assets/dream-skin.css @@ -49,6 +49,11 @@ --ds-immersive-mid: rgb(var(--ds-bg-rgb) / .26); --ds-immersive-far: rgb(var(--ds-bg-rgb) / .16); --ds-immersive-sidebar: rgb(var(--ds-panel-rgb) / .46); + --ds-sidebar-block-start: var(--ds-immersive-sidebar); + --ds-sidebar-block-end: var(--ds-immersive-edge); + --ds-main-block-start: var(--ds-immersive-edge); + --ds-main-block-mid: var(--ds-immersive-mid); + --ds-main-block-end: var(--ds-immersive-far); --ds-task-immersive-sidebar: rgb(var(--ds-panel-rgb) / .70); --ds-immersive-chrome: rgb(var(--ds-panel-rgb) / .28); --ds-immersive-composer: rgb(var(--ds-panel-rgb) / .44); @@ -99,6 +104,11 @@ html[data-dream-skin="active"][data-dream-shell="light"] { --ds-immersive-mid: rgb(var(--ds-panel-rgb) / .28); --ds-immersive-far: rgb(var(--ds-panel-rgb) / .14); --ds-immersive-sidebar: rgb(var(--ds-panel-rgb) / .48); + --ds-sidebar-block-start: var(--ds-immersive-sidebar); + --ds-sidebar-block-end: var(--ds-immersive-edge); + --ds-main-block-start: var(--ds-immersive-edge); + --ds-main-block-mid: var(--ds-immersive-mid); + --ds-main-block-end: var(--ds-immersive-far); --ds-task-immersive-sidebar: rgb(var(--ds-panel-rgb) / .72); --ds-immersive-chrome: rgb(var(--ds-panel-rgb) / .34); --ds-immersive-composer: rgb(var(--ds-panel-rgb) / .46); @@ -343,6 +353,14 @@ html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream- height: auto; } +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"]):not(:has(main.main-surface [role="main"])) { + --ds-sidebar-block-start: var(--ds-task-immersive-sidebar); + --ds-sidebar-block-end: var(--ds-task-immersive-edge); + --ds-main-block-start: var(--ds-task-immersive-edge); + --ds-main-block-mid: var(--ds-task-immersive-mid); + --ds-main-block-end: var(--ds-task-immersive-far); +} + html[data-dream-skin="active"]: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"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) body { background-image: var(--dream-skin-art) !important; background-position: var(--ds-art-position) !important; @@ -527,7 +545,7 @@ html[data-dream-skin="active"][data-dream-shell="light"] main.main-surface:not(: html[data-dream-skin="active"][data-dream-shell="light"] main.main-surface:has([role="main"])::after, -html[data-dream-skin="active"][data-dream-shell="light"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::before { +html[data-dream-skin="active"][data-dream-shell="light"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"]::before { text-shadow: none; } @@ -538,80 +556,67 @@ html[data-dream-skin="active"] main.main-surface > header.app-header-tint { backdrop-filter: blur(14px) saturate(108%) !important; } -/* Decorative chrome lives on the native shell; no injected positioning host is needed. */ -html[data-dream-skin="active"] - main.main-surface:not(:has([role="main"])) > header.app-header-tint::before { - content: var(--dream-skin-name, "Codex Dream Skin") " · " - var(--dream-skin-brand-subtitle, "CODEX DREAM SKIN"); - position: absolute; - left: 22px; - top: 4px; - z-index: 2; - max-width: min(42%, 360px); - overflow: hidden; - color: var(--ds-accent); - font: 800 11px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - letter-spacing: .03em; - text-overflow: ellipsis; - text-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .32); - white-space: nowrap; - pointer-events: none; +/* Windows keeps this native bar outside main; macOS simply has no match. */ +html[data-dream-skin="active"][data-dream-shell="dark"] + [class~="group/application-menu-top-bar"] { + color: var(--ds-text) !important; + background: rgb(var(--ds-panel-rgb) / .90) !important; + border-bottom: 1px solid var(--ds-line) !important; + box-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .72) !important; + backdrop-filter: blur(16px) saturate(90%) !important; } -html[data-dream-skin="active"] - main.main-surface:not(:has([role="main"])) > header.app-header-tint::after { - content: var(--dream-skin-status, "DREAM SKIN ONLINE"); - position: absolute; - right: 84px; - top: 13px; - z-index: 2; - max-width: 28%; - overflow: hidden; - color: var(--ds-muted); - font: 700 9px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - letter-spacing: .08em; - text-overflow: ellipsis; - text-shadow: 0 1px 10px rgb(var(--ds-bg-rgb) / .32); - white-space: nowrap; - pointer-events: none; +html[data-dream-skin="active"][data-dream-shell="dark"] + [class~="group/application-menu-top-bar"] :is(button, svg) { + color: var(--ds-text) !important; + text-shadow: 0 1px 2px rgb(var(--ds-bg-rgb) / .82) !important; } -html[data-dream-skin="active"] - main.main-surface:has([role="main"])::after { - content: var(--dream-skin-quote, "MAKE SOMETHING WONDERFUL"); - position: absolute; - right: 28px; - bottom: 72px; - z-index: 2; - pointer-events: none; - color: rgb(var(--ds-accent-rgb) / .74); - font: italic 14px/1.2 "Segoe Print", "Comic Sans MS", cursive; - letter-spacing: 0; - text-shadow: 0 0 13px rgb(var(--ds-accent-rgb) / .30); - transform: rotate(-3deg); +/* Continue the sidebar through the Windows application menu while keeping the + unmasked home artwork clear. The renderer publishes the live split without + synchronous layout reads. */ +html[data-dream-skin="active"][data-dream-chrome-mode="left"]:has(aside.app-shell-left-panel) + [class~="group/application-menu-top-bar"] { + background-color: transparent !important; + background-image: linear-gradient(90deg, + var(--ds-sidebar-block-start) 0, + var(--ds-sidebar-block-end) var(--ds-sidebar-extent, 0px), + transparent var(--ds-sidebar-extent, 0px)) !important; + background-position: 0 0 !important; + background-size: auto !important; + background-repeat: no-repeat !important; + border-bottom: 0 !important; + box-shadow: none !important; + backdrop-filter: none !important; } -html[data-dream-skin="active"][data-dream-shell="light"] - main.main-surface:not(:has([role="main"])) > header.app-header-tint::before, -html[data-dream-skin="active"][data-dream-shell="light"] - main.main-surface:not(:has([role="main"])) > header.app-header-tint::after { - text-shadow: none; +/* Conversation routes mask the main surface as well. Use the exact same + non-home condition as that surface: during new-conversation startup the + main mask appears before the thread scroller, so waiting for the scroller + would expose a temporary bright menu strip. A single continuous gradient + shares one stop at the fractional sidebar edge, avoiding both a transparent + raster gap and double-alpha dark overlap. */ +html[data-dream-skin="active"][data-dream-chrome-mode="left"]:has(aside.app-shell-left-panel):not(:has(main.main-surface [role="main"])) + [class~="group/application-menu-top-bar"] { + background-image: linear-gradient(90deg, + var(--ds-sidebar-block-start) 0, + var(--ds-sidebar-block-end) var(--ds-sidebar-extent, 0px), + var(--ds-main-block-mid) calc(64% + var(--ds-sidebar-extent, 0px) * .36), + var(--ds-main-block-end) 100%) !important; } -/* Windows keeps this native bar outside main; macOS simply has no match. */ -html[data-dream-skin="active"][data-dream-shell="dark"] +html[data-dream-skin="active"][data-dream-chrome-mode="full"] [class~="group/application-menu-top-bar"] { color: var(--ds-text) !important; background: rgb(var(--ds-panel-rgb) / .90) !important; border-bottom: 1px solid var(--ds-line) !important; - box-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .72) !important; + box-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .42) !important; backdrop-filter: blur(16px) saturate(90%) !important; } -html[data-dream-skin="active"][data-dream-shell="dark"] +html[data-dream-skin="active"][data-dream-chrome-mode="full"] [class~="group/application-menu-top-bar"] :is(button, svg) { color: var(--ds-text) !important; - text-shadow: 0 1px 2px rgb(var(--ds-bg-rgb) / .82) !important; } html[data-dream-skin="active"] main.main-surface [role="main"] { @@ -619,17 +624,17 @@ html[data-dream-skin="active"] main.main-surface [role="main"] { scrollbar-color: rgb(var(--ds-accent-rgb) / .38) transparent; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) { --thread-content-max-width: min(1180px, calc(100cqw - 44px)) !important; overflow-x: hidden !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child { min-height: 100% !important; padding-top: 15px !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child { flex: 0 0 440px !important; min-height: 440px !important; align-items: flex-start !important; @@ -644,19 +649,19 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > di (see #244). Override with higher specificity only when that new slot is present, so pre-26.721 layouts are untouched. */ html[data-dream-skin="active"] - [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) { + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) { min-height: 0 !important; padding-top: 0 !important; } html[data-dream-skin="active"] - [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) > .home-banners { + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) > .home-banners { flex: 0 1 auto !important; min-height: 0 !important; } html[data-dream-skin="active"] - [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) > .home-banners > div:first-child { + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) > .home-banners > div:first-child { height: auto !important; min-height: 0 !important; border: 0 !important; @@ -666,10 +671,89 @@ html[data-dream-skin="active"] } html[data-dream-skin="active"] - [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) > .home-banners > div:first-child::before { + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) > .home-banners > div:first-child::before { content: none !important; } +/* Codex 26.721+ renders the title, suggestions and composer in the sibling + after the native banner wrapper. Keep that real content visible while + restoring a compact, safe-area-aware composition instead of stretching + every control across the full window. */ +html[data-dream-skin="active"] + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div { + box-sizing: border-box; + width: min(1120px, calc(100% - 48px)) !important; + max-width: 1120px !important; + margin-inline: auto !important; +} + +html[data-dream-skin="active"] + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div + [data-feature="game-source"] { + box-sizing: border-box; + width: min(48%, 520px) !important; + margin-right: auto !important; + padding: 18px 22px !important; + border: 1px solid rgb(var(--ds-muted-rgb) / .16); + border-radius: 18px; + background: rgb(var(--ds-panel-rgb) / .58); + box-shadow: 0 10px 28px rgb(var(--ds-bg-rgb) / .16); + backdrop-filter: blur(10px) saturate(104%); + text-align: left !important; +} + +html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div + [data-feature="game-source"] { + margin-right: 0 !important; + margin-left: auto !important; + text-align: right !important; +} + +html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div + [data-feature="game-source"] { + width: min(62%, 640px) !important; + margin-inline: auto !important; + text-align: center !important; +} + +html[data-dream-skin="active"] + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div + [data-home-ambient-suggestions], +html[data-dream-skin="active"] + [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions, +html[data-dream-skin="active"] + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div + [class*="_homeUtilityBar_"], +html[data-dream-skin="active"] + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div + .composer-surface-chrome { + box-sizing: border-box; + width: min(980px, 100%) !important; + max-width: 980px !important; + margin-inline: auto !important; +} + +/* v1.6.0 keeps the v1.5.9 home as the upgrade default, while allowing the + v1.5.8 clean wallpaper-first home to hide only the title and both native + suggestion layers. The composer and project controls remain interactive. */ +html[data-dream-skin="active"][data-dream-home-mode="clean"] + [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"], +html[data-dream-skin="active"][data-dream-home-mode="clean"] + [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions], +html[data-dream-skin="active"][data-dream-home-mode="clean"] + [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions { + display: none !important; +} + +html[data-dream-skin="active"] + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div + [data-home-ambient-suggestions] button, +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button { + min-height: 104px !important; +} + /* Codex 26.721+: this chain (3-6 first-child levels deep) targets the old compact hero-art card that used to live inside div:first-child's second level. That level is now the empty .home-banners slot (see #244), so on @@ -677,7 +761,7 @@ html[data-dream-skin="active"] is dormant rather than misrendered. Left as-is pending a real fixture of the new nested shape; the critical content-invisible bug is the block above this one. */ -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child { position: relative !important; isolation: isolate; width: calc(100% - 44px) !important; @@ -697,7 +781,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > di box-shadow: 0 16px 38px rgb(var(--ds-bg-rgb) / .30) !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child::before { content: ""; position: absolute; inset: 0 auto 0 0; @@ -709,25 +793,25 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > di } html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child::before { inset: 0 0 0 auto; } html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child::before { inset: 0 14%; width: auto; border-radius: 0; } html[data-dream-skin="active"]:is([data-dream-art-safe="none"], [data-dream-art-safe-area="none"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child::before { inset: 0; width: auto; border-radius: 21px; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:first-child { position: relative; z-index: 1; height: 100%; @@ -737,37 +821,37 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > di } html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:first-child { justify-content: flex-end !important; } html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:first-child { justify-content: center !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { width: min(46%, 520px) !important; align-items: flex-start !important; gap: 0 !important; } html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { align-items: flex-end !important; text-align: right !important; } html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { + [role="main"]:has([data-feature="game-source"]) > 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; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-testid="home-icon"] { display: none !important; } +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-testid="home-icon"] { display: none !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"] { display: block !important; max-width: 100% !important; color: var(--ds-text) !important; @@ -780,7 +864,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [dat pointer-events: auto !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::before { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"]::before { content: var(--dream-skin-name, "Codex Dream Skin"); display: block; margin-bottom: 9px; @@ -790,7 +874,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [dat text-shadow: 0 0 14px rgb(var(--ds-accent-rgb) / .26); } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::after { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"]::after { content: var(--dream-skin-tagline, "把喜欢的画面变成可交互的 Codex 工作台。"); display: block; margin-top: 13px; @@ -801,7 +885,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [dat letter-spacing: 0; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] button { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"] button { margin: 0 5px; padding: 2px 9px 3px; border: 1px solid rgb(var(--ds-accent-rgb) / .36); @@ -812,7 +896,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [dat text-underline-offset: 5px; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] button::before { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"] button::before { content: var(--dream-skin-project-prefix, "选择项目 · "); font-size: .46em; font-weight: 700; @@ -820,16 +904,20 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [dat opacity: .86; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:nth-child(2) { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:nth-child(2) { left: 14px !important; right: 14px !important; top: 100% !important; margin-top: 13px !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions { overflow: visible !important; } +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions], +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions { + overflow: visible !important; +} -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button, +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button { position: relative !important; min-height: 118px !important; padding: 14px 13px 12px !important; @@ -850,17 +938,20 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .gro border-color .18s cubic-bezier(.22, 1, .36, 1) !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button [class~="text-token-text-primary"] { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button [class~="text-token-text-primary"], +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button [class~="text-token-text-primary"] { color: var(--ds-text) !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button:hover { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button:hover, +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button:hover { transform: translateY(-2px) !important; border-color: rgb(var(--ds-accent-rgb) / .42) !important; box-shadow: 0 12px 28px rgb(var(--ds-bg-rgb) / .24) !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:first-child > span:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button > span:first-child > span:first-child, +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button > span:first-child > span:first-child { width: 38px; height: 38px; /* Native spans carry justify-start; grid + place-items cannot override @@ -876,7 +967,8 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .gro box-shadow: 0 0 0 6px rgb(var(--ds-accent-rgb) / .05); } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button svg { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button svg, +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button svg { width: 21px !important; height: 21px !important; display: block !important; @@ -885,8 +977,10 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .gro color: var(--ds-accent) !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:first-child { justify-content: center !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:last-child { align-items: center !important; text-align: center !important; } +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button > span:first-child, +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button > span:first-child { justify-content: center !important; } +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button > span:last-child, +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button > span:last-child { align-items: center !important; text-align: center !important; } html[data-dream-skin="active"] .composer-surface-chrome { overflow: visible !important; @@ -926,10 +1020,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"]:has(main.main-surface } html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) { - background: linear-gradient(90deg, - var(--ds-immersive-edge), - var(--ds-immersive-mid) 64%, - var(--ds-immersive-far)) !important; + background: transparent !important; border: 0 !important; border-radius: 0 !important; box-shadow: none !important; @@ -958,7 +1049,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface } html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child { border: 0 !important; border-radius: 0 !important; background: transparent !important; @@ -966,12 +1057,14 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has } html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child::before { content: none !important; } html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) - [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button { + [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button, +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) + [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button { background: rgb(var(--ds-panel-rgb) / .56) !important; box-shadow: 0 8px 22px rgb(var(--ds-bg-rgb) / .16), @@ -1021,7 +1114,7 @@ html[data-dream-skin="active"] [class*="_homeUtilityBar_"] { backdrop-filter: blur(16px) saturate(108%) !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]):has([class*="_homeUtilityBar_"]) +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]):has([class*="_homeUtilityBar_"]) .composer-surface-chrome { border: 0 !important; border-radius: 0 0 22px 22px !important; @@ -1042,7 +1135,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] [class*="_homeUtility } html[data-dream-skin="active"][data-dream-art-wide="true"] - [role="main"]:has([data-testid="home-icon"]):has([class*="_homeUtilityBar_"]) .composer-surface-chrome { + [role="main"]:has([data-feature="game-source"]):has([class*="_homeUtilityBar_"]) .composer-surface-chrome { box-shadow: 0 10px 30px rgb(var(--ds-bg-rgb) / .20), inset 1px 0 var(--ds-immersive-line), @@ -1134,7 +1227,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface background: transparent !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) div:has(> .horizontal-scroll-fade-mask .group\/project-selector) { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) div:has(> .horizontal-scroll-fade-mask .group\/project-selector) { position: relative; padding-top: 28px !important; border: 1px solid rgb(var(--ds-muted-rgb) / .16); @@ -1142,7 +1235,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) div: background: rgb(var(--ds-panel-rgb) / .92) !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) div:has(> .horizontal-scroll-fade-mask .group\/project-selector)::before { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) div:has(> .horizontal-scroll-fade-mask .group\/project-selector)::before { content: var(--dream-skin-project-label, "选择项目"); position: absolute; left: 13px; @@ -1155,7 +1248,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) div: white-space: nowrap; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/project-selector > button { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/project-selector > button { border-color: rgb(var(--ds-accent-rgb) / .22) !important; background: rgb(var(--ds-accent-rgb) / .08) !important; color: var(--ds-text) !important; @@ -1179,27 +1272,28 @@ html[data-dream-skin="active"] [data-message-author-role] { border-radius: 16px; @media (max-width: 1120px) { html[data-dream-skin="active"] main.main-surface:has([role="main"])::after { content: none; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) { --thread-content-max-width: min(940px, calc(100cqw - 30px)) !important; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { width: calc(100% - 28px) !important; } + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) { --thread-content-max-width: min(940px, calc(100cqw - 30px)) !important; } + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child { width: calc(100% - 28px) !important; } } @media (max-width: 900px) { - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child { + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child { flex-basis: 408px !important; min-height: 408px !important; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child { height: 232px !important; min-height: 232px !important; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { padding: 0 26px; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:first-child { padding: 0 26px; } + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { width: min(58%, 460px) !important; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] { font-size: 18px !important; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::before { margin-bottom: 7px; font-size: 10px; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::after { margin-top: 9px; font-size: 11px; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button { min-height: 112px !important; font-size: 12px !important; } + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"] { font-size: 18px !important; } + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"]::before { margin-bottom: 7px; font-size: 10px; } + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"]::after { margin-top: 9px; font-size: 11px; } + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button, + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button { min-height: 112px !important; font-size: 12px !important; } } @media (prefers-reduced-motion: reduce) { diff --git a/macos/assets/renderer-inject.js b/macos/assets/renderer-inject.js index be799888..eed374ff 100644 --- a/macos/assets/renderer-inject.js +++ b/macos/assets/renderer-inject.js @@ -1,6 +1,6 @@ // Canonical cross-platform renderer. Run tools/sync-runtime-assets.mjs after editing. ((cssText, artDataUrl, themeConfig) => { - const SELECTOR_CONTRACT = {"schema":"codex-dream-skin-selectors/1","selectors":[{"key":"shell-main","selector":"main.main-surface","tier":"L1","scope":"all","required":true},{"key":"left-panel","selector":"aside.app-shell-left-panel","tier":"L1","scope":"all","required":true},{"key":"header-tint","selector":"header.app-header-tint","tier":"L1","scope":"all","required":true},{"key":"home-icon","selector":"[data-testid=\"home-icon\"]","tier":"L1","scope":"home","required":true},{"key":"home-route","selector":"[role=\"main\"]:has([data-testid=\"home-icon\"])","tier":"L1","scope":"home","required":true},{"key":"home-route-css","selector":"[role=\"main\"]","tier":"L1","scope":"home","required":true},{"key":"home-banners","selector":".home-banners","tier":"L2","scope":"home","required":false},{"key":"composer-chrome","selector":".composer-surface-chrome","tier":"L2","scope":"home+thread","required":false},{"key":"composer-toolbar","selector":".composer-surface-chrome [class*=\"_footer_\"]","tier":"L2","scope":"home+thread","required":false},{"key":"home-utility","selector":"[class*=\"_homeUtilityBar_\"]","tier":"L2","scope":"home","required":false},{"key":"game-source","selector":"[data-feature=\"game-source\"]","tier":"L2","scope":"home","required":false},{"key":"home-suggestions","selector":".group\\/home-suggestions","tier":"L2","scope":"home","required":false},{"key":"project-selector","selector":".group\\/project-selector","tier":"L2","scope":"home config","required":false},{"key":"markdown","selector":"[class*=\"_markdown\"]","tier":"L2","scope":"thread","required":false},{"key":"thread-surface","selector":".thread-scroll-container","tier":"L2","scope":"thread","required":false},{"key":"message","selector":"[data-message-author-role]","tier":"L2","scope":"thread","required":false},{"key":"appearance-radio","selector":"input[name=\"appearance-theme\"]","tier":"L2","scope":"settings","required":false},{"key":"overlay-menu","selector":"[role=\"menu\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-dialog","selector":"[role=\"dialog\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-popper","selector":"[data-radix-popper-content-wrapper]","tier":"L2","scope":"overlay","required":false}],"stableTestids":["app-shell-header-context-menu-surface","home-icon","theme-preview"]}; + const SELECTOR_CONTRACT = {"schema":"codex-dream-skin-selectors/1","selectors":[{"key":"shell-main","selector":"main.main-surface","tier":"L1","scope":"all","required":true},{"key":"left-panel","selector":"aside.app-shell-left-panel","tier":"L1","scope":"all","required":true},{"key":"header-tint","selector":"header.app-header-tint","tier":"L1","scope":"all","required":true},{"key":"home-icon","selector":"[data-testid=\"home-icon\"]","tier":"L2","scope":"home","required":false},{"key":"home-route","selector":"[role=\"main\"]:has([data-feature=\"game-source\"])","tier":"L1","scope":"home","required":true},{"key":"home-route-css","selector":"[role=\"main\"]","tier":"L1","scope":"home","required":true},{"key":"home-banners","selector":".home-banners","tier":"L2","scope":"home","required":false},{"key":"composer-chrome","selector":".composer-surface-chrome","tier":"L2","scope":"home+thread","required":false},{"key":"composer-toolbar","selector":".composer-surface-chrome [class*=\"_footer_\"]","tier":"L2","scope":"home+thread","required":false},{"key":"home-utility","selector":"[class*=\"_homeUtilityBar_\"]","tier":"L2","scope":"home","required":false},{"key":"game-source","selector":"[data-feature=\"game-source\"]","tier":"L2","scope":"home","required":false},{"key":"home-suggestions","selector":"[data-home-ambient-suggestions]","tier":"L2","scope":"home","required":false},{"key":"home-suggestion-cards","selector":".group\\/home-suggestions","tier":"L2","scope":"home","required":false},{"key":"project-selector","selector":".group\\/project-selector","tier":"L2","scope":"home config","required":false},{"key":"markdown","selector":"[class*=\"_markdown\"]","tier":"L2","scope":"thread","required":false},{"key":"thread-surface","selector":".thread-scroll-container","tier":"L2","scope":"thread","required":false},{"key":"message","selector":"[data-message-author-role]","tier":"L2","scope":"thread","required":false},{"key":"appearance-radio","selector":"input[name=\"appearance-theme\"]","tier":"L2","scope":"settings","required":false},{"key":"overlay-menu","selector":"[role=\"menu\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-dialog","selector":"[role=\"dialog\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-popper","selector":"[data-radix-popper-content-wrapper]","tier":"L2","scope":"overlay","required":false}],"stableTestids":["app-shell-header-context-menu-surface","home-icon","theme-preview"]}; const STATE_KEY = "__CODEX_DREAM_SKIN_STATE__"; const DISABLED_KEY = "__CODEX_DREAM_SKIN_DISABLED__"; const STYLE_REGISTRY_KEY = "__CODEX_DREAM_SKIN_STYLE_SHEETS__"; @@ -12,6 +12,7 @@ "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", "data-dream-art-ready", + "data-dream-chrome-mode", "data-dream-home-mode", ]; const VERSION = __DREAM_SKIN_VERSION_JSON__; const STYLE_REVISION = __DREAM_SKIN_STYLE_REVISION_JSON__; @@ -43,7 +44,7 @@ "--ds-theme-surface-shadow", "--ds-theme-image-focus-x", "--ds-theme-image-focus-y", "--ds-theme-image-zoom", "--ds-theme-image-dim", "--ds-theme-image-task-intensity", - "--ds-theme-density-scale", "--ds-theme-motion-level", + "--ds-theme-density-scale", "--ds-theme-motion-level", "--ds-sidebar-extent", ]; const selectorByKey = new Map(SELECTOR_CONTRACT.selectors.map((entry) => [entry.key, entry])); const stableTestidSelector = (testid) => SELECTOR_CONTRACT.stableTestids?.includes(testid) @@ -57,6 +58,8 @@ let analysisTimer = null; let rootObserver = null; let partObserver = null; + let sidebarResizeObserver = null; + let observedSidebar = null; let bodyReadyHandler = null; let styleMode = null; let styleNode = null; @@ -550,6 +553,8 @@ const shell = resolvedShell(); setAttribute(root, "data-dream-skin", "active"); setAttribute(root, SHELL_ATTR, shell); + setAttribute(root, "data-dream-chrome-mode", THEME.chromeMode === "full" ? "full" : "left"); + setAttribute(root, "data-dream-home-mode", THEME.homeMode === "clean" ? "clean" : "classic"); setStyleProperty(root, "--dream-skin-art", `url("${artUrl}")`); applyTheme(root, shell); applyArtMetadata(root); @@ -581,11 +586,35 @@ } } }; + const setSidebarExtent = (width) => { + const numeric = Number(width); + const bounded = Number.isFinite(numeric) && numeric > 0 ? clamp(numeric, 0, 2048) : 0; + setStyleProperty(document.documentElement, "--ds-sidebar-extent", `${Number(bounded.toFixed(2))}px`); + }; + const inlineSidebarWidth = (sidebar) => { + const inline = sidebar?.style?.width || sidebar?.getAttribute?.("style") || ""; + const match = String(inline).match(/(?:^|;)\s*width\s*:\s*([\d.]+)px(?:\s*;|$)/i); + return match ? Number(match[1]) : null; + }; + const observeSidebar = (sidebar) => { + if (observedSidebar === sidebar) return; + if (observedSidebar) sidebarResizeObserver?.unobserve?.(observedSidebar); + observedSidebar = sidebar || null; + if (!observedSidebar) { + setSidebarExtent(0); + return; + } + const inlineWidth = inlineSidebarWidth(observedSidebar); + if (inlineWidth !== null) setSidebarExtent(inlineWidth); + sidebarResizeObserver?.observe?.(observedSidebar); + }; const refreshParts = () => { metrics.partPasses += 1; const desired = new Map(); addPart(desired, "root", [document.documentElement]); - addPart(desired, "sidebar", selectorNodes("left-panel")); + const sidebars = selectorNodes("left-panel"); + addPart(desired, "sidebar", sidebars); + observeSidebar(sidebars[0] || null); addPart(desired, "main", selectorNodes("shell-main")); addPart(desired, "header", selectorNodes("header-tint")); addPart(desired, "home", selectorNodes("home-route")); @@ -687,6 +716,7 @@ removeParts(); state?.rootObserver?.disconnect(); state?.partObserver?.disconnect(); + state?.sidebarResizeObserver?.disconnect(); if (bodyReadyHandler && typeof document.removeEventListener === "function") { document.removeEventListener("DOMContentLoaded", bodyReadyHandler); } @@ -733,7 +763,21 @@ }; if (typeof MutationObserver === "function") { rootObserver = new MutationObserver(() => scheduleEnsure({ root: true })); - partObserver = new MutationObserver(() => scheduleEnsure({ parts: true }, 80)); + // Some Codex sidebar transitions replace the route DOM without firing the + // Navigation API. Refresh scope with the same debounced pass that already + // remaps dynamic public parts so verification cannot retain a stale route. + partObserver = new MutationObserver(() => scheduleEnsure({ scope: true, parts: true }, 80)); + } + if (typeof ResizeObserver === "function") { + sidebarResizeObserver = new ResizeObserver((entries) => { + for (const entry of entries || []) { + if (entry?.target !== observedSidebar) continue; + const borderBox = Array.isArray(entry.borderBoxSize) + ? entry.borderBoxSize[0] : entry.borderBoxSize; + const width = borderBox?.inlineSize ?? entry.contentRect?.width; + if (Number.isFinite(Number(width))) setSidebarExtent(width); + } + }); } let mediaQuery = null; @@ -755,6 +799,7 @@ cleanup, rootObserver, partObserver, + sidebarResizeObserver, timer: null, scheduler, mediaQuery, @@ -803,7 +848,7 @@ bodyReadyHandler = () => { if (!window[DISABLED_KEY]) { observeBody(); - scheduleEnsure({ parts: true }, 0); + scheduleEnsure({ scope: true, parts: true }, 0); } }; document.addEventListener("DOMContentLoaded", bodyReadyHandler, { once: true }); diff --git a/macos/assets/safe-css-validator.mjs b/macos/assets/safe-css-validator.mjs index 8321e8ea..1d925d51 100644 --- a/macos/assets/safe-css-validator.mjs +++ b/macos/assets/safe-css-validator.mjs @@ -278,11 +278,9 @@ function validatePropertyValue(property, value) { } if (property === "backdrop-filter") { if (value.toLowerCase() === "none") return true; - const match = value.match(/^blur\(\s*([^\s)]+)\s*\)$/i); - return Boolean(match && ( - registeredVar(match[1], new Set(["--ds-theme-surface-blur"])) - || zeroOrPx(match[1], 0, 20) - )); + if (/^blur\(\s*var\(\s*--ds-theme-surface-blur\s*\)\s*\)$/i.test(value)) return true; + const match = value.match(/^blur\(\s*([^\s()]+)\s*\)$/i); + return Boolean(match && zeroOrPx(match[1], 0, 20)); } if (property === "font-family") return fontFamilyValue(value); if (property === "font-size") return numeric(value, 12, 20, "px"); diff --git a/macos/assets/selectors.json b/macos/assets/selectors.json index 06e22231..2fc46e0a 100644 --- a/macos/assets/selectors.json +++ b/macos/assets/selectors.json @@ -29,15 +29,16 @@ { "key": "shell-main", "selector": "main.main-surface", "tier": "L1", "scope": "all", "required": true, "notes": "全局主表面。mac 设置页缺失(win 存在),缺失时整页仅 L0" }, { "key": "left-panel", "selector": "aside.app-shell-left-panel", "tier": "L1", "scope": "all", "required": true, "notes": "左侧栏;收起/重建时会移除节点,勿在其消失时清皮" }, { "key": "header-tint", "selector": "header.app-header-tint", "tier": "L1", "scope": "all", "required": true, "notes": "顶栏;皮肤不得覆盖其 position/z-index(既有回归约束)" }, - { "key": "home-icon", "selector": "[data-testid=\"home-icon\"]", "tier": "L1", "scope": "home", "required": true, "notes": "首页判定信号,双端一致" }, - { "key": "home-route", "selector": "[role=\"main\"]:has([data-testid=\"home-icon\"])", "tier": "L1", "scope": "home", "required": true, "notes": "首页容器。实测 [role=main] 仅存在于首页——它不是通用路由锚点,thread 页没有" }, + { "key": "home-icon", "selector": "[data-testid=\"home-icon\"]", "tier": "L2", "scope": "home", "required": false, "notes": "旧版首页标记;Codex 26.721.4979 已不再渲染,仅保留为兼容探针" }, + { "key": "home-route", "selector": "[role=\"main\"]:has([data-feature=\"game-source\"])", "tier": "L1", "scope": "home", "required": true, "notes": "Codex 26.721.4979 首页容器,以稳定的原生 hero 标记判定;thread 页没有 role=main" }, { "key": "home-route-css", "selector": "[role=\"main\"]", "tier": "L1", "scope": "home", "required": true, "notes": "home-route 的 CSS 可组合别名:不含 :has()。CSS 禁止 :has() 嵌套,home-route 原选择器一旦写进 :has()/:not(:has()) 整条规则会被解析器丢弃(v1.3.1 全窗口首页与任务页氛围背景因此失效)。依据与 home-route 相同的实测:[role=main] 仅存在于首页;该锚点缺失时 CSS 路由门控整体失效,故必须按 L1 必需项监控" }, { "key": "home-banners", "selector": ".home-banners", "tier": "L2", "scope": "home", "required": false, "notes": "Codex 26.721+ 新增的原生首页 banner 插槽,位于 home-route 第一个子节点内、通常为空(0 子节点)靠原生 empty:hidden 折叠。真正的首页内容(标题/建议卡/输入框)在这个版本上已变成该插槽外层容器的兄弟节点,不再是它的后代(见 #244 实测:home-route > div:first-child 被撑到 100% 高度会把内容挤出视口)。用于判定新旧 home DOM 形态,皮肤据此把 first-child 链路的强制尺寸限定在旧形态或改走兄弟节点" }, { "key": "composer-chrome", "selector": ".composer-surface-chrome", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框壳。win 实测存在无 composer 的 detail 路由,不可当全局锚点" }, { "key": "composer-toolbar", "selector": ".composer-surface-chrome [class*=\"_footer_\"]", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框工具栏;CSS Modules 名称跨端稳定,hash 随版本变化" }, { "key": "home-utility", "selector": "[class*=\"_homeUtilityBar_\"]", "tier": "L2", "scope": "home", "required": false, "notes": "CSS Modules 前缀匹配;模块名跨端稳定、hash 随版本变,禁止匹配完整 hash" }, { "key": "game-source", "selector": "[data-feature=\"game-source\"]", "tier": "L2", "scope": "home", "required": false }, - { "key": "home-suggestions", "selector": ".group\\/home-suggestions", "tier": "L2", "scope": "home", "required": false }, + { "key": "home-suggestions", "selector": "[data-home-ambient-suggestions]", "tier": "L2", "scope": "home", "required": false, "notes": "Codex 26.721.4979 原生首页建议区的显式稳定标记" }, + { "key": "home-suggestion-cards", "selector": ".group\\/home-suggestions", "tier": "L2", "scope": "home", "required": false, "notes": "Codex 26.721.4979 renders the visible starter cards in this separate layer; keep it distinct because callers append button selectors." }, { "key": "project-selector", "selector": ".group\\/project-selector", "tier": "L2", "scope": "home config", "required": false, "notes": "依赖用户项目配置;win 快照未出现(该机可能未配置项目),不算平台差异" }, { "key": "markdown", "selector": "[class*=\"_markdown\"]", "tier": "L2", "scope": "thread", "required": false, "notes": "会话内容块,单页可达 100+ 个,样式规则注意性能" }, { "key": "thread-surface", "selector": ".thread-scroll-container", "tier": "L2", "scope": "thread", "required": false, "notes": "会话滚动表面;双端活动会话快照均存在" }, diff --git a/macos/package.json b/macos/package.json index b992b814..143909b7 100644 --- a/macos/package.json +++ b/macos/package.json @@ -1,6 +1,6 @@ { "name": "codex-dream-skin-studio", - "version": "1.5.6", + "version": "1.6.0", "private": true, "type": "module", "scripts": { diff --git a/macos/scripts/common-macos.sh b/macos/scripts/common-macos.sh index 921ae633..f7ebfbaa 100755 --- a/macos/scripts/common-macos.sh +++ b/macos/scripts/common-macos.sh @@ -29,7 +29,7 @@ CODEX_APP_JOB_LABEL="com.openai.codex-dream-skin-studio.app" INJECTOR_JOB_LABEL="com.openai.codex-dream-skin-studio.injector" EXPECTED_CODEX_TEAM_ID="2DC432GLL2" EXPECTED_CODEX_REQUIREMENT="anchor apple generic and certificate leaf[subject.OU] = \"$EXPECTED_CODEX_TEAM_ID\"" -SKIN_VERSION="1.5.6" +SKIN_VERSION="1.6.0" DREAM_SKIN_VALIDATED_RUNTIME_PID="" DREAM_SKIN_VALIDATED_RUNTIME_BUNDLE="" DREAM_SKIN_VALIDATED_RUNTIME_EXE="" diff --git a/macos/scripts/injector.mjs b/macos/scripts/injector.mjs index aa3679d6..73651af1 100644 --- a/macos/scripts/injector.mjs +++ b/macos/scripts/injector.mjs @@ -43,7 +43,7 @@ const stableTestidLiteral = (testid) => { } return JSON.stringify(`[data-testid="${testid}"]`); }; -const SKIN_VERSION = "1.5.6"; +const SKIN_VERSION = "1.6.0"; const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]"]); const CDP_ID_PATTERN = /^[A-Za-z0-9._-]{1,200}$/; const MAX_ART_BYTES = 10 * 1024 * 1024; @@ -1084,11 +1084,16 @@ async function verifySession(session, expectedThemeId = null, expectedRevision = }; const homeIndicator = document.querySelector(${selectorLiteral("home-icon")}); const homeSignal = homeIndicator ?? document.querySelector(${selectorLiteral("game-source")}) ?? - document.querySelector(${selectorLiteral("home-suggestions")}); + document.querySelector(${selectorLiteral("home-suggestions")}) ?? + document.querySelector(${selectorLiteral("home-suggestion-cards")}); const homeRoute = homeSignal?.closest('[role="main"]') ?? null; const home = document.querySelector(${selectorLiteral("home-route")}); - const suggestions = home?.querySelector(${selectorLiteral("home-suggestions")}) ?? null; - const cardButtons = suggestions ? [...suggestions.querySelectorAll('button')] : []; + const suggestionLayers = [ + home?.querySelector(${selectorLiteral("home-suggestions")}) ?? null, + home?.querySelector(${selectorLiteral("home-suggestion-cards")}) ?? null, + ].filter((node, index, nodes) => node && nodes.indexOf(node) === index); + const cardButtons = [...new Set(suggestionLayers.flatMap((node) => + [...node.querySelectorAll('button')]))]; const cardBoxes = cardButtons.map(box); const visibleCards = cardBoxes.filter((item) => item?.visible); const suggestionLabels = cardButtons.flatMap((button) => { diff --git a/macos/tests/injector-bootstrap.test.mjs b/macos/tests/injector-bootstrap.test.mjs index 03a6bfbd..4c721662 100644 --- a/macos/tests/injector-bootstrap.test.mjs +++ b/macos/tests/injector-bootstrap.test.mjs @@ -110,5 +110,7 @@ assert.match( ); assert.match(source, /visibleSuggestionLabels\.length >= result\.visibleCardCount/); assert.match(source, /result\.suggestionLabelColorsMatch/); +assert.match(source, /selectorLiteral\("home-suggestion-cards"\)/, + "Live verification must inspect the separate portal-rendered starter-card layer."); console.log("PASS: early injection is L0-ready, generation-safe, and removed on shutdown."); diff --git a/macos/tests/run-tests.sh b/macos/tests/run-tests.sh index e5c9ec3e..1599410f 100755 --- a/macos/tests/run-tests.sh +++ b/macos/tests/run-tests.sh @@ -86,7 +86,7 @@ UPDATE_JSON="$({ })" "$NODE" -e ' const value = JSON.parse(process.argv[1]); - if (value.currentVersion !== "v1.5.6" || value.latestVersion !== "v9.8.7") process.exit(1); + if (value.currentVersion !== "v1.6.0" || value.latestVersion !== "v9.8.7") process.exit(1); if (!value.updateAvailable) process.exit(1); if (value.releaseUrl !== "https://github.com/Fei-Away/Codex-Dream-Skin/releases/latest") process.exit(1); ' "$UPDATE_JSON" @@ -1071,7 +1071,7 @@ CRLF_BACKUP="$TMP/config-crlf-backup.json" "$NODE" "$ROOT/scripts/theme-config.mjs" restore "$CRLF_CONFIG" "$CRLF_BACKUP" >/dev/null /usr/bin/cmp -s "$CRLF_CONFIG" "$TMP/original-crlf.toml" -/usr/bin/env -u HOME /bin/bash -c '. "$1/scripts/common-macos.sh"; [ -n "$HOME" ] && [ "$SKIN_VERSION" = "1.5.6" ]' _ "$ROOT" +/usr/bin/env -u HOME /bin/bash -c '. "$1/scripts/common-macos.sh"; [ -n "$HOME" ] && [ "$SKIN_VERSION" = "1.6.0" ]' _ "$ROOT" if [ "${CODEX_DREAM_SKIN_SKIP_DOCTOR:-0}" = "1" ]; then printf 'SKIP: Doctor requires an installed, signed Codex app.\n' DOCTOR_RESULT="skipped" diff --git a/macos/tests/safe-css-validator.test.mjs b/macos/tests/safe-css-validator.test.mjs index a72ea627..900c3347 100644 --- a/macos/tests/safe-css-validator.test.mjs +++ b/macos/tests/safe-css-validator.test.mjs @@ -50,7 +50,7 @@ test("accepts only the bounded public part/property/value contract", () => { border-style: solid; border-radius: 12px; box-shadow: 0 4px 18px rgba(0, 0, 0, 0.25); - backdrop-filter: blur(18px); + backdrop-filter: blur(var(--ds-theme-surface-blur)); transition-property: background-color, border-color, box-shadow; transition-duration: 180ms; } @@ -132,6 +132,8 @@ test("rejects layout, concealment, interaction, animation, and unbounded values" assertRejected(`[data-ds-part="main"] { border-width: 99px; }`, "value/unsupported"); assertRejected(`[data-ds-part="main"] { border-radius: 999px; }`, "value/unsupported"); assertRejected(`[data-ds-part="main"] { backdrop-filter: blur(21px); }`, "value/unsupported"); + assertRejected(`[data-ds-part="main"] { backdrop-filter: blur(var(--unknown)); }`, "value/unsupported"); + assertRejected(`[data-ds-part="main"] { backdrop-filter: blur(var(--ds-theme-surface-blur, 20px)); }`, "value/unsupported"); assertRejected(`[data-ds-part="main"] { transition-duration: 10s; }`, "value/unsupported"); }); diff --git a/macos/tests/window-readiness.test.mjs b/macos/tests/window-readiness.test.mjs index bc11a6d1..876573d1 100644 --- a/macos/tests/window-readiness.test.mjs +++ b/macos/tests/window-readiness.test.mjs @@ -56,6 +56,20 @@ const baseRenderer = { assert.equal(readyNativeWindow.status, "ready"); assert.equal(assessRendererVerification(baseRenderer, readyNativeWindow, exactPayload).pass, true); +const hiddenHomeSuggestions = { + ...baseRenderer, + scope: { level: "L1", baseState: "home" }, + homeRoute: true, + homePresent: true, + hero: { visible: true, width: 900, height: 650 }, + visibleCardCount: 0, +}; +assert.equal( + assessRendererVerification(hiddenHomeSuggestions, readyNativeWindow, exactPayload).pass, + true, + "A themed home page intentionally hiding native starter cards must verify.", +); + const windowCalls = []; assert.equal((await inspectNativeWindow({ target: { id: "target-main" }, @@ -171,6 +185,9 @@ assert.match( "Window activation must use the exact bundle without starting another instance.", ); +if (process.platform === "win32") { + console.log("SKIP: macOS shell startup fixture requires /bin/bash."); +} else { const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "dreamskin-window-ready-test.")); let watcherPid = null; try { @@ -325,5 +342,6 @@ mark_state_active() { /usr/bin/printf 'active\\n' > "$ACTIVE_MARKER"; } } await fs.rm(fixtureRoot, { recursive: true, force: true }); } +} console.log("PASS: native-window readiness commits active only after visible verification."); diff --git a/runtime/dream-skin.css b/runtime/dream-skin.css index b2559ffc..63176f41 100644 --- a/runtime/dream-skin.css +++ b/runtime/dream-skin.css @@ -49,6 +49,11 @@ --ds-immersive-mid: rgb(var(--ds-bg-rgb) / .26); --ds-immersive-far: rgb(var(--ds-bg-rgb) / .16); --ds-immersive-sidebar: rgb(var(--ds-panel-rgb) / .46); + --ds-sidebar-block-start: var(--ds-immersive-sidebar); + --ds-sidebar-block-end: var(--ds-immersive-edge); + --ds-main-block-start: var(--ds-immersive-edge); + --ds-main-block-mid: var(--ds-immersive-mid); + --ds-main-block-end: var(--ds-immersive-far); --ds-task-immersive-sidebar: rgb(var(--ds-panel-rgb) / .70); --ds-immersive-chrome: rgb(var(--ds-panel-rgb) / .28); --ds-immersive-composer: rgb(var(--ds-panel-rgb) / .44); @@ -99,6 +104,11 @@ html[data-dream-skin="active"][data-dream-shell="light"] { --ds-immersive-mid: rgb(var(--ds-panel-rgb) / .28); --ds-immersive-far: rgb(var(--ds-panel-rgb) / .14); --ds-immersive-sidebar: rgb(var(--ds-panel-rgb) / .48); + --ds-sidebar-block-start: var(--ds-immersive-sidebar); + --ds-sidebar-block-end: var(--ds-immersive-edge); + --ds-main-block-start: var(--ds-immersive-edge); + --ds-main-block-mid: var(--ds-immersive-mid); + --ds-main-block-end: var(--ds-immersive-far); --ds-task-immersive-sidebar: rgb(var(--ds-panel-rgb) / .72); --ds-immersive-chrome: rgb(var(--ds-panel-rgb) / .34); --ds-immersive-composer: rgb(var(--ds-panel-rgb) / .46); @@ -343,6 +353,14 @@ html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream- height: auto; } +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"]):not(:has(__DREAM_SELECTOR_SHELL_MAIN__ __DREAM_SELECTOR_HOME_ROUTE_CSS__)) { + --ds-sidebar-block-start: var(--ds-task-immersive-sidebar); + --ds-sidebar-block-end: var(--ds-task-immersive-edge); + --ds-main-block-start: var(--ds-task-immersive-edge); + --ds-main-block-mid: var(--ds-task-immersive-mid); + --ds-main-block-end: var(--ds-task-immersive-far); +} + html[data-dream-skin="active"]: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"]:has(__DREAM_SELECTOR_SHELL_MAIN__):not(:has(__DREAM_SELECTOR_SHELL_MAIN__ __DREAM_SELECTOR_HOME_ROUTE_CSS__)) body { background-image: var(--dream-skin-art) !important; background-position: var(--ds-art-position) !important; @@ -538,80 +556,67 @@ html[data-dream-skin="active"] __DREAM_SELECTOR_SHELL_MAIN__ > __DREAM_SELECTOR_ backdrop-filter: blur(14px) saturate(108%) !important; } -/* Decorative chrome lives on the native shell; no injected positioning host is needed. */ -html[data-dream-skin="active"] - __DREAM_SELECTOR_SHELL_MAIN__:not(:has(__DREAM_SELECTOR_HOME_ROUTE_CSS__)) > __DREAM_SELECTOR_HEADER_TINT__::before { - content: var(--dream-skin-name, "Codex Dream Skin") " · " - var(--dream-skin-brand-subtitle, "CODEX DREAM SKIN"); - position: absolute; - left: 22px; - top: 4px; - z-index: 2; - max-width: min(42%, 360px); - overflow: hidden; - color: var(--ds-accent); - font: 800 11px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - letter-spacing: .03em; - text-overflow: ellipsis; - text-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .32); - white-space: nowrap; - pointer-events: none; +/* Windows keeps this native bar outside main; macOS simply has no match. */ +html[data-dream-skin="active"][data-dream-shell="dark"] + [class~="group/application-menu-top-bar"] { + color: var(--ds-text) !important; + background: rgb(var(--ds-panel-rgb) / .90) !important; + border-bottom: 1px solid var(--ds-line) !important; + box-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .72) !important; + backdrop-filter: blur(16px) saturate(90%) !important; } -html[data-dream-skin="active"] - __DREAM_SELECTOR_SHELL_MAIN__:not(:has(__DREAM_SELECTOR_HOME_ROUTE_CSS__)) > __DREAM_SELECTOR_HEADER_TINT__::after { - content: var(--dream-skin-status, "DREAM SKIN ONLINE"); - position: absolute; - right: 84px; - top: 13px; - z-index: 2; - max-width: 28%; - overflow: hidden; - color: var(--ds-muted); - font: 700 9px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - letter-spacing: .08em; - text-overflow: ellipsis; - text-shadow: 0 1px 10px rgb(var(--ds-bg-rgb) / .32); - white-space: nowrap; - pointer-events: none; +html[data-dream-skin="active"][data-dream-shell="dark"] + [class~="group/application-menu-top-bar"] :is(button, svg) { + color: var(--ds-text) !important; + text-shadow: 0 1px 2px rgb(var(--ds-bg-rgb) / .82) !important; } -html[data-dream-skin="active"] - __DREAM_SELECTOR_SHELL_MAIN__:has(__DREAM_SELECTOR_HOME_ROUTE_CSS__)::after { - content: var(--dream-skin-quote, "MAKE SOMETHING WONDERFUL"); - position: absolute; - right: 28px; - bottom: 72px; - z-index: 2; - pointer-events: none; - color: rgb(var(--ds-accent-rgb) / .74); - font: italic 14px/1.2 "Segoe Print", "Comic Sans MS", cursive; - letter-spacing: 0; - text-shadow: 0 0 13px rgb(var(--ds-accent-rgb) / .30); - transform: rotate(-3deg); +/* Continue the sidebar through the Windows application menu while keeping the + unmasked home artwork clear. The renderer publishes the live split without + synchronous layout reads. */ +html[data-dream-skin="active"][data-dream-chrome-mode="left"]:has(__DREAM_SELECTOR_LEFT_PANEL__) + [class~="group/application-menu-top-bar"] { + background-color: transparent !important; + background-image: linear-gradient(90deg, + var(--ds-sidebar-block-start) 0, + var(--ds-sidebar-block-end) var(--ds-sidebar-extent, 0px), + transparent var(--ds-sidebar-extent, 0px)) !important; + background-position: 0 0 !important; + background-size: auto !important; + background-repeat: no-repeat !important; + border-bottom: 0 !important; + box-shadow: none !important; + backdrop-filter: none !important; } -html[data-dream-skin="active"][data-dream-shell="light"] - __DREAM_SELECTOR_SHELL_MAIN__:not(:has(__DREAM_SELECTOR_HOME_ROUTE_CSS__)) > __DREAM_SELECTOR_HEADER_TINT__::before, -html[data-dream-skin="active"][data-dream-shell="light"] - __DREAM_SELECTOR_SHELL_MAIN__:not(:has(__DREAM_SELECTOR_HOME_ROUTE_CSS__)) > __DREAM_SELECTOR_HEADER_TINT__::after { - text-shadow: none; +/* Conversation routes mask the main surface as well. Use the exact same + non-home condition as that surface: during new-conversation startup the + main mask appears before the thread scroller, so waiting for the scroller + would expose a temporary bright menu strip. A single continuous gradient + shares one stop at the fractional sidebar edge, avoiding both a transparent + raster gap and double-alpha dark overlap. */ +html[data-dream-skin="active"][data-dream-chrome-mode="left"]:has(__DREAM_SELECTOR_LEFT_PANEL__):not(:has(__DREAM_SELECTOR_SHELL_MAIN__ __DREAM_SELECTOR_HOME_ROUTE_CSS__)) + [class~="group/application-menu-top-bar"] { + background-image: linear-gradient(90deg, + var(--ds-sidebar-block-start) 0, + var(--ds-sidebar-block-end) var(--ds-sidebar-extent, 0px), + var(--ds-main-block-mid) calc(64% + var(--ds-sidebar-extent, 0px) * .36), + var(--ds-main-block-end) 100%) !important; } -/* Windows keeps this native bar outside main; macOS simply has no match. */ -html[data-dream-skin="active"][data-dream-shell="dark"] +html[data-dream-skin="active"][data-dream-chrome-mode="full"] [class~="group/application-menu-top-bar"] { color: var(--ds-text) !important; background: rgb(var(--ds-panel-rgb) / .90) !important; border-bottom: 1px solid var(--ds-line) !important; - box-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .72) !important; + box-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .42) !important; backdrop-filter: blur(16px) saturate(90%) !important; } -html[data-dream-skin="active"][data-dream-shell="dark"] +html[data-dream-skin="active"][data-dream-chrome-mode="full"] [class~="group/application-menu-top-bar"] :is(button, svg) { color: var(--ds-text) !important; - text-shadow: 0 1px 2px rgb(var(--ds-bg-rgb) / .82) !important; } html[data-dream-skin="active"] __DREAM_SELECTOR_SHELL_MAIN__ [role="main"] { @@ -670,6 +675,85 @@ html[data-dream-skin="active"] content: none !important; } +/* Codex 26.721+ renders the title, suggestions and composer in the sibling + after the native banner wrapper. Keep that real content visible while + restoring a compact, safe-area-aware composition instead of stretching + every control across the full window. */ +html[data-dream-skin="active"] + __DREAM_SELECTOR_HOME_ROUTE__ > div:first-child:has(> __DREAM_SELECTOR_HOME_BANNERS__) + div { + box-sizing: border-box; + width: min(1120px, calc(100% - 48px)) !important; + max-width: 1120px !important; + margin-inline: auto !important; +} + +html[data-dream-skin="active"] + __DREAM_SELECTOR_HOME_ROUTE__ > div:first-child:has(> __DREAM_SELECTOR_HOME_BANNERS__) + div + __DREAM_SELECTOR_GAME_SOURCE__ { + box-sizing: border-box; + width: min(48%, 520px) !important; + margin-right: auto !important; + padding: 18px 22px !important; + border: 1px solid rgb(var(--ds-muted-rgb) / .16); + border-radius: 18px; + background: rgb(var(--ds-panel-rgb) / .58); + box-shadow: 0 10px 28px rgb(var(--ds-bg-rgb) / .16); + backdrop-filter: blur(10px) saturate(104%); + text-align: left !important; +} + +html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) + __DREAM_SELECTOR_HOME_ROUTE__ > div:first-child:has(> __DREAM_SELECTOR_HOME_BANNERS__) + div + __DREAM_SELECTOR_GAME_SOURCE__ { + margin-right: 0 !important; + margin-left: auto !important; + text-align: right !important; +} + +html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) + __DREAM_SELECTOR_HOME_ROUTE__ > div:first-child:has(> __DREAM_SELECTOR_HOME_BANNERS__) + div + __DREAM_SELECTOR_GAME_SOURCE__ { + width: min(62%, 640px) !important; + margin-inline: auto !important; + text-align: center !important; +} + +html[data-dream-skin="active"] + __DREAM_SELECTOR_HOME_ROUTE__ > div:first-child:has(> __DREAM_SELECTOR_HOME_BANNERS__) + div + __DREAM_SELECTOR_HOME_SUGGESTIONS__, +html[data-dream-skin="active"] + __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTION_CARDS__, +html[data-dream-skin="active"] + __DREAM_SELECTOR_HOME_ROUTE__ > div:first-child:has(> __DREAM_SELECTOR_HOME_BANNERS__) + div + __DREAM_SELECTOR_HOME_UTILITY__, +html[data-dream-skin="active"] + __DREAM_SELECTOR_HOME_ROUTE__ > div:first-child:has(> __DREAM_SELECTOR_HOME_BANNERS__) + div + __DREAM_SELECTOR_COMPOSER_CHROME__ { + box-sizing: border-box; + width: min(980px, 100%) !important; + max-width: 980px !important; + margin-inline: auto !important; +} + +/* v1.6.0 keeps the v1.5.9 home as the upgrade default, while allowing the + v1.5.8 clean wallpaper-first home to hide only the title and both native + suggestion layers. The composer and project controls remain interactive. */ +html[data-dream-skin="active"][data-dream-home-mode="clean"] + __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_GAME_SOURCE__, +html[data-dream-skin="active"][data-dream-home-mode="clean"] + __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__, +html[data-dream-skin="active"][data-dream-home-mode="clean"] + __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTION_CARDS__ { + display: none !important; +} + +html[data-dream-skin="active"] + __DREAM_SELECTOR_HOME_ROUTE__ > div:first-child:has(> __DREAM_SELECTOR_HOME_BANNERS__) + div + __DREAM_SELECTOR_HOME_SUGGESTIONS__ button, +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTION_CARDS__ button { + min-height: 104px !important; +} + /* Codex 26.721+: this chain (3-6 first-child levels deep) targets the old compact hero-art card that used to live inside div:first-child's second level. That level is now the empty .home-banners slot (see #244), so on @@ -827,9 +911,13 @@ html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ > div:first-child > margin-top: 13px !important; } -html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ { overflow: visible !important; } +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__, +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTION_CARDS__ { + overflow: visible !important; +} -html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button { +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button, +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTION_CARDS__ button { position: relative !important; min-height: 118px !important; padding: 14px 13px 12px !important; @@ -850,17 +938,20 @@ html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HO border-color .18s cubic-bezier(.22, 1, .36, 1) !important; } -html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button [class~="text-token-text-primary"] { +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button [class~="text-token-text-primary"], +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTION_CARDS__ button [class~="text-token-text-primary"] { color: var(--ds-text) !important; } -html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button:hover { +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button:hover, +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTION_CARDS__ button:hover { transform: translateY(-2px) !important; border-color: rgb(var(--ds-accent-rgb) / .42) !important; box-shadow: 0 12px 28px rgb(var(--ds-bg-rgb) / .24) !important; } -html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button > span:first-child > span:first-child { +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button > span:first-child > span:first-child, +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTION_CARDS__ button > span:first-child > span:first-child { width: 38px; height: 38px; /* Native spans carry justify-start; grid + place-items cannot override @@ -876,7 +967,8 @@ html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HO box-shadow: 0 0 0 6px rgb(var(--ds-accent-rgb) / .05); } -html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button svg { +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button svg, +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTION_CARDS__ button svg { width: 21px !important; height: 21px !important; display: block !important; @@ -885,8 +977,10 @@ html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HO color: var(--ds-accent) !important; } -html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button > span:first-child { justify-content: center !important; } -html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button > span:last-child { align-items: center !important; text-align: center !important; } +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button > span:first-child, +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTION_CARDS__ button > span:first-child { justify-content: center !important; } +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button > span:last-child, +html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTION_CARDS__ button > span:last-child { align-items: center !important; text-align: center !important; } html[data-dream-skin="active"] __DREAM_SELECTOR_COMPOSER_CHROME__ { overflow: visible !important; @@ -926,10 +1020,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"]:has(__DREAM_SELECTOR_ } html[data-dream-skin="active"][data-dream-art-wide="true"] __DREAM_SELECTOR_SHELL_MAIN__:has(__DREAM_SELECTOR_HOME_ROUTE_CSS__) { - background: linear-gradient(90deg, - var(--ds-immersive-edge), - var(--ds-immersive-mid) 64%, - var(--ds-immersive-far)) !important; + background: transparent !important; border: 0 !important; border-radius: 0 !important; box-shadow: none !important; @@ -971,7 +1062,9 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] __DREAM_SELECTOR_SHEL } html[data-dream-skin="active"][data-dream-art-wide="true"] __DREAM_SELECTOR_SHELL_MAIN__:has(__DREAM_SELECTOR_HOME_ROUTE_CSS__) - __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button { + __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button, +html[data-dream-skin="active"][data-dream-art-wide="true"] __DREAM_SELECTOR_SHELL_MAIN__:has(__DREAM_SELECTOR_HOME_ROUTE_CSS__) + __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTION_CARDS__ button { background: rgb(var(--ds-panel-rgb) / .56) !important; box-shadow: 0 8px 22px rgb(var(--ds-bg-rgb) / .16), @@ -1199,7 +1292,8 @@ html[data-dream-skin="active"] [data-message-author-role] { border-radius: 16px; html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_GAME_SOURCE__ { font-size: 18px !important; } html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_GAME_SOURCE__::before { margin-bottom: 7px; font-size: 10px; } html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_GAME_SOURCE__::after { margin-top: 9px; font-size: 11px; } - html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button { min-height: 112px !important; font-size: 12px !important; } + html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTIONS__ button, + html[data-dream-skin="active"] __DREAM_SELECTOR_HOME_ROUTE__ __DREAM_SELECTOR_HOME_SUGGESTION_CARDS__ button { min-height: 112px !important; font-size: 12px !important; } } @media (prefers-reduced-motion: reduce) { diff --git a/runtime/renderer-inject.js b/runtime/renderer-inject.js index 4066c711..b9c91ea4 100644 --- a/runtime/renderer-inject.js +++ b/runtime/renderer-inject.js @@ -12,6 +12,7 @@ "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", "data-dream-art-ready", + "data-dream-chrome-mode", "data-dream-home-mode", ]; const VERSION = __DREAM_SKIN_VERSION_JSON__; const STYLE_REVISION = __DREAM_SKIN_STYLE_REVISION_JSON__; @@ -43,7 +44,7 @@ "--ds-theme-surface-shadow", "--ds-theme-image-focus-x", "--ds-theme-image-focus-y", "--ds-theme-image-zoom", "--ds-theme-image-dim", "--ds-theme-image-task-intensity", - "--ds-theme-density-scale", "--ds-theme-motion-level", + "--ds-theme-density-scale", "--ds-theme-motion-level", "--ds-sidebar-extent", ]; const selectorByKey = new Map(SELECTOR_CONTRACT.selectors.map((entry) => [entry.key, entry])); const stableTestidSelector = (testid) => SELECTOR_CONTRACT.stableTestids?.includes(testid) @@ -57,6 +58,8 @@ let analysisTimer = null; let rootObserver = null; let partObserver = null; + let sidebarResizeObserver = null; + let observedSidebar = null; let bodyReadyHandler = null; let styleMode = null; let styleNode = null; @@ -550,6 +553,8 @@ const shell = resolvedShell(); setAttribute(root, "data-dream-skin", "active"); setAttribute(root, SHELL_ATTR, shell); + setAttribute(root, "data-dream-chrome-mode", THEME.chromeMode === "full" ? "full" : "left"); + setAttribute(root, "data-dream-home-mode", THEME.homeMode === "clean" ? "clean" : "classic"); setStyleProperty(root, "--dream-skin-art", `url("${artUrl}")`); applyTheme(root, shell); applyArtMetadata(root); @@ -581,11 +586,35 @@ } } }; + const setSidebarExtent = (width) => { + const numeric = Number(width); + const bounded = Number.isFinite(numeric) && numeric > 0 ? clamp(numeric, 0, 2048) : 0; + setStyleProperty(document.documentElement, "--ds-sidebar-extent", `${Number(bounded.toFixed(2))}px`); + }; + const inlineSidebarWidth = (sidebar) => { + const inline = sidebar?.style?.width || sidebar?.getAttribute?.("style") || ""; + const match = String(inline).match(/(?:^|;)\s*width\s*:\s*([\d.]+)px(?:\s*;|$)/i); + return match ? Number(match[1]) : null; + }; + const observeSidebar = (sidebar) => { + if (observedSidebar === sidebar) return; + if (observedSidebar) sidebarResizeObserver?.unobserve?.(observedSidebar); + observedSidebar = sidebar || null; + if (!observedSidebar) { + setSidebarExtent(0); + return; + } + const inlineWidth = inlineSidebarWidth(observedSidebar); + if (inlineWidth !== null) setSidebarExtent(inlineWidth); + sidebarResizeObserver?.observe?.(observedSidebar); + }; const refreshParts = () => { metrics.partPasses += 1; const desired = new Map(); addPart(desired, "root", [document.documentElement]); - addPart(desired, "sidebar", selectorNodes("left-panel")); + const sidebars = selectorNodes("left-panel"); + addPart(desired, "sidebar", sidebars); + observeSidebar(sidebars[0] || null); addPart(desired, "main", selectorNodes("shell-main")); addPart(desired, "header", selectorNodes("header-tint")); addPart(desired, "home", selectorNodes("home-route")); @@ -687,6 +716,7 @@ removeParts(); state?.rootObserver?.disconnect(); state?.partObserver?.disconnect(); + state?.sidebarResizeObserver?.disconnect(); if (bodyReadyHandler && typeof document.removeEventListener === "function") { document.removeEventListener("DOMContentLoaded", bodyReadyHandler); } @@ -733,7 +763,21 @@ }; if (typeof MutationObserver === "function") { rootObserver = new MutationObserver(() => scheduleEnsure({ root: true })); - partObserver = new MutationObserver(() => scheduleEnsure({ parts: true }, 80)); + // Some Codex sidebar transitions replace the route DOM without firing the + // Navigation API. Refresh scope with the same debounced pass that already + // remaps dynamic public parts so verification cannot retain a stale route. + partObserver = new MutationObserver(() => scheduleEnsure({ scope: true, parts: true }, 80)); + } + if (typeof ResizeObserver === "function") { + sidebarResizeObserver = new ResizeObserver((entries) => { + for (const entry of entries || []) { + if (entry?.target !== observedSidebar) continue; + const borderBox = Array.isArray(entry.borderBoxSize) + ? entry.borderBoxSize[0] : entry.borderBoxSize; + const width = borderBox?.inlineSize ?? entry.contentRect?.width; + if (Number.isFinite(Number(width))) setSidebarExtent(width); + } + }); } let mediaQuery = null; @@ -755,6 +799,7 @@ cleanup, rootObserver, partObserver, + sidebarResizeObserver, timer: null, scheduler, mediaQuery, @@ -803,7 +848,7 @@ bodyReadyHandler = () => { if (!window[DISABLED_KEY]) { observeBody(); - scheduleEnsure({ parts: true }, 0); + scheduleEnsure({ scope: true, parts: true }, 0); } }; document.addEventListener("DOMContentLoaded", bodyReadyHandler, { once: true }); diff --git a/runtime/safe-css-validator.mjs b/runtime/safe-css-validator.mjs index 8321e8ea..1d925d51 100644 --- a/runtime/safe-css-validator.mjs +++ b/runtime/safe-css-validator.mjs @@ -278,11 +278,9 @@ function validatePropertyValue(property, value) { } if (property === "backdrop-filter") { if (value.toLowerCase() === "none") return true; - const match = value.match(/^blur\(\s*([^\s)]+)\s*\)$/i); - return Boolean(match && ( - registeredVar(match[1], new Set(["--ds-theme-surface-blur"])) - || zeroOrPx(match[1], 0, 20) - )); + if (/^blur\(\s*var\(\s*--ds-theme-surface-blur\s*\)\s*\)$/i.test(value)) return true; + const match = value.match(/^blur\(\s*([^\s()]+)\s*\)$/i); + return Boolean(match && zeroOrPx(match[1], 0, 20)); } if (property === "font-family") return fontFamilyValue(value); if (property === "font-size") return numeric(value, 12, 20, "px"); diff --git a/tools/capture-dom-fixture.mjs b/tools/capture-dom-fixture.mjs index 29bd012e..749c246a 100644 --- a/tools/capture-dom-fixture.mjs +++ b/tools/capture-dom-fixture.mjs @@ -532,7 +532,7 @@ function deriveLabel(data) { let base = "state"; if (hit("overlay-dialog")) base = "dialog"; else if (hit("overlay-menu") || hit("overlay-popper")) base = "menu"; - else if (hit("home-icon")) base = "home"; + else if (hit("home-route") || hit("home-icon")) base = "home"; else if (hit("markdown")) base = "thread"; else if (hit("shell-main")) base = "route"; const scheme = data.appearance?.computedColorScheme || ""; diff --git a/tools/check-secrets.mjs b/tools/check-secrets.mjs new file mode 100644 index 00000000..fab77b1f --- /dev/null +++ b/tools/check-secrets.mjs @@ -0,0 +1,74 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const maxBytes = 2 * 1024 * 1024; +const binaryExtensions = new Set([ + ".avif", ".dmg", ".exe", ".gif", ".ico", ".jpeg", ".jpg", ".pdf", ".png", ".webp", ".zip", +]); +const rules = [ + ["private-key", /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/g], + ["openai-style-key", /\bsk-[A-Za-z0-9_-]{20,}\b/g], + ["github-token", /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b|\bgithub_pat_[A-Za-z0-9_]{20,}\b/g], + ["aws-access-key", /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g], + ["google-api-key", /\bAIza[A-Za-z0-9_-]{30,}\b/g], + ["slack-token", /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g], + ["slack-webhook", /https:\/\/hooks\.slack\.com\/services\/[^\s"'<>]+/g], + ["wechat-webhook", /https:\/\/qyapi\.weixin\.qq\.com\/cgi-bin\/webhook\/send[^\s"'<>]*/g], + ["feishu-webhook", /https:\/\/open\.feishu\.cn\/open-apis\/bot\/v2\/hook\/[^\s"'<>]+/g], + ["generic-secret-assignment", /\b(?:api[_-]?key|client[_-]?secret|access[_-]?token|auth[_-]?token|password)\b\s*[:=]\s*["'][^"'\r\n]{12,}["']/gi], +]; +const placeholder = /(?:your[_ -]?(?:api[_ -]?)?key|replace[_ -]?me|changeme|example|placeholder|dummy|test[_ -]?key|<[^>]+>|你的密钥)/i; + +function git(args) { + return execFileSync("git", ["-C", root, ...args], { encoding: "utf8" }); +} + +function repositoryFiles() { + return git(["ls-files", "--cached", "--others", "--exclude-standard", "-z"]) + .split("\0") + .filter(Boolean); +} + +function lineAt(text, offset) { + let line = 1; + for (let index = 0; index < offset; index += 1) { + if (text.charCodeAt(index) === 10) line += 1; + } + return line; +} + +const findings = []; +const files = repositoryFiles(); +for (const relative of files) { + if (binaryExtensions.has(path.extname(relative).toLowerCase())) continue; + const file = path.join(root, relative); + const stat = fs.statSync(file); + if (!stat.isFile() || stat.size > maxBytes) continue; + const text = fs.readFileSync(file, "utf8"); + for (const [type, pattern] of rules) { + pattern.lastIndex = 0; + for (const match of text.matchAll(pattern)) { + const lineStart = text.lastIndexOf("\n", match.index) + 1; + const lineEnd = text.indexOf("\n", match.index); + const lineText = text.slice(lineStart, lineEnd === -1 ? text.length : lineEnd); + if (placeholder.test(lineText)) continue; + findings.push({ path: relative, line: lineAt(text, match.index), type }); + } + } +} + +if (findings.length) { + for (const finding of findings) { + console.error(`${finding.path}:${finding.line}: possible ${finding.type}`); + } + console.error("Secret scan failed. Move real credentials to an ignored local .env and rotate exposed values."); + process.exit(1); +} + +console.log(`PASS: ${files.length} tracked or committable files contain no recognized secret patterns.`); diff --git a/tools/doctor-selectors.test.mjs b/tools/doctor-selectors.test.mjs index 71bc824c..5d441edc 100644 --- a/tools/doctor-selectors.test.mjs +++ b/tools/doctor-selectors.test.mjs @@ -11,14 +11,15 @@ const resultFor = (baseState, hits, overlay = false) => gradeDoctorResult(contra }); const home = resultFor("home", [ - "shell-main", "left-panel", "header-tint", "home-icon", "home-route", "home-route-css", + "shell-main", "left-panel", "header-tint", "home-route", "home-route-css", "game-source", ]); assert.equal(home.pass, true); assert.equal(home.exitCode, 0); -assert.equal(home.tiers.L1.length, 6); +assert.equal(home.tiers.L1.length, 5); +assert.equal(home.tiers.L2.find(({ key }) => key === "home-icon").status, "miss"); assert.equal(home.tiers.L2.find(({ key }) => key === "project-selector").status, "miss(config)"); -const brokenHome = resultFor("home", ["shell-main", "left-panel", "header-tint", "home-icon"]); +const brokenHome = resultFor("home", ["shell-main", "left-panel", "header-tint", "game-source"]); assert.equal(brokenHome.pass, false); assert.equal(brokenHome.exitCode, 1); diff --git a/tools/renderer-runtime.test.mjs b/tools/renderer-runtime.test.mjs index c0492567..af71d89c 100644 --- a/tools/renderer-runtime.test.mjs +++ b/tools/renderer-runtime.test.mjs @@ -35,6 +35,7 @@ function makeFixture({ nativeAppearance = "dark", settings = false, adopted = tr const domNodes = new Set(); const selectorNodes = new Map(); const observers = []; + const resizeObservers = []; const timers = new Map(); const intervals = new Map(); const listeners = new Map(); @@ -76,12 +77,15 @@ function makeFixture({ nativeAppearance = "dark", settings = false, adopted = tr }; const partFixtures = {}; if (!settings) { - partFixtures.sidebar = makeDomNode("sidebar", body); + partFixtures.sidebar = makeDomNode("sidebar", body, new Map([["style", "width: 420px;"]])); partFixtures.main = makeDomNode("main", body); partFixtures.header = makeDomNode("header", body); partFixtures.home = makeDomNode("home", partFixtures.main); partFixtures.homeHero = makeDomNode("home-hero", partFixtures.home); partFixtures.homeIcon = makeDomNode("home-icon", partFixtures.homeHero); + partFixtures.gameSource = makeDomNode("game-source", partFixtures.homeHero); + partFixtures.homeSuggestions = makeDomNode("home-suggestions", partFixtures.home); + partFixtures.homeSuggestionCards = makeDomNode("home-suggestion-cards", partFixtures.home); partFixtures.projectList = makeDomNode("project-list", partFixtures.home); partFixtures.thread = makeDomNode("thread", partFixtures.main); partFixtures.message = makeDomNode("message", partFixtures.thread); @@ -91,7 +95,10 @@ function makeFixture({ nativeAppearance = "dark", settings = false, adopted = tr register("main.main-surface", partFixtures.main); register("header.app-header-tint", partFixtures.header); register('[data-testid="home-icon"]', partFixtures.homeIcon); - register('[role="main"]:has([data-testid="home-icon"])', partFixtures.home); + register('[data-feature="game-source"]', partFixtures.gameSource); + register('[data-home-ambient-suggestions]', partFixtures.homeSuggestions); + register(".group\\/home-suggestions", partFixtures.homeSuggestionCards); + register('[role="main"]:has([data-feature="game-source"])', partFixtures.home); register('[role="main"]', partFixtures.home); register(".group\\/project-selector", partFixtures.projectList); register(".thread-scroll-container", partFixtures.thread); @@ -141,6 +148,12 @@ function makeFixture({ nativeAppearance = "dark", settings = false, adopted = tr class MockSheet { replaceSync(text) { this.text = text; } } + class MockResizeObserver { + constructor(callback) { this.callback = callback; this.targets = new Set(); resizeObservers.push(this); } + observe(target) { this.targets.add(target); } + unobserve(target) { this.targets.delete(target); } + disconnect() { this.disconnected = true; this.targets.clear(); } + } const window = { navigation, matchMedia() { @@ -157,6 +170,7 @@ function makeFixture({ nativeAppearance = "dark", settings = false, adopted = tr window, document, MutationObserver: MockMutationObserver, + ResizeObserver: MockResizeObserver, CSSStyleSheet: adopted ? MockSheet : undefined, Blob, Uint8Array, @@ -194,7 +208,8 @@ function makeFixture({ nativeAppearance = "dark", settings = false, adopted = tr }; return { addDynamicMessage, attrs, context, document, domNodes, flushTimers, intervals, listeners, - nodes, observers, partFixtures, payloadFor, revoked, root, rootClasses, rootStyle, timers, window, + nodes, observers, partFixtures, payloadFor, resizeObservers, revoked, root, rootClasses, rootStyle, + timers, window, }; } @@ -244,7 +259,8 @@ export async function runRendererRuntimeTest(assetRoot) { assert.doesNotMatch(template, /electron-opaque|home-suggestion-list-item/, "Runtime payload must not carry retired selector documentation/fossils."); assert.doesNotMatch(template, /classList\.(add|remove|toggle)/); - assert.doesNotMatch(template, /getBoundingClientRect|ResizeObserver/); + assert.doesNotMatch(template, /getBoundingClientRect/); + assert.match(template, /ResizeObserver/); assert.match(template, /childList:\s*true/); assert.match(template, /subtree:\s*true/); // The new contract intentionally keeps the `data-dream-*` attribute names @@ -258,9 +274,27 @@ export async function runRendererRuntimeTest(assetRoot) { assert.match(css, /main\.main-surface:has\(\[role="main"\]\)/); assert.match(css, /main\.main-surface:not\(:has\(\[role="main"\]\)\)/); assert.doesNotMatch(css, /:has\([^()]*:has\(/); - assert.match(css, /content:\s*var\(--dream-skin-name[\s\S]{0,180}var\(--dream-skin-brand-subtitle/); - assert.match(css, /content:\s*var\(--dream-skin-status/); - assert.match(css, /content:\s*var\(--dream-skin-quote/); + // A wide-image home/new-task route must expose the wallpaper directly. + // Conversation routes retain their own task gradient behind this separate + // positive home gate. + assert.match(css, + /\[data-dream-art-wide="true"\]\s+main\.main-surface:has\(\[role="main"\]\)\s*\{\s*background:\s*transparent\s*!important;/); + assert.doesNotMatch(css, /content:\s*var\(--dream-skin-brand-subtitle/); + assert.doesNotMatch(css, /content:\s*var\(--dream-skin-status/); + assert.doesNotMatch(css, /content:\s*var\(--dream-skin-quote/); + assert.match(css, /> div:first-child:has\(> \.home-banners\) \+ div/); + assert.match(css, /\[role="main"\]:has\(\[data-feature="game-source"\]\) \[data-feature="game-source"\]/); + assert.doesNotMatch(css, + /\[data-feature="game-source"\]\s*\{\s*display:\s*none\s*!important;/); + assert.match(css, + /\.group\\\/home-suggestions button\s*\{[\s\S]*?min-height:\s*104px\s*!important;/); + assert.match(css, + /\.group\\\/home-suggestions button\s*\{[\s\S]*?border-radius:\s*18px\s*!important;[\s\S]*?backdrop-filter:\s*blur\(10px\)/); + assert.match(css, /width:\s*min\(980px, 100%\)/); + assert.match(css, /var\(--ds-sidebar-block-end\) var\(--ds-sidebar-extent, 0px\)/); + assert.match(css, /transparent var\(--ds-sidebar-extent, 0px\)/); + assert.match(css, /var\(--ds-main-block-mid\) calc\(64% \+ var\(--ds-sidebar-extent, 0px\) \* \.36\)/); + assert.doesNotMatch(css, /calc\(var\(--ds-sidebar-extent, 0px\) [+-] 1px\)/); assert.match(css, /--ds-task-full-veil/); assert.match(css, /data-dream-task-mode="full"/); assert.match(css, /background-image:\s*var\(--ds-task-full-veil\),\s*var\(--dream-skin-art\)/); @@ -268,7 +302,7 @@ export async function runRendererRuntimeTest(assetRoot) { // marker-class-to-:has() conversion must never leave native layout rules // active after pause/restore. const unscoped = unscopedCssRules(css).join("\n"); - assert.doesNotMatch(unscoped, /\[role="main"\]:has\(\[data-testid="home-icon"\]\)/); + assert.doesNotMatch(unscoped, /\[role="main"\]:has\(\[data-feature="game-source"\]\)/); assert.doesNotMatch(unscoped, /\.group\\\/project-selector/); const home = makeFixture({ nativeAppearance: "dark" }); @@ -276,6 +310,8 @@ export async function runRendererRuntimeTest(assetRoot) { const state = home.window.__CODEX_DREAM_SKIN_STATE__; assert.equal(home.attrs.get("data-dream-skin"), "active"); assert.equal(home.attrs.get("data-dream-shell"), "dark"); + assert.equal(home.attrs.get("data-dream-chrome-mode"), "left"); + assert.equal(home.attrs.get("data-dream-home-mode"), "classic"); assert.equal(home.attrs.get("data-ds-part"), "root"); assert.equal(state.styleMode, "adopted"); assert.equal(home.document.adoptedStyleSheets.length, 1); @@ -286,6 +322,14 @@ export async function runRendererRuntimeTest(assetRoot) { assert.equal(home.rootStyle.values.get("--ds-theme-surface-radius"), "12px"); assert.equal(home.rootStyle.values.get("--ds-theme-surface-opacity"), "1"); assert.equal(home.rootStyle.values.get("--ds-theme-surface-blur"), "0px"); + assert.equal(home.rootStyle.values.get("--ds-sidebar-extent"), "420px"); + const sidebarResizeObserver = home.resizeObservers[0]; + assert.ok(sidebarResizeObserver?.targets.has(home.partFixtures.sidebar)); + sidebarResizeObserver.callback([{ + target: home.partFixtures.sidebar, + borderBoxSize: { inlineSize: 419.65625 }, + }]); + assert.equal(home.rootStyle.values.get("--ds-sidebar-extent"), "419.66px"); const publicDefaults = { "--ds-theme-font-family": "system", "--ds-theme-font-scale": "1", @@ -330,6 +374,8 @@ export async function runRendererRuntimeTest(assetRoot) { partObserver.callback([{ type: "childList" }]); home.flushTimers(80); assert.equal(dynamicMessage.getAttribute("data-ds-part"), "message"); + assert.equal(state.metrics.routePasses, 2, + "Route scope must refresh when Codex replaces route DOM without a navigation event"); const full = makeFixture({ nativeAppearance: "dark" }); vm.runInNewContext(full.payloadFor({ art: { taskMode: "full" } }), full.context); @@ -355,6 +401,15 @@ export async function runRendererRuntimeTest(assetRoot) { explicitColorKeys: Object.keys(explicitColors), colors: explicitColors, }), explicitLight.context); + assert.equal(explicitLight.attrs.get("data-dream-chrome-mode"), "left"); + + const fullChrome = makeFixture({ nativeAppearance: "dark" }); + vm.runInNewContext(fullChrome.payloadFor({ chromeMode: "full" }), fullChrome.context); + assert.equal(fullChrome.attrs.get("data-dream-chrome-mode"), "full"); + + const cleanHome = makeFixture({ nativeAppearance: "dark" }); + vm.runInNewContext(cleanHome.payloadFor({ homeMode: "clean" }), cleanHome.context); + assert.equal(cleanHome.attrs.get("data-dream-home-mode"), "clean"); const renderedColors = { background: "--ds-bg", panel: "--ds-panel", @@ -406,13 +461,13 @@ export async function runRendererRuntimeTest(assetRoot) { rootObserver.callback([]); home.flushTimers(64); - assert.equal(state.metrics.routePasses, 1, "Attribute safety pass must not be a route pass"); + assert.equal(state.metrics.routePasses, 2, "Attribute safety pass must not be a route pass"); const navigationHandler = home.listeners.get("navigation:navigate"); assert.equal(typeof navigationHandler, "function"); navigationHandler(); home.flushTimers(180); assert.equal(state.metrics.navigationEvents, 1); - assert.equal(state.metrics.routePasses, 2); + assert.equal(state.metrics.routePasses, 3); const settings = makeFixture({ nativeAppearance: "light", settings: true }); vm.runInNewContext(settings.payloadFor(), settings.context); diff --git a/tools/selectors.json b/tools/selectors.json index 06e22231..2fc46e0a 100644 --- a/tools/selectors.json +++ b/tools/selectors.json @@ -29,15 +29,16 @@ { "key": "shell-main", "selector": "main.main-surface", "tier": "L1", "scope": "all", "required": true, "notes": "全局主表面。mac 设置页缺失(win 存在),缺失时整页仅 L0" }, { "key": "left-panel", "selector": "aside.app-shell-left-panel", "tier": "L1", "scope": "all", "required": true, "notes": "左侧栏;收起/重建时会移除节点,勿在其消失时清皮" }, { "key": "header-tint", "selector": "header.app-header-tint", "tier": "L1", "scope": "all", "required": true, "notes": "顶栏;皮肤不得覆盖其 position/z-index(既有回归约束)" }, - { "key": "home-icon", "selector": "[data-testid=\"home-icon\"]", "tier": "L1", "scope": "home", "required": true, "notes": "首页判定信号,双端一致" }, - { "key": "home-route", "selector": "[role=\"main\"]:has([data-testid=\"home-icon\"])", "tier": "L1", "scope": "home", "required": true, "notes": "首页容器。实测 [role=main] 仅存在于首页——它不是通用路由锚点,thread 页没有" }, + { "key": "home-icon", "selector": "[data-testid=\"home-icon\"]", "tier": "L2", "scope": "home", "required": false, "notes": "旧版首页标记;Codex 26.721.4979 已不再渲染,仅保留为兼容探针" }, + { "key": "home-route", "selector": "[role=\"main\"]:has([data-feature=\"game-source\"])", "tier": "L1", "scope": "home", "required": true, "notes": "Codex 26.721.4979 首页容器,以稳定的原生 hero 标记判定;thread 页没有 role=main" }, { "key": "home-route-css", "selector": "[role=\"main\"]", "tier": "L1", "scope": "home", "required": true, "notes": "home-route 的 CSS 可组合别名:不含 :has()。CSS 禁止 :has() 嵌套,home-route 原选择器一旦写进 :has()/:not(:has()) 整条规则会被解析器丢弃(v1.3.1 全窗口首页与任务页氛围背景因此失效)。依据与 home-route 相同的实测:[role=main] 仅存在于首页;该锚点缺失时 CSS 路由门控整体失效,故必须按 L1 必需项监控" }, { "key": "home-banners", "selector": ".home-banners", "tier": "L2", "scope": "home", "required": false, "notes": "Codex 26.721+ 新增的原生首页 banner 插槽,位于 home-route 第一个子节点内、通常为空(0 子节点)靠原生 empty:hidden 折叠。真正的首页内容(标题/建议卡/输入框)在这个版本上已变成该插槽外层容器的兄弟节点,不再是它的后代(见 #244 实测:home-route > div:first-child 被撑到 100% 高度会把内容挤出视口)。用于判定新旧 home DOM 形态,皮肤据此把 first-child 链路的强制尺寸限定在旧形态或改走兄弟节点" }, { "key": "composer-chrome", "selector": ".composer-surface-chrome", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框壳。win 实测存在无 composer 的 detail 路由,不可当全局锚点" }, { "key": "composer-toolbar", "selector": ".composer-surface-chrome [class*=\"_footer_\"]", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框工具栏;CSS Modules 名称跨端稳定,hash 随版本变化" }, { "key": "home-utility", "selector": "[class*=\"_homeUtilityBar_\"]", "tier": "L2", "scope": "home", "required": false, "notes": "CSS Modules 前缀匹配;模块名跨端稳定、hash 随版本变,禁止匹配完整 hash" }, { "key": "game-source", "selector": "[data-feature=\"game-source\"]", "tier": "L2", "scope": "home", "required": false }, - { "key": "home-suggestions", "selector": ".group\\/home-suggestions", "tier": "L2", "scope": "home", "required": false }, + { "key": "home-suggestions", "selector": "[data-home-ambient-suggestions]", "tier": "L2", "scope": "home", "required": false, "notes": "Codex 26.721.4979 原生首页建议区的显式稳定标记" }, + { "key": "home-suggestion-cards", "selector": ".group\\/home-suggestions", "tier": "L2", "scope": "home", "required": false, "notes": "Codex 26.721.4979 renders the visible starter cards in this separate layer; keep it distinct because callers append button selectors." }, { "key": "project-selector", "selector": ".group\\/project-selector", "tier": "L2", "scope": "home config", "required": false, "notes": "依赖用户项目配置;win 快照未出现(该机可能未配置项目),不算平台差异" }, { "key": "markdown", "selector": "[class*=\"_markdown\"]", "tier": "L2", "scope": "thread", "required": false, "notes": "会话内容块,单页可达 100+ 个,样式规则注意性能" }, { "key": "thread-surface", "selector": ".thread-scroll-container", "tier": "L2", "scope": "thread", "required": false, "notes": "会话滚动表面;双端活动会话快照均存在" }, diff --git a/windows/CHANGELOG.md b/windows/CHANGELOG.md index 972f3362..2b807d4b 100644 --- a/windows/CHANGELOG.md +++ b/windows/CHANGELOG.md @@ -1,5 +1,43 @@ # Windows Changelog +## 1.6.0 — 2026-07-29 + +- 修复社区一键换肤在主题已经应用并通过可见渲染验证后仍不结束、下一次点击持续报“已有一键换肤正在运行”的问题。Windows 的 `Start-Process -Wait` 会等待整个后代进程树,而启动脚本会有意留下长期运行的 Node 注入器;协议处理器现在只对直接的启动/验证子进程执行有界等待,成功后不再停留在确认提示框,并自动退出。真正尚未完成的下载、写入和验证仍由互斥锁串行保护。 +- 修复社区网站一键换肤下载并通过 ZIP 校验后,采用新版 `colors` 配色且未声明旧版可选 `palette` 的主题在 PowerShell 严格模式下报“找不到属性 palette”的问题。运行时指纹与活动主题写入现在先检查可选属性是否存在,再补齐兼容默认值;主题 ZIP、manifest、图片与 Safe CSS 校验保持不变。 +- 托盘新增“首页样式”,可在“1.5.8 简洁首页”和“1.5.9 标题与建议卡”之间切换。 +- 首页样式作为独立本地偏好保存,不修改主题 ZIP、活动主题或 last-known-good;默认继续使用 v1.5.9 样式。 +- 后台注入器把首页样式纳入实时载荷身份和文件变化检测,切换后无需重启 Codex 或重新启动皮肤流程即可热更新。 +- v1.5.8 模式仅隐藏首页标题和两层原生建议卡,保留项目控件、插件入口、输入框及所有对话页面。 + +## 1.5.9 — 2026-07-29 + +- 修复托盘“菜单背景”切换只保存选项、运行中的页面却继续使用旧背景的问题。后台注入器现在把 `left/full` 独立偏好纳入实时变更检测与载荷身份,切换后无需重启 Codex 或重新启动整套皮肤流程即可更新菜单栏。 +- 在 Codex 26.721.4979 的新首页双层建议结构上恢复 v1.5.6/v1.5.7 的主题标题、主题副标题与玻璃建议卡风格。 +- 同时适配 `[data-home-ambient-suggestions]` 占位层和实际 `.group/home-suggestions` 卡片层,避免只改到不可见容器。 +- 保留 v1.5.8 的首页无遮罩主背景、连续左侧背景、对话页渐变及可靠的首页/对话状态刷新。 + +> v1.5.8 follow-up: Codex 26.721.4979 home compatibility hides the native rotating headline and three new-task suggestion cards that obscure wide wallpaper, while preserving project selection, plugin controls, the real composer, and all conversation routes. + +## 1.5.8 — 2026-07-28 + +### 修复 + +- 修复 v1.5.8 新建任务首页右侧主界面仍被整块深色渐变覆盖的问题:首页只保留左侧栏及菜单栏左段背景,右侧主表面直接显示壁纸;已有对话页继续保留用于可读性的主窗口渐变。 +- 消除菜单栏与内容区之间的深浅分界:新建任务首页仍只延续左侧栏背景、右侧保持无遮罩;进入对话后,菜单栏右段会延续下方主窗口的同一组渐变遮罩。对话菜单使用单张连续渐变处理 419.66px 等小数侧栏宽度,不再出现漏底亮缝或双层透明度形成的黑缝。 +- 修复 DreamSkin.cc 一键换肤被 Chromium 规范化为 `dreamskin://apply/?version=...` 后遭客户端误拒绝的问题。仅额外接受单个根斜杠,任意附加路径、参数、片段、端口、凭据和非固定来源仍然拒绝。 +- 修复 Safe CSS 校验器无法解析已登记的 `backdrop-filter: blur(var(--ds-theme-surface-blur))`。未知变量、fallback、额外滤镜和超过上限的 blur 仍然拒绝;官方 Studio ZIP 可按既有安全合同正常导入。 +- 修正左侧背景范围:实机 Codex 26.721.4979 的侧栏从菜单栏下方开始且可拖动到约 420px,而旧规则给整条菜单栏铺色,未形成用户期望的连续左侧矩形。渲染器现在通过 `ResizeObserver` 发布侧栏实时宽度,不做同步布局读取;菜单栏左段延续与侧栏相同的渐变背景,首页右侧保持透明、对话页右侧匹配主窗口,侧栏拖动、收起和重建时同步更新。 +- 托盘新增“菜单背景”选择,可在“左侧连续背景”和“整条菜单栏背景”之间切换。选择以独立本机偏好保存,不改写主题包;切换后沿用既有安全启动流程重新应用当前主题。 + +## 1.5.7 — 2026-07-28 + +### 修复 + +- 适配 Codex 26.721+ 的新首页兄弟节点结构,限制标题区、建议卡片、项目选择与输入框的最大宽度,避免宽屏下内容过度拉伸、标题与主题主体重叠。 +- 移除首页右下角引语、任务页左上角主题标签和右上角在线状态三个皮肤装饰文字,保留原生控件与托盘中的主题身份信息。 +- 采用上游的左侧栏深色蒙版与仅深色外壳生效的应用菜单处理,不再给浅色原生菜单栏额外增加独立背景。 +- 修复 Release 安装器生成的登录、开始菜单与 `dreamskin://` 持久入口错误使用 `Sysnative`、导致 64 位 Explorer 无法启动的问题。开始菜单的主入口现在真正应用皮肤,托盘入口单独保留;显式启用登录启动时会同时打开托盘与带皮肤的 Codex,并尊重持久暂停状态。 + ## 1.5.6 — 2026-07-26 ### 安全 diff --git a/windows/README.md b/windows/README.md index 1f7e2e2d..7e931dd5 100644 --- a/windows/README.md +++ b/windows/README.md @@ -6,6 +6,8 @@ Codex Dream Skin 通过本机回环 CDP 给官方 Codex Windows 桌面应用加载外部主题。它保留原生侧栏、项目选择、任务内容和输入框,不修改 WindowsApps、`app.asar` 或应用签名。 +当前 fork 版本为 `v1.6.0`,基于上游 `v1.5.6` 继续维护。原有的 Windows PowerShell 5.1 托盘菜单兼容修复已被上游吸收;左侧栏与菜单栏左段采用随侧栏宽度同步的连续深色背景块:首页右侧保持无遮罩,对话页菜单栏右段与主窗口遮罩连续,并适配 Codex 26.721+ 首页结构。v1.6.0 可在托盘中切换 v1.5.8 简洁首页与 v1.5.9 标题、玻璃建议卡首页。 + ## 运行要求 - Windows 10 或更高版本(x64;安装器声明 Windows 10 为最低版本)。 @@ -52,6 +54,9 @@ powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\install- ## 启动与验证 推荐从 `Codex Dream Skin` 快捷方式启动。它发现 Codex 已经运行时会先询问是否重启。 +托盘菜单中的“登录时启动皮肤和 Codex”是显式选择:登录 Windows 后会同时启动托盘与带 CDP 的 Codex 皮肤会话;如果此前选择了“暂停皮肤”,登录时只启动托盘,不会清除暂停状态。 +普通 Codex 快捷方式本身不会携带本项目需要的 CDP 参数。启用上述登录启动后,普通快捷方式通常只会唤起已在运行的皮肤会话;未启用时仍应使用 `Codex Dream Skin` 快捷方式。 +托盘的“菜单背景”可在“左侧连续背景”和“整条菜单栏背景”之间切换;“左侧连续背景”在首页保持右侧透明、进入对话后让右段延续主窗口遮罩。选择会保存在本机,并立即重新应用当前主题。 命令行启动: @@ -59,6 +64,8 @@ powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\install- powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\start-dream-skin.ps1 -PromptRestart ``` +输出 `Codex Dream Skin is active on verified loopback port ...` 表示注入器已经连接。以后建议使用安装器创建的 `Codex Dream Skin` 快捷方式启动,不要直接从普通 Codex 快捷方式启动主题会话。 + 启动后运行验证脚本: ```powershell @@ -78,7 +85,7 @@ powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\verify-d ## 更换和保存主题 -打开 `Codex Dream Skin - Tray` 后可以: +从开始菜单打开 `Codex Dream Skin - Tray`,在系统托盘右键图标后可以: - 更换 PNG、JPEG 或 WebP 背景图。 - 导入普通 `.zip` 主题包到“已保存主题”(不支持 `.dreamskin`)。 @@ -107,6 +114,15 @@ powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File .\scripts\verify-d `%LOCALAPPDATA%\CodexDreamSkin\themes\`。重新打开托盘菜单后即可看到;不要再套一层目录。手动目录 不会经过 ZIP 导入器的归档校验,请只移动可信内容。 +推荐使用 `16:9` 横图,例如 `1672 × 941`、`1920 × 1080` 或 `2560 × 1440`。程序把宽高比不低于 `1.75` 的图片作为整窗宽屏背景;比例低于 `1.75` 的图片会进入首页横幅布局,并由 `background-size: cover` 裁切,因此可能只看到图片中间的一部分。 + +自定义主题的常用流程: + +1. 右键托盘图标,选择「更换背景图」。 +2. 选择纯背景 PNG、JPEG 或 WebP。 +3. 确认显示效果后选择「保存当前主题」。 +4. 输入名称;以后从「已保存主题」直接切换。 + ## 恢复与卸载快捷方式 恢复官方外观;如果 Codex 正在运行,确认后关闭并重新打开: diff --git a/windows/VERSION b/windows/VERSION index eac1e0ad..dc1e644a 100644 --- a/windows/VERSION +++ b/windows/VERSION @@ -1 +1 @@ -1.5.6 +1.6.0 diff --git a/windows/assets/dream-skin.css b/windows/assets/dream-skin.css index bd112ec0..735038a6 100644 --- a/windows/assets/dream-skin.css +++ b/windows/assets/dream-skin.css @@ -49,6 +49,11 @@ --ds-immersive-mid: rgb(var(--ds-bg-rgb) / .26); --ds-immersive-far: rgb(var(--ds-bg-rgb) / .16); --ds-immersive-sidebar: rgb(var(--ds-panel-rgb) / .46); + --ds-sidebar-block-start: var(--ds-immersive-sidebar); + --ds-sidebar-block-end: var(--ds-immersive-edge); + --ds-main-block-start: var(--ds-immersive-edge); + --ds-main-block-mid: var(--ds-immersive-mid); + --ds-main-block-end: var(--ds-immersive-far); --ds-task-immersive-sidebar: rgb(var(--ds-panel-rgb) / .70); --ds-immersive-chrome: rgb(var(--ds-panel-rgb) / .28); --ds-immersive-composer: rgb(var(--ds-panel-rgb) / .44); @@ -99,6 +104,11 @@ html[data-dream-skin="active"][data-dream-shell="light"] { --ds-immersive-mid: rgb(var(--ds-panel-rgb) / .28); --ds-immersive-far: rgb(var(--ds-panel-rgb) / .14); --ds-immersive-sidebar: rgb(var(--ds-panel-rgb) / .48); + --ds-sidebar-block-start: var(--ds-immersive-sidebar); + --ds-sidebar-block-end: var(--ds-immersive-edge); + --ds-main-block-start: var(--ds-immersive-edge); + --ds-main-block-mid: var(--ds-immersive-mid); + --ds-main-block-end: var(--ds-immersive-far); --ds-task-immersive-sidebar: rgb(var(--ds-panel-rgb) / .72); --ds-immersive-chrome: rgb(var(--ds-panel-rgb) / .34); --ds-immersive-composer: rgb(var(--ds-panel-rgb) / .46); @@ -343,6 +353,14 @@ html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream- height: auto; } +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"]):not(:has(main.main-surface [role="main"])) { + --ds-sidebar-block-start: var(--ds-task-immersive-sidebar); + --ds-sidebar-block-end: var(--ds-task-immersive-edge); + --ds-main-block-start: var(--ds-task-immersive-edge); + --ds-main-block-mid: var(--ds-task-immersive-mid); + --ds-main-block-end: var(--ds-task-immersive-far); +} + html[data-dream-skin="active"]: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"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) body { background-image: var(--dream-skin-art) !important; background-position: var(--ds-art-position) !important; @@ -527,7 +545,7 @@ html[data-dream-skin="active"][data-dream-shell="light"] main.main-surface:not(: html[data-dream-skin="active"][data-dream-shell="light"] main.main-surface:has([role="main"])::after, -html[data-dream-skin="active"][data-dream-shell="light"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::before { +html[data-dream-skin="active"][data-dream-shell="light"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"]::before { text-shadow: none; } @@ -538,80 +556,67 @@ html[data-dream-skin="active"] main.main-surface > header.app-header-tint { backdrop-filter: blur(14px) saturate(108%) !important; } -/* Decorative chrome lives on the native shell; no injected positioning host is needed. */ -html[data-dream-skin="active"] - main.main-surface:not(:has([role="main"])) > header.app-header-tint::before { - content: var(--dream-skin-name, "Codex Dream Skin") " · " - var(--dream-skin-brand-subtitle, "CODEX DREAM SKIN"); - position: absolute; - left: 22px; - top: 4px; - z-index: 2; - max-width: min(42%, 360px); - overflow: hidden; - color: var(--ds-accent); - font: 800 11px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - letter-spacing: .03em; - text-overflow: ellipsis; - text-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .32); - white-space: nowrap; - pointer-events: none; +/* Windows keeps this native bar outside main; macOS simply has no match. */ +html[data-dream-skin="active"][data-dream-shell="dark"] + [class~="group/application-menu-top-bar"] { + color: var(--ds-text) !important; + background: rgb(var(--ds-panel-rgb) / .90) !important; + border-bottom: 1px solid var(--ds-line) !important; + box-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .72) !important; + backdrop-filter: blur(16px) saturate(90%) !important; } -html[data-dream-skin="active"] - main.main-surface:not(:has([role="main"])) > header.app-header-tint::after { - content: var(--dream-skin-status, "DREAM SKIN ONLINE"); - position: absolute; - right: 84px; - top: 13px; - z-index: 2; - max-width: 28%; - overflow: hidden; - color: var(--ds-muted); - font: 700 9px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - letter-spacing: .08em; - text-overflow: ellipsis; - text-shadow: 0 1px 10px rgb(var(--ds-bg-rgb) / .32); - white-space: nowrap; - pointer-events: none; +html[data-dream-skin="active"][data-dream-shell="dark"] + [class~="group/application-menu-top-bar"] :is(button, svg) { + color: var(--ds-text) !important; + text-shadow: 0 1px 2px rgb(var(--ds-bg-rgb) / .82) !important; } -html[data-dream-skin="active"] - main.main-surface:has([role="main"])::after { - content: var(--dream-skin-quote, "MAKE SOMETHING WONDERFUL"); - position: absolute; - right: 28px; - bottom: 72px; - z-index: 2; - pointer-events: none; - color: rgb(var(--ds-accent-rgb) / .74); - font: italic 14px/1.2 "Segoe Print", "Comic Sans MS", cursive; - letter-spacing: 0; - text-shadow: 0 0 13px rgb(var(--ds-accent-rgb) / .30); - transform: rotate(-3deg); +/* Continue the sidebar through the Windows application menu while keeping the + unmasked home artwork clear. The renderer publishes the live split without + synchronous layout reads. */ +html[data-dream-skin="active"][data-dream-chrome-mode="left"]:has(aside.app-shell-left-panel) + [class~="group/application-menu-top-bar"] { + background-color: transparent !important; + background-image: linear-gradient(90deg, + var(--ds-sidebar-block-start) 0, + var(--ds-sidebar-block-end) var(--ds-sidebar-extent, 0px), + transparent var(--ds-sidebar-extent, 0px)) !important; + background-position: 0 0 !important; + background-size: auto !important; + background-repeat: no-repeat !important; + border-bottom: 0 !important; + box-shadow: none !important; + backdrop-filter: none !important; } -html[data-dream-skin="active"][data-dream-shell="light"] - main.main-surface:not(:has([role="main"])) > header.app-header-tint::before, -html[data-dream-skin="active"][data-dream-shell="light"] - main.main-surface:not(:has([role="main"])) > header.app-header-tint::after { - text-shadow: none; +/* Conversation routes mask the main surface as well. Use the exact same + non-home condition as that surface: during new-conversation startup the + main mask appears before the thread scroller, so waiting for the scroller + would expose a temporary bright menu strip. A single continuous gradient + shares one stop at the fractional sidebar edge, avoiding both a transparent + raster gap and double-alpha dark overlap. */ +html[data-dream-skin="active"][data-dream-chrome-mode="left"]:has(aside.app-shell-left-panel):not(:has(main.main-surface [role="main"])) + [class~="group/application-menu-top-bar"] { + background-image: linear-gradient(90deg, + var(--ds-sidebar-block-start) 0, + var(--ds-sidebar-block-end) var(--ds-sidebar-extent, 0px), + var(--ds-main-block-mid) calc(64% + var(--ds-sidebar-extent, 0px) * .36), + var(--ds-main-block-end) 100%) !important; } -/* Windows keeps this native bar outside main; macOS simply has no match. */ -html[data-dream-skin="active"][data-dream-shell="dark"] +html[data-dream-skin="active"][data-dream-chrome-mode="full"] [class~="group/application-menu-top-bar"] { color: var(--ds-text) !important; background: rgb(var(--ds-panel-rgb) / .90) !important; border-bottom: 1px solid var(--ds-line) !important; - box-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .72) !important; + box-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .42) !important; backdrop-filter: blur(16px) saturate(90%) !important; } -html[data-dream-skin="active"][data-dream-shell="dark"] +html[data-dream-skin="active"][data-dream-chrome-mode="full"] [class~="group/application-menu-top-bar"] :is(button, svg) { color: var(--ds-text) !important; - text-shadow: 0 1px 2px rgb(var(--ds-bg-rgb) / .82) !important; } html[data-dream-skin="active"] main.main-surface [role="main"] { @@ -619,17 +624,17 @@ html[data-dream-skin="active"] main.main-surface [role="main"] { scrollbar-color: rgb(var(--ds-accent-rgb) / .38) transparent; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) { --thread-content-max-width: min(1180px, calc(100cqw - 44px)) !important; overflow-x: hidden !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child { min-height: 100% !important; padding-top: 15px !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child { flex: 0 0 440px !important; min-height: 440px !important; align-items: flex-start !important; @@ -644,19 +649,19 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > di (see #244). Override with higher specificity only when that new slot is present, so pre-26.721 layouts are untouched. */ html[data-dream-skin="active"] - [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) { + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) { min-height: 0 !important; padding-top: 0 !important; } html[data-dream-skin="active"] - [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) > .home-banners { + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) > .home-banners { flex: 0 1 auto !important; min-height: 0 !important; } html[data-dream-skin="active"] - [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) > .home-banners > div:first-child { + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) > .home-banners > div:first-child { height: auto !important; min-height: 0 !important; border: 0 !important; @@ -666,10 +671,89 @@ html[data-dream-skin="active"] } html[data-dream-skin="active"] - [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) > .home-banners > div:first-child::before { + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) > .home-banners > div:first-child::before { content: none !important; } +/* Codex 26.721+ renders the title, suggestions and composer in the sibling + after the native banner wrapper. Keep that real content visible while + restoring a compact, safe-area-aware composition instead of stretching + every control across the full window. */ +html[data-dream-skin="active"] + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div { + box-sizing: border-box; + width: min(1120px, calc(100% - 48px)) !important; + max-width: 1120px !important; + margin-inline: auto !important; +} + +html[data-dream-skin="active"] + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div + [data-feature="game-source"] { + box-sizing: border-box; + width: min(48%, 520px) !important; + margin-right: auto !important; + padding: 18px 22px !important; + border: 1px solid rgb(var(--ds-muted-rgb) / .16); + border-radius: 18px; + background: rgb(var(--ds-panel-rgb) / .58); + box-shadow: 0 10px 28px rgb(var(--ds-bg-rgb) / .16); + backdrop-filter: blur(10px) saturate(104%); + text-align: left !important; +} + +html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div + [data-feature="game-source"] { + margin-right: 0 !important; + margin-left: auto !important; + text-align: right !important; +} + +html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div + [data-feature="game-source"] { + width: min(62%, 640px) !important; + margin-inline: auto !important; + text-align: center !important; +} + +html[data-dream-skin="active"] + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div + [data-home-ambient-suggestions], +html[data-dream-skin="active"] + [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions, +html[data-dream-skin="active"] + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div + [class*="_homeUtilityBar_"], +html[data-dream-skin="active"] + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div + .composer-surface-chrome { + box-sizing: border-box; + width: min(980px, 100%) !important; + max-width: 980px !important; + margin-inline: auto !important; +} + +/* v1.6.0 keeps the v1.5.9 home as the upgrade default, while allowing the + v1.5.8 clean wallpaper-first home to hide only the title and both native + suggestion layers. The composer and project controls remain interactive. */ +html[data-dream-skin="active"][data-dream-home-mode="clean"] + [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"], +html[data-dream-skin="active"][data-dream-home-mode="clean"] + [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions], +html[data-dream-skin="active"][data-dream-home-mode="clean"] + [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions { + display: none !important; +} + +html[data-dream-skin="active"] + [role="main"]:has([data-feature="game-source"]) > div:first-child:has(> .home-banners) + div + [data-home-ambient-suggestions] button, +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button { + min-height: 104px !important; +} + /* Codex 26.721+: this chain (3-6 first-child levels deep) targets the old compact hero-art card that used to live inside div:first-child's second level. That level is now the empty .home-banners slot (see #244), so on @@ -677,7 +761,7 @@ html[data-dream-skin="active"] is dormant rather than misrendered. Left as-is pending a real fixture of the new nested shape; the critical content-invisible bug is the block above this one. */ -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child { position: relative !important; isolation: isolate; width: calc(100% - 44px) !important; @@ -697,7 +781,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > di box-shadow: 0 16px 38px rgb(var(--ds-bg-rgb) / .30) !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child::before { content: ""; position: absolute; inset: 0 auto 0 0; @@ -709,25 +793,25 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > di } html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child::before { inset: 0 0 0 auto; } html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child::before { inset: 0 14%; width: auto; border-radius: 0; } html[data-dream-skin="active"]:is([data-dream-art-safe="none"], [data-dream-art-safe-area="none"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child::before { inset: 0; width: auto; border-radius: 21px; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:first-child { position: relative; z-index: 1; height: 100%; @@ -737,37 +821,37 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > di } html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:first-child { justify-content: flex-end !important; } html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:first-child { justify-content: center !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { width: min(46%, 520px) !important; align-items: flex-start !important; gap: 0 !important; } html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { align-items: flex-end !important; text-align: right !important; } html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { + [role="main"]:has([data-feature="game-source"]) > 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; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-testid="home-icon"] { display: none !important; } +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-testid="home-icon"] { display: none !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"] { display: block !important; max-width: 100% !important; color: var(--ds-text) !important; @@ -780,7 +864,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [dat pointer-events: auto !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::before { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"]::before { content: var(--dream-skin-name, "Codex Dream Skin"); display: block; margin-bottom: 9px; @@ -790,7 +874,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [dat text-shadow: 0 0 14px rgb(var(--ds-accent-rgb) / .26); } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::after { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"]::after { content: var(--dream-skin-tagline, "把喜欢的画面变成可交互的 Codex 工作台。"); display: block; margin-top: 13px; @@ -801,7 +885,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [dat letter-spacing: 0; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] button { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"] button { margin: 0 5px; padding: 2px 9px 3px; border: 1px solid rgb(var(--ds-accent-rgb) / .36); @@ -812,7 +896,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [dat text-underline-offset: 5px; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] button::before { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"] button::before { content: var(--dream-skin-project-prefix, "选择项目 · "); font-size: .46em; font-weight: 700; @@ -820,16 +904,20 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [dat opacity: .86; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:nth-child(2) { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:nth-child(2) { left: 14px !important; right: 14px !important; top: 100% !important; margin-top: 13px !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions { overflow: visible !important; } +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions], +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions { + overflow: visible !important; +} -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button, +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button { position: relative !important; min-height: 118px !important; padding: 14px 13px 12px !important; @@ -850,17 +938,20 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .gro border-color .18s cubic-bezier(.22, 1, .36, 1) !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button [class~="text-token-text-primary"] { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button [class~="text-token-text-primary"], +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button [class~="text-token-text-primary"] { color: var(--ds-text) !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button:hover { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button:hover, +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button:hover { transform: translateY(-2px) !important; border-color: rgb(var(--ds-accent-rgb) / .42) !important; box-shadow: 0 12px 28px rgb(var(--ds-bg-rgb) / .24) !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:first-child > span:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button > span:first-child > span:first-child, +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button > span:first-child > span:first-child { width: 38px; height: 38px; /* Native spans carry justify-start; grid + place-items cannot override @@ -876,7 +967,8 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .gro box-shadow: 0 0 0 6px rgb(var(--ds-accent-rgb) / .05); } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button svg { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button svg, +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button svg { width: 21px !important; height: 21px !important; display: block !important; @@ -885,8 +977,10 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .gro color: var(--ds-accent) !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:first-child { justify-content: center !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:last-child { align-items: center !important; text-align: center !important; } +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button > span:first-child, +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button > span:first-child { justify-content: center !important; } +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button > span:last-child, +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button > span:last-child { align-items: center !important; text-align: center !important; } html[data-dream-skin="active"] .composer-surface-chrome { overflow: visible !important; @@ -926,10 +1020,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"]:has(main.main-surface } html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) { - background: linear-gradient(90deg, - var(--ds-immersive-edge), - var(--ds-immersive-mid) 64%, - var(--ds-immersive-far)) !important; + background: transparent !important; border: 0 !important; border-radius: 0 !important; box-shadow: none !important; @@ -958,7 +1049,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface } html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child { border: 0 !important; border-radius: 0 !important; background: transparent !important; @@ -966,12 +1057,14 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has } html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) - [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { + [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child::before { content: none !important; } html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) - [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button { + [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button, +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) + [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button { background: rgb(var(--ds-panel-rgb) / .56) !important; box-shadow: 0 8px 22px rgb(var(--ds-bg-rgb) / .16), @@ -1021,7 +1114,7 @@ html[data-dream-skin="active"] [class*="_homeUtilityBar_"] { backdrop-filter: blur(16px) saturate(108%) !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]):has([class*="_homeUtilityBar_"]) +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]):has([class*="_homeUtilityBar_"]) .composer-surface-chrome { border: 0 !important; border-radius: 0 0 22px 22px !important; @@ -1042,7 +1135,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] [class*="_homeUtility } html[data-dream-skin="active"][data-dream-art-wide="true"] - [role="main"]:has([data-testid="home-icon"]):has([class*="_homeUtilityBar_"]) .composer-surface-chrome { + [role="main"]:has([data-feature="game-source"]):has([class*="_homeUtilityBar_"]) .composer-surface-chrome { box-shadow: 0 10px 30px rgb(var(--ds-bg-rgb) / .20), inset 1px 0 var(--ds-immersive-line), @@ -1134,7 +1227,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface background: transparent !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) div:has(> .horizontal-scroll-fade-mask .group\/project-selector) { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) div:has(> .horizontal-scroll-fade-mask .group\/project-selector) { position: relative; padding-top: 28px !important; border: 1px solid rgb(var(--ds-muted-rgb) / .16); @@ -1142,7 +1235,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) div: background: rgb(var(--ds-panel-rgb) / .92) !important; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) div:has(> .horizontal-scroll-fade-mask .group\/project-selector)::before { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) div:has(> .horizontal-scroll-fade-mask .group\/project-selector)::before { content: var(--dream-skin-project-label, "选择项目"); position: absolute; left: 13px; @@ -1155,7 +1248,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) div: white-space: nowrap; } -html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/project-selector > button { +html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/project-selector > button { border-color: rgb(var(--ds-accent-rgb) / .22) !important; background: rgb(var(--ds-accent-rgb) / .08) !important; color: var(--ds-text) !important; @@ -1179,27 +1272,28 @@ html[data-dream-skin="active"] [data-message-author-role] { border-radius: 16px; @media (max-width: 1120px) { html[data-dream-skin="active"] main.main-surface:has([role="main"])::after { content: none; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) { --thread-content-max-width: min(940px, calc(100cqw - 30px)) !important; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { width: calc(100% - 28px) !important; } + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) { --thread-content-max-width: min(940px, calc(100cqw - 30px)) !important; } + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child { width: calc(100% - 28px) !important; } } @media (max-width: 900px) { - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child { + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child { flex-basis: 408px !important; min-height: 408px !important; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child { height: 232px !important; min-height: 232px !important; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { padding: 0 26px; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:first-child { padding: 0 26px; } + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { width: min(58%, 460px) !important; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] { font-size: 18px !important; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::before { margin-bottom: 7px; font-size: 10px; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::after { margin-top: 9px; font-size: 11px; } - html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button { min-height: 112px !important; font-size: 12px !important; } + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"] { font-size: 18px !important; } + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"]::before { margin-bottom: 7px; font-size: 10px; } + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-feature="game-source"]::after { margin-top: 9px; font-size: 11px; } + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) [data-home-ambient-suggestions] button, + html[data-dream-skin="active"] [role="main"]:has([data-feature="game-source"]) .group\/home-suggestions button { min-height: 112px !important; font-size: 12px !important; } } @media (prefers-reduced-motion: reduce) { diff --git a/windows/assets/renderer-inject.js b/windows/assets/renderer-inject.js index be799888..eed374ff 100644 --- a/windows/assets/renderer-inject.js +++ b/windows/assets/renderer-inject.js @@ -1,6 +1,6 @@ // Canonical cross-platform renderer. Run tools/sync-runtime-assets.mjs after editing. ((cssText, artDataUrl, themeConfig) => { - const SELECTOR_CONTRACT = {"schema":"codex-dream-skin-selectors/1","selectors":[{"key":"shell-main","selector":"main.main-surface","tier":"L1","scope":"all","required":true},{"key":"left-panel","selector":"aside.app-shell-left-panel","tier":"L1","scope":"all","required":true},{"key":"header-tint","selector":"header.app-header-tint","tier":"L1","scope":"all","required":true},{"key":"home-icon","selector":"[data-testid=\"home-icon\"]","tier":"L1","scope":"home","required":true},{"key":"home-route","selector":"[role=\"main\"]:has([data-testid=\"home-icon\"])","tier":"L1","scope":"home","required":true},{"key":"home-route-css","selector":"[role=\"main\"]","tier":"L1","scope":"home","required":true},{"key":"home-banners","selector":".home-banners","tier":"L2","scope":"home","required":false},{"key":"composer-chrome","selector":".composer-surface-chrome","tier":"L2","scope":"home+thread","required":false},{"key":"composer-toolbar","selector":".composer-surface-chrome [class*=\"_footer_\"]","tier":"L2","scope":"home+thread","required":false},{"key":"home-utility","selector":"[class*=\"_homeUtilityBar_\"]","tier":"L2","scope":"home","required":false},{"key":"game-source","selector":"[data-feature=\"game-source\"]","tier":"L2","scope":"home","required":false},{"key":"home-suggestions","selector":".group\\/home-suggestions","tier":"L2","scope":"home","required":false},{"key":"project-selector","selector":".group\\/project-selector","tier":"L2","scope":"home config","required":false},{"key":"markdown","selector":"[class*=\"_markdown\"]","tier":"L2","scope":"thread","required":false},{"key":"thread-surface","selector":".thread-scroll-container","tier":"L2","scope":"thread","required":false},{"key":"message","selector":"[data-message-author-role]","tier":"L2","scope":"thread","required":false},{"key":"appearance-radio","selector":"input[name=\"appearance-theme\"]","tier":"L2","scope":"settings","required":false},{"key":"overlay-menu","selector":"[role=\"menu\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-dialog","selector":"[role=\"dialog\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-popper","selector":"[data-radix-popper-content-wrapper]","tier":"L2","scope":"overlay","required":false}],"stableTestids":["app-shell-header-context-menu-surface","home-icon","theme-preview"]}; + const SELECTOR_CONTRACT = {"schema":"codex-dream-skin-selectors/1","selectors":[{"key":"shell-main","selector":"main.main-surface","tier":"L1","scope":"all","required":true},{"key":"left-panel","selector":"aside.app-shell-left-panel","tier":"L1","scope":"all","required":true},{"key":"header-tint","selector":"header.app-header-tint","tier":"L1","scope":"all","required":true},{"key":"home-icon","selector":"[data-testid=\"home-icon\"]","tier":"L2","scope":"home","required":false},{"key":"home-route","selector":"[role=\"main\"]:has([data-feature=\"game-source\"])","tier":"L1","scope":"home","required":true},{"key":"home-route-css","selector":"[role=\"main\"]","tier":"L1","scope":"home","required":true},{"key":"home-banners","selector":".home-banners","tier":"L2","scope":"home","required":false},{"key":"composer-chrome","selector":".composer-surface-chrome","tier":"L2","scope":"home+thread","required":false},{"key":"composer-toolbar","selector":".composer-surface-chrome [class*=\"_footer_\"]","tier":"L2","scope":"home+thread","required":false},{"key":"home-utility","selector":"[class*=\"_homeUtilityBar_\"]","tier":"L2","scope":"home","required":false},{"key":"game-source","selector":"[data-feature=\"game-source\"]","tier":"L2","scope":"home","required":false},{"key":"home-suggestions","selector":"[data-home-ambient-suggestions]","tier":"L2","scope":"home","required":false},{"key":"home-suggestion-cards","selector":".group\\/home-suggestions","tier":"L2","scope":"home","required":false},{"key":"project-selector","selector":".group\\/project-selector","tier":"L2","scope":"home config","required":false},{"key":"markdown","selector":"[class*=\"_markdown\"]","tier":"L2","scope":"thread","required":false},{"key":"thread-surface","selector":".thread-scroll-container","tier":"L2","scope":"thread","required":false},{"key":"message","selector":"[data-message-author-role]","tier":"L2","scope":"thread","required":false},{"key":"appearance-radio","selector":"input[name=\"appearance-theme\"]","tier":"L2","scope":"settings","required":false},{"key":"overlay-menu","selector":"[role=\"menu\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-dialog","selector":"[role=\"dialog\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-popper","selector":"[data-radix-popper-content-wrapper]","tier":"L2","scope":"overlay","required":false}],"stableTestids":["app-shell-header-context-menu-surface","home-icon","theme-preview"]}; const STATE_KEY = "__CODEX_DREAM_SKIN_STATE__"; const DISABLED_KEY = "__CODEX_DREAM_SKIN_DISABLED__"; const STYLE_REGISTRY_KEY = "__CODEX_DREAM_SKIN_STYLE_SHEETS__"; @@ -12,6 +12,7 @@ "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", "data-dream-art-ready", + "data-dream-chrome-mode", "data-dream-home-mode", ]; const VERSION = __DREAM_SKIN_VERSION_JSON__; const STYLE_REVISION = __DREAM_SKIN_STYLE_REVISION_JSON__; @@ -43,7 +44,7 @@ "--ds-theme-surface-shadow", "--ds-theme-image-focus-x", "--ds-theme-image-focus-y", "--ds-theme-image-zoom", "--ds-theme-image-dim", "--ds-theme-image-task-intensity", - "--ds-theme-density-scale", "--ds-theme-motion-level", + "--ds-theme-density-scale", "--ds-theme-motion-level", "--ds-sidebar-extent", ]; const selectorByKey = new Map(SELECTOR_CONTRACT.selectors.map((entry) => [entry.key, entry])); const stableTestidSelector = (testid) => SELECTOR_CONTRACT.stableTestids?.includes(testid) @@ -57,6 +58,8 @@ let analysisTimer = null; let rootObserver = null; let partObserver = null; + let sidebarResizeObserver = null; + let observedSidebar = null; let bodyReadyHandler = null; let styleMode = null; let styleNode = null; @@ -550,6 +553,8 @@ const shell = resolvedShell(); setAttribute(root, "data-dream-skin", "active"); setAttribute(root, SHELL_ATTR, shell); + setAttribute(root, "data-dream-chrome-mode", THEME.chromeMode === "full" ? "full" : "left"); + setAttribute(root, "data-dream-home-mode", THEME.homeMode === "clean" ? "clean" : "classic"); setStyleProperty(root, "--dream-skin-art", `url("${artUrl}")`); applyTheme(root, shell); applyArtMetadata(root); @@ -581,11 +586,35 @@ } } }; + const setSidebarExtent = (width) => { + const numeric = Number(width); + const bounded = Number.isFinite(numeric) && numeric > 0 ? clamp(numeric, 0, 2048) : 0; + setStyleProperty(document.documentElement, "--ds-sidebar-extent", `${Number(bounded.toFixed(2))}px`); + }; + const inlineSidebarWidth = (sidebar) => { + const inline = sidebar?.style?.width || sidebar?.getAttribute?.("style") || ""; + const match = String(inline).match(/(?:^|;)\s*width\s*:\s*([\d.]+)px(?:\s*;|$)/i); + return match ? Number(match[1]) : null; + }; + const observeSidebar = (sidebar) => { + if (observedSidebar === sidebar) return; + if (observedSidebar) sidebarResizeObserver?.unobserve?.(observedSidebar); + observedSidebar = sidebar || null; + if (!observedSidebar) { + setSidebarExtent(0); + return; + } + const inlineWidth = inlineSidebarWidth(observedSidebar); + if (inlineWidth !== null) setSidebarExtent(inlineWidth); + sidebarResizeObserver?.observe?.(observedSidebar); + }; const refreshParts = () => { metrics.partPasses += 1; const desired = new Map(); addPart(desired, "root", [document.documentElement]); - addPart(desired, "sidebar", selectorNodes("left-panel")); + const sidebars = selectorNodes("left-panel"); + addPart(desired, "sidebar", sidebars); + observeSidebar(sidebars[0] || null); addPart(desired, "main", selectorNodes("shell-main")); addPart(desired, "header", selectorNodes("header-tint")); addPart(desired, "home", selectorNodes("home-route")); @@ -687,6 +716,7 @@ removeParts(); state?.rootObserver?.disconnect(); state?.partObserver?.disconnect(); + state?.sidebarResizeObserver?.disconnect(); if (bodyReadyHandler && typeof document.removeEventListener === "function") { document.removeEventListener("DOMContentLoaded", bodyReadyHandler); } @@ -733,7 +763,21 @@ }; if (typeof MutationObserver === "function") { rootObserver = new MutationObserver(() => scheduleEnsure({ root: true })); - partObserver = new MutationObserver(() => scheduleEnsure({ parts: true }, 80)); + // Some Codex sidebar transitions replace the route DOM without firing the + // Navigation API. Refresh scope with the same debounced pass that already + // remaps dynamic public parts so verification cannot retain a stale route. + partObserver = new MutationObserver(() => scheduleEnsure({ scope: true, parts: true }, 80)); + } + if (typeof ResizeObserver === "function") { + sidebarResizeObserver = new ResizeObserver((entries) => { + for (const entry of entries || []) { + if (entry?.target !== observedSidebar) continue; + const borderBox = Array.isArray(entry.borderBoxSize) + ? entry.borderBoxSize[0] : entry.borderBoxSize; + const width = borderBox?.inlineSize ?? entry.contentRect?.width; + if (Number.isFinite(Number(width))) setSidebarExtent(width); + } + }); } let mediaQuery = null; @@ -755,6 +799,7 @@ cleanup, rootObserver, partObserver, + sidebarResizeObserver, timer: null, scheduler, mediaQuery, @@ -803,7 +848,7 @@ bodyReadyHandler = () => { if (!window[DISABLED_KEY]) { observeBody(); - scheduleEnsure({ parts: true }, 0); + scheduleEnsure({ scope: true, parts: true }, 0); } }; document.addEventListener("DOMContentLoaded", bodyReadyHandler, { once: true }); diff --git a/windows/assets/safe-css-validator.mjs b/windows/assets/safe-css-validator.mjs index 8321e8ea..1d925d51 100644 --- a/windows/assets/safe-css-validator.mjs +++ b/windows/assets/safe-css-validator.mjs @@ -278,11 +278,9 @@ function validatePropertyValue(property, value) { } if (property === "backdrop-filter") { if (value.toLowerCase() === "none") return true; - const match = value.match(/^blur\(\s*([^\s)]+)\s*\)$/i); - return Boolean(match && ( - registeredVar(match[1], new Set(["--ds-theme-surface-blur"])) - || zeroOrPx(match[1], 0, 20) - )); + if (/^blur\(\s*var\(\s*--ds-theme-surface-blur\s*\)\s*\)$/i.test(value)) return true; + const match = value.match(/^blur\(\s*([^\s()]+)\s*\)$/i); + return Boolean(match && zeroOrPx(match[1], 0, 20)); } if (property === "font-family") return fontFamilyValue(value); if (property === "font-size") return numeric(value, 12, 20, "px"); diff --git a/windows/assets/selectors.json b/windows/assets/selectors.json index 06e22231..2fc46e0a 100644 --- a/windows/assets/selectors.json +++ b/windows/assets/selectors.json @@ -29,15 +29,16 @@ { "key": "shell-main", "selector": "main.main-surface", "tier": "L1", "scope": "all", "required": true, "notes": "全局主表面。mac 设置页缺失(win 存在),缺失时整页仅 L0" }, { "key": "left-panel", "selector": "aside.app-shell-left-panel", "tier": "L1", "scope": "all", "required": true, "notes": "左侧栏;收起/重建时会移除节点,勿在其消失时清皮" }, { "key": "header-tint", "selector": "header.app-header-tint", "tier": "L1", "scope": "all", "required": true, "notes": "顶栏;皮肤不得覆盖其 position/z-index(既有回归约束)" }, - { "key": "home-icon", "selector": "[data-testid=\"home-icon\"]", "tier": "L1", "scope": "home", "required": true, "notes": "首页判定信号,双端一致" }, - { "key": "home-route", "selector": "[role=\"main\"]:has([data-testid=\"home-icon\"])", "tier": "L1", "scope": "home", "required": true, "notes": "首页容器。实测 [role=main] 仅存在于首页——它不是通用路由锚点,thread 页没有" }, + { "key": "home-icon", "selector": "[data-testid=\"home-icon\"]", "tier": "L2", "scope": "home", "required": false, "notes": "旧版首页标记;Codex 26.721.4979 已不再渲染,仅保留为兼容探针" }, + { "key": "home-route", "selector": "[role=\"main\"]:has([data-feature=\"game-source\"])", "tier": "L1", "scope": "home", "required": true, "notes": "Codex 26.721.4979 首页容器,以稳定的原生 hero 标记判定;thread 页没有 role=main" }, { "key": "home-route-css", "selector": "[role=\"main\"]", "tier": "L1", "scope": "home", "required": true, "notes": "home-route 的 CSS 可组合别名:不含 :has()。CSS 禁止 :has() 嵌套,home-route 原选择器一旦写进 :has()/:not(:has()) 整条规则会被解析器丢弃(v1.3.1 全窗口首页与任务页氛围背景因此失效)。依据与 home-route 相同的实测:[role=main] 仅存在于首页;该锚点缺失时 CSS 路由门控整体失效,故必须按 L1 必需项监控" }, { "key": "home-banners", "selector": ".home-banners", "tier": "L2", "scope": "home", "required": false, "notes": "Codex 26.721+ 新增的原生首页 banner 插槽,位于 home-route 第一个子节点内、通常为空(0 子节点)靠原生 empty:hidden 折叠。真正的首页内容(标题/建议卡/输入框)在这个版本上已变成该插槽外层容器的兄弟节点,不再是它的后代(见 #244 实测:home-route > div:first-child 被撑到 100% 高度会把内容挤出视口)。用于判定新旧 home DOM 形态,皮肤据此把 first-child 链路的强制尺寸限定在旧形态或改走兄弟节点" }, { "key": "composer-chrome", "selector": ".composer-surface-chrome", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框壳。win 实测存在无 composer 的 detail 路由,不可当全局锚点" }, { "key": "composer-toolbar", "selector": ".composer-surface-chrome [class*=\"_footer_\"]", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框工具栏;CSS Modules 名称跨端稳定,hash 随版本变化" }, { "key": "home-utility", "selector": "[class*=\"_homeUtilityBar_\"]", "tier": "L2", "scope": "home", "required": false, "notes": "CSS Modules 前缀匹配;模块名跨端稳定、hash 随版本变,禁止匹配完整 hash" }, { "key": "game-source", "selector": "[data-feature=\"game-source\"]", "tier": "L2", "scope": "home", "required": false }, - { "key": "home-suggestions", "selector": ".group\\/home-suggestions", "tier": "L2", "scope": "home", "required": false }, + { "key": "home-suggestions", "selector": "[data-home-ambient-suggestions]", "tier": "L2", "scope": "home", "required": false, "notes": "Codex 26.721.4979 原生首页建议区的显式稳定标记" }, + { "key": "home-suggestion-cards", "selector": ".group\\/home-suggestions", "tier": "L2", "scope": "home", "required": false, "notes": "Codex 26.721.4979 renders the visible starter cards in this separate layer; keep it distinct because callers append button selectors." }, { "key": "project-selector", "selector": ".group\\/project-selector", "tier": "L2", "scope": "home config", "required": false, "notes": "依赖用户项目配置;win 快照未出现(该机可能未配置项目),不算平台差异" }, { "key": "markdown", "selector": "[class*=\"_markdown\"]", "tier": "L2", "scope": "thread", "required": false, "notes": "会话内容块,单页可达 100+ 个,样式规则注意性能" }, { "key": "thread-surface", "selector": ".thread-scroll-container", "tier": "L2", "scope": "thread", "required": false, "notes": "会话滚动表面;双端活动会话快照均存在" }, diff --git a/windows/installer/build-release.ps1 b/windows/installer/build-release.ps1 index 620e154f..988af4e5 100644 --- a/windows/installer/build-release.ps1 +++ b/windows/installer/build-release.ps1 @@ -407,6 +407,7 @@ try { 'scripts\image-metadata.mjs', 'scripts\injector.mjs', 'scripts\install-dream-skin.ps1', + 'scripts\login-dream-skin.ps1', 'scripts\restore-dream-skin.ps1', 'scripts\start-dream-skin.ps1', 'scripts\theme-windows.ps1', diff --git a/windows/installer/codex-dream-skin.iss b/windows/installer/codex-dream-skin.iss index 798e3a3f..d828caca 100644 --- a/windows/installer/codex-dream-skin.iss +++ b/windows/installer/codex-dream-skin.iss @@ -11,7 +11,8 @@ #define AppName "Codex Dream Skin" #define AppPublisher "Codex Dream Skin contributors" #define AppUrl "https://dreamskin.cc" -#define PowerShellPath "{sysnative}\WindowsPowerShell\v1.0\powershell.exe" +#define NativePowerShellPath "{sysnative}\WindowsPowerShell\v1.0\powershell.exe" +#define PersistedPowerShellPath "{win}\System32\WindowsPowerShell\v1.0\powershell.exe" [Setup] AppId={{DCCDAF1A-9ACD-4AAB-B55B-DF17EB2CDA2E} @@ -58,7 +59,7 @@ english.ConfirmUninstall=Uninstall will close Codex, restore its original appear chinesesimplified.ConfirmUninstall=卸载将关闭 Codex、恢复官方外观并移除 Dream Skin 运行时;已保存主题和图片会保留。%n%n是否继续? [Tasks] -Name: "startup"; Description: "Start Codex Dream Skin when I sign in"; GroupDescription: "Additional options:"; Flags: unchecked +Name: "startup"; Description: "Start Codex with Dream Skin when I sign in"; GroupDescription: "Additional options:"; Flags: unchecked [Files] ; Keep a second, temporary copy so initialization runs before Inno starts @@ -72,17 +73,18 @@ Source: "{#StageRoot}\NOTICE.md"; DestDir: "{app}"; Flags: ignoreversion Source: "{#StageRoot}\payload\*"; DestDir: "{app}\payload"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] -Name: "{group}\Codex Dream Skin"; Filename: "{#PowerShellPath}"; Parameters: "-NoProfile -STA -WindowStyle Hidden -ExecutionPolicy RemoteSigned -File ""{app}\setup-bootstrap.ps1"" -LaunchTray"; WorkingDir: "{app}"; IconFilename: "{app}\payload\assets\codex-dream-skin.ico" -Name: "{userstartup}\Codex Dream Skin"; Filename: "{#PowerShellPath}"; Parameters: "-NoProfile -STA -WindowStyle Hidden -ExecutionPolicy RemoteSigned -File ""{app}\setup-bootstrap.ps1"" -LaunchTray"; WorkingDir: "{app}"; IconFilename: "{app}\payload\assets\codex-dream-skin.ico"; Tasks: startup +Name: "{group}\Codex Dream Skin"; Filename: "{#PersistedPowerShellPath}"; Parameters: "-NoProfile -ExecutionPolicy RemoteSigned -File ""{localappdata}\CodexDreamSkin\engine\scripts\start-dream-skin.ps1"" -PromptRestart"; WorkingDir: "{localappdata}\CodexDreamSkin\engine"; IconFilename: "{app}\payload\assets\codex-dream-skin.ico" +Name: "{group}\Codex Dream Skin - Tray"; Filename: "{#PersistedPowerShellPath}"; Parameters: "-NoProfile -STA -WindowStyle Hidden -ExecutionPolicy RemoteSigned -File ""{localappdata}\CodexDreamSkin\engine\scripts\tray-dream-skin.ps1"""; WorkingDir: "{localappdata}\CodexDreamSkin\engine"; IconFilename: "{app}\payload\assets\codex-dream-skin.ico" +Name: "{userstartup}\Codex Dream Skin"; Filename: "{#PersistedPowerShellPath}"; Parameters: "-NoProfile -STA -WindowStyle Hidden -ExecutionPolicy RemoteSigned -File ""{localappdata}\CodexDreamSkin\engine\scripts\login-dream-skin.ps1"""; WorkingDir: "{localappdata}\CodexDreamSkin\engine"; IconFilename: "{app}\payload\assets\codex-dream-skin.ico"; Tasks: startup [Registry] Root: HKCU; Subkey: "Software\Classes\dreamskin"; ValueType: string; ValueName: ""; ValueData: "URL:DreamSkin Protocol"; Flags: uninsdeletekey Root: HKCU; Subkey: "Software\Classes\dreamskin"; ValueType: string; ValueName: "URL Protocol"; ValueData: "" Root: HKCU; Subkey: "Software\Classes\dreamskin\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\payload\assets\codex-dream-skin.ico" -Root: HKCU; Subkey: "Software\Classes\dreamskin\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{#PowerShellPath}"" -NoProfile -STA -WindowStyle Hidden -ExecutionPolicy RemoteSigned -File ""{localappdata}\CodexDreamSkin\engine\scripts\apply-community-theme.ps1"" ""%1""" +Root: HKCU; Subkey: "Software\Classes\dreamskin\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{#PersistedPowerShellPath}"" -NoProfile -STA -WindowStyle Hidden -ExecutionPolicy RemoteSigned -File ""{localappdata}\CodexDreamSkin\engine\scripts\apply-community-theme.ps1"" ""%1""" [Run] -Filename: "{#PowerShellPath}"; Parameters: "-NoProfile -STA -WindowStyle Hidden -ExecutionPolicy RemoteSigned -File ""{app}\setup-bootstrap.ps1"" -LaunchTray"; WorkingDir: "{app}"; Description: "Launch Codex Dream Skin"; Flags: nowait postinstall skipifsilent +Filename: "{#NativePowerShellPath}"; Parameters: "-NoProfile -STA -WindowStyle Hidden -ExecutionPolicy RemoteSigned -File ""{app}\setup-bootstrap.ps1"" -LaunchTray"; WorkingDir: "{app}"; Description: "Launch Codex Dream Skin"; Flags: nowait postinstall skipifsilent [Code] function PowerShellArguments( @@ -105,7 +107,7 @@ function RunBootstrap( ): Boolean; begin Result := Exec( - ExpandConstant('{#PowerShellPath}'), + ExpandConstant('{#NativePowerShellPath}'), PowerShellArguments(ScriptPath, ActionArguments, Silent), ExtractFileDir(ScriptPath), SW_HIDE, diff --git a/windows/installer/setup-bootstrap.ps1 b/windows/installer/setup-bootstrap.ps1 index 4fc4ed63..419f1c19 100644 --- a/windows/installer/setup-bootstrap.ps1 +++ b/windows/installer/setup-bootstrap.ps1 @@ -128,6 +128,7 @@ try { 'scripts\image-metadata.mjs', 'scripts\injector.mjs', 'scripts\install-dream-skin.ps1', + 'scripts\login-dream-skin.ps1', 'scripts\restore-dream-skin.ps1', 'scripts\start-dream-skin.ps1', 'scripts\theme-windows.ps1', diff --git a/windows/scripts/apply-community-theme.ps1 b/windows/scripts/apply-community-theme.ps1 index 426f2702..71da269c 100644 --- a/windows/scripts/apply-community-theme.ps1 +++ b/windows/scripts/apply-community-theme.ps1 @@ -292,6 +292,23 @@ function Set-DreamSkinActiveThemeFromSnapshot { return $activeFingerprint } +function Wait-DreamSkinCommunityChildProcess { + param( + [Parameter(Mandatory = $true)][System.Diagnostics.Process]$Process, + [ValidateRange(1000, 600000)][int]$TimeoutMilliseconds + ) + try { + if (-not $Process.WaitForExit($TimeoutMilliseconds)) { + try { $Process.Kill() } catch {} + [void]$Process.WaitForExit(15000) + throw "Dream Skin start and visible verification did not finish within $TimeoutMilliseconds ms." + } + return $Process.ExitCode + } finally { + $Process.Dispose() + } +} + function Invoke-DreamSkinCommunityStartAndVerify { param( [ValidateRange(1000, 300000)] @@ -308,9 +325,18 @@ function Invoke-DreamSkinCommunityStartAndVerify { ' -RequireUnpaused -OperationLockTimeoutMilliseconds ' + "$OperationLockTimeoutMilliseconds" $startProcess = Start-Process -FilePath $powershell -ArgumentList $argumentLine ` - -WindowStyle Hidden -Wait -PassThru - if ($startProcess.ExitCode -ne 0) { - throw "Dream Skin could not start and visibly verify the active theme (exit code $($startProcess.ExitCode))." + -WindowStyle Hidden -PassThru + # Start-Process -Wait follows the whole descendant tree on Windows. The start + # script intentionally leaves the injector watcher running, so wait only for + # the direct start/verify child and bound that wait independently. + $startTimeoutMilliseconds = [Math]::Min( + 450000, + ([int64]$OperationLockTimeoutMilliseconds + 180000) + ) + $exitCode = Wait-DreamSkinCommunityChildProcess -Process $startProcess ` + -TimeoutMilliseconds ([int]$startTimeoutMilliseconds) + if ($exitCode -ne 0) { + throw "Dream Skin could not start and visibly verify the active theme (exit code $exitCode)." } } @@ -761,10 +787,6 @@ $previousProtocol = [System.Net.ServicePointManager]::SecurityProtocol try { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 $result = Invoke-DreamSkinCommunityApply -ApplyUri $Uri - if (-not $result.Canceled) { - Show-DreamSkinCommunityMessage ` - -Message "“$($result.Name)”已通过下载、SHA-256、主题包与 Safe CSS 校验,并已应用到 Codex。" - } exit 0 } catch { $recovery = "$($_.Exception.Data['DreamSkinRecovery'])" diff --git a/windows/scripts/common-windows.ps1 b/windows/scripts/common-windows.ps1 index 54c5497c..983706a0 100644 --- a/windows/scripts/common-windows.ps1 +++ b/windows/scripts/common-windows.ps1 @@ -68,10 +68,61 @@ function Get-DreamSkinRuntimeEnginePaths { Start = Join-Path $scripts 'start-dream-skin.ps1' Restore = Join-Path $scripts 'restore-dream-skin.ps1' Tray = Join-Path $scripts 'tray-dream-skin.ps1' + Login = Join-Path $scripts 'login-dream-skin.ps1' CheckUpdate = Join-Path $scripts 'check-update.ps1' } } +function Get-DreamSkinChromeMode { + param([string]$StateRoot = (Join-Path $env:LOCALAPPDATA 'CodexDreamSkin')) + $path = Join-Path ([System.IO.Path]::GetFullPath($StateRoot)) 'chrome-mode' + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return 'left' } + try { + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -lt 1 -or $bytes.Length -gt 16) { return 'left' } + $value = (ConvertFrom-DreamSkinUtf8Bytes -Bytes $bytes -Path $path).Trim().ToLowerInvariant() + if ($value -eq 'full') { return 'full' } + } catch {} + return 'left' +} + +function Set-DreamSkinChromeMode { + param( + [Parameter(Mandatory = $true)][ValidateSet('left', 'full')][string]$Mode, + [string]$StateRoot = (Join-Path $env:LOCALAPPDATA 'CodexDreamSkin') + ) + $fullStateRoot = [System.IO.Path]::GetFullPath($StateRoot) + [System.IO.Directory]::CreateDirectory($fullStateRoot) | Out-Null + Write-DreamSkinUtf8FileAtomically -Path (Join-Path $fullStateRoot 'chrome-mode') ` + -Content "$Mode`r`n" + return $Mode +} + +function Get-DreamSkinHomeMode { + param([string]$StateRoot = (Join-Path $env:LOCALAPPDATA 'CodexDreamSkin')) + $path = Join-Path ([System.IO.Path]::GetFullPath($StateRoot)) 'home-mode' + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return 'classic' } + try { + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -lt 1 -or $bytes.Length -gt 16) { return 'classic' } + $value = (ConvertFrom-DreamSkinUtf8Bytes -Bytes $bytes -Path $path).Trim().ToLowerInvariant() + if ($value -eq 'clean') { return 'clean' } + } catch {} + return 'classic' +} + +function Set-DreamSkinHomeMode { + param( + [Parameter(Mandatory = $true)][ValidateSet('clean', 'classic')][string]$Mode, + [string]$StateRoot = (Join-Path $env:LOCALAPPDATA 'CodexDreamSkin') + ) + $fullStateRoot = [System.IO.Path]::GetFullPath($StateRoot) + [System.IO.Directory]::CreateDirectory($fullStateRoot) | Out-Null + Write-DreamSkinUtf8FileAtomically -Path (Join-Path $fullStateRoot 'home-mode') ` + -Content "$Mode`r`n" + return $Mode +} + function Test-DreamSkinTrayActive { $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value $mutex = [System.Threading.Mutex]::new($false, "Local\CodexDreamSkin.$sid.Tray") @@ -200,6 +251,7 @@ function Install-DreamSkinRuntimeEngine { 'scripts\image-metadata.mjs', 'scripts\injector.mjs', 'scripts\install-dream-skin.ps1', + 'scripts\login-dream-skin.ps1', 'scripts\restore-dream-skin.ps1', 'scripts\start-dream-skin.ps1', 'scripts\theme-windows.ps1', diff --git a/windows/scripts/injector.mjs b/windows/scripts/injector.mjs index 636bf5bf..3e05bd29 100644 --- a/windows/scripts/injector.mjs +++ b/windows/scripts/injector.mjs @@ -39,7 +39,7 @@ const stableTestidLiteral = (testid) => { } return JSON.stringify(`[data-testid="${testid}"]`); }; -const SKIN_VERSION = "1.5.6"; +const SKIN_VERSION = "1.6.0"; const MAX_ART_BYTES = 10 * 1024 * 1024; const MAX_SAFE_CSS_BYTES = 256 * 1024; const STRONG_THEME_AUDIT_MS = 30000; @@ -625,8 +625,56 @@ export async function loadTheme(themeDir) { }; } -export async function loadPayload(themeDir = path.join(root, "assets"), candidateTheme = null) { +async function loadPreferenceState(themeDir, fileName, defaultMode, acceptedMode) { + const resolvedThemeDir = path.resolve(themeDir); + if (path.basename(resolvedThemeDir).toLowerCase() !== "active-theme") { + return { mode: defaultMode, sourceStamp: `fixed:${defaultMode}` }; + } + const modePath = path.join(path.dirname(resolvedThemeDir), fileName); + try { + const stat = await fs.stat(modePath); + if (!stat.isFile() || stat.size < 1 || stat.size > 16) { + return { mode: defaultMode, sourceStamp: `invalid:${defaultMode}` }; + } + const value = (await fs.readFile(modePath, "utf8")).trim().toLowerCase(); + const mode = value === acceptedMode ? acceptedMode : defaultMode; + return { mode, sourceStamp: `${stat.size}:${stat.mtimeMs}:${mode}` }; + } catch (error) { + if (error?.code === "ENOENT") { + return { mode: defaultMode, sourceStamp: `none:${defaultMode}` }; + } + return { mode: defaultMode, sourceStamp: `unreadable:${defaultMode}` }; + } +} + +const loadChromeModeState = (themeDir) => + loadPreferenceState(themeDir, "chrome-mode", "left", "full"); +const loadHomeModeState = (themeDir) => + loadPreferenceState(themeDir, "home-mode", "classic", "clean"); + +export async function loadPayload( + themeDir = path.join(root, "assets"), + candidateTheme = null, + candidateChromeModeState = null, + candidateHomeModeState = null, +) { const loadedTheme = candidateTheme ?? await loadTheme(themeDir); + const [chromeModeState, homeModeState] = await Promise.all([ + candidateChromeModeState ?? loadChromeModeState(themeDir), + candidateHomeModeState ?? loadHomeModeState(themeDir), + ]); + loadedTheme.theme.chromeMode = chromeModeState.mode; + loadedTheme.theme.homeMode = homeModeState.mode; + loadedTheme.themeFingerprint = loadedTheme.fingerprint; + loadedTheme.fingerprint = createHash("sha256") + .update(loadedTheme.themeFingerprint) + .update("\0chrome-mode\0") + .update(chromeModeState.mode) + .update("\0home-mode\0") + .update(homeModeState.mode) + .digest("hex"); + loadedTheme.sourceStamp = `${loadedTheme.sourceStamp}:chrome:${chromeModeState.sourceStamp}` + + `:home:${homeModeState.sourceStamp}`; const [css, template] = await Promise.all([ fs.readFile(path.join(root, "assets", "dream-skin.css"), "utf8"), fs.readFile(path.join(root, "assets", "renderer-inject.js"), "utf8"), @@ -687,16 +735,19 @@ async function fileExists(filePath) { } async function readThemeSourceStamp(loadedTheme) { - const [themeStat, imageStat, cssStat] = await Promise.all([ + const [themeStat, imageStat, cssStat, chromeModeState, homeModeState] = await Promise.all([ fs.stat(loadedTheme.themePath), fs.stat(loadedTheme.imagePath), fs.stat(path.join(path.dirname(loadedTheme.themePath), "theme.css")).catch((error) => { if (error.code === "ENOENT") return null; throw error; }), + loadChromeModeState(path.dirname(loadedTheme.themePath)), + loadHomeModeState(path.dirname(loadedTheme.themePath)), ]); return `${themeStat.size}:${themeStat.mtimeMs}:${imageStat.size}:${imageStat.mtimeMs}:` + - (cssStat ? `${cssStat.size}:${cssStat.mtimeMs}` : "none"); + (cssStat ? `${cssStat.size}:${cssStat.mtimeMs}` : "none") + + `:chrome:${chromeModeState.sourceStamp}:home:${homeModeState.sourceStamp}`; } async function probeSession(session) { @@ -1148,8 +1199,14 @@ export async function verifySession( const home = document.querySelector(${selectorLiteral("home-route")}); const settingsAnchor = document.querySelector(${selectorLiteral("appearance-radio")}) || document.querySelector(${stableTestidLiteral("theme-preview")}); - const suggestions = home?.querySelector(${selectorLiteral("home-suggestions")}) ?? null; - const cards = suggestions ? [...suggestions.querySelectorAll('button')].map(box) : []; + const suggestionLayers = [ + home?.querySelector(${selectorLiteral("home-suggestions")}) ?? null, + home?.querySelector(${selectorLiteral("home-suggestion-cards")}) ?? null, + ].filter((node, index, nodes) => node && nodes.indexOf(node) === index); + const cardButtons = [...new Set(suggestionLayers.flatMap((node) => + [...node.querySelectorAll('button')]))]; + const cards = cardButtons.map(box); + const visibleCardCount = cards.filter((item) => item?.visible).length; const runtime = window.__CODEX_DREAM_SKIN_STATE__; const adopted = runtime?.styleMode === 'adopted' && [...document.adoptedStyleSheets].includes(runtime.styleSheet); @@ -1186,11 +1243,12 @@ export async function verifySession( [...node.classList].some((name) => /^(?:dream-|codex-dream-skin(?:-|$))/.test(name)) ).length, homePresent: Boolean(home), - suggestionsPresent: Boolean(suggestions), + suggestionsPresent: suggestionLayers.length > 0, homeSurface: box(home), settingsAnchor: box(settingsAnchor), hero, cards, + visibleCardCount, composer: box(document.querySelector(${selectorLiteral("composer-chrome")})), shell: box(document.querySelector(${selectorLiteral("shell-main")})), sidebar: box(document.querySelector(${selectorLiteral("left-panel")})), @@ -1236,7 +1294,8 @@ export async function verifySession( documentPass && viewportPass && structurePass && payloadPass && (!result.homePresent || (Boolean(result.homeSurface?.visible && result.hero?.visible) && - (!result.suggestionsPresent || (result.cards.length >= 2 && result.cards.length <= 4)))); + (result.visibleCardCount === 0 || + (result.visibleCardCount >= 2 && result.visibleCardCount <= 4)))); return result; })()`); } @@ -1515,12 +1574,25 @@ async function runWatch(options) { } } if (shouldAudit) { - const candidateTheme = await loadTheme(options.themeDir); + const [candidateTheme, candidateChromeModeState, candidateHomeModeState] = await Promise.all([ + loadTheme(options.themeDir), + loadChromeModeState(options.themeDir), + loadHomeModeState(options.themeDir), + ]); lastStrongThemeAuditAt = now; - if (!loadedPayload || candidateTheme.fingerprint !== loadedPayload.fingerprint) { - nextPayload = await loadPayload(options.themeDir, candidateTheme); + if (!loadedPayload || candidateTheme.fingerprint !== loadedPayload.themeFingerprint || + candidateChromeModeState.mode !== loadedPayload.theme.chromeMode || + candidateHomeModeState.mode !== loadedPayload.theme.homeMode) { + nextPayload = await loadPayload( + options.themeDir, + candidateTheme, + candidateChromeModeState, + candidateHomeModeState, + ); } else { - loadedPayload.sourceStamp = candidateTheme.sourceStamp; + loadedPayload.sourceStamp = + `${candidateTheme.sourceStamp}:chrome:${candidateChromeModeState.sourceStamp}` + + `:home:${candidateHomeModeState.sourceStamp}`; } } } catch (error) { diff --git a/windows/scripts/login-dream-skin.ps1 b/windows/scripts/login-dream-skin.ps1 new file mode 100644 index 00000000..51585bfa --- /dev/null +++ b/windows/scripts/login-dream-skin.ps1 @@ -0,0 +1,27 @@ +[CmdletBinding()] +param([int]$Port = 9335) + +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'common-windows.ps1') +. (Join-Path $PSScriptRoot 'theme-windows.ps1') + +Assert-DreamSkinPort -Port $Port +$stateRoot = Join-Path $env:LOCALAPPDATA 'CodexDreamSkin' +$powershell = (Get-Command powershell.exe -ErrorAction Stop).Source +$trayScript = Join-Path $PSScriptRoot 'tray-dream-skin.ps1' +$startScript = Join-Path $PSScriptRoot 'start-dream-skin.ps1' + +# Login startup is an explicit opt-in. Start the tray first, then open a real +# Dream Skin session so a later click on the ordinary Codex shortcut reuses the +# already-running CDP-enabled app instead of creating an unskinned session. +if (-not (Test-DreamSkinTrayActive)) { + $trayToken = ConvertTo-DreamSkinProcessArgument -Value $trayScript + $trayArguments = "-NoProfile -STA -WindowStyle Hidden -ExecutionPolicy RemoteSigned -File $trayToken -Port $Port" + Start-Process -FilePath $powershell -ArgumentList $trayArguments -WindowStyle Hidden | Out-Null +} + +# Pausing is persistent and must remain authoritative across sign-in. +if (Test-DreamSkinPaused -StateRoot $stateRoot) { exit 0 } + +& $startScript -Port $Port -PromptRestart -RequireUnpaused ` + -OperationLockTimeoutMilliseconds 30000 diff --git a/windows/scripts/theme-windows.ps1 b/windows/scripts/theme-windows.ps1 index 17434594..decd7696 100644 --- a/windows/scripts/theme-windows.ps1 +++ b/windows/scripts/theme-windows.ps1 @@ -22,7 +22,7 @@ function Resolve-DreamSkinCommunityApplyUri { param([Parameter(Mandatory = $true)][string]$Uri) $match = [regex]::Match( $Uri, - '\Adreamskin://apply\?version=(ver_[a-z0-9]{8,64})\z', + '\Adreamskin://apply/?\?version=(ver_[a-z0-9]{8,64})\z', [System.Text.RegularExpressions.RegexOptions]::CultureInvariant ) if (-not $match.Success) { @@ -396,6 +396,15 @@ function Write-DreamSkinTheme { Write-DreamSkinUtf8FileAtomically -Path $themePath -Content ($json + "`r`n") } +function Test-DreamSkinObjectPropertyValue { + param( + [Parameter(Mandatory = $true)][object]$InputObject, + [Parameter(Mandatory = $true)][string]$Name + ) + $property = $InputObject.PSObject.Properties[$Name] + return $null -ne $property -and [bool]$property.Value +} + function Get-DreamSkinActiveThemeAppearance { param([Parameter(Mandatory = $true)][string]$ThemeDirectory) try { @@ -564,13 +573,17 @@ function Set-DreamSkinActiveTheme { Assert-DreamSkinImageFile -Path $target $Theme | Add-Member -NotePropertyName image -NotePropertyValue $imageName -Force if ($Name) { $Theme | Add-Member -NotePropertyName name -NotePropertyValue $Name -Force } - if (-not $Theme.id) { $Theme | Add-Member -NotePropertyName id -NotePropertyValue 'custom' -Force } - if (-not $Theme.appearance) { $Theme | Add-Member -NotePropertyName appearance -NotePropertyValue 'auto' -Force } - if (-not $Theme.art) { + if (-not (Test-DreamSkinObjectPropertyValue -InputObject $Theme -Name 'id')) { + $Theme | Add-Member -NotePropertyName id -NotePropertyValue 'custom' -Force + } + if (-not (Test-DreamSkinObjectPropertyValue -InputObject $Theme -Name 'appearance')) { + $Theme | Add-Member -NotePropertyName appearance -NotePropertyValue 'auto' -Force + } + if (-not (Test-DreamSkinObjectPropertyValue -InputObject $Theme -Name 'art')) { $Theme | Add-Member -NotePropertyName art -NotePropertyValue ` ([pscustomobject]@{ focusX = $null; focusY = $null; safeArea = 'auto'; taskMode = 'auto' }) -Force } - if (-not $Theme.palette) { + if (-not (Test-DreamSkinObjectPropertyValue -InputObject $Theme -Name 'palette')) { $Theme | Add-Member -NotePropertyName palette -NotePropertyValue ([pscustomobject]@{}) -Force } $activeCss = Join-Path $paths.Active 'theme.css' @@ -715,17 +728,17 @@ function Get-DreamSkinThemeRuntimeContentFingerprint { $loaded = Read-DreamSkinTheme -ThemeDirectory $ThemeDirectory -SkipImageMetadata $runtimeTheme = $loaded.Theme | ConvertTo-Json -Depth 8 | ConvertFrom-Json $runtimeTheme.image = '