Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,536 changes: 3,643 additions & 893 deletions assets/inject/dream-skin.css

Large diffs are not rendered by default.

375 changes: 360 additions & 15 deletions assets/inject/renderer-inject.js

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/presets/happy-coding-dog/hero.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/presets/happy-coding-dog/preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/presets/happy-coding-dog/stamp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
78 changes: 78 additions & 0 deletions assets/presets/happy-coding-dog/theme.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"schemaVersion": 2,
"uuid": "preset-happy-coding-dog",
"id": "happy-coding-dog",
"version": "1.1.0",
"minEngineVersion": "2.0.0",
"name": "快乐码犬 Happy Coding Dog",
"description": "粗线条、亮色块与马克笔笔触组成的快乐狗狗手绘工作台。",
"tagline": "新的一天开始啦,今天要做什么?",
"tags": [
"狗狗",
"手绘",
"治愈",
"高饱和"
],
"hero": "hero-workspace-blended.png",
"preview": "preview.png",
"wallpaper": "wallpaper.png",
"stamp": "stamp.png",
"light": {
"background": "#fff5d8",
"panel": "#fffaf0",
"panelAlt": "#f4efd9",
"surface": "#ffffff",
"text": "#20201d",
"muted": "#776f62",
"border": "rgba(32, 32, 29, 0.24)",
"accent": "#087f98",
"accentAlt": "#23b7c9",
"secondary": "#f04f3d",
"highlight": "#f2be16"
},
"dark": {
"background": "#171b1c",
"panel": "#202627",
"panelAlt": "#293031",
"surface": "#242a2b",
"text": "#fff5d8",
"muted": "#bbb4a6",
"border": "rgba(255, 245, 216, 0.19)",
"accent": "#38c5d3",
"accentAlt": "#77d9df",
"secondary": "#ff7261",
"highlight": "#ffd84a"
},
"layout": "dream-banner",
"heroFit": "cover",
"heroFocusX": 0.62,
"heroFocusY": 0.5,
"heroZoom": 1,
"heroHeight": 360,
"heroTextAlign": "left",
"heroScrim": 0.16,
"wallpaperEnabled": true,
"wallpaperFocusX": 0.5,
"wallpaperFocusY": 0.5,
"wallpaperOpacity": 0.45,
"wallpaperBlur": 0,
"radius": "xl",
"density": "spacious",
"fontPreset": "rounded",
"glass": false,
"shadow": "lg",
"decoration": 0.86,
"effects": {
"particles": 0.16,
"aurora": 0,
"glow": 0.06,
"noise": 0.08,
"grid": 0,
"float": 0.12
},
"brandSubtitle": "HAPPY CODING",
"projectPrefix": "快乐任务 · ",
"projectLabel": "选择今天的任务",
"statusText": "TAIL WAGGING · ONLINE",
"quote": "人生是旷野,没矿不敢野"
}
Binary file added assets/presets/happy-coding-dog/wallpaper.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/presets/yan-companion/motion-poster.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/presets/yan-companion/motion.mp4
Binary file not shown.
Binary file added assets/presets/yan-companion/preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions assets/presets/yan-companion/theme.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"schemaVersion": 2,
"uuid": "preset-yan-companion",
"id": "yan-companion",
"version": "1.0.0",
"minEngineVersion": "1.1.0",
"galleryVisible": true,
"name": "沉浸伴生 Yan Companion",
"description": "右侧动态人物、深夜电影光效与左侧玻璃对话层组成的沉浸式 Codex 工作台。",
"tagline": "让她陪你把想法写成结果。",
"tags": [
"动态",
"陪伴",
"电影感",
"沉浸"
],
"hero": "motion-poster.webp",
"preview": "preview.png",
"motionBackground": "motion.mp4",
"motionPoster": "motion-poster.webp",
"light": {
"background": "#03070a",
"panel": "#071014",
"panelAlt": "#0a171b",
"surface": "#071014",
"text": "#edf8f6",
"muted": "#94aaa7",
"border": "rgba(139, 213, 202, 0.2)",
"accent": "#65bfae",
"accentAlt": "#8be0d2",
"secondary": "#bf8f78",
"highlight": "#4c8d86"
},
"dark": {
"background": "#020608",
"panel": "#061014",
"panelAlt": "#09171b",
"surface": "#071216",
"text": "#edf8f6",
"muted": "#94aaa7",
"border": "rgba(139, 213, 202, 0.2)",
"accent": "#65bfae",
"accentAlt": "#8be0d2",
"secondary": "#bf8f78",
"highlight": "#4c8d86"
},
"layout": "cinematic-live",
"heroFit": "cover",
"heroFocusX": 0.72,
"heroFocusY": 0.5,
"heroZoom": 1,
"heroHeight": 340,
"heroTextAlign": "left",
"heroScrim": 0.24,
"wallpaperEnabled": false,
"wallpaperFocusX": 0.5,
"wallpaperFocusY": 0.5,
"wallpaperOpacity": 0,
"wallpaperBlur": 0,
"radius": "md",
"density": "normal",
"fontPreset": "system",
"glass": true,
"shadow": "lg",
"decoration": 0.3,
"effects": {
"particles": 0,
"aurora": 0,
"glow": 0.18,
"noise": 0.08,
"grid": 0,
"float": 0
},
"brandSubtitle": "YAN COMPANION",
"projectPrefix": "当前项目 · ",
"projectLabel": "选择工作项目",
"statusText": "OBJECT LIVE · 伴生在线",
"quote": "把想法告诉我,我们一起推进"
}
14 changes: 14 additions & 0 deletions electron/controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import assert from "node:assert/strict";
import test from "node:test";
import { canAttemptAutoApply, shouldStopStaleWatcher } from "./controller";

