Skip to content

Experiment: UI for linking external plugins and themes#3128

Draft
cbravobernal wants to merge 8 commits intoadd-plugin-theme-link-clifrom
experiment-plugin-theme-link-ui
Draft

Experiment: UI for linking external plugins and themes#3128
cbravobernal wants to merge 8 commits intoadd-plugin-theme-link-clifrom
experiment-plugin-theme-link-ui

Conversation

@cbravobernal
Copy link
Copy Markdown

@cbravobernal cbravobernal commented Apr 17, 2026

Experiment / spike — pairs with the CLI work in #3123. Branch base is add-plugin-theme-link-cli so the diff shows only the UI additions.

Goal

Fixes #1728

Mirror the CLI feature (studio plugin/theme link|unlink|list-linked) with a UI surface so non-CLI users can manage symlinks from the Studio app.

ui.mp4

Surfaces added

  • New site-detail tab "Linked extensions" with two sections: Plugins, Themes
  • + Link plugin / + Link theme buttons → native folder picker → validate → symlink
  • Unlink per row → confirmation dialog → remove symlink only (source untouched)
  • Per-row "Open in…" kebab menu (mirrors Overview's Shortcuts section): Finder / File Explorer, user's configured editor (VS Code, etc.), user's configured terminal (iTerm, Terminal, etc.) — all point at the linked source path.
  • Inline error/notification feedback via existing showErrorMessageBox / showNotification.

Architecture

UI delegates entirely to the CLI via a child process — no duplicated symlink/validation logic in the main process. Mirrors the pattern established for import/export in #3129 (execute-import-command.ts / execute-export-command.ts).

  • apps/studio/src/modules/cli/lib/execute-link-command.ts — thin wrapper over executeCliCommand that shells out to studio plugin|theme link|unlink|list-linked and awaits completion. Parses list-linked --format json stdout, surfaces CLI errors via CliCommandError.message.
  • IPC handlers in apps/studio/src/ipc-handlers.ts (6 of them) now do nothing but resolve the site path and call the wrapper. Zero business logic in main.
  • Any CLI-side hardening (path traversal, recursive-source guard, empty-basename, OWASP fixes already landed on CLI: Add plugin and theme link management. #3123) automatically applies to the UI path. No parallel-hardening drift risk.

Files

  • apps/studio/src/modules/cli/lib/execute-link-command.ts — new. CLI wrapper + LinkedExtension type.
  • apps/studio/src/ipc-handlers.ts — 6 handlers: linkPlugin, unlinkPlugin, listLinkedPlugins + theme equivalents. All async, take siteId, fork the CLI.
  • apps/studio/src/preload.ts — exposes the 6 handlers via ipcRendererInvoke.
  • apps/studio/src/stores/linked-extensions-api.ts — RTK Query slice with useGet*Query and useLink*/useUnlink*Mutation hooks; auto-invalidates list on mutation. queryFn wraps IPC calls in try/catch and returns structured { error } so .unwrap() rejects with a readable message instead of [object Object].
  • apps/studio/src/stores/index.ts — registers slice in store + middleware.
  • apps/studio/src/hooks/use-content-tabs.tsx — adds 'linked-extensions' tab between Previews and Import / Export.
  • apps/studio/src/components/site-content-tabs.tsx — conditional render for the new tab.
  • apps/studio/src/components/content-tab-linked-extensions.tsx — the tab UI itself.

Removed in the refactor: apps/studio/src/lib/plugin-theme-link.ts (231 lines) and its test file (167 lines). Net −356 lines.

Status

  • Lint clean (npx eslint --fix on all touched files)
  • Type check clean (npx tsc -p apps/studio/tsconfig.json --noEmit)
  • All studio tests pass locally (850/850)
  • CLI tests for plugin/theme still pass (51/51)
  • Manual UI verification — tab + dialog + folder picker + Open-in menu (Finder/editor/terminal) + Unlink flow all exercised in dev Studio against a real site with /tmp/test-plugin and /tmp/test-theme fixtures.
  • No duplicated logic between CLI and Studio main process.
  • i18n strings reviewed for tone / wording
  • Telemetry hooks (CLI added PluginCommandLoggerAction / ThemeCommandLoggerAction; UI side relies on CLI telemetry via child process).

What this is NOT

This is a vibe-coded spike to validate the UI shape and IPC surface. Architecture, error UX, and string copy are starting points, not merge-ready polish.

…inked telemetry

- Reject plugin/theme names containing path separators in unlink to prevent
  lstat/readlink/unlink from escaping the plugins/themes directory.
- Wire PluginCommandLoggerAction.LIST_LINKED and ThemeCommandLoggerAction.LIST_LINKED
  through Logger.reportStart/reportSuccess/reportError so telemetry fires and
  UX matches other list commands (e.g. preview list).
- Add unit tests for traversal inputs and update list-linked tests to assert
  on logger messages.
Guard against edge cases where path.basename(absoluteSourcePath) resolves
to '', '.', or '..' (e.g. source = filesystem root). Without this check
targetPath collapses to the plugins/themes directory itself and produces
confusing downstream errors. Emit a clear LoggerError instead.
…-link-ui

# Conflicts:
#	apps/cli/README.md
#	apps/cli/commands/plugin/link.ts
#	apps/cli/commands/plugin/tests/link.test.ts
#	apps/cli/commands/plugin/tests/list-linked.test.ts
#	apps/cli/commands/theme/link.ts
#	apps/cli/commands/theme/tests/link.test.ts
#	apps/cli/commands/theme/tests/list-linked.test.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant