Skip to content

Commit 52081be

Browse files
[codex] fix Pet activity pill theme compatibility
1 parent 1c7a859 commit 52081be

3 files changed

Lines changed: 256 additions & 3 deletions

File tree

TASK_PROGRESS.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Task Progress
22

3+
## Pet overlay + Dream Skin compatibility — locally fixed (2026-08-04)
4+
5+
- [goal] Preserve the native Codex Pet/avatar overlay while a Dream Skin theme is active; auxiliary pet windows remain transparent while activity pills do not repeat the skin wallpaper.
6+
- [scope] Branch `codex/fix-pet-overlay-compat` from clean `main` at `1c7a859`. The fix is macOS-only because the reproduced failure is native transparent-window composition on macOS. Repository changes are limited to `macos/scripts/injector.mjs`, its focused bootstrap regression, and this progress record.
7+
- [root cause] The native Pet activity pill intentionally uses a transparent material. With Dream Skin painting a high-contrast wallpaper behind the separate Pet composition windows, each `activity-slot-0` through `activity-slot-3` surface samples the wallpaper into a rounded rectangle. The Pet renderer was not receiving Dream Skin CSS; the missing behavior was a bounded Dream Skin compatibility treatment.
8+
- [implemented] The watcher recognizes only the exact `app://-/avatar-overlay-composition-surface.html?surfaceId=activity-slot-[0-3]` pages with the exact native title, re-verifies live identity, and applies an opaque native-token background only to `[class*="_activityPillMaterial_"]`. Main Codex, avatar root, mascot, voice output, voice controls, and microphone surfaces remain untouched. Pause/shutdown removal is verified before acknowledgement; target replacement and same-target reload are reprotected.
9+
- [tested] The focused regression first failed on the missing exported contract and now passes. The final complete `bash macos/tests/run-tests.sh` passes after the reload lifecycle change, including Swift build/XCTest, shared runtime checks, Safe CSS, import/rollback, signed-runtime integration, runtime-state integration, and Doctor. `node --check` and `git diff --check` pass.
10+
- [deployed locally] The installed v1.5.11 injector source was updated only after its pre-change SHA-256 matched repository `HEAD`, then the recorded injector was identity-checked and hot-restarted. Codex PID stayed `50570`; only the injector changed to PID `75775`. All four activity slots now contain the bounded style and compute to opaque `rgb(24, 24, 24)` while their HTML/body remain transparent. Main/avatar/mascot/voice surfaces contain no compatibility style. Reloading activity slot 0 produced a replacement target that the watcher automatically protected again.
11+
- [runtime status] The local theme operation record is `success` with message `皮肤已应用`, and a live scan of every CDP page found no Dream Skin operation host or operation registry. Pet activity slot 0 instead reports this still-running task as `Thinking`; the user's remaining spinner is therefore the Pet task-state indicator, not a stuck theme application. Codex was not restarted or switched.
12+
- [accepted locally] The user confirmed the native composite returned to normal after the task-state spinner ended.
13+
- [pending] Commit, push, open the requested draft PR, and synchronize the exact reviewed commit to Metis. No merge, version bump, or Release has occurred.
14+
315
## Client release v1.5.11 — preparing (2026-08-01)
416

517
- [base/merged] Settings renderer PR #334 passed exact-head CI run

macos/scripts/injector.mjs

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ const OPERATION_UI_HOST_ID = "chatgpt-dream-skin-operation";
5252
const OPERATION_UI_REGISTRY_KEY = "__CHATGPT_DREAM_SKIN_OPERATION_UI__";
5353
const OPERATION_KINDS = new Set(["apply", "pause", "switch"]);
5454
const OPERATION_UI_STATES = new Set(["success", "error", "cancelled"]);
55+
const PET_ACTIVITY_STYLE_ID = "codex-dream-skin-pet-compat-style";
56+
const PET_ACTIVITY_SURFACE_PATTERN = /^activity-slot-[0-3]$/;
57+
const PET_COMPOSITION_TITLE = "Codex Pet Composition Surface";
58+
const PET_COMPOSITION_PATH = "/avatar-overlay-composition-surface.html";
59+
const PET_ACTIVITY_COMPATIBILITY_CSS = `
60+
[class*="_activityPillMaterial_"] {
61+
background-color: var(--color-token-main-surface-primary, Canvas) !important;
62+
}
63+
`;
5564
const MIN_RENDERER_WIDTH = 320;
5665
const MIN_RENDERER_HEIGHT = 240;
5766
const MAX_RENDERER_DIMENSION = 65536;
@@ -370,6 +379,42 @@ function isValidCdpPageTarget(item, port) {
370379
}
371380
}
372381

