codex-plus-patcher 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,153 @@
1
+ (function () {
2
+ const CodexPlus = window.CodexPlus;
3
+ const STORAGE_KEY = "codex-plus:project-colors-enabled";
4
+ const EVENT = "codex-plus:project-colors-change";
5
+ const palette = [
6
+ ["#5b8ff9", "#dbeafe", "#1d4ed8", "#f8fbff"], ["#61dDAA", "#dcfce7", "#15803d", "#f7fff9"],
7
+ ["#65789b", "#e0e7ff", "#4338ca", "#f8faff"], ["#f6bd16", "#fef3c7", "#b45309", "#fffdf5"],
8
+ ["#7262fd", "#ede9fe", "#6d28d9", "#fbf8ff"], ["#78d3f8", "#e0f2fe", "#0369a1", "#f5fcff"],
9
+ ["#9661bc", "#f3e8ff", "#7e22ce", "#fdf7ff"], ["#f6903d", "#ffedd5", "#c2410c", "#fff9f4"],
10
+ ["#008685", "#ccfbf1", "#0f766e", "#f5fffd"], ["#f08bb4", "#fce7f3", "#be185d", "#fff7fb"],
11
+ ["#6dc8ec", "#e0f7ff", "#0e7490", "#f5fdff"], ["#8d70f8", "#ede9fe", "#5b21b6", "#faf8ff"],
12
+ ["#c2c8d5", "#e5e7eb", "#4b5563", "#fbfbfc"], ["#ff9d4d", "#fee2e2", "#b91c1c", "#fff7f7"],
13
+ ["#269a99", "#d1fae5", "#047857", "#f6fffb"], ["#ff99c3", "#fce7f3", "#be123c", "#fff8fb"],
14
+ ["#4c78a8", "#dbeafe", "#1e40af", "#f8fbff"], ["#72b7b2", "#ccfbf1", "#0f766e", "#f5fffd"],
15
+ ["#54a24b", "#dcfce7", "#166534", "#f7fff7"], ["#eeca3b", "#fef9c3", "#a16207", "#fffdf2"],
16
+ ["#b279a2", "#fce7f3", "#9d174d", "#fff7fb"], ["#ff9da6", "#ffe4e6", "#be123c", "#fff7f8"],
17
+ ["#9d755d", "#ffedd5", "#9a3412", "#fff9f4"], ["#bab0ac", "#e7e5e4", "#57534e", "#fbfaf9"],
18
+ ["#7f7f7f", "#e5e7eb", "#374151", "#fafafa"], ["#bcbd22", "#fef9c3", "#854d0e", "#fffdf2"],
19
+ ["#17becf", "#cffafe", "#0e7490", "#f5feff"], ["#1f77b4", "#dbeafe", "#1d4ed8", "#f7fbff"],
20
+ ["#2ca02c", "#dcfce7", "#15803d", "#f7fff7"], ["#9467bd", "#f3e8ff", "#7e22ce", "#fcf7ff"],
21
+ ["#8c564b", "#fee2e2", "#991b1b", "#fff7f6"], ["#e377c2", "#fce7f3", "#be185d", "#fff8fb"],
22
+ ];
23
+
24
+ function readEnabled() {
25
+ try {
26
+ const stored = localStorage.getItem(STORAGE_KEY);
27
+ return stored == null ? true : stored === "true";
28
+ } catch {
29
+ return true;
30
+ }
31
+ }
32
+
33
+ function writeEnabled(value) {
34
+ try {
35
+ localStorage.setItem(STORAGE_KEY, value ? "true" : "false");
36
+ window.dispatchEvent(new CustomEvent(EVENT, { detail: { key: STORAGE_KEY, value } }));
37
+ } catch {}
38
+ }
39
+
40
+ function fnv1a32(value) {
41
+ let result = 0x811c9dc5;
42
+ for (const char of String(value || "")) result = Math.imul(result ^ char.charCodeAt(0), 0x01000193);
43
+ return result >>> 0;
44
+ }
45
+
46
+ function colorKey(project) {
47
+ if (project == null) return "";
48
+ if (typeof project === "string") return project.trim();
49
+ const id = project.projectId ?? project.id;
50
+ if (id != null && String(id).trim() !== "") return String(id).trim();
51
+ const host = project.hostId ?? project.host ?? project.remoteHostId ?? "local";
52
+ const path = project.path ?? project.cwd ?? project.projectPath ?? project.remotePath ?? project.root ?? project.workspaceRoot;
53
+ if (path != null && String(path).trim() !== "") return `${host}:${path}`;
54
+ return [project.label, project.name].filter(Boolean).join(":");
55
+ }
56
+
57
+ function colorFor(project) {
58
+ return palette[fnv1a32(colorKey(project)) % palette.length];
59
+ }
60
+
61
+ function style(project) {
62
+ const key = colorKey(project);
63
+ if (!readEnabled() || key.trim() === "") return undefined;
64
+ const [accent, bgLight, fgLight, softLight] = colorFor(key);
65
+ return {
66
+ "--codex-plus-project-accent": accent,
67
+ "--codex-plus-project-bg-light": bgLight,
68
+ "--codex-plus-project-fg-light": fgLight,
69
+ "--codex-plus-project-soft-light": softLight,
70
+ "--codex-plus-project-bg-dark": `color-mix(in srgb, ${accent} 24%, transparent)`,
71
+ "--codex-plus-project-fg-dark": "#f8fafc",
72
+ "--codex-plus-project-border-dark": `color-mix(in srgb, ${accent} 62%, transparent)`,
73
+ "--codex-plus-project-separator-light": "rgba(17,24,39,.24)",
74
+ "--codex-plus-project-separator-dark": "rgba(255,255,255,.34)",
75
+ borderLeft: `6px solid ${accent}`,
76
+ };
77
+ }
78
+
79
+ function dataAttributes(project, sidebar) {
80
+ const inlineStyle = style(project);
81
+ if (inlineStyle == null) return undefined;
82
+ return {
83
+ "data-codex-plus-project-color": "",
84
+ ...(sidebar ? { "data-codex-plus-project-sidebar-color": "" } : {}),
85
+ style: inlineStyle,
86
+ };
87
+ }
88
+
89
+ function renderToggleRow({ React, jsx, SettingRow, Switch, label, ariaLabel }) {
90
+ const [enabled, setEnabled] = React.useState(readEnabled);
91
+ React.useEffect(() => {
92
+ const listener = () => setEnabled(readEnabled());
93
+ window.addEventListener(EVENT, listener);
94
+ return () => window.removeEventListener(EVENT, listener);
95
+ }, []);
96
+ return jsx(SettingRow, {
97
+ control: jsx(Switch, {
98
+ checked: enabled,
99
+ onChange: (next) => {
100
+ setEnabled(next);
101
+ writeEnabled(next);
102
+ },
103
+ ariaLabel,
104
+ }),
105
+ label,
106
+ variant: "nested",
107
+ });
108
+ }
109
+
110
+ CodexPlus.registerPlugin(
111
+ CodexPlus.definePlugin({
112
+ id: "projectColors",
113
+ name: "Project Colors",
114
+ description: "Provides deterministic project accent colors across sidebar, messages, and composer surfaces.",
115
+ required: true,
116
+ styles:
117
+ ":root:not(.dark):not(.electron-dark) [data-codex-plus-project-sidebar-color]{border-radius:0;background-color:var(--codex-plus-project-soft-light);border-left-color:var(--codex-plus-project-accent)}" +
118
+ ":root.dark [data-codex-plus-project-sidebar-color],:root.electron-dark [data-codex-plus-project-sidebar-color]{border-radius:0;background-color:var(--codex-plus-project-bg-dark);border-left-color:var(--codex-plus-project-border-dark)}" +
119
+ ":root:not(.dark):not(.electron-dark) [data-codex-plus-project-color]{border-left-color:var(--codex-plus-project-accent)}" +
120
+ ":root.dark [data-codex-plus-project-color],:root.electron-dark [data-codex-plus-project-color]{border-left-color:var(--codex-plus-project-border-dark)}" +
121
+ ":root:not(.dark):not(.electron-dark) [data-codex-plus-project-color]:not([data-codex-plus-project-sidebar-color]){background-image:linear-gradient(to right,var(--codex-plus-project-separator-light),var(--codex-plus-project-separator-light));background-repeat:no-repeat;background-size:2px 100%;background-position:left top}" +
122
+ ":root.dark [data-codex-plus-project-color]:not([data-codex-plus-project-sidebar-color]),:root.electron-dark [data-codex-plus-project-color]:not([data-codex-plus-project-sidebar-color]){background-image:linear-gradient(to right,var(--codex-plus-project-separator-dark),var(--codex-plus-project-separator-dark));background-repeat:no-repeat;background-size:2px 100%;background-position:left top}",
123
+ exports: {
124
+ colorFor,
125
+ colorKey,
126
+ dataAttributes,
127
+ eventName: EVENT,
128
+ fnv1a32,
129
+ palette,
130
+ readEnabled,
131
+ renderToggleRow,
132
+ style,
133
+ writeEnabled,
134
+ },
135
+ start(api) {
136
+ api.ui.settings.appearance.addRow({
137
+ id: "codex-plus-project-colors",
138
+ order: 20,
139
+ plugin: "projectColors",
140
+ render: (deps) => renderToggleRow({
141
+ ...deps,
142
+ label: "Project colors",
143
+ ariaLabel: `${deps.variant || "Current"} project colors`,
144
+ }),
145
+ });
146
+ api.ui.sidebar.decorateProjectRow((props) => dataAttributes(props?.project, true));
147
+ api.ui.sidebar.decorateThreadRow((props) => dataAttributes(props?.project, true));
148
+ api.ui.message.decorateUserBubble((props) => dataAttributes(props?.project, false));
149
+ api.ui.composer.decorateSurface((props) => dataAttributes(props?.project, false));
150
+ },
151
+ }),
152
+ );
153
+ })();
@@ -0,0 +1,28 @@
1
+ (function () {
2
+ const CodexPlus = window.CodexPlus;
3
+ CodexPlus.registerPlugin(
4
+ CodexPlus.definePlugin({
5
+ id: "sidebarNameBlur",
6
+ name: "Sidebar Name Blur",
7
+ description: "Registers the session-only Toggle sidebar blur command.",
8
+ required: true,
9
+ styles: ':root[data-codex-plus-sidebar-names-blurred="true"] :is([data-thread-title],[data-codex-plus-sidebar-name]){filter:blur(4px);user-select:none}',
10
+ commands: [
11
+ {
12
+ id: "codexPlusToggleSidebarNameBlur",
13
+ title: "Toggle sidebar blur",
14
+ description: "Blur or show sidebar chat and project names",
15
+ menu: { groups: ["suggested", "panels"] },
16
+ palette: { enabled: true, keywords: ["privacy", "blur"] },
17
+ shortcut: { defaultKeybindings: [] },
18
+ run() {
19
+ const root = document.documentElement;
20
+ const enabled = root.getAttribute("data-codex-plus-sidebar-names-blurred") === "true";
21
+ if (enabled) root.removeAttribute("data-codex-plus-sidebar-names-blurred");
22
+ else root.setAttribute("data-codex-plus-sidebar-names-blurred", "true");
23
+ },
24
+ },
25
+ ],
26
+ }),
27
+ );
28
+ })();
@@ -0,0 +1,134 @@
1
+ (function () {
2
+ const CodexPlus = window.CodexPlus;
3
+ const STORAGE_KEY = "codex-plus:user-message-bubble-colors";
4
+ const EVENT = "codex-plus:user-message-bubble-colors-change";
5
+
6
+ function isColor(value) {
7
+ return typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value);
8
+ }
9
+
10
+ function defaultColor(variant) {
11
+ return variant === "dark" ? "#2f2f2f" : "#f2f2f2";
12
+ }
13
+
14
+ function isStoredColor(variant, value) {
15
+ return isColor(value) && value.toLowerCase() !== defaultColor(variant);
16
+ }
17
+
18
+ function readColors(emptyValue = null) {
19
+ try {
20
+ const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}") || {};
21
+ return {
22
+ light: isStoredColor("light", stored.light) ? stored.light : emptyValue,
23
+ dark: isStoredColor("dark", stored.dark) ? stored.dark : emptyValue,
24
+ };
25
+ } catch {
26
+ return { light: emptyValue, dark: emptyValue };
27
+ }
28
+ }
29
+
30
+ function writeColor(variant, value) {
31
+ const next = readColors(undefined);
32
+ if (isStoredColor(variant, value)) next[variant] = value;
33
+ else delete next[variant];
34
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
35
+ window.dispatchEvent(new CustomEvent(EVENT, { detail: next }));
36
+ }
37
+
38
+ function textColor(background) {
39
+ const red = parseInt(background.slice(1, 3), 16);
40
+ const green = parseInt(background.slice(3, 5), 16);
41
+ const blue = parseInt(background.slice(5, 7), 16);
42
+ const channel = (value) => {
43
+ const normalized = value / 255;
44
+ return normalized <= 0.03928 ? normalized / 12.92 : Math.pow((normalized + 0.055) / 1.055, 2.4);
45
+ };
46
+ const luminance = 0.2126 * channel(red) + 0.7152 * channel(green) + 0.0722 * channel(blue);
47
+ const againstNearBlack = (luminance + 0.05) / (0.0056 + 0.05);
48
+ const againstBlack = (luminance + 0.05) / 0.05;
49
+ const againstWhite = 1.05 / (luminance + 0.05);
50
+ if (againstNearBlack >= 4.5 && againstNearBlack >= againstWhite) return "#111111";
51
+ return againstBlack >= againstWhite ? "#000000" : "#ffffff";
52
+ }
53
+
54
+ function setVars() {
55
+ const colors = readColors(null);
56
+ for (const variant of ["light", "dark"]) {
57
+ const color = colors[variant];
58
+ if (color == null) {
59
+ document.documentElement.style.removeProperty(`--codex-plus-user-bubble-${variant}-bg`);
60
+ document.documentElement.style.removeProperty(`--codex-plus-user-bubble-${variant}-fg`);
61
+ } else {
62
+ document.documentElement.style.setProperty(`--codex-plus-user-bubble-${variant}-bg`, color);
63
+ document.documentElement.style.setProperty(`--codex-plus-user-bubble-${variant}-fg`, textColor(color));
64
+ }
65
+ }
66
+ }
67
+
68
+ function renderColorRow({ React, jsx, SettingRow, ColorInput, variant, label, ariaLabel }) {
69
+ const [value, setValue] = React.useState(() => readColors("")[variant] || defaultColor(variant));
70
+ React.useEffect(() => {
71
+ const listener = () => setValue(readColors("")[variant] || defaultColor(variant));
72
+ window.addEventListener(EVENT, listener);
73
+ return () => window.removeEventListener(EVENT, listener);
74
+ }, [variant]);
75
+ return jsx(SettingRow, {
76
+ control: jsx(ColorInput, {
77
+ ariaLabel,
78
+ value,
79
+ onChange: (next) => {
80
+ setValue(next);
81
+ writeColor(variant, next);
82
+ },
83
+ }),
84
+ label,
85
+ variant: "nested",
86
+ });
87
+ }
88
+
89
+ CodexPlus.registerPlugin(
90
+ CodexPlus.definePlugin({
91
+ id: "userBubbleColors",
92
+ name: "User Bubble Colors",
93
+ description: "Manages user-message bubble color settings and CSS variables.",
94
+ required: true,
95
+ styles:
96
+ ':root:not(.dark):not(.electron-dark) :is([data-codex-plus-user-bubble],[data-codex-plus-user-entry]){background-color:var(--codex-plus-user-bubble-light-bg);color:var(--codex-plus-user-bubble-light-fg)}' +
97
+ ':root:not(.dark):not(.electron-dark) [data-codex-plus-user-entry] :is(.ProseMirror,.ProseMirror *,textarea,[contenteditable="true"],[data-placeholder]),:root:not(.dark):not(.electron-dark) [data-codex-plus-user-entry] :is(button:not([class*="bg-token-foreground"]),[role="button"]:not([class*="bg-token-foreground"]),button:not([class*="bg-token-foreground"]) svg,[role="button"]:not([class*="bg-token-foreground"]) svg,[class*="text-token-foreground"],[class*="text-token-description-foreground"],[class*="text-token-input-placeholder-foreground"],[class*="text-token-text-link-foreground"],[class*="text-token-editor-warning-foreground"]){color:var(--codex-plus-user-bubble-light-fg)}' +
98
+ ':root:not(.dark):not(.electron-dark) [data-codex-plus-user-entry] :is([data-placeholder],[class*="text-token-input-placeholder-foreground"])::before,:root:not(.dark):not(.electron-dark) [data-codex-plus-user-entry] :is([data-placeholder],[class*="text-token-input-placeholder-foreground"])::after,:root:not(.dark):not(.electron-dark) [data-codex-plus-user-entry] :is(input,textarea,[contenteditable="true"],[class*="placeholder:text-token-input-placeholder-foreground"])::placeholder{color:var(--codex-plus-user-bubble-light-fg)}' +
99
+ ':root.dark :is([data-codex-plus-user-bubble],[data-codex-plus-user-entry]),:root.electron-dark :is([data-codex-plus-user-bubble],[data-codex-plus-user-entry]){background-color:var(--codex-plus-user-bubble-dark-bg);color:var(--codex-plus-user-bubble-dark-fg)}' +
100
+ ':root.dark [data-codex-plus-user-entry] :is(.ProseMirror,.ProseMirror *,textarea,[contenteditable="true"],[data-placeholder]),:root.electron-dark [data-codex-plus-user-entry] :is(.ProseMirror,.ProseMirror *,textarea,[contenteditable="true"],[data-placeholder]),:root.dark [data-codex-plus-user-entry] :is(button:not([class*="bg-token-foreground"]),[role="button"]:not([class*="bg-token-foreground"]),button:not([class*="bg-token-foreground"]) svg,[role="button"]:not([class*="bg-token-foreground"]) svg,[class*="text-token-foreground"],[class*="text-token-description-foreground"],[class*="text-token-input-placeholder-foreground"],[class*="text-token-text-link-foreground"],[class*="text-token-editor-warning-foreground"]),:root.electron-dark [data-codex-plus-user-entry] :is(button:not([class*="bg-token-foreground"]),[role="button"]:not([class*="bg-token-foreground"]),button:not([class*="bg-token-foreground"]) svg,[role="button"]:not([class*="bg-token-foreground"]) svg,[class*="text-token-foreground"],[class*="text-token-description-foreground"],[class*="text-token-input-placeholder-foreground"],[class*="text-token-text-link-foreground"],[class*="text-token-editor-warning-foreground"]){color:var(--codex-plus-user-bubble-dark-fg)}' +
101
+ ':root.dark [data-codex-plus-user-entry] :is([data-placeholder],[class*="text-token-input-placeholder-foreground"])::before,:root.dark [data-codex-plus-user-entry] :is([data-placeholder],[class*="text-token-input-placeholder-foreground"])::after,:root.dark [data-codex-plus-user-entry] :is(input,textarea,[contenteditable="true"],[class*="placeholder:text-token-input-placeholder-foreground"])::placeholder,:root.electron-dark [data-codex-plus-user-entry] :is([data-placeholder],[class*="text-token-input-placeholder-foreground"])::before,:root.electron-dark [data-codex-plus-user-entry] :is([data-placeholder],[class*="text-token-input-placeholder-foreground"])::after,:root.electron-dark [data-codex-plus-user-entry] :is(input,textarea,[contenteditable="true"],[class*="placeholder:text-token-input-placeholder-foreground"])::placeholder{color:var(--codex-plus-user-bubble-dark-fg)}',
102
+ exports: {
103
+ defaultColor,
104
+ eventName: EVENT,
105
+ isColor,
106
+ isStoredColor,
107
+ readColors,
108
+ renderColorRow,
109
+ setVars,
110
+ textColor,
111
+ writeColor,
112
+ },
113
+ start(api) {
114
+ api.ui.settings.appearance.addRow({
115
+ id: "codex-plus-user-bubble-colors",
116
+ order: 10,
117
+ plugin: "userBubbleColors",
118
+ render: (deps) => renderColorRow({
119
+ ...deps,
120
+ label: "User bubble",
121
+ ariaLabel: `${deps.variant || "Current"} user message bubble color`,
122
+ }),
123
+ });
124
+ api.ui.message.decorateUserBubble(() => ({ "data-codex-plus-user-bubble": "" }));
125
+ api.ui.composer.decorateSurface(() => ({ "data-codex-plus-user-entry": "" }));
126
+ setVars();
127
+ window.addEventListener(EVENT, setVars);
128
+ },
129
+ stop() {
130
+ window.removeEventListener(EVENT, setVars);
131
+ },
132
+ }),
133
+ );
134
+ })();
@@ -0,0 +1,363 @@
1
+ (function () {
2
+ const globalObject = typeof window !== "undefined" ? window : globalThis;
3
+ const plugins = new Map();
4
+ const startedPlugins = new Set();
5
+ const hostModules = new Map();
6
+ const waiters = [];
7
+ const patchDescriptors = [];
8
+ const commands = new Map();
9
+ const styleElements = new Map();
10
+ const settingsListeners = new Map();
11
+ const storagePrefix = "codex-plus:plugin:";
12
+
13
+ function safeId(id) {
14
+ if (typeof id !== "string" || id.trim() === "") throw new Error("Codex Plus plugin ids must be non-empty strings");
15
+ return id.trim();
16
+ }
17
+
18
+ function definePlugin(definition) {
19
+ const id = safeId(definition.id || definition.name);
20
+ return { ...definition, id };
21
+ }
22
+
23
+ function notifyWaiters(name, value) {
24
+ for (let index = waiters.length - 1; index >= 0; index -= 1) {
25
+ const waiter = waiters[index];
26
+ if (!waiter.filter(value, name)) continue;
27
+ waiters.splice(index, 1);
28
+ waiter.resolve(value);
29
+ }
30
+ }
31
+
32
+ function registerHostModule(name, value) {
33
+ hostModules.set(name, value);
34
+ notifyWaiters(name, value);
35
+ return value;
36
+ }
37
+
38
+ function moduleValues() {
39
+ return Array.from(hostModules.values());
40
+ }
41
+
42
+ function findByProps(...props) {
43
+ return moduleValues().find((value) => value && props.every((prop) => prop in value));
44
+ }
45
+
46
+ function findByCode(text) {
47
+ return moduleValues().find((value) => {
48
+ try {
49
+ return String(value).includes(text);
50
+ } catch {
51
+ return false;
52
+ }
53
+ });
54
+ }
55
+
56
+ function findComponentByCode(text) {
57
+ return moduleValues().find((value) => {
58
+ try {
59
+ return typeof value === "function" && String(value).includes(text);
60
+ } catch {
61
+ return false;
62
+ }
63
+ });
64
+ }
65
+
66
+ function waitFor(filter) {
67
+ for (const [name, value] of hostModules) {
68
+ if (filter(value, name)) return Promise.resolve(value);
69
+ }
70
+ return new Promise((resolve) => waiters.push({ filter, resolve }));
71
+ }
72
+
73
+ function getPluginStore(pluginId) {
74
+ const key = `${storagePrefix}${pluginId}`;
75
+ try {
76
+ return JSON.parse(globalObject.localStorage?.getItem(key) || "{}") || {};
77
+ } catch {
78
+ return {};
79
+ }
80
+ }
81
+
82
+ function writePluginStore(pluginId, store) {
83
+ const key = `${storagePrefix}${pluginId}`;
84
+ globalObject.localStorage?.setItem(key, JSON.stringify(store));
85
+ }
86
+
87
+ function emitSetting(pluginId, key, value) {
88
+ const listenerKey = `${pluginId}:${key}`;
89
+ for (const listener of settingsListeners.get(listenerKey) || []) listener(value);
90
+ }
91
+
92
+ function defineSettings(pluginId, definitions) {
93
+ const id = safeId(pluginId);
94
+ const store = getPluginStore(id);
95
+ for (const [key, definition] of Object.entries(definitions || {})) {
96
+ if (!(key in store) && "default" in definition) store[key] = definition.default;
97
+ }
98
+ writePluginStore(id, store);
99
+ return {
100
+ definitions,
101
+ get(key) {
102
+ return getPluginStore(id)[key];
103
+ },
104
+ set(key, value) {
105
+ const next = getPluginStore(id);
106
+ next[key] = value;
107
+ writePluginStore(id, next);
108
+ emitSetting(id, key, value);
109
+ },
110
+ use(key, listener) {
111
+ const listenerKey = `${id}:${key}`;
112
+ const listeners = settingsListeners.get(listenerKey) || new Set();
113
+ listeners.add(listener);
114
+ settingsListeners.set(listenerKey, listeners);
115
+ listener(getPluginStore(id)[key]);
116
+ return () => listeners.delete(listener);
117
+ },
118
+ };
119
+ }
120
+
121
+ function registerPatch(descriptor) {
122
+ patchDescriptors.push(descriptor);
123
+ return descriptor;
124
+ }
125
+
126
+ function applyPatchDescriptors(source, descriptors = patchDescriptors) {
127
+ let output = source;
128
+ for (const descriptor of descriptors) {
129
+ const moduleMatches =
130
+ typeof descriptor.find === "string" ? output.includes(descriptor.find) : descriptor.find.test(output);
131
+ if (!moduleMatches) continue;
132
+ const replacements = Array.isArray(descriptor.replacement) ? descriptor.replacement : [descriptor.replacement];
133
+ const beforeGroup = output;
134
+ let appliedGroup = true;
135
+ for (const replacement of replacements) {
136
+ const before = output;
137
+ output = output.replace(replacement.match, replacement.replace);
138
+ if (before === output) appliedGroup = false;
139
+ }
140
+ if (descriptor.group && !appliedGroup) output = beforeGroup;
141
+ if (!descriptor.all) break;
142
+ }
143
+ return output;
144
+ }
145
+
146
+ function registerCommand(command) {
147
+ commands.set(safeId(command.id), command);
148
+ return command;
149
+ }
150
+
151
+ function runCommand(id, ...args) {
152
+ const command = commands.get(id);
153
+ if (!command) throw new Error(`Unknown Codex Plus command: ${id}`);
154
+ return command.run?.(...args);
155
+ }
156
+
157
+ function commandGroups(command) {
158
+ return command.menu?.groups || [];
159
+ }
160
+
161
+ function commandKeybindings(command) {
162
+ return command.shortcut?.defaultKeybindings || [];
163
+ }
164
+
165
+ function commandMetadata() {
166
+ return Array.from(commands.values())
167
+ .filter((command) => command.palette?.enabled !== false)
168
+ .map((command) => {
169
+ const groups = commandGroups(command);
170
+ return {
171
+ id: command.id,
172
+ title: command.title,
173
+ description: command.description,
174
+ menuGroups: groups,
175
+ defaultKeybindings: commandKeybindings(command),
176
+ commandMenuGroupKey: groups.includes("panels") ? "panels" : groups[0],
177
+ commandMenu: true,
178
+ electron: {
179
+ menuTitle: command.title,
180
+ defaultKeybindings: commandKeybindings(command),
181
+ },
182
+ };
183
+ });
184
+ }
185
+
186
+ function CommandMenuItemHost({ command, deps, group, close }) {
187
+ const jsx = deps?.jsx;
188
+ const MenuItem = deps?.MenuItem;
189
+ if (typeof jsx !== "function" || MenuItem == null) return null;
190
+ const render = (closeMenu) =>
191
+ jsx(
192
+ MenuItem,
193
+ {
194
+ value: command.title,
195
+ title: command.title,
196
+ description: command.description,
197
+ onSelect() {
198
+ runCommand(command.id);
199
+ closeMenu?.();
200
+ close?.();
201
+ },
202
+ },
203
+ command.id,
204
+ );
205
+ deps?.register?.(command.id, () => runCommand(command.id), {
206
+ menuItem: { id: command.id, groupKey: group, render },
207
+ });
208
+ return null;
209
+ }
210
+
211
+ function renderCommandMenuItems({ group, deps, close } = {}) {
212
+ const jsx = deps?.jsx;
213
+ if (typeof jsx !== "function") return [];
214
+ return Array.from(commands.values())
215
+ .filter((command) => commandGroups(command).includes(group))
216
+ .map((command) => jsx(CommandMenuItemHost, { command, deps, group, close }, command.id));
217
+ }
218
+
219
+ function mergeDataAttributes(base, extra) {
220
+ if (extra == null) return base;
221
+ if (base == null) return extra;
222
+ return { ...base, ...extra, style: { ...base.style, ...extra.style } };
223
+ }
224
+
225
+ function applyDecorators(props, decorators) {
226
+ let result;
227
+ for (const decorator of decorators) result = mergeDataAttributes(result, decorator(props));
228
+ return result;
229
+ }
230
+
231
+ function AppearanceRowHost({ row, deps, variant }) {
232
+ return row.render?.({ ...deps, variant, row }) ?? null;
233
+ }
234
+
235
+ function renderAppearanceRows({ deps, variant, section = "appearance" } = {}) {
236
+ const jsx = deps?.jsx;
237
+ if (typeof jsx !== "function") return [];
238
+ return CodexPlus.ui.settings.appearance.rows
239
+ .filter((row) => (row.section || "appearance") === section)
240
+ .slice()
241
+ .sort((left, right) => (left.order || 0) - (right.order || 0))
242
+ .map((row) => jsx(AppearanceRowHost, { row, deps, variant }, row.id));
243
+ }
244
+
245
+ function renderReviewBody({ props, deps, defaultBody } = {}) {
246
+ let body = defaultBody;
247
+ for (const wrapper of CodexPlus.ui.review.wrappers) {
248
+ body = wrapper({ ...props, mainReviewContent: body }, deps);
249
+ }
250
+ return body;
251
+ }
252
+
253
+ function renderErrorDetails({ jsx, error, componentStack } = {}) {
254
+ for (const decorator of CodexPlus.ui.errors.boundaryDecorators) {
255
+ const detail = decorator({ jsx, error, componentStack });
256
+ if (detail != null) return detail;
257
+ }
258
+ return null;
259
+ }
260
+
261
+ function registerStyle(pluginId, cssText) {
262
+ if (typeof document === "undefined") return null;
263
+ const id = `codex-plus-style-${safeId(pluginId)}`;
264
+ let element = styleElements.get(id) || document.getElementById(id);
265
+ if (!element) {
266
+ element = document.createElement("style");
267
+ element.id = id;
268
+ document.head?.appendChild(element);
269
+ }
270
+ element.textContent = cssText;
271
+ styleElements.set(id, element);
272
+ return element;
273
+ }
274
+
275
+ function setRootVars(vars) {
276
+ if (typeof document === "undefined") return;
277
+ for (const [key, value] of Object.entries(vars || {})) {
278
+ if (value == null) document.documentElement.style.removeProperty(key);
279
+ else document.documentElement.style.setProperty(key, value);
280
+ }
281
+ }
282
+
283
+ function registerPlugin(definition) {
284
+ const plugin = definePlugin(definition);
285
+ plugins.set(plugin.id, plugin);
286
+ if (plugin.settings) plugin.settingsStore = defineSettings(plugin.id, plugin.settings);
287
+ for (const descriptor of plugin.patches || []) registerPatch({ ...descriptor, plugin: plugin.id });
288
+ for (const command of plugin.commands || []) registerCommand({ ...command, plugin: plugin.id });
289
+ if (plugin.styles) registerStyle(plugin.id, plugin.styles);
290
+ if (plugin.required || plugin.enabledByDefault) startPlugin(plugin.id);
291
+ return plugin;
292
+ }
293
+
294
+ function startPlugin(id) {
295
+ const plugin = plugins.get(id);
296
+ if (!plugin || startedPlugins.has(id)) return;
297
+ plugin.start?.(CodexPlus);
298
+ startedPlugins.add(id);
299
+ }
300
+
301
+ function stopPlugin(id) {
302
+ const plugin = plugins.get(id);
303
+ if (!plugin || !startedPlugins.has(id)) return;
304
+ plugin.stop?.(CodexPlus);
305
+ startedPlugins.delete(id);
306
+ }
307
+
308
+ const CodexPlus = {
309
+ definePlugin,
310
+ registerPlugin,
311
+ startPlugin,
312
+ stopPlugin,
313
+ plugins,
314
+ patches: { register: registerPatch, apply: applyPatchDescriptors, all: patchDescriptors },
315
+ modules: { registerHostModule, findByCode, findByProps, findComponentByCode, waitFor },
316
+ ui: {
317
+ commands: { renderMenuItems: renderCommandMenuItems, commandMetadata },
318
+ settings: { appearance: { rows: [], addRow(row) { this.rows.push(row); return row; }, renderRows: renderAppearanceRows } },
319
+ review: { wrappers: [], wrapBody(wrapper) { this.wrappers.push(wrapper); return wrapper; }, renderBody: renderReviewBody },
320
+ sidebar: {
321
+ projectDecorators: [],
322
+ threadDecorators: [],
323
+ decorateProjectRow(fn) { this.projectDecorators.push(fn); return fn; },
324
+ decorateThreadRow(fn) { this.threadDecorators.push(fn); return fn; },
325
+ mergeDataAttributes,
326
+ projectRowProps(props) { return applyDecorators(props, this.projectDecorators); },
327
+ threadRowProps(props) { return applyDecorators(props, this.threadDecorators); },
328
+ },
329
+ message: { userBubbleDecorators: [], decorateUserBubble(fn) { this.userBubbleDecorators.push(fn); return fn; }, userBubbleProps(props) { return applyDecorators(props, this.userBubbleDecorators); } },
330
+ composer: { surfaceDecorators: [], decorateSurface(fn) { this.surfaceDecorators.push(fn); return fn; }, surfaceProps(props) { return applyDecorators(props, this.surfaceDecorators); } },
331
+ about: { buildInfo: [], addBuildInfo(fn) { this.buildInfo.push(fn); return fn; } },
332
+ errors: { boundaryDecorators: [], decorateBoundary(fn) { this.boundaryDecorators.push(fn); return fn; }, renderDetails: renderErrorDetails },
333
+ },
334
+ commands: { register: registerCommand, run: runCommand, all: () => Array.from(commands.values()), menuItems: (group) => Array.from(commands.values()).filter((command) => commandGroups(command).includes(group)) },
335
+ settings: { define: defineSettings },
336
+ native: { async request(method, params) { return globalObject.CodexPlusHost?.nativeRequest?.(method, params); } },
337
+ styles: { register: registerStyle, setRootVars },
338
+ };
339
+
340
+ globalObject.CodexPlus = CodexPlus;
341
+ globalObject.CodexPlusHost ||= {};
342
+ globalObject.CodexPlusHost.register = registerHostModule;
343
+
344
+ const pluginFiles = [
345
+ "plugins/aboutMetadata.js",
346
+ "plugins/nestedRepositories.js",
347
+ "plugins/diagnosticErrors.js",
348
+ "plugins/userBubbleColors.js",
349
+ "plugins/projectColors.js",
350
+ "plugins/sidebarNameBlur.js",
351
+ ];
352
+
353
+ if (typeof document !== "undefined") {
354
+ const base = new URL(".", document.currentScript?.src || globalObject.location?.href || "");
355
+ for (const file of pluginFiles) {
356
+ const script = document.createElement("script");
357
+ script.src = new URL(file, base).href;
358
+ script.async = false;
359
+ script.defer = false;
360
+ document.head?.appendChild(script);
361
+ }
362
+ }
363
+ })();