Skip to content

feat(plugins): add main window plugins hub#1818

Merged
zerob13 merged 15 commits into
devfrom
codex/plugins-hub-plan
Jun 29, 2026
Merged

feat(plugins): add main window plugins hub#1818
zerob13 merged 15 commits into
devfrom
codex/plugins-hub-plan

Conversation

@zerob13

@zerob13 zerob13 commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add main-window /plugins routes with top tabs for Plugins, Skills, and MCP
  • move visible entry points for Plugins-owned pages out of Settings while keeping compatibility redirects
  • update expanded sidebar commands and workspace grouping
  • fold Remote channels into plugin catalog cards; remote detail uses unified /plugins/:pluginId virtual ids such as remote:telegram
  • use plugin-style top-level enable/disable controls for Remote virtual plugin detail pages
  • merge Feishu/Lark Remote configuration into the Feishu/Lark Integration detail page
  • use the Feishu/Lark Integration top-level enable control for both the official plugin and Feishu/Lark Remote
  • auto-enable legacy remote channel configs when credentials/accounts exist and no explicit enabled flag was stored
image image image 620aac125078e7a93e4591182042bd6b

UI Layout

Before:

AppBar
├─ rail
└─ expanded sidebar
   ├─ 所有 Agents
   ├─ search input
   ├─ pinned sessions
   └─ projects

After:

AppBar
├─ rail
└─ expanded sidebar
   ├─ 所有 Agents
   ├─ New Chat
   ├─ Search
   ├─ Plugins
   │
   ├─ Pinned, if any
   ├─ Chat
   └─ 工作区 + project sort toggle

Main content
└─ /plugins
   ├─ [Plugins] [Skills] [MCP]
   ├─ catalog: official plugins + Remote virtual plugins
   └─ detail: /plugins/:pluginId

Validation

  • pnpm run format
  • pnpm run i18n
  • pnpm run lint
  • pnpm run typecheck
  • pnpm exec vitest run test/main/presenter/remoteControlPresenter/remoteBindingStore.test.ts test/renderer/components/WindowSideBar.test.ts test/renderer/components/RemoteSettings.test.ts
  • pnpm exec vitest run test/renderer/components/WindowSideBar.test.ts test/renderer/components/McpIndicator.test.ts test/renderer/components/McpSettings.test.ts test/renderer/components/RemoteSettings.test.ts test/renderer/components/WelcomePage.test.ts test/renderer/lib/storeInitializer.test.ts
  • pnpm exec vitest run test/renderer/components/OfficialPluginDetailPage.test.ts test/renderer/components/RemoteSettings.test.ts

Summary by CodeRabbit

  • New Features
    • Added a new Plugins hub with dedicated pages for plugins, skills, and MCP.
    • Introduced skill management tabs for library, agents, and sync directory, plus Git install and import/export flows.
    • Added plugin details for official and remote plugins with in-app enable/disable actions.
  • Bug Fixes
    • Improved navigation to plugin-related settings and pages.
    • Updated sidebar grouping and route labels for clearer access.
  • Documentation
    • Added and updated specs and task lists for plugins, skills, and remote behavior.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 75ca9094-5f34-424c-93b3-430f2f78b02d

📥 Commits

Reviewing files that changed from the base of the PR and between 0973d8a and faf576b.

📒 Files selected for processing (9)
  • docs/architecture/agent-plan-task-refactor/acp-plan-reachability.md
  • docs/architecture/agent-plan-task-refactor/plan.md
  • docs/architecture/agent-plan-task-refactor/tasks.md
  • docs/features/remote-feishu-lark-scan-auth/plan.md
  • docs/features/remote-feishu-lark-scan-auth/tasks.md
  • docs/issues/pr1818-plugin-review-fixes/spec.md
  • src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue
  • test/renderer/components/OfficialPluginDetailPage.test.ts
  • test/renderer/components/PluginsCatalogPage.test.ts
💤 Files with no reviewable changes (5)
  • docs/architecture/agent-plan-task-refactor/tasks.md
  • docs/architecture/agent-plan-task-refactor/plan.md
  • docs/features/remote-feishu-lark-scan-auth/plan.md
  • docs/architecture/agent-plan-task-refactor/acp-plan-reachability.md
  • docs/features/remote-feishu-lark-scan-auth/tasks.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/renderer/components/PluginsCatalogPage.test.ts
  • test/renderer/components/OfficialPluginDetailPage.test.ts
  • src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue

📝 Walkthrough

Walkthrough

Introduces a /plugins main-window route (PluginsHub, catalog, and official plugin detail pages) that consolidates Skills, MCP, and Remote channels previously managed in separate Settings windows. Overhauls SkillPresenter and SkillSyncPresenter with database-backed management state, git/sync-directory install flows, and agent skill adoption/link operations. Refactors the Skills settings UI into Library/Agents/Sync Directory tabs. Updates sidebar navigation, deeplink routing, and 22-locale i18n strings.

Changes

Plugins Hub and Skills Management

