Skip to content

Refactor extension state and UI surfaces - #1

Merged
zh30 merged 1 commit into
mainfrom
codex/lightweight-extension-framework
Jun 22, 2026
Merged

Refactor extension state and UI surfaces#1
zh30 merged 1 commit into
mainfrom
codex/lightweight-extension-framework

Conversation

@zh30

@zh30 zh30 commented Jun 22, 2026

Copy link
Copy Markdown
Owner

No description provided.

@changeset-bot

changeset-bot Bot commented Jun 22, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 48cba81

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@zh30
zh30 merged commit 77df000 into main Jun 22, 2026
1 check passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request transitions the project from 'Tiny Helmet' to 'CRXKit', introducing a config-driven architecture powered by a new extension.config.json file and a project-local CLI for validation, scaffolding, and packaging. The codebase has been refactored to support this structure, alongside the addition of Vitest unit tests and Playwright E2E tests. Feedback on the changes highlights a potential race condition in the background script's settings hydration, robustness issues in CLI argument parsing when flags precede positional arguments, a redundant ternary operator in the CLI script, and unsafe nested property access in the options page.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +73 to +87
let cachedSettings = copySettings(config.defaultSettings);
let hydrating = false;

async function hydrateSettings() {
if (hydrating) {
return;
}

try {
hydrating = true;
cachedSettings = copySettings(await loadSettings());
} finally {
hydrating = false;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a potential race condition in hydrateSettings. If multiple events (like tab updates or activations) trigger hydration concurrently, subsequent callers will return undefined immediately while hydrating is true, proceeding to syncSidePanel before the settings have actually finished loading. Using a promise-coalescing pattern resolves this issue by returning the active hydration promise to all concurrent callers.

  let cachedSettings = copySettings(config.defaultSettings);
  let hydrationPromise: Promise<void> | null = null;

  function hydrateSettings(): Promise<void> {
    if (hydrationPromise) {
      return hydrationPromise;
    }

    hydrationPromise = (async () => {
      try {
        cachedSettings = copySettings(await loadSettings());
      } finally {
        hydrationPromise = null;
      }
    })();

    return hydrationPromise;
  }

Comment thread scripts/lib/cli.mjs

const entryDir = path.join('src/entries', name);
const input = `${entryDir}/main.${PAGE_KINDS.has(kind) ? 'tsx' : 'ts'}`;
const output = kind === 'background' ? `${name}.js` : `${name}.js`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ternary operator here is redundant because both branches return the exact same string literal. It can be simplified directly.

Suggested change
const output = kind === 'background' ? `${name}.js` : `${name}.js`;
const output = `${name}.js`;

Comment thread scripts/lib/cli.mjs
Comment on lines +195 to +198
async function addEntry(cwd, args, io) {
const name = args[0];
const { flags } = parseFlags(args.slice(1));
const kind = flags.kind;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Extracting the positional argument directly from args[0] before parsing flags assumes that the name is always the first argument. If a user passes flags before the name (e.g., crx entry add --kind page demo), the flag will be incorrectly treated as the entry name. Parsing the entire args array first is more robust.

Suggested change
async function addEntry(cwd, args, io) {
const name = args[0];
const { flags } = parseFlags(args.slice(1));
const kind = flags.kind;
async function addEntry(cwd, args, io) {
const { flags, positional } = parseFlags(args);
const name = positional[0];
const kind = flags.kind;

Comment thread scripts/lib/cli.mjs
Comment on lines +319 to +321
async function createProject(cwd, args, io) {
const target = args[0];
const { flags } = parseFlags(args.slice(1));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Extracting the target directory directly from args[0] before parsing flags assumes that the target is always the first argument. If a user passes flags before the target (e.g., crx create --package com.example my-app), the flag will be incorrectly treated as the target directory. Parsing the entire args array first is more robust.

Suggested change
async function createProject(cwd, args, io) {
const target = args[0];
const { flags } = parseFlags(args.slice(1));
async function createProject(cwd, args, io) {
const { flags, positional } = parseFlags(args);
const target = positional[0];

Comment on lines 92 to 107
<button
type="button"
key={section.id}
onClick={() => setActiveTab(section.id)}
onClick={() => setSidePanelAutoOpen(!settings.sidePanel.autoOpen)}
className={clsx(
'flex w-full items-center gap-3 rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200',
activeTab === section.id
? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20 scale-[1.02]'
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors',
settings.sidePanel.autoOpen ? 'bg-primary' : 'bg-muted'
)}
aria-pressed={settings.sidePanel.autoOpen}
>
<section.icon className="h-4 w-4" />
{section.label}
<span
className={clsx(
'block h-5 w-5 rounded-full bg-background shadow transition-transform',
settings.sidePanel.autoOpen ? 'translate-x-5' : 'translate-x-1'
)}
/>
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Directly accessing nested properties on settings.sidePanel without optional chaining or fallback guards can cause runtime crashes if the settings object is partially hydrated or if backward compatibility issues arise (e.g., missing the sidePanel property). Adding optional chaining and a fallback value ensures defensive programming.

Suggested change
<button
type="button"
key={section.id}
onClick={() => setActiveTab(section.id)}
onClick={() => setSidePanelAutoOpen(!settings.sidePanel.autoOpen)}
className={clsx(
'flex w-full items-center gap-3 rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200',
activeTab === section.id
? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20 scale-[1.02]'
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors',
settings.sidePanel.autoOpen ? 'bg-primary' : 'bg-muted'
)}
aria-pressed={settings.sidePanel.autoOpen}
>
<section.icon className="h-4 w-4" />
{section.label}
<span
className={clsx(
'block h-5 w-5 rounded-full bg-background shadow transition-transform',
settings.sidePanel.autoOpen ? 'translate-x-5' : 'translate-x-1'
)}
/>
</button>
<button
type="button"
onClick={() => setSidePanelAutoOpen(!(settings.sidePanel?.autoOpen ?? true))}
className={clsx(
'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors',
(settings.sidePanel?.autoOpen ?? true) ? 'bg-primary' : 'bg-muted'
)}
aria-pressed={settings.sidePanel?.autoOpen ?? true}
>
<span
className={clsx(
'block h-5 w-5 rounded-full bg-background shadow transition-transform',
(settings.sidePanel?.autoOpen ?? true) ? 'translate-x-5' : 'translate-x-1'
)}
/>
</button>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant