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
86 changes: 72 additions & 14 deletions packages/installer/assets/runtime/preload.js

Large diffs are not rendered by default.

62 changes: 49 additions & 13 deletions packages/installer/assets/runtime/preload/settings-injector.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type InjectedSettingsGroupKind = "native-nav-header" | "nav-group" | "pages-group";
export interface InjectedSettingsGroupCandidate<T extends object> {
group: T;
kind: InjectedSettingsGroupKind;
parent: T | null;
validPlacement: boolean;
current: boolean;
}
export declare function selectInjectedSettingsGroupsToRemove<T extends object>(candidates: readonly InjectedSettingsGroupCandidate<T>[]): T[];

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 63 additions & 12 deletions packages/runtime/src/preload/settings-injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ import {
type TweakStoreEntry,
type TweakStorePublishSubmission,
} from "../tweak-store";
import {
selectInjectedSettingsGroupsToRemove,
type InjectedSettingsGroupCandidate,
type InjectedSettingsGroupKind,
} from "./settings-sidebar-dedupe";

const CODEX_PLUSPLUS_RELEASES_URL = "https://github.com/b-nnett/codex-plusplus/releases";

Expand Down Expand Up @@ -744,23 +749,36 @@ function syncPagesGroup(): void {
const desiredKey = pages.length === 0
? "EMPTY"
: pages.map((p) => `${p.id}|${p.page.title}|${p.page.iconSvg ?? ""}`).join("\n");
const groupAttached = !!state.pagesGroup && outer.contains(state.pagesGroup);
if (state.pagesGroupKey === desiredKey && (pages.length === 0 ? !groupAttached : groupAttached)) {
const previousPagesGroup = state.pagesGroup;
const attachedPagesGroup =
state.pagesGroup && outer.contains(state.pagesGroup)
? state.pagesGroup
: findExistingPagesGroup(outer);
const adoptedPagesGroup = !!attachedPagesGroup && previousPagesGroup !== attachedPagesGroup;
if (adoptedPagesGroup) {
state.pagesGroup = attachedPagesGroup;
}
const groupAttached = !!attachedPagesGroup && outer.contains(attachedPagesGroup);
if (
!adoptedPagesGroup &&
state.pagesGroupKey === desiredKey &&
(pages.length === 0 ? !groupAttached : groupAttached)
) {
return;
}

if (pages.length === 0) {
if (state.pagesGroup) {
state.pagesGroup.remove();
if (attachedPagesGroup) {
attachedPagesGroup.remove();
state.pagesGroup = null;
}
for (const p of state.pages.values()) p.navButton = null;
state.pagesGroupKey = desiredKey;
return;
}

let group = state.pagesGroup;
if (!group || !outer.contains(group)) {
let group = attachedPagesGroup;
if (!group) {
group = document.createElement("div");
group.dataset.codexpp = "pages-group";
group.className = "flex flex-col gap-px";
Expand Down Expand Up @@ -793,6 +811,13 @@ function syncPagesGroup(): void {
setNavActive(state.activePage);
}

function findExistingPagesGroup(outer: HTMLElement): HTMLElement | null {
return (
outer.querySelector<HTMLElement>(':scope > [data-codexpp="pages-group"]') ??
outer.querySelector<HTMLElement>('[data-codexpp="pages-group"]')
);
}

function makeSidebarItem(label: string, iconSvg: string): HTMLButtonElement {
// Class string copied verbatim from Codex's sidebar buttons (General etc).
const btn = document.createElement("button");
Expand Down Expand Up @@ -3004,24 +3029,50 @@ function removeMisplacedSettingsGroups(): void {
const groups = document.querySelectorAll<HTMLElement>(
"[data-codexpp='nav-group'], [data-codexpp='pages-group'], [data-codexpp='native-nav-header']",
);
for (const group of Array.from(groups)) {
if (isCodexPpInjectedSettingsGroupPlacementValid(group)) continue;
const groupsToRemove = selectInjectedSettingsGroupsToRemove(
Array.from(groups).map((group): InjectedSettingsGroupCandidate<HTMLElement> => {
const sidebar = codexPpInjectedSettingsGroupSidebar(group);
return {
group,
kind: codexPpInjectedSettingsGroupKind(group),
parent: sidebar,
validPlacement: sidebar !== null,
current: isCurrentCodexPpInjectedSettingsGroup(group),
};
}),
);

for (const group of groupsToRemove) {
resetCodexPpInjectedSettingsGroupState(group);
group.remove();
}
}

function codexPpInjectedSettingsGroupKind(group: HTMLElement): InjectedSettingsGroupKind {
if (group.dataset.codexpp === "pages-group") return "pages-group";
if (group.dataset.codexpp === "native-nav-header") return "native-nav-header";
return "nav-group";
}

function isCurrentCodexPpInjectedSettingsGroup(group: HTMLElement): boolean {
return group === state.navGroup || group === state.pagesGroup || group === state.nativeNavHeader;
}

function isCodexPpInjectedSettingsGroupPlacementValid(group: HTMLElement): boolean {
if (isForbiddenSettingsSidebarSurface(group)) return false;
return codexPpInjectedSettingsGroupSidebar(group) !== null;
}

function codexPpInjectedSettingsGroupSidebar(group: HTMLElement): HTMLElement | null {
if (isForbiddenSettingsSidebarSurface(group)) return null;

let node = group.parentElement;
for (let depth = 0; node && depth < 4; depth++) {
if (isForbiddenSettingsSidebarSurface(node)) return false;
if (isSettingsSidebarCandidate(node)) return true;
if (isForbiddenSettingsSidebarSurface(node)) return null;
if (isSettingsSidebarCandidate(node)) return node;
node = node.parentElement;
}

return false;
return null;
}

function resetCodexPpInjectedSettingsGroupState(group: HTMLElement): void {
Expand Down
50 changes: 50 additions & 0 deletions packages/runtime/src/preload/settings-sidebar-dedupe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export type InjectedSettingsGroupKind =
| "native-nav-header"
| "nav-group"
| "pages-group";

export interface InjectedSettingsGroupCandidate<T extends object> {
group: T;
kind: InjectedSettingsGroupKind;
parent: T | null;
validPlacement: boolean;
current: boolean;
}

export function selectInjectedSettingsGroupsToRemove<T extends object>(
candidates: readonly InjectedSettingsGroupCandidate<T>[],
): T[] {
const remove = new Set<T>();
const groupsByParentAndKind = new Map<T, Map<InjectedSettingsGroupKind, InjectedSettingsGroupCandidate<T>[]>>();

for (const candidate of candidates) {
if (!candidate.validPlacement || !candidate.parent) {
remove.add(candidate.group);
continue;
}

let groupsByKind = groupsByParentAndKind.get(candidate.parent);
if (!groupsByKind) {
groupsByKind = new Map();
groupsByParentAndKind.set(candidate.parent, groupsByKind);
}

const repeatedGroups = groupsByKind.get(candidate.kind) ?? [];
repeatedGroups.push(candidate);
groupsByKind.set(candidate.kind, repeatedGroups);
}

for (const groupsByKind of groupsByParentAndKind.values()) {
for (const repeatedGroups of groupsByKind.values()) {
if (repeatedGroups.length <= 1) continue;
const keep = repeatedGroups.find((candidate) => candidate.current) ?? repeatedGroups[0]!;
for (const candidate of repeatedGroups) {
if (candidate !== keep) remove.add(candidate.group);
}
}
}

return candidates
.map((candidate) => candidate.group)
.filter((group) => remove.has(group));
}
Loading