Layer / File(s) Summary
Shared types and route contracts
src/shared/types/skillManagement.ts, src/shared/types/skillSync.ts, src/shared/types/skill.ts, src/shared/contracts/routes/skills.routes.ts, src/shared/contracts/routes/skillSync.routes.ts, src/shared/contracts/routes.ts, src/shared/contracts/events/skills.events.ts
Adds SkillManagementItem, UnifiedSkillItem, AgentSkillItem, and related types; defines Zod-backed route contracts for catalog listing, disable, git scan/install, sync-directory export/import, and all agent adoption/linking endpoints; expands skillsCatalogChangedEvent reason enum.
SkillPresenter: management state, install, git, and sync flows
src/main/presenter/skillPresenter/index.ts
Migrates per-skill sidecar JSON to centralized skills.managementState; adds unified catalog merge; expands install flows with source provenance; implements git clone/scan/install and sync-directory export/import; adds registerAdoptedSkill/registerAgentSkillLink/removeAgentSkillLink; updates backup location and legacy sidecar migration.
SkillSyncPresenter: agent scan, adopt, and link operations
src/main/presenter/skillSyncPresenter/index.ts
Adds scanSkillAgents, scanSkillAgent, getAgentSkillDetail; implements preview/execute for agent skill adoption (temp/backup/symlink) and DeepChat-to-agent linking; adds repairAgentSkillLink/removeAgentSkillLink; adds classification helpers and path-safety guards.
Main-process route dispatcher and config sensitive keys
src/main/presenter/configPresenter/configDbStores.ts, src/main/routes/index.ts
Adds skills.managementState to SENSITIVE_APP_SETTING_KEYS; wires all new skills catalog, git, sync-directory, and skill-sync agent/adoption/link routes in dispatchDeepchatRoute.
Renderer API clients and skills store
src/renderer/api/SkillClient.ts, src/renderer/api/SkillSyncClient.ts, src/renderer/src/stores/skillsStore.ts
Expands SkillClient with unified catalog, disable, git, and sync-directory methods; extends SkillSyncClient with agent scan, adopt, and link methods; updates skillsStore state type to UnifiedSkillItem[] and adds setSkillDisabled action.
Plugins Hub router and page components
src/renderer/src/router/index.ts, src/renderer/src/pages/plugins/*
Adds /plugins route family; introduces PluginsHubPage (tabbed shell), PluginsCatalogPage (official + remote virtual items), OfficialPluginDetailPage (local + remote:<channel> routing with RemoteSettings embed), McpPluginsPage, and SkillsPluginsPage.
Skills UI component refactor
src/renderer/settings/components/skills/*
Refactors SkillsSettings into Library/Agents/Sync Directory tabs with a DropdownMenu for installs; replaces SkillCard with UnifiedSkillItem props; removes SkillEditorSheet; adds SkillDetailDialog, AdoptSkillDialog, AgentSkillTable, InstallFromGitDialog, InstallSkillToAgentDialog, SkillAgentsTab, and SkillImportExportTab.
Navigation, sidebar, deeplink, and settings wiring
src/shared/settingsNavigation.ts, src/renderer/src/components/WindowSideBar.vue, src/renderer/settings/components/RemoteSettings.vue, src/renderer/settings/components/SettingsOverview.vue, src/renderer/src/components/chat-input/McpIndicator.vue, src/renderer/src/components/chat-input/SkillsIndicator.vue, src/main/presenter/deeplinkPresenter/index.ts, src/main/presenter/remoteControlPresenter/types.ts, src/renderer/settings/lib/guidedOnboardingSettings.ts, src/renderer/src/lib/storeInitializer.ts, src/renderer/src/pages/WelcomePage.vue, src/renderer/src/stores/ui/spotlight.ts, src/renderer/settings/components/McpSettings.vue
Hides settings-mcp/remote/plugins/skills from sidebar; adds hasRoute-guarded redirects to plugins-* routes; rewrites WindowSideBar with command-list header and Chat/Workspace session split; adds embedded/singleChannel/hideHeader/hideChannelToggle props to RemoteSettings; removes MCP from SettingsOverview; redirects MCP deeplink to chat window; updates remote credential-presence-based enabled inference.
i18n strings for all 22 locales
src/renderer/src/i18n/*/chat.json, src/renderer/src/i18n/*/routes.json, src/renderer/src/i18n/*/settings.json
Adds sidebar.searchCommand/chatSection/workspace, plugins route labels, new pluginsHub section, and expanded skills management strings (tabs, agents, git, importExport, detail, installToAgent, enable/disable) across all 22 locales; zh-CN also updates remote Telegram/Feishu descriptions.
Tests
test/main/presenter/skillPresenter/skillPresenter.test.ts, test/main/presenter/skillSyncPresenter/index.test.ts, test/main/routes/contracts.test.ts, test/renderer/api/clients.test.ts, test/renderer/components/OfficialPluginDetailPage.test.ts, test/renderer/components/PluginsCatalogPage.test.ts, test/renderer/components/SkillEditorSheet.test.ts, test/main/presenter/remoteControlPresenter/remoteBindingStore.test.ts
Adds/updates tests for management-state persistence, git/sync flows, agent classification, adoption/link enforcement, route catalog coverage, renderer client wiring, plugin detail/catalog page rendering, and remote legacy-enablement inference.
Specification, plan, and task documentation
docs/features/*, docs/issues/*, docs/architecture/*
Adds spec/plan/tasks for deepchat-skills-management and plugins-hub; adds smaller issue specs (CUA/Feishu copy, hub UI, remote icon, sidebar sort, remote copy, PR review fixes); removes completed architecture docs.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant WindowSideBar
  participant PluginsHubPage
  participant PluginsCatalogPage
  participant OfficialPluginDetailPage
  participant PluginClient
  participant RemoteControlClient

  User->>WindowSideBar: click Plugins command
  WindowSideBar->>PluginsHubPage: router.push({ name: 'plugins' })
  PluginsHubPage->>PluginsCatalogPage: render index child route
  PluginsCatalogPage->>PluginClient: listPlugins()
  PluginsCatalogPage->>RemoteControlClient: listChannels() + getChannelStatus() per channel
  PluginsCatalogPage-->>User: sorted catalog (enabled first)
  User->>PluginsCatalogPage: click item
  PluginsCatalogPage->>OfficialPluginDetailPage: navigate /plugins/:pluginId
  OfficialPluginDetailPage->>PluginClient: getPlugin(pluginId) [if local plugin]
  OfficialPluginDetailPage->>RemoteControlClient: getChannelStatus [if remote:]
  User->>OfficialPluginDetailPage: click Enable/Disable
  OfficialPluginDetailPage->>PluginClient: enablePlugin / disablePlugin
  OfficialPluginDetailPage->>RemoteControlClient: saveChannelSettings [if Feishu]
Loading
sequenceDiagram
  participant UI
  participant SkillSyncClient
  participant SkillSyncPresenter
  participant SkillPresenter
  participant Filesystem

  UI->>SkillSyncClient: previewAdoptAgentSkill({ agentId, skillName })
  SkillSyncClient->>SkillSyncPresenter: previewAdoptAgentSkill
  SkillSyncPresenter->>Filesystem: resolveAdoptionSource + readAdoptableSkill(SKILL.md)
  SkillSyncPresenter-->>UI: AdoptAgentSkillPreview (conflict, targetName, warnings)
  UI->>SkillSyncClient: executeAdoptAgentSkill(input)
  SkillSyncClient->>SkillSyncPresenter: executeAdoptAgentSkill
  SkillSyncPresenter->>Filesystem: prepareAdoptionTemp → rename to target → backup agent dir → createDirectoryLink
  SkillSyncPresenter->>SkillPresenter: registerAdoptedSkill (writes skills.managementState)
  SkillSyncPresenter-->>UI: AdoptAgentSkillResult { success, skillName }
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • ThinkInAIXYZ/deepchat#1138: Both PRs modify handleMcpInstall in deeplinkPresenter/index.ts, changing how the MCP install deeplink payload is routed to a window.
  • ThinkInAIXYZ/deepchat#1266: This PR extends the skills management implementation from PR #1266, adding unified catalog/management state and agent/link/sync-directory operations on top of the existing skills presenter.
  • ThinkInAIXYZ/deepchat#1659: Both PRs modify SENSITIVE_APP_SETTING_KEYS in configDbStores.ts; this PR adds skills.managementState to the sensitive keys handled by that mechanism.

Suggested reviewers

  • deepinfect

Poem

🐇 A bunny built a hub so grand,
With plugins lined up, neatly planned.
Skills adopt their agents now,
Git repos installed — and how!
The sidebar sings a command refrain,
No more settings window pain! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a main-window plugins hub.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 codex/plugins-hub-plan

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.

@zerob13
zerob13 marked this pull request as ready for review June 29, 2026 06:37

@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.

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (27)
src/main/presenter/skillPresenter/index.ts-2409-2413 (1)

2409-2413: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Clean up DeepChat-created agent links before deleting management state.

Deleting the management item also drops agentLinks, so any symlinks created in external agent directories remain behind but can no longer be identified as DeepChat-owned for repair/removal.

🤖 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 `@src/main/presenter/skillPresenter/index.ts` around lines 2409 - 2413, The
cleanup path in cleanupUninstalledSkillState currently deletes the skill
management item before removing DeepChat-created agent links, which leaves
external symlinks orphaned. Update this method so it first identifies and cleans
up any DeepChat-owned agent links associated with the skill name, then deletes
the management state via deleteSkillManagementItem. Keep the change scoped to
the cleanupUninstalledSkillState flow and use the existing isSafeSkillName guard
plus the agent-link management helpers already used in SkillPresenter.
src/main/presenter/skillPresenter/index.ts-1893-1904 (1)

1893-1904: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Restore the backup if replacement install fails after moving the old skill.

Overwrite/replacement moves the existing skill to backup before copyDirectory, manifest rewrite, parsing, and state updates. If any later step fails, the old skill is left only in backup and the target can be partially written.

🤖 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 `@src/main/presenter/skillPresenter/index.ts` around lines 1893 - 1904, The
replacement install flow in skillPresenter’s install path moves the existing
skill aside before copyDirectory and subsequent manifest/parsing/state updates,
but it does not roll back if a later step fails. Update the replace/overwrite
branch around prepareExistingSkillTargetForInstall, copyDirectory, and the later
rewrite/parsing/cache updates so any failure restores the backup back into place
and leaves the original skill intact, rather than only existing in backup or a
partially written target.
src/main/presenter/skillSyncPresenter/index.ts-899-919 (1)

899-919: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Rollback adoption if management-state registration fails.

After the agent directory is moved and the symlink is created, registerAdoptedSkill can still throw. The catch block only removes tempPath, leaving a DeepChat skill folder, an agent symlink, and a backup without management 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 `@src/main/presenter/skillSyncPresenter/index.ts` around lines 899 - 919,
Rollback the adoption flow in skillSyncPresenter when `registerAdoptedSkill`
fails, not just when the move/link step fails. Update the adoption logic around
`createDirectoryLink`, `pathExists`, and `skillPresenter.registerAdoptedSkill`
so that any exception after the agent directory is moved and the symlink is
created restores `preview.agentPath` from `backupPath` and removes
`preview.targetPath` if it was created. Make sure the cleanup happens for
failures thrown by `registerAdoptedSkill` as well, leaving no DeepChat skill
folder, symlink, or backup behind without management state.
src/main/presenter/skillPresenter/index.ts-1489-1497 (1)

1489-1497: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject empty sync-directory input before resolving it.

path.resolve(input.skillsDirectory.trim()) turns an empty string into the process CWD, so saving a blank value can create/use ./skills unexpectedly.

🐛 Proposed fix
-    const skillsDirectory = path.resolve(input.skillsDirectory.trim())
+    const trimmedSkillsDirectory = input.skillsDirectory.trim()
+    if (!trimmedSkillsDirectory) {
+      throw new Error('Skills sync directory is required')
+    }
+    const skillsDirectory = path.resolve(trimmedSkillsDirectory)
🤖 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 `@src/main/presenter/skillPresenter/index.ts` around lines 1489 - 1497, Reject
empty sync-directory input before calling path.resolve in the skill sync setup.
In the presenter logic that builds skillsDirectory and SkillSyncDirectoryConfig,
validate input.skillsDirectory.trim() first and return an error or skip saving
when it is blank, so an empty value cannot fall through to the process CWD. Keep
the existing path.resolve and fs.mkdirSync behavior only for non-empty,
user-provided directories.
src/main/presenter/skillSyncPresenter/index.ts-1041-1047 (1)

1041-1047: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Remove the symlink if link registration fails.

createDirectoryLink succeeds before registerAgentSkillLink; if state persistence fails, the external agent gets a DeepChat symlink that is not marked as DeepChat-owned and cannot be removed through the new link-management flow.

🐛 Proposed fix
       try {
         await this.createDirectoryLink(item.sourcePath, item.targetPath)
-        await this.skillPresenter.registerAgentSkillLink({
-          skillName: item.skillName,
-          agentId: input.agentId,
-          agentPath: item.targetPath
-        })
+        try {
+          await this.skillPresenter.registerAgentSkillLink({
+            skillName: item.skillName,
+            agentId: input.agentId,
+            agentPath: item.targetPath
+          })
+        } catch (error) {
+          await fs.promises.rm(item.targetPath, { recursive: true, force: true }).catch(() => undefined)
+          throw error
+        }
         result.linked += 1
🤖 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 `@src/main/presenter/skillSyncPresenter/index.ts` around lines 1041 - 1047, The
symlink creation in skillSyncPresenter’s flow succeeds before
registerAgentSkillLink, so if registration fails the link is left behind without
DeepChat ownership metadata. Update the try/catch around createDirectoryLink and
skillPresenter.registerAgentSkillLink to clean up by removing the newly created
directory link when registration throws, using the existing
link-management/remove helper associated with createDirectoryLink so the
operation is fully rolled back.
src/main/presenter/skillPresenter/index.ts-2149-2149 (1)

2149-2149: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Guard git clone against option-like repo URLs.
repoUrl comes directly from user input in src/main/presenter/skillPresenter/index.ts:2149, so a value starting with - can still be parsed by git clone as another option. Add -- before the repository argument.

🛡️ Proposed fix
-      await execFileAsync('git', ['clone', '--depth', '1', repoUrl, cloneDir], {
+      await execFileAsync('git', ['clone', '--depth', '1', '--', repoUrl, cloneDir], {
🤖 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 `@src/main/presenter/skillPresenter/index.ts` at line 2149, The git clone call
in skillPresenter’s repository setup is passing user-controlled repoUrl directly
as an argument, so option-like values can be interpreted by git. Update the
execFileAsync invocation in the clone flow to insert a `--` separator before
repoUrl so it is always treated as the repository target, and keep the rest of
the clone arguments unchanged.
src/main/presenter/skillPresenter/index.ts-1446-1450 (1)

1446-1450: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use path.dirname() for multi-skill Git source paths.

relativePath is built with path.join(...), so on Windows it contains \ separators and the /SKILL\.md$/ regex will not strip the manifest filename. Multi-skill Git installs then pass a SKILL.md file path as the source directory.

🐛 Proposed fix
         const sourceDir =
           scan.repoFormat === 'single-skill'
             ? cloneDir
-            : path.join(cloneDir, item.relativePath.replace(/\/SKILL\.md$/, ''))
+            : path.join(cloneDir, path.dirname(item.relativePath))
🤖 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 `@src/main/presenter/skillPresenter/index.ts` around lines 1446 - 1450, The
multi-skill Git source path handling in the skill install flow is stripping the
manifest with a POSIX-only regex, so Windows paths built via path.join() keep
SKILL.md in the source directory. Update the sourceDir logic in
skillPresenter/index.ts to use path.dirname() for the multi-skill case (while
keeping single-skill behavior unchanged) so the directory is derived correctly
regardless of path separator. Reference the sourceDir calculation that uses
scan.repoFormat, cloneDir, and item.relativePath to locate the fix.
src/renderer/src/i18n/de-DE/chat.json-364-366 (1)

364-366: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Translate the new sidebar labels in the German locale.

These three entries are still English, so the updated sidebar will render partly untranslated for de-DE.

🤖 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 `@src/renderer/src/i18n/de-DE/chat.json` around lines 364 - 366, The new
sidebar labels in the de-DE locale are still using English strings, so update
the translation entries for the chat sidebar labels in the German locale file.
Locate the sidebar label keys in the de-DE chat localization object, and replace
the values for searchCommand, chatSection, and workspace with proper German
translations so the updated sidebar renders fully localized.
src/renderer/src/i18n/da-DK/chat.json-348-350 (1)

348-350: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Localize the new sidebar labels for the Danish locale.

searchCommand, chatSection, and workspace are still English here, so the new sidebar section ships with mixed-language UI for da-DK.

🤖 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 `@src/renderer/src/i18n/da-DK/chat.json` around lines 348 - 350, The Danish
locale strings for the sidebar are still in English, so update the
`searchCommand`, `chatSection`, and `workspace` entries in the `da-DK/chat.json`
translation file to proper Danish text. Keep the keys unchanged and only replace
the values so the new sidebar labels are fully localized for the `chatSection`
and related sidebar UI.
src/renderer/src/i18n/ru-RU/chat.json-348-350 (1)

348-350: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Translate the new sidebar labels in ru-RU.

searchCommand, chatSection, and workspace are still English, so the sidebar update is only partially localized for Russian users.

🤖 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 `@src/renderer/src/i18n/ru-RU/chat.json` around lines 348 - 350, The new
sidebar labels in the ru-RU translation file are still in English, so localize
the `searchCommand`, `chatSection`, and `workspace` entries to Russian in the
JSON resource used by the sidebar. Update the corresponding keys in the `ru-RU`
locale so the translated labels match the rest of the Russian UI.
src/renderer/src/i18n/pt-BR/settings.json-1824-1824 (1)

1824-1824: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Localize the new pt-BR strings before shipping this feature.

A lot of the added copy is still English (envWarning, card actions, enable/disable toasts, most of the agents workflow, and several pluginsHub labels). That leaves the new Plugins/Skills UI mixed-language for Brazilian Portuguese users.

Also applies to: 1959-2166, 2750-2759

🤖 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 `@src/renderer/src/i18n/pt-BR/settings.json` at line 1824, The new pt-BR
settings copy is still partially in English, leaving the Plugins/Skills UI
mixed-language for Brazilian Portuguese users. Translate the remaining strings
in the pt-BR localization file, including envWarning, the card action labels,
enable/disable toast messages, the agents workflow text, and the pluginsHub
labels, and verify the referenced settings sections all use localized text
consistently.
src/renderer/src/i18n/ru-RU/settings.json-1824-1824 (1)

1824-1824: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Complete the Russian localization for the new Skills and Plugins Hub flows.

This block still contains a lot of English UI copy (envWarning, enable/disable messages, agents/adoption/sync dialogs, and pluginsHub.subtitle/manage/pluginNotFound/capabilities/actionResult). The new feature will otherwise ship half-translated in ru-RU.

Also applies to: 1959-2166, 2750-2759

🤖 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 `@src/renderer/src/i18n/ru-RU/settings.json` at line 1824, The ru-RU settings
localization is incomplete for the new Skills and Plugins Hub flows, leaving
several UI strings in English. Update the affected entries in settings.json for
the Skills/Plugins Hub-related keys such as envWarning, enable/disable messages,
agents/adoption/sync dialogs, and
pluginsHub.subtitle/manage/pluginNotFound/capabilities/actionResult so they are
fully translated into Russian and consistent with the existing ru-RU tone.
src/renderer/src/i18n/da-DK/settings.json-1757-1757 (1)

1757-1757: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Finish localizing the new Skills and Plugins Hub copy in da-DK.

A large part of the newly added surface is still English here (envWarning, enable/disable toasts, tabs/agents flows, and pluginsHub.subtitle/manage/pluginNotFound/capabilities/actionResult). Danish users will get a mixed-language experience across the new feature.

Also applies to: 1892-2099, 2750-2759

🤖 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 `@src/renderer/src/i18n/da-DK/settings.json` at line 1757, The new Skills and
Plugins Hub copy in settings.json is still partially English, causing
mixed-language UI for da-DK users. Localize the remaining strings in the
Skills/Plugins Hub section, including envWarning, the enable/disable toast
messages, the tabs/agents flow copy, and
pluginsHub.subtitle/manage/pluginNotFound/capabilities/actionResult. Use the
surrounding translated keys in this file as a guide and keep the terminology
consistent across the related settings entries.
src/renderer/src/i18n/de-DE/settings.json-1928-1945 (1)

1928-1945: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Complete the German localization for the new Skills/Plugins copy.

Large parts of the added de-DE strings are still English (Enabled, Library, Refresh, Plugin not found., the env warning, most agent dialogs, etc.), so German users will get a mixed-language UI throughout the new flows. Please either translate these entries before merge or intentionally rely on the en-US fallback instead of shipping partial manual translations.

Also applies to: 1967-1967, 2077-2180, 2741-2749

🤖 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 `@src/renderer/src/i18n/de-DE/settings.json` around lines 1928 - 1945, The new
Skills/Plugins German locale is only partially translated, leaving many added
`de-DE` keys in English and causing a mixed-language UI. Update the missing copy
in the `settings.json` locale entries for the Skills/Plugins flows, including
the `enable`/`disable` blocks and the related agent/dialog strings referenced in
this diff, or remove the partial manual translations so the app cleanly falls
back to `en-US`. Make sure the translated values are consistent across the newly
added settings, plugin, and agent-related messages so the full workflow appears
in German.
src/renderer/src/i18n/tr-TR/settings.json-1928-1945 (1)

1928-1945: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Finish the Turkish localization before shipping these flows.

A large share of the new tr-TR copy is still English (Enabled, Library, Agents, Refresh, Plugin not found., the env warning, most agent actions/statuses, etc.), which leaves the new Skills/Plugins UI noticeably mixed-language. Please translate these keys now or fall back to en-US for the unfinished entries.

Also applies to: 1967-1967, 2077-2180, 2741-2749

🤖 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 `@src/renderer/src/i18n/tr-TR/settings.json` around lines 1928 - 1945, The
Turkish locale in settings.json is still mixed with English for several
Skills/Plugins and agent-related labels, so finish translating the remaining
tr-TR entries or route them to en-US defaults. Update the affected string groups
around the existing keys such as enabled/disabled, library/agents/refresh,
plugin error/status messages, and the agent action/status sections so the UI is
consistently localized. Use the surrounding translation blocks in this file to
locate and complete the unfinished copy.
src/renderer/src/stores/ui/spotlight.ts-254-256 (1)

254-256: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep Plugins and Skills reachable from Spotlight.

This blanket hiddenInSidebar filter drops settings-plugins and settings-skills from search results, but the new explicit action handling in this store only replaces MCP and Remote. After this change, Spotlight can no longer navigate to the Plugins hub or Skills page.

Suggested fix
   const buildSettingMatches = (normalizedQuery: string): SpotlightItem[] =>
     SETTINGS_NAVIGATION_ITEMS.filter(
-      (item) => item.routeName !== 'settings-provider' && !item.hiddenInSidebar
+      (item) =>
+        item.routeName !== 'settings-provider' &&
+        (!item.hiddenInSidebar ||
+          item.routeName === 'settings-plugins' ||
+          item.routeName === 'settings-skills')
     )
🤖 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 `@src/renderer/src/stores/ui/spotlight.ts` around lines 254 - 256, The
Spotlight settings item filter in the store is too broad and removes Plugins and
Skills from search. Update the filtering logic around SETTINGS_NAVIGATION_ITEMS
so it still excludes the intended sidebar-hidden entries but preserves
settings-plugins and settings-skills, since the explicit action handling in this
store only replaces MCP and Remote. Use the existing routeName checks in this
area to narrow the filter instead of applying hiddenInSidebar universally.
src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue-241-248 (1)

241-248: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Refresh the embedded Feishu settings after the page-level toggle.

enablePlugin / disablePlugin also update feishu.remoteEnabled, but this embedded RemoteSettings instance never reloads afterwards. The inner Feishu controls will keep reading the old feishuSettings.remoteEnabled value until the user leaves the page.

Suggested fix
         <RemoteSettings
           v-if="isFeishuPlugin"
+          :key="`feishu:${remoteSettingsVersion}`"
           channel="feishu"
           embedded
           hide-channel-toggle
           hide-header
           single-channel
         />
   void runPluginAction(async () => {
     const result = await pluginClient.enablePlugin(currentPlugin.id)
     if (result.ok && currentPlugin.id === FEISHU_PLUGIN_ID) {
       await setFeishuRemoteEnabled(true)
+      remoteSettingsVersion.value += 1
     }
     return result
   })
 }
@@
   void runPluginAction(async () => {
     const result = await pluginClient.disablePlugin(currentPlugin.id)
     if (result.ok && currentPlugin.id === FEISHU_PLUGIN_ID) {
       await setFeishuRemoteEnabled(false)
+      remoteSettingsVersion.value += 1
     }
     return result
   })
 }

Also applies to: 495-510

🤖 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 `@src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue` around lines 241
- 248, The embedded Feishu RemoteSettings view is not reloaded after the
page-level enablePlugin/disablePlugin toggle updates feishu.remoteEnabled, so
the inner controls keep stale state. Update OfficialPluginDetailPage to make the
embedded RemoteSettings react to the remoteEnabled change by forcing a
refresh/remount or passing the changing value as a reactive key/prop, using the
existing isFeishuPlugin, enablePlugin, disablePlugin, and feishuSettings
references so the embedded settings re-read the latest state immediately.
src/renderer/src/components/chat-input/McpIndicator.vue-611-613 (1)

611-613: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the settings fallback for routers without the hub route.

Line 612 assumes plugins-mcp is always registered. The other new entry points in this PR guard plugins-* redirects with router.hasRoute(...), so this button is now the path that breaks when the component is mounted under a router without that route. Keep the old settings-mcp fallback here.

Proposed fix
+import { createSettingsClient } from '`@api/SettingsClient`'
...
 const { t } = useI18n()
 const router = useRouter()
+const settingsClient = createSettingsClient()
...
 const openSettings = async () => {
-  await router.push({ name: 'plugins-mcp' })
+  if (router.hasRoute('plugins-mcp')) {
+    await router.push({ name: 'plugins-mcp' })
+  } else {
+    await settingsClient.openSettings({ routeName: 'settings-mcp' })
+  }
   panelOpen.value = false
 }
🤖 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 `@src/renderer/src/components/chat-input/McpIndicator.vue` around lines 611 -
613, The openSettings handler in McpIndicator.vue should not assume the hub
route is always registered; it needs the same route fallback used elsewhere in
this PR. Update openSettings to check router.hasRoute for plugins-mcp and
navigate to settings-mcp when that route is unavailable, while still closing
panelOpen afterward. Use the openSettings function and the router.push call as
the main place to apply the fallback logic.
src/renderer/src/components/chat-input/SkillsIndicator.vue-79-82 (1)

79-82: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard plugins-skills navigation and keep the legacy fallback.

Line 80 has the same missing-route assumption as the MCP indicator. Other updated entry points in this PR check router.hasRoute(...) before redirecting to plugin pages, but this one dropped the old settings fallback entirely, so the settings button stops working anywhere plugins-skills is not registered.

Proposed fix
+import { createSettingsClient } from '`@api/SettingsClient`'
...
 const { t } = useI18n()
 const router = useRouter()
+const settingsClient = createSettingsClient()
...
-const openSettings = () => {
-  void router.push({ name: 'plugins-skills' })
+const openSettings = async () => {
+  if (router.hasRoute('plugins-skills')) {
+    await router.push({ name: 'plugins-skills' })
+  } else {
+    await settingsClient.openSettings({ routeName: 'settings-skills' })
+  }
   panelOpen.value = false
 }
🤖 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 `@src/renderer/src/components/chat-input/SkillsIndicator.vue` around lines 79 -
82, The openSettings navigation in SkillsIndicator.vue assumes the
plugins-skills route always exists, which removes the legacy settings fallback.
Update openSettings to first check router.hasRoute for the plugins-skills route
and only push there when present; otherwise preserve the old settings
destination used before this change. Keep panelOpen.value reset behavior
unchanged.
src/renderer/settings/components/skills/SkillDetailDialog.vue-255-289 (1)

255-289: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve unknown frontmatter fields when saving.

Lines 255-265 only hydrate description and allowedTools, then Lines 268-289 rebuild the YAML from { name, description, allowedTools }. Saving an unchanged skill will drop every other existing frontmatter key, which corrupts metadata this dialog does not edit.

🛠️ Suggested fix
 const buildSkillContent = () => {
-  const frontmatterData: Record<string, unknown> = {
-    name: props.name,
-    description: editDescription.value
-  }
+  const { frontmatter } = parseSkillContent(props.markdown)
+  const frontmatterData: Record<string, unknown> = {
+    ...frontmatter,
+    name: props.name,
+    description: editDescription.value
+  }
@@
   if (tools.length) {
     frontmatterData.allowedTools = tools
+  } else {
+    delete frontmatterData.allowedTools
   }
🤖 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 `@src/renderer/settings/components/skills/SkillDetailDialog.vue` around lines
255 - 289, Preserve existing frontmatter keys when saving in SkillDetailDialog
by carrying through all unknown metadata from parseSkillContent, not just name,
description, and allowedTools. Update hydrateEditor/buildSkillContent to parse
and store the original frontmatter (or a passthrough object) and merge edited
fields back into it before yaml.stringify, so unchanged skill saves do not drop
metadata.
src/renderer/settings/components/skills/SkillsSettings.vue-316-333 (1)

316-333: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Ignore stale detail loads before updating dialog state.

Line 318 stores the selected skill before Line 323 awaits readSkillFile(). If the user opens skill A and then skill B quickly, A’s slower response can still overwrite skillDetail while selectedDetailSkill points at B. handleDetailSave() then writes the current dialog content back under the wrong skill name.

🛠️ Suggested fix
+let detailRequestId = 0
+
 const openSkillDetail = async (skill: UnifiedSkillItem) => {
+  const requestId = ++detailRequestId
   try {
-    selectedDetailSkill.value = skill
+    const markdown = await skillClient.readSkillFile(skill.name)
+    if (requestId !== detailRequestId) return
+
+    selectedDetailSkill.value = skill
     skillDetail.value = {
       name: skill.name,
       description: skill.description,
       sourcePath: skill.path,
-      markdown: await skillClient.readSkillFile(skill.name),
+      markdown,
       mutable: skill.mutable
     }
     detailDialogOpen.value = true
🤖 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 `@src/renderer/settings/components/skills/SkillsSettings.vue` around lines 316
- 333, The `openSkillDetail` flow in `SkillsSettings.vue` can overwrite the
dialog with stale data when `readSkillFile()` resolves out of order after a
newer skill selection. Update `openSkillDetail` to guard against stale loads by
capturing the requested skill identity and verifying it is still the current
`selectedDetailSkill` before assigning `skillDetail` or opening the dialog. Keep
the dialog state consistent with the latest selection so `handleDetailSave()`
always saves against the correct skill.
src/renderer/settings/components/skills/SkillImportExportTab.vue-238-259 (1)

238-259: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle client-call failures instead of letting them escape.

loadConfig(), saveDirectory(), and the preview/execute flows all await IPC/filesystem calls without a catch. When one rejects, the tab only drops the spinner and Vue gets the error; the user never sees a recoverable failure state or toast.

Also applies to: 287-351, 366-371

🤖 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 `@src/renderer/settings/components/skills/SkillImportExportTab.vue` around
lines 238 - 259, Handle rejected IPC/filesystem calls in the
SkillImportExportTab flows instead of letting them bubble to Vue. Wrap
loadConfig(), saveDirectory(), and the preview/execute paths in try/catch around
the skillClient and deviceClient awaits, set a user-visible error/recoverable
state, and show an error toast on failure while still clearing the
loading/saving flags in finally. Use the existing loadConfig, browse, and
saveDirectory symbols as entry points, and apply the same pattern to the
preview/execute handlers in this component.
src/renderer/settings/components/skills/InstallSkillToAgentDialog.vue-190-199 (1)

190-199: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Deduplicate loadPreview() and ignore stale responses.

Line 197 already triggers the watcher on Lines 302-304, but Line 198 calls loadPreview() again. Because Line 218 assigns the result unconditionally, a slower response from an older agent selection can overwrite the preview for the current one and enable the wrong action.

Suggested fix
 const loadingAgents = ref(false)
 const loadingPreview = ref(false)
 const executing = ref(false)
 const error = ref<string | null>(null)
+let previewRequestId = 0
 
 const loadAgents = async () => {
   loadingAgents.value = true
   error.value = null
   try {
     agents.value = (await skillSyncClient.scanAgents()).filter(
       (agent) => agent.supportsLinkManagement && agent.status === 'ready'
     )
     selectedAgentId.value = agents.value[0]?.id ?? null
-    await loadPreview()
   } catch (cause) {
     error.value = cause instanceof Error ? cause.message : String(cause)
   } finally {
     loadingAgents.value = false
   }
 }
 
 const loadPreview = async () => {
   if (!props.skill || !selectedAgentId.value) {
     previewItem.value = null
     return
   }
+  const requestId = ++previewRequestId
+  const agentId = selectedAgentId.value
   loadingPreview.value = true
   error.value = null
   try {
     const preview = await skillSyncClient.previewLinkDeepChatSkills({
-      agentId: selectedAgentId.value,
+      agentId,
       skillNames: [props.skill.name]
     })
-    previewItem.value = preview.items[0] ?? null
+    if (requestId === previewRequestId && selectedAgentId.value === agentId) {
+      previewItem.value = preview.items[0] ?? null
+    }
   } catch (cause) {
-    error.value = cause instanceof Error ? cause.message : String(cause)
-    previewItem.value = null
+    if (requestId === previewRequestId) {
+      error.value = cause instanceof Error ? cause.message : String(cause)
+      previewItem.value = null
+    }
   } finally {
-    loadingPreview.value = false
+    if (requestId === previewRequestId) {
+      loadingPreview.value = false
+    }
   }
 }

Also applies to: 206-225, 292-304

🤖 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 `@src/renderer/settings/components/skills/InstallSkillToAgentDialog.vue` around
lines 190 - 199, The preview loading flow in loadAgents/loadPreview is
triggering twice and can apply stale results to the current selection. Remove
the redundant direct loadPreview() call from loadAgents so the selectedAgentId
watcher remains the single trigger, and update loadPreview() to ignore
out-of-order responses by checking the currently selected agent before assigning
preview state or enabling actions. Make the fix in the loadAgents, loadPreview,
and selectedAgentId watcher paths so older agent responses cannot overwrite
newer ones.
src/renderer/settings/components/skills/SkillImportExportTab.vue-250-255 (1)

250-255: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear preview state after changing the sync directory.

Lines 253-254 switch the configured directory, but the old exportPreview, importPreview, and selectedImportNames stay live. The action buttons can still run against the new directory while the UI is showing preview data from the previous one.

Suggested fix
 const saveDirectory = async () => {
   saving.value = true
   try {
     config.value = await skillClient.setSkillsSyncDirectory(directory.value)
     directory.value = config.value.skillsDirectory
+    exportPreview.value = null
+    importPreview.value = null
+    selectedImportNames.value = new Set()
     toast({ title: t('settings.skills.importExport.saved') })
   } finally {
     saving.value = false
   }
 }

Also applies to: 319-351

🤖 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 `@src/renderer/settings/components/skills/SkillImportExportTab.vue` around
lines 250 - 255, The sync-directory update in saveDirectory leaves stale preview
state behind, so clear exportPreview, importPreview, and selectedImportNames
whenever the configured directory changes. Update the directory-switch flow in
SkillImportExportTab.vue to reset these refs right after
skillClient.setSkillsSyncDirectory succeeds, and apply the same cleanup in the
related import/export action handlers that reuse preview state so the UI and
actions stay aligned with the new directory.
src/renderer/settings/components/skills/InstallFromGitDialog.vue-13-18 (1)

13-18: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Invalidate the scan state when repoUrl changes.

After Line 183 scans one repository, the user can edit the input and still click Install; Line 212 then sends the new repoUrl with skill names selected from the old scanResult. That mixes two different repositories and can install the wrong set or fail unexpectedly.

Suggested fix
 const repoUrl = ref('https://github.com/op7418/guizang-ppt-skill')
 const scanResult = ref<GitSkillRepoScanResult | null>(null)
 const selectedNames = ref<Set<string>>(new Set())
+const scannedRepoUrl = ref<string | null>(null)
 const strategy = ref<SkillInstallConflictStrategy>('rename')
 const scanning = ref(false)
 const installing = ref(false)
 const error = ref<string | null>(null)
 
 const canInstall = computed(
   () =>
     Boolean(scanResult.value) &&
+    scannedRepoUrl.value === repoUrl.value &&
     selectedNames.value.size > 0 &&
     !scanning.value &&
     !installing.value
 )
 
 const scan = async () => {
   error.value = null
   scanning.value = true
   try {
     scanResult.value = await skillClient.scanGitSkillRepo(repoUrl.value)
+    scannedRepoUrl.value = repoUrl.value
     selectedNames.value = new Set(
       scanResult.value.skills.filter((skill) => skill.valid).map((skill) => skill.name)
     )
   } catch (cause) {
     error.value = cause instanceof Error ? cause.message : String(cause)
     scanResult.value = null
     selectedNames.value = new Set()
+    scannedRepoUrl.value = null
   } finally {
     scanning.value = false
   }
 }
+
+watch(repoUrl, (next, prev) => {
+  if (next !== prev) {
+    scanResult.value = null
+    selectedNames.value = new Set()
+    scannedRepoUrl.value = null
+  }
+})

Also applies to: 171-177, 179-215

🤖 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 `@src/renderer/settings/components/skills/InstallFromGitDialog.vue` around
lines 13 - 18, The InstallFromGitDialog component is allowing a stale scanResult
to be reused after repoUrl changes, which can mix repositories between scan and
install. Update the dialog logic so changing repoUrl invalidates or clears the
existing scan state (including selected skill names) and disables Install until
scan is run again. Make sure the scan and install flows in InstallFromGitDialog
stay tied to the same repository by resetting any cached scanResult when repoUrl
updates.
src/renderer/settings/components/skills/SkillAgentsTab.vue-173-208 (1)

173-208: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Ignore stale getAgentDetail() responses.

Rapid agent switches start overlapping detail requests, and Line 197 stores whichever one resolves last. That can render agent A's table while agent B is selected, so follow-up actions operate against mismatched UI state.

Suggested fix
 const selectedAgentId = ref<string | null>(null)
 const selectedAgentDetail = ref<InstalledSkillAgentDetail | null>(null)
+let detailRequestId = 0
 
 const loadAgentDetail = async (agentId: string) => {
+  const requestId = ++detailRequestId
   detailLoading.value = true
   error.value = null
   try {
-    selectedAgentDetail.value = await skillSyncClient.getAgentDetail(agentId)
+    const detail = await skillSyncClient.getAgentDetail(agentId)
+    if (requestId === detailRequestId && selectedAgentId.value === agentId) {
+      selectedAgentDetail.value = detail
+    }
   } catch (cause) {
-    error.value = cause instanceof Error ? cause.message : String(cause)
+    if (requestId === detailRequestId) {
+      error.value = cause instanceof Error ? cause.message : String(cause)
+    }
   } finally {
-    detailLoading.value = false
+    if (requestId === detailRequestId) {
+      detailLoading.value = false
+    }
   }
 }
 
 const selectAgent = async (agentId: string) => {
   selectedAgentId.value = agentId
+  selectedAgentDetail.value = null
   await loadAgentDetail(agentId)
 }
🤖 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 `@src/renderer/settings/components/skills/SkillAgentsTab.vue` around lines 173
- 208, The detail-loading flow in SkillAgentsTab.vue can apply an out-of-date
response from loadAgentDetail after the user switches agents quickly, causing
selectedAgentDetail to mismatch selectedAgentId. Update loadAgentDetail and
selectAgent to ignore stale getAgentDetail results by tracking the active
request/agent id and only assigning selectedAgentDetail when the response still
matches the currently selected agent. Also ensure loadAgents does not let an
older in-flight detail request overwrite the newer selection state.
src/shared/contracts/routes/skills.routes.ts-99-119 (1)

99-119: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject option-like Git repo inputs at the contract boundary. These routes accept any non-empty repoUrl, but the presenter passes it straight to git clone. A value beginning with - can be parsed as a Git option instead of a repository, so tighten the schema here and pass -- to git clone as defense in depth.

🤖 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 `@src/shared/contracts/routes/skills.routes.ts` around lines 99 - 119, Tighten
the Git repo contract validation for skills.scanGitRepo and
skills.installFromGit so repoUrl cannot start with a dash, since these values
are passed through to git clone. Update the zod schema in skills.routes.ts to
reject option-like inputs at the boundary, and in the presenter/service that
invokes git clone for these routes, pass -- before the repo URL as a
defense-in-depth safeguard.
🧹 Nitpick comments (9)
docs/features/deepchat-skills-management/tasks.md (1)

3-9: 📐 Maintainability & Code Quality | 🔵 Trivial

Historical context note: "Install tab" references describe pre-Phase 8 state.

Lines 97 and 129 reference an "Install tab" that was later removed in Phase 8. Consider adding a historical note or updating to "Add Skill menu" for future readers, though this is minor since the document tracks implementation history.

🤖 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 `@docs/features/deepchat-skills-management/tasks.md` around lines 3 - 9, The
task notes still refer to the removed “Install tab,” which can confuse future
readers. Update the wording in the relevant task descriptions to use the current
“Add Skill menu” terminology, or add a short historical note where “Install tab”
is mentioned in docs/features/deepchat-skills-management/tasks.md so the Phase 8
rename is clear. Keep the references consistent with the existing DeepChat
skills management terms used in this document.
docs/features/deepchat-skills-management/plan.md (1)

634-641: 📐 Maintainability & Code Quality | 🔵 Trivial

Phase numbering inconsistency with tasks.md.

This section lists 6 delivery steps, but tasks.md uses Phases 0-8 with UX consolidation as Phase 8. Align the phase numbers or add a mapping note to prevent confusion during implementation tracking.

🤖 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 `@docs/features/deepchat-skills-management/plan.md` around lines 634 - 641, The
phase list in the plan is inconsistent with tasks.md, which can confuse
implementation tracking. Update the numbering in this section to match the Phase
0-8 structure used elsewhere, or add a clear mapping note that links each listed
delivery step to its corresponding phase; use the surrounding plan section and
the UX consolidation item as the reference points when renumbering or
annotating.
docs/features/deepchat-skills-management/spec.md (1)

514-515: 📐 Maintainability & Code Quality | 🔵 Trivial

Clarify how SkillDetailDialog distinguishes Library vs. Agent context.

The spec states the same dialog is used from both Library and Agents rows. Consider adding a note about whether the dialog receives a context prop (source: 'library' | 'agent') or infers available actions from the source descriptor shape. This prevents UI bugs where agent-owned skills show "Delete" or Library skills show "Adopt."

🤖 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 `@docs/features/deepchat-skills-management/spec.md` around lines 514 - 515,
Clarify how SkillDetailDialog determines whether it is being opened from a
Library row or an Agents row by explicitly documenting the source signal it
uses. Update the spec section around the dialog behavior to state whether the
dialog accepts a context prop like source: 'library' | 'agent' or derives the
context from the source descriptor shape, and then tie action availability (for
example Delete vs Adopt) to that identified symbol so the UI cannot show the
wrong actions.
src/main/routes/index.ts (1)

3008-3013: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Track skillsListCatalogRoute in the startup scheduler.

This route is wrapped in runTrackedRouteTask, but resolveTrackedRouteTask only matches skillsListMetadataRoute, so the new catalog load bypasses the intended deferred settings workload path.

⚙️ Proposed fix
-  if (routeName === skillsListMetadataRoute.name && isSettings) {
+  if (
+    (routeName === skillsListMetadataRoute.name || routeName === skillsListCatalogRoute.name) &&
+    isSettings
+  ) {
🤖 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 `@src/main/routes/index.ts` around lines 3008 - 3013, The
`skillsListCatalogRoute` handler is currently wrapped in `runTrackedRouteTask`
but `resolveTrackedRouteTask` only recognizes `skillsListMetadataRoute`, so the
catalog load is not being scheduled through the deferred settings workload path.
Update the route tracking logic in `resolveTrackedRouteTask` and the related
`runTrackedRouteTask` flow so `skillsListCatalogRoute` is also classified and
scheduled consistently with the existing metadata route.
test/main/presenter/remoteControlPresenter/remoteBindingStore.test.ts (1)

267-274: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the legacy Telegram migration is persisted.

This only checks the derived enabled flag. If RemoteBindingStore stops writing the migrated nested telegram config back to remoteControl, the test still passes and the migration path stays unguarded. Please assert the saved remoteControl shape or the setSetting call too.

🤖 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 `@test/main/presenter/remoteControlPresenter/remoteBindingStore.test.ts` around
lines 267 - 274, The legacy Telegram migration test only verifies the derived
enabled flag, so it can miss regressions where RemoteBindingStore does not
persist the migrated telegram config back into remoteControl. Update the test
around RemoteBindingStore and getTelegramConfig to also assert the saved
remoteControl shape or verify the setSetting call includes the nested telegram
payload after migration, so the persistence path is covered as well as the
derived value.
test/renderer/components/PluginsCatalogPage.test.ts (1)

81-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Localize the mocked catalog strings you assert against.

Because this t() stub returns the key for titles, headings, and action labels, the test still passes if the catalog renders raw i18n keys. Please return distinct translated values for the asserted keys and check those values instead. As per coding guidelines, src/renderer/src/**/*.{vue,ts,tsx}: All user-facing strings must use vue-i18n keys in src/renderer/src/i18n.