382+
export function classifyPetActivityTarget(target) {
383+
if (target?.type !== "page" || target.title !== PET_COMPOSITION_TITLE) return null;
384+
let url;
385+
try {
386+
url = new URL(target.url);
387+
} catch {
388+
return null;
389+
}
390+
if (url.protocol !== "app:" || url.host !== "-" || url.pathname !== PET_COMPOSITION_PATH
391+
|| url.username || url.password || url.hash) return null;
392+
const entries = [...url.searchParams.entries()];
393+
if (entries.length !== 1 || entries[0][0] !== "surfaceId"
394+
|| !PET_ACTIVITY_SURFACE_PATTERN.test(entries[0][1])) return null;
395+
return entries[0][1];
396+
}
397+
398+
export function petActivityCompatibilityPayloadFor(enabled) {
399+
return `(() => {
400+
const styleId = ${JSON.stringify(PET_ACTIVITY_STYLE_ID)};
401+
const applicable = location.protocol === "app:" && location.host === "-" &&
402+
location.pathname === ${JSON.stringify(PET_COMPOSITION_PATH)} &&
403+
/^\\?surfaceId=activity-slot-[0-3]$/.test(location.search);
404+
const existing = document.getElementById(styleId);
405+
if (!applicable || !${JSON.stringify(Boolean(enabled))}) {
406+
existing?.remove();
407+
return { applicable, installed: false };
408+
}
409+
if (!document.head) return { applicable: true, installed: false };
410+
const style = existing || document.createElement("style");
411+
style.id = styleId;
412+
style.textContent = ${JSON.stringify(PET_ACTIVITY_COMPATIBILITY_CSS)};
413+
if (!existing) document.head.append(style);
414+
return { applicable: true, installed: true };
415+
})()`;
416+
}
417+
373418
class CdpSession {
374419
constructor(target, port) {
375420
this.target = target;
@@ -867,6 +912,23 @@ async function applyToSession(session, payload) {
867912
return session.evaluate(payload);
868913
}
869914

915+
async function setPetActivityCompatibility(session, enabled) {
916+
const result = await session.evaluate(petActivityCompatibilityPayloadFor(enabled));
917+
if (!result?.applicable || result.installed !== Boolean(enabled)) {
918+
throw new Error(`Pet activity compatibility ${enabled ? "install" : "removal"} did not verify`);
919+
}
920+
return result;
921+
}
922+
923+
async function livePetActivityTarget(session) {
924+
const target = await session.evaluate(`({
925+
type: "page",
926+
title: document.title,
927+
url: location.href,
928+
})`);
929+
return { target, surfaceId: classifyPetActivityTarget(target) };
930+
}
931+
870932
function nextOperationToken() {
871933
operationSequence += 1;
872934
return `${process.pid}:${Date.now()}:${operationSequence}`;
@@ -1560,6 +1622,7 @@ async function watchOperationState(statePath, onState) {
15601622
async function runWatch(options) {
15611623
let current = await loadPayload(options.themeDir);
15621624
const sessions = new Map();
1625+
const petSessions = new Map();
15631626
const rejected = new Set();
15641627
let stopping = false;
15651628
let reloadTimer = null;
@@ -1689,6 +1752,28 @@ async function runWatch(options) {
16891752
sessions.clear();
16901753
};
16911754

1755+
const releasePetSessions = async ({ strict = false } = {}) => {
1756+
const failures = [];
1757+
await Promise.all([...petSessions.entries()].map(async ([id, { session }]) => {
1758+
if (!session.closed) {
1759+
try {
1760+
await setPetActivityCompatibility(session, false);
1761+
} catch (error) {
1762+
if (strict) {
1763+
failures.push(error);
1764+
return;
1765+
}
1766+
console.error(`[dream-skin] pet compatibility removal failed: ${error.message}`);
1767+
}
1768+
}
1769+
session.close();
1770+
petSessions.delete(id);
1771+
}));
1772+
if (failures.length) {
1773+
throw new Error(`Pet compatibility removal did not verify: ${failures[0].message}`);
1774+
}
1775+
};
1776+
16921777
const restoreAfterAbortedPause = async (operation) => {
16931778
mutationEpoch += 1;
16941779
controlOnly = false;
@@ -1833,6 +1918,7 @@ async function runWatch(options) {
18331918
if (busy && operation.status === "pausing") {
18341919
await reloadChain.catch(() => {});
18351920
await waitForTargetSetups();
1921+
await releasePetSessions({ strict: true });
18361922
await Promise.all([...sessions.values()].map(async (record) => {
18371923
await invalidateEarly(record, { strict: true });
18381924
}));
@@ -1841,6 +1927,7 @@ async function runWatch(options) {
18411927
else if (operation.status === "paused") {
18421928
await reloadChain.catch(() => {});
18431929
await waitForTargetSetups().catch(() => {});
1930+
await releasePetSessions({ strict: true });
18441931
await Promise.all([...sessions.values()].map((record) =>
18451932
invalidateEarly(record, { strict: true }))).catch((error) => {
18461933
console.error(`[dream-skin] final pause invalidation failed: ${error.message}`);
@@ -1874,6 +1961,7 @@ async function runWatch(options) {
18741961
}
18751962
}
18761963
if (controlOnly && !activeOperation) {
1964+
await releasePetSessions();
18771965
releaseControlSessions();
18781966
await waitForControlOperation();
18791967
continue;
@@ -1893,6 +1981,7 @@ async function runWatch(options) {
18931981
}
18941982

18951983
if (controlOnly && !activeOperation) {
1984+
await releasePetSessions();
18961985
releaseControlSessions();
18971986
continue;
18981987
}
@@ -1909,12 +1998,71 @@ async function runWatch(options) {
19091998
sessions.delete(id);
19101999
}
19112000
}
2001+
for (const [id, record] of petSessions) {
2002+
if (!activeIds.has(id) || record.session.closed) {
2003+
record.session.close();
2004+
petSessions.delete(id);
2005+
}
2006+
}
19122007

19132008
const cycleRecovery = activeOperation ? null : pauseRecovery;
19142009
let recoveredPauseThisCycle = false;
19152010
let recoveryFailedThisCycle = false;
19162011
for (const target of targets) {
1917-
if (sessions.has(target.id)) continue;
2012+
if (sessions.has(target.id) || petSessions.has(target.id)) continue;
2013+
const petSurfaceId = classifyPetActivityTarget(target);
2014+
if (petSurfaceId) {
2015+
let petSession;
2016+
let petRecord;
2017+
const petConnectionEpoch = mutationEpoch;
2018+
beginTargetSetup();
2019+
try {
2020+
petSession = await connectTarget(target, options.port);
2021+
const liveTarget = await livePetActivityTarget(petSession);
2022+
if (liveTarget.surfaceId !== petSurfaceId || liveTarget.target.url !== target.url) {
2023+
throw new Error("Pet activity target identity changed after connection");
2024+
}
2025+
if (controlOnly || mutationEpoch !== petConnectionEpoch) {
2026+
throw new Error("Pet activity target became inactive during setup");
2027+
}
2028+
await setPetActivityCompatibility(petSession, true);
2029+
if (controlOnly || mutationEpoch !== petConnectionEpoch) {
2030+
await setPetActivityCompatibility(petSession, false);
2031+
throw new Error("Pet activity target became inactive during setup");
2032+
}
2033+
petRecord = { session: petSession, surfaceId: petSurfaceId };
2034+
petSessions.set(target.id, petRecord);
2035+
petSession.on("Page.loadEventFired", () => {
2036+
const reloadEpoch = mutationEpoch;
2037+
setTimeout(async () => {
2038+
if (petSession.closed || controlOnly || mutationEpoch !== reloadEpoch
2039+
|| petSessions.get(target.id) !== petRecord) return;
2040+
try {
2041+
const reloaded = await livePetActivityTarget(petSession);
2042+
if (reloaded.surfaceId !== petSurfaceId) {
2043+
petSession.close();
2044+
petSessions.delete(target.id);
2045+
return;
2046+
}
2047+
await setPetActivityCompatibility(petSession, true);
2048+
} catch (error) {
2049+
console.error(`[dream-skin] pet compatibility reload failed for ${target.id}: ${error.message}`);
2050+
}
2051+
}, 0);
2052+
});
2053+
rejected.delete(target.id);
2054+
console.log(`[dream-skin] protected pet activity surface ${petSurfaceId}`);
2055+
} catch (error) {
2056+
petSession?.close();
2057+
if (!rejected.has(target.id)) {
2058+
console.error(`[dream-skin] pet compatibility failed for ${target.id}: ${error.message}`);
2059+
rejected.add(target.id);
2060+
}
2061+
} finally {
2062+
finishTargetSetup();
2063+
}
2064+
continue;
2065+
}
19182066
let session;
19192067
let record;
19202068
let connectionEpoch;
@@ -2080,6 +2228,7 @@ async function runWatch(options) {
20802228
? bestEffortOperationUi(record.session, "hide", record.operationToken, "loading", "")
20812229
: Promise.resolve(false)));
20822230
await Promise.all([...sessions.values()].map((record) => removeEarly(record)));
2231+
await releasePetSessions();
20832232
for (const record of sessions.values()) record.session.close();
20842233
}
20852234
}

macos/tests/injector-bootstrap.test.mjs

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import fs from "node:fs/promises";
33
import path from "node:path";
44
import vm from "node:vm";
55
import { fileURLToPath } from "node:url";
6-
import { earlyPayloadFor } from "../scripts/injector.mjs";
6+
import {
7+
classifyPetActivityTarget,
8+
earlyPayloadFor,
9+
petActivityCompatibilityPayloadFor,
10+
} from "../scripts/injector.mjs";
711

812
const here = path.dirname(fileURLToPath(import.meta.url));
913
const injectorPath = path.resolve(here, "../scripts/injector.mjs");
@@ -203,4 +207,92 @@ assert.match(
203207
assert.match(source, /visibleSuggestionLabels\.length >= result\.visibleCardCount/);
204208
assert.match(source, /result\.suggestionLabelColorsMatch/);
205209

206-
console.log("PASS: early injection is L0-ready, generation-safe, and removed on shutdown.");
210+
const petTarget = (url, title = "Codex Pet Composition Surface") => ({
211+
type: "page",
212+
title,
213+
url,
214+
});
215+
assert.equal(classifyPetActivityTarget(petTarget(
216+
"app://-/avatar-overlay-composition-surface.html?surfaceId=activity-slot-0",
217+
)), "activity-slot-0");
218+
assert.equal(classifyPetActivityTarget(petTarget(
219+
"app://-/avatar-overlay-composition-surface.html?surfaceId=activity-slot-3",
220+
)), "activity-slot-3");
221+
for (const url of [
222+
"app://-/index.html?initialRoute=%2Favatar-overlay",
223+
"app://-/avatar-overlay-composition-surface.html?surfaceId=mascot-badge",
224+
"app://-/avatar-overlay-composition-surface.html?surfaceId=voice-controls",
225+
"app://-/avatar-overlay-composition-surface.html?surfaceId=activity-slot-4",
226+
"app://-/avatar-overlay-composition-surface.html?surfaceId=activity-slot-0&extra=1",
227+
"https://-/avatar-overlay-composition-surface.html?surfaceId=activity-slot-0",
228+
]) {
229+
assert.equal(classifyPetActivityTarget(petTarget(url)), null,
230+
`Non-activity or non-app target must not receive pet compatibility CSS: ${url}`);
231+
}
232+
assert.equal(classifyPetActivityTarget(petTarget(
233+
"app://-/avatar-overlay-composition-surface.html?surfaceId=activity-slot-0",
234+
"Unexpected Surface",
235+
)), null, "The exact native pet title is part of the auxiliary-target identity boundary.");
236+
237+
function createPetCompatibilityFixture(href) {
238+
const nodes = new Map();
239+
const head = {
240+
append(node) {
241+
node.parentNode = head;
242+
nodes.set(node.id, node);
243+
},
244+
};
245+
const document = {
246+
head,
247+
createElement(tagName) {
248+
return { tagName: tagName.toUpperCase(), id: "", textContent: "", parentNode: null };
249+
},
250+
getElementById(id) { return nodes.get(id) ?? null; },
251+
};
252+
return {
253+
context: {
254+
location: new URL(href),
255+
document,
256+
},
257+
get style() { return nodes.get("codex-dream-skin-pet-compat-style") ?? null; },
258+
remove(id) { nodes.delete(id); },
259+
};
260+
}
261+
262+
const petCompat = createPetCompatibilityFixture(
263+
"app://-/avatar-overlay-composition-surface.html?surfaceId=activity-slot-1",
264+
);
265+
vm.runInNewContext(petActivityCompatibilityPayloadFor(true), petCompat.context);
266+
assert.ok(petCompat.style, "An exact activity slot must receive the bounded compatibility style.");
267+
assert.match(petCompat.style.textContent, /\[class\*="_activityPillMaterial_"\]/);
268+
assert.match(petCompat.style.textContent, /var\(--color-token-main-surface-primary, Canvas\)/);
269+
assert.doesNotMatch(petCompat.style.textContent, /(?:html|body|main|form)\s*\{/,
270+
"Pet compatibility CSS must not paint the auxiliary window or native input surface.");
271+
assert.doesNotMatch(petCompat.style.textContent, /background-image|url\(/,
272+
"Pet compatibility CSS must never copy the Dream Skin wallpaper into pet surfaces.");
273+
petCompat.style.remove = () => petCompat.remove(petCompat.style.id);
274+
vm.runInNewContext(petActivityCompatibilityPayloadFor(false), petCompat.context);
275+
assert.equal(petCompat.style, null, "Pausing or stopping Dream Skin must remove the pet workaround.");
276+
277+
const petCompatWrongSurface = createPetCompatibilityFixture(
278+
"app://-/avatar-overlay-composition-surface.html?surfaceId=voice-controls",
279+
);
280+
vm.runInNewContext(petActivityCompatibilityPayloadFor(true), petCompatWrongSurface.context);
281+
assert.equal(petCompatWrongSurface.style, null,
282+
"Voice, mascot, and other auxiliary surfaces must remain completely untouched.");
283+
const targetLoopStart = source.indexOf("for (const target of targets)", source.indexOf("async function runWatch"));
284+
const petClassificationStart = source.indexOf("classifyPetActivityTarget(target)", targetLoopStart);
285+
const mainEarlyRegistrationStart = source.indexOf("registerEarlyForRecord(", petClassificationStart);
286+
assert.ok(targetLoopStart >= 0 && petClassificationStart > targetLoopStart
287+
&& mainEarlyRegistrationStart > petClassificationStart,
288+
"Pet activity surfaces must be classified before the full Dream Skin early payload is registered.");
289+
assert.match(source, /await setPetActivityCompatibility\(petSession, true\)/,
290+
"The watcher must install the bounded pet style only after live target identity verification.");
291+
assert.match(source, /petSession\.on\("Page\.loadEventFired"[\s\S]*livePetActivityTarget\(petSession\)[\s\S]*setPetActivityCompatibility\(petSession, true\)/,
292+
"A same-target pet renderer reload must reverify its identity and restore the bounded style.");
293+
assert.match(source, /operation\.status === "pausing"[\s\S]*await releasePetSessions\(\{ strict: true \}\)/,
294+
"Pausing Dream Skin must remove pet compatibility before acknowledging control-only mode.");
295+
assert.match(source, /finally\s*\{[\s\S]*await releasePetSessions\(\)[\s\S]*record\.session\.close/,
296+
"Watcher shutdown must remove the pet style before closing auxiliary CDP sessions.");
297+
298+
console.log("PASS: early injection is L0-ready, generation-safe, removed on shutdown, and pet-safe.");

0 commit comments

Comments
 (0)