Skip to content

feat: rename Explore to ClawHub, swap tab order, add disclaimer#477

Merged
alchemistklk merged 13 commits intomainfrom
feat/clawhub-rename
Mar 24, 2026
Merged

feat: rename Explore to ClawHub, swap tab order, add disclaimer#477
alchemistklk merged 13 commits intomainfrom
feat/clawhub-rename

Conversation

@alchemistklk
Copy link
Copy Markdown
Contributor

@alchemistklk alchemistklk commented Mar 24, 2026

Summary

  • Swap Skills page tabs: Yours first, ClawHub (formerly Explore) second
  • Rename "Explore" → "ClawHub" in both en and zh-CN locales
  • Add always-visible ClawHub disclaimer with GitHub Issues link below category pills
  • Fix runtime plugin writer symlink crash (fs.cp EINVAL on .bin/ symlinks)
  • Fix import-skill-modal biome formatting

Test plan

  • Open Skills page — verify "Yours" tab appears first, "ClawHub" second
  • Verify ClawHub disclaimer text is visible on both tabs
  • Verify "GitHub Issues" link opens https://github.com/nexu-io/nexu/issues
  • Switch locale to zh-CN — verify Chinese disclaimer text
  • pnpm start — verify desktop boots without controller EINVAL crash
  • pnpm typecheck && pnpm lint && pnpm test — all pass

Summary by CodeRabbit

  • New Features

    • Added a ClawHub disclaimer section to the Skills page with external reference links
    • New feedback message displayed when skill imports fail
  • Localization

    • Extended Chinese language support with translations for new Skills page components and import status messages
  • UI/UX

    • Reorganized Skills page tab layout to prioritize personal skills section

On process restart, lastWrittenContent is null, causing the first write()
to always hit disk even if the config file already has identical content.
This triggers an unnecessary OpenClaw reload on every controller restart.

Read the existing file to seed the in-memory cache on the first write()
call, so cold starts skip the write when content matches.
Replace the inline hidden file input + button with a polished modal
dialog ported from the design-system. Two tabs: Upload Zip (wired to
existing zip import backend) and GitHub Link (placeholder, coming soon).
Adds Dialog UI component (Radix-based) and i18n keys for EN/ZH-CN.

Also fixes controller startup crash (EINVAL) when copying runtime plugin
dirs containing node_modules/.bin symlinks by filtering out symlinks
during the recursive copy.
…lter

Keep main's more targeted approach (dereference: true + skip .bin dirs)
instead of the broader all-symlink filter. Both fix the same EINVAL
crash but main's approach is more specific and preserves useful symlinks.
The modal state helpers (getSelectedZipFile, createAutoCloseController)
were extracted but not committed, breaking the build with TS2307.
The test expected non-.bin symlinks to be preserved as symlinks, but
the implementation uses dereference: true which materializes them into
real directories. Update the test to assert directory materialization
instead of symlink preservation.
Disable native link drag on SkillCard <Link> elements so that
dragging a card in Electron no longer triggers a new BrowserWindow.
The "All" sub-tab count was derived from yourSkillsList which is
filtered by the active sub-tab, so switching to "Installed" made
the "All" count drop to 0. Use installedSkills.length instead.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR updates the Skills page branding from "Explore" to "ClawHub" with corresponding UI reordering and disclaimer text, modifies the desktop build script to dynamically configure API endpoints using controller ports, refactors gateway synchronization startup logic, and refines plugin template materialization with symlink-aware filtering for .bin entries.

Changes