🤖 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 `@test/renderer/components/PluginsCatalogPage.test.ts` around lines 81 - 89,
The test mock for vue-i18n in PluginsCatalogPage.test.ts is still returning raw
keys for several asserted catalog strings, so it won’t catch untranslated UI.
Update the useI18n().t stub to return distinct localized values for every title,
heading, and action label the test checks, and then assert against those
translated values in the PluginsCatalogPage assertions.

Source: Coding guidelines

test/renderer/components/OfficialPluginDetailPage.test.ts (1)

116-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Return concrete translations for the asserted title/action keys.

Right now t() falls back to the raw key for most strings, so these assertions still pass if the page renders i18n keys verbatim instead of localized text. Stub distinct values for the checked title/button keys and assert those instead. As per coding guidelines, src/renderer/src/**/*.{vue,ts,tsx}: All user-facing strings must use vue-i18n keys in src/renderer/src/i18n.

🤖 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 `@test/renderer/components/OfficialPluginDetailPage.test.ts` around lines 116 -
124, The vue-i18n mock in OfficialPluginDetailPage.test.ts only returns
localized text for a couple of descriptions and falls back to raw keys for the
asserted title/action strings, so the test can still pass when the UI renders
untranslated keys. Update the mocked useI18n().t in the test to return concrete
distinct translations for the specific title/button keys being asserted, and
change the expectations to match those concrete values. Use the checked key
names in this test to locate the assertions and ensure the mock covers them
explicitly.