test("a running watcher cannot block auto-apply after its CDP endpoint disappears", () => {
assert.equal(shouldStopStaleWatcher(true, false), true);
assert.equal(shouldStopStaleWatcher(true, true), false);
assert.equal(shouldStopStaleWatcher(false, false), false);
});

test("auto-apply cannot restart Codex again before the user ends the current run", () => {
assert.equal(canAttemptAutoApply(false), true);
assert.equal(canAttemptAutoApply(true), false);
});
48 changes: 44 additions & 4 deletions electron/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import { EventEmitter } from "node:events";
import fs from "node:fs/promises";
import type { AppState, ApplyResult, ApplyStatus, CodexCliStatus, LayoutKind, LogLine } from "./shared/types";
import type { AppState, ApplyResult, ApplyStatus, ApplyThemeOptions, CodexCliStatus, LayoutKind, LogLine, ThemeSource } from "./shared/types";
import type { AppPaths } from "./paths";
import { PREFERRED_CDP_PORT, SKIN_VERSION } from "./engine/constants";
import { buildPayload } from "./engine/payload";
Expand Down Expand Up @@ -48,6 +48,7 @@ import type {
interface PersistedState {
activeThemeId: string | null;
activeThemeName: string | null;
activeThemeSource: ThemeSource | null;
activeLayout: LayoutKind | null;
cdpPort: number | null;
appliedAt: string | null;
Expand All @@ -61,11 +62,25 @@ const AUTO_APPLY_SETTLE_MS = 4_000;
const EMPTY_PERSISTED: PersistedState = {
activeThemeId: null,
activeThemeName: null,
activeThemeSource: null,
activeLayout: null,
cdpPort: null,
appliedAt: null,
};

function isThemeSource(value: unknown): value is ThemeSource {
return value === "preset" || value === "custom" || value === "imported" || value === "purchased";
}

export function shouldStopStaleWatcher(watcherActive: boolean, cdpHealthy: boolean): boolean {
return watcherActive && !cdpHealthy;
}

/** Allow at most one automatic restart during one observed Codex run. */
export function canAttemptAutoApply(restartBlocked: boolean): boolean {
return !restartBlocked;
}

/**
* Keep local filesystem paths and Codex thread/turn identifiers in the main
* process. Renderer consumers only need stable UI ids and preview URLs.
Expand Down Expand Up @@ -108,6 +123,7 @@ export class ThemeController extends EventEmitter {
private codexRunning = false;
private cdpHealthy = false;
private autoApplyRunningSince: number | null = null;
private autoApplyRestartBlocked = false;
private lastAutoApplyAt = 0;
private cliService: CodexCliStatusService;
private jobService: AiThemeJobService;
Expand Down Expand Up @@ -149,6 +165,7 @@ export class ThemeController extends EventEmitter {
try {
const dir = await this.store.resolveThemeDir(activeThemeId, {
preferPurchased: true,
preferredSource: this.persisted.activeThemeSource ?? undefined,
allowCatalogOnly: false,
});
if (!dir) throw new Error("theme directory is gone");
Expand Down Expand Up @@ -275,12 +292,25 @@ export class ThemeController extends EventEmitter {
*/
async tick(): Promise<void> {
await this.refreshStatus();
if (!this.applying && !this.codexRunning) {
// A real user-driven quit ends the current run. The next launch may make
// one fresh recovery attempt, but a failed recovery cannot loop forever.
this.autoApplyRestartBlocked = false;
}
if (shouldStopStaleWatcher(this.watcher?.active ?? false, this.cdpHealthy)) {
this.log("warn", "Codex 调试端口已失效,正在停止旧注入守护并准备恢复主题。");
await this.watcher?.stop();
this.watcher = null;
this.emitState();
}
if (!this.settings?.current.autoApply) {
this.autoApplyRunningSince = null;
this.autoApplyRestartBlocked = false;
return;
}
const { activeThemeId } = this.persisted;
if (!activeThemeId || this.applying || this.watcher?.active) return;
if (!canAttemptAutoApply(this.autoApplyRestartBlocked)) return;
if (!this.install || !this.codexRunning || this.cdpHealthy) {
this.autoApplyRunningSince = null;
return;
Expand All @@ -297,9 +327,15 @@ export class ThemeController extends EventEmitter {
this.lastAutoApplyAt = Date.now();
this.autoApplyRunningSince = null;
this.log("info", "检测到 Codex 未带调试端口启动,按偏好设置自动恢复主题…");
const result = await this.applyTheme(activeThemeId, { confirmRestart: true });
const result = await this.applyTheme(activeThemeId, {
confirmRestart: true,
source: this.persisted.activeThemeSource ?? undefined,
});
// Whether the launch failed outright or CDP disappeared again shortly
// afterwards, do not restart this same observed Codex run a second time.
this.autoApplyRestartBlocked = true;
if (!result.ok) {
this.log("warn", `自动应用失败(将在冷却期后重试):${result.error ?? "未知错误"}`);
this.log("warn", `自动应用失败(本次 Codex 运行不再自动重试):${result.error ?? "未知错误"}`);
}
}

Expand Down Expand Up @@ -337,7 +373,7 @@ export class ThemeController extends EventEmitter {

// ----------------------------------------------------------------- apply

async applyTheme(themeId: string, opts: { confirmRestart?: boolean } = {}): Promise<ApplyResult> {
async applyTheme(themeId: string, opts: ApplyThemeOptions = {}): Promise<ApplyResult> {
if (this.applying) {
return this.result("failed", false, false, [], "另一个主题正在应用中,请稍候。");
}
Expand All @@ -360,6 +396,7 @@ export class ThemeController extends EventEmitter {
}
const dir = await this.store.resolveThemeDir(themeId, {
preferPurchased: true,
preferredSource: opts.source,
allowCatalogOnly: false,
});
if (!dir) {
Expand Down Expand Up @@ -433,6 +470,7 @@ export class ThemeController extends EventEmitter {
// 4) Persist, then soft-verify against a live session.
this.persisted.activeThemeId = themeId;
this.persisted.activeThemeName = themeName;
this.persisted.activeThemeSource = opts.source ?? null;
this.persisted.activeLayout = themeLayout;
this.persisted.appliedAt = new Date().toISOString();
await this.writePersisted();
Expand Down Expand Up @@ -489,6 +527,7 @@ export class ThemeController extends EventEmitter {
await restoreAppearanceKeys(this.paths.codexConfigPath, this.paths.configBackupFile);
this.persisted.activeThemeId = null;
this.persisted.activeThemeName = null;
this.persisted.activeThemeSource = null;
this.persisted.activeLayout = null;
this.persisted.appliedAt = null;
await this.writePersisted();
Expand Down Expand Up @@ -556,6 +595,7 @@ export class ThemeController extends EventEmitter {
return {
activeThemeId: typeof raw?.activeThemeId === "string" ? raw.activeThemeId : null,
activeThemeName: typeof raw?.activeThemeName === "string" ? raw.activeThemeName : null,
activeThemeSource: isThemeSource(raw?.activeThemeSource) ? raw.activeThemeSource : null,
activeLayout: typeof raw?.activeLayout === "string" ? raw.activeLayout : null,
cdpPort: Number.isInteger(raw?.cdpPort) ? raw.cdpPort : null,
appliedAt: typeof raw?.appliedAt === "string" ? raw.appliedAt : null,
Expand Down
2 changes: 1 addition & 1 deletion electron/engine/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

/** Bumped whenever the injected payload format changes. */
export const SKIN_VERSION = "1.2.0";
export const SKIN_VERSION = "1.2.4";

export const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]"]);

Expand Down
2 changes: 2 additions & 0 deletions electron/engine/home-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface HomeSurfaceSignals {
connected: boolean;
rendered: boolean;
visibleThemeHome?: boolean;
visibleWelcome?: boolean;
visibleGameSource: boolean;
visibleSuggestions: boolean;
visibleTaskContent: boolean;
Expand All @@ -20,6 +21,7 @@ export function isActiveHomeSurface(signals: HomeSurfaceSignals): boolean {
signals.rendered &&
!signals.visibleTaskContent &&
(signals.visibleThemeHome === true ||
signals.visibleWelcome === true ||
signals.visibleGameSource ||
signals.visibleSuggestions);
}
39 changes: 39 additions & 0 deletions electron/engine/package-safety.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,45 @@ describe("ThemeStore package inspection", () => {
assert.equal(resolved, purchasedDir);
});

it("honors the selected library source when duplicate theme ids exist", async () => {
const presetDir = path.join(root, "presets", "source-collision");
const purchasedDir = path.join(purchasedRoot, "source-collision");
const baseManifest = JSON.parse(themeJson) as Record<string, unknown>;
await Promise.all([
fs.mkdir(presetDir, { recursive: true }),
fs.mkdir(purchasedDir, { recursive: true }),
]);
await Promise.all([
fs.writeFile(path.join(presetDir, "theme.json"), JSON.stringify({
...baseManifest,
uuid: "550e8400-e29b-41d4-a716-446655440011",
id: "source-collision",
name: "Preset Theme",
})),
fs.writeFile(path.join(presetDir, "hero.png"), heroPng),
fs.writeFile(path.join(purchasedDir, "theme.json"), JSON.stringify({
...baseManifest,
uuid: "550e8400-e29b-41d4-a716-446655440012",
id: "source-collision",
name: "Purchased Theme",
})),
fs.writeFile(path.join(purchasedDir, "hero.png"), heroPng),
]);

const preset = await store.resolveThemeDir("source-collision", {
preferPurchased: true,
preferredSource: "preset",
allowCatalogOnly: false,
});
const purchased = await store.resolveThemeDir("source-collision", {
preferPurchased: true,
preferredSource: "purchased",
allowCatalogOnly: false,
});
assert.equal(preset, presetDir);
assert.equal(purchased, purchasedDir);
});

it("never resolves a catalog-only placeholder as an applicable theme", async () => {
const placeholderDir = path.join(root, "presets", "preview-only");
const baseManifest = JSON.parse(themeJson) as Record<string, unknown>;
Expand Down
Loading