diff --git a/CHANGELOG.md b/CHANGELOG.md index 2678e4c..87e981a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,10 @@ All notable changes are documented here. The project follows semantic versioning ### Changed -- **Satoru Gojo (五条 悟)** `1.2.0` remains the Featured package under the stable `satoru-gojo` slug. It is attempted on every numeric Codex Desktop version and marks `26.707.*` and `26.715.*` as live-verified. +- **Satoru Gojo (五条 悟)** `1.2.1` keeps the Featured `satoru-gojo` slug, moves decorative status chrome away from profile actions, and gives keyboard focus the sole strong rail when Codex exposes combined skill filters as simultaneously pressed. +- Skills reuse the plugin collection treatment in Chinese and English Codex interfaces. +- Renderer maintenance now verifies the active theme before reapplying, while component-role reconciliation avoids redundant DOM writes and keeps the home/Composer layout stable through transient typing rerenders. +- The theme remains attempted on every numeric Codex Desktop version and marks `26.707.*` and `26.715.*` as live-verified. - Four early engineering study themes and the temporary intake-slot scaffolding were removed from the release candidate. - Theme submissions use an explicit provenance and necessary-rights declaration. diff --git a/README.md b/README.md index 2f53209..a1c5591 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ After an official Codex update, the agent attempts the shared adapter again. If ## Current status -Version `0.4.3` includes the engine, twelve complete local theme packages, self-contained user installation, opt-in bounded login takeover, LaunchAgent persistence, conflict diagnostics, safe restore, and a static Gallery. +Version `0.4.4` includes the engine, twelve complete local theme packages, self-contained user installation, opt-in bounded login takeover, verification-first renderer maintenance, LaunchAgent persistence, conflict diagnostics, safe restore, and a static Gallery. | Capability | State | | --- | --- | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d21b20a..e5aa41c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -39,7 +39,7 @@ The public CLI keeps `_watch` and `_agent` internal. The shell entrypoint finds ```text Git clone (mutable source) → validated staging copy - → ~/Library/Application Support/AwesomeCodexThemes/releases/0.4.3/ + → ~/Library/Application Support/AwesomeCodexThemes/releases/0.4.4/ → atomic current symlink → ~/Library/LaunchAgents/io.github.awesome-codex-themes.agent.plist → current/bin/awesome-codex-themes _agent diff --git a/docs/RELEASING.md b/docs/RELEASING.md index d405ab3..f61972a 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -52,14 +52,14 @@ Before approval is executed, verify: - the repository description, topics, default branch, and security-advisory link are correct; - the owner understands that forks, caches, and downloaded release assets cannot be recalled reliably after publication. -## 5. Main and `v0.4.3` +## 5. Main and `v0.4.4` After the source, lifecycle, and visibility gates pass: 1. Land the reviewed release commit on `main` without overwriting unrelated local work. 2. Rerun `pnpm check` from the exact `main` commit. 3. Push `main` and confirm GitHub Actions succeeds on that SHA. -4. Create the annotated `v0.4.3` tag from the verified commit. +4. Create the annotated `v0.4.4` tag from the verified commit. 5. Publish a GitHub Release that links to [INSTALL.md](INSTALL.md), [MIGRATION.md](MIGRATION.md), [SAFETY.md](SAFETY.md), and the exact live-verified Codex ranges while explaining the best-effort fallback. 6. Confirm a fresh public clone can run `doctor`, `list`, an isolated `install-agent satoru-gojo --takeover-at-login` flow, and an `upgrade-agent` flow that preserves paused and takeover configuration. diff --git a/docs/STATUS.md b/docs/STATUS.md index 205a9eb..c4bbda9 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1,6 +1,6 @@ # Implementation status -Last verified: 2026-07-20 (Asia/Shanghai) +Last verified: 2026-07-21 (Asia/Shanghai) ## State definitions @@ -23,7 +23,7 @@ Last verified: 2026-07-20 (Asia/Shanghai) | Full app exit/relaunch recovery | Integration verified | An unmanaged process transitioned through `restart-required` and was replaced by a verified CDP-managed official process without a second profile. | | Login service bootstrap | Implemented and integration verified; physical reboot pending | `RunAtLoad`/`KeepAlive`, LaunchServices PID acquisition, and an opt-in one-shot 120-second login handoff have automated coverage. A real bootout/bootstrap and managed app restart were verified; a full physical reboot after this implementation is still pending. | | Best-effort version fallback | Implemented | Automated coverage confirms every valid numeric app version selects the shared `codex-best-effort` adapter instead of being rejected. Live visual verification on an unverified future build remains pending. | -| Satoru Gojo (native Japanese name: 五条 悟) | Featured complete package, experimental compatibility | Version `1.2.0` is live-verified on 26.707/26.715 with current assets, owned-state behavior, and persistent lifecycle recovery. | +| Satoru Gojo (native Japanese name: 五条 悟) | Featured complete package, experimental compatibility | Version `1.2.1` is live-verified on 26.707/26.715 with current assets, owned-state behavior, persistent lifecycle recovery, stable typing geometry, and collision-safe status chrome. | | Twelve-theme collection | Implemented and current-version verified | Satoru Gojo is Featured. Eleven additional Schema 2 packages include complete local backgrounds, previews, CSS, metadata, and component assets; all eleven passed the recorded 26.715 workspace, right-panel, home, plugins, scheduled, sites, pull-request, and narrow-window matrix. Compatibility remains experimental outside the recorded builds. | | Static Gallery | Implemented | Catalog, search, native-name discovery, detail, copy, and production-build tests are present. | | GitHub repository | Publicly released | The repository is public under `BeatAPI/Awesome-codex-themes`; no tagged GitHub Release has been created yet. | @@ -42,7 +42,7 @@ Last verified: 2026-07-20 (Asia/Shanghai) The active renderer was checked for owned style/chrome markers, theme variables, workspace classification, idempotent apply, renderer reload reapply, pointer-transparent decoration, and complete owned-state removal. Private screenshots with active workspace content and internal visual iteration labels remain local development evidence and are not public release assets. See the sanitized [26.715 lifecycle audit](audits/2026-07-17-satoru-gojo-26.715-lifecycle-audit.md). -The public source candidate is `0.4.3`. The current local service is separately verified as `active` on Codex `26.715.31925` with a literal-loopback endpoint and one owned `nocturne-lite` style; installing the final `0.4.3` local derivative and exercising a physical reboot remain separate gates. +The public source candidate is `0.4.4`. The current maintainer runtime is separately verified as `active` on Codex `26.715.52143` with a literal-loopback endpoint, one owned `satoru-gojo` style/chrome pair, stable Composer geometry during real text input, and no maintenance rewrites while the theme remains active. Installing the final `0.4.4` source candidate and exercising a physical reboot remain separate gates. The legacy injector plist and script were backed up outside the repository before migration. The old plist is no longer present in `~/Library/LaunchAgents`; its source script was not deleted. @@ -50,4 +50,4 @@ The legacy injector plist and script were backed up outside the repository befor The approved live session recorded the unmanaged `restart-required` transition, renderer reload reapply, pause and owned-state removal, paused-state upgrade, resume, managed relaunch, a real LaunchAgent bootout/bootstrap cycle, the former unknown-version safe mode before the policy changed, uninstall, and a successful reinstall with the original Codex profile preserved. -A clean release candidate passed `release:check`: 34 test files / 248 tests, all 12 theme packages, TypeScript, production Gallery build, production dependency audit, public-file hygiene, and diff whitespace. The public repository and anonymous clone path are live. Remaining tagged-release gates are a physical logout/reboot observation, the exact-commit CI result, and an explicit version-tag decision. +A clean release candidate passed `release:check`: 34 test files / 272 tests, all 12 theme packages, TypeScript, production Gallery build, production dependency audit, public-file hygiene, and diff whitespace. The public repository and anonymous clone path are live. Remaining tagged-release gates are a physical logout/reboot observation, the exact-commit CI result, and an explicit version-tag decision. diff --git a/package.json b/package.json index e0374bf..ef05520 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "awesome-codex-themes", - "version": "0.4.3", + "version": "0.4.4", "packageManager": "pnpm@10.32.1", "private": false, "type": "module", diff --git a/src/cli/main.mjs b/src/cli/main.mjs index 0080eca..0256e41 100644 --- a/src/cli/main.mjs +++ b/src/cli/main.mjs @@ -24,6 +24,7 @@ import { } from '../engine/macos.mjs'; import { applyThemeAtPort, + maintainThemeAtPort, removeThemeAtPort, restoreThemeSession, startThemeSession, @@ -701,7 +702,7 @@ export function createDefaultDependencies() { while (!stopping && processExists(appPid)) { try { await assertOfficialPortOwner(app, port); - await applyThemeAtPort({ themesRoot, themeSlug, port, appVersion: app.version }); + await maintainThemeAtPort({ themesRoot, themeSlug, port, appVersion: app.version }); } catch (error) { if (!RETRYABLE_CDP_ERRORS.has(error?.code)) throw error; } @@ -748,7 +749,7 @@ export function createDefaultDependencies() { requestAppQuit: requestOfficialAppQuit, waitForAppExit: waitForOfficialAppExit, applyTheme: ({ themeSlug, port, app }) => - applyThemeAtPort({ themesRoot, themeSlug, port, appVersion: app.version }), + maintainThemeAtPort({ themesRoot, themeSlug, port, appVersion: app.version }), removeTheme: ({ port }) => removeThemeAtPort({ port }), }, shouldStop: () => stopping, diff --git a/src/engine/injection.mjs b/src/engine/injection.mjs index 50d50a6..5f7ac18 100644 --- a/src/engine/injection.mjs +++ b/src/engine/injection.mjs @@ -217,7 +217,7 @@ export function buildApplyExpression(theme, adapter) { const findPluginSearchInput = () => { for (const candidate of document.querySelectorAll('input[placeholder]')) { const placeholder = candidate.getAttribute('placeholder') || ''; - if (/search plugins|搜索插件/i.test(placeholder)) return candidate; + if (/search (?:plugins|skills)|搜索(?:插件|技能)/i.test(placeholder)) return candidate; } return null; }; @@ -302,23 +302,23 @@ export function buildApplyExpression(theme, adapter) { 'awesomeCodexCollectionStatus', 'awesomeCodexCollectionAction', ]; - for (const roleKey of roleKeys) { - const attribute = 'data-' + roleKey.replace(/[A-Z]/g, (letter) => '-' + letter.toLowerCase()); - for (const candidate of document.querySelectorAll('[' + attribute + ']')) { - delete candidate.dataset[roleKey]; + const desiredRoles = new Map(roleKeys.map((roleKey) => [roleKey, new Map()])); + const markRole = (candidate, roleKey, value = 'true') => { + if (candidate && desiredRoles.has(roleKey)) { + desiredRoles.get(roleKey).set(candidate, String(value)); } - } + }; for (const candidate of document.querySelectorAll('.bg-token-dropdown-background:not(header)')) { const headers = candidate.querySelectorAll('section > header.bg-token-dropdown-background'); if (headers.length === 0) continue; - candidate.dataset.awesomeCodexArtifactPanel = 'true'; + markRole(candidate, 'awesomeCodexArtifactPanel'); const panelHost = candidate.closest('[data-pip-obstacle="thread-summary-panel"]') || candidate.parentElement; - if (panelHost) panelHost.dataset.awesomeCodexArtifactPanelHost = 'true'; - for (const header of headers) header.dataset.awesomeCodexArtifactHeader = 'true'; + markRole(panelHost, 'awesomeCodexArtifactPanelHost'); + for (const header of headers) markRole(header, 'awesomeCodexArtifactHeader'); for (const panelMore of candidate.querySelectorAll('.text-token-conversation-summary-trailing')) { - panelMore.dataset.awesomeCodexPanelMore = 'true'; + markRole(panelMore, 'awesomeCodexPanelMore'); } } @@ -328,50 +328,50 @@ export function buildApplyExpression(theme, adapter) { candidate.querySelector('.text-token-conversation-body'), ); if (isCompletedSummary) { - candidate.dataset.awesomeCodexCompletedSummary = 'true'; + markRole(candidate, 'awesomeCodexCompletedSummary'); } else if (candidate.classList.contains('group/activity-header')) { - candidate.dataset.awesomeCodexActivityItem = 'true'; + markRole(candidate, 'awesomeCodexActivityItem'); } } for (const candidate of document.querySelectorAll('.app-header-tint :is(button, [role="button"])')) { - candidate.dataset.awesomeCodexHeaderControl = 'true'; + markRole(candidate, 'awesomeCodexHeaderControl'); } for (const candidate of document.querySelectorAll('[class~="group/folder-row"]')) { - candidate.dataset.awesomeCodexProjectRow = 'true'; + markRole(candidate, 'awesomeCodexProjectRow'); } for (const [index, candidate] of [ - ...document.querySelectorAll('[data-awesome-codex-project-row="true"]'), + ...document.querySelectorAll('[class~="group/folder-row"]'), ].entries()) { - candidate.dataset.awesomeCodexDomainId = 'D-' + String(index + 1).padStart(2, '0'); + markRole(candidate, 'awesomeCodexDomainId', 'D-' + String(index + 1).padStart(2, '0')); } for (const dragHandle of document.querySelectorAll('[class~="cursor-grab"]')) { const taskRow = dragHandle.closest('div.group.relative') || dragHandle.parentElement; - if (taskRow) taskRow.dataset.awesomeCodexTaskRow = 'true'; + markRole(taskRow, 'awesomeCodexTaskRow'); } for (const sidebar of document.querySelectorAll('.app-shell-left-panel')) { const domainHeaders = [...sidebar.querySelectorAll('[class~="group/section-toggle"]')]; for (const candidate of domainHeaders) { - candidate.dataset.awesomeCodexDomainHeader = 'true'; + markRole(candidate, 'awesomeCodexDomainHeader'); } const firstDomainHeader = domainHeaders[0] ?? null; for (const candidate of sidebar.querySelectorAll('button')) { const label = (candidate.getAttribute('aria-label') || '').toLocaleLowerCase(); if (/switch mode|切换模式/.test(label)) { - candidate.dataset.awesomeCodexSidebarBrand = 'true'; + markRole(candidate, 'awesomeCodexSidebarBrand'); } if (candidate.querySelector('img.rounded-full')) { - candidate.dataset.awesomeCodexAccountIdentity = 'true'; - candidate.dataset.awesomeCodexAccountTrigger = 'true'; + markRole(candidate, 'awesomeCodexAccountIdentity'); + markRole(candidate, 'awesomeCodexAccountTrigger'); } } for (const candidate of sidebar.querySelectorAll('button')) { if (candidate === firstDomainHeader) break; if (candidate.classList.contains('h-[var(--height-token-row)]')) { - const currentIndex = sidebar.querySelectorAll('[data-awesome-codex-sidebar-nav]').length + 1; - candidate.dataset.awesomeCodexSidebarNav = String(currentIndex).padStart(2, '0'); + const currentIndex = desiredRoles.get('awesomeCodexSidebarNav').size + 1; + markRole(candidate, 'awesomeCodexSidebarNav', String(currentIndex).padStart(2, '0')); } } for (const candidate of sidebar.querySelectorAll('button[aria-label], [role="button"][aria-label]')) { @@ -382,9 +382,9 @@ export function buildApplyExpression(theme, adapter) { label.includes('个人资料菜单') || label.includes('账户菜单') ) { - candidate.dataset.awesomeCodexAccountTrigger = 'true'; + markRole(candidate, 'awesomeCodexAccountTrigger'); } else if (label.includes('help') || label.includes('帮助')) { - candidate.dataset.awesomeCodexHelpTrigger = 'true'; + markRole(candidate, 'awesomeCodexHelpTrigger'); } } } @@ -393,9 +393,9 @@ export function buildApplyExpression(theme, adapter) { '[data-awesome-codex-home="true"] [data-feature="game-source"]', ); if (homePrompt) { - homePrompt.dataset.awesomeCodexHomePrompt = 'true'; + markRole(homePrompt, 'awesomeCodexHomePrompt'); const domainMarker = homePrompt.querySelector('button'); - if (domainMarker) domainMarker.dataset.awesomeCodexDomainMarker = 'true'; + markRole(domainMarker, 'awesomeCodexDomainMarker'); } for (const composer of document.querySelectorAll('.composer-surface-chrome')) { @@ -423,7 +423,7 @@ export function buildApplyExpression(theme, adapter) { } else if (candidate.classList.contains('h-token-button-composer') && !isSquare) { control = 'model'; } - if (control) candidate.dataset.awesomeCodexComposerControl = control; + if (control) markRole(candidate, 'awesomeCodexComposerControl', control); } } @@ -432,23 +432,23 @@ export function buildApplyExpression(theme, adapter) { const pluginRoot = closestMainSurface(pluginSearchInput); const searchSurface = pluginSearchInput.parentElement; if (searchSurface) { - searchSurface.dataset.awesomeCodexPluginSearch = 'true'; - searchSurface.dataset.awesomeCodexCollectionSearch = 'true'; + markRole(searchSurface, 'awesomeCodexPluginSearch'); + markRole(searchSurface, 'awesomeCodexCollectionSearch'); } for (const heading of pluginRoot.querySelectorAll('h2')) { if (heading.parentElement) { - heading.parentElement.dataset.awesomeCodexPluginSectionHeader = 'true'; - heading.parentElement.dataset.awesomeCodexCollectionSectionHeader = 'true'; + markRole(heading.parentElement, 'awesomeCodexPluginSectionHeader'); + markRole(heading.parentElement, 'awesomeCodexCollectionSectionHeader'); } } for (const installedRow of pluginRoot.querySelectorAll('[class~="group/plugin-row"]')) { for (const button of installedRow.querySelectorAll(':scope > button')) { - button.dataset.awesomeCodexPluginInstalled = 'true'; - button.dataset.awesomeCodexCollectionRow = 'true'; + markRole(button, 'awesomeCodexPluginInstalled'); + markRole(button, 'awesomeCodexCollectionRow'); const icon = button.querySelector('img')?.parentElement; - if (icon) icon.dataset.awesomeCodexPluginIcon = 'true'; + markRole(icon, 'awesomeCodexPluginIcon'); } } @@ -458,8 +458,8 @@ export function buildApplyExpression(theme, adapter) { !candidate.closest('[data-awesome-codex-plugin-card="true"]') && !candidate.closest('[class~="group/plugin-row"]') ) { - candidate.dataset.awesomeCodexPluginTab = 'true'; - candidate.dataset.awesomeCodexCollectionTab = 'true'; + markRole(candidate, 'awesomeCodexPluginTab'); + markRole(candidate, 'awesomeCodexCollectionTab'); } } @@ -467,14 +467,16 @@ export function buildApplyExpression(theme, adapter) { const iconImage = candidate.querySelector('img'); const contentRow = candidate.querySelector(':scope > .flex.items-center.gap-3'); if (!iconImage || !contentRow) continue; - candidate.dataset.awesomeCodexPluginCard = 'true'; - candidate.dataset.awesomeCodexCollectionRow = 'true'; + markRole(candidate, 'awesomeCodexPluginCard'); + markRole(candidate, 'awesomeCodexCollectionRow'); const icon = iconImage.parentElement; - if (icon) icon.dataset.awesomeCodexPluginIcon = 'true'; + markRole(icon, 'awesomeCodexPluginIcon'); for (const action of candidate.querySelectorAll('button')) { - action.dataset.awesomeCodexPluginAction = action.classList.contains('aspect-square') - ? 'menu' - : 'install'; + markRole( + action, + 'awesomeCodexPluginAction', + action.classList.contains('aspect-square') ? 'menu' : 'install', + ); } } } @@ -482,33 +484,27 @@ export function buildApplyExpression(theme, adapter) { const scheduledSearchInput = findScheduledSearchInput(); if (scheduledSearchInput) { const scheduledRoot = closestMainSurface(scheduledSearchInput); - if (scheduledSearchInput.parentElement) { - scheduledSearchInput.parentElement.dataset.awesomeCodexCollectionSearch = 'true'; - } + markRole(scheduledSearchInput.parentElement, 'awesomeCodexCollectionSearch'); for (const candidate of scheduledRoot.querySelectorAll('button.shrink-0.h-token-button-composer')) { - candidate.dataset.awesomeCodexCollectionTab = 'true'; + markRole(candidate, 'awesomeCodexCollectionTab'); } for (const candidate of scheduledRoot.querySelectorAll('.automation-row')) { - candidate.dataset.awesomeCodexCollectionRow = 'true'; + markRole(candidate, 'awesomeCodexCollectionRow'); } for (const heading of scheduledRoot.querySelectorAll('h2')) { - if (heading.parentElement) { - heading.parentElement.dataset.awesomeCodexCollectionSectionHeader = 'true'; - } + markRole(heading.parentElement, 'awesomeCodexCollectionSectionHeader'); } } const sitesSearchInput = findSitesSearchInput(); if (sitesSearchInput) { const sitesRoot = closestMainSurface(sitesSearchInput); - if (sitesSearchInput.parentElement) { - sitesSearchInput.parentElement.dataset.awesomeCodexCollectionSearch = 'true'; - } + markRole(sitesSearchInput.parentElement, 'awesomeCodexCollectionSearch'); const emptyState = sitesRoot.querySelector('.max-w-xl.flex-col.items-center'); if (emptyState) { - emptyState.dataset.awesomeCodexCollectionEmpty = 'true'; + markRole(emptyState, 'awesomeCodexCollectionEmpty'); const action = emptyState.querySelector('button'); - if (action) action.dataset.awesomeCodexCollectionAction = 'primary'; + markRole(action, 'awesomeCodexCollectionAction', 'primary'); } } @@ -516,16 +512,25 @@ export function buildApplyExpression(theme, adapter) { const pullRequestsRoot = closestMainSurface(pullRequestsSearchInput) || findPullRequestsRoot(); if (pullRequestsRoot) { - if (pullRequestsSearchInput?.parentElement) { - pullRequestsSearchInput.parentElement.dataset.awesomeCodexCollectionSearch = 'true'; - } + markRole(pullRequestsSearchInput?.parentElement, 'awesomeCodexCollectionSearch'); for (const candidate of pullRequestsRoot.querySelectorAll('button.shrink-0.h-token-button-composer')) { - candidate.dataset.awesomeCodexCollectionTab = 'true'; + markRole(candidate, 'awesomeCodexCollectionTab'); } const status = pullRequestsRoot.querySelector('[role="status"]'); - if (status) status.dataset.awesomeCodexCollectionStatus = 'true'; + markRole(status, 'awesomeCodexCollectionStatus'); const emptyState = pullRequestsRoot.querySelector('.max-w-xl.flex-col.items-center'); - if (emptyState) emptyState.dataset.awesomeCodexCollectionEmpty = 'true'; + markRole(emptyState, 'awesomeCodexCollectionEmpty'); + } + + for (const roleKey of roleKeys) { + const desired = desiredRoles.get(roleKey); + const attribute = 'data-' + roleKey.replace(/[A-Z]/g, (letter) => '-' + letter.toLowerCase()); + for (const candidate of document.querySelectorAll('[' + attribute + ']')) { + if (!desired.has(candidate)) delete candidate.dataset[roleKey]; + } + for (const [candidate, value] of desired) { + if (candidate.dataset[roleKey] !== value) candidate.dataset[roleKey] = value; + } } }; const updateSurface = () => { @@ -534,6 +539,7 @@ export function buildApplyExpression(theme, adapter) { const sitesSearchInput = findSitesSearchInput(); const pullRequestsSearchInput = findPullRequestsSearchInput(); const pullRequestsRoot = findPullRequestsRoot(); + const retainHomeStructure = root.dataset.awesomeCodexSurface === 'home'; let home = null; if ( !pluginSearchInput && @@ -548,7 +554,11 @@ export function buildApplyExpression(theme, adapter) { const suggestions = candidate.querySelector('[class~="group/home-suggestions"]'); const hasIdentity = isVisible(icon) && isVisible(source); const hasActions = isVisible(source) && isVisible(suggestions); - if (isVisible(candidate) && (hasIdentity || hasActions)) { + const hasHomeStructure = Boolean(source && (icon || suggestions)); + if ( + isVisible(candidate) && + (hasIdentity || hasActions || (retainHomeStructure && hasHomeStructure)) + ) { home = candidate; break; } @@ -557,8 +567,8 @@ export function buildApplyExpression(theme, adapter) { for (const candidate of document.querySelectorAll('[data-awesome-codex-home]')) { if (candidate !== home) delete candidate.dataset.awesomeCodexHome; } - if (home) home.dataset.awesomeCodexHome = 'true'; - root.dataset.awesomeCodexSurface = pluginSearchInput + if (home && home.dataset.awesomeCodexHome !== 'true') home.dataset.awesomeCodexHome = 'true'; + const nextSurface = pluginSearchInput ? 'plugins' : scheduledSearchInput ? 'scheduled' @@ -569,14 +579,20 @@ export function buildApplyExpression(theme, adapter) { : home ? 'home' : 'workspace'; + if (root.dataset.awesomeCodexSurface !== nextSurface) { + root.dataset.awesomeCodexSurface = nextSurface; + } updateComponentRoles(); const artifactPanels = [...document.querySelectorAll('[data-awesome-codex-artifact-panel="true"]')]; if (artifactPanels.length === 0) { delete root.dataset.awesomeCodexArtifactPanelState; } else { - root.dataset.awesomeCodexArtifactPanelState = artifactPanels.some(isEffectivelyVisible) + const nextPanelState = artifactPanels.some(isEffectivelyVisible) ? 'open' : 'closed'; + if (root.dataset.awesomeCodexArtifactPanelState !== nextPanelState) { + root.dataset.awesomeCodexArtifactPanelState = nextPanelState; + } } }; updateSurface(); diff --git a/src/engine/session.mjs b/src/engine/session.mjs index ec26866..ef69350 100644 --- a/src/engine/session.mjs +++ b/src/engine/session.mjs @@ -5,6 +5,7 @@ import { evaluateOnRendererTargets } from './cdp.mjs'; import { buildApplyExpression, buildRemoveExpression, + buildVerificationExpression, } from './injection.mjs'; import { matchesProcessIdentity } from './state.mjs'; import { assertThemeCompatibility, loadThemePackage } from './theme.mjs'; @@ -47,6 +48,38 @@ export async function applyThemeAtPort({ return { theme: themeSlug, adapter: adapter.id, renderers: results.length }; } +export async function isThemeActiveAtPort({ + themeSlug, + port, + appVersion, + evaluate = evaluateOnRendererTargets, +}) { + const adapter = await loadCodexAdapter(appVersion); + const results = await evaluate({ + port, + expression: buildVerificationExpression(themeSlug, adapter.id), + }); + return Boolean( + Array.isArray(results) && + results.length > 0 && + results.every( + (result) => + result?.pass && result.theme === themeSlug && result.adapter === adapter.id, + ), + ); +} + +export async function maintainThemeAtPort( + options, + { isActive = isThemeActiveAtPort, apply = applyThemeAtPort } = {}, +) { + if (await isActive(options)) { + return { theme: options.themeSlug, active: true, reapplied: false }; + } + const result = await apply(options); + return { ...result, active: true, reapplied: true }; +} + export async function removeThemeAtPort({ port, evaluate = evaluateOnRendererTargets }) { const results = await evaluate({ port, expression: buildRemoveExpression() }); if (!Array.isArray(results) || results.length === 0 || results.some((result) => !result?.pass)) { diff --git a/src/generated/themes.json b/src/generated/themes.json index 3e8c83f..3554869 100644 --- a/src/generated/themes.json +++ b/src/generated/themes.json @@ -657,7 +657,7 @@ }, { "slug": "satoru-gojo", - "version": "1.2.0", + "version": "1.2.1", "name": "Satoru Gojo", "nativeName": "五条 悟", "nativeLocale": "ja-JP", diff --git a/tests/docs/public-docs.test.mjs b/tests/docs/public-docs.test.mjs index c034265..863c051 100644 --- a/tests/docs/public-docs.test.mjs +++ b/tests/docs/public-docs.test.mjs @@ -53,7 +53,7 @@ describe('public English-first documentation', () => { expect(releasing).toContain('Public visibility gate'); expect(releasing).toContain('pnpm check'); expect(releasing).toContain('macOS lifecycle evidence'); - expect(releasing).toContain('v0.4.3'); + expect(releasing).toContain('v0.4.4'); }); test('publishes only complete runnable theme previews', async () => { @@ -100,6 +100,6 @@ describe('public English-first documentation', () => { test('bumps the platform for the persistent-agent release line', async () => { const packageJson = JSON.parse(await text('package.json')); - expect(packageJson.version).toBe('0.4.3'); + expect(packageJson.version).toBe('0.4.4'); }); }); diff --git a/tests/engine/injection.test.mjs b/tests/engine/injection.test.mjs index 97f7424..858a738 100644 --- a/tests/engine/injection.test.mjs +++ b/tests/engine/injection.test.mjs @@ -365,6 +365,46 @@ describe('theme injection expressions', () => { expect(project.dataset.awesomeCodexDomainMarker).toBeUndefined(); }); + test('keeps the home surface stable while its visible children rerender during typing', async () => { + const dom = createDom(); + const createHome = ({ visibleChildren }) => { + const home = dom.window.document.createElement('main'); + home.setAttribute('role', 'main'); + const icon = dom.window.document.createElement('div'); + icon.dataset.testid = 'home-icon'; + const source = dom.window.document.createElement('div'); + source.dataset.feature = 'game-source'; + const suggestions = dom.window.document.createElement('div'); + suggestions.className = 'group/home-suggestions'; + Object.defineProperty(home, 'getBoundingClientRect', { + value: () => ({ width: 600, height: 400, top: 80, left: 300, right: 900, bottom: 480 }), + }); + if (visibleChildren) { + for (const candidate of [icon, source, suggestions]) { + Object.defineProperty(candidate, 'getBoundingClientRect', { + value: () => ({ width: 300, height: 80, top: 100, left: 350, right: 650, bottom: 180 }), + }); + } + } + home.append(icon, source, suggestions); + return home; + }; + + const initialHome = createHome({ visibleChildren: true }); + dom.window.document.body.appendChild(initialHome); + dom.window.eval(buildApplyExpression(runtimeTheme(), runtimeAdapter())); + await new Promise((resolve) => dom.window.setTimeout(resolve, 0)); + expect(dom.window.document.documentElement.dataset.awesomeCodexSurface).toBe('home'); + + const rerenderedHome = createHome({ visibleChildren: false }); + initialHome.replaceWith(rerenderedHome); + await new Promise((resolve) => dom.window.setTimeout(resolve, 0)); + await new Promise((resolve) => dom.window.setTimeout(resolve, 0)); + + expect(dom.window.document.documentElement.dataset.awesomeCodexSurface).toBe('home'); + expect(rerenderedHome.dataset.awesomeCodexHome).toBe('true'); + }); + test('classifies the plugin marketplace and its interactive surfaces', async () => { const dom = createDom(); const pluginMarket = dom.window.document.createElement('main'); @@ -452,6 +492,61 @@ describe('theme injection expressions', () => { expect(install.dataset.awesomeCodexPluginAction).toBeUndefined(); }); + test('reuses the plugin marketplace treatment for the skills search surface', async () => { + const dom = createDom(); + const skillsMarket = dom.window.document.createElement('main'); + skillsMarket.setAttribute('role', 'main'); + const search = dom.window.document.createElement('div'); + const searchInput = dom.window.document.createElement('input'); + searchInput.placeholder = '搜索技能'; + search.appendChild(searchInput); + skillsMarket.appendChild(search); + dom.window.document.body.appendChild(skillsMarket); + + dom.window.eval(buildApplyExpression(runtimeTheme(), runtimeAdapter())); + await new Promise((resolve) => dom.window.setTimeout(resolve, 0)); + + expect(dom.window.document.documentElement.dataset.awesomeCodexSurface).toBe('plugins'); + expect(search.dataset.awesomeCodexPluginSearch).toBe('true'); + expect(search.dataset.awesomeCodexCollectionSearch).toBe('true'); + }); + + test('does not reclassify the document for transient composer typing mutations', async () => { + const dom = createDom(); + const projectRow = dom.window.document.createElement('div'); + projectRow.className = 'group/folder-row'; + const composer = dom.window.document.createElement('div'); + composer.className = 'composer-surface-chrome'; + const editor = dom.window.document.createElement('div'); + editor.className = 'ProseMirror'; + editor.contentEditable = 'true'; + const sendButton = dom.window.document.createElement('button'); + sendButton.className = 'composer-send-button'; + composer.append(editor, sendButton); + dom.window.document.body.append(projectRow, composer); + + dom.window.eval(buildApplyExpression(runtimeTheme(), runtimeAdapter())); + await new Promise((resolve) => dom.window.setTimeout(resolve, 0)); + + let themeAttributeMutationCount = 0; + const observer = new dom.window.MutationObserver((records) => { + themeAttributeMutationCount += records.filter( + (record) => + record.type === 'attributes' && + record.attributeName?.startsWith('data-awesome-codex-'), + ).length; + }); + observer.observe(dom.window.document.documentElement, { attributes: true, subtree: true }); + + editor.appendChild(dom.window.document.createTextNode('x')); + sendButton.classList.add('is-ready'); + await new Promise((resolve) => dom.window.setTimeout(resolve, 0)); + await new Promise((resolve) => dom.window.setTimeout(resolve, 0)); + observer.disconnect(); + + expect(themeAttributeMutationCount).toBe(0); + }); + test('classifies scheduled tasks and marks shared collection roles', async () => { const dom = createDom(); const main = dom.window.document.createElement('main'); diff --git a/tests/engine/session.test.mjs b/tests/engine/session.test.mjs index 366e4aa..dfac70a 100644 --- a/tests/engine/session.test.mjs +++ b/tests/engine/session.test.mjs @@ -6,6 +6,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest'; import { SessionError, applyThemeAtPort, + maintainThemeAtPort, restoreThemeSession, startThemeSession, } from '../../src/engine/session.mjs'; @@ -128,6 +129,40 @@ describe('applyThemeAtPort', () => { }); }); +describe('maintainThemeAtPort', () => { + test('does not reapply while the requested theme is already active', async () => { + const isActive = vi.fn(async () => true); + const apply = vi.fn(); + + await expect( + maintainThemeAtPort( + { themeSlug: 'test-theme', port: 9341, appVersion: '26.707.72221' }, + { isActive, apply }, + ), + ).resolves.toEqual({ theme: 'test-theme', active: true, reapplied: false }); + + expect(apply).not.toHaveBeenCalled(); + }); + + test('reapplies once after the renderer loses the requested theme', async () => { + const options = { themeSlug: 'test-theme', port: 9341, appVersion: '26.707.72221' }; + const apply = vi.fn(async () => ({ theme: 'test-theme', adapter: 'codex-26.707', renderers: 1 })); + + await expect( + maintainThemeAtPort(options, { isActive: async () => false, apply }), + ).resolves.toEqual({ + theme: 'test-theme', + adapter: 'codex-26.707', + renderers: 1, + active: true, + reapplied: true, + }); + + expect(apply).toHaveBeenCalledTimes(1); + expect(apply).toHaveBeenCalledWith(options); + }); +}); + describe('startThemeSession', () => { test('never launches another app while the official app is already running', async () => { const launchApp = vi.fn(); diff --git a/tests/themes/satoru-gojo.test.mjs b/tests/themes/satoru-gojo.test.mjs index a70b655..4a05df5 100644 --- a/tests/themes/satoru-gojo.test.mjs +++ b/tests/themes/satoru-gojo.test.mjs @@ -12,7 +12,7 @@ describe('Satoru Gojo flagship theme', () => { const loaded = await loadThemePackage(themeRoot); expect(loaded.manifest.slug).toBe('satoru-gojo'); - expect(loaded.manifest.version).toBe('1.2.0'); + expect(loaded.manifest.version).toBe('1.2.1'); expect(loaded.manifest.name).toBe('Satoru Gojo'); expect(loaded.manifest.nativeName).toBe('五条 悟'); expect(loaded.manifest.nativeLocale).toBe('ja-JP'); @@ -150,12 +150,21 @@ describe('Satoru Gojo flagship theme', () => { expect(css).toMatch( /\[data-awesome-codex-plugin-card="true"\][^{]*\{[^}]*border:\s*0\s*!important/s, ); + expect(css).toMatch( + /\[data-awesome-codex-plugin-tab="true"\]\[aria-pressed="true"\]:not\(:focus\)\s*\{[^}]*box-shadow:\s*none\s*!important/s, + ); + expect(css).toMatch( + /\[data-awesome-codex-plugin-tab="true"\]\[aria-pressed="true"\]:focus\s*\{[^}]*inset\s+2px\s+0\s+#5e7fe0/s, + ); expect(css).toMatch( /\[data-awesome-codex-composer-control="access"\]::before\s*\{[^}]*border:\s*0\s*!important/s, ); expect(css).toMatch( /\.act-experience__status\s*\{[^}]*border-radius:\s*0\s*;/s, ); + expect(css).toMatch( + /\.act-experience__status\s*\{[^}]*right:\s*max\(14rem,\s*env\(safe-area-inset-right\)\)\s*;/s, + ); expect(css).toMatch( /\[data-awesome-codex-artifact-panel="true"\]\s*\{[^}]*border:\s*0\s*!important/s, ); diff --git a/themes/satoru-gojo/theme.css b/themes/satoru-gojo/theme.css index eec08c8..64c94a8 100644 --- a/themes/satoru-gojo/theme.css +++ b/themes/satoru-gojo/theme.css @@ -804,6 +804,18 @@ html.awesome-codex-theme[data-awesome-codex-theme="satoru-gojo"][data-awesome-co box-shadow: inset 2px 0 #5e7fe0 !important; } +html.awesome-codex-theme[data-awesome-codex-theme="satoru-gojo"][data-awesome-codex-surface="plugins"] [data-awesome-codex-plugin-tab="true"][aria-pressed="true"]:not(:focus) { + color: #657a9c !important; + background: transparent !important; + box-shadow: none !important; +} + +html.awesome-codex-theme[data-awesome-codex-theme="satoru-gojo"][data-awesome-codex-surface="plugins"] [data-awesome-codex-plugin-tab="true"][aria-pressed="true"]:focus { + color: #2b4e86 !important; + background: linear-gradient(90deg, #dceaffb8, #eef6ff69 78%, transparent) !important; + box-shadow: inset 2px 0 #5e7fe0 !important; +} + html.awesome-codex-theme[data-awesome-codex-theme="satoru-gojo"][data-awesome-codex-surface="plugins"] [data-awesome-codex-plugin-tab="true"]:hover { color: #315a97 !important; background: #dfeeff73 !important; @@ -1645,7 +1657,8 @@ html.awesome-codex-theme-experience[data-awesome-codex-surface="home"] #awesome- #awesome-codex-theme-chrome .act-experience__status { position: absolute; top: 11px; - right: 118px; + /* Keep Codex's native profile visibility and edit actions unobstructed. */ + right: max(14rem, env(safe-area-inset-right)); display: flex; align-items: center; gap: 8px; diff --git a/themes/satoru-gojo/theme.json b/themes/satoru-gojo/theme.json index ccdd300..07e339f 100644 --- a/themes/satoru-gojo/theme.json +++ b/themes/satoru-gojo/theme.json @@ -1,7 +1,7 @@ { "schemaVersion": 2, "slug": "satoru-gojo", - "version": "1.2.0", + "version": "1.2.1", "name": "Satoru Gojo", "nativeName": "五条 悟", "nativeLocale": "ja-JP",