Source: Coding guidelines

test/renderer/api/clients.test.ts (1)

2022-2100: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the returned values from the new client methods.

These additions mostly verify route names and payloads. For these thin bridge clients, the bug-prone part is response unwrapping; the suite still passes if a method starts returning the raw { result } / { preview } / { config } envelope instead of the inner value. Please capture and assert representative return payloads for the new skill and agent-link methods as well.

Also applies to: 2197-2254

🤖 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 `@test/renderer/api/clients.test.ts` around lines 2022 - 2100, The new
skillSync client tests only verify invoke payloads, so they miss regressions
where methods return the raw response envelope instead of the unwrapped value.
Update the assertions in clients.test.ts around skillSyncClient methods like
getAgentDetail, getAgentSkillDetail, previewAdoptAgentSkill,
executeAdoptAgentSkill, previewLinkDeepChatSkills, executeLinkDeepChatSkills,
repairAgentSkillLink, removeAgentSkillLink, previewImport, executeImport,
previewExport, and executeExport to capture and assert representative returned
values from each call. Make sure the checks validate the inner
result/preview/config values, not just the bridge.invoke arguments.
src/renderer/settings/components/skills/SkillsSettings.vue (1)

293-314: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Remove the extra catalog subscription.

useSkillsStore() already subscribes to skillClient.onCatalogChanged() in src/renderer/src/stores/skillsStore.ts, so Lines 308-314 add a second identical listener. While this page is mounted, every catalog event reloads the store twice.

