Skip to content

fix(web): prevent skill card drag from opening a second window#361

Merged
lefarcen merged 8 commits intomainfrom
fix/skill-card-drag-new-window
Mar 23, 2026
Merged

fix(web): prevent skill card drag from opening a second window#361
lefarcen merged 8 commits intomainfrom
fix/skill-card-drag-new-window

Conversation

@alchemistklk
Copy link
Copy Markdown
Contributor

@alchemistklk alchemistklk commented Mar 23, 2026

Summary

  • Dragging a skill card in the Electron desktop app opened a second BrowserWindow because the <Link> rendered as a native <a> tag with default drag behavior
  • Added draggable={false} to the SkillCard <Link> component to suppress the browser's built-in link-drag navigation

Test plan

  • Launch desktop app, navigate to Skills page
  • Click and drag a skill card — verify no second window appears
  • Click (not drag) a skill card — verify normal navigation still works

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added skill import modal with ZIP upload (picker + drag-and-drop) and ZIP validation
    • Modal auto-closes 1.2s after successful import; GitHub import coming soon
  • UI Components

    • New dialog component library for modal interfaces
    • Minor Skills page UX tweaks (prevent card dragging; adjust installed count)
  • Localization

    • Added English and Chinese translations for import workflow
  • Tests

    • New tests for import modal state, auto-close behavior, and plugin writer file handling

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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f187bd1-af87-4486-a38d-0c78e9673dd8

📥 Commits

Reviewing files that changed from the base of the PR and between d005f3e and a207c98.

📒 Files selected for processing (1)
  • apps/web/src/pages/skills.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/pages/skills.tsx

📝 Walkthrough

Walkthrough

Adds a skill import feature (modal UI, file validation, auto-close controller, dialog primitives, i18n) plus tests for import-state logic and OpenClawRuntimePluginWriter symlink handling; integrates the modal into the Skills page and removes the prior inline file-upload flow.

Changes

Cohort / File(s) Summary
Skill Import Modal
apps/web/src/components/skills/import-skill-modal-state.ts, apps/web/src/components/skills/import-skill-modal.tsx
New modal component and state utilities: ZIP selection validation, drag-and-drop and file-picker handling, auto-close controller, GitHub tab placeholder, import mutation wiring, and automatic close after success.
Dialog UI Components
apps/web/src/components/ui/dialog.tsx
New Radix-based dialog primitives and styled wrapper components (overlay, content, header/footer, title, description, body) with animations and accessible close button.
Skills Page Integration
apps/web/src/pages/skills.tsx
Replaced inline file-upload flow with controlled ImportSkillModal; simplified import button UI; prevented Link dragging on SkillCard; adjusted installed/your counts.
Internationalization
apps/web/src/i18n/locales/en.ts, apps/web/src/i18n/locales/zh-CN.ts
Added English and Simplified Chinese localization keys for the import-skill flow, upload UI, status messages, hints, and actions.
Import State Tests
apps/web/tests/import-skill-modal-state.test.ts
New Vitest suite covering getSelectedZipFile and createAutoCloseController behavior (including fake-timers scenarios).
Plugin Writer Tests
apps/controller/tests/openclaw-runtime-plugin-writer.test.ts
New Vitest tests validating OpenClawRuntimePluginWriter filesystem behavior: do not materialize .bin symlinks; convert non-.bin symlinks into real directories with expected files.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • lefarcen

Poem

