diff --git a/docs/adr/0008-codex-startup-shadow-and-composer-surface.md b/docs/adr/0008-codex-startup-shadow-and-composer-surface.md new file mode 100644 index 0000000..2bfb35c --- /dev/null +++ b/docs/adr/0008-codex-startup-shadow-and-composer-surface.md @@ -0,0 +1,32 @@ +# ADR 0008:Codex 初始化阴影与输入区装饰层 + +- 日期:2026-10-23 +- 状态:接受 +- 范围:Codex adapter;保持现有背景档案、存储键及其他客户端行为。 + +## 问题与证据 + +用户报告初始化阴影与设置不符,并提供输入框外围黑色矩形截图。 +现场 Codex 包版本为 26.1016.6573.0,CDP 与探索下的背景入口可识别。 +读取现场时保存值、API、根节点 CSS 变量及清单都为 46%,背景未活动;这次快照不代表已经复现真实客户端的启动时序。 + +浏览器回归使用真实媒体载荷,复现两个初始化缺口:只有控制台负责恢复阴影,独立媒体启动不恢复;宿主替换 html.style 后,界面仍显示 46%,媒体阴影却回到 0%。 +现场还发现 composer 后有空的、不可交互、绝对定位的 bg-surface 元素;现有样式没有清除它的底色,与用户截图一致。生成的 CSS 模块哈希不作为适配契约。 + +## 决定 + +将阴影偏好的读取、校验、持久化及 CSS 同步移入独立 dim.js,媒体初始化前安装,控制台仅调用同一 API。继续使用 beauticode-dim 存储键与 0~1 值域,0 是有效设置,非法值回退自动。 + +只监听 html 的 style 与 data-bc-dim-user 属性。幂等写入避免自身触发循环;重复安装复用同版本实例,替换版本释放旧监听。窗口关闭结束生命周期,清除媒体保留用户偏好供下次应用。自动滑块位置分别对应深色 0%、浅色 48%。 + +只在背景活动时,将 main 内底部定位容器的空、不可交互 bg-surface 子装饰层设为透明。输入框、对话框和其他 surface 保留底色;清除背景恢复宿主样式。不引入通用 surface 透明规则。 + +2026-10-26 补充:用户继续报告输入框上方的黑色渐变。现场确认它属于 thread-scroll-container 内 sticky bottom-0 页脚的空、不可交互、横向绝对定位 from-surface 子层;实际背景为从 #181818 到透明的线性渐变。旧的 bg-gradient-to-t/from-token-main-surface-primary 规则不匹配该结构。新增仅针对这一布局职责的规则,不依赖 _background_ / _fade_ 生成类名,不改变有内容的节点、非粘滞结构或聊天区之外的渐变。背景清除后由 data-bc-active 门控恢复宿主渐变。 + +## 验证与边界 + +appearance.browser.test.js 使用 Edge 实际渲染,覆盖首次恢复、根样式替换、零值、非法值、浅深色、清除偏好不重建舞台及输入区边界。通过 BEAUTICODE_PLAYWRIGHT_MODULE 指向已有 Playwright 运行时;没有运行时时显式跳过,不添加发布依赖。 + +其他客户端现场均未运行,安装状态不能代表新版 DOM 验收。另一个已有五端浅色检查发现,四个非 DSH 客户端默认 48% 白色遮罩在纯黑壁纸上只有约 3.75:1 对比度,本次保留既有亮度策略,未将其宣称为通过。 + +回滚可恢复安装目录 current.json 指向上一版本,再重启 beautiCode 辅助进程;无需重启或修改 Codex 二进制。源码回滚同时恢复 payload、console、CSS 并移除 dim.js,原存储键无需迁移。 diff --git a/docs/adr/0009-codex-wallpaper-surface-and-guest-lifecycle.md b/docs/adr/0009-codex-wallpaper-surface-and-guest-lifecycle.md new file mode 100644 index 0000000..eb5620c --- /dev/null +++ b/docs/adr/0009-codex-wallpaper-surface-and-guest-lifecycle.md @@ -0,0 +1,101 @@ +# ADR 0009:Codex 壁纸外壳与 guest 生命周期 + +- 日期:2026-10-24 +- 状态:接受;现场与自动化证据的范围分别列于验收记录。 +- 基线:main aea3220;来源 PR #96 head d5f3252。 +- 关联:[实施与验收记录](../pr96-owner-completion.md)、PR #96。 + +## 问题与现场事实 + +PR 把原生圆角、tooltip、功能浮层、内容路由和 guest 注入混在两个文件中。 +固定浅色背景与宿主深色文字体系冲突;同代次短路前创建观察器会泄漏。 +尺寸扫描不能辨认页面外壳和阅读底板,直接修改内联背景还必须保存声明优先级。 + +26.1016.6573.0 的宿主静态资源声明 /plugins、/library、/sites、/images 原生路由。 +实际切页也确认这些入口在宿主文档呈现;不能据路由声明推断所有版本均无 webview。 +当前存在独立的定价预热 webview:src 是 about:blank,toUrl 是相对路径,实际地址为 chatgpt.com。 +因此排除 sandbox 或仅限制 chatgpt.com 都不能形成正确的页面边界。 +支付与工作区设置资源还包含独立 webview 流程,本适配不进入这些流程。 + +站点页的外壳由 workspace slot、全高 bg-surface、contents、滚动容器及居中 tabpanel 构成。 +资料库复用滚动标题结构,但不具有站点页的全高 bg-surface 根节点。 +两者的标题底板是滚动内容标题壳的 ::before;清理 h1 本身无效。 +真实 DOM 中存在生成类名,但它们只作为调查证据,不作为本次适配选择器。 + +## 决策 + +### 宿主样式 + +只删除 beautiCode 对左栏、main 和首页卡片圆角的清零,保留宿主实际圆角。 +当前宿主部分外层原生圆角为 0;本修复不承诺把这些外层改成指定像素。 +普通 tooltip 保留原生底色与前景;Radix 定位壳透明,真正菜单面板保留主题底色。 +浮层及 sidebar peek 使用 --bc-readable-fill;peek 仅裁切并修正直接底板,沿用宿主圆角。 +所有新浮层规则排除 beautiCode 自身 UI 及其后代。 + +保留 ADR 0008 的空、非交互 composer 装饰层规则。 +原生页面只匹配观测到的 workspace/滚动/标题/tabpanel 结构。 +不清空所有 bg-surface,不使用视口大小推断语义,也不新增生成类名兜底。 +布局契约失效时保持宿主底板,而不是回退到更宽泛的透明规则。 + +### 模块与所有权 + +媒体入口只负责代次、组合、舞台及销毁;工作态、用户播放偏好、媒体来源、图片、 +视频节点、就绪/重试和既有 API 分别拥有状态。 +图片与视频附件均需要可取消的原生事件等待,因此复用小型 media-wait 模块。 +销毁会取消等待并解绑事件;旧图片完成后不能更新下一代根属性。 +公开七个注入参数、既有方法名及健康快照字段保持不变。 +修正偏好恢复调用:沿用已有 isBackgroundTone 方法,不新增兼容别名。 +模块通过操作、只读 getter 和回调协作,不共享可变状态包。 + +payload 在注入表达式的词法作用域内组合工厂,避免浏览器端 ESM/CSP 或新增全局注册框架。 +主入口从约 2022 行降到不足 400 行,新增媒体与 guest 模块均小于 500 行。 +保留现有 renderer 资产复制和插件打包机制,不引入运行时依赖。 + +### guest 契约与生命周期 + +guest 控制器内部接口为 setActive(boolean)、sync()、dispose()。 +同一文档同版本复用;媒体交接不捕获旧代次闭包,完整销毁释放实例。 +未激活时不创建宿主观察器,也不绑定 guest 事件或注入 CSS。 + +条件允许的内容根路由仅 /plugins、/library、/sites、/images。 +还要求宿主 main 中的已连接 webview、about:blank 的预期路径和当前 HTTPS +chatgpt.com 精确路径一致;不接受端口、用户凭据、相似域名或任意子路径。 +prewarm、sandbox、定价/结算/工作区 partition 及所有未知路由均拒绝。 +/pricing-plan、/checkout、/admin、认证流程和未验证的 /plans 不在允许表中。 +查询参数不作为账号授权依据,不将它们写入日志。 + +“候选页面”只允许绑定就绪事件,只有实际地址通过校验才可注入。 +dom-ready、主框架导航、页内导航与动态挂载均重新检查。 +不支持或尚未验证的版本保持原生;没有合适现场实例时不得宣称 guest 现场验收通过。 + +用 Electron insertCSS/removeInsertedCSS 的键管理根外壳样式。 +guest 自身表单、卡片、弹层及主题变量不改写;不安装 guest DOM 观察器、不改内联背景。 +样式只涉及 html/body 及直接根节点,内层没有已验证契约时不扩展清理范围。 +宿主视图使用宿主 CSS 处理其原生内层外壳。 + +每个登记实例只有一个未完成插入调用;操作版本随导航、清除和销毁递增。 +过期结果通过自己的样式键撤销,不能删除重新激活后的新样式。 +旧调用阻塞过新文档时,完成后只为变化过的版本补同步一次;拒绝/超时不无限重试。 +超时为 3000ms。Electron Promise 不能真正取消;超时保留原生显示并诊断, +迟到的成功结果仍会回收,底层 Promise 永不完成时不再堆积新调用。 +清理依赖登记表,能够处理已脱离 DOM 的 webview。 +撤销 API 拒绝时给出阶段/类别诊断,不能把 API 失败宣称为成功恢复。 + +## 验证与限制 + +Node 测试覆盖边界、候选就绪、动态新增/移除、100 次实例复用、清理竞态、超时、 +错误脱敏、视频交接后永久错误和图片取消。 +真实 Edge 浏览器执行完整载荷,覆盖主题切换、圆角、保留阅读卡片、媒体和交接; +最终 Codex 插件包内载荷重复执行同组关键回归。 +合成浏览器中的 webview API 替身不是 Electron 现场证明。 +真实客户端检查仅临时替换 CSS,检查原生内容页后恢复;不安装新运行时或修改媒体档案。 +详细通过数、截图、采样视频及未验收边界见实施记录。 + +## 演进与回滚 + +宿主变更后先更新脱敏 DOM 样例和对应回归,再调整结构或允许表。 +新增 guest 页面必须有真实 URL、身份及根外壳证据,不扩大域名或尺寸扫描兜底。 +现有日志不会包含正文、账号或完整 URL。 +本次不改存储键及档案协议;回滚本组提交并重新打包即可恢复旧运行时。 +若已经安装某个包,应先停止该包的辅助注入进程,再切换到上一包,避免两代注入竞争; +不修改 Codex 二进制,也无需数据迁移。 diff --git a/docs/pr96-owner-completion.md b/docs/pr96-owner-completion.md new file mode 100644 index 0000000..e11c54e --- /dev/null +++ b/docs/pr96-owner-completion.md @@ -0,0 +1,130 @@ +# PR #96:owner 实施与验收记录 + +日期:2026-10-24。基线 origin/main aea32209f02de0e14b7248ac870f5868e195d167; +来源 PR head d5f3252,经执行前在线复核未变。 + +实施位于附加 worktree 的 codex/pr96-owner-completion 分支。 +原 E:/WoRk/beautiCode 工作区未修改;其 tracked patch 与 untracked 文件先复制后整合, +包括阴影恢复、空 composer 装饰层、相关浏览器测试及 ADR 0008。 +其他复制的本地 DSH 文档/脚本不属于本次新增实现,不混入本次修复提交。 +实施与初次验收阶段未发布、推送或合并,也未把新运行时安装到用户应用。 + +2026-10-26 提交准备:远程主线已通过 405fd81 合并 #96,本分支同步该主线, +作为后续修复提交。冲突仅涉及旧运行时和 CSS;保留本分支已验证的新控制器、 +模块拆分和精确样式规则,没有重新引入被替换的尺寸扫描或宽泛透明规则。 +同步后重新执行类型检查及全仓测试:435 通过、20 显式跳过、0 失败; +串行执行两组真实 Edge 浏览器回归共 9 项,全部通过、零跳过。 +对应日志为 pr-submission-typecheck.log、pr-submission-tests.log、pr-submission-browser.log。 + +## 已实现 + +- 保留宿主圆角与普通 tooltip 配色;定位壳透明,菜单/浮层/peek 随宿主主题。 +- 排除 beautiCode 自身 UI;保留输入框、普通卡片和无关 aside 底色。 +- 按现场结构清理站点的三层外壳及资料库/站点的标题伪元素,不新增生成类名选择器。 +- 媒体按职责拆分,修改后的主运行时及新增模块小于 500 行;既有大测试文件也按职责分拆。 +- guest 按预期/实际地址和宿主插槽验证;支付、预热、认证、sandbox 与未知页面保持原生。 +- 样式键替代内联修改和尺寸扫描;登记表管理事件、观察器和异步操作。 +- 同代次复用、交接、完整清除、销毁、动态节点、导航期间延迟完成均有行为回归。 +- 修正旧有图片等待取消及色调读取方法不一致,保留原公开方法。 + +架构与明确的允许/拒绝规则见 [ADR 0009](adr/0009-codex-wallpaper-surface-and-guest-lifecycle.md)。 + +## 实施过程与可复现证据 + +基线 Codex:74 通过、5 跳过、0 失败;显式浏览器基线 4 通过、0 跳过。 +失败先于修复的记录保存在 artifacts/pr96/: + +| 失败证据 | 检查内容 | +| --- | --- | +| red.log | 固定样式、未实现 guest 行为 | +| guest-readiness-red.log | about:blank 候选页面需要等待 dom-ready | +| handoff-error-red.log | 交接已结束后,永久视频错误不能继续 pending | +| navigation-race-red.log | 旧插入调用不能阻断新文档注入 | +| image-cancel-red.log | 已销毁的图片附件不能更新下一代 | +| guest-timer-red.log | 销毁必须释放插入超时计时器 | +| owned-ui-red.log | 自有 UI 的嵌套浮层也必须排除 | +| header-red.log / library-header-red.log | 清 h1 无效,且 Library 与 Sites 外壳不同 | +| tone-red.log | 通过已有公开方法恢复色调偏好 | + +独立只读审查发现并复现 handoff 快照和 pending 导航收敛两项 Important; +均已按失败测试→修复→通过完成,未发现 Critical。 +首次视频测试的 MediaRecorder 只产生文件头,原生 video 也无法解码; +补充真实帧录制后才用于运行时验收,没有通过放宽断言绕过失败。 + +## 自动化验收 + +| 检查 | 结果 / 原始日志 | +| --- | --- | +| 全仓类型检查 | 通过;typecheck.log | +| 全仓构建 | 通过;build.log | +| 全仓最终确认 | 435 通过、19 跳过、0 失败;full-test-confirmation.log | +| 最新 Codex 测试 | 106 通过、9 跳过、0 失败;adapter-final.log | +| 完整载荷 Edge 浏览器 | 8 通过、0 跳过;browser-final.log | +| 最终插件包内完整载荷 | 4 通过、0 跳过;packaged-browser.log | +| 打包模块加载/语法 | 进入必跑 Node 测试,未遗漏私有模块 | +| 空白与规模检查 | git diff --check;主入口及新运行时模块均不超过 500 行 | + +浏览器回归直接执行 loadRendererSource/buildInjectionExpression 产生的真实载荷, +覆盖浅→深→浅、圆角、tooltip、peek、自有 UI、原生路由外壳及普通阅读卡片, +以及真实视频解码、交接、静音、定位、摸鱼、色调继承和清除。 +包内验证通过 BEAUTICODE_RENDERER_PAYLOAD 指向 staged vendor/payload.js, +不是只检查源文件字符串或包文件清单。 + +全仓复跑曾在未修改的 DSH pack.test.mjs 出现一次 Node 测试器 +“Unable to deserialize cloned data due to invalid or unsupported version”。 +DSH 单独重跑 122 通过、10 跳过、0 失败,记录为 dsh-recheck.log; +该异常记录保留为 full-test-final.log,不将其抹掉或归因于已证实的业务缺陷。 +最后一轮全仓确认 435 通过、19 跳过、0 失败,见 full-test-confirmation.log。 + +## 真实客户端与视觉边界 + +现场版本:Codex Desktop 26.1016.6573.0 / Windows,深色主题。 +通过当前 9335 CDP 读取 DOM/静态资源、切换内容入口并临时替换壁纸 CSS; +finally 恢复原 CSS,并恢复本聊天,没有改动背景档案或安装包。 + +- Plugins、Library、Sites、Images 均实际打开并采集截图。 +- Sites 的根外壳、内容容器、tabpanel 背景确认透明;普通卡片保留自身外观。 +- 初次截图暴露 Library 标题仍不透明,随后增加独立结构样例及回归再修复。 +- sandbox 未绑定旧式透明事件;定价预热保持排除。 +- 外层 sidebar 在本版本原生值可为 0px;保留原生不是强制增加圆角。 + +截图:artifacts/pr96/live-route-0.png 至 live-route-3.png, +合成但真实渲染的主题对照:fixture-dark.png、fixture-light.png。 +navigation-00.png 至 navigation-05.png 是实际切页采样; +navigation-sampled.webm 是这六帧的回放,不是连续录屏,不能证明采样间绝无闪烁。 +原始无正文结构快照及结果为 live-*.json / live-css-results.json。 +这些本地证据位于忽略目录,不随提交公开。 + +未宣称通过的范围: + +- 用户应用的浅色主题:已在真实 Edge + 脱敏结构中覆盖,未现场更改用户主题设置。 +- 允许的 Electron guest 现场:当前目标入口为原生页面,只有被排除的预热 guest; + 不能把 API 替身测试或方法存在当作此项现场验收。 +- Plans/结算/认证等未验证页面:保留原生,不扩大透明范围。 +- 未安装旧版客户端,没有将本版本结论推广至 PR 描述的旧版本。 + +## 2026-10-26 输入框上方渐变补充 + +现场定位并清除了聊天滚动区粘滞页脚的空 from-surface 渐变层。 +当前窗口应用独立、带背景活动门控的样式修正;不修改安装包,窗口重载后须由更新的载荷重新注入。 +读取修正前后的计算样式:渐变由 linear-gradient 变为 none,输入框仍为 rgb(31, 31, 31)。 +截图 composer-fade-after.png 与脱敏计算结果 composer-fade-live.json 位于本地 artifacts/pr96。 +新增回归先失败后通过,覆盖浅→深→浅、相似元素不受影响及 dispose 后恢复。 +appearance 浏览器测试实际执行 5 项,全部通过、零跳过(composer-fade-browser.log)。 +本次全仓复跑为 435 通过、20 显式跳过、0 失败(composer-fade-full-test.log); +新打包 CSS 与源码 SHA256 一致。包内载荷浏览器测试首次视频用例仅录得 171 字节文件头而失败, +未改测试或运行时代码,原样复跑 4 项全部通过、零跳过。 +首次失败与复跑日志分别为 composer-fade-packaged-browser.log、composer-fade-packaged-recheck.log; +这是现有实时录制样例的时序波动,不能将该次首次执行记为通过。 + +## 复跑 + +从仓库根运行 npm run typecheck、npm test、npm run build。 +浏览器测试使用现有 BEAUTICODE_PLAYWRIGHT_MODULE,Windows 下赋值为 file:/// 模块 URL, +运行 node --test packages/adapter-codex/test/appearance.browser.test.js +packages/adapter-codex/test/wallpaper.browser.test.js。 +未配置该变量时浏览器测试会显式跳过,不能算视觉通过。 + +运行 node scripts/pack-codex-plugin.mjs --no-build 后, +将 BEAUTICODE_RENDERER_PAYLOAD 指向包内 vendor/adapter-codex/payload.js 的 file URL, +再运行 wallpaper.browser.test.js 即可验证最终载荷。 diff --git a/packages/adapter-codex/src/payload.ts b/packages/adapter-codex/src/payload.ts index a85148d..5411daa 100644 --- a/packages/adapter-codex/src/payload.ts +++ b/packages/adapter-codex/src/payload.ts @@ -10,16 +10,36 @@ export async function loadRendererSource(): Promise<{ runtimeIife: string; consoleSource: string; }> { - const [cssText, runtimeIife, consoleJs, galleryJs, themeJs] = await Promise.all([ + const [cssText, runtimeIife, consoleJs, galleryJs, themeJs, dimJs] = await Promise.all([ fs.readFile(path.join(here, "renderer", "background.css"), "utf8"), fs.readFile(path.join(here, "renderer", "background-runtime.js"), "utf8"), fs.readFile(path.join(here, "renderer", "console.js"), "utf8"), fs.readFile(path.join(here, "renderer", "gallery.js"), "utf8"), fs.readFile(path.join(here, "renderer", "theme.js"), "utf8"), + fs.readFile(path.join(here, "renderer", "dim.js"), "utf8"), ]); - // Resolve host appearance after media installs, including healthy re-injects. - const themedRuntime = `((...args) => Promise.resolve((${runtimeIife})(...args)).then(result => { ${themeJs}\nreturn result; }))`; - return { cssText, runtimeIife: themedRuntime, consoleSource: `${consoleJs}\n${galleryJs}` }; + const factories = { + createWorkingState: "runtime-working.js", + createPlaybackControls: "runtime-controls.js", + createMediaSources: "runtime-sources.js", + createMediaWaiter: "runtime-media-wait.js", + createImageRuntime: "runtime-image.js", + createVideoLifecycle: "runtime-video-lifecycle.js", + createVideoElementBinding: "runtime-video-element.js", + createVideoRuntime: "runtime-video.js", + createBackgroundApi: "runtime-api.js", + createGuestPolicy: "guest-policy.js", + createGuestController: "guest-wallpaper.js", + createGuestOwner: "guest-install.js", + }; + const modules = await Promise.all(Object.entries(factories).map(async ([name,file]) => + `const ${name} = ${await fs.readFile(path.join(here,"renderer",file),"utf8")};`)); + // Factories remain lexical to the injected payload. Only the document-scoped + // guest owner survives media handoffs; modules expose operations, not state bags. + const themedRuntime = `((...args) => { ${dimJs}\n${modules.join("\n")} + const guest = createGuestOwner({createController:createGuestController,policy:createGuestPolicy()}); + return Promise.resolve((${runtimeIife})(...args)).then(result => { ${themeJs}\nreturn result; }); })`; + return { cssText, runtimeIife: themedRuntime, consoleSource: `${dimJs}\n${consoleJs}\n${galleryJs}` }; } /** 1×1 PNG. Video injects must not ship the live poster as a multi-MB data URL. */ diff --git a/packages/adapter-codex/src/renderer/background-runtime.js b/packages/adapter-codex/src/renderer/background-runtime.js index 30d0ab1..3ab6818 100644 --- a/packages/adapter-codex/src/renderer/background-runtime.js +++ b/packages/adapter-codex/src/renderer/background-runtime.js @@ -20,2198 +20,370 @@ * So media MUST arrive as data: or blob: URLs. */ ((cssText, imageDataUrl, videoConfig, generation, imageUrl, forceRebuild, imageBlob) => { - const STAGE_ID = "beauticode-bg-stage"; - const STYLE_ID = "beauticode-bg-style"; - const VIDEO_INPUT_ID = "beauticode-video-input"; - const IMAGE_INPUT_ID = "beauticode-image-input"; - const gen = Number(generation) || 0; - const wantImageBlob = Boolean(imageBlob); - - const toChineseMediaError = (error) => { - const message = String(error?.message || error || "").trim(); - if (/^(?:failed to fetch|fetch failed|fail fetch)$/i.test(message)) { - return "未发现注入CDP的Codex进程"; - } - if (/media fetch failed/i.test(message)) { - return "背景媒体加载失败。"; - } - if (/data url fetch failed/i.test(message)) { - return "背景数据加载失败。"; - } - if (/missing (?:media )?url|no video source/i.test(message)) { - return "背景媒体地址缺失。"; - } - if (/malformed data url/i.test(message)) { - return "背景数据地址格式错误。"; - } - if (/stale generation/i.test(message)) { - return "背景版本已过期,正在重新加载。"; - } - return message || "背景媒体加载失败。"; - }; - - const root = document.documentElement; - const previous = window.__BEAUTICODE_BG__; - - // Full-page ChatGPT routes (Plugins, Skills, Library, Plans) live in an - // Electron . Host CSS cannot cross that document boundary, so clear - // the guest's opaque shell tokens from the host through executeJavaScript. - // Controls keep their own borders and foregrounds; only page-sized surfaces - // are made transparent so the full-window wallpaper remains the base layer. - const GUEST_STYLE_ID = "beauticode-guest-wallpaper-style"; - const GUEST_CSS = ` - html, body, body > #root, body > #__next { - background: transparent !important; - background-color: transparent !important; - background-image: none !important; - } - `; - const guestExpression = `(() => { - const id = ${JSON.stringify(GUEST_STYLE_ID)}; - let style = document.getElementById(id); - if (!style) { - style = document.createElement("style"); - style.id = id; - (document.head || document.documentElement).appendChild(style); - } - style.textContent = ${JSON.stringify(GUEST_CSS)}; - const ATTR = "data-beauticode-guest-cleared"; - const paint = () => { - if (!document.body) return; - const vw = window.innerWidth || 1; - const vh = window.innerHeight || 1; - const visit = (el, depth) => { - if (!(el instanceof HTMLElement) || depth > 6) return; - if (!el.hasAttribute(ATTR)) { - const rect = el.getBoundingClientRect(); - if (rect.width >= vw * 0.9 && rect.height >= vh * 0.9) { - const bg = getComputedStyle(el).backgroundColor; - if (bg && bg !== "transparent" && bg !== "rgba(0, 0, 0, 0)") { - el.setAttribute(ATTR, el.style.backgroundColor || ""); - el.style.setProperty("background-color", "transparent", "important"); - } - } - } - for (const child of el.children) visit(child, depth + 1); - }; - for (const child of document.body.children) visit(child, 0); - }; - paint(); - if (!window.__beauticodeGuestObserver) { - const observer = new MutationObserver(() => { - if (window.__beauticodeGuestPaintTimer) return; - window.__beauticodeGuestPaintTimer = setTimeout(() => { - window.__beauticodeGuestPaintTimer = 0; - try { paint(); } catch (_) {} - }, 300); - }); - observer.observe(document.documentElement, { childList: true, subtree: true }); - window.__beauticodeGuestObserver = observer; - } - return true; - })()`; - const clearGuestExpression = `(() => { - document.getElementById(${JSON.stringify(GUEST_STYLE_ID)})?.remove(); - try { - window.__beauticodeGuestObserver?.disconnect(); - window.__beauticodeGuestObserver = null; - if (window.__beauticodeGuestPaintTimer) { - clearTimeout(window.__beauticodeGuestPaintTimer); - window.__beauticodeGuestPaintTimer = 0; - } - } catch (_) {} - document.querySelectorAll("[data-beauticode-guest-cleared]").forEach((el) => { - const prev = el.getAttribute("data-beauticode-guest-cleared"); - el.removeAttribute("data-beauticode-guest-cleared"); - if (prev) el.style.backgroundColor = prev; - else el.style.removeProperty("background-color"); - }); - return true; - })()`; - const guestTransparencyAllowed = (webview) => { - try { - const src = webview.getAttribute("src") || ""; - return !src.startsWith("codex-sandbox:"); - } catch (_) { - return false; - } - }; - const installGuestTransparency = (webview) => { - if ( - !webview || - !guestTransparencyAllowed(webview) || - typeof webview.executeJavaScript !== "function" - ) { - return; - } - try { - const result = webview.executeJavaScript(guestExpression); - if (result && typeof result.catch === "function") result.catch(() => {}); - } catch (_) {} - if (!webview.__beauticodeGuestTransparencyBound) { - webview.__beauticodeGuestTransparencyBound = true; - const refresh = () => installGuestTransparency(webview); - webview.__beauticodeGuestTransparencyRefresh = refresh; - try { webview.addEventListener("dom-ready", refresh); } catch (_) {} - try { webview.addEventListener("did-finish-load", refresh); } catch (_) {} - } - }; - const syncGuestTransparency = () => { - try { - document.querySelectorAll('webview:not([src^="codex-sandbox:"])') - .forEach(installGuestTransparency); - } catch (_) {} - }; - const clearGuestTransparency = () => { - try { - document.querySelectorAll("webview").forEach((webview) => { - if (!webview.__beauticodeGuestTransparencyBound) return; - if (typeof webview.executeJavaScript === "function") { - try { - const result = webview.executeJavaScript(clearGuestExpression); - if (result && typeof result.catch === "function") { - result.catch(() => {}); - } - } catch (_) {} - } - const refresh = webview.__beauticodeGuestTransparencyRefresh; - if (refresh) { - try { webview.removeEventListener("dom-ready", refresh); } catch (_) {} - try { - webview.removeEventListener("did-finish-load", refresh); - } catch (_) {} - } + const STAGE_ID = "beauticode-bg-stage"; + const STYLE_ID = "beauticode-bg-style"; + const VIDEO_INPUT_ID = "beauticode-video-input"; + const IMAGE_INPUT_ID = "beauticode-image-input"; + const gen = Number(generation) || 0; + const wantImageBlob = Boolean(imageBlob); + const root = document.documentElement; + const previous = window.__BEAUTICODE_BG__; + // ---- Same-generation short-circuit (critical anti-flash) ---- + // The tray watch loop re-publishes the active payload every ~1s. Rebuilding + // the stage on every poll creates stacked