♻️ Suggested cleanup
-import { ref, computed, onMounted, onUnmounted } from 'vue'
+import { ref, computed, onMounted } from 'vue'
@@
-const eventCleanup = ref<(() => void) | null>(null)
-
 onMounted(async () => {
   const enabled = await configClient.getSkillDraftSuggestionsEnabled()
   draftSuggestionsEnabled.value = enabled ?? false
   await skillsStore.loadSkills()
-  setupEventListeners()
-})
-
-onUnmounted(() => {
-  if (eventCleanup.value) {
-    eventCleanup.value()
-  }
 })
-
-const setupEventListeners = () => {
-  const handleSkillEvent = () => {
-    skillsStore.loadSkills()
-  }
-
-  eventCleanup.value = skillClient.onCatalogChanged(handleSkillEvent)
-}
🤖 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 `@src/renderer/settings/components/skills/SkillsSettings.vue` around lines 293
- 314, The SkillsSettings.vue component is registering a duplicate catalog
change listener on top of the one already set up by useSkillsStore() in
skillsStore.ts. Remove the extra setupEventListeners/onUnmounted subscription
logic from SkillsSettings and keep only the initial load in onMounted so catalog
updates are handled once by the store’s existing skillClient.onCatalogChanged()
subscription.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@zerob13
zerob13 merged commit eb81a61 into dev Jun 29, 2026
3 checks passed
@zhangmo8
zhangmo8 deleted the codex/plugins-hub-plan branch June 30, 2026 01:44
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.

1 participant