feat: rename Explore to ClawHub, swap tab order, add disclaimer#477
feat: rename Explore to ClawHub, swap tab order, add disclaimer#477alchemistklk merged 13 commits intomainfrom
Conversation
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.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis 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 Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 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".
- 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
385075a to
6e3a6b2
Compare
There was a problem hiding this comment.
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 ControllerEnvcreates a partial mock with only the required paths. This is acceptable for focused unit tests, but a brief comment could clarify that otherControllerEnvproperties 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/DialogContentsharez-50, which collides with other overlays/popovers already usingz-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.ZIPand 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
📒 Files selected for processing (15)
apps/controller/src/app/container.tsapps/controller/src/runtime/openclaw-config-writer.tsapps/controller/src/runtime/openclaw-runtime-plugin-writer.tsapps/controller/src/services/openclaw-gateway-service.tsapps/controller/src/store/nexu-config-store.tsapps/controller/tests/openclaw-runtime-plugin-writer.test.tsapps/desktop/scripts/dist-mac.mjsapps/desktop/scripts/prepare-openclaw-sidecar.mjsapps/web/src/components/skills/import-skill-modal-state.tsapps/web/src/components/skills/import-skill-modal.tsxapps/web/src/components/ui/dialog.tsxapps/web/src/i18n/locales/en.tsapps/web/src/i18n/locales/zh-CN.tsapps/web/src/pages/skills.tsxapps/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
- 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
Deploying nexu-docs with
|
| 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 |
There was a problem hiding this comment.
🧹 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 explicitENOENThandling; 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: Coupleauth.tokenemission to the effective auth mode.At Line 371 you now compute/use
gatewayAuthMode, but Lines 372-374 still emittokenwhenever env token exists. This can producemode: "none"plus a token in the same compiled block. I’d gate token emission bygatewayAuthMode === "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 matchspecs/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
📒 Files selected for processing (6)
apps/controller/src/lib/openclaw-config-compiler.tsapps/desktop/scripts/dist-mac.mjsapps/web/src/i18n/locales/en.tsapps/web/src/i18n/locales/zh-CN.tsapps/web/src/pages/skills.tsxtests/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
There was a problem hiding this comment.
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.binsymlink crash.The implementation now properly:
- Skips symlinked
node_modules/.binentries (preventing the EINVAL crash fromfs.cp)- Preserves real
node_modules/.bindirectories- Materializes non-
.binsymlinks as real directories viadereference: trueThis addresses the past review concern about all symlinks being skipped.
Nitpick: The
basename(source) !== ".bin"check on line 47 is redundant—isNodeModulesBinPathalready guaranteessegments.at(-1) === ".bin", which is equivalent tobasename(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
📒 Files selected for processing (5)
apps/controller/src/app/container.tsapps/controller/src/runtime/openclaw-runtime-plugin-writer.tsapps/controller/tests/openclaw-runtime-plugin-writer.test.tsapps/desktop/scripts/dist-mac.mjstests/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
|
/cr |
|
✅ CR topic created in Feishu topic group Refly CR. |
Summary
fs.cpEINVAL on.bin/symlinks)Test plan
pnpm start— verify desktop boots without controller EINVAL crashpnpm typecheck && pnpm lint && pnpm test— all passSummary by CodeRabbit
New Features
Localization
UI/UX