"🐰 I hopped through code to make imports bright,
A modal opens, zip files take flight,
Dialogs that sparkle, auto-close on cue,
Tests that tame symlinks, making paths true,
Hooray for features, stitched by a rabbit’s bite!"

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description lacks the required template structure. It has a 'Summary' and 'Test plan' but is missing 'What', 'Why', 'How', 'Affected areas', and 'Checklist' sections from the template. Restructure the description to follow the template: add 'What/Why/How' sections, check 'Affected areas' boxes, and complete the checklist (typecheck, lint, test, credentials check, etc.).
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 (1 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'fix(web): prevent skill card drag from opening a second window' is clear and specific, accurately describing the main fix (preventing a second window from opening when dragging skill cards).

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/skill-card-drag-new-window

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

@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 (2)
apps/controller/tests/openclaw-runtime-plugin-writer.test.ts (2)

29-31: Guard teardown to avoid masking setup failures.

If Line 22 throws before assignment, Line 30 can throw during cleanup and obscure the real failure. A small guard makes the failure signal cleaner.

🛠️ Proposed hardening
-let rootDir: string;
+let rootDir: string | undefined;
@@
 afterEach(async () => {
-  await rm(rootDir, { recursive: true, force: true });
+  if (rootDir) {
+    await rm(rootDir, { recursive: true, force: true });
+    rootDir = undefined;
+  }
 });
🤖 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 29
- 31, The teardown in the afterEach block can throw if rootDir was never
assigned during setup; modify the afterEach to guard the cleanup by checking
that rootDir is defined and exists before calling rm (e.g., typeof rootDir !==
'undefined' and/or fs.existsSync(rootDir)), then await rm only when the guard
passes; update the afterEach containing rm(...) so it references rootDir and rm
safely to avoid masking setup failures.

23-26: Narrow the env contract instead of force-casting to ControllerEnv.

Line 23–26 uses as ControllerEnv, which hides the actual interface contract. The class only accesses runtimePluginTemplatesDir and openclawExtensionsDir, so the constructor should declare a minimal interface rather than relying on a broad type assertion.

Define a RuntimePluginWriterEnv interface in the writer, update the constructor parameter, and remove the type assertion in the test. This prevents contract drift and keeps type checks strict without deferred failures.

🤖 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, Define a narrow interface named RuntimePluginWriterEnv in the openclaw
runtime writer module that exposes only runtimePluginTemplatesDir and
openclawExtensionsDir, change the writer class constructor signature to accept
RuntimePluginWriterEnv instead of ControllerEnv (e.g., constructor(env:
RuntimePluginWriterEnv) or update the class name's constructor accordingly), and
then remove the test's force-cast "as ControllerEnv" in
apps/controller/tests/openclaw-runtime-plugin-writer.test.ts so the test object
matches the new RuntimePluginWriterEnv shape directly.
🤖 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/web/src/components/skills/import-skill-modal.tsx`:
- Around line 189-197: The fallback hardcoded message "Import failed" in the
import-skill-modal UI should be replaced with a localized string; update the JSX
where importMutation.error?.message is used (inside the component in
import-skill-modal.tsx) to call the app's i18n/translation helper (e.g.,
t('skills.importFailed') or the project's equivalent) so the fallback uses a
translated message instead of the hardcoded English text.

---

Nitpick comments:
In `@apps/controller/tests/openclaw-runtime-plugin-writer.test.ts`:
- Around line 29-31: The teardown in the afterEach block can throw if rootDir
was never assigned during setup; modify the afterEach to guard the cleanup by
checking that rootDir is defined and exists before calling rm (e.g., typeof
rootDir !== 'undefined' and/or fs.existsSync(rootDir)), then await rm only when
the guard passes; update the afterEach containing rm(...) so it references
rootDir and rm safely to avoid masking setup failures.
- Around line 23-26: Define a narrow interface named RuntimePluginWriterEnv in
the openclaw runtime writer module that exposes only runtimePluginTemplatesDir
and openclawExtensionsDir, change the writer class constructor signature to
accept RuntimePluginWriterEnv instead of ControllerEnv (e.g., constructor(env:
RuntimePluginWriterEnv) or update the class name's constructor accordingly), and
then remove the test's force-cast "as ControllerEnv" in
apps/controller/tests/openclaw-runtime-plugin-writer.test.ts so the test object
matches the new RuntimePluginWriterEnv shape directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2b8848f9-209e-4de5-acc4-e89f17e5199c

📥 Commits

Reviewing files that changed from the base of the PR and between 64215ab and d005f3e.

📒 Files selected for processing (8)
  • apps/controller/tests/openclaw-runtime-plugin-writer.test.ts
  • 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

Comment thread apps/web/src/components/skills/import-skill-modal.tsx
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.
@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