Cohort / File(s) Summary
Localization Updates
apps/web/src/i18n/locales/en.ts, apps/web/src/i18n/locales/zh-CN.ts
Rebranded Skills page "explore" label to "ClawHub", added disclaimer localization strings with punctuation, and added import failure status message (skills.importFailed).
Skills Page UI
apps/web/src/pages/skills.tsx
Reordered tabs so "yours" appears before "explore", added ClawHub disclaimer paragraph with external GitHub Issues link above the skill grid.
Desktop Build Configuration
apps/desktop/scripts/dist-mac.mjs
Imported resolveDistControllerPort and augmented build environment variables to point API/Auth URLs to computed controller port instead of using original environment.
Runtime Plugin Template Handling
apps/controller/src/runtime/openclaw-runtime-plugin-writer.ts
Changed copy filter from simple basename check to asynchronous symlink-aware logic that excludes only symlinked .bin entries under node_modules/.bin, preserving real .bin directories.
Container Startup Refactoring
apps/controller/src/app/container.ts
Extracted gateway synchronization startup logic to syncManagedRuntimeGatewayOnStartup function, moving conditional gating and parameter selection out of inline initialization.
Plugin Writer Test Coverage
tests/desktop/openclaw-runtime-plugin-writer.test.ts, apps/controller/tests/openclaw-runtime-plugin-writer.test.ts
Added comprehensive test suite for plugin template materialization covering symlinked .bin exclusion, real directory symlink materialization, and real .bin directory preservation scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • nexu-io/nexu#445: Modifies the same frontend localization and Skills page files for UI/text updates
  • nexu-io/nexu#355: Addresses similar symlink handling in plugin writer's .bin directory filtering logic with corresponding test coverage
  • nexu-io/nexu#361: Updates Skills page UI to prevent unintended link dragging behavior

Suggested reviewers

  • lefarcen

Poem

🐰 ClawHub now shines on Skills so bright,
With symlinks untangled, the paths are just right,
The disclaimer warns with a GitHub-bound link,
While plugins materialize quicker than a blink! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: renaming Explore to ClawHub, swapping tab order, and adding a disclaimer to the Skills page.
Description check ✅ Passed The PR description covers all essential sections including summary of changes, affected areas (web dashboard/skills), test plan with checkboxes, and implementation notes addressing multiple issues.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/clawhub-rename

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 385075a74b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/controller/src/app/container.ts
Comment thread apps/controller/src/runtime/openclaw-runtime-plugin-writer.ts Outdated
Comment thread apps/desktop/scripts/dist-mac.mjs Outdated
- Swap Skills page tabs: Yours first, ClawHub (formerly Explore) second
- Rename "Explore" to "ClawHub" in both en and zh-CN locales
- Add always-visible ClawHub disclaimer with GitHub Issues link
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
apps/controller/tests/openclaw-runtime-plugin-writer.test.ts (1)

23-26: Consider documenting the partial mock intent.

The type assertion as ControllerEnv creates a partial mock with only the required paths. This is acceptable for focused unit tests, but a brief comment could clarify that other ControllerEnv properties aren't needed for this test.

📝 Optional: Add clarifying comment
   beforeEach(async () => {
     rootDir = await mkdtemp(path.join(tmpdir(), "nexu-runtime-plugin-writer-"));
+    // Partial mock: only runtimePluginTemplatesDir and openclawExtensionsDir are used
     env = {
       runtimePluginTemplatesDir: path.join(rootDir, "runtime-plugins"),
       openclawExtensionsDir: path.join(rootDir, "extensions"),
     } as ControllerEnv;
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/tests/openclaw-runtime-plugin-writer.test.ts` around lines 23
- 26, The test creates a partial mock by asserting env = {
runtimePluginTemplatesDir: path.join(rootDir, "runtime-plugins"),
openclawExtensionsDir: path.join(rootDir, "extensions") } as ControllerEnv but
doesn't document that other ControllerEnv properties are intentionally omitted;
add a brief inline comment above this assignment explaining that this is an
intentional partial/mock ControllerEnv used only to supply the two path
properties required by the openclaw-runtime-plugin-writer tests so readers won't
expect the full environment object.
apps/web/src/components/ui/dialog.tsx (1)

19-21: Unify modal layering with a dedicated z-index scale

DialogOverlay/DialogContent share z-50, which collides with other overlays/popovers already using z-50 (e.g. select content and channel-connect modal). This can produce inconsistent stacking when both are active.

Proposed fix
-        "fixed inset-0 z-50 bg-black/40 backdrop-blur-sm",
+        "fixed inset-0 z-[60] bg-black/40 backdrop-blur-sm",
...
-          "fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2",
+          "fixed left-1/2 top-1/2 z-[61] w-full max-w-md -translate-x-1/2 -translate-y-1/2",

Also applies to: 40-44

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/ui/dialog.tsx` around lines 19 - 21, DialogOverlay
and DialogContent both use the generic "z-50" which conflicts with other
overlays/popovers; update the dialog components to use a dedicated, higher
z-index token (e.g., replace "z-50" with a unique class like "z-dialog" or a
higher numeric value such as "z-60") so dialogs consistently stack above other
UI layers; change the class on both DialogOverlay and DialogContent (look for
the strings "DialogOverlay" and "DialogContent" or the array entries containing
"z-50") and ensure any CSS/tailwind config defines the new z-dialog value so it
does not collide with other z-50 elements.
apps/web/tests/import-skill-modal-state.test.ts (1)

7-17: Add edge-case assertions for .ZIP and nullish input.

Current tests verify happy-path lowercase and one invalid extension, but they don’t lock behavior for uppercase extension or missing input.

Proposed test additions
 describe("getSelectedZipFile", () => {
   it("keeps valid zip files", () => {
     const file = { name: "skill.zip" };

     expect(getSelectedZipFile(file)).toBe(file);
   });

+  it("keeps valid zip files with uppercase extension", () => {
+    const file = { name: "skill.ZIP" };
+
+    expect(getSelectedZipFile(file)).toBe(file);
+  });
+
   it("clears the selection for invalid files", () => {
     expect(getSelectedZipFile({ name: "skill.txt" })).toBeNull();
   });
+
+  it("returns null when no file is provided", () => {
+    expect(getSelectedZipFile(null)).toBeNull();
+    expect(getSelectedZipFile(undefined)).toBeNull();
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/tests/import-skill-modal-state.test.ts` around lines 7 - 17, Tests
for getSelectedZipFile miss two edge cases: an uppercase extension and nullish
input. Add two assertions in apps/web/tests/import-skill-modal-state.test.ts:
one test that passes a file object with name "skill.ZIP" and expects
getSelectedZipFile(...) to return that file, and another test that calls
getSelectedZipFile(undefined) (and optionally null) and expects null; reference
the getSelectedZipFile function to locate where to add these it blocks alongside
the existing cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/controller/src/runtime/openclaw-runtime-plugin-writer.ts`:
- Around line 36-39: The current filter function passed to cp() (the async arrow
at filter: async (src) => { ... }) rejects all symlinks causing non-.bin
symlinks like shared-assets to be skipped; change cp() options to include
dereference: true and update the filter to only skip paths that end with ".bin"
(i.e., check stat.isSymbolicLink() and the src basename/suffix) so that non-.bin
symlinks are materialized as real directories while .bin symlinks are still
skipped. Ensure the modified filter and dereference option are used together
when invoking cp() so tests expecting materialized shared-assets succeed.

In `@apps/controller/tests/openclaw-runtime-plugin-writer.test.ts`:
- Around line 71-96: The test assumes OpenClawRuntimePluginWriter.materializes
symlinked directories but the implementation currently skips all symlinks via
the filter that returns !stat.isSymbolicLink() and doesn't set dereference:
true; update the implementation in OpenClawRuntimePluginWriter (the
ensurePlugins/copy logic and its filter callback) to copy with dereference: true
so symlinked directories are materialized, and change the filter so it only
excludes .bin symlinks (e.g., check the entry name endsWith(".bin") before
skipping) so shared-assets symlink is copied and the test passes; alternatively,
if you prefer skipping symlinks, update this test's assertions to expect the
symlink to be absent rather than materialized.

In `@apps/desktop/scripts/dist-mac.mjs`:
- Around line 451-453: The code computes controllerPort from process.env
directly (const controllerPort = process.env.NEXU_CONTROLLER_PORT ??
process.env.NEXU_API_PORT ?? "50800") which ignores values loaded by
loadDesktopEnv(); move this computation to after the merged env object is
constructed (the env variable created around loadDesktopEnv()) or compute it
from the merged env (e.g., env.NEXU_CONTROLLER_PORT ?? env.NEXU_API_PORT ??
"50800"), and remove the original direct-process.env declaration so
controllerPort always respects .env-loaded values.

In `@apps/web/src/components/skills/import-skill-modal.tsx`:
- Around line 81-87: The import success continuation in handleImport (which
calls importMutation.mutateAsync, setDone, and
autoCloseControllerRef.current.schedule(handleClose,...)) can run after the
modal has been closed, causing stale state and unwanted auto-close; fix by
recording/checking the modal-open state (e.g., an isOpen or isMounted ref/state)
before awaiting mutateAsync and again immediately after it returns, and only
call setDone and schedule handleClose if the modal is still open; also ensure
any scheduled autoClose is cancelled when the modal closes (use
autoCloseControllerRef.current.clear/cancel in the close handler) — apply the
same pattern to the similar block at lines 239-247.

In `@apps/web/src/components/ui/dialog.tsx`:
- Around line 29-33: The SR-only "Close" text inside the dialog is hardcoded in
DialogContent (and the similar close button at the other occurrence), so update
the component to use a localized string instead: either accept a closeLabel prop
(e.g., closeLabel?: string) and use that for the SR-only span/aria-label, or
import your i18n hook (e.g., useTranslation()/t) and call t('close') inside
DialogContent (and the other close button) and replace the literal "Close" with
the localized value; ensure the value is used for both the visible accessible
label (aria-label) and the SR-only text so screen readers get the translated
string.

In `@apps/web/src/pages/skills.tsx`:
- Around line 605-612: The anchor tag rendering the hardcoded "GitHub Issues"
label should use the app's i18n key instead of English text: replace the literal
string in the anchor (the <a> element in skills.tsx) with the translation lookup
(e.g., t('skills.clawhubIssuesLink') or the project's equivalent translation
hook) and ensure the same translation key is added to both locale files with
appropriate zh-CN and en values; keep the href, target and rel attributes
unchanged.

---

Nitpick comments:
In `@apps/controller/tests/openclaw-runtime-plugin-writer.test.ts`:
- Around line 23-26: The test creates a partial mock by asserting env = {
runtimePluginTemplatesDir: path.join(rootDir, "runtime-plugins"),
openclawExtensionsDir: path.join(rootDir, "extensions") } as ControllerEnv but
doesn't document that other ControllerEnv properties are intentionally omitted;
add a brief inline comment above this assignment explaining that this is an
intentional partial/mock ControllerEnv used only to supply the two path
properties required by the openclaw-runtime-plugin-writer tests so readers won't
expect the full environment object.

In `@apps/web/src/components/ui/dialog.tsx`:
- Around line 19-21: DialogOverlay and DialogContent both use the generic "z-50"
which conflicts with other overlays/popovers; update the dialog components to
use a dedicated, higher z-index token (e.g., replace "z-50" with a unique class
like "z-dialog" or a higher numeric value such as "z-60") so dialogs
consistently stack above other UI layers; change the class on both DialogOverlay
and DialogContent (look for the strings "DialogOverlay" and "DialogContent" or
the array entries containing "z-50") and ensure any CSS/tailwind config defines
the new z-dialog value so it does not collide with other z-50 elements.

In `@apps/web/tests/import-skill-modal-state.test.ts`:
- Around line 7-17: Tests for getSelectedZipFile miss two edge cases: an
uppercase extension and nullish input. Add two assertions in
apps/web/tests/import-skill-modal-state.test.ts: one test that passes a file
object with name "skill.ZIP" and expects getSelectedZipFile(...) to return that
file, and another test that calls getSelectedZipFile(undefined) (and optionally
null) and expects null; reference the getSelectedZipFile function to locate
where to add these it blocks alongside the existing cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6835ad62-6162-4baa-b5d8-993775dc4022

📥 Commits

Reviewing files that changed from the base of the PR and between 9beeb87 and 385075a.

📒 Files selected for processing (15)
  • apps/controller/src/app/container.ts
  • apps/controller/src/runtime/openclaw-config-writer.ts
  • apps/controller/src/runtime/openclaw-runtime-plugin-writer.ts
  • apps/controller/src/services/openclaw-gateway-service.ts
  • apps/controller/src/store/nexu-config-store.ts
  • apps/controller/tests/openclaw-runtime-plugin-writer.test.ts
  • apps/desktop/scripts/dist-mac.mjs
  • apps/desktop/scripts/prepare-openclaw-sidecar.mjs
  • apps/web/src/components/skills/import-skill-modal-state.ts
  • apps/web/src/components/skills/import-skill-modal.tsx
  • apps/web/src/components/ui/dialog.tsx
  • apps/web/src/i18n/locales/en.ts
  • apps/web/src/i18n/locales/zh-CN.ts
  • apps/web/src/pages/skills.tsx
  • apps/web/tests/import-skill-modal-state.test.ts
💤 Files with no reviewable changes (3)
  • apps/controller/src/app/container.ts
  • apps/controller/src/services/openclaw-gateway-service.ts
  • apps/controller/src/store/nexu-config-store.ts

Comment thread apps/controller/src/runtime/openclaw-runtime-plugin-writer.ts Outdated
Comment thread apps/controller/tests/openclaw-runtime-plugin-writer.test.ts
Comment thread apps/desktop/scripts/dist-mac.mjs Outdated
Comment thread apps/web/src/components/skills/import-skill-modal.tsx
Comment thread apps/web/src/components/ui/dialog.tsx
Comment thread apps/web/src/pages/skills.tsx
- Config compiler: use env port/authMode when managing OpenClaw process,
  preventing stale persisted values after dynamic port reallocation
- Plugin writer: skip only .bin dirs (not all symlinks) with dereference,
  preserving non-.bin symlinked content in runtime plugins
- dist-mac: read controller port from merged env (includes desktop .env),
  fixing wrong VITE_API_BASE_URL in packaged builds
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Mar 24, 2026

Deploying nexu-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 044bbe6
Status: ✅  Deploy successful!
Preview URL: https://18a69433.nexu-docs.pages.dev
Branch Preview URL: https://feat-clawhub-rename.nexu-docs.pages.dev

View logs

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tests/desktop/openclaw-runtime-plugin-writer.test.ts (1)

17-118: Add one test for missing templates directory (ENOENT) early-return path.

ensurePlugins() has explicit ENOENT handling; adding a dedicated assertion would complete branch coverage for this method.

Proposed test addition
 describe("OpenClawRuntimePluginWriter", () => {
@@
   it("excludes all node_modules/.bin directories including real ones", async () => {
@@
     ).rejects.toMatchObject({ code: "ENOENT" });
   });
+
+  it("returns without error when runtime plugin templates directory is missing", async () => {
+    await rm(env.runtimePluginTemplatesDir, { recursive: true, force: true });
+
+    const writer = new OpenClawRuntimePluginWriter(env);
+    await expect(writer.ensurePlugins()).resolves.toBeUndefined();
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/desktop/openclaw-runtime-plugin-writer.test.ts` around lines 17 - 118,
Add a test that verifies OpenClawRuntimePluginWriter.ensurePlugins() handles a
missing templates directory by early-returning: in
tests/desktop/openclaw-runtime-plugin-writer.test.ts create an it block that
does NOT create env.runtimePluginTemplatesDir (leave it nonexistent),
instantiate new OpenClawRuntimePluginWriter(env), call await
writer.ensurePlugins() and assert it resolves (doesn't throw) and that
env.openclawExtensionsDir was not created (e.g.,
access(env.openclawExtensionsDir) rejects with { code: "ENOENT" }); reference
ensurePlugins, OpenClawRuntimePluginWriter, and env.runtimePluginTemplatesDir in
the test to locate the code.
apps/controller/src/lib/openclaw-config-compiler.ts (1)

371-374: Couple auth.token emission to the effective auth mode.

At Line 371 you now compute/use gatewayAuthMode, but Lines 372-374 still emit token whenever env token exists. This can produce mode: "none" plus a token in the same compiled block. I’d gate token emission by gatewayAuthMode === "token".

Proposed patch
       auth: {
         mode: gatewayAuthMode,
-        ...(env.openclawGatewayToken
-          ? { token: env.openclawGatewayToken }
-          : {}),
+        ...(gatewayAuthMode === "token" && env.openclawGatewayToken
+          ? { token: env.openclawGatewayToken }
+          : {}),
       },

As per coding guidelines, apps/controller/src/lib/openclaw-config-compiler.ts: Config generator output must match specs/references/openclaw-config-schema.md.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/lib/openclaw-config-compiler.ts` around lines 371 - 374,
The emitted auth block can include a token even when gatewayAuthMode is "none";
update the config generator in openclaw-config-compiler.ts so token is only
emitted when gatewayAuthMode === "token" and env.openclawGatewayToken is
present: replace the unconditional spread of { token: env.openclawGatewayToken }
with a conditional that checks both gatewayAuthMode === "token" and
env.openclawGatewayToken (affecting the auth output where mode: gatewayAuthMode
is set) so the compiled config conforms to the openclaw-config schema.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/controller/src/lib/openclaw-config-compiler.ts`:
- Around line 371-374: The emitted auth block can include a token even when
gatewayAuthMode is "none"; update the config generator in
openclaw-config-compiler.ts so token is only emitted when gatewayAuthMode ===
"token" and env.openclawGatewayToken is present: replace the unconditional
spread of { token: env.openclawGatewayToken } with a conditional that checks
both gatewayAuthMode === "token" and env.openclawGatewayToken (affecting the
auth output where mode: gatewayAuthMode is set) so the compiled config conforms
to the openclaw-config schema.

In `@tests/desktop/openclaw-runtime-plugin-writer.test.ts`:
- Around line 17-118: Add a test that verifies
OpenClawRuntimePluginWriter.ensurePlugins() handles a missing templates
directory by early-returning: in
tests/desktop/openclaw-runtime-plugin-writer.test.ts create an it block that
does NOT create env.runtimePluginTemplatesDir (leave it nonexistent),
instantiate new OpenClawRuntimePluginWriter(env), call await
writer.ensurePlugins() and assert it resolves (doesn't throw) and that
env.openclawExtensionsDir was not created (e.g.,
access(env.openclawExtensionsDir) rejects with { code: "ENOENT" }); reference
ensurePlugins, OpenClawRuntimePluginWriter, and env.runtimePluginTemplatesDir in
the test to locate the code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2df45e5e-0a59-4231-a4b0-40a6fe151241

📥 Commits

Reviewing files that changed from the base of the PR and between fddd98a and 044bbe6.

📒 Files selected for processing (6)
  • apps/controller/src/lib/openclaw-config-compiler.ts
  • apps/desktop/scripts/dist-mac.mjs
  • apps/web/src/i18n/locales/en.ts
  • apps/web/src/i18n/locales/zh-CN.ts
  • apps/web/src/pages/skills.tsx
  • tests/desktop/openclaw-runtime-plugin-writer.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/pages/skills.tsx
  • apps/web/src/i18n/locales/en.ts

…ter test

- Revert gatewayPort/gatewayAuthMode env override in config compiler —
  syncManagedRuntimeGateway() already ensures persisted config stays aligned
- Fix plugin writer test to match refined filter that preserves real .bin dirs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/controller/src/runtime/openclaw-runtime-plugin-writer.ts (1)

5-12: LGTM! Filter logic correctly addresses the .bin symlink crash.

The implementation now properly:

  1. Skips symlinked node_modules/.bin entries (preventing the EINVAL crash from fs.cp)
  2. Preserves real node_modules/.bin directories
  3. Materializes non-.bin symlinks as real directories via dereference: true

This addresses the past review concern about all symlinks being skipped.

Nitpick: The basename(source) !== ".bin" check on line 47 is redundant—isNodeModulesBinPath already guarantees segments.at(-1) === ".bin", which is equivalent to basename(source) === ".bin". Consider simplifying:

♻️ Optional simplification
         filter: async (source) => {
-          if (!isNodeModulesBinPath(source) || basename(source) !== ".bin") {
+          if (!isNodeModulesBinPath(source)) {
             return true;
           }

           const stat = await lstat(source);
           return !stat.isSymbolicLink();
         },

,

Also applies to: 46-53

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/runtime/openclaw-runtime-plugin-writer.ts` around lines 5
- 12, Remove the redundant basename check by relying solely on
isNodeModulesBinPath(source): the helper isNodeModulesBinPath already returns
true when the path ends with "node_modules/.bin", so drop any additional checks
like basename(source) !== ".bin" wherever you guarded logic (e.g., inside the
code that filters or handles symlinks prior to fs.cp); update the conditional to
call isNodeModulesBinPath(source) (or negate it) instead of combining it with
basename-based logic to simplify and avoid duplicate checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/controller/src/app/container.ts`:
- Line 38: The import of syncManagedRuntimeGatewayOnStartup is failing because
./runtime-gateway-sync.js is missing; create a new module that exports async
function syncManagedRuntimeGatewayOnStartup(configStore: NexuConfigStore, env:
ControllerEnv): Promise<void> which returns early if !env.manageOpenclawProcess
and otherwise calls configStore.syncManagedRuntimeGateway with { port:
env.openclawGatewayPort, authMode: env.openclawGatewayToken ? "token" : "none"
}; alternatively remove the import and any references to
syncManagedRuntimeGatewayOnStartup to revert the refactor.

---

Nitpick comments:
In `@apps/controller/src/runtime/openclaw-runtime-plugin-writer.ts`:
- Around line 5-12: Remove the redundant basename check by relying solely on
isNodeModulesBinPath(source): the helper isNodeModulesBinPath already returns
true when the path ends with "node_modules/.bin", so drop any additional checks
like basename(source) !== ".bin" wherever you guarded logic (e.g., inside the
code that filters or handles symlinks prior to fs.cp); update the conditional to
call isNodeModulesBinPath(source) (or negate it) instead of combining it with
basename-based logic to simplify and avoid duplicate checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 59906bcb-ff69-4a73-8ac7-6e2e9885aa24

📥 Commits

Reviewing files that changed from the base of the PR and between 044bbe6 and d9ee406.

📒 Files selected for processing (5)
  • apps/controller/src/app/container.ts
  • apps/controller/src/runtime/openclaw-runtime-plugin-writer.ts
  • apps/controller/tests/openclaw-runtime-plugin-writer.test.ts
  • apps/desktop/scripts/dist-mac.mjs
  • tests/desktop/openclaw-runtime-plugin-writer.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/desktop/openclaw-runtime-plugin-writer.test.ts

Comment thread apps/controller/src/app/container.ts Outdated
@alchemistklk
Copy link
Copy Markdown
Contributor Author

/cr

@slack-code-review-channel
Copy link
Copy Markdown

✅ CR topic created in Feishu topic group Refly CR.

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.

2 participants