Skip to content

feat: wework支持安装插件 - #1234

Merged
qdaxb merged 3 commits into
wecode-ai:mainfrom
Micro66:human/tortoise-20260602-043405
Jun 3, 2026
Merged

qdaxb merged 3 commits into
wecode-ai:mainfrom
Micro66:human/tortoise-20260602-043405

Conversation

@Micro66

@Micro66 Micro66 commented Jun 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Full plugin management: upload (ZIP, 50MB limit), list, view details, enable/disable, per-component toggles, update settings, uninstall, and download plugin packages.
    • Plugin-aware device sync that downloads and refreshes plugin packages when checksums change.
    • English and Simplified Chinese i18n for plugin flows and new model picker labels.
  • Style

    • Global UI background/theme updates for more consistent styling.
  • Tests

    • Removed legacy start.sh end-to-end test; added plugin upload/sync-related unit tests.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds Claude Code plugin management end-to-end: ZIP parsing, on-disk package storage and DB-backed installed-plugin records, REST CRUD endpoints (/plugins), executor-side secure download/extract and capability sync integration, and full frontend UI for listing, uploading, detail view, per-component toggles, and uninstall.

Changes

Plugin Management End-to-End

Layer / File(s) Summary
Backend and Frontend Type Contracts
backend/app/schemas/installed_plugin.py, wework/src/types/api.ts
Pydantic and TypeScript schemas define the InstalledPlugin resource with spec (source, components, enabled state, manifest, package ref), status, and component inventory (skills, commands, agents, hooks, MCPs, monitors, bins).
Plugin Package Parser
backend/app/services/claude_plugin_parser.py
Validates ZIP size and safety, locates plugin root .claude-plugin/plugin.json, parses manifest and component types (skills/markdown/json/MCPs/bins), and returns normalized PluginUploadInfo.
Installation Service & Storage
backend/app/services/installed_plugin_service.py
upload_plugin computes checksum, stores ZIP under data/plugin_packages, creates/reactivates DB Kind rows with composed spec/status, lists active installs, updates merged componentStates/display/description, uninstalls, and resolves download paths with 404 semantics.
REST API Endpoints & Router Wiring
backend/app/api/api.py, backend/app/api/endpoints/installed_plugins.py
Registers /plugins router, exposes endpoints: list, upload (file), download (FileResponse), update (PUT), and uninstall (DELETE); mutation endpoints trigger an async global capability sync.
Device Capability Sync & Executor Handling
backend/app/services/device/capability_sync_service.py, executor/modes/local/capabilities.py
Enriches plugin payloads with component_states/components/packageRef/checksum/download_path; executor handler can download ZIPs (with optional checksum), safely extract/normalize plugin root, and apply componentStates during skill scanning.
Frontend Plugin API Client
wework/src/api/plugins.ts
createPluginApi provides list/update/uninstall and upload (FormData) used by workspaces and components.
Frontend UI Components
wework/src/components/plugins/PluginCreateMenu.tsx, wework/src/components/plugins/PluginDetailView.tsx, wework/src/components/plugins/PluginManagementRows.tsx, wework/src/components/plugins/PluginUploadDialog.tsx
Create menu with plugin entry, plugin detail view with per-component toggles and manifest info, InstalledPluginRow list item, and modal upload dialog with ZIP validation (50MB client limit).
Frontend Workspaces & Integration
wework/src/components/plugins/PluginManagementWorkspace.tsx, wework/src/components/plugins/PluginsWorkspace.tsx
Workspaces load installed plugins, support optimistic enable/component toggles, upload flow, search/filtering, selection/detail navigation, and uninstall handling.
Tests, i18n, Theming & Misc
backend/tests/*, executor/tests/*, wework/src/i18n/locales/*, wework/tailwind.config.js, start.sh, wework/scripts/build-mac-app.sh
Adds unit/integration tests for upload/read/extract flows, mobile model-picker tests, i18n keys for plugin UI, renames Tailwind color key base→background and updates many components, exports VITE_SOCKET_BASE_URL in startup and macOS build script, and updates ESLint/npm scripts. Note: an existing start.sh test script was removed in this diff.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • qdaxb

Poem

🐰 A plugin arrives in a ZIP so bright,
Parsed and stored with checksum and light,
Components toggled, synced with care,
From backend vault to frontend square,
Rabbits cheer — plugins handled right!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@Micro66 Micro66 changed the title feat: 支持安装插件 feat: wework支持安装插件 Jun 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (6)
wework/src/api/plugins.ts (1)

24-26: 💤 Low value

Consider making the enabled flag a parameter.

The enabled field is currently hardcoded to 'true' when uploading a plugin. Consider making this a parameter to uploadPlugin(file: File, enabled = true) for greater flexibility, in case you later need to support uploading plugins in a disabled state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/api/plugins.ts` around lines 24 - 26, The uploadPlugin function
currently hardcodes enabled to 'true'; change its signature to accept an enabled
parameter (e.g., uploadPlugin(file: File, enabled = true)) and use that
parameter when building the FormData (replace formData.append('enabled', 'true')
with formData.append('enabled', String(enabled))). Update any call sites of
uploadPlugin if they need to upload a disabled plugin.
wework/src/components/plugins/PluginDetailView.tsx (1)

79-85: ⚡ Quick win

MCP component description is hardcoded instead of using actual metadata.

The MCP components have their description hardcoded to 'MCP server' (line 84), while all other component types (skills, commands, agents, hooks, lsps, monitors, bins) use the actual description or path from the item. This creates an inconsistency and loses potentially useful information about each MCP server.

Consider following the same pattern as other components:

Proposed fix
     ...components.mcps.map((item) => ({
       key: `mcp-${item.name}`,
       componentKey: `mcp:${item.name}`,
       type: 'mcp',
       name: item.name,
-      description: 'MCP server',
+      description: item.description || item.command || 'MCP server',
     })),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/plugins/PluginDetailView.tsx` around lines 79 - 85, The
MCP entries in the components.mcps.map are using a hardcoded description ('MCP
server') instead of the item's metadata; update the mapping (the block that
creates objects with key `mcp-${item.name}`, componentKey `mcp:${item.name}`,
and type 'mcp') to set description to the actual metadata, e.g. use
item.description || item.path || item.name (or similar fallback) so MCPs follow
the same pattern as skills/commands/agents/hooks/lsps/monitors/bins.
wework/src/components/plugins/PluginUploadDialog.tsx (1)

76-81: 💤 Low value

No client-side size guard despite the 50MB hint.

The hint advertises a 50MB limit but the dialog never validates file size, unlike SkillUploadDialog (which enforces its 10MB limit via plugins_skill_upload_size_error). Large files will only fail after a full upload round-trip. Consider validating nextFile.size in selectFile.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/plugins/PluginUploadDialog.tsx` around lines 76 - 81,
The UI advertises a 50MB limit but PluginUploadDialog never checks file size
client-side; update the selectFile handler in PluginUploadDialog to validate
nextFile.size and reject files >50*1024*1024 bytes, setting the same error flow
used by SkillUploadDialog (use the i18n key plugins_skill_upload_size_error or
add a plugins_plugin_upload_size_error if preferred) and clear/reset state
appropriately so the large file is rejected before upload starts. Ensure you
reference the selectFile function and nextFile.size when adding the check and
trigger the existing error display path used for skill uploads.
backend/app/api/endpoints/installed_plugins.py (1)

26-120: ⚡ Quick win

Trace the new plugin endpoints.

These handlers are now the install/update/remove control plane, but they do not use @trace_async/@trace_sync or attach useful span attributes such as user_id, installed_id, and upload size.

As per coding guidelines, "Use OpenTelemetry tracing decorators: @trace_async or @trace_sync for entire functions, and add_span_event() or set_span_attribute() for adding details to spans".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/endpoints/installed_plugins.py` around lines 26 - 120, Add
OpenTelemetry tracing to the plugin endpoint handlers by decorating synchronous
handlers (list_installed_plugins, download_installed_plugin) with `@trace_sync`
and async handlers (upload_plugin, update_installed_plugin,
uninstall_installed_plugin) with `@trace_async`; inside each handler call
set_span_attribute to record user_id and relevant resource ids (e.g.,
installed_id) and for upload_plugin also set upload_size (len(content)) and
add_span_event for filename; ensure the decorators are imported and used
consistently and keep tracing calls near the start of each function so spans
capture the whole handler execution.
backend/app/services/claude_plugin_parser.py (1)

23-26: ⚡ Quick win

Trace and document the parser entrypoint.

parse_package() is a new public backend entrypoint for untrusted uploads, but it has neither a method docstring nor a @trace_sync span with package-level attributes. That will make parse failures much harder to diagnose in production.

As per coding guidelines, "Use descriptive names and docstrings for public functions and classes in Python code" and "Use OpenTelemetry tracing decorators: @trace_async or @trace_sync for entire functions, and add_span_event() or set_span_attribute() for adding details to spans".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/claude_plugin_parser.py` around lines 23 - 26, Add a
concise docstring to the ClaudePluginParser class and a method docstring for
parse_package describing its purpose (parsing and validating untrusted Claude
Code plugin ZIP bytes) and its return type PluginUploadInfo; decorate
parse_package with `@trace_sync` and inside the function set span attributes
(e.g., package_size_bytes, package_sha256 or filename if available) and add span
events around major steps (e.g., "received_package", "validation_started",
"validation_failed") so failures are traceable; ensure any caught exceptions
call add_span_event()/set_span_attribute with error details before re-raising or
returning an error result.
backend/app/services/installed_plugin_service.py (1)

31-159: ⚡ Quick win

Instrument the new service API surface.

upload_plugin(), list_installed_plugins(), update_installed_plugin(), uninstall_installed_plugin(), and package_path_for_download() are new public backend entrypoints, but none of them carry method docstrings or tracing decorators. That leaves install/update failures opaque in production.

As per coding guidelines, "Use descriptive names and docstrings for public functions and classes in Python code" and "Use OpenTelemetry tracing decorators: @trace_async or @trace_sync for entire functions, and add_span_event() or set_span_attribute() for adding details to spans".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/installed_plugin_service.py` around lines 31 - 159, Add
docstrings and OpenTelemetry tracing to the new public methods upload_plugin,
list_installed_plugins, update_installed_plugin, uninstall_installed_plugin, and
package_path_for_download: add a one-sentence docstring explaining the purpose
and parameters for each method, decorate each method with `@trace_sync` (or
`@trace_async` if converted to async) and instrument key steps with
add_span_event() and set_span_attribute() calls — e.g., log start/finish,
user_id and installed_id, parsed.name/checksum/filename in upload_plugin,
updated fields in update_installed_plugin, installState in
uninstall_installed_plugin, and storageKey/path existence checks in
package_path_for_download; ensure you record errors/exceptions to spans before
raising HTTPException or returning.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/api/endpoints/installed_plugins.py`:
- Around line 43-57: In upload_plugin (function upload_plugin in
installed_plugins.py) avoid using await file.read() which buffers the whole
upload; instead enforce the 50MB cap before buffering by first checking the
request/file metadata (Content-Length) and reject with HTTPException(413) if
present and >50MB, and otherwise read the UploadFile in chunks (e.g., loop
calling await file.read(chunk_size)) accumulating bytes up to the 50MB limit and
immediately raise HTTPException(413) when the limit is exceeded; pass the
accumulated bytes to ClaudePluginParser / installed_plugin_service.upload_plugin
as before.

In `@backend/app/services/installed_plugin_service.py`:
- Around line 61-66: The current _safe_kind_name lossily slugs/truncates
parsed.name which causes collisions; change the name generation used when
constructing Kind (the Kind(...) call and the other usage around lines 289-291)
to append a short deterministic hash suffix of the original parsed.name (e.g.,
sha256/sha1 hex truncated to 8-10 chars) so the final name =
slugified_truncated_base + "-" + hash; ensure truncation keeps room for the
suffix (max total 100 chars), use the same deterministic hashing function in
both places, and keep namespace and user_id unchanged so the {namespace, name,
user_id} identity remains unique and stable.

In `@executor/modes/local/capabilities.py`:
- Around line 756-777: The installer currently skips downloads if install_path
exists, so when the backend sends a new checksum for the same
installed_plugin_id we must force-refresh; update the install logic (the branch
around install_path.exists() and calls to _download_plugin_package) to compare
the incoming item["checksum"] with the persisted checksum for that
installed_plugin_id and, if different, treat it as missing: download and replace
the install path and update the stored checksum; ensure this change is applied
to the same pattern used elsewhere (the other install branches that call
_download_plugin_package / check client and item.get("download_path")), and
update the record persistence so installed_plugin_id -> checksum is
saved/updated after a successful download.
- Around line 873-898: In _extract_plugin_zip ensure you never remove the
existing install_path until the ZIP has been fully extracted and validated in a
temp directory: create temp_path (as you already do), extract the archive into
temp_path, run the safety checks and call _normalized_plugin_root on temp_path,
then only after validation succeed atomically replace the existing install_path
(remove or rename install_path and move/replace root → install_path). Move the
existing shutil.rmtree(install_path) out of the pre-extraction path and perform
it (or use os.replace/os.rename for atomic swap) only after
archive.extractall(temp_path) and root normalization succeed, and keep the final
cleanup of temp_path in the existing finally block to avoid leaving a broken
install.

In `@wework/src/api/plugins.ts`:
- Around line 23-44: The uploadPlugin method currently uses raw fetch and reads
the auth token from localStorage; replace it with the shared HttpClient to keep
auth and error handling consistent: in uploadPlugin, build the FormData (as now)
and call HttpClient.post (or an added HttpClient.upload/multipart method) to
POST to `${apiBaseUrl}/plugins/upload`, remove direct localStorage access and
let HttpClient attach Authorization, and preserve throwing an Error on non-ok
responses and returning the parsed InstalledPlugin; if HttpClient lacks FormData
support, add a multipart/form-data codepath to HttpClient (e.g., HttpClient.post
can accept a FormData body and not set Content-Type) and use that from
uploadPlugin.

In `@wework/src/components/plugins/PluginsWorkspace.tsx`:
- Around line 604-618: uploadPlugin currently uses try/finally so upload errors
are swallowed; add an error path similar to uploadPersonalSkill: catch errors
from pluginApi.uploadPlugin and either set a plugin upload error state (e.g.,
setPluginUploadError) so the UI/dialog can show it, and then rethrow the error
so callers (PluginUploadDialog) can handle it, or at minimum rethrow after
setting state; update the uploadPlugin function to include a catch block that
references pluginApi.uploadPlugin, setIsUploadingPlugin,
setShowPluginUploadDialog, setInstalledPlugins, toInstalledPluginItem and
setActiveTab appropriately.

In `@wework/src/components/plugins/PluginUploadDialog.tsx`:
- Around line 24-28: The current .zip check in PluginUploadDialog uses
nextFile.name.endsWith('.zip') which is case-sensitive; change the condition to
compare a lowercased filename (e.g., use
nextFile.name?.toLowerCase().endsWith('.zip')) so names like "plugin.ZIP" pass;
update the if guard that calls setError(...) and setFile(null) to use this
case-insensitive check and ensure you handle a possibly undefined name safely.
- Around line 36-45: The form onSubmit currently calls void onUpload(file) which
discards the Promise and swallows upload failures; change the handler in
PluginUploadDialog's onSubmit to await onUpload(file) (or call
onUpload(file).catch(...)) and in the catch call setError(...) to surface the
backend error to the user (use the existing setError state and t() for i18n),
and optionally manage a local "uploading" flag to disable resubmission while
pending; also add a corresponding .catch to the workspace uploadPlugin function
to reject/propagate errors so failures bubble up to PluginUploadDialog's
handler.

---

Nitpick comments:
In `@backend/app/api/endpoints/installed_plugins.py`:
- Around line 26-120: Add OpenTelemetry tracing to the plugin endpoint handlers
by decorating synchronous handlers (list_installed_plugins,
download_installed_plugin) with `@trace_sync` and async handlers (upload_plugin,
update_installed_plugin, uninstall_installed_plugin) with `@trace_async`; inside
each handler call set_span_attribute to record user_id and relevant resource ids
(e.g., installed_id) and for upload_plugin also set upload_size (len(content))
and add_span_event for filename; ensure the decorators are imported and used
consistently and keep tracing calls near the start of each function so spans
capture the whole handler execution.

In `@backend/app/services/claude_plugin_parser.py`:
- Around line 23-26: Add a concise docstring to the ClaudePluginParser class and
a method docstring for parse_package describing its purpose (parsing and
validating untrusted Claude Code plugin ZIP bytes) and its return type
PluginUploadInfo; decorate parse_package with `@trace_sync` and inside the
function set span attributes (e.g., package_size_bytes, package_sha256 or
filename if available) and add span events around major steps (e.g.,
"received_package", "validation_started", "validation_failed") so failures are
traceable; ensure any caught exceptions call add_span_event()/set_span_attribute
with error details before re-raising or returning an error result.

In `@backend/app/services/installed_plugin_service.py`:
- Around line 31-159: Add docstrings and OpenTelemetry tracing to the new public
methods upload_plugin, list_installed_plugins, update_installed_plugin,
uninstall_installed_plugin, and package_path_for_download: add a one-sentence
docstring explaining the purpose and parameters for each method, decorate each
method with `@trace_sync` (or `@trace_async` if converted to async) and instrument
key steps with add_span_event() and set_span_attribute() calls — e.g., log
start/finish, user_id and installed_id, parsed.name/checksum/filename in
upload_plugin, updated fields in update_installed_plugin, installState in
uninstall_installed_plugin, and storageKey/path existence checks in
package_path_for_download; ensure you record errors/exceptions to spans before
raising HTTPException or returning.

In `@wework/src/api/plugins.ts`:
- Around line 24-26: The uploadPlugin function currently hardcodes enabled to
'true'; change its signature to accept an enabled parameter (e.g.,
uploadPlugin(file: File, enabled = true)) and use that parameter when building
the FormData (replace formData.append('enabled', 'true') with
formData.append('enabled', String(enabled))). Update any call sites of
uploadPlugin if they need to upload a disabled plugin.

In `@wework/src/components/plugins/PluginDetailView.tsx`:
- Around line 79-85: The MCP entries in the components.mcps.map are using a
hardcoded description ('MCP server') instead of the item's metadata; update the
mapping (the block that creates objects with key `mcp-${item.name}`,
componentKey `mcp:${item.name}`, and type 'mcp') to set description to the
actual metadata, e.g. use item.description || item.path || item.name (or similar
fallback) so MCPs follow the same pattern as
skills/commands/agents/hooks/lsps/monitors/bins.

In `@wework/src/components/plugins/PluginUploadDialog.tsx`:
- Around line 76-81: The UI advertises a 50MB limit but PluginUploadDialog never
checks file size client-side; update the selectFile handler in
PluginUploadDialog to validate nextFile.size and reject files >50*1024*1024
bytes, setting the same error flow used by SkillUploadDialog (use the i18n key
plugins_skill_upload_size_error or add a plugins_plugin_upload_size_error if
preferred) and clear/reset state appropriately so the large file is rejected
before upload starts. Ensure you reference the selectFile function and
nextFile.size when adding the check and trigger the existing error display path
used for skill uploads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 068f9ebe-29a4-4f44-bbdb-2744ba0c118f

📥 Commits

Reviewing files that changed from the base of the PR and between 08e6547 and 6218804.

📒 Files selected for processing (19)
  • backend/app/api/api.py
  • backend/app/api/endpoints/installed_plugins.py
  • backend/app/schemas/installed_plugin.py
  • backend/app/services/claude_plugin_parser.py
  • backend/app/services/device/capability_sync_service.py
  • backend/app/services/installed_plugin_service.py
  • executor/modes/local/capabilities.py
  • start.sh
  • tests/test_start_sh.sh
  • wework/src/api/plugins.ts
  • wework/src/components/plugins/PluginCreateMenu.tsx
  • wework/src/components/plugins/PluginDetailView.tsx
  • wework/src/components/plugins/PluginManagementRows.tsx
  • wework/src/components/plugins/PluginManagementWorkspace.tsx
  • wework/src/components/plugins/PluginUploadDialog.tsx
  • wework/src/components/plugins/PluginsWorkspace.tsx
  • wework/src/i18n/locales/en/common.json
  • wework/src/i18n/locales/zh-CN/common.json
  • wework/src/types/api.ts
💤 Files with no reviewable changes (1)
  • tests/test_start_sh.sh

Comment thread backend/app/api/endpoints/installed_plugins.py
Comment thread backend/app/services/installed_plugin_service.py
Comment thread executor/modes/local/capabilities.py Outdated
Comment thread executor/modes/local/capabilities.py
Comment thread wework/src/api/plugins.ts Outdated
Comment thread wework/src/components/plugins/PluginsWorkspace.tsx
Comment thread wework/src/components/plugins/PluginUploadDialog.tsx Outdated
Comment thread wework/src/components/plugins/PluginUploadDialog.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
wework/src/components/chat/composer/ModelSelector.tsx (1)

245-450: 🏗️ Heavy lift

Split the mobile picker out of ModelSelector.

This component now contains two substantially different interaction models plus a large mobile-only render path, which makes future state/layout changes harder to reason about.

Based on learnings, "Responsive architecture: separate components when layout differences exceed 30% or different interaction patterns exist, use Mobile/Desktop component separation or Tailwind responsive classes appropriately".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/chat/composer/ModelSelector.tsx` around lines 245 -
450, ModelSelector mixes desktop and mobile UI/interaction logic (see
renderMobileSheet, renderMobileControlSection,
renderMobileAutomaticReasoningSection and mobile-specific state like
mobileQuery, activateMobileFamily, mobileModels), making it hard to maintain;
extract the entire mobile picker into a new MobileModelSelector component that
owns its mobile-only state and handlers (mobileQuery, activeGroup selection,
activateMobileFamily, handleSelectModel, handleSelectModelOption, mobileModels,
selectedModelOptions), move renderMobile* helpers into that component, keep the
same data-testid values and prop-drill only what’s necessary (current
selectedModel, selectedModelOptions, onClose/closeMenu, onSelectModel callback,
i18n t and controls/familyGroups), and replace the internal renderMobileSheet
call in ModelSelector with <MobileModelSelector .../> so desktop logic/state
stays focused and mobile layout/interaction is isolated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wework/src/components/chat/composer/ModelSelector.tsx`:
- Around line 260-270: The model chip and family tab buttons in the
ModelSelector component use a height of h-10 which is below the 44×44 mobile
hit-area requirement; update the button class strings (the elements rendered
with key={option.value} and the other similar button around
handleSelectModelOption/control.id/option.value) to use h-11 and min-w-[44px]
(e.g., replace 'h-10' with 'h-11 min-w-[44px]') so all interactive elements meet
the 44×44 guideline while preserving the existing rounded, border and text
classes.
- Around line 303-318: The mobile sheet rendered in ModelSelector.tsx is missing
dialog semantics; update the element with data-testid="model-selector-menu" to
include role="dialog" and aria-modal="true", add an id to the title (the h2
rendering t('workbench.model_picker_title')) and set aria-labelledby on the menu
to that id so screen readers announce the title, and ensure focus is moved into
the sheet when opened and returned when closed (implement a simple focus
trap/initial focus within the menu and restore focus on close) while keeping the
existing onClick={event => event.stopPropagation()} behavior.

In `@wework/src/components/layout/MobileWorkbenchLayout.test.tsx`:
- Around line 63-70: The helper renderAtMobileWidth mutates global
window.innerWidth and never resets it; save the original value at the start of
renderAtMobileWidth (const originalInnerWidth = window.innerWidth), set
innerWidth to 400 as you already do, then restore window.innerWidth in an
afterEach cleanup in this test file by redefining the property back to
originalInnerWidth (Object.defineProperty(window, 'innerWidth', { configurable:
true, writable: true, value: originalInnerWidth })) and dispatching a resize
event (window.dispatchEvent(new Event('resize'))) so subsequent tests don’t
inherit the mobile width; reference the renderAtMobileWidth helper and the
file-level afterEach when making the change.

In `@wework/src/components/plugins/PluginsWorkspace.tsx`:
- Line 1084: The search input's height is 40px due to "h-10" and must meet the
44px mobile touch target; update the className on the search input in
PluginsWorkspace.tsx by replacing "h-10" with "h-11" and add a min-height to
guarantee the touch target (e.g., include "min-h-[44px]" or the equivalent
utility), so the class string becomes something like "h-11 min-h-[44px] w-full
rounded-xl border ...".

---

Nitpick comments:
In `@wework/src/components/chat/composer/ModelSelector.tsx`:
- Around line 245-450: ModelSelector mixes desktop and mobile UI/interaction
logic (see renderMobileSheet, renderMobileControlSection,
renderMobileAutomaticReasoningSection and mobile-specific state like
mobileQuery, activateMobileFamily, mobileModels), making it hard to maintain;
extract the entire mobile picker into a new MobileModelSelector component that
owns its mobile-only state and handlers (mobileQuery, activeGroup selection,
activateMobileFamily, handleSelectModel, handleSelectModelOption, mobileModels,
selectedModelOptions), move renderMobile* helpers into that component, keep the
same data-testid values and prop-drill only what’s necessary (current
selectedModel, selectedModelOptions, onClose/closeMenu, onSelectModel callback,
i18n t and controls/familyGroups), and replace the internal renderMobileSheet
call in ModelSelector with <MobileModelSelector .../> so desktop logic/state
stays focused and mobile layout/interaction is isolated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66edd025-0f40-4455-9a99-0e9f4f9192ec

📥 Commits

Reviewing files that changed from the base of the PR and between 6218804 and b111139.

📒 Files selected for processing (33)
  • wework/src/App.plugins.test.tsx
  • wework/src/components/chat/composer/AddContextMenu.tsx
  • wework/src/components/chat/composer/AttachmentBadges.tsx
  • wework/src/components/chat/composer/CompactChatComposer.tsx
  • wework/src/components/chat/composer/ModelSelector.tsx
  • wework/src/components/chat/composer/ProjectChatComposer.tsx
  • wework/src/components/chat/composer/ProjectWorkBar.tsx
  • wework/src/components/chat/composer/SkillSelector.tsx
  • wework/src/components/layout/AppLayout.tsx
  • wework/src/components/layout/DesktopSettingsMenu.tsx
  • wework/src/components/layout/DesktopSidebar.tsx
  • wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
  • wework/src/components/layout/DesktopWorkbenchLayout.tsx
  • wework/src/components/layout/Header.tsx
  • wework/src/components/layout/MobileDrawer.tsx
  • wework/src/components/layout/MobileWorkbenchLayout.test.tsx
  • wework/src/components/layout/MobileWorkbenchLayout.tsx
  • wework/src/components/layout/workspace-panels/BottomWorkspacePanel.tsx
  • wework/src/components/layout/workspace-panels/RightWorkspacePanel.tsx
  • wework/src/components/plugins/McpManagementSections.tsx
  • wework/src/components/plugins/PluginCatalogSections.tsx
  • wework/src/components/plugins/PluginDetailView.tsx
  • wework/src/components/plugins/PluginManagementWorkspace.tsx
  • wework/src/components/plugins/PluginUploadDialog.tsx
  • wework/src/components/plugins/PluginsWorkspace.tsx
  • wework/src/components/plugins/SkillUploadDialog.tsx
  • wework/src/i18n/locales/en/common.json
  • wework/src/i18n/locales/zh-CN/common.json
  • wework/src/pages/LoginPage.tsx
  • wework/src/pages/OidcCallbackPage.tsx
  • wework/src/pages/PluginManagementPage.tsx
  • wework/src/pages/PluginsPage.tsx
  • wework/tailwind.config.js
✅ Files skipped from review due to trivial changes (16)
  • wework/src/components/layout/DesktopSettingsMenu.tsx
  • wework/src/components/chat/composer/AttachmentBadges.tsx
  • wework/src/components/chat/composer/SkillSelector.tsx
  • wework/src/pages/OidcCallbackPage.tsx
  • wework/src/components/layout/workspace-panels/RightWorkspacePanel.tsx
  • wework/src/pages/PluginsPage.tsx
  • wework/src/components/layout/Header.tsx
  • wework/src/components/layout/DesktopWorkbenchLayout.tsx
  • wework/src/components/layout/workspace-panels/BottomWorkspacePanel.tsx
  • wework/src/components/layout/AppLayout.tsx
  • wework/src/components/chat/composer/AddContextMenu.tsx
  • wework/src/components/plugins/PluginCatalogSections.tsx
  • wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
  • wework/src/components/chat/composer/CompactChatComposer.tsx
  • wework/src/components/layout/MobileDrawer.tsx
  • wework/src/components/plugins/McpManagementSections.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • wework/src/i18n/locales/zh-CN/common.json
  • wework/src/i18n/locales/en/common.json
  • wework/src/components/plugins/PluginDetailView.tsx
  • wework/src/components/plugins/PluginUploadDialog.tsx
  • wework/src/components/plugins/PluginManagementWorkspace.tsx

Comment thread wework/src/components/chat/composer/ModelSelector.tsx
Comment thread wework/src/components/chat/composer/ModelSelector.tsx
Comment thread wework/src/components/layout/MobileWorkbenchLayout.test.tsx
Comment thread wework/src/components/plugins/PluginsWorkspace.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
executor/modes/local/capabilities.py (1)

854-868: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid buffering the entire plugin ZIP in memory.

response.content materializes the full archive before any guard runs. A large or malformed response can exhaust the executor before checksum verification or extraction starts. Stream the download to a temp file or enforce a hard byte cap while reading, ideally matching the backend package limit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/modes/local/capabilities.py` around lines 854 - 868, The code
currently uses response.content (materializing the whole ZIP) and then calls
self._extract_plugin_zip(content, install_path); change this to stream the HTTP
response into a temporary file (or a file-like stream) while enforcing a hard
byte cap (match backend package limit) and computing the sha256 checksum
incrementally; after streaming completes, compare the computed actual_checksum
to expected_checksum and only then call a variant of _extract_plugin_zip that
accepts a file path or file-like object (or refactor _extract_plugin_zip to
accept a file), and ensure the temp file is deleted on success or error.
wework/src/components/plugins/PluginUploadDialog.tsx (2)

80-84: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bump the upload dialog actions to 44×44 touch targets.

The close button is 36×36, and the cancel/upload actions are only 40px tall. That misses the mobile minimum from the repo guidelines and makes the dialog harder to use on small screens.

As per coding guidelines, "All interactive elements on mobile must be at least 44px × 44px - use h-11 min-w-[44px] for buttons".

Also applies to: 116-129

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/plugins/PluginUploadDialog.tsx` around lines 80 - 84,
In PluginUploadDialog.tsx update the touch targets for the dialog buttons (the
close button and the cancel/upload action buttons rendered in
PluginUploadDialog) to meet mobile minimums by replacing their current sizing
classes (e.g., "h-9 w-9" and the 40px tall action button classes) with the repo
guideline classes "h-11 min-w-[44px]" (keep existing padding/rounded/text
classes and onClick/aria-label props intact) so all interactive elements in the
dialog are at least 44×44.

48-50: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Expose this modal as an actual dialog.

The upload panel is rendered as a plain overlay/form, so assistive tech won’t announce it as a modal. Add role="dialog", aria-modal="true", and wire it to the existing title via aria-labelledby. Since this dialog already has dedicated tests, adding a stable dialog-level data-testid here would also align with the repo’s testing guidance.

Based on learnings, "Add data-testid attributes to interactive elements (buttons, inputs, links, selects), key UI components, dynamic content containers, and modal dialogs when creating new tests".

Also applies to: 68-72

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/plugins/PluginUploadDialog.tsx` around lines 48 - 50,
The modal overlay in PluginUploadDialog is not exposed as an accessible dialog;
update the root form/div used as the dialog (in the PluginUploadDialog
component) to include role="dialog", aria-modal="true", and aria-labelledby
pointing to the existing title element (give the title a stable id like
"plugin-upload-title"); also add a stable data-testid on the dialog container
(eg. data-testid="plugin-upload-dialog") so tests can target it; apply the same
attributes/data-testid to the alternate dialog markup referenced around the
other block (the secondary form at 68-72) so both dialog render paths are
accessible and testable.
wework/src/components/plugins/PluginDetailView.tsx (2)

173-236: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Increase the mobile hit areas for the detail actions.

The back button, uninstall button, and both switch controls are all below the repo’s 44×44 mobile minimum (h-9, w-9, h-7). On phones this makes the detail view noticeably harder to operate.

As per coding guidelines, "All interactive elements on mobile must be at least 44px × 44px - use h-11 min-w-[44px] for buttons".

Also applies to: 268-295

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/plugins/PluginDetailView.tsx` around lines 173 - 236,
The interactive controls (back button, uninstall button, and toggle switch) have
hit areas smaller than the 44×44 mobile minimum; update the classNames for the
back button (onClick handler onBack), the uninstall button (onClick handler
onUninstall / data-testid `plugin-detail-uninstall-...`), and the toggle switch
button (onClick handler onToggle / data-testid `plugin-detail-toggle-...`) to
use the recommended sizing: replace h-9 or h-7 with h-11 and add min-w-[44px]
where appropriate (e.g., `h-11 min-w-[44px]`) and ensure the inner switch knob
remains visually centered after the size change; apply the same adjustments to
the other action buttons in this component referenced later (the section called
out in the comment).

115-150: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the detail metadata labels.

Version, Author, Source, Package, Homepage, Repository, and the breadcrumb aria-label are hard-coded in English, so the new detail view will still render mixed-language UI under zh-CN.

Also applies to: 169-171

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/plugins/PluginDetailView.tsx` around lines 115 - 150,
The hard-coded English labels in detailRows and the breadcrumb aria-label should
be replaced with localized strings: import/obtain the app's i18n translate
function (e.g., t or useTranslation) in PluginDetailView and replace literal
labels 'Version', 'Author', 'Source', 'Package', 'Homepage', 'Repository' with
t(...) keys (e.g., t('plugin.version'), etc.) inside detailRows, ensure href
logic is unchanged, and also replace the breadcrumb aria-label (currently
hard-coded) with a localized string via the same translate function so all
metadata and aria text are localized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@executor/modes/local/capabilities.py`:
- Around line 893-914: The temp_path and staged_path currently use os.getpid()
which can collide across concurrent syncs in the same process; change the
creation of temp_path and staged_path (used around archive.extractall,
_normalized_plugin_root, and the final os.replace to install_path) to use
per-call unique directories (for example via tempfile.mkdtemp() or appending a
uuid4() suffix or using tempfile.TemporaryDirectory()) so each extraction gets
its own unique temp/staged directories; ensure the rest of the logic (moving
root to staged_path, cleanup in the finally block) still operates on those
unique paths and that cleanup uses ignore_errors=True as before.

---

Outside diff comments:
In `@executor/modes/local/capabilities.py`:
- Around line 854-868: The code currently uses response.content (materializing
the whole ZIP) and then calls self._extract_plugin_zip(content, install_path);
change this to stream the HTTP response into a temporary file (or a file-like
stream) while enforcing a hard byte cap (match backend package limit) and
computing the sha256 checksum incrementally; after streaming completes, compare
the computed actual_checksum to expected_checksum and only then call a variant
of _extract_plugin_zip that accepts a file path or file-like object (or refactor
_extract_plugin_zip to accept a file), and ensure the temp file is deleted on
success or error.

In `@wework/src/components/plugins/PluginDetailView.tsx`:
- Around line 173-236: The interactive controls (back button, uninstall button,
and toggle switch) have hit areas smaller than the 44×44 mobile minimum; update
the classNames for the back button (onClick handler onBack), the uninstall
button (onClick handler onUninstall / data-testid
`plugin-detail-uninstall-...`), and the toggle switch button (onClick handler
onToggle / data-testid `plugin-detail-toggle-...`) to use the recommended
sizing: replace h-9 or h-7 with h-11 and add min-w-[44px] where appropriate
(e.g., `h-11 min-w-[44px]`) and ensure the inner switch knob remains visually
centered after the size change; apply the same adjustments to the other action
buttons in this component referenced later (the section called out in the
comment).
- Around line 115-150: The hard-coded English labels in detailRows and the
breadcrumb aria-label should be replaced with localized strings: import/obtain
the app's i18n translate function (e.g., t or useTranslation) in
PluginDetailView and replace literal labels 'Version', 'Author', 'Source',
'Package', 'Homepage', 'Repository' with t(...) keys (e.g., t('plugin.version'),
etc.) inside detailRows, ensure href logic is unchanged, and also replace the
breadcrumb aria-label (currently hard-coded) with a localized string via the
same translate function so all metadata and aria text are localized.

In `@wework/src/components/plugins/PluginUploadDialog.tsx`:
- Around line 80-84: In PluginUploadDialog.tsx update the touch targets for the
dialog buttons (the close button and the cancel/upload action buttons rendered
in PluginUploadDialog) to meet mobile minimums by replacing their current sizing
classes (e.g., "h-9 w-9" and the 40px tall action button classes) with the repo
guideline classes "h-11 min-w-[44px]" (keep existing padding/rounded/text
classes and onClick/aria-label props intact) so all interactive elements in the
dialog are at least 44×44.
- Around line 48-50: The modal overlay in PluginUploadDialog is not exposed as
an accessible dialog; update the root form/div used as the dialog (in the
PluginUploadDialog component) to include role="dialog", aria-modal="true", and
aria-labelledby pointing to the existing title element (give the title a stable
id like "plugin-upload-title"); also add a stable data-testid on the dialog
container (eg. data-testid="plugin-upload-dialog") so tests can target it; apply
the same attributes/data-testid to the alternate dialog markup referenced around
the other block (the secondary form at 68-72) so both dialog render paths are
accessible and testable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ead98d1f-0e55-4200-83e6-f31d3215756a

📥 Commits

Reviewing files that changed from the base of the PR and between b111139 and 3f101b9.

📒 Files selected for processing (21)
  • backend/app/api/endpoints/installed_plugins.py
  • backend/app/services/installed_plugin_service.py
  • backend/tests/services/test_installed_plugin_upload.py
  • executor/modes/local/capabilities.py
  • executor/tests/test_local_capability_sync_handler.py
  • wework/eslint.config.js
  • wework/package.json
  • wework/scripts/build-mac-app.sh
  • wework/src/api/adapters.test.ts
  • wework/src/api/http.test.ts
  • wework/src/api/http.ts
  • wework/src/api/plugins.ts
  • wework/src/components/chat/composer/ModelSelector.tsx
  • wework/src/components/layout/MobileWorkbenchLayout.test.tsx
  • wework/src/components/plugins/PluginDetailView.tsx
  • wework/src/components/plugins/PluginUploadDialog.test.tsx
  • wework/src/components/plugins/PluginUploadDialog.tsx
  • wework/src/components/plugins/PluginsWorkspace.test.tsx
  • wework/src/components/plugins/PluginsWorkspace.tsx
  • wework/src/i18n/locales/en/common.json
  • wework/src/i18n/locales/zh-CN/common.json
✅ Files skipped from review due to trivial changes (4)
  • wework/eslint.config.js
  • wework/package.json
  • wework/src/i18n/locales/en/common.json
  • wework/src/i18n/locales/zh-CN/common.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • wework/src/components/layout/MobileWorkbenchLayout.test.tsx
  • backend/app/api/endpoints/installed_plugins.py
  • backend/app/services/installed_plugin_service.py
  • wework/src/components/plugins/PluginsWorkspace.tsx

Comment on lines +893 to +914
temp_path = parent / f".{install_path.name}.tmp-{os.getpid()}"
staged_path = parent / f".{install_path.name}.staged-{os.getpid()}"
if temp_path.exists():
shutil.rmtree(temp_path)
if staged_path.exists():
shutil.rmtree(staged_path)
temp_path.mkdir(parents=True)
try:
archive.extractall(temp_path)
root = self._normalized_plugin_root(temp_path)
if root != temp_path:
shutil.move(str(root), str(staged_path))
else:
os.replace(temp_path, staged_path)
if install_path.exists():
shutil.rmtree(install_path)
os.replace(staged_path, install_path)
finally:
if temp_path.exists():
shutil.rmtree(temp_path, ignore_errors=True)
if staged_path.exists():
shutil.rmtree(staged_path, ignore_errors=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use unique staging directories per extraction.

Line 893 and Line 894 derive both temp paths from os.getpid() only. Two syncs for the same plugin in the same process will reuse the same tmp/staged directories, so one run can delete or replace the other's workspace mid-install. Use tempfile.mkdtemp() or another per-call unique suffix here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@executor/modes/local/capabilities.py` around lines 893 - 914, The temp_path
and staged_path currently use os.getpid() which can collide across concurrent
syncs in the same process; change the creation of temp_path and staged_path
(used around archive.extractall, _normalized_plugin_root, and the final
os.replace to install_path) to use per-call unique directories (for example via
tempfile.mkdtemp() or appending a uuid4() suffix or using
tempfile.TemporaryDirectory()) so each extraction gets its own unique
temp/staged directories; ensure the rest of the logic (moving root to
staged_path, cleanup in the finally block) still operates on those unique paths
and that cleanup uses ignore_errors=True as before.

@qdaxb
qdaxb merged commit 2b6086b into wecode-ai:main Jun 3, 2026
22 checks passed
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