From 9e0d09456bf9a23af1e5de5d3e7decd8a79731f8 Mon Sep 17 00:00:00 2001
From: zerob13
Date: Thu, 25 Jun 2026 14:38:26 +0800
Subject: [PATCH 01/14] docs(plugins): plan main route hub
---
docs/features/plugins-hub/plan.md | 435 +++++++++++++++++++++++++++++
docs/features/plugins-hub/spec.md | 358 ++++++++++++++++++++++++
docs/features/plugins-hub/tasks.md | 119 ++++++++
3 files changed, 912 insertions(+)
create mode 100644 docs/features/plugins-hub/plan.md
create mode 100644 docs/features/plugins-hub/spec.md
create mode 100644 docs/features/plugins-hub/tasks.md
diff --git a/docs/features/plugins-hub/plan.md b/docs/features/plugins-hub/plan.md
new file mode 100644
index 0000000000..b6a59380ca
--- /dev/null
+++ b/docs/features/plugins-hub/plan.md
@@ -0,0 +1,435 @@
+# Plugins Hub Implementation Plan
+
+## Strategy
+
+Do not build a new plugin platform and do not add a new window. Move the UI ownership boundary into
+the existing main window.
+
+The smallest reliable implementation is:
+
+- Add `/plugins` routes to the existing main renderer router.
+- Keep `WindowSideBar` and `AppBar` as the app shell around the Plugins page.
+- Reuse current clients/stores/components where they already own behavior.
+- Treat Remote channels as renderer-only virtual plugin cards backed by existing `remoteControl.*` routes.
+- Hide Settings navigation entries for Plugins-owned areas, while keeping compatibility redirects.
+- Update the main sidebar expanded layout only; keep collapsed behavior unchanged.
+
+No data migration is required.
+
+## Main Route Architecture
+
+```text
+Main window
+ App.vue
+ AppBar
+ WindowSideBar
+ RouterView
+ /chat -> ChatTabView
+ /welcome -> WelcomePage
+ /plugins -> PluginsHubPage
+ /plugins
+ /plugins/skills
+ /plugins/mcp
+ /plugins/remote
+ /plugins/official/:pluginId
+ /plugins/remote/:channel
+```
+
+Use the existing `src/renderer/src/router/index.ts`. Do not add `src/renderer/plugins`, a new Vite
+entry, or a new BrowserWindow.
+
+Route names can be:
+
+```text
+plugins
+plugins-skills
+plugins-mcp
+plugins-remote
+plugins-official-detail
+plugins-remote-detail
+```
+
+External/main-process callers should not know UI component internals. Add a narrow route or event only where the main process needs to focus the main window and navigate:
+
+```text
+system.openMainRoute({ routeName: 'plugins-mcp', params? }) -> { focused: boolean }
+```
+
+If an existing main-window navigation route already exists during implementation, reuse it. Do not add a Plugins-specific window route.
+
+## Affected Boundaries
+
+| Boundary | Required change |
+| --- | --- |
+| `src/renderer/src/router/index.ts` | Add `/plugins` route family |
+| `src/renderer/src/App.vue` | Keep existing shell; ensure `/plugins` receives same global overlays/theme/i18n |
+| `WindowSideBar.vue` | Add expanded command list and route Plugins row to `/plugins` |
+| `renderer/api` | Add or reuse a small main-window navigation client only for main-process initiated navigation |
+| `shared/contracts/routes` | Add narrow focus/navigate route only if deeplink/main process cannot use existing event path |
+| Settings renderer | Remove/hide Plugins-owned nav entries and overview links |
+| Deeplink presenter | Route MCP install deeplink to main `/plugins/mcp` page |
+| Plugin presenter | Stop using per-plugin BrowserWindow as primary UI path |
+
+## Data Ownership
+
+Do not create a persisted unified plugin table.
+
+Use a renderer-only union for cards:
+
+```text
+CatalogItem =
+ official plugin item from plugins.list
+ MCP server summary from mcp store
+ Skill metadata from skills store
+ Remote virtual item from remoteControl.listChannels + status
+```
+
+This union only drives list rendering and search filtering. Writes go back to the current owner:
+
+| User action | Owner route/client |
+| --- | --- |
+| Enable official plugin | `PluginClient.enablePlugin` |
+| Disable official plugin | `PluginClient.disablePlugin` |
+| CUA runtime/permission action | `PluginClient.invokeAction` existing runtime actions |
+| MCP add/edit/toggle | `McpClient` / `useMcpStore` existing paths |
+| Skill install/edit/delete/sync | `SkillClient` / `SkillSyncClient` / `useSkillsStore` |
+| Remote enable/save/pair/remove binding | `RemoteControlClient` |
+
+## Page Shell
+
+Create the Plugins UI under the existing renderer:
+
+```text
+src/renderer/src/pages/plugins/
+├── PluginsHubPage.vue
+├── PluginsCatalogPage.vue
+├── OfficialPluginDetailPage.vue
+├── McpPluginsPage.vue
+├── SkillsPluginsPage.vue
+├── RemotePluginsPage.vue
+├── RemotePluginDetailPage.vue
+├── components/
+│ ├── PluginsTopTabs.vue
+│ ├── PluginCatalogGrid.vue
+│ ├── PluginCatalogCard.vue
+│ ├── AddedPluginsStrip.vue
+│ └── PluginSearchBar.vue
+└── composables/
+ ├── usePluginCatalog.ts
+ └── useRemotePluginItems.ts
+```
+
+Keep this list flexible during implementation; do not split files unless the component becomes hard to read.
+
+Visual baseline:
+
+- Main content starts with top tabs (`Plugins`, `Skills`, optionally `MCP`, `Remote`).
+- Catalog page uses the Codex-like layout: title, subtitle, search, added strip, segmented filters, sectioned list.
+- Avoid settings-style full-width form pages for the catalog. Detail routes may use denser settings sections.
+- Cards are individual repeated items only. Do not put page sections inside floating cards.
+
+## Route and Navigation Behavior
+
+Renderer-side navigation:
+
+| Trigger | Behavior |
+| --- | --- |
+| Sidebar `Plugins` row | `router.push({ name: 'plugins' })` |
+| Top tab `Skills` | `router.push({ name: 'plugins-skills' })` |
+| Top tab `MCP` | `router.push({ name: 'plugins-mcp' })` |
+| Top tab `Remote` | `router.push({ name: 'plugins-remote' })` |
+| Official plugin card/detail | `router.push({ name: 'plugins-official-detail', params: { pluginId } })` |
+| Remote channel card/detail | `router.push({ name: 'plugins-remote-detail', params: { channel } })` |
+| `New Chat` row while on `/plugins` | `router.push({ name: 'chat' })`, then start new conversation |
+
+Main-process initiated navigation:
+
+- MCP install deeplink must focus the main window and route to `/plugins/mcp`.
+- Historical Settings route redirects can focus main and route to the matching `/plugins...` route.
+- If the main window does not exist, create/focus the normal app window, not a Plugins window.
+
+## Official Plugin Detail
+
+Current behavior opens `PluginPresenter.openPluginSettingsWindow(pluginId)`.
+
+Target behavior:
+
+- List page opens `/plugins/official/:pluginId`.
+- Detail page loads `plugins.get(pluginId)`.
+- Enable/disable remains in detail and list.
+- Runtime status and MCP status remain visible.
+- Known first-party plugin actions are exposed as native detail sections:
+ - `runtime.getStatus`
+ - `runtime.checkPermissions`
+ - `runtime.openPermissionGuide`
+
+Do not add a generic embedded HTML settings host in the first increment. Current shipped plugins are first-party (`cua`, `feishu`), so native Vue detail pages are enough and safer than enabling arbitrary webview/iframe behavior.
+
+Legacy fallback:
+
+- Keep `settingsContributions` in manifests during migration.
+- Keep `settings.open` action available only as a temporary compatibility path if some old package still calls it.
+- The first-party UI must not call `settings.open`.
+
+When to add a generic plugin settings host:
+
+- Only when third-party plugin settings contributions are a supported product requirement.
+- Use an isolated child WebContents/WebContentsView with the plugin-specific preload, not an iframe without preload.
+- Keep external navigation denied.
+
+## MCP Migration
+
+`McpSettings.vue` already owns most behavior. Move by reuse, not rewrite.
+
+Recommended first pass:
+
+- Create `McpPluginsPage.vue`.
+- Import/reuse `McpServers`, `McpBuiltinMarket`, NPM registry controls, guide overlay only if still needed.
+- Preserve current route query shape for market view inside Plugins (`/plugins/mcp?view=market`).
+- Move deeplink handler from Settings bootstrap to main app or Plugins page bootstrap for MCP install.
+
+Compatibility:
+
+- `deepchat://mcp/install` focuses main window and routes to `/plugins/mcp`.
+- Hidden `settings-mcp` route can redirect/open main `/plugins/mcp` during transition.
+
+Settings cleanup:
+
+- Remove visible `settings-mcp` navigation item.
+- Remove MCP Overview metric.
+- Remove `start-mcp` quick task or replace it with a non-Plugins Settings task.
+
+## Skills Migration
+
+`SkillsSettings.vue` can become a Plugins page with minimal changes:
+
+- Rename/wrap visually as `SkillsPluginsPage`.
+- Keep `SkillCard`, `SkillInstallDialog`, `SkillEditorSheet`, `SkillSyncDialog`, `SyncStatusSection`.
+- Keep draft suggestion toggle.
+- Keep first-launch sync prompt if product still relies on it.
+
+Avoid duplicating the skills store or install logic.
+
+Compatibility:
+
+- Hidden `settings-skills` route should route the main window to `/plugins/skills`.
+- Settings Overview search should not list Skills.
+
+## Remote Migration
+
+Do not keep `RemoteSettings.vue` as one giant page inside Plugins. It is already too large.
+
+Refactor only around real channel boundaries:
+
+```text
+RemotePluginsPage
+ -> virtual cards from listRemoteChannels()
+RemotePluginDetailPage(channel)
+ -> channel header/status/toggle
+ -> credentials section
+ -> default agent/workdir section
+ -> pairing section when supportsPairing
+ -> bindings section
+ -> channel-specific section
+```
+
+Suggested extracted components:
+
+| Component | Scope |
+| --- | --- |
+| `RemotePluginCard` | card summary for one channel |
+| `RemotePluginDetailPage` | detail shell and save status |
+| `RemoteCredentialsSection` | token/app secret fields; channel-specific props |
+| `RemoteDefaultsSection` | default agent and default workdir |
+| `RemotePairingSection` | pair code and principals for pairable channels |
+| `RemoteBindingsSection` | bound chats/groups/topics |
+| `WeixinIlinkAccountsSection` | WeChat iLink login/account controls |
+
+Keep shared logic tiny:
+
+- load channel settings
+- save channel settings
+- load channel status
+- load bindings/pairing
+
+Do not invent a generic form schema for all channels.
+
+Virtual item mapping:
+
+```text
+remote:
+ kind: remote
+ title: channel title + ' Remote' when needed
+ description: descriptor.descriptionKey
+ enabled: status.enabled
+ state: status.state
+ detailRoute: /plugins/remote/:channel
+```
+
+## Settings Removal and Redirects
+
+Change visible navigation source:
+
+- Remove or mark hidden:
+ - `settings-mcp`
+ - `settings-remote`
+ - `settings-plugins`
+ - `settings-skills`
+
+Because `settingsNavigation.ts` is the single source for Settings sidebar and Overview search, this should remove most visible Settings entries without scattered conditions.
+
+Route compatibility options:
+
+1. Keep hidden route items so old route names still exist.
+2. When entered, focus main window and navigate to the mapped `/plugins...` route.
+3. Do not show these items in Settings sidebar/search.
+
+Mapping:
+
+| Old Settings route | Main window target |
+| --- | --- |
+| `settings-mcp` | `/plugins/mcp` |
+| `settings-remote` | `/plugins/remote` |
+| `settings-plugins` | `/plugins` |
+| `settings-skills` | `/plugins/skills` |
+
+`settings-acp` stays in Settings for this feature. ACP is an agent/provider configuration surface, not part of the four requested Plugins-owned areas.
+
+## Sidebar Implementation Plan
+
+Current `WindowSideBar.vue` should not be rewritten. Modify the expanded right column header area.
+
+Before:
+
+```text
+right column
+├── header row: selectedAgentName + group toggle + plus
+├── search input
+├── pinned section
+└── session groups
+```
+
+After:
+
+```text
+right column
+├── title row: selectedAgentName
+├── command list
+│ ├── New Chat
+│ ├── Search
+│ └── Plugins
+├── compact session controls when needed
+├── pinned section
+└── session groups
+```
+
+Command behavior:
+
+| Row | Existing behavior to call |
+| --- | --- |
+| New Chat | `router.push({ name: 'chat' })` then `sessionStore.startNewConversation({ refresh: true })` |
+| Search | `spotlightStore.toggleSpotlight()` |
+| Plugins | `router.push({ name: 'plugins' })` |
+
+Keep:
+
+- Agent icon rail.
+- collapsed width and transitions.
+- session pagination and fill checks.
+- pinned collapse behavior.
+- project grouping/reorder behavior.
+- shortcut badge logic for sessions.
+
+Question the old inline session search:
+
+- First increment should remove the inline search input from expanded sidebar to match the requested command-list shape.
+- Search row opens Spotlight, which already searches sessions/messages/settings/actions.
+- If users later need local-only filtering, add it inside Spotlight or as a session-list filter command, not as a second persistent input.
+
+## Deeplinks and External Entry Points
+
+Update callers:
+
+| Current caller | New behavior |
+| --- | --- |
+| MCP install deeplink | focus main window, route to `/plugins/mcp`, dispatch MCP install event there |
+| Settings sidebar old MCP/Skills/Plugins/Remote | no visible entry |
+| Settings activity old route | focus main window and route to matching `/plugins...` page |
+| Sidebar remote status button | route to `/plugins/remote` or a selected channel detail |
+| Chat input MCP indicator `openSettings` text | route to `/plugins/mcp` |
+
+Provider install deeplink stays in Settings Provider. Do not route provider/model setup to Plugins.
+
+## i18n
+
+Add route/page labels:
+
+- `routes.plugins`
+- `pluginsHub.title`
+- `pluginsHub.subtitle`
+- `pluginsHub.searchPlaceholder`
+- `pluginsHub.tabs.plugins`
+- `pluginsHub.tabs.skills`
+- `pluginsHub.tabs.mcp`
+- `pluginsHub.tabs.remote`
+- sidebar command labels if existing `common.newChat` and spotlight labels are not enough.
+
+Avoid moving existing `settings.mcp`, `settings.skills`, `settings.remote` keys in the first increment. Reuse them from Plugins pages to keep the diff smaller. Later cleanup can rename namespaces if the old naming becomes misleading.
+
+## Testing Strategy
+
+Small checks with high signal:
+
+| Area | Tests |
+| --- | --- |
+| Main router | `/plugins` and child routes render inside app shell |
+| Settings navigation | removed entries do not appear in `getSettingsNavigationGroups`; hidden redirects still resolve |
+| Sidebar | expanded command rows render; collapsed state unchanged; Plugins row routes to `/plugins` |
+| Remote virtual items | descriptors + status produce cards; detail saves via `remoteControl.saveChannelSettings` |
+| Official plugin detail | list/detail enable-disable; settings button no longer calls `settings.open` |
+| Deeplink | MCP install focuses main and routes to `/plugins/mcp` |
+
+Manual visual QA:
+
+- macOS light/dark with app sidebar and main content.
+- Windows light/dark with main app shell.
+- Linux opaque backgrounds.
+- Narrow width with collapsed and expanded sidebar.
+- Long remote token/error/path strings.
+- Chinese and English labels.
+
+Final implementation gates:
+
+```bash
+pnpm run format
+pnpm run i18n
+pnpm run lint
+pnpm run typecheck
+```
+
+Renderer tests should be run for touched components. Full app smoke test should open Chat, Plugins and Settings separately.
+
+## Risks and Mitigations
+
+| Risk | Mitigation |
+| --- | --- |
+| Settings routes are used by deeplinks/onboarding | Keep hidden compatibility routes and redirect to main `/plugins...` |
+| RemoteSettings monolith makes migration risky | Extract per-channel detail only; reuse current route/client behavior |
+| Plugin settings HTML depends on plugin preload | Do not embed arbitrary HTML in first increment; build first-party native details |
+| Feishu official plugin vs Feishu Remote naming collision | Use explicit `Integration` vs `Remote` labels and category badges |
+| Plugins page becomes another Settings | Catalog page stays Codex-like; detail pages are dense only where settings are unavoidable |
+| Main route conflicts with chat internal `pageRouter` | Use Vue router for `/plugins`; keep `pageRouter` scoped to ChatTabView |
+| Search behavior confusion | Sidebar Search row opens existing Spotlight; do not add a new search engine |
+
+## Rollout Plan
+
+1. Land `/plugins` main route skeleton with top tabs and catalog placeholder.
+2. Move visible Settings entries out, with redirects to main Plugins routes.
+3. Move MCP page and deeplink.
+4. Move Skills page.
+5. Add official plugin native list/detail and stop first-party UI from opening plugin settings windows.
+6. Add Remote virtual plugin list/detail.
+7. Update main sidebar expanded command list.
+8. Run visual QA and clean up i18n/tests.
+
+This order keeps each PR reviewable and avoids breaking every surface at once.
diff --git a/docs/features/plugins-hub/spec.md b/docs/features/plugins-hub/spec.md
new file mode 100644
index 0000000000..1ffa1d2e39
--- /dev/null
+++ b/docs/features/plugins-hub/spec.md
@@ -0,0 +1,358 @@
+# Plugins Hub Specification
+
+## User Need
+
+DeepChat 的 Settings 里混入了高频工具能力、扩展能力和系统偏好。用户想管理 Skills、MCP
+servers、official Plugins 和 Remote control channels 时,不应该打开 Settings,也不应该弹一个新的
+插件设置窗口。
+
+本目标是把插件型能力移动到主窗口的一级页面和子路由中,形态参考 Codex 的主窗口 Plugins 页面:
+左侧仍是主窗口 sidebar,右侧主内容区显示 `Plugins` 页面、顶部 tabs、搜索、已添加项和推荐项。
+
+## Product Position
+
+`Plugins` 是主窗口里的扩展能力页面,不是独立 BrowserWindow,也不是 Settings 的子页面。
+
+| Capability | 在主窗口 Plugins 页面中的定位 | 数据事实源 |
+| --- | --- | --- |
+| Official Plugins | 可启停的 DeepChat first-party plugin package,例如 CUA、Feishu/Lark Integration | `PluginPresenter` + `plugins.*` routes |
+| MCP | 工具 server 管理、market、global MCP enablement | `McpPresenter` / `useMcpStore` |
+| Skills | agent skill 管理、导入导出、sync、draft suggestion | `SkillPresenter` / `SkillSyncPresenter` / `useSkillsStore` |
+| Remote | Telegram、Feishu/Lark、QQBot、Discord、WeChat iLink 作为 virtual plugin card | `RemoteControlPresenter` + `remoteControl.*` routes |
+
+Remote channel 是 Plugins UI 里的 virtual plugin,不是 `.dcplugin` 安装包。这个建模只改变用户入口和
+展示方式,不改变 remote control 的配置存储、runtime 生命周期或消息协议。
+
+## Goals
+
+- 新增主窗口 route:`/plugins`。
+- 在主窗口主内容区集中管理 Official Plugins、MCP、Skills、Remote。
+- 主窗口左侧 sidebar 展开态显示 `New Chat`、`Search`、`Plugins` command list,点击 `Plugins` 进入 `/plugins`。
+- `Plugins` 页面保留左侧 sidebar,右侧内容区切换,不创建新窗口。
+- Remote 每个 implemented channel 都作为 plugin-like card 出现,并能进入该 channel 的详情子路由。
+- Remote 设置页不再在 Settings 中展示;从列表进入详情时使用主窗口 Plugins 子路由。
+- Official plugin 的详情和设置入口不再弹出 per-plugin BrowserWindow;从列表进入详情时使用主窗口 Plugins 子路由。
+- Settings 侧边栏、Settings Overview 搜索和 quick entry 不再展示 Skills、MCP、Plugins、Remote。
+- 保留 Settings 内部旧 route 的兼容能力,避免 deeplink、onboarding 或历史入口直接 404。
+- `所有 Agents` 标题保留。
+- UI 需要在 macOS、Windows、Linux 以及窄窗口下保持可用和美观。
+
+## Non-Goals
+
+- 不做第三方 plugin marketplace。
+- 不把 Remote channel 改造成真实 `.dcplugin` 包。
+- 不迁移 provider、model、DeepChat Agents、ACP Agents、prompt、memory、knowledge、data、shortcut、about 等系统设置。
+- 不新增 Automations 入口;参考截图中有 Automations,但本目标只做 `New Chat`、`Search`、`Plugins`。
+- 不新增独立 Plugins BrowserWindow。
+- 不新增 `src/renderer/plugins` 独立 renderer entry。
+- 不重写 MCP、Skill、Remote、Plugin presenter。
+- 不新增统一持久化表来存一个“大插件模型”。
+- 不改变 existing Remote commands、pairing protocol、channel binding behavior。
+- 不改变 existing MCP server config schema、Skill sidecar schema 或 plugin manifest schema,除非内嵌设置页确实需要最小 route 补充。
+
+## Current State
+
+Relevant current files:
+
+| Area | Current files |
+| --- | --- |
+| Main app shell | `src/renderer/src/App.vue`, `src/renderer/src/router/index.ts` |
+| Main sidebar | `src/renderer/src/components/WindowSideBar.vue`, `src/renderer/src/stores/ui/sidebar.ts` |
+| Chat page internal route state | `src/renderer/src/stores/ui/pageRouter.ts`, `src/renderer/src/views/ChatTabView.vue` |
+| Settings shell and navigation | `src/renderer/settings/App.vue`, `src/renderer/settings/main.ts`, `src/shared/settingsNavigation.ts` |
+| Settings window lifecycle | `src/main/presenter/windowPresenter/index.ts`, `src/shared/contracts/routes/system.routes.ts` |
+| Plugins settings page | `src/renderer/settings/components/PluginsSettings.vue`, `src/renderer/api/PluginClient.ts`, `src/shared/contracts/routes/plugins.routes.ts` |
+| MCP settings page | `src/renderer/settings/components/McpSettings.vue`, `src/renderer/src/components/mcp-config/**`, `src/renderer/src/stores/mcp.ts` |
+| Skills settings page | `src/renderer/settings/components/skills/SkillsSettings.vue`, `src/renderer/src/stores/skillsStore.ts` |
+| Remote settings page | `src/renderer/settings/components/RemoteSettings.vue`, `src/renderer/api/RemoteControlClient.ts` |
+
+Important current constraints:
+
+- Main window Vue router currently exposes `/chat` and `/welcome`.
+- Main shell already keeps `WindowSideBar` outside `RouterView`, so adding `/plugins` naturally preserves the sidebar.
+- Settings navigation is centralized in `src/shared/settingsNavigation.ts`.
+- Settings routes are generated from navigation items in `src/renderer/settings/main.ts`.
+- `system.openSettings` only accepts `SettingsRouteNameSchema`.
+- MCP install deeplinks currently open Settings and send `DEEPLINK_EVENTS.MCP_INSTALL`.
+- Plugin settings currently call `plugins.invokeAction({ actionId: 'settings.open' })`, which opens a per-plugin BrowserWindow.
+- Remote channels already expose `RemoteChannelDescriptor`, status, settings, bindings and pairing through typed routes.
+
+## Proposed Main Route Structure
+
+```text
+src/renderer/src/router/index.ts
+├── /chat
+├── /welcome
+└── /plugins
+ ├── tab=plugins or child /plugins
+ ├── /plugins/skills
+ ├── /plugins/mcp
+ ├── /plugins/remote
+ ├── /plugins/official/:pluginId
+ └── /plugins/remote/:channel
+```
+
+Implementation can use nested Vue routes or one `/plugins` route with internal tab state. The URL must be shareable enough for internal navigation and redirects:
+
+| Target | Required addressable route |
+| --- | --- |
+| Plugins catalog | `/plugins` |
+| Skills | `/plugins/skills` |
+| MCP | `/plugins/mcp` |
+| Remote list | `/plugins/remote` |
+| Official plugin detail | `/plugins/official/:pluginId` |
+| Remote channel detail | `/plugins/remote/:channel` |
+
+## Proposed Information Architecture
+
+Top-level sections:
+
+| Section | User label | Contents |
+| --- | --- | --- |
+| Plugins | Plugins | official plugin packages, added/recommended plugin cards, Remote virtual plugin cards when filtered into plugin catalog |
+| Skills | Skills | installed skills, install, edit, sync import/export, draft suggestion toggle |
+| MCP | MCP Servers | user MCP servers, plugin-owned MCP status, MCP market/add flow |
+| Remote | Remote | virtual plugin cards for Telegram, Feishu/Lark Remote, QQBot, Discord, WeChat iLink |
+
+The visual top tab row can start with `Plugins` and `Skills` as in the Codex screenshot, then add `MCP` and `Remote` if all four areas ship in the same increment. If product wants the screenshot to stay visually lighter, `MCP` and `Remote` can appear as catalog filters/cards inside `Plugins`, but they still need addressable routes.
+
+Remote naming must avoid collision with official plugins:
+
+| Existing item | Display name in Plugins |
+| --- | --- |
+| `com.deepchat.plugins.feishu` official plugin | `Feishu/Lark Integration` |
+| `remote:feishu` virtual plugin | `Feishu/Lark Remote` |
+| `remote:telegram` virtual plugin | `Telegram Remote` |
+
+## Main Window Plugins UX
+
+### Desktop Layout
+
+```text
+┌──────────────────────────────────────────────────────────────────────────────┐
+│ AppBar │
+├───────────────┬──────────────────────────────────────────────────────────────┤
+│ New Chat │ [Plugins] [Skills] [MCP] [Remote] + ↻ │
+│ Search │ │
+│ Plugins │ Plugins │
+│ │ Work with DeepChat across your favorite tools │
+│ Pinned │ ┌────────────────────────────────────────────┐ │
+│ ... │ │ Search plugins, skills, MCP servers... │ │
+│ Projects │ └────────────────────────────────────────────┘ │
+│ ... │ │
+│ Settings │ Added Manage │
+│ │ [CUA] [Feishu] [Telegram] [Skill pack] │
+│ │ │
+│ │ Featured │
+│ │ Computer Use Add Chrome Add │
+│ │ Spreadsheets ... Presentations ... │
+└───────────────┴──────────────────────────────────────────────────────────────┘
+```
+
+### Detail Route Layout
+
+```text
+┌──────────────────────────────────────────────────────────────────────────────┐
+│ AppBar │
+├───────────────┬──────────────────────────────────────────────────────────────┤
+│ New Chat │ [Plugins] [Skills] [MCP] [Remote] │
+│ Search │ ← Back to Plugins │
+│ Plugins │ Telegram Remote on/off │
+│ │ Status: running · bindings: 2 · last error: none │
+│ Pinned │ │
+│ ... │ ┌ Credentials ────────────────────────────────────────────┐ │
+│ Settings │ │ Bot token / app credentials │ │
+│ │ └─────────────────────────────────────────────────────────┘ │
+│ │ ┌ Remote Control ────────────────────────────────────────┐ │
+│ │ │ Default agent · Default workdir · Pairing │ │
+│ │ └─────────────────────────────────────────────────────────┘ │
+│ │ ┌ Bindings ──────────────────────────────────────────────┐ │
+│ │ │ Existing chats/channels and remove actions │ │
+│ │ └─────────────────────────────────────────────────────────┘ │
+└───────────────┴──────────────────────────────────────────────────────────────┘
+```
+
+### Narrow Main Window Layout
+
+At constrained widths, keep the same app shell and avoid modal navigation:
+
+```text
+┌────────────────────────────────────┐
+│ AppBar │
+├────┬───────────────────────────────┤
+│ AI │ [Plugins][Skills][MCP] │
+│ .. │ [Remote] │
+│ ├───────────────────────────────┤
+│ │ Search │
+│ │ │
+│ │ Card list / Detail page │
+│ │ │
+└────┴───────────────────────────────┘
+```
+
+Narrow behavior:
+
+- If sidebar is collapsed, it stays collapsed.
+- Section navigation becomes a wrapped horizontal tab row.
+- Detail pages keep a top back button.
+- Long tokens, paths and errors must truncate with tooltip or wrap in a controlled block.
+- Forms use one column.
+
+## Sidebar UX
+
+### Target Expanded Shape
+
+```text
+┌────┬──────────────────────────────┐
+│ AI │ 所有 Agents │
+│ │ │
+│ │ ┌──────────────────────────┐ │
+│ │ │ ✎ New Chat ⌘N │ │
+│ │ │ 🔍 Search ⌘P │ │
+│ │ │ ⌘ Plugins │ │
+│ │ └──────────────────────────┘ │
+│ │ │
+│ │ 置顶会话 │
+│ │ ... │
+└────┴──────────────────────────────┘
+```
+
+Notes:
+
+- `New Chat` starts a new conversation through the existing session store path and navigates to `/chat` if needed.
+- `Search` opens existing Spotlight/search behavior; it is not a second search implementation.
+- `Plugins` routes the current main window to `/plugins`.
+- Shortcut badges display only for existing registered shortcuts. Do not add a new shortcut just to fill the badge.
+- The existing collapsed rail stays visually and behaviorally unchanged.
+- The existing Agent icon rail remains the collapsed-state affordance; no new collapsed Plugins icon is added.
+- The group-mode toggle and new-chat plus button in the old header should not remain as competing primary actions in expanded mode. Group mode can move near the session list header or stay as a compact secondary control.
+
+### Collapsed Shape
+
+Collapsed state remains current:
+
+```text
+┌────┐
+│ ◎ │ all agents / agent icons
+│ .. │
+│ 🔍 │ existing search affordance
+│ .. │ existing status/theme/sidebar/settings affordances
+└────┘
+```
+
+## Settings UX
+
+Settings remains for system/model/account/data preferences:
+
+```text
+Settings
+├── Overview
+├── Common
+├── Display
+├── Environments
+├── Providers
+├── DeepChat Agents
+├── ACP
+├── Notifications / Hooks
+├── Scheduled Tasks
+├── Prompt
+├── Memory
+├── Knowledge Base
+├── Database
+├── Shortcuts
+└── About
+```
+
+Removed from visible Settings navigation:
+
+- MCP
+- Remote
+- Plugins
+- Skills
+
+Compatibility behavior:
+
+- Existing internal settings routes can remain hidden during migration.
+- Direct navigation to removed route names should focus the main window and route to the matching `/plugins...` page when possible.
+- Settings Overview search should not list hidden Plugins-owned entries.
+- Settings activity records can keep historical `routeName` values; opening them should redirect to Plugins when the route is now Plugins-owned.
+
+## Acceptance Criteria
+
+### Main Window Route
+
+- `/plugins` renders inside the existing main app shell and keeps `WindowSideBar` visible.
+- Opening Plugins from the main sidebar navigates the current main window to `/plugins`.
+- No new Plugins BrowserWindow is created.
+- No new `src/renderer/plugins` renderer entry is added.
+- `AppBar`, sidebar, theme, language direction and global overlays continue to work.
+- The page has stable responsive behavior and remains usable at narrow widths.
+- User-facing strings use i18n keys.
+
+### Official Plugins
+
+- Official plugin list keeps enable/disable/status behavior.
+- Plugin-owned MCP errors remain visible.
+- Opening an official plugin settings/detail uses `/plugins/official/:pluginId`, not Settings and not a per-plugin BrowserWindow.
+- CUA detail includes runtime/MCP status, permission checks and permission guide actions.
+- Feishu/Lark Integration detail distinguishes MCP/Skill integration from Feishu/Lark Remote control.
+- Legacy `settings.open` plugin action is not used as the primary UI path after migration.
+
+### MCP
+
+- MCP global enablement, server list, add/edit, market view and NPM registry controls remain available from Plugins.
+- Plugin-owned MCP servers remain read-only where their owning plugin controls lifecycle.
+- MCP install deeplinks focus the main window and route to `/plugins/mcp` instead of opening Settings.
+
+### Skills
+
+- Installed Skills list, search, install, edit, delete, sync import/export and draft suggestion toggle remain available from Plugins.
+- First-launch sync prompt remains available if it is still part of the current product flow.
+- Skill drag/drop and URL/zip/folder install behavior remains unchanged.
+
+### Remote
+
+- Every implemented `RemoteChannelDescriptor` appears as a Remote virtual plugin card.
+- Each card shows enabled state, runtime state, binding/pairing summary and last error when present.
+- Each card opens `/plugins/remote/:channel`.
+- Channel settings preserve current behavior:
+ - credentials
+ - enable/disable
+ - default agent
+ - default workdir
+ - pairing
+ - bindings/principals
+ - channel-specific login/account controls for WeChat iLink
+- Saving a channel setting still uses `remoteControl.saveChannelSettings`.
+- Remote status indicator in the main sidebar continues to work.
+
+### Main Sidebar
+
+- Expanded sidebar shows `New Chat`, `Search`, `Plugins` before pinned sessions.
+- `所有 Agents` remains visible.
+- Collapsed sidebar keeps current visual shape and behavior.
+- No new collapsed icon button is added.
+- Session list pagination, pinned section, project grouping, drag reorder and keyboard shortcut badges continue working.
+
+### Settings Removal
+
+- Settings sidebar no longer shows MCP, Remote, Plugins or Skills.
+- Settings Overview search no longer returns MCP, Remote, Plugins or Skills as settings pages.
+- Settings Overview no longer uses MCP as one of the primary system metrics or quick-start tasks.
+- Existing app code that opens `settings-mcp`, `settings-remote`, `settings-plugins` or `settings-skills` is migrated or redirected to `/plugins...`.
+
+## Platform and Accessibility Requirements
+
+- Keyboard navigation works across top tabs, search, card list, detail forms and back navigation.
+- `Esc` closes transient dialogs only; it does not leave `/plugins`.
+- `Tab` order follows visual order.
+- Buttons and icon-only controls have accessible labels.
+- Status colors are not the only status signal; labels must remain visible.
+- Long file paths, tokens, error strings and command lines do not overflow their container.
+- Remote credentials remain password inputs by default, preserving current reveal behavior.
+- Linux and Windows backgrounds must not rely on macOS-only materials.
+- RTL languages should inherit existing app i18n direction handling.
+
+## Open Questions
+
+None.
diff --git a/docs/features/plugins-hub/tasks.md b/docs/features/plugins-hub/tasks.md
new file mode 100644
index 0000000000..bc9faa1fcd
--- /dev/null
+++ b/docs/features/plugins-hub/tasks.md
@@ -0,0 +1,119 @@
+# Plugins Hub Tasks
+
+## 0. Review Gate
+
+- [ ] Review `spec.md` with product/maintainers.
+- [ ] Review `plan.md` main-route architecture, route compatibility, and sidebar layout.
+- [ ] Confirm no unresolved clarification markers exist before implementation.
+- [ ] Keep this SDD folder active until the feature lands or is deliberately abandoned.
+
+## 1. Main Route Skeleton
+
+- [ ] Add `/plugins` route family to the existing main renderer router.
+- [ ] Add `PluginsHubPage.vue` inside `src/renderer/src/pages/plugins/`.
+- [ ] Add top tab navigation for Plugins, Skills, MCP and Remote or the chosen first-increment subset.
+- [ ] Add Codex-like catalog placeholder with title, subtitle, search, added strip and featured sections.
+- [ ] Keep `WindowSideBar`, `AppBar`, global overlays, theme and i18n behavior intact.
+- [ ] Add i18n keys for route, page title, subtitle, tabs and search placeholder.
+- [ ] Add renderer tests proving `/plugins` renders inside the existing app shell.
+
+## 2. Main-Process Navigation Compatibility
+
+- [ ] Add or reuse a typed main-window focus/navigation route for main-process initiated navigation.
+- [ ] Ensure the route can focus/create the normal main window and navigate to `/plugins...`.
+- [ ] Do not add a Plugins BrowserWindow.
+- [ ] Do not add `src/renderer/plugins` or a separate renderer entry.
+- [ ] Add tests for focusing main and navigating to `/plugins/mcp`.
+
+## 3. Settings Navigation Cleanup
+
+- [ ] Hide or remove visible Settings navigation items for MCP, Remote, Plugins, and Skills.
+- [ ] Keep compatibility routes or redirect handlers for old route names.
+- [ ] Map old route names to main `/plugins...` routes.
+- [ ] Remove MCP from Settings Overview primary metric.
+- [ ] Remove or replace Settings Overview `start-mcp` quick task.
+- [ ] Ensure Settings Overview search does not return hidden Plugins-owned pages.
+- [ ] Update Settings activity click behavior for historical routes.
+- [ ] Add tests for Settings navigation groups and hidden route handling.
+
+## 4. MCP Section
+
+- [ ] Create `/plugins/mcp` page using current MCP store/client behavior.
+- [ ] Reuse `McpServers` for list/add/edit/toggle.
+- [ ] Reuse MCP market view inside `/plugins/mcp?view=market`.
+- [ ] Reuse NPM registry controls.
+- [ ] Move MCP install deeplink target from Settings to main `/plugins/mcp`.
+- [ ] Move MCP install event handling into the main app or Plugins route bootstrap.
+- [ ] Keep plugin-owned MCP server read-only behavior.
+- [ ] Add tests for deeplink route target and MCP page render.
+
+## 5. Skills Section
+
+- [ ] Create `/plugins/skills` page from current Skills settings behavior.
+- [ ] Reuse skill list, search, install, edit, delete, sync import/export.
+- [ ] Preserve draft suggestion toggle.
+- [ ] Preserve first-launch sync prompt if still required.
+- [ ] Ensure skill dialogs/sheets fit the main Plugins page shell.
+- [ ] Add renderer tests for empty/list/search/install entry behavior.
+
+## 6. Official Plugins Section
+
+- [ ] Create official plugin list route from `PluginClient.listPlugins`.
+- [ ] Add detail route `/plugins/official/:pluginId`.
+- [ ] Keep enable/disable actions.
+- [ ] Show runtime status, plugin-owned MCP status and last errors.
+- [ ] Add native CUA detail sections for runtime status, permissions and permission guide actions.
+- [ ] Add native Feishu/Lark Integration detail that distinguishes it from Feishu/Lark Remote.
+- [ ] Stop first-party Plugins UI from calling `settings.open`.
+- [ ] Keep `settings.open` only as temporary compatibility fallback.
+- [ ] Add tests for list/detail action behavior.
+
+## 7. Remote Virtual Plugins
+
+- [ ] Build remote virtual cards from `remoteControl.listChannels`.
+- [ ] Fetch and display per-channel status.
+- [ ] Add `/plugins/remote` list route.
+- [ ] Add `/plugins/remote/:channel` detail route.
+- [ ] Extract only the needed RemoteSettings channel sections into reusable components.
+- [ ] Preserve credentials fields and password reveal behavior.
+- [ ] Preserve enable/disable save behavior.
+- [ ] Preserve default agent and default workdir behavior.
+- [ ] Preserve pairing flow for Telegram, Feishu/Lark, QQBot and Discord.
+- [ ] Preserve binding/principal removal behavior.
+- [ ] Preserve WeChat iLink login/account controls.
+- [ ] Route sidebar remote status button to `/plugins/remote` or selected channel detail.
+- [ ] Add tests for card mapping, save, pairing and bindings.
+
+## 8. Main Sidebar Layout
+
+- [ ] Replace expanded sidebar header/search area with command list.
+- [ ] Keep `所有 Agents` title.
+- [ ] Wire `New Chat` row to navigate to `/chat` and start a new conversation.
+- [ ] Wire `Search` row to existing Spotlight behavior.
+- [ ] Wire `Plugins` row to `router.push({ name: 'plugins' })`.
+- [ ] Display shortcut badges only for existing shortcuts.
+- [ ] Keep collapsed sidebar visual behavior unchanged.
+- [ ] Preserve session list pagination, pinned section, project grouping and reorder.
+- [ ] Add renderer tests for expanded rows and collapsed state.
+- [ ] Capture before/after ASCII blocks in PR description.
+
+## 9. Cross-Platform UI QA
+
+- [ ] Verify macOS light/dark with app shell and sidebar.
+- [ ] Verify Windows light/dark with app shell and sidebar.
+- [ ] Verify Linux opaque background.
+- [ ] Verify narrow main window layout with expanded sidebar.
+- [ ] Verify narrow main window layout with collapsed sidebar.
+- [ ] Verify long paths/tokens/errors do not overflow.
+- [ ] Verify keyboard navigation and focus order.
+- [ ] Verify Chinese and English labels.
+
+## 10. Final Quality Gates
+
+- [ ] Run `pnpm run format`.
+- [ ] Run `pnpm run i18n`.
+- [ ] Run `pnpm run lint`.
+- [ ] Run `pnpm run typecheck`.
+- [ ] Run targeted renderer tests for Plugins route and sidebar.
+- [ ] Run targeted main tests for navigation/deeplink behavior.
+- [ ] Update durable docs or remove/archive active plan/tasks after implementation lands.
From f11cae2550f6a8d6442832903fbb0ff5795543c9 Mon Sep 17 00:00:00 2001
From: zerob13
Date: Thu, 25 Jun 2026 15:15:32 +0800
Subject: [PATCH 02/14] docs: add plugins hub doc
---
docs/features/plugins-hub/plan.md | 30 +++++++++-
docs/features/plugins-hub/spec.md | 90 ++++++++++++++++++------------
docs/features/plugins-hub/tasks.md | 6 ++
3 files changed, 86 insertions(+), 40 deletions(-)
diff --git a/docs/features/plugins-hub/plan.md b/docs/features/plugins-hub/plan.md
index b6a59380ca..aa5a879e01 100644
--- a/docs/features/plugins-hub/plan.md
+++ b/docs/features/plugins-hub/plan.md
@@ -318,9 +318,11 @@ right column
│ ├── New Chat
│ ├── Search
│ └── Plugins
-├── compact session controls when needed
-├── pinned section
-└── session groups
+├── blank spacer
+├── pinned section when non-empty
+├── Chat group
+├── 工作区 header + existing group-mode/sort toggle
+└── project groups
```
Command behavior:
@@ -334,10 +336,12 @@ Command behavior:
Keep:
- Agent icon rail.
+- Settings/theme/sidebar controls in the existing left rail.
- collapsed width and transitions.
- session pagination and fill checks.
- pinned collapse behavior.
- project grouping/reorder behavior.
+- existing group-mode/sort behavior, moved to the `工作区` header.
- shortcut badge logic for sessions.
Question the old inline session search:
@@ -346,6 +350,26 @@ Question the old inline session search:
- Search row opens Spotlight, which already searches sessions/messages/settings/actions.
- If users later need local-only filtering, add it inside Spotlight or as a session-list filter command, not as a second persistent input.
+Right column ordering:
+
+```text
+所有 Agents
+
+New Chat
+Search
+Plugins
+
+Pinned (only if any)
+...
+Chat
+...
+工作区 [group/sort toggle]
+project groups
+...
+```
+
+Do not add Settings, theme, collapse, remote status or other rail controls into this right column.
+
## Deeplinks and External Entry Points
Update callers:
diff --git a/docs/features/plugins-hub/spec.md b/docs/features/plugins-hub/spec.md
index 1ffa1d2e39..b5c8bbbc93 100644
--- a/docs/features/plugins-hub/spec.md
+++ b/docs/features/plugins-hub/spec.md
@@ -27,7 +27,9 @@ Remote channel 是 Plugins UI 里的 virtual plugin,不是 `.dcplugin` 安装
- 新增主窗口 route:`/plugins`。
- 在主窗口主内容区集中管理 Official Plugins、MCP、Skills、Remote。
-- 主窗口左侧 sidebar 展开态显示 `New Chat`、`Search`、`Plugins` command list,点击 `Plugins` 进入 `/plugins`。
+- 主窗口 sidebar 展开态右栏显示 `New Chat`、`Search`、`Plugins` command list,点击 `Plugins` 进入 `/plugins`。
+- `Plugins` command 下方留出空行,再显示 `Pinned`、`Chat`、`工作区` 和 project groups。
+- Settings 等底部 app controls 继续留在现有左侧 rail,不进入展开右栏。
- `Plugins` 页面保留左侧 sidebar,右侧内容区切换,不创建新窗口。
- Remote 每个 implemented channel 都作为 plugin-like card 出现,并能进入该 channel 的详情子路由。
- Remote 设置页不再在 Settings 中展示;从列表进入详情时使用主窗口 Plugins 子路由。
@@ -132,21 +134,21 @@ Remote naming must avoid collision with official plugins:
┌──────────────────────────────────────────────────────────────────────────────┐
│ AppBar │
├───────────────┬──────────────────────────────────────────────────────────────┤
-│ New Chat │ [Plugins] [Skills] [MCP] [Remote] + ↻ │
-│ Search │ │
-│ Plugins │ Plugins │
-│ │ Work with DeepChat across your favorite tools │
-│ Pinned │ ┌────────────────────────────────────────────┐ │
-│ ... │ │ Search plugins, skills, MCP servers... │ │
-│ Projects │ └────────────────────────────────────────────┘ │
-│ ... │ │
-│ Settings │ Added Manage │
-│ │ [CUA] [Feishu] [Telegram] [Skill pack] │
-│ │ │
-│ │ Featured │
-│ │ Computer Use Add Chrome Add │
-│ │ Spreadsheets ... Presentations ... │
-└───────────────┴──────────────────────────────────────────────────────────────┘
+│rail│ expanded sidebar │ [Plugins] [Skills] [MCP] [Remote] + ↻ │
+│ │ 所有 Agents │ │
+│ │ New Chat │ Plugins │
+│ │ Search │ Work with DeepChat across your favorite tools │
+│ │ Plugins │ ┌────────────────────────────────────────────┐ │
+│ │ │ │ Search plugins, skills, MCP servers... │ │
+│ │ Pinned │ └────────────────────────────────────────────┘ │
+│ │ ... │ │
+│ │ Chat │ Added Manage │
+│ │ ... │ [CUA] [Feishu] [Telegram] [Skill pack] │
+│ │ 工作区 [sort]│ │
+│ │ project A │ Featured │
+│ │ project B │ Computer Use Add Chrome Add │
+│⚙︎ │ │ Spreadsheets ... Presentations ... │
+└────┴──────────────────┴──────────────────────────────────────────────────────────────┘
```
### Detail Route Layout
@@ -155,21 +157,21 @@ Remote naming must avoid collision with official plugins:
┌──────────────────────────────────────────────────────────────────────────────┐
│ AppBar │
├───────────────┬──────────────────────────────────────────────────────────────┤
-│ New Chat │ [Plugins] [Skills] [MCP] [Remote] │
-│ Search │ ← Back to Plugins │
-│ Plugins │ Telegram Remote on/off │
-│ │ Status: running · bindings: 2 · last error: none │
-│ Pinned │ │
-│ ... │ ┌ Credentials ────────────────────────────────────────────┐ │
-│ Settings │ │ Bot token / app credentials │ │
-│ │ └─────────────────────────────────────────────────────────┘ │
-│ │ ┌ Remote Control ────────────────────────────────────────┐ │
-│ │ │ Default agent · Default workdir · Pairing │ │
-│ │ └─────────────────────────────────────────────────────────┘ │
-│ │ ┌ Bindings ──────────────────────────────────────────────┐ │
-│ │ │ Existing chats/channels and remove actions │ │
-│ │ └─────────────────────────────────────────────────────────┘ │
-└───────────────┴──────────────────────────────────────────────────────────────┘
+│rail│ expanded sidebar │ [Plugins] [Skills] [MCP] [Remote] │
+│ │ 所有 Agents │ ← Back to Plugins │
+│ │ New Chat │ Telegram Remote on/off │
+│ │ Search │ Status: running · bindings: 2 · last error: none │
+│ │ Plugins │ │
+│ │ │ ┌ Credentials ────────────────────────────────────┐ │
+│ │ Pinned │ │ Bot token / app credentials │ │
+│ │ ... │ └─────────────────────────────────────────────────┘ │
+│ │ Chat │ ┌ Remote Control ────────────────────────────────┐ │
+│ │ 工作区 [sort]│ │ Default agent · Default workdir · Pairing │ │
+│ │ project A │ └─────────────────────────────────────────────────┘ │
+│⚙︎ │ │ ┌ Bindings ──────────────────────────────────────┐ │
+│ │ │ │ Existing chats/channels and remove actions │ │
+│ │ │ └─────────────────────────────────────────────────┘ │
+└────┴──────────────────┴──────────────────────────────────────────────────────┘
```
### Narrow Main Window Layout
@@ -180,9 +182,9 @@ At constrained widths, keep the same app shell and avoid modal navigation:
┌────────────────────────────────────┐
│ AppBar │
├────┬───────────────────────────────┤
-│ AI │ [Plugins][Skills][MCP] │
-│ .. │ [Remote] │
-│ ├───────────────────────────────┤
+│rail│ [Plugins][Skills][MCP] │
+│ │ [Remote] │
+│⚙︎ ├───────────────────────────────┤
│ │ Search │
│ │ │
│ │ Card list / Detail page │
@@ -204,7 +206,7 @@ Narrow behavior:
```text
┌────┬──────────────────────────────┐
-│ AI │ 所有 Agents │
+│rail│ 所有 Agents │
│ │ │
│ │ ┌──────────────────────────┐ │
│ │ │ ✎ New Chat ⌘N │ │
@@ -212,8 +214,13 @@ Narrow behavior:
│ │ │ ⌘ Plugins │ │
│ │ └──────────────────────────┘ │
│ │ │
-│ │ 置顶会话 │
+│ │ Pinned (if any) │
│ │ ... │
+│ │ Chat │
+│ │ ... │
+│ │ 工作区 [sort] │
+│ │ project groups │
+│⚙︎ │ ... │
└────┴──────────────────────────────┘
```
@@ -222,10 +229,15 @@ Notes:
- `New Chat` starts a new conversation through the existing session store path and navigates to `/chat` if needed.
- `Search` opens existing Spotlight/search behavior; it is not a second search implementation.
- `Plugins` routes the current main window to `/plugins`.
+- There is a blank spacer after `Plugins` before the conversation sections.
+- `Pinned` is rendered only when pinned sessions exist.
+- `Chat` remains the unprojected/default chat group.
+- `工作区` is a section title for project groups; the existing group-mode/sort toggle moves to this row.
- Shortcut badges display only for existing registered shortcuts. Do not add a new shortcut just to fill the badge.
- The existing collapsed rail stays visually and behaviorally unchanged.
- The existing Agent icon rail remains the collapsed-state affordance; no new collapsed Plugins icon is added.
-- The group-mode toggle and new-chat plus button in the old header should not remain as competing primary actions in expanded mode. Group mode can move near the session list header or stay as a compact secondary control.
+- Settings, theme and other bottom controls stay in the existing left rail. They are not listed under `Plugins` in the expanded right column.
+- The old header new-chat plus button is removed as a competing primary action. The existing group-mode/sort control moves to the `工作区` header.
### Collapsed Shape
@@ -329,7 +341,11 @@ Compatibility behavior:
### Main Sidebar
- Expanded sidebar shows `New Chat`, `Search`, `Plugins` before pinned sessions.
+- Expanded sidebar inserts a blank spacer after `Plugins`.
+- Expanded sidebar shows `Pinned` only when pinned sessions exist, then `Chat`, then `工作区`.
+- The existing group-mode/sort toggle is placed on the `工作区` header row.
- `所有 Agents` remains visible.
+- Settings and other bottom controls remain in the existing left rail, not in the expanded right column.
- Collapsed sidebar keeps current visual shape and behavior.
- No new collapsed icon button is added.
- Session list pagination, pinned section, project grouping, drag reorder and keyboard shortcut badges continue working.
diff --git a/docs/features/plugins-hub/tasks.md b/docs/features/plugins-hub/tasks.md
index bc9faa1fcd..65c000a335 100644
--- a/docs/features/plugins-hub/tasks.md
+++ b/docs/features/plugins-hub/tasks.md
@@ -91,6 +91,12 @@
- [ ] Wire `New Chat` row to navigate to `/chat` and start a new conversation.
- [ ] Wire `Search` row to existing Spotlight behavior.
- [ ] Wire `Plugins` row to `router.push({ name: 'plugins' })`.
+- [ ] Add a blank spacer after the `Plugins` command row.
+- [ ] Render `Pinned` only when pinned sessions exist.
+- [ ] Keep the `Chat` group after `Pinned`.
+- [ ] Add `工作区` header before project groups.
+- [ ] Move the existing group-mode/sort toggle to the `工作区` header.
+- [ ] Keep Settings/theme/sidebar controls in the existing left rail, not in the expanded right column.
- [ ] Display shortcut badges only for existing shortcuts.
- [ ] Keep collapsed sidebar visual behavior unchanged.
- [ ] Preserve session list pagination, pinned section, project grouping and reorder.
From 6cf01389547cb5a0187abb1b38fd0314b02cded3 Mon Sep 17 00:00:00 2001
From: zerob13
Date: Thu, 25 Jun 2026 15:35:26 +0800
Subject: [PATCH 03/14] feat(plugins): add main window plugins hub
---
docs/features/plugins-hub/plan.md | 10 +-
docs/features/plugins-hub/tasks.md | 132 +++---
src/main/presenter/deeplinkPresenter/index.ts | 9 +-
.../settings/components/McpSettings.vue | 12 +-
.../settings/components/RemoteSettings.vue | 26 +-
.../settings/components/SettingsOverview.vue | 27 --
.../settings/lib/guidedOnboardingSettings.ts | 13 +-
src/renderer/src/components/WindowSideBar.vue | 225 ++++++---
.../components/chat-input/McpIndicator.vue | 6 +-
.../components/chat-input/SkillsIndicator.vue | 6 +-
src/renderer/src/i18n/da-DK/chat.json | 5 +-
src/renderer/src/i18n/da-DK/routes.json | 3 +-
src/renderer/src/i18n/da-DK/settings.json | 18 +
src/renderer/src/i18n/de-DE/chat.json | 5 +-
src/renderer/src/i18n/de-DE/routes.json | 3 +-
src/renderer/src/i18n/de-DE/settings.json | 18 +
src/renderer/src/i18n/en-US/chat.json | 3 +
src/renderer/src/i18n/en-US/routes.json | 1 +
src/renderer/src/i18n/en-US/settings.json | 18 +
src/renderer/src/i18n/es-ES/chat.json | 5 +-
src/renderer/src/i18n/es-ES/routes.json | 3 +-
src/renderer/src/i18n/es-ES/settings.json | 18 +
src/renderer/src/i18n/fa-IR/chat.json | 5 +-
src/renderer/src/i18n/fa-IR/routes.json | 3 +-
src/renderer/src/i18n/fa-IR/settings.json | 18 +
src/renderer/src/i18n/fr-FR/chat.json | 5 +-
src/renderer/src/i18n/fr-FR/routes.json | 3 +-
src/renderer/src/i18n/fr-FR/settings.json | 18 +
src/renderer/src/i18n/he-IL/chat.json | 5 +-
src/renderer/src/i18n/he-IL/routes.json | 3 +-
src/renderer/src/i18n/he-IL/settings.json | 18 +
src/renderer/src/i18n/id-ID/chat.json | 5 +-
src/renderer/src/i18n/id-ID/routes.json | 3 +-
src/renderer/src/i18n/id-ID/settings.json | 18 +
src/renderer/src/i18n/it-IT/chat.json | 5 +-
src/renderer/src/i18n/it-IT/routes.json | 3 +-
src/renderer/src/i18n/it-IT/settings.json | 18 +
src/renderer/src/i18n/ja-JP/chat.json | 5 +-
src/renderer/src/i18n/ja-JP/routes.json | 3 +-
src/renderer/src/i18n/ja-JP/settings.json | 18 +
src/renderer/src/i18n/ko-KR/chat.json | 5 +-
src/renderer/src/i18n/ko-KR/routes.json | 3 +-
src/renderer/src/i18n/ko-KR/settings.json | 18 +
src/renderer/src/i18n/ms-MY/chat.json | 5 +-
src/renderer/src/i18n/ms-MY/routes.json | 3 +-
src/renderer/src/i18n/ms-MY/settings.json | 18 +
src/renderer/src/i18n/pl-PL/chat.json | 5 +-
src/renderer/src/i18n/pl-PL/routes.json | 3 +-
src/renderer/src/i18n/pl-PL/settings.json | 18 +
src/renderer/src/i18n/pt-BR/chat.json | 5 +-
src/renderer/src/i18n/pt-BR/routes.json | 3 +-
src/renderer/src/i18n/pt-BR/settings.json | 18 +
src/renderer/src/i18n/ru-RU/chat.json | 5 +-
src/renderer/src/i18n/ru-RU/routes.json | 3 +-
src/renderer/src/i18n/ru-RU/settings.json | 18 +
src/renderer/src/i18n/tr-TR/chat.json | 5 +-
src/renderer/src/i18n/tr-TR/routes.json | 3 +-
src/renderer/src/i18n/tr-TR/settings.json | 18 +
src/renderer/src/i18n/vi-VN/chat.json | 5 +-
src/renderer/src/i18n/vi-VN/routes.json | 3 +-
src/renderer/src/i18n/vi-VN/settings.json | 18 +
src/renderer/src/i18n/zh-CN/chat.json | 3 +
src/renderer/src/i18n/zh-CN/routes.json | 1 +
src/renderer/src/i18n/zh-CN/settings.json | 18 +
src/renderer/src/i18n/zh-HK/chat.json | 5 +-
src/renderer/src/i18n/zh-HK/routes.json | 3 +-
src/renderer/src/i18n/zh-HK/settings.json | 18 +
src/renderer/src/i18n/zh-TW/chat.json | 5 +-
src/renderer/src/i18n/zh-TW/routes.json | 3 +-
src/renderer/src/i18n/zh-TW/settings.json | 18 +
src/renderer/src/lib/storeInitializer.ts | 13 +
src/renderer/src/pages/WelcomePage.vue | 8 +
.../src/pages/plugins/McpPluginsPage.vue | 7 +
.../plugins/OfficialPluginDetailPage.vue | 241 ++++++++++
.../src/pages/plugins/PluginsCatalogPage.vue | 437 ++++++++++++++++++
.../src/pages/plugins/PluginsHubPage.vue | 79 ++++
.../src/pages/plugins/RemotePluginsPage.vue | 7 +
.../src/pages/plugins/SkillsPluginsPage.vue | 7 +
src/renderer/src/router/index.ts | 64 +++
src/renderer/src/stores/ui/spotlight.ts | 16 +-
src/shared/settingsNavigation.ts | 12 +-
.../renderer/components/WindowSideBar.test.ts | 158 +++----
82 files changed, 1699 insertions(+), 328 deletions(-)
create mode 100644 src/renderer/src/pages/plugins/McpPluginsPage.vue
create mode 100644 src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue
create mode 100644 src/renderer/src/pages/plugins/PluginsCatalogPage.vue
create mode 100644 src/renderer/src/pages/plugins/PluginsHubPage.vue
create mode 100644 src/renderer/src/pages/plugins/RemotePluginsPage.vue
create mode 100644 src/renderer/src/pages/plugins/SkillsPluginsPage.vue
diff --git a/docs/features/plugins-hub/plan.md b/docs/features/plugins-hub/plan.md
index aa5a879e01..965d3d5bb9 100644
--- a/docs/features/plugins-hub/plan.md
+++ b/docs/features/plugins-hub/plan.md
@@ -49,13 +49,13 @@ plugins-official-detail
plugins-remote-detail
```
-External/main-process callers should not know UI component internals. Add a narrow route or event only where the main process needs to focus the main window and navigate:
+External/main-process callers should not know UI component internals. Reuse the existing app-runtime event path where possible. Only add a narrow route if a future main-process caller needs generic main-window navigation:
```text
system.openMainRoute({ routeName: 'plugins-mcp', params? }) -> { focused: boolean }
```
-If an existing main-window navigation route already exists during implementation, reuse it. Do not add a Plugins-specific window route.
+For the first increment, MCP install deeplinks reuse `DEEPLINK_EVENTS.MCP_INSTALL` and the main app deeplink handler routes the renderer to `/plugins/mcp`. Do not add a Plugins-specific window route.
## Affected Boundaries
@@ -64,7 +64,7 @@ If an existing main-window navigation route already exists during implementation
| `src/renderer/src/router/index.ts` | Add `/plugins` route family |
| `src/renderer/src/App.vue` | Keep existing shell; ensure `/plugins` receives same global overlays/theme/i18n |
| `WindowSideBar.vue` | Add expanded command list and route Plugins row to `/plugins` |
-| `renderer/api` | Add or reuse a small main-window navigation client only for main-process initiated navigation |
+| `renderer/api` | Reuse existing clients; add a main-window navigation client only if a generic main-process caller appears |
| `shared/contracts/routes` | Add narrow focus/navigate route only if deeplink/main process cannot use existing event path |
| Settings renderer | Remove/hide Plugins-owned nav entries and overview links |
| Deeplink presenter | Route MCP install deeplink to main `/plugins/mcp` page |
@@ -217,7 +217,9 @@ Compatibility:
## Remote Migration
-Do not keep `RemoteSettings.vue` as one giant page inside Plugins. It is already too large.
+First increment: reuse `RemoteSettings.vue` inside `/plugins/remote` and `/plugins/remote/:channel`, with route-param synchronization so direct channel links open the matching tab. This keeps the existing credential, pairing, default agent/workdir, bindings and WeChat iLink behavior intact.
+
+Follow-up refactor: extract channel sections from `RemoteSettings.vue` into reusable components. The file is already large, but splitting it before moving the route would increase regression risk and delay the user-visible entry-point cleanup.
Refactor only around real channel boundaries:
diff --git a/docs/features/plugins-hub/tasks.md b/docs/features/plugins-hub/tasks.md
index 65c000a335..2b68d2a790 100644
--- a/docs/features/plugins-hub/tasks.md
+++ b/docs/features/plugins-hub/tasks.md
@@ -2,104 +2,104 @@
## 0. Review Gate
-- [ ] Review `spec.md` with product/maintainers.
-- [ ] Review `plan.md` main-route architecture, route compatibility, and sidebar layout.
-- [ ] Confirm no unresolved clarification markers exist before implementation.
+- [x] Review `spec.md` with product/maintainers.
+- [x] Review `plan.md` main-route architecture, route compatibility, and sidebar layout.
+- [x] Confirm no unresolved clarification markers exist before implementation.
- [ ] Keep this SDD folder active until the feature lands or is deliberately abandoned.
## 1. Main Route Skeleton
-- [ ] Add `/plugins` route family to the existing main renderer router.
-- [ ] Add `PluginsHubPage.vue` inside `src/renderer/src/pages/plugins/`.
-- [ ] Add top tab navigation for Plugins, Skills, MCP and Remote or the chosen first-increment subset.
-- [ ] Add Codex-like catalog placeholder with title, subtitle, search, added strip and featured sections.
-- [ ] Keep `WindowSideBar`, `AppBar`, global overlays, theme and i18n behavior intact.
-- [ ] Add i18n keys for route, page title, subtitle, tabs and search placeholder.
+- [x] Add `/plugins` route family to the existing main renderer router.
+- [x] Add `PluginsHubPage.vue` inside `src/renderer/src/pages/plugins/`.
+- [x] Add top tab navigation for Plugins, Skills, MCP and Remote or the chosen first-increment subset.
+- [x] Add Codex-like catalog placeholder with title, subtitle, search, added strip and featured sections.
+- [x] Keep `WindowSideBar`, `AppBar`, global overlays, theme and i18n behavior intact.
+- [x] Add i18n keys for route, page title, subtitle, tabs and search placeholder.
- [ ] Add renderer tests proving `/plugins` renders inside the existing app shell.
## 2. Main-Process Navigation Compatibility
-- [ ] Add or reuse a typed main-window focus/navigation route for main-process initiated navigation.
-- [ ] Ensure the route can focus/create the normal main window and navigate to `/plugins...`.
-- [ ] Do not add a Plugins BrowserWindow.
-- [ ] Do not add `src/renderer/plugins` or a separate renderer entry.
+- [x] Reuse existing deeplink event handling for main-process initiated MCP navigation.
+- [x] Ensure MCP install deeplink can focus/create the normal main window and navigate to `/plugins/mcp`.
+- [x] Do not add a Plugins BrowserWindow.
+- [x] Do not add `src/renderer/plugins` or a separate renderer entry.
- [ ] Add tests for focusing main and navigating to `/plugins/mcp`.
## 3. Settings Navigation Cleanup
-- [ ] Hide or remove visible Settings navigation items for MCP, Remote, Plugins, and Skills.
-- [ ] Keep compatibility routes or redirect handlers for old route names.
-- [ ] Map old route names to main `/plugins...` routes.
-- [ ] Remove MCP from Settings Overview primary metric.
-- [ ] Remove or replace Settings Overview `start-mcp` quick task.
-- [ ] Ensure Settings Overview search does not return hidden Plugins-owned pages.
+- [x] Hide or remove visible Settings navigation items for MCP, Remote, Plugins, and Skills.
+- [x] Keep compatibility routes or redirect handlers for old route names.
+- [ ] Map every old route name to main `/plugins...` routes.
+- [x] Remove MCP from Settings Overview primary metric.
+- [x] Remove or replace Settings Overview `start-mcp` quick task.
+- [x] Ensure Settings Overview search does not return hidden Plugins-owned pages.
- [ ] Update Settings activity click behavior for historical routes.
- [ ] Add tests for Settings navigation groups and hidden route handling.
## 4. MCP Section
-- [ ] Create `/plugins/mcp` page using current MCP store/client behavior.
-- [ ] Reuse `McpServers` for list/add/edit/toggle.
-- [ ] Reuse MCP market view inside `/plugins/mcp?view=market`.
-- [ ] Reuse NPM registry controls.
-- [ ] Move MCP install deeplink target from Settings to main `/plugins/mcp`.
-- [ ] Move MCP install event handling into the main app or Plugins route bootstrap.
-- [ ] Keep plugin-owned MCP server read-only behavior.
+- [x] Create `/plugins/mcp` page using current MCP store/client behavior.
+- [x] Reuse `McpSettings`/current MCP components for list/add/edit/toggle.
+- [x] Reuse MCP market view inside `/plugins/mcp?view=market`.
+- [x] Reuse NPM registry controls.
+- [x] Move MCP install deeplink target from Settings to main `/plugins/mcp`.
+- [x] Move MCP install event handling into the main app or Plugins route bootstrap.
+- [x] Keep plugin-owned MCP server read-only behavior.
- [ ] Add tests for deeplink route target and MCP page render.
## 5. Skills Section
-- [ ] Create `/plugins/skills` page from current Skills settings behavior.
-- [ ] Reuse skill list, search, install, edit, delete, sync import/export.
-- [ ] Preserve draft suggestion toggle.
-- [ ] Preserve first-launch sync prompt if still required.
-- [ ] Ensure skill dialogs/sheets fit the main Plugins page shell.
+- [x] Create `/plugins/skills` page from current Skills settings behavior.
+- [x] Reuse skill list, search, install, edit, delete, sync import/export.
+- [x] Preserve draft suggestion toggle.
+- [x] Preserve first-launch sync prompt if still required.
+- [x] Ensure skill dialogs/sheets fit the main Plugins page shell.
- [ ] Add renderer tests for empty/list/search/install entry behavior.
## 6. Official Plugins Section
-- [ ] Create official plugin list route from `PluginClient.listPlugins`.
-- [ ] Add detail route `/plugins/official/:pluginId`.
-- [ ] Keep enable/disable actions.
-- [ ] Show runtime status, plugin-owned MCP status and last errors.
+- [x] Create official plugin list route from `PluginClient.listPlugins`.
+- [x] Add detail route `/plugins/official/:pluginId`.
+- [x] Keep enable/disable actions.
+- [x] Show runtime status, plugin-owned MCP status and last errors.
- [ ] Add native CUA detail sections for runtime status, permissions and permission guide actions.
- [ ] Add native Feishu/Lark Integration detail that distinguishes it from Feishu/Lark Remote.
-- [ ] Stop first-party Plugins UI from calling `settings.open`.
-- [ ] Keep `settings.open` only as temporary compatibility fallback.
+- [x] Stop first-party Plugins UI from calling `settings.open`.
+- [x] Keep `settings.open` only as temporary compatibility fallback.
- [ ] Add tests for list/detail action behavior.
## 7. Remote Virtual Plugins
-- [ ] Build remote virtual cards from `remoteControl.listChannels`.
-- [ ] Fetch and display per-channel status.
-- [ ] Add `/plugins/remote` list route.
-- [ ] Add `/plugins/remote/:channel` detail route.
+- [x] Build remote virtual cards from `remoteControl.listChannels`.
+- [x] Fetch and display per-channel status.
+- [x] Add `/plugins/remote` route.
+- [x] Add `/plugins/remote/:channel` detail route.
- [ ] Extract only the needed RemoteSettings channel sections into reusable components.
-- [ ] Preserve credentials fields and password reveal behavior.
-- [ ] Preserve enable/disable save behavior.
-- [ ] Preserve default agent and default workdir behavior.
-- [ ] Preserve pairing flow for Telegram, Feishu/Lark, QQBot and Discord.
-- [ ] Preserve binding/principal removal behavior.
-- [ ] Preserve WeChat iLink login/account controls.
-- [ ] Route sidebar remote status button to `/plugins/remote` or selected channel detail.
+- [x] Preserve credentials fields and password reveal behavior.
+- [x] Preserve enable/disable save behavior.
+- [x] Preserve default agent and default workdir behavior.
+- [x] Preserve pairing flow for Telegram, Feishu/Lark, QQBot and Discord.
+- [x] Preserve binding/principal removal behavior.
+- [x] Preserve WeChat iLink login/account controls.
+- [x] Route sidebar remote status button to `/plugins/remote` or selected channel detail.
- [ ] Add tests for card mapping, save, pairing and bindings.
## 8. Main Sidebar Layout
-- [ ] Replace expanded sidebar header/search area with command list.
-- [ ] Keep `所有 Agents` title.
-- [ ] Wire `New Chat` row to navigate to `/chat` and start a new conversation.
-- [ ] Wire `Search` row to existing Spotlight behavior.
-- [ ] Wire `Plugins` row to `router.push({ name: 'plugins' })`.
-- [ ] Add a blank spacer after the `Plugins` command row.
-- [ ] Render `Pinned` only when pinned sessions exist.
-- [ ] Keep the `Chat` group after `Pinned`.
-- [ ] Add `工作区` header before project groups.
-- [ ] Move the existing group-mode/sort toggle to the `工作区` header.
-- [ ] Keep Settings/theme/sidebar controls in the existing left rail, not in the expanded right column.
-- [ ] Display shortcut badges only for existing shortcuts.
-- [ ] Keep collapsed sidebar visual behavior unchanged.
-- [ ] Preserve session list pagination, pinned section, project grouping and reorder.
+- [x] Replace expanded sidebar header/search area with command list.
+- [x] Keep `所有 Agents` title.
+- [x] Wire `New Chat` row to navigate to `/chat` and start a new conversation.
+- [x] Wire `Search` row to existing Spotlight behavior.
+- [x] Wire `Plugins` row to `router.push({ name: 'plugins' })`.
+- [x] Add a blank spacer after the `Plugins` command row.
+- [x] Render `Pinned` only when pinned sessions exist.
+- [x] Keep the `Chat` group after `Pinned`.
+- [x] Add `工作区` header before project groups.
+- [x] Move the existing group-mode/sort toggle to the `工作区` header.
+- [x] Keep Settings/theme/sidebar controls in the existing left rail, not in the expanded right column.
+- [x] Display shortcut badges only for existing shortcuts.
+- [x] Keep collapsed sidebar visual behavior unchanged.
+- [x] Preserve session list pagination, pinned section, project grouping and reorder.
- [ ] Add renderer tests for expanded rows and collapsed state.
- [ ] Capture before/after ASCII blocks in PR description.
@@ -116,10 +116,10 @@
## 10. Final Quality Gates
-- [ ] Run `pnpm run format`.
-- [ ] Run `pnpm run i18n`.
-- [ ] Run `pnpm run lint`.
-- [ ] Run `pnpm run typecheck`.
+- [x] Run `pnpm run format`.
+- [x] Run `pnpm run i18n`.
+- [x] Run `pnpm run lint`.
+- [x] Run `pnpm run typecheck`.
- [ ] Run targeted renderer tests for Plugins route and sidebar.
- [ ] Run targeted main tests for navigation/deeplink behavior.
- [ ] Update durable docs or remove/archive active plan/tasks after implementation lands.
diff --git a/src/main/presenter/deeplinkPresenter/index.ts b/src/main/presenter/deeplinkPresenter/index.ts
index a0a05d6746..c63624f9a9 100644
--- a/src/main/presenter/deeplinkPresenter/index.ts
+++ b/src/main/presenter/deeplinkPresenter/index.ts
@@ -384,13 +384,14 @@ export class DeeplinkPresenter implements IDeeplinkPresenter {
return
}
- const settingsWindowId = await presenter.windowPresenter.createSettingsWindow()
- if (!settingsWindowId) {
- console.error('Failed to open Settings window for MCP install deeplink')
+ const targetWindow = await this.resolveChatWindow()
+ if (!targetWindow) {
+ console.error('Failed to resolve main window for MCP install deeplink')
return
}
- presenter.windowPresenter.sendToWindow(settingsWindowId, DEEPLINK_EVENTS.MCP_INSTALL, {
+ await this.ensureChatWindowReady(targetWindow.id)
+ presenter.windowPresenter.sendToWindow(targetWindow.id, DEEPLINK_EVENTS.MCP_INSTALL, {
mcpConfig: JSON.stringify(completeMcpConfig)
})
diff --git a/src/renderer/settings/components/McpSettings.vue b/src/renderer/settings/components/McpSettings.vue
index b0f925ac66..39bad89f9f 100644
--- a/src/renderer/settings/components/McpSettings.vue
+++ b/src/renderer/settings/components/McpSettings.vue
@@ -497,15 +497,23 @@ const closeMarketView = async () => {
const nextQuery = { ...route.query }
delete nextQuery.view
+ const routeName =
+ typeof router.hasRoute === 'function' && router.hasRoute('plugins-mcp')
+ ? 'plugins-mcp'
+ : 'settings-mcp'
await router.replace({
- name: 'settings-mcp',
+ name: routeName,
query: nextQuery
})
}
const openMarketView = async () => {
+ const routeName =
+ typeof router.hasRoute === 'function' && router.hasRoute('plugins-mcp')
+ ? 'plugins-mcp'
+ : 'settings-mcp'
await router.push({
- name: 'settings-mcp',
+ name: routeName,
query: {
...route.query,
view: 'market'
diff --git a/src/renderer/settings/components/RemoteSettings.vue b/src/renderer/settings/components/RemoteSettings.vue
index 3579dc5acc..b426fd2175 100644
--- a/src/renderer/settings/components/RemoteSettings.vue
+++ b/src/renderer/settings/components/RemoteSettings.vue
@@ -1515,8 +1515,9 @@
diff --git a/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue b/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue
new file mode 100644
index 0000000000..5a9dfbc01c
--- /dev/null
+++ b/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+
+
+ {{ t('settings.pluginsHub.pluginNotFound') }}
+
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
{{ t('settings.plugins.runtime') }}
+
+ - {{ t('settings.plugins.runtime') }}
+ - {{ formatRuntimeState(plugin.runtime?.state) }}
+ - {{ t('settings.plugins.version') }}
+ - {{ plugin.runtime?.version || '-' }}
+ - {{ t('settings.plugins.command') }}
+ - {{ plugin.runtime?.command || '-' }}
+
+
+ {{ plugin.runtime.lastError }}
+
+
+
+
+
+ {{ t('settings.pluginsHub.capabilities') }}
+
+
+
+ {{ capability }}
+
+
+
+
+
+
+ {{ t('routes.settings-mcp') }}
+
+
+
+
{{ server.serverId }}
+
+ {{ server.lastError }}
+
+
+
+ {{
+ server.running
+ ? t('settings.plugins.runtimeStates.running')
+ : t('common.disabled')
+ }}
+
+
+
+
+
+
+ {{ t('settings.pluginsHub.actionResult') }}
+ {{
+ lastActionData
+ }}
+
+
+
+
+
+
+
diff --git a/src/renderer/src/pages/plugins/PluginsCatalogPage.vue b/src/renderer/src/pages/plugins/PluginsCatalogPage.vue
new file mode 100644
index 0000000000..f15edaa4a4
--- /dev/null
+++ b/src/renderer/src/pages/plugins/PluginsCatalogPage.vue
@@ -0,0 +1,437 @@
+
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
{{ t('settings.pluginsHub.added') }}
+
+ {{ t('settings.pluginsHub.manage') }}
+
+
+
+
+
+
+
+ {{ t('settings.pluginsHub.noAdded') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ item.title }}
+
+ {{ item.badge }}
+
+
+
+ {{ item.description }}
+
+
+
+
+
+
+
+ {{ t('settings.pluginsHub.emptySearch') }}
+
+
+
+
+
+
+
diff --git a/src/renderer/src/pages/plugins/PluginsHubPage.vue b/src/renderer/src/pages/plugins/PluginsHubPage.vue
new file mode 100644
index 0000000000..204f83d2be
--- /dev/null
+++ b/src/renderer/src/pages/plugins/PluginsHubPage.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/src/pages/plugins/RemotePluginsPage.vue b/src/renderer/src/pages/plugins/RemotePluginsPage.vue
new file mode 100644
index 0000000000..e756b81223
--- /dev/null
+++ b/src/renderer/src/pages/plugins/RemotePluginsPage.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/renderer/src/pages/plugins/SkillsPluginsPage.vue b/src/renderer/src/pages/plugins/SkillsPluginsPage.vue
new file mode 100644
index 0000000000..7b871bfdc6
--- /dev/null
+++ b/src/renderer/src/pages/plugins/SkillsPluginsPage.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/renderer/src/router/index.ts b/src/renderer/src/router/index.ts
index f805bcc8c0..338ecdf71b 100644
--- a/src/renderer/src/router/index.ts
+++ b/src/renderer/src/router/index.ts
@@ -16,6 +16,70 @@ const router = createRouter({
icon: 'lucide:message-square'
}
},
+ {
+ path: '/plugins',
+ component: () => import('@/pages/plugins/PluginsHubPage.vue'),
+ meta: {
+ titleKey: 'routes.plugins',
+ icon: 'lucide:puzzle'
+ },
+ children: [
+ {
+ path: '',
+ name: 'plugins',
+ component: () => import('@/pages/plugins/PluginsCatalogPage.vue'),
+ meta: {
+ titleKey: 'routes.plugins',
+ icon: 'lucide:puzzle'
+ }
+ },
+ {
+ path: 'skills',
+ name: 'plugins-skills',
+ component: () => import('@/pages/plugins/SkillsPluginsPage.vue'),
+ meta: {
+ titleKey: 'routes.settings-skills',
+ icon: 'lucide:wand-sparkles'
+ }
+ },
+ {
+ path: 'mcp',
+ name: 'plugins-mcp',
+ component: () => import('@/pages/plugins/McpPluginsPage.vue'),
+ meta: {
+ titleKey: 'routes.settings-mcp',
+ icon: 'lucide:server'
+ }
+ },
+ {
+ path: 'remote',
+ name: 'plugins-remote',
+ component: () => import('@/pages/plugins/RemotePluginsPage.vue'),
+ meta: {
+ titleKey: 'routes.settings-remote',
+ icon: 'lucide:smartphone'
+ }
+ },
+ {
+ path: 'remote/:channel',
+ name: 'plugins-remote-detail',
+ component: () => import('@/pages/plugins/RemotePluginsPage.vue'),
+ meta: {
+ titleKey: 'routes.settings-remote',
+ icon: 'lucide:smartphone'
+ }
+ },
+ {
+ path: 'official/:pluginId',
+ name: 'plugins-official-detail',
+ component: () => import('@/pages/plugins/OfficialPluginDetailPage.vue'),
+ meta: {
+ titleKey: 'routes.plugins',
+ icon: 'lucide:puzzle'
+ }
+ }
+ ]
+ },
{
path: '/welcome',
name: 'welcome',
diff --git a/src/renderer/src/stores/ui/spotlight.ts b/src/renderer/src/stores/ui/spotlight.ts
index 90da7cd88a..21417565bc 100644
--- a/src/renderer/src/stores/ui/spotlight.ts
+++ b/src/renderer/src/stores/ui/spotlight.ts
@@ -1,5 +1,6 @@
import { computed, ref, watch } from 'vue'
import { defineStore } from 'pinia'
+import { useRouter } from 'vue-router'
import { useDebounceFn } from '@vueuse/core'
import { createSettingsClient } from '@api/SettingsClient'
import { createSessionClient } from '@api/SessionClient'
@@ -106,7 +107,6 @@ const actionItems: Array<{
{
id: 'open-mcp',
titleKey: 'routes.settings-mcp',
- routeName: 'settings-mcp',
icon: 'lucide:server',
keywords: ['mcp', 'tools', 'server', '工具']
},
@@ -120,13 +120,13 @@ const actionItems: Array<{
{
id: 'open-remote',
titleKey: 'routes.settings-remote',
- routeName: 'settings-remote',
icon: 'lucide:smartphone',
keywords: ['remote', 'telegram', 'feishu', '远程']
}
]
export const useSpotlightStore = defineStore('spotlight', () => {
+ const router = useRouter()
const sessionClient = createSessionClient()
const settingsClient = createSettingsClient()
const providerStore = useProviderStore()
@@ -251,7 +251,9 @@ export const useSpotlightStore = defineStore('spotlight', () => {
.filter((item) => item.score > 0)
const buildSettingMatches = (normalizedQuery: string): SpotlightItem[] =>
- SETTINGS_NAVIGATION_ITEMS.filter((item) => item.routeName !== 'settings-provider')
+ SETTINGS_NAVIGATION_ITEMS.filter(
+ (item) => item.routeName !== 'settings-provider' && !item.hiddenInSidebar
+ )
.map((item) => ({
id: `setting:${item.routeName}`,
kind: 'setting' as const,
@@ -459,11 +461,15 @@ export const useSpotlightStore = defineStore('spotlight', () => {
return
case 'open-providers':
case 'open-agents':
- case 'open-mcp':
case 'open-shortcuts':
- case 'open-remote':
await navigateToSettings(item.routeName)
return
+ case 'open-mcp':
+ await router.push({ name: 'plugins-mcp' })
+ return
+ case 'open-remote':
+ await router.push({ name: 'plugins-remote' })
+ return
default:
return
}
diff --git a/src/shared/settingsNavigation.ts b/src/shared/settingsNavigation.ts
index 066b1e9528..ba0f55743d 100644
--- a/src/shared/settingsNavigation.ts
+++ b/src/shared/settingsNavigation.ts
@@ -166,7 +166,8 @@ export const SETTINGS_NAVIGATION_ITEMS: SettingsNavigationItem[] = [
icon: 'lucide:server',
position: 5,
groupKey: 'tools',
- keywords: ['mcp', 'tools', 'server', 'model context protocol', '工具', '服务']
+ keywords: ['mcp', 'tools', 'server', 'model context protocol', '工具', '服务'],
+ hiddenInSidebar: true
},
{
routeName: 'settings-remote',
@@ -175,7 +176,8 @@ export const SETTINGS_NAVIGATION_ITEMS: SettingsNavigationItem[] = [
icon: 'lucide:smartphone',
position: 5.25,
groupKey: 'system',
- keywords: ['remote', 'telegram', 'feishu', 'control', '远程', '控制']
+ keywords: ['remote', 'telegram', 'feishu', 'control', '远程', '控制'],
+ hiddenInSidebar: true
},
{
routeName: 'settings-notifications-hooks',
@@ -213,7 +215,8 @@ export const SETTINGS_NAVIGATION_ITEMS: SettingsNavigationItem[] = [
position: 5.75,
groupKey: 'tools',
keywords: ['plugin', 'plugins', 'extension', 'runtime', '插件', '扩展', '运行时'],
- supportedTargets: ['darwin/arm64', 'darwin/x64', 'win32/x64', 'win32/arm64', 'linux/x64']
+ supportedTargets: ['darwin/arm64', 'darwin/x64', 'win32/x64', 'win32/arm64', 'linux/x64'],
+ hiddenInSidebar: true
},
{
routeName: 'settings-skills',
@@ -222,7 +225,8 @@ export const SETTINGS_NAVIGATION_ITEMS: SettingsNavigationItem[] = [
icon: 'lucide:wand-sparkles',
position: 6,
groupKey: 'knowledge',
- keywords: ['skill', 'skills', '技能']
+ keywords: ['skill', 'skills', '技能'],
+ hiddenInSidebar: true
},
{
routeName: 'settings-prompt',
diff --git a/test/renderer/components/WindowSideBar.test.ts b/test/renderer/components/WindowSideBar.test.ts
index be097b288a..c35a50e004 100644
--- a/test/renderer/components/WindowSideBar.test.ts
+++ b/test/renderer/components/WindowSideBar.test.ts
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
-import { defineComponent, reactive } from 'vue'
+import { defineComponent, reactive, ref } from 'vue'
import { flushPromises, mount } from '@vue/test-utils'
type SetupOptions = {
@@ -30,6 +30,7 @@ type SetupOptions = {
projectEnvironments?: Array<{ path: string }>
archivedProjectEnvironments?: Array<{ path: string }>
defaultChatWorkspacePath?: string | null
+ currentRouteName?: string
}
const TEST_TIMEOUT_MS = 20000
@@ -225,6 +226,28 @@ const setup = async (options: SetupOptions = {}) => {
spotlightStore.open = !spotlightStore.open
})
})
+ const router = {
+ currentRoute: ref({
+ name: options.currentRouteName ?? 'chat',
+ query: {},
+ params: {}
+ }),
+ hasRoute: vi.fn((name: string) => ['chat', 'plugins', 'plugins-remote'].includes(String(name))),
+ push: vi.fn(async (location: { name?: string }) => {
+ router.currentRoute.value = {
+ name: location.name ?? router.currentRoute.value.name,
+ query: {},
+ params: {}
+ }
+ }),
+ replace: vi.fn(async (location: { name?: string }) => {
+ router.currentRoute.value = {
+ name: location.name ?? router.currentRoute.value.name,
+ query: {},
+ params: {}
+ }
+ })
+ }
const settingsClient = {
openSettings: vi.fn().mockResolvedValue({ windowId: 99 })
}
@@ -343,6 +366,9 @@ const setup = async (options: SetupOptions = {}) => {
t: (key: string) => key
})
}))
+ vi.doMock('vue-router', () => ({
+ useRouter: () => router
+ }))
const passthrough = defineComponent({
template: '
'
@@ -461,6 +487,7 @@ const setup = async (options: SetupOptions = {}) => {
deviceClient,
remoteControlClient,
spotlightStore,
+ router,
pageRouterStore,
sidebarStore,
projectStore
@@ -512,11 +539,14 @@ describe('WindowSideBar agent switch', () => {
TEST_TIMEOUT_MS
)
- it('delegates sidebar new chat clicks to the unified session action', async () => {
- const { wrapper, sessionStore } = await setup()
+ it('routes to chat before delegating sidebar new chat clicks to the unified session action', async () => {
+ const { wrapper, sessionStore, router } = await setup({
+ currentRouteName: 'plugins'
+ })
await (wrapper.vm as any).handleNewChat()
+ expect(router.push).toHaveBeenCalledWith({ name: 'chat' })
expect(sessionStore.startNewConversation).toHaveBeenCalledWith({ refresh: true })
})
@@ -642,37 +672,14 @@ describe('WindowSideBar agent switch', () => {
)
it(
- 'filters pinned and grouped sessions by the sidebar search input',
+ 'toggles spotlight from the expanded sidebar search command',
async () => {
- const { wrapper } = await setup({
- pinnedSessions: [
- {
- id: 'pinned-1',
- title: 'Alpha Session',
- status: 'none'
- }
- ],
- groups: [
- {
- id: 'common.time.today',
- label: 'common.time.today',
- labelKey: 'common.time.today',
- sessions: [
- {
- id: 'group-1',
- title: 'Beta Session',
- status: 'none'
- }
- ]
- }
- ]
- })
+ const { wrapper, spotlightStore } = await setup()
- await wrapper.find('input').setValue('alpha')
+ await wrapper.get('[data-testid="app-search-command-button"]').trigger('click')
await flushPromises()
- expect(wrapper.text()).toContain('Alpha Session')
- expect(wrapper.text()).not.toContain('Beta Session')
+ expect(spotlightStore.toggleSpotlight).toHaveBeenCalledTimes(1)
},
TEST_TIMEOUT_MS
)
@@ -922,9 +929,8 @@ describe('WindowSideBar agent switch', () => {
cancelable: true
})
- Object.defineProperty(event, 'target', {
- value: wrapper.find('input').element
- })
+ const input = document.createElement('input')
+ Object.defineProperty(event, 'target', { value: input })
;(wrapper.vm as any).handleWindowShortcutKeydown(event)
await flushPromises()
@@ -1081,16 +1087,15 @@ describe('WindowSideBar agent switch', () => {
)
it(
- 'keeps the sidebar search region interactive outside the drag area',
+ 'keeps the expanded sidebar command region interactive outside the drag area',
async () => {
const { wrapper } = await setup()
expect(wrapper.get('[data-testid="window-sidebar-session-column"]').classes()).toContain(
'window-no-drag-region'
)
- expect(wrapper.get('[data-testid="window-sidebar-search"]').classes()).toContain(
- 'window-no-drag-region'
- )
+ expect(wrapper.get('[data-testid="app-search-command-button"]').exists()).toBe(true)
+ expect(wrapper.get('[data-testid="app-plugins-button"]').exists()).toBe(true)
},
TEST_TIMEOUT_MS
)
@@ -1387,24 +1392,16 @@ describe('WindowSideBar agent switch', () => {
await wrapper.vm.$nextTick()
- expect(wrapper.text()).toContain('chat.sidebar.chats')
+ expect(wrapper.text()).toContain('chat.sidebar.chatSection')
+ expect(wrapper.text()).toContain('chat.sidebar.workspace')
expect(
wrapper.findAll('button[data-group-id]').map((button) => button.attributes('data-group-id'))
- ).toEqual(['__pinned__', '/Users/test/Documents/DeepChat', '/work/alpha', '/work/beta'])
- expect(
- wrapper.get('[data-group-id="/Users/test/Documents/DeepChat"]').attributes('aria-expanded')
- ).toBe('false')
- expect(
- wrapper
- .get('[data-group-id="/Users/test/Documents/DeepChat"]')
- .find('[data-testid="window-sidebar-group-icon"]')
- .attributes('data-icon')
- ).toBe('lucide:message-square')
+ ).toEqual(['__pinned__', '/work/alpha', '/work/beta'])
expect(wrapper.findAll('[aria-label="chat.sidebar.projectGroupActions"]')).toHaveLength(2)
wrapper
.getComponent({ name: 'draggable' })
- .vm.$emit('update:modelValue', [chatGroup, betaGroup, alphaGroup])
+ .vm.$emit('update:modelValue', [betaGroup, alphaGroup])
await flushPromises()
expect(projectStore.reorderEnvironments).toHaveBeenCalledWith([
@@ -1461,22 +1458,15 @@ describe('WindowSideBar agent switch', () => {
await wrapper.vm.$nextTick()
- expect(wrapper.text()).toContain('chat.sidebar.chats')
+ expect(wrapper.text()).toContain('chat.sidebar.chatSection')
+ expect(wrapper.text()).toContain('chat.sidebar.workspace')
expect(wrapper.text()).not.toContain('common.project.none')
- expect(wrapper.get('[data-group-id="__no_project__"]').attributes('aria-expanded')).toBe(
- 'false'
- )
- expect(
- wrapper
- .get('[data-group-id="__no_project__"]')
- .find('[data-testid="window-sidebar-group-icon"]')
- .attributes('data-icon')
- ).toBe('lucide:message-square')
+ expect(wrapper.find('[data-group-id="__no_project__"]').exists()).toBe(false)
expect(wrapper.findAll('[aria-label="chat.sidebar.projectGroupActions"]')).toHaveLength(2)
wrapper
.getComponent({ name: 'draggable' })
- .vm.$emit('update:modelValue', [noProjectGroup, betaGroup, alphaGroup])
+ .vm.$emit('update:modelValue', [betaGroup, alphaGroup])
await flushPromises()
expect(projectStore.reorderEnvironments).toHaveBeenCalledWith(['/work/beta', '/work/alpha'])
@@ -1500,7 +1490,7 @@ describe('WindowSideBar agent switch', () => {
await wrapper.vm.$nextTick()
expect(wrapper.find('[data-group-id="__no_project__"]').exists()).toBe(false)
- expect(wrapper.text()).not.toContain('chat.sidebar.chats')
+ expect(wrapper.text()).not.toContain('chat.sidebar.chatSection')
})
it(
@@ -1534,7 +1524,7 @@ describe('WindowSideBar agent switch', () => {
groups: [alphaGroup, betaGroup]
})
- await wrapper.find('input').setValue('shared')
+ ;(wrapper.vm as any).sessionSearchQuery = 'shared'
await flushPromises()
const draggable = wrapper.getComponent({ name: 'draggable' })
@@ -1660,8 +1650,8 @@ describe('WindowSideBar agent switch', () => {
disabledSetup.wrapper.unmount()
})
- it('opens settings and navigates to remote settings when remote button is clicked', async () => {
- const { wrapper, settingsClient } = await setup({
+ it('routes to Plugins Remote when remote button is clicked', async () => {
+ const { wrapper, settingsClient, router } = await setup({
remoteStatus: {
enabled: true,
state: 'running'
@@ -1670,10 +1660,8 @@ describe('WindowSideBar agent switch', () => {
await wrapper.find('[data-testid=\"remote-control-button\"]').trigger('click')
await flushPromises()
- expect(settingsClient.openSettings).toHaveBeenCalledTimes(1)
- expect(settingsClient.openSettings).toHaveBeenCalledWith({
- routeName: 'settings-remote'
- })
+ expect(router.push).toHaveBeenCalledWith({ name: 'plugins-remote' })
+ expect(settingsClient.openSettings).not.toHaveBeenCalled()
wrapper.unmount()
})
@@ -1764,38 +1752,4 @@ describe('WindowSideBar viewport auto-fill', () => {
},
TEST_TIMEOUT_MS
)
-
- it(
- 'rechecks pagination when local search filters visible sessions below the viewport',
- async () => {
- const { wrapper, sessionStore } = await setup({
- hasMore: true,
- sessions: [{ id: 'session-1' }, { id: 'session-2' }],
- groups: [
- {
- id: 'common.time.today',
- label: 'common.time.today',
- labelKey: 'common.time.today',
- sessions: [
- { id: 'session-1', title: 'Alpha', status: 'none' },
- { id: 'session-2', title: 'Bravo', status: 'none' }
- ]
- }
- ],
- nextPages: [{ items: [{ id: 'session-3' }], hasMore: false }]
- })
- setSidebarListSize(wrapper, { scrollHeight: 240, clientHeight: 120 })
- await flushSidebarFillFrame()
- expect(sessionStore.loadNextPage).not.toHaveBeenCalled()
-
- await wrapper.get('[data-testid="window-sidebar-search"] input').setValue('missing older')
- setSidebarListSize(wrapper, { scrollHeight: 60, clientHeight: 120 })
- await flushSidebarFillFrame()
-
- expect(sessionStore.loadNextPage).toHaveBeenCalledTimes(1)
-
- wrapper.unmount()
- },
- TEST_TIMEOUT_MS
- )
})
From 811b608bfd9560452094afaf3c1daed618988d72 Mon Sep 17 00:00:00 2001
From: zerob13
Date: Thu, 25 Jun 2026 17:01:14 +0800
Subject: [PATCH 04/14] fix(plugins): remove mcp and skills catalog cards
---
docs/features/plugins-hub/plan.md | 7 ++--
docs/features/plugins-hub/spec.md | 6 ++--
docs/features/plugins-hub/tasks.md | 1 +
src/renderer/src/i18n/da-DK/settings.json | 4 +--
src/renderer/src/i18n/de-DE/settings.json | 4 +--
src/renderer/src/i18n/en-US/settings.json | 4 +--
src/renderer/src/i18n/es-ES/settings.json | 4 +--
src/renderer/src/i18n/fa-IR/settings.json | 4 +--
src/renderer/src/i18n/fr-FR/settings.json | 4 +--
src/renderer/src/i18n/he-IL/settings.json | 4 +--
src/renderer/src/i18n/id-ID/settings.json | 4 +--
src/renderer/src/i18n/it-IT/settings.json | 4 +--
src/renderer/src/i18n/ja-JP/settings.json | 4 +--
src/renderer/src/i18n/ko-KR/settings.json | 4 +--
src/renderer/src/i18n/ms-MY/settings.json | 4 +--
src/renderer/src/i18n/pl-PL/settings.json | 4 +--
src/renderer/src/i18n/pt-BR/settings.json | 4 +--
src/renderer/src/i18n/ru-RU/settings.json | 4 +--
src/renderer/src/i18n/tr-TR/settings.json | 4 +--
src/renderer/src/i18n/vi-VN/settings.json | 4 +--
src/renderer/src/i18n/zh-CN/settings.json | 4 +--
src/renderer/src/i18n/zh-HK/settings.json | 4 +--
src/renderer/src/i18n/zh-TW/settings.json | 4 +--
.../src/pages/plugins/PluginsCatalogPage.vue | 32 ++-----------------
24 files changed, 29 insertions(+), 97 deletions(-)
diff --git a/docs/features/plugins-hub/plan.md b/docs/features/plugins-hub/plan.md
index 965d3d5bb9..6fddc356b2 100644
--- a/docs/features/plugins-hub/plan.md
+++ b/docs/features/plugins-hub/plan.md
@@ -79,12 +79,10 @@ Use a renderer-only union for cards:
```text
CatalogItem =
official plugin item from plugins.list
- MCP server summary from mcp store
- Skill metadata from skills store
Remote virtual item from remoteControl.listChannels + status
```
-This union only drives list rendering and search filtering. Writes go back to the current owner:
+`MCP` and `Skills` are top-level sibling tabs under `/plugins`, not catalog cards. This union only drives plugin catalog rendering and search filtering. Writes go back to the current owner:
| User action | Owner route/client |
| --- | --- |
@@ -123,8 +121,9 @@ Keep this list flexible during implementation; do not split files unless the com
Visual baseline:
-- Main content starts with top tabs (`Plugins`, `Skills`, optionally `MCP`, `Remote`).
+- Main content starts with top tabs (`Plugins`, `Skills`, `MCP`, `Remote`).
- Catalog page uses the Codex-like layout: title, subtitle, search, added strip, segmented filters, sectioned list.
+- Catalog cards include official plugins and Remote virtual plugins only; MCP and Skills remain reachable through top tabs.
- Avoid settings-style full-width form pages for the catalog. Detail routes may use denser settings sections.
- Cards are individual repeated items only. Do not put page sections inside floating cards.
diff --git a/docs/features/plugins-hub/spec.md b/docs/features/plugins-hub/spec.md
index b5c8bbbc93..7ddcc30967 100644
--- a/docs/features/plugins-hub/spec.md
+++ b/docs/features/plugins-hub/spec.md
@@ -111,12 +111,12 @@ Top-level sections:
| Section | User label | Contents |
| --- | --- | --- |
-| Plugins | Plugins | official plugin packages, added/recommended plugin cards, Remote virtual plugin cards when filtered into plugin catalog |
+| Plugins | Plugins | official plugin packages, added/recommended plugin cards, Remote virtual plugin cards |
| Skills | Skills | installed skills, install, edit, sync import/export, draft suggestion toggle |
| MCP | MCP Servers | user MCP servers, plugin-owned MCP status, MCP market/add flow |
| Remote | Remote | virtual plugin cards for Telegram, Feishu/Lark Remote, QQBot, Discord, WeChat iLink |
-The visual top tab row can start with `Plugins` and `Skills` as in the Codex screenshot, then add `MCP` and `Remote` if all four areas ship in the same increment. If product wants the screenshot to stay visually lighter, `MCP` and `Remote` can appear as catalog filters/cards inside `Plugins`, but they still need addressable routes.
+The visual top tab row uses `Plugins`, `Skills`, `MCP` and `Remote`. `MCP` and `Skills` are sibling tabs, not plugin catalog cards.
Remote naming must avoid collision with official plugins:
@@ -139,7 +139,7 @@ Remote naming must avoid collision with official plugins:
│ │ New Chat │ Plugins │
│ │ Search │ Work with DeepChat across your favorite tools │
│ │ Plugins │ ┌────────────────────────────────────────────┐ │
-│ │ │ │ Search plugins, skills, MCP servers... │ │
+│ │ │ │ Search plugins and remote channels... │ │
│ │ Pinned │ └────────────────────────────────────────────┘ │
│ │ ... │ │
│ │ Chat │ Added Manage │
diff --git a/docs/features/plugins-hub/tasks.md b/docs/features/plugins-hub/tasks.md
index 2b68d2a790..5a657d751c 100644
--- a/docs/features/plugins-hub/tasks.md
+++ b/docs/features/plugins-hub/tasks.md
@@ -13,6 +13,7 @@
- [x] Add `PluginsHubPage.vue` inside `src/renderer/src/pages/plugins/`.
- [x] Add top tab navigation for Plugins, Skills, MCP and Remote or the chosen first-increment subset.
- [x] Add Codex-like catalog placeholder with title, subtitle, search, added strip and featured sections.
+- [x] Keep MCP and Skills as top tabs only, not plugin catalog cards.
- [x] Keep `WindowSideBar`, `AppBar`, global overlays, theme and i18n behavior intact.
- [x] Add i18n keys for route, page title, subtitle, tabs and search placeholder.
- [ ] Add renderer tests proving `/plugins` renders inside the existing app shell.
diff --git a/src/renderer/src/i18n/da-DK/settings.json b/src/renderer/src/i18n/da-DK/settings.json
index 8ce654568d..f9940e82c5 100644
--- a/src/renderer/src/i18n/da-DK/settings.json
+++ b/src/renderer/src/i18n/da-DK/settings.json
@@ -2487,7 +2487,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2495,8 +2495,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/de-DE/settings.json b/src/renderer/src/i18n/de-DE/settings.json
index 8a6f8fb22e..f0cecf3bf8 100644
--- a/src/renderer/src/i18n/de-DE/settings.json
+++ b/src/renderer/src/i18n/de-DE/settings.json
@@ -2478,7 +2478,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2486,8 +2486,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json
index b3f07ca6f4..19dad928aa 100644
--- a/src/renderer/src/i18n/en-US/settings.json
+++ b/src/renderer/src/i18n/en-US/settings.json
@@ -2374,7 +2374,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2382,8 +2382,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/es-ES/settings.json b/src/renderer/src/i18n/es-ES/settings.json
index 6e9099d412..9f78d23ccf 100644
--- a/src/renderer/src/i18n/es-ES/settings.json
+++ b/src/renderer/src/i18n/es-ES/settings.json
@@ -2478,7 +2478,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2486,8 +2486,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/fa-IR/settings.json b/src/renderer/src/i18n/fa-IR/settings.json
index d4dccb26f3..9548bf5714 100644
--- a/src/renderer/src/i18n/fa-IR/settings.json
+++ b/src/renderer/src/i18n/fa-IR/settings.json
@@ -2487,7 +2487,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2495,8 +2495,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/fr-FR/settings.json b/src/renderer/src/i18n/fr-FR/settings.json
index f24a95b5ba..58980c417c 100644
--- a/src/renderer/src/i18n/fr-FR/settings.json
+++ b/src/renderer/src/i18n/fr-FR/settings.json
@@ -2487,7 +2487,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2495,8 +2495,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/he-IL/settings.json b/src/renderer/src/i18n/he-IL/settings.json
index 17e95087bf..9fcf7c71ba 100644
--- a/src/renderer/src/i18n/he-IL/settings.json
+++ b/src/renderer/src/i18n/he-IL/settings.json
@@ -2487,7 +2487,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2495,8 +2495,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/id-ID/settings.json b/src/renderer/src/i18n/id-ID/settings.json
index 69bf7c653a..6fa582d500 100644
--- a/src/renderer/src/i18n/id-ID/settings.json
+++ b/src/renderer/src/i18n/id-ID/settings.json
@@ -2478,7 +2478,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2486,8 +2486,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/it-IT/settings.json b/src/renderer/src/i18n/it-IT/settings.json
index d3ad316437..4e8e54f9fa 100644
--- a/src/renderer/src/i18n/it-IT/settings.json
+++ b/src/renderer/src/i18n/it-IT/settings.json
@@ -2478,7 +2478,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2486,8 +2486,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/ja-JP/settings.json b/src/renderer/src/i18n/ja-JP/settings.json
index 094e11ee1d..fde61ad49d 100644
--- a/src/renderer/src/i18n/ja-JP/settings.json
+++ b/src/renderer/src/i18n/ja-JP/settings.json
@@ -2487,7 +2487,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2495,8 +2495,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/ko-KR/settings.json b/src/renderer/src/i18n/ko-KR/settings.json
index 97e115da20..7f39b5339c 100644
--- a/src/renderer/src/i18n/ko-KR/settings.json
+++ b/src/renderer/src/i18n/ko-KR/settings.json
@@ -2487,7 +2487,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2495,8 +2495,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/ms-MY/settings.json b/src/renderer/src/i18n/ms-MY/settings.json
index afea526197..dee811fb79 100644
--- a/src/renderer/src/i18n/ms-MY/settings.json
+++ b/src/renderer/src/i18n/ms-MY/settings.json
@@ -2478,7 +2478,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2486,8 +2486,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/pl-PL/settings.json b/src/renderer/src/i18n/pl-PL/settings.json
index cc2d8991c5..6f357bbb56 100644
--- a/src/renderer/src/i18n/pl-PL/settings.json
+++ b/src/renderer/src/i18n/pl-PL/settings.json
@@ -2478,7 +2478,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2486,8 +2486,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/pt-BR/settings.json b/src/renderer/src/i18n/pt-BR/settings.json
index a55558c0b0..0e75e47be1 100644
--- a/src/renderer/src/i18n/pt-BR/settings.json
+++ b/src/renderer/src/i18n/pt-BR/settings.json
@@ -2487,7 +2487,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2495,8 +2495,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/ru-RU/settings.json b/src/renderer/src/i18n/ru-RU/settings.json
index 0b8fad9f82..909c9cec0a 100644
--- a/src/renderer/src/i18n/ru-RU/settings.json
+++ b/src/renderer/src/i18n/ru-RU/settings.json
@@ -2487,7 +2487,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2495,8 +2495,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/tr-TR/settings.json b/src/renderer/src/i18n/tr-TR/settings.json
index d4e72834bc..117f227df7 100644
--- a/src/renderer/src/i18n/tr-TR/settings.json
+++ b/src/renderer/src/i18n/tr-TR/settings.json
@@ -2478,7 +2478,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2486,8 +2486,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/vi-VN/settings.json b/src/renderer/src/i18n/vi-VN/settings.json
index f0a4347f62..0ba5be299c 100644
--- a/src/renderer/src/i18n/vi-VN/settings.json
+++ b/src/renderer/src/i18n/vi-VN/settings.json
@@ -2478,7 +2478,7 @@
},
"pluginsHub": {
"subtitle": "Work with DeepChat across skills, MCP servers, official plugins, and remote channels.",
- "searchPlaceholder": "Search plugins, skills, MCP, and remote channels",
+ "searchPlaceholder": "Search plugins and remote channels",
"added": "Added",
"manage": "Manage",
"noAdded": "No enabled plugins yet.",
@@ -2486,8 +2486,6 @@
"pluginNotFound": "Plugin not found.",
"capabilities": "Capabilities",
"actionResult": "Action result",
- "mcpDescription": "Configure MCP servers and marketplace entries.",
- "skillsDescription": "Manage agent skills available to conversations.",
"filters": {
"official": "By DeepChat",
"workspace": "By your workspace",
diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json
index 5febd6fd7b..de618bdc3d 100644
--- a/src/renderer/src/i18n/zh-CN/settings.json
+++ b/src/renderer/src/i18n/zh-CN/settings.json
@@ -2374,7 +2374,7 @@
},
"pluginsHub": {
"subtitle": "统一管理 DeepChat 的 Skills、MCP、官方插件和远程渠道。",
- "searchPlaceholder": "搜索插件、Skills、MCP 和远程渠道",
+ "searchPlaceholder": "搜索插件和远程渠道",
"added": "已添加",
"manage": "管理",
"noAdded": "还没有启用任何插件。",
@@ -2382,8 +2382,6 @@
"pluginNotFound": "未找到插件。",
"capabilities": "能力",
"actionResult": "操作结果",
- "mcpDescription": "配置 MCP 服务和市场条目。",
- "skillsDescription": "管理会话中可用的 Agent Skills。",
"filters": {
"official": "DeepChat 官方",
"workspace": "工作区",
diff --git a/src/renderer/src/i18n/zh-HK/settings.json b/src/renderer/src/i18n/zh-HK/settings.json
index 336d8cf963..4b62b743aa 100644
--- a/src/renderer/src/i18n/zh-HK/settings.json
+++ b/src/renderer/src/i18n/zh-HK/settings.json
@@ -2487,7 +2487,7 @@
},
"pluginsHub": {
"subtitle": "統一管理 DeepChat 的 Skills、MCP、官方插件和遠端渠道。",
- "searchPlaceholder": "搜尋插件、Skills、MCP 和遠端渠道",
+ "searchPlaceholder": "搜尋插件和遠端渠道",
"added": "已新增",
"manage": "管理",
"noAdded": "還沒有啟用任何插件。",
@@ -2495,8 +2495,6 @@
"pluginNotFound": "找不到插件。",
"capabilities": "能力",
"actionResult": "操作結果",
- "mcpDescription": "設定 MCP 服務和市場項目。",
- "skillsDescription": "管理會話中可用的 Agent Skills。",
"filters": {
"official": "DeepChat 官方",
"workspace": "工作區",
diff --git a/src/renderer/src/i18n/zh-TW/settings.json b/src/renderer/src/i18n/zh-TW/settings.json
index 4c0e9be4ae..e1a8e14a75 100644
--- a/src/renderer/src/i18n/zh-TW/settings.json
+++ b/src/renderer/src/i18n/zh-TW/settings.json
@@ -2487,7 +2487,7 @@
},
"pluginsHub": {
"subtitle": "統一管理 DeepChat 的 Skills、MCP、官方插件和遠端渠道。",
- "searchPlaceholder": "搜尋插件、Skills、MCP 和遠端渠道",
+ "searchPlaceholder": "搜尋插件和遠端渠道",
"added": "已新增",
"manage": "管理",
"noAdded": "還沒有啟用任何插件。",
@@ -2495,8 +2495,6 @@
"pluginNotFound": "找不到插件。",
"capabilities": "能力",
"actionResult": "操作結果",
- "mcpDescription": "設定 MCP 服務和市場項目。",
- "skillsDescription": "管理會話中可用的 Agent Skills。",
"filters": {
"official": "DeepChat 官方",
"workspace": "工作區",
diff --git a/src/renderer/src/pages/plugins/PluginsCatalogPage.vue b/src/renderer/src/pages/plugins/PluginsCatalogPage.vue
index f15edaa4a4..00bf51c2a5 100644
--- a/src/renderer/src/pages/plugins/PluginsCatalogPage.vue
+++ b/src/renderer/src/pages/plugins/PluginsCatalogPage.vue
@@ -149,7 +149,7 @@ type AddedItem = {
}
type CatalogItem = {
id: string
- kind: 'official' | 'remote' | 'built-in'
+ kind: 'official' | 'remote'
plugin?: PluginListItem
channel?: RemoteChannel
title: string
@@ -308,26 +308,7 @@ const catalogItems = computed(() => {
}
})
- const builtInItems: CatalogItem[] = [
- {
- id: 'built-in:mcp',
- kind: 'built-in',
- title: t('routes.settings-mcp'),
- description: t('settings.pluginsHub.mcpDescription'),
- icon: 'lucide:server',
- actionLabel: t('settings.pluginsHub.manage')
- },
- {
- id: 'built-in:skills',
- kind: 'built-in',
- title: t('routes.settings-skills'),
- description: t('settings.pluginsHub.skillsDescription'),
- icon: 'lucide:wand-sparkles',
- actionLabel: t('settings.pluginsHub.manage')
- }
- ]
-
- return [...builtInItems, ...officialItems, ...remoteItems]
+ return [...officialItems, ...remoteItems]
})
const filteredCatalogItems = computed(() => {
@@ -420,15 +401,6 @@ function handleCatalogAction(item: CatalogItem): void {
void router.push({ name: 'plugins-remote-detail', params: { channel: item.channel } })
return
}
-
- if (item.id === 'built-in:mcp') {
- void router.push({ name: 'plugins-mcp' })
- return
- }
-
- if (item.id === 'built-in:skills') {
- void router.push({ name: 'plugins-skills' })
- }
}
onMounted(() => {
From 8312790854a7eb7de7a0d6d52670c11ffcfe35c8 Mon Sep 17 00:00:00 2001
From: zerob13
Date: Thu, 25 Jun 2026 17:02:27 +0800
Subject: [PATCH 05/14] fix(sidebar): localize search command
---
docs/features/plugins-hub/tasks.md | 1 +
src/renderer/src/i18n/zh-CN/chat.json | 2 +-
src/renderer/src/i18n/zh-HK/chat.json | 2 +-
src/renderer/src/i18n/zh-TW/chat.json | 2 +-
4 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/docs/features/plugins-hub/tasks.md b/docs/features/plugins-hub/tasks.md
index 5a657d751c..ac470d6e4e 100644
--- a/docs/features/plugins-hub/tasks.md
+++ b/docs/features/plugins-hub/tasks.md
@@ -91,6 +91,7 @@
- [x] Keep `所有 Agents` title.
- [x] Wire `New Chat` row to navigate to `/chat` and start a new conversation.
- [x] Wire `Search` row to existing Spotlight behavior.
+- [x] Localize the `Search` command label for Chinese locales.
- [x] Wire `Plugins` row to `router.push({ name: 'plugins' })`.
- [x] Add a blank spacer after the `Plugins` command row.
- [x] Render `Pinned` only when pinned sessions exist.
diff --git a/src/renderer/src/i18n/zh-CN/chat.json b/src/renderer/src/i18n/zh-CN/chat.json
index 354e9405b2..a969984e86 100644
--- a/src/renderer/src/i18n/zh-CN/chat.json
+++ b/src/renderer/src/i18n/zh-CN/chat.json
@@ -356,7 +356,7 @@
"themeLight": "浅色",
"themeDark": "深色",
"themeSystem": "跟随系统",
- "searchCommand": "Search",
+ "searchCommand": "搜索",
"chatSection": "Chat",
"workspace": "工作区",
"remoteControlDisabled": "未启用",
diff --git a/src/renderer/src/i18n/zh-HK/chat.json b/src/renderer/src/i18n/zh-HK/chat.json
index e178824f82..177df51101 100644
--- a/src/renderer/src/i18n/zh-HK/chat.json
+++ b/src/renderer/src/i18n/zh-HK/chat.json
@@ -345,7 +345,7 @@
"moveProjectGroupUp": "上移",
"moveProjectGroupDown": "下移",
"moveProjectGroupBottom": "移至最下方",
- "searchCommand": "Search",
+ "searchCommand": "搜尋",
"chatSection": "Chat",
"workspace": "工作區"
},
diff --git a/src/renderer/src/i18n/zh-TW/chat.json b/src/renderer/src/i18n/zh-TW/chat.json
index 7931080e15..4f5a12b6ca 100644
--- a/src/renderer/src/i18n/zh-TW/chat.json
+++ b/src/renderer/src/i18n/zh-TW/chat.json
@@ -345,7 +345,7 @@
"moveProjectGroupUp": "上移",
"moveProjectGroupDown": "下移",
"moveProjectGroupBottom": "移至最下方",
- "searchCommand": "Search",
+ "searchCommand": "搜尋",
"chatSection": "Chat",
"workspace": "工作區"
},
From 139c6e79475bd4464d6bb211b65c0e4fd40861a4 Mon Sep 17 00:00:00 2001
From: zerob13
Date: Thu, 25 Jun 2026 17:15:25 +0800
Subject: [PATCH 06/14] refactor(plugins): fold remote channels into catalog
---
docs/features/plugins-hub/plan.md | 40 ++++------
docs/features/plugins-hub/spec.md | 34 ++++----
docs/features/plugins-hub/tasks.md | 15 ++--
.../presenter/remoteControlPresenter/types.ts | 24 ++++--
.../settings/components/RemoteSettings.vue | 56 ++++++++------
src/renderer/src/components/WindowSideBar.vue | 13 +++-
.../plugins/OfficialPluginDetailPage.vue | 42 +++++++++-
.../src/pages/plugins/PluginsCatalogPage.vue | 77 +++++++++++--------
.../src/pages/plugins/PluginsHubPage.vue | 9 ---
.../src/pages/plugins/RemotePluginsPage.vue | 7 --
src/renderer/src/router/index.ts | 26 +++----
src/renderer/src/stores/ui/spotlight.ts | 2 +-
.../remoteBindingStore.test.ts | 46 +++++++++++
.../renderer/components/WindowSideBar.test.ts | 9 ++-
14 files changed, 250 insertions(+), 150 deletions(-)
delete mode 100644 src/renderer/src/pages/plugins/RemotePluginsPage.vue
diff --git a/docs/features/plugins-hub/plan.md b/docs/features/plugins-hub/plan.md
index 6fddc356b2..15f48a312c 100644
--- a/docs/features/plugins-hub/plan.md
+++ b/docs/features/plugins-hub/plan.md
@@ -30,9 +30,7 @@ Main window
/plugins
/plugins/skills
/plugins/mcp
- /plugins/remote
- /plugins/official/:pluginId
- /plugins/remote/:channel
+ /plugins/:pluginId
```
Use the existing `src/renderer/src/router/index.ts`. Do not add `src/renderer/plugins`, a new Vite
@@ -44,9 +42,7 @@ Route names can be:
plugins
plugins-skills
plugins-mcp
-plugins-remote
-plugins-official-detail
-plugins-remote-detail
+plugins-detail
```
External/main-process callers should not know UI component internals. Reuse the existing app-runtime event path where possible. Only add a narrow route if a future main-process caller needs generic main-window navigation:
@@ -104,8 +100,6 @@ src/renderer/src/pages/plugins/
├── OfficialPluginDetailPage.vue
├── McpPluginsPage.vue
├── SkillsPluginsPage.vue
-├── RemotePluginsPage.vue
-├── RemotePluginDetailPage.vue
├── components/
│ ├── PluginsTopTabs.vue
│ ├── PluginCatalogGrid.vue
@@ -121,9 +115,10 @@ Keep this list flexible during implementation; do not split files unless the com
Visual baseline:
-- Main content starts with top tabs (`Plugins`, `Skills`, `MCP`, `Remote`).
+- Main content starts with top tabs (`Plugins`, `Skills`, `MCP`).
- Catalog page uses the Codex-like layout: title, subtitle, search, added strip, segmented filters, sectioned list.
-- Catalog cards include official plugins and Remote virtual plugins only; MCP and Skills remain reachable through top tabs.
+- Catalog cards include official plugins and Remote virtual plugins; MCP and Skills remain reachable through top tabs.
+- Remote does not have a top tab or product list route. Each channel opens as a virtual plugin detail.
- Avoid settings-style full-width form pages for the catalog. Detail routes may use denser settings sections.
- Cards are individual repeated items only. Do not put page sections inside floating cards.
@@ -136,9 +131,8 @@ Renderer-side navigation:
| Sidebar `Plugins` row | `router.push({ name: 'plugins' })` |
| Top tab `Skills` | `router.push({ name: 'plugins-skills' })` |
| Top tab `MCP` | `router.push({ name: 'plugins-mcp' })` |
-| Top tab `Remote` | `router.push({ name: 'plugins-remote' })` |
-| Official plugin card/detail | `router.push({ name: 'plugins-official-detail', params: { pluginId } })` |
-| Remote channel card/detail | `router.push({ name: 'plugins-remote-detail', params: { channel } })` |
+| Plugin card/detail | `router.push({ name: 'plugins-detail', params: { pluginId } })` |
+| Remote channel card/detail | `router.push({ name: 'plugins-detail', params: { pluginId: 'remote:' } })` |
| `New Chat` row while on `/plugins` | `router.push({ name: 'chat' })`, then start new conversation |
Main-process initiated navigation:
@@ -153,7 +147,7 @@ Current behavior opens `PluginPresenter.openPluginSettingsWindow(pluginId)`.
Target behavior:
-- List page opens `/plugins/official/:pluginId`.
+- List page opens `/plugins/:pluginId`.
- Detail page loads `plugins.get(pluginId)`.
- Enable/disable remains in detail and list.
- Runtime status and MCP status remain visible.
@@ -216,16 +210,16 @@ Compatibility:
## Remote Migration
-First increment: reuse `RemoteSettings.vue` inside `/plugins/remote` and `/plugins/remote/:channel`, with route-param synchronization so direct channel links open the matching tab. This keeps the existing credential, pairing, default agent/workdir, bindings and WeChat iLink behavior intact.
+First increment: reuse `RemoteSettings.vue` inside `/plugins/:pluginId` for virtual plugin ids such as `remote:telegram`. Single-channel mode hides the old Remote tab strip and renders the existing channel form inside the plugin detail page. This keeps the existing credential, pairing, default agent/workdir, bindings and WeChat iLink behavior intact.
Follow-up refactor: extract channel sections from `RemoteSettings.vue` into reusable components. The file is already large, but splitting it before moving the route would increase regression risk and delay the user-visible entry-point cleanup.
Refactor only around real channel boundaries:
```text
-RemotePluginsPage
+PluginsCatalogPage
-> virtual cards from listRemoteChannels()
-RemotePluginDetailPage(channel)
+PluginDetailPage(remote:)
-> channel header/status/toggle
-> credentials section
-> default agent/workdir section
@@ -239,7 +233,7 @@ Suggested extracted components:
| Component | Scope |
| --- | --- |
| `RemotePluginCard` | card summary for one channel |
-| `RemotePluginDetailPage` | detail shell and save status |
+| `PluginDetailPage(remote:)` | detail shell and save status |
| `RemoteCredentialsSection` | token/app secret fields; channel-specific props |
| `RemoteDefaultsSection` | default agent and default workdir |
| `RemotePairingSection` | pair code and principals for pairable channels |
@@ -264,7 +258,7 @@ remote:
description: descriptor.descriptionKey
enabled: status.enabled
state: status.state
- detailRoute: /plugins/remote/:channel
+ detailRoute: /plugins/:pluginId
```
## Settings Removal and Redirects
@@ -290,7 +284,7 @@ Mapping:
| Old Settings route | Main window target |
| --- | --- |
| `settings-mcp` | `/plugins/mcp` |
-| `settings-remote` | `/plugins/remote` |
+| `settings-remote` | `/plugins` |
| `settings-plugins` | `/plugins` |
| `settings-skills` | `/plugins/skills` |
@@ -380,7 +374,7 @@ Update callers:
| MCP install deeplink | focus main window, route to `/plugins/mcp`, dispatch MCP install event there |
| Settings sidebar old MCP/Skills/Plugins/Remote | no visible entry |
| Settings activity old route | focus main window and route to matching `/plugins...` page |
-| Sidebar remote status button | route to `/plugins/remote` or a selected channel detail |
+| Sidebar remote status button | route to the first enabled `remote:` plugin detail |
| Chat input MCP indicator `openSettings` text | route to `/plugins/mcp` |
Provider install deeplink stays in Settings Provider. Do not route provider/model setup to Plugins.
@@ -439,9 +433,9 @@ Renderer tests should be run for touched components. Full app smoke test should
| Risk | Mitigation |
| --- | --- |
| Settings routes are used by deeplinks/onboarding | Keep hidden compatibility routes and redirect to main `/plugins...` |
-| RemoteSettings monolith makes migration risky | Extract per-channel detail only; reuse current route/client behavior |
+| RemoteSettings monolith makes migration risky | Reuse it in single-channel mode; extract per-channel sections only when needed |
| Plugin settings HTML depends on plugin preload | Do not embed arbitrary HTML in first increment; build first-party native details |
-| Feishu official plugin vs Feishu Remote naming collision | Use explicit `Integration` vs `Remote` labels and category badges |
+| Feishu official plugin vs Feishu Remote naming collision | Merge Feishu Remote into the Feishu/Lark Integration detail page |
| Plugins page becomes another Settings | Catalog page stays Codex-like; detail pages are dense only where settings are unavoidable |
| Main route conflicts with chat internal `pageRouter` | Use Vue router for `/plugins`; keep `pageRouter` scoped to ChatTabView |
| Search behavior confusion | Sidebar Search row opens existing Spotlight; do not add a new search engine |
diff --git a/docs/features/plugins-hub/spec.md b/docs/features/plugins-hub/spec.md
index 7ddcc30967..f2e071bc0f 100644
--- a/docs/features/plugins-hub/spec.md
+++ b/docs/features/plugins-hub/spec.md
@@ -89,9 +89,7 @@ src/renderer/src/router/index.ts
├── tab=plugins or child /plugins
├── /plugins/skills
├── /plugins/mcp
- ├── /plugins/remote
- ├── /plugins/official/:pluginId
- └── /plugins/remote/:channel
+ └── /plugins/:pluginId
```
Implementation can use nested Vue routes or one `/plugins` route with internal tab state. The URL must be shareable enough for internal navigation and redirects:
@@ -101,9 +99,9 @@ Implementation can use nested Vue routes or one `/plugins` route with internal t
| Plugins catalog | `/plugins` |
| Skills | `/plugins/skills` |
| MCP | `/plugins/mcp` |
-| Remote list | `/plugins/remote` |
-| Official plugin detail | `/plugins/official/:pluginId` |
-| Remote channel detail | `/plugins/remote/:channel` |
+| Plugin detail | `/plugins/:pluginId` |
+
+Legacy `/plugins/official/:pluginId`, `/plugins/remote` and `/plugins/remote/:channel` paths may redirect for compatibility, but they are not product routes.
## Proposed Information Architecture
@@ -114,17 +112,12 @@ Top-level sections:
| Plugins | Plugins | official plugin packages, added/recommended plugin cards, Remote virtual plugin cards |
| Skills | Skills | installed skills, install, edit, sync import/export, draft suggestion toggle |
| MCP | MCP Servers | user MCP servers, plugin-owned MCP status, MCP market/add flow |
-| Remote | Remote | virtual plugin cards for Telegram, Feishu/Lark Remote, QQBot, Discord, WeChat iLink |
-The visual top tab row uses `Plugins`, `Skills`, `MCP` and `Remote`. `MCP` and `Skills` are sibling tabs, not plugin catalog cards.
+The visual top tab row uses `Plugins`, `Skills` and `MCP`. `Remote` is not a top tab; each remote channel is a virtual plugin card in the catalog.
-Remote naming must avoid collision with official plugins:
+Remote virtual plugin ids use `remote:`. Feishu/Lark is special: when the official Feishu/Lark Integration plugin is installed, the Feishu/Lark Remote card is merged into that official plugin detail page.
-| Existing item | Display name in Plugins |
-| --- | --- |
-| `com.deepchat.plugins.feishu` official plugin | `Feishu/Lark Integration` |
-| `remote:feishu` virtual plugin | `Feishu/Lark Remote` |
-| `remote:telegram` virtual plugin | `Telegram Remote` |
+Historical remote settings compatibility: if a channel has credentials/accounts from an older configuration and no explicit enabled flag, that virtual plugin starts enabled by default. Explicit `enabled: false` still stays disabled.
## Main Window Plugins UX
@@ -134,7 +127,7 @@ Remote naming must avoid collision with official plugins:
┌──────────────────────────────────────────────────────────────────────────────┐
│ AppBar │
├───────────────┬──────────────────────────────────────────────────────────────┤
-│rail│ expanded sidebar │ [Plugins] [Skills] [MCP] [Remote] + ↻ │
+│rail│ expanded sidebar │ [Plugins] [Skills] [MCP] + ↻ │
│ │ 所有 Agents │ │
│ │ New Chat │ Plugins │
│ │ Search │ Work with DeepChat across your favorite tools │
@@ -157,7 +150,7 @@ Remote naming must avoid collision with official plugins:
┌──────────────────────────────────────────────────────────────────────────────┐
│ AppBar │
├───────────────┬──────────────────────────────────────────────────────────────┤
-│rail│ expanded sidebar │ [Plugins] [Skills] [MCP] [Remote] │
+│rail│ expanded sidebar │ [Plugins] [Skills] [MCP] │
│ │ 所有 Agents │ ← Back to Plugins │
│ │ New Chat │ Telegram Remote on/off │
│ │ Search │ Status: running · bindings: 2 · last error: none │
@@ -183,7 +176,6 @@ At constrained widths, keep the same app shell and avoid modal navigation:
│ AppBar │
├────┬───────────────────────────────┤
│rail│ [Plugins][Skills][MCP] │
-│ │ [Remote] │
│⚙︎ ├───────────────────────────────┤
│ │ Search │
│ │ │
@@ -305,9 +297,9 @@ Compatibility behavior:
- Official plugin list keeps enable/disable/status behavior.
- Plugin-owned MCP errors remain visible.
-- Opening an official plugin settings/detail uses `/plugins/official/:pluginId`, not Settings and not a per-plugin BrowserWindow.
+- Opening a plugin settings/detail uses `/plugins/:pluginId`, not Settings and not a per-plugin BrowserWindow.
- CUA detail includes runtime/MCP status, permission checks and permission guide actions.
-- Feishu/Lark Integration detail distinguishes MCP/Skill integration from Feishu/Lark Remote control.
+- Feishu/Lark Integration detail includes Feishu/Lark Remote configuration instead of showing a separate Feishu/Lark Remote card.
- Legacy `settings.open` plugin action is not used as the primary UI path after migration.
### MCP
@@ -326,8 +318,8 @@ Compatibility behavior:
- Every implemented `RemoteChannelDescriptor` appears as a Remote virtual plugin card.
- Each card shows enabled state, runtime state, binding/pairing summary and last error when present.
-- Each card opens `/plugins/remote/:channel`.
-- Channel settings preserve current behavior:
+- Each card opens `/plugins/:pluginId`; remote virtual plugin ids use `remote:`.
+- Channel settings render inside the plugin detail page and preserve current behavior:
- credentials
- enable/disable
- default agent
diff --git a/docs/features/plugins-hub/tasks.md b/docs/features/plugins-hub/tasks.md
index ac470d6e4e..ed82ff4e39 100644
--- a/docs/features/plugins-hub/tasks.md
+++ b/docs/features/plugins-hub/tasks.md
@@ -11,7 +11,7 @@
- [x] Add `/plugins` route family to the existing main renderer router.
- [x] Add `PluginsHubPage.vue` inside `src/renderer/src/pages/plugins/`.
-- [x] Add top tab navigation for Plugins, Skills, MCP and Remote or the chosen first-increment subset.
+- [x] Add top tab navigation for Plugins, Skills and MCP.
- [x] Add Codex-like catalog placeholder with title, subtitle, search, added strip and featured sections.
- [x] Keep MCP and Skills as top tabs only, not plugin catalog cards.
- [x] Keep `WindowSideBar`, `AppBar`, global overlays, theme and i18n behavior intact.
@@ -60,11 +60,11 @@
## 6. Official Plugins Section
- [x] Create official plugin list route from `PluginClient.listPlugins`.
-- [x] Add detail route `/plugins/official/:pluginId`.
+- [x] Add unified detail route `/plugins/:pluginId`.
- [x] Keep enable/disable actions.
- [x] Show runtime status, plugin-owned MCP status and last errors.
- [ ] Add native CUA detail sections for runtime status, permissions and permission guide actions.
-- [ ] Add native Feishu/Lark Integration detail that distinguishes it from Feishu/Lark Remote.
+- [x] Merge Feishu/Lark Remote configuration into the Feishu/Lark Integration detail page.
- [x] Stop first-party Plugins UI from calling `settings.open`.
- [x] Keep `settings.open` only as temporary compatibility fallback.
- [ ] Add tests for list/detail action behavior.
@@ -73,16 +73,17 @@
- [x] Build remote virtual cards from `remoteControl.listChannels`.
- [x] Fetch and display per-channel status.
-- [x] Add `/plugins/remote` route.
-- [x] Add `/plugins/remote/:channel` detail route.
-- [ ] Extract only the needed RemoteSettings channel sections into reusable components.
+- [x] Route remote virtual plugin cards through `/plugins/:pluginId` using `remote:` ids.
+- [x] Remove the Remote top tab/product list route.
+- [x] Reuse `RemoteSettings` in single-channel mode inside plugin detail pages.
+- [x] Auto-enable configured legacy channels when the explicit enabled flag is missing.
- [x] Preserve credentials fields and password reveal behavior.
- [x] Preserve enable/disable save behavior.
- [x] Preserve default agent and default workdir behavior.
- [x] Preserve pairing flow for Telegram, Feishu/Lark, QQBot and Discord.
- [x] Preserve binding/principal removal behavior.
- [x] Preserve WeChat iLink login/account controls.
-- [x] Route sidebar remote status button to `/plugins/remote` or selected channel detail.
+- [x] Route sidebar remote status button to the first enabled remote plugin detail.
- [ ] Add tests for card mapping, save, pairing and bindings.
## 8. Main Sidebar Layout
diff --git a/src/main/presenter/remoteControlPresenter/types.ts b/src/main/presenter/remoteControlPresenter/types.ts
index 6d317a0157..1188939cf5 100644
--- a/src/main/presenter/remoteControlPresenter/types.ts
+++ b/src/main/presenter/remoteControlPresenter/types.ts
@@ -1127,7 +1127,7 @@ const extractLegacyTelegramConfig = (input: unknown): LegacyTelegramRemoteConfig
const record = input as Record
if (
- !hasAnyOwn(record, ['allowlist', 'streamMode', 'pollOffset', 'lastFatalError']) &&
+ !hasAnyOwn(record, ['botToken', 'allowlist', 'streamMode', 'pollOffset', 'lastFatalError']) &&
!hasBindingPrefix(record, 'telegram:')
) {
return null
@@ -1367,6 +1367,9 @@ const normalizeBindings = (
return bindings
}
+const resolveRemoteEnabled = (enabled: boolean | undefined, configured: boolean): boolean =>
+ typeof enabled === 'boolean' ? enabled : configured
+
export const normalizeRemoteControlConfig = (input: unknown): RemoteControlConfig => {
const defaults = createDefaultRemoteControlConfig()
const parsed = RemoteControlConfigSchema.safeParse(input)
@@ -1379,11 +1382,12 @@ export const normalizeRemoteControlConfig = (input: unknown): RemoteControlConfi
const qqbot = parsed.data.qqbot ?? extractLegacyQQBotConfig(input) ?? {}
const discord = parsed.data.discord ?? extractLegacyDiscordConfig(input) ?? {}
const weixinIlink = parsed.data.weixinIlink ?? extractLegacyWeixinIlinkConfig(input) ?? {}
+ const weixinIlinkAccounts = normalizeWeixinIlinkRuntimeAccounts(weixinIlink.accounts)
return {
telegram: {
botToken: telegram.botToken?.trim() || '',
- enabled: Boolean(telegram.enabled),
+ enabled: resolveRemoteEnabled(telegram.enabled, Boolean(telegram.botToken?.trim())),
allowlist: normalizeTelegramUserIds(telegram.allowlist),
streamMode: telegram.streamMode === 'final' ? 'final' : defaults.telegram.streamMode,
defaultAgentId: telegram.defaultAgentId?.trim() || defaults.telegram.defaultAgentId,
@@ -1411,7 +1415,10 @@ export const normalizeRemoteControlConfig = (input: unknown): RemoteControlConfi
appSecret: feishu.appSecret?.trim() || '',
verificationToken: feishu.verificationToken?.trim() || '',
encryptKey: feishu.encryptKey?.trim() || '',
- enabled: Boolean(feishu.enabled),
+ enabled: resolveRemoteEnabled(
+ feishu.enabled,
+ Boolean(feishu.appId?.trim() && feishu.appSecret?.trim())
+ ),
defaultAgentId: feishu.defaultAgentId?.trim() || defaults.feishu.defaultAgentId,
defaultWorkdir: feishu.defaultWorkdir?.trim() || '',
pairedUserOpenIds: normalizeFeishuOpenIds(feishu.pairedUserOpenIds),
@@ -1429,7 +1436,10 @@ export const normalizeRemoteControlConfig = (input: unknown): RemoteControlConfi
qqbot: {
appId: qqbot.appId?.trim() || '',
clientSecret: qqbot.clientSecret?.trim() || '',
- enabled: Boolean(qqbot.enabled),
+ enabled: resolveRemoteEnabled(
+ qqbot.enabled,
+ Boolean(qqbot.appId?.trim() && qqbot.clientSecret?.trim())
+ ),
defaultAgentId: qqbot.defaultAgentId?.trim() || defaults.qqbot.defaultAgentId,
defaultWorkdir: qqbot.defaultWorkdir?.trim() || '',
pairedUserIds: normalizeQQBotUserIds(qqbot.pairedUserIds),
@@ -1447,7 +1457,7 @@ export const normalizeRemoteControlConfig = (input: unknown): RemoteControlConfi
},
discord: {
botToken: discord.botToken?.trim() || '',
- enabled: Boolean(discord.enabled),
+ enabled: resolveRemoteEnabled(discord.enabled, Boolean(discord.botToken?.trim())),
defaultAgentId: discord.defaultAgentId?.trim() || defaults.discord.defaultAgentId,
defaultWorkdir: discord.defaultWorkdir?.trim() || '',
pairedChannelIds: normalizeDiscordChannelIds(discord.pairedChannelIds),
@@ -1464,10 +1474,10 @@ export const normalizeRemoteControlConfig = (input: unknown): RemoteControlConfi
bindings: normalizeBindings(discord.bindings, 'discord')
},
weixinIlink: {
- enabled: Boolean(weixinIlink.enabled),
+ enabled: resolveRemoteEnabled(weixinIlink.enabled, weixinIlinkAccounts.length > 0),
defaultAgentId: weixinIlink.defaultAgentId?.trim() || defaults.weixinIlink.defaultAgentId,
defaultWorkdir: weixinIlink.defaultWorkdir?.trim() || '',
- accounts: normalizeWeixinIlinkRuntimeAccounts(weixinIlink.accounts)
+ accounts: weixinIlinkAccounts
}
}
}
diff --git a/src/renderer/settings/components/RemoteSettings.vue b/src/renderer/settings/components/RemoteSettings.vue
index b426fd2175..6206858f04 100644
--- a/src/renderer/settings/components/RemoteSettings.vue
+++ b/src/renderer/settings/components/RemoteSettings.vue
@@ -1,5 +1,5 @@
-
+
@@ -25,20 +25,27 @@
{{ t('common.error.requestFailed') }}
-
+
-
{{ t('settings.remote.title') }}
+
+ {{ singleChannelMode ? channelTitle(activeChannel) : t('settings.remote.title') }}
+
{{ t('common.saving') }}
- {{ t('settings.remote.description') }}
+ {{
+ singleChannelMode
+ ? channelDescription(activeChannel)
+ : t('settings.remote.description')
+ }}
@@ -1274,7 +1281,7 @@
-
+
-
-
+
{{
feishuSettings.remoteEnabled ? t('common.enabled') : t('common.disabled')
}}
@@ -574,7 +580,10 @@
{{ qqbotStatus.lastError }}
-
+
{{
qqbotSettings.remoteEnabled ? t('common.enabled') : t('common.disabled')
}}
@@ -784,7 +793,10 @@
{{ discordStatus.lastError }}
-
+
{{
discordSettings.remoteEnabled ? t('common.enabled') : t('common.disabled')
}}
@@ -1004,7 +1016,10 @@
{{ weixinIlinkStatus.lastError }}
-
+
{{
weixinIlinkSettings.remoteEnabled ? t('common.enabled') : t('common.disabled')
}}
@@ -1643,6 +1658,7 @@ const props = defineProps<{
channel?: RemoteChannel
embedded?: boolean
hideHeader?: boolean
+ hideChannelToggle?: boolean
singleChannel?: boolean
}>()
diff --git a/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue b/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue
index 585ccca03c..865610ba34 100644
--- a/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue
+++ b/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue
@@ -155,6 +155,7 @@
v-if="isFeishuPlugin"
channel="feishu"
embedded
+ hide-channel-toggle
hide-header
single-channel
/>
@@ -178,6 +179,7 @@ import { Icon } from '@iconify/vue'
import { Button } from '@shadcn/components/ui/button'
import { ScrollArea } from '@shadcn/components/ui/scroll-area'
import { createPluginClient } from '@api/PluginClient'
+import { createRemoteControlClient } from '@api/RemoteControlClient'
import RemoteSettings from '../../../settings/components/RemoteSettings.vue'
import type { RemoteChannel } from '@shared/presenter'
import type { PluginActionResult, PluginListItem, PluginRuntimeState } from '@shared/types/plugin'
@@ -186,6 +188,7 @@ const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const pluginClient = createPluginClient()
+const remoteControlClient = createRemoteControlClient()
const plugin = ref(null)
const loading = ref(false)
@@ -255,12 +258,30 @@ async function runPluginAction(action: () => Promise): Promi
}
}
+async function setFeishuRemoteEnabled(remoteEnabled: boolean): Promise {
+ const settings = await remoteControlClient.getChannelSettings('feishu')
+ if (settings.remoteEnabled === remoteEnabled) {
+ return
+ }
+
+ await remoteControlClient.saveChannelSettings('feishu', {
+ ...settings,
+ remoteEnabled
+ })
+}
+
function enablePlugin(): void {
const currentPlugin = plugin.value
if (!currentPlugin) {
return
}
- void runPluginAction(() => pluginClient.enablePlugin(currentPlugin.id))
+ void runPluginAction(async () => {
+ const result = await pluginClient.enablePlugin(currentPlugin.id)
+ if (result.ok && currentPlugin.id === FEISHU_PLUGIN_ID) {
+ await setFeishuRemoteEnabled(true)
+ }
+ return result
+ })
}
function disablePlugin(): void {
@@ -268,7 +289,13 @@ function disablePlugin(): void {
if (!currentPlugin) {
return
}
- void runPluginAction(() => pluginClient.disablePlugin(currentPlugin.id))
+ void runPluginAction(async () => {
+ const result = await pluginClient.disablePlugin(currentPlugin.id)
+ if (result.ok && currentPlugin.id === FEISHU_PLUGIN_ID) {
+ await setFeishuRemoteEnabled(false)
+ }
+ return result
+ })
}
watch(pluginId, () => {
diff --git a/test/renderer/components/OfficialPluginDetailPage.test.ts b/test/renderer/components/OfficialPluginDetailPage.test.ts
new file mode 100644
index 0000000000..3c959c6804
--- /dev/null
+++ b/test/renderer/components/OfficialPluginDetailPage.test.ts
@@ -0,0 +1,154 @@
+import { describe, expect, it, vi } from 'vitest'
+import { defineComponent } from 'vue'
+import { flushPromises, shallowMount } from '@vue/test-utils'
+
+const passthrough = (name: string) =>
+ defineComponent({
+ name,
+ template: '
'
+ })
+
+const buttonStub = defineComponent({
+ name: 'Button',
+ props: {
+ disabled: { type: Boolean, default: false }
+ },
+ emits: ['click'],
+ template: ''
+})
+
+const remoteSettingsStub = defineComponent({
+ name: 'RemoteSettings',
+ props: {
+ channel: { type: String, default: '' },
+ hideChannelToggle: { type: Boolean, default: false }
+ },
+ template:
+ ''
+})
+
+async function mountFeishuDetail(options: { enabled?: boolean; remoteEnabled?: boolean } = {}) {
+ vi.resetModules()
+ vi.clearAllMocks()
+
+ const pluginClient = {
+ getPlugin: vi.fn().mockResolvedValue({
+ id: 'com.deepchat.plugins.feishu',
+ name: 'Feishu/Lark Integration',
+ publisher: 'DeepChat',
+ version: '1.0.4',
+ enabled: options.enabled ?? false,
+ capabilities: [],
+ mcpServers: []
+ }),
+ enablePlugin: vi.fn().mockResolvedValue({ ok: true }),
+ disablePlugin: vi.fn().mockResolvedValue({ ok: true })
+ }
+ const remoteControlClient = {
+ getChannelSettings: vi.fn().mockResolvedValue({
+ brand: 'feishu',
+ appId: 'cli_a',
+ appSecret: 'secret',
+ verificationToken: '',
+ encryptKey: '',
+ remoteEnabled: options.remoteEnabled ?? false,
+ defaultAgentId: 'feishu-bot',
+ defaultWorkdir: '',
+ pairedUserOpenIds: []
+ }),
+ saveChannelSettings: vi.fn().mockResolvedValue({})
+ }
+ const router = {
+ push: vi.fn()
+ }
+
+ vi.doMock('@api/PluginClient', () => ({
+ createPluginClient: () => pluginClient
+ }))
+ vi.doMock('@api/RemoteControlClient', () => ({
+ createRemoteControlClient: () => remoteControlClient
+ }))
+ vi.doMock('vue-router', async () => {
+ const actual = await vi.importActual('vue-router')
+ return {
+ ...actual,
+ useRoute: () => ({
+ params: { pluginId: 'com.deepchat.plugins.feishu' }
+ }),
+ useRouter: () => router
+ }
+ })
+ vi.doMock('vue-i18n', () => ({
+ useI18n: () => ({
+ t: (key: string) => key
+ })
+ }))
+ vi.doMock('@iconify/vue', () => ({
+ Icon: defineComponent({
+ name: 'Icon',
+ template: ''
+ })
+ }))
+ vi.doMock('../../../src/renderer/settings/components/RemoteSettings.vue', () => ({
+ default: remoteSettingsStub
+ }))
+ vi.doMock('../../../settings/components/RemoteSettings.vue', () => ({
+ default: remoteSettingsStub
+ }))
+
+ const OfficialPluginDetailPage = (await import('@/pages/plugins/OfficialPluginDetailPage.vue'))
+ .default
+ const wrapper = shallowMount(OfficialPluginDetailPage, {
+ global: {
+ stubs: {
+ Button: buttonStub,
+ ScrollArea: passthrough('ScrollArea'),
+ RemoteSettings: remoteSettingsStub
+ }
+ }
+ })
+ await flushPromises()
+
+ return { wrapper, pluginClient, remoteControlClient }
+}
+
+describe('OfficialPluginDetailPage', () => {
+ it('uses the plugin enable button to start Feishu remote too', async () => {
+ const { wrapper, pluginClient, remoteControlClient } = await mountFeishuDetail()
+
+ expect(wrapper.find('[data-testid="remote-settings"]').attributes('data-hide-toggle')).toBe(
+ 'true'
+ )
+
+ await wrapper
+ .findAll('button')
+ .find((button) => button.text() === 'settings.plugins.enable')!
+ .trigger('click')
+ await flushPromises()
+
+ expect(pluginClient.enablePlugin).toHaveBeenCalledWith('com.deepchat.plugins.feishu')
+ expect(remoteControlClient.saveChannelSettings).toHaveBeenCalledWith(
+ 'feishu',
+ expect.objectContaining({ remoteEnabled: true })
+ )
+ })
+
+ it('uses the plugin disable button to stop Feishu remote too', async () => {
+ const { wrapper, pluginClient, remoteControlClient } = await mountFeishuDetail({
+ enabled: true,
+ remoteEnabled: true
+ })
+
+ await wrapper
+ .findAll('button')
+ .find((button) => button.text() === 'settings.plugins.disable')!
+ .trigger('click')
+ await flushPromises()
+
+ expect(pluginClient.disablePlugin).toHaveBeenCalledWith('com.deepchat.plugins.feishu')
+ expect(remoteControlClient.saveChannelSettings).toHaveBeenCalledWith(
+ 'feishu',
+ expect.objectContaining({ remoteEnabled: false })
+ )
+ })
+})
From a9d6450680461a188cc50e32616c8927e5735b44 Mon Sep 17 00:00:00 2001
From: zerob13
Date: Thu, 25 Jun 2026 17:28:45 +0800
Subject: [PATCH 08/14] fix(plugins): use remote detail enable buttons
---
docs/features/plugins-hub/plan.md | 2 +-
docs/features/plugins-hub/spec.md | 1 +
docs/features/plugins-hub/tasks.md | 1 +
.../plugins/OfficialPluginDetailPage.vue | 240 ++++++++++++++++--
.../OfficialPluginDetailPage.test.ts | 104 ++++++--
5 files changed, 309 insertions(+), 39 deletions(-)
diff --git a/docs/features/plugins-hub/plan.md b/docs/features/plugins-hub/plan.md
index 23af8191be..a88fc62d5f 100644
--- a/docs/features/plugins-hub/plan.md
+++ b/docs/features/plugins-hub/plan.md
@@ -210,7 +210,7 @@ Compatibility:
## Remote Migration
-First increment: reuse `RemoteSettings.vue` inside `/plugins/:pluginId` for virtual plugin ids such as `remote:telegram`. Single-channel mode hides the old Remote tab strip and renders the existing channel form inside the plugin detail page. Feishu/Lark Integration passes a hide-toggle mode so its top-level plugin enable button controls both the official plugin and Feishu/Lark Remote. This keeps the existing credential, pairing, default agent/workdir, bindings and WeChat iLink behavior intact.
+First increment: reuse `RemoteSettings.vue` inside `/plugins/:pluginId` for virtual plugin ids such as `remote:telegram`. The detail shell owns the plugin-style enable/disable button, while single-channel mode hides the old Remote tab strip and the embedded channel toggle. Feishu/Lark Integration uses the same hide-toggle mode so its top-level plugin enable button controls both the official plugin and Feishu/Lark Remote. This keeps the existing credential, pairing, default agent/workdir, bindings and WeChat iLink behavior intact.
Follow-up refactor: extract channel sections from `RemoteSettings.vue` into reusable components. The file is already large, but splitting it before moving the route would increase regression risk and delay the user-visible entry-point cleanup.
diff --git a/docs/features/plugins-hub/spec.md b/docs/features/plugins-hub/spec.md
index 7da31513b3..64376e7a6d 100644
--- a/docs/features/plugins-hub/spec.md
+++ b/docs/features/plugins-hub/spec.md
@@ -299,6 +299,7 @@ Compatibility behavior:
- Plugin-owned MCP errors remain visible.
- Opening a plugin settings/detail uses `/plugins/:pluginId`, not Settings and not a per-plugin BrowserWindow.
- CUA detail includes runtime/MCP status, permission checks and permission guide actions.
+- Remote virtual plugin detail pages use the same top-level enable/disable button style as official plugin details. The embedded remote form must not show a second channel toggle.
- Feishu/Lark Integration detail includes Feishu/Lark Remote configuration instead of showing a separate Feishu/Lark Remote card.
- Feishu/Lark Integration has one top-level enable/disable control; it enables/disables both the official plugin and the embedded Feishu/Lark Remote configuration. The embedded remote form must not show a second channel toggle.
- Legacy `settings.open` plugin action is not used as the primary UI path after migration.
diff --git a/docs/features/plugins-hub/tasks.md b/docs/features/plugins-hub/tasks.md
index 6f8a92fab3..a2133d0fdf 100644
--- a/docs/features/plugins-hub/tasks.md
+++ b/docs/features/plugins-hub/tasks.md
@@ -77,6 +77,7 @@
- [x] Route remote virtual plugin cards through `/plugins/:pluginId` using `remote:` ids.
- [x] Remove the Remote top tab/product list route.
- [x] Reuse `RemoteSettings` in single-channel mode inside plugin detail pages.
+- [x] Use the plugin detail top-level enable/disable button for remote virtual plugin state.
- [x] Auto-enable configured legacy channels when the explicit enabled flag is missing.
- [x] Preserve credentials fields and password reveal behavior.
- [x] Preserve enable/disable save behavior.
diff --git a/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue b/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue
index 865610ba34..c41af27401 100644
--- a/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue
+++ b/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue
@@ -1,15 +1,102 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ {{ remoteErrorMessage }}
+
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
-
+
@@ -181,7 +268,12 @@ import { ScrollArea } from '@shadcn/components/ui/scroll-area'
import { createPluginClient } from '@api/PluginClient'
import { createRemoteControlClient } from '@api/RemoteControlClient'
import RemoteSettings from '../../../settings/components/RemoteSettings.vue'
-import type { RemoteChannel } from '@shared/presenter'
+import type {
+ ChannelSettingsMap,
+ RemoteChannel,
+ RemoteChannelSettings,
+ RemoteChannelStatus
+} from '@shared/presenter'
import type { PluginActionResult, PluginListItem, PluginRuntimeState } from '@shared/types/plugin'
const { t } = useI18n()
@@ -192,10 +284,29 @@ const remoteControlClient = createRemoteControlClient()
const plugin = ref
(null)
const loading = ref(false)
+const remoteLoading = ref(false)
const pending = ref(false)
const errorMessage = ref('')
+const remoteErrorMessage = ref('')
const lastActionData = ref('')
+const remoteSettings = ref(null)
+const remoteStatus = ref(null)
+const remoteSettingsVersion = ref(0)
const FEISHU_PLUGIN_ID = 'com.deepchat.plugins.feishu'
+const remoteI18nKeyByChannel: Record = {
+ telegram: 'telegram',
+ feishu: 'feishu',
+ qqbot: 'qqbot',
+ discord: 'discord',
+ 'weixin-ilink': 'weixinIlink'
+}
+const remoteIconByChannel: Record = {
+ telegram: 'lucide:send',
+ feishu: 'lucide:message-circle',
+ qqbot: 'lucide:bot',
+ discord: 'lucide:radio-tower',
+ 'weixin-ilink': 'lucide:messages-square'
+}
const pluginId = computed(() => String(route.params.pluginId ?? ''))
const remoteChannel = computed(() => {
@@ -210,6 +321,19 @@ const remoteChannel = computed(() => {
: null
})
const isFeishuPlugin = computed(() => pluginId.value === FEISHU_PLUGIN_ID)
+const remoteEnabled = computed(() => Boolean(remoteSettings.value?.remoteEnabled))
+const remoteTitle = computed(() => {
+ const channel = remoteChannel.value
+ return channel ? t(`settings.remote.${remoteI18nKeyByChannel[channel]}.title`) : ''
+})
+const remoteDescription = computed(() => {
+ const channel = remoteChannel.value
+ return channel ? t(`settings.remote.${remoteI18nKeyByChannel[channel]}.description`) : ''
+})
+const remoteIcon = computed(() => {
+ const channel = remoteChannel.value
+ return channel ? remoteIconByChannel[channel] : 'lucide:puzzle'
+})
function formatRuntimeState(state?: PluginRuntimeState): string {
if (!state) {
@@ -219,11 +343,6 @@ function formatRuntimeState(state?: PluginRuntimeState): string {
}
async function loadPlugin(): Promise {
- if (remoteChannel.value) {
- plugin.value = null
- return
- }
-
if (!pluginId.value) {
plugin.value = null
return
@@ -240,6 +359,46 @@ async function loadPlugin(): Promise {
}
}
+async function loadRemotePlugin(): Promise {
+ const channel = remoteChannel.value
+ if (!channel) {
+ remoteSettings.value = null
+ remoteStatus.value = null
+ return
+ }
+
+ plugin.value = null
+ remoteLoading.value = true
+ remoteErrorMessage.value = ''
+ errorMessage.value = ''
+ try {
+ const [settings, status] = await Promise.all([
+ remoteControlClient.getChannelSettings(channel),
+ remoteControlClient.getChannelStatus(channel)
+ ])
+ remoteSettings.value = settings
+ remoteStatus.value = status
+ } catch (error) {
+ remoteSettings.value = null
+ remoteStatus.value = null
+ remoteErrorMessage.value =
+ error instanceof Error ? error.message : t('common.error.requestFailed')
+ } finally {
+ remoteLoading.value = false
+ }
+}
+
+async function loadCurrentDetail(): Promise {
+ if (remoteChannel.value) {
+ await loadRemotePlugin()
+ return
+ }
+
+ remoteSettings.value = null
+ remoteStatus.value = null
+ await loadPlugin()
+}
+
async function runPluginAction(action: () => Promise): Promise {
pending.value = true
errorMessage.value = ''
@@ -258,16 +417,37 @@ async function runPluginAction(action: () => Promise): Promi
}
}
-async function setFeishuRemoteEnabled(remoteEnabled: boolean): Promise {
- const settings = await remoteControlClient.getChannelSettings('feishu')
+async function setRemoteChannelEnabled(
+ channel: T,
+ remoteEnabled: boolean
+): Promise {
+ const settings = await remoteControlClient.getChannelSettings(channel)
if (settings.remoteEnabled === remoteEnabled) {
return
}
- await remoteControlClient.saveChannelSettings('feishu', {
+ await remoteControlClient.saveChannelSettings(channel, {
...settings,
remoteEnabled
- })
+ } as ChannelSettingsMap[T])
+}
+
+async function setFeishuRemoteEnabled(remoteEnabled: boolean): Promise {
+ await setRemoteChannelEnabled('feishu', remoteEnabled)
+}
+
+async function runRemoteAction(action: () => Promise): Promise {
+ pending.value = true
+ errorMessage.value = ''
+ try {
+ await action()
+ await loadRemotePlugin()
+ remoteSettingsVersion.value += 1
+ } catch (error) {
+ errorMessage.value = error instanceof Error ? error.message : t('settings.plugins.actionFailed')
+ } finally {
+ pending.value = false
+ }
}
function enablePlugin(): void {
@@ -298,11 +478,27 @@ function disablePlugin(): void {
})
}
+function enableRemotePlugin(): void {
+ const channel = remoteChannel.value
+ if (!channel) {
+ return
+ }
+ void runRemoteAction(() => setRemoteChannelEnabled(channel, true))
+}
+
+function disableRemotePlugin(): void {
+ const channel = remoteChannel.value
+ if (!channel) {
+ return
+ }
+ void runRemoteAction(() => setRemoteChannelEnabled(channel, false))
+}
+
watch(pluginId, () => {
- void loadPlugin()
+ void loadCurrentDetail()
})
onMounted(() => {
- void loadPlugin()
+ void loadCurrentDetail()
})
diff --git a/test/renderer/components/OfficialPluginDetailPage.test.ts b/test/renderer/components/OfficialPluginDetailPage.test.ts
index 3c959c6804..18a5973369 100644
--- a/test/renderer/components/OfficialPluginDetailPage.test.ts
+++ b/test/renderer/components/OfficialPluginDetailPage.test.ts
@@ -21,16 +21,42 @@ const remoteSettingsStub = defineComponent({
name: 'RemoteSettings',
props: {
channel: { type: String, default: '' },
- hideChannelToggle: { type: Boolean, default: false }
+ hideChannelToggle: { type: Boolean, default: false },
+ hideHeader: { type: Boolean, default: false }
},
template:
- ''
+ ''
})
-async function mountFeishuDetail(options: { enabled?: boolean; remoteEnabled?: boolean } = {}) {
+const defaultFeishuSettings = (remoteEnabled: boolean) => ({
+ brand: 'feishu',
+ appId: 'cli_a',
+ appSecret: 'secret',
+ verificationToken: '',
+ encryptKey: '',
+ remoteEnabled,
+ defaultAgentId: 'feishu-bot',
+ defaultWorkdir: '',
+ pairedUserOpenIds: []
+})
+
+const defaultTelegramSettings = (remoteEnabled: boolean) => ({
+ botToken: 'token',
+ remoteEnabled,
+ defaultAgentId: 'telegram-bot',
+ defaultWorkdir: '',
+ allowedUserIds: []
+})
+
+async function mountDetail(
+ options: { enabled?: boolean; pluginId?: string; remoteEnabled?: boolean } = {}
+) {
vi.resetModules()
vi.clearAllMocks()
+ const pluginId = options.pluginId ?? 'com.deepchat.plugins.feishu'
+ const remoteChannel = pluginId.startsWith('remote:') ? pluginId.slice('remote:'.length) : 'feishu'
+ const remoteEnabled = options.remoteEnabled ?? false
const pluginClient = {
getPlugin: vi.fn().mockResolvedValue({
id: 'com.deepchat.plugins.feishu',
@@ -45,16 +71,20 @@ async function mountFeishuDetail(options: { enabled?: boolean; remoteEnabled?: b
disablePlugin: vi.fn().mockResolvedValue({ ok: true })
}
const remoteControlClient = {
- getChannelSettings: vi.fn().mockResolvedValue({
- brand: 'feishu',
- appId: 'cli_a',
- appSecret: 'secret',
- verificationToken: '',
- encryptKey: '',
- remoteEnabled: options.remoteEnabled ?? false,
- defaultAgentId: 'feishu-bot',
- defaultWorkdir: '',
- pairedUserOpenIds: []
+ getChannelSettings: vi
+ .fn()
+ .mockResolvedValue(
+ remoteChannel === 'telegram'
+ ? defaultTelegramSettings(remoteEnabled)
+ : defaultFeishuSettings(remoteEnabled)
+ ),
+ getChannelStatus: vi.fn().mockResolvedValue({
+ channel: remoteChannel,
+ enabled: remoteEnabled,
+ state: remoteEnabled ? 'running' : 'disabled',
+ bindingCount: 1,
+ allowedUserCount: 1,
+ lastError: null
}),
saveChannelSettings: vi.fn().mockResolvedValue({})
}
@@ -73,7 +103,7 @@ async function mountFeishuDetail(options: { enabled?: boolean; remoteEnabled?: b
return {
...actual,
useRoute: () => ({
- params: { pluginId: 'com.deepchat.plugins.feishu' }
+ params: { pluginId }
}),
useRouter: () => router
}
@@ -114,7 +144,7 @@ async function mountFeishuDetail(options: { enabled?: boolean; remoteEnabled?: b
describe('OfficialPluginDetailPage', () => {
it('uses the plugin enable button to start Feishu remote too', async () => {
- const { wrapper, pluginClient, remoteControlClient } = await mountFeishuDetail()
+ const { wrapper, pluginClient, remoteControlClient } = await mountDetail()
expect(wrapper.find('[data-testid="remote-settings"]').attributes('data-hide-toggle')).toBe(
'true'
@@ -134,7 +164,7 @@ describe('OfficialPluginDetailPage', () => {
})
it('uses the plugin disable button to stop Feishu remote too', async () => {
- const { wrapper, pluginClient, remoteControlClient } = await mountFeishuDetail({
+ const { wrapper, pluginClient, remoteControlClient } = await mountDetail({
enabled: true,
remoteEnabled: true
})
@@ -151,4 +181,46 @@ describe('OfficialPluginDetailPage', () => {
expect.objectContaining({ remoteEnabled: false })
)
})
+
+ it('uses the top detail button to start remote virtual plugins', async () => {
+ const { wrapper, pluginClient, remoteControlClient } = await mountDetail({
+ pluginId: 'remote:telegram'
+ })
+
+ expect(pluginClient.getPlugin).not.toHaveBeenCalled()
+ expect(wrapper.find('[data-testid="remote-settings"]').attributes()).toMatchObject({
+ 'data-channel': 'telegram',
+ 'data-hide-toggle': 'true',
+ 'data-hide-header': 'true'
+ })
+
+ await wrapper
+ .findAll('button')
+ .find((button) => button.text() === 'settings.plugins.enable')!
+ .trigger('click')
+ await flushPromises()
+
+ expect(remoteControlClient.saveChannelSettings).toHaveBeenCalledWith(
+ 'telegram',
+ expect.objectContaining({ remoteEnabled: true })
+ )
+ })
+
+ it('uses the top detail button to stop remote virtual plugins', async () => {
+ const { wrapper, remoteControlClient } = await mountDetail({
+ pluginId: 'remote:telegram',
+ remoteEnabled: true
+ })
+
+ await wrapper
+ .findAll('button')
+ .find((button) => button.text() === 'settings.plugins.disable')!
+ .trigger('click')
+ await flushPromises()
+
+ expect(remoteControlClient.saveChannelSettings).toHaveBeenCalledWith(
+ 'telegram',
+ expect.objectContaining({ remoteEnabled: false })
+ )
+ })
})
From 9a0b3db610aa80ff660b01cefa3314dda284dbb5 Mon Sep 17 00:00:00 2001
From: zerob13
Date: Fri, 26 Jun 2026 21:54:42 +0800
Subject: [PATCH 09/14] fix(plugins): localize feishu remote display
---
.../plugin-remote-icon-consistency/plan.md | 18 +++
.../plugin-remote-icon-consistency/spec.md | 30 +++++
.../plugin-remote-icon-consistency/tasks.md | 7 ++
.../plugins/OfficialPluginDetailPage.vue | 26 +++-
.../src/pages/plugins/PluginsCatalogPage.vue | 6 +-
.../OfficialPluginDetailPage.test.ts | 29 ++++-
.../components/PluginsCatalogPage.test.ts | 119 ++++++++++++++++++
7 files changed, 229 insertions(+), 6 deletions(-)
create mode 100644 docs/issues/plugin-remote-icon-consistency/plan.md
create mode 100644 docs/issues/plugin-remote-icon-consistency/spec.md
create mode 100644 docs/issues/plugin-remote-icon-consistency/tasks.md
create mode 100644 test/renderer/components/PluginsCatalogPage.test.ts
diff --git a/docs/issues/plugin-remote-icon-consistency/plan.md b/docs/issues/plugin-remote-icon-consistency/plan.md
new file mode 100644
index 0000000000..f9bb91e3f4
--- /dev/null
+++ b/docs/issues/plugin-remote-icon-consistency/plan.md
@@ -0,0 +1,18 @@
+# Plan
+
+## Implementation
+
+- Add the remote icon color map to `OfficialPluginDetailPage.vue`, matching `PluginsCatalogPage.vue`.
+- Use the Feishu remote icon/color for `com.deepchat.plugins.feishu` in the official plugin detail header.
+- Bind remote detail header icons to both icon name and color class.
+- Use the Feishu remote i18n title for the official Feishu plugin in catalog and detail headers.
+
+## Affected Interfaces
+
+- Renderer-only Vue template/computed state.
+- No IPC, presenter, or persisted data changes.
+
+## Test Strategy
+
+- Add focused renderer tests for Feishu official detail icons/titles and catalog title localization.
+- Run format, i18n, lint, and the focused renderer test.
diff --git a/docs/issues/plugin-remote-icon-consistency/spec.md b/docs/issues/plugin-remote-icon-consistency/spec.md
new file mode 100644
index 0000000000..d2c44bef53
--- /dev/null
+++ b/docs/issues/plugin-remote-icon-consistency/spec.md
@@ -0,0 +1,30 @@
+# Plugin Remote Detail Consistency
+
+## User Need
+
+Remote-control plugins should keep the same icon and localized title treatment when moving between the plugin list and detail page.
+
+## Goal
+
+Make remote virtual plugins and the official Feishu/Lark plugin use the same icon, color, and localized title shown by remote channel metadata.
+
+## Acceptance Criteria
+
+- Feishu/Lark catalog and detail both show the message-circle icon with the blue remote color.
+- Remote virtual plugin details keep their catalog icon color.
+- In Chinese, Feishu/Lark keeps the localized `飞书 / Lark` title instead of flashing to the plugin manifest name.
+- Non-remote official plugins still use the generic puzzle icon.
+
+## Constraints
+
+- Keep the fix in the renderer plugin detail page.
+- Do not change plugin manifest data or remote-control settings behavior.
+
+## Non-Goals
+
+- Redesign the plugin hub layout.
+- Add new icon configuration infrastructure.
+
+## Open Questions
+
+- None.
diff --git a/docs/issues/plugin-remote-icon-consistency/tasks.md b/docs/issues/plugin-remote-icon-consistency/tasks.md
new file mode 100644
index 0000000000..0df51f32f4
--- /dev/null
+++ b/docs/issues/plugin-remote-icon-consistency/tasks.md
@@ -0,0 +1,7 @@
+# Tasks
+
+- [x] Inspect plugin list/detail icon rendering.
+- [x] Update detail header icon bindings.
+- [x] Add focused tests.
+- [x] Run required checks.
+- [x] Keep Feishu official plugin titles localized.
diff --git a/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue b/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue
index c41af27401..c99318e1df 100644
--- a/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue
+++ b/src/renderer/src/pages/plugins/OfficialPluginDetailPage.vue
@@ -29,7 +29,7 @@
-
+
@@ -128,10 +128,10 @@
-
+
-
{{ plugin.name }}
+
{{ pluginTitle }}
{{ plugin.publisher }} · {{ plugin.id }}
@@ -307,6 +307,13 @@ const remoteIconByChannel: Record
= {
discord: 'lucide:radio-tower',
'weixin-ilink': 'lucide:messages-square'
}
+const remoteIconClassByChannel: Record = {
+ telegram: 'text-sky-500',
+ feishu: 'text-blue-500',
+ qqbot: 'text-emerald-500',
+ discord: 'text-indigo-500',
+ 'weixin-ilink': 'text-green-500'
+}
const pluginId = computed(() => String(route.params.pluginId ?? ''))
const remoteChannel = computed(() => {
@@ -334,6 +341,19 @@ const remoteIcon = computed(() => {
const channel = remoteChannel.value
return channel ? remoteIconByChannel[channel] : 'lucide:puzzle'
})
+const remoteIconClass = computed(() => {
+ const channel = remoteChannel.value
+ return channel ? remoteIconClassByChannel[channel] : undefined
+})
+const pluginIcon = computed(() =>
+ isFeishuPlugin.value ? remoteIconByChannel.feishu : 'lucide:puzzle'
+)
+const pluginIconClass = computed(() =>
+ isFeishuPlugin.value ? remoteIconClassByChannel.feishu : undefined
+)
+const pluginTitle = computed(() =>
+ isFeishuPlugin.value ? t('settings.remote.feishu.title') : (plugin.value?.name ?? '')
+)
function formatRuntimeState(state?: PluginRuntimeState): string {
if (!state) {
diff --git a/src/renderer/src/pages/plugins/PluginsCatalogPage.vue b/src/renderer/src/pages/plugins/PluginsCatalogPage.vue
index de8cf9fa34..b66bc6d0e9 100644
--- a/src/renderer/src/pages/plugins/PluginsCatalogPage.vue
+++ b/src/renderer/src/pages/plugins/PluginsCatalogPage.vue
@@ -247,6 +247,8 @@ const searchQuery = ref('')
const activeFilter = ref('official')
const isPending = (itemId: string) => pendingItemId.value === itemId
+const pluginTitle = (plugin: PluginListItem): string =>
+ isFeishuOfficialPlugin(plugin) ? t('settings.remote.feishu.title') : plugin.name
const implementedRemoteChannels = computed(() =>
remoteChannels.value.filter((channel) => channel.implemented)
@@ -263,7 +265,7 @@ const addedItems = computed(() => {
id: `official:${plugin.id}`,
kind: 'official' as const,
pluginId: plugin.id,
- title: plugin.name,
+ title: pluginTitle(plugin),
icon: isFeishuOfficialPlugin(plugin) ? remoteIconByChannel.feishu : 'lucide:puzzle',
iconClass: isFeishuOfficialPlugin(plugin)
? remoteIconClassByChannel.feishu
@@ -290,7 +292,7 @@ const catalogItems = computed(() => {
id: `official:${plugin.id}`,
kind: 'official' as const,
plugin,
- title: plugin.name,
+ title: pluginTitle(plugin),
description: plugin.publisher,
badge: plugin.enabled
? t('settings.plugins.status.enabled')
diff --git a/test/renderer/components/OfficialPluginDetailPage.test.ts b/test/renderer/components/OfficialPluginDetailPage.test.ts
index 18a5973369..7b01eec038 100644
--- a/test/renderer/components/OfficialPluginDetailPage.test.ts
+++ b/test/renderer/components/OfficialPluginDetailPage.test.ts
@@ -48,6 +48,9 @@ const defaultTelegramSettings = (remoteEnabled: boolean) => ({
allowedUserIds: []
})
+const findIcon = (wrapper: ReturnType, icon: string) =>
+ wrapper.find(`[data-icon="${icon}"], [icon="${icon}"]`)
+
async function mountDetail(
options: { enabled?: boolean; pluginId?: string; remoteEnabled?: boolean } = {}
) {
@@ -116,7 +119,10 @@ async function mountDetail(
vi.doMock('@iconify/vue', () => ({
Icon: defineComponent({
name: 'Icon',
- template: ''
+ props: {
+ icon: { type: String, required: true }
+ },
+ template: ''
})
}))
vi.doMock('../../../src/renderer/settings/components/RemoteSettings.vue', () => ({
@@ -143,6 +149,27 @@ async function mountDetail(
}
describe('OfficialPluginDetailPage', () => {
+ it('uses the Feishu remote icon on the official plugin detail header', async () => {
+ const { wrapper } = await mountDetail()
+
+ const icon = findIcon(wrapper, 'lucide:message-circle')
+
+ expect(icon.exists()).toBe(true)
+ expect(icon.classes()).toContain('text-blue-500')
+ expect(findIcon(wrapper, 'lucide:puzzle').exists()).toBe(false)
+ expect(wrapper.text()).toContain('settings.remote.feishu.title')
+ expect(wrapper.text()).not.toContain('Feishu/Lark Integration')
+ })
+
+ it('uses the remote channel icon color on remote virtual plugin details', async () => {
+ const { wrapper } = await mountDetail({ pluginId: 'remote:telegram' })
+
+ const icon = findIcon(wrapper, 'lucide:send')
+
+ expect(icon.exists()).toBe(true)
+ expect(icon.classes()).toContain('text-sky-500')
+ })
+
it('uses the plugin enable button to start Feishu remote too', async () => {
const { wrapper, pluginClient, remoteControlClient } = await mountDetail()
diff --git a/test/renderer/components/PluginsCatalogPage.test.ts b/test/renderer/components/PluginsCatalogPage.test.ts
new file mode 100644
index 0000000000..9cddcb1489
--- /dev/null
+++ b/test/renderer/components/PluginsCatalogPage.test.ts
@@ -0,0 +1,119 @@
+import { describe, expect, it, vi } from 'vitest'
+import { defineComponent } from 'vue'
+import { flushPromises, shallowMount } from '@vue/test-utils'
+
+const passthrough = (name: string) =>
+ defineComponent({
+ name,
+ template: '
'
+ })
+
+const buttonStub = defineComponent({
+ name: 'Button',
+ props: {
+ disabled: { type: Boolean, default: false }
+ },
+ emits: ['click'],
+ template: ''
+})
+
+const inputStub = defineComponent({
+ name: 'Input',
+ props: {
+ modelValue: { type: String, default: '' }
+ },
+ emits: ['update:modelValue'],
+ template: ''
+})
+
+async function mountCatalog() {
+ vi.resetModules()
+ vi.clearAllMocks()
+
+ const pluginClient = {
+ listPlugins: vi.fn().mockResolvedValue([
+ {
+ id: 'com.deepchat.plugins.feishu',
+ name: 'Feishu/Lark Integration',
+ publisher: 'DeepChat',
+ version: '1.0.4',
+ enabled: false,
+ capabilities: [],
+ mcpServers: []
+ }
+ ]),
+ enablePlugin: vi.fn().mockResolvedValue({ ok: true })
+ }
+ const remoteControlClient = {
+ listRemoteChannels: vi.fn().mockResolvedValue([
+ {
+ id: 'feishu',
+ type: 'builtin',
+ implemented: true,
+ titleKey: 'settings.remote.feishu.title',
+ descriptionKey: 'settings.remote.feishu.description',
+ supportsPairing: true,
+ supportsNotifications: false
+ }
+ ]),
+ getChannelStatus: vi.fn().mockResolvedValue({
+ channel: 'feishu',
+ enabled: false,
+ state: 'disabled',
+ bindingCount: 0,
+ pairedUserCount: 0,
+ lastError: null
+ })
+ }
+ const router = {
+ push: vi.fn()
+ }
+
+ vi.doMock('@api/PluginClient', () => ({
+ createPluginClient: () => pluginClient
+ }))
+ vi.doMock('@api/RemoteControlClient', () => ({
+ createRemoteControlClient: () => remoteControlClient
+ }))
+ vi.doMock('vue-router', () => ({
+ RouterLink: passthrough('RouterLink'),
+ useRouter: () => router
+ }))
+ vi.doMock('vue-i18n', () => ({
+ useI18n: () => ({
+ t: (key: string) => key
+ })
+ }))
+ vi.doMock('@iconify/vue', () => ({
+ Icon: defineComponent({
+ name: 'Icon',
+ props: {
+ icon: { type: String, required: true }
+ },
+ template: ''
+ })
+ }))
+
+ const PluginsCatalogPage = (await import('@/pages/plugins/PluginsCatalogPage.vue')).default
+ const wrapper = shallowMount(PluginsCatalogPage, {
+ global: {
+ stubs: {
+ Button: buttonStub,
+ Input: inputStub,
+ ScrollArea: passthrough('ScrollArea')
+ }
+ }
+ })
+ await flushPromises()
+
+ return { wrapper, pluginClient, remoteControlClient }
+}
+
+describe('PluginsCatalogPage', () => {
+ it('keeps the Feishu official plugin title localized after catalog load', async () => {
+ const { wrapper } = await mountCatalog()
+
+ expect(wrapper.text()).toContain('settings.remote.feishu.title')
+ expect(wrapper.text()).not.toContain('Feishu/Lark Integration')
+ })
+})
From dae71d9b406d7b182979010186dfc510c12b8336 Mon Sep 17 00:00:00 2001
From: zerob13
Date: Mon, 29 Jun 2026 12:46:47 +0800
Subject: [PATCH 10/14] fix(sidebar): separate chat workspace
---
.../sidebar-chat-workspace-sort/plan.md | 20 ++
.../sidebar-chat-workspace-sort/spec.md | 52 +++++
.../sidebar-chat-workspace-sort/tasks.md | 8 +
src/renderer/src/components/WindowSideBar.vue | 202 ++++++++++--------
.../renderer/components/WindowSideBar.test.ts | 128 ++++++++++-
5 files changed, 312 insertions(+), 98 deletions(-)
create mode 100644 docs/issues/sidebar-chat-workspace-sort/plan.md
create mode 100644 docs/issues/sidebar-chat-workspace-sort/spec.md
create mode 100644 docs/issues/sidebar-chat-workspace-sort/tasks.md
diff --git a/docs/issues/sidebar-chat-workspace-sort/plan.md b/docs/issues/sidebar-chat-workspace-sort/plan.md
new file mode 100644
index 0000000000..3d5e735095
--- /dev/null
+++ b/docs/issues/sidebar-chat-workspace-sort/plan.md
@@ -0,0 +1,20 @@
+# Sidebar Chat Workspace Sort Plan
+
+## Approach
+
+- Keep using `sessionStore.groupMode` as the Workspace grouping mode.
+- In `WindowSideBar.vue`, flatten the already filtered non-pinned sessions, split them by project
+ path into Chat and Workspace, then render each section separately.
+- Make the Chat section header use the existing collapsed group set.
+- Use the same chat icon name as the new-thread project selector for the Chat section header.
+- Update existing sidebar tests for Chat collapse and date-mode Workspace scoping.
+
+## Affected Files
+
+- `src/renderer/src/components/WindowSideBar.vue`
+- `test/renderer/components/WindowSideBar.test.ts`
+
+## Test Strategy
+
+- Run the targeted WindowSideBar test file.
+- Run project formatting, i18n, and lint checks required by repository instructions.
diff --git a/docs/issues/sidebar-chat-workspace-sort/spec.md b/docs/issues/sidebar-chat-workspace-sort/spec.md
new file mode 100644
index 0000000000..4b7181b139
--- /dev/null
+++ b/docs/issues/sidebar-chat-workspace-sort/spec.md
@@ -0,0 +1,52 @@
+# Sidebar Chat Workspace Sort Spec
+
+## User Need
+
+The expanded sidebar must keep Chat and Workspace as separate sections. The Workspace sort toggle
+must not move workspace sessions into Chat or make the Workspace section disappear.
+
+## Goal
+
+- Chat sessions remain under Chat and can be collapsed.
+- Workspace sessions remain under Workspace.
+- The Workspace toggle only changes Workspace grouping between project/date modes.
+
+## Acceptance Criteria
+
+- Clicking Chat collapses and expands Chat sessions.
+- The Chat section icon matches the chat icon used by the new-thread project selector.
+- In date mode, date groups for workspace sessions render under Workspace, not Chat.
+- Workspace stays visible in the same sidebar position after toggling grouping.
+- Pinned sessions stay independent.
+
+## Constraints
+
+- Keep the fix local to the renderer sidebar.
+- Do not add dependencies or new persistent settings.
+- Do not touch unrelated Skills work already dirty in the worktree.
+
+## Non-Goals
+
+- Redesign the sidebar.
+- Change session storage, pagination, or pin behavior.
+
+## UI Shape
+
+Before:
+
+```text
+Pinned
+Chat
+ Recent / Earlier workspace groups
+Workspace
+```
+
+After:
+
+```text
+Pinned
+Chat [collapsible]
+ chat sessions only
+Workspace [project/date toggle]
+ workspace groups only
+```
diff --git a/docs/issues/sidebar-chat-workspace-sort/tasks.md b/docs/issues/sidebar-chat-workspace-sort/tasks.md
new file mode 100644
index 0000000000..cbc71055bc
--- /dev/null
+++ b/docs/issues/sidebar-chat-workspace-sort/tasks.md
@@ -0,0 +1,8 @@
+# Sidebar Chat Workspace Sort Tasks
+
+- [x] Add SDD issue docs.
+- [x] Split Chat and Workspace rendering in `WindowSideBar.vue`.
+- [x] Restore Chat collapse behavior.
+- [x] Add/update sidebar tests.
+- [x] Match the Chat section icon to the new-thread chat selector icon.
+- [x] Run validation commands.
diff --git a/src/renderer/src/components/WindowSideBar.vue b/src/renderer/src/components/WindowSideBar.vue
index 2d73d2e308..cd6a6d1bd6 100644
--- a/src/renderer/src/components/WindowSideBar.vue
+++ b/src/renderer/src/components/WindowSideBar.vue
@@ -215,7 +215,8 @@
v-if="
sessionStore.hasLoadedInitialPage &&
pinnedSessions.length === 0 &&
- filteredGroups.length === 0
+ !chatSectionGroup &&
+ workspaceGroups.length === 0
"
class="flex flex-col items-center justify-center h-full px-4 text-center"
>
@@ -280,57 +281,46 @@
-
-
- {{ t('chat.sidebar.chatSection') }}
-
-
-
-
-
-
-
-
-
+
-
+
+
+ {{ t('chat.sidebar.chatSection') }}
+
+
+
+
+
+
@@ -554,6 +544,8 @@ const PIN_TARGET_SETTLE_MAX_FRAMES = 10
const PIN_TARGET_SETTLE_EPSILON_PX = 0.5
const SIDEBAR_SHORTCUT_BADGE_DELAY_MS = 500
const SIDEBAR_SHORTCUT_MAX_ROWS = 10
+const CHAT_SECTION_GROUP_ID = '__chat__'
+const CHAT_SECTION_ICON = 'lucide:message-square'
const NO_PROJECT_GROUP_ID = '__no_project__'
const getPinFeedbackMode = (nextPinned: boolean): PinFeedbackMode =>
nextPinned ? 'pinning' : 'unpinning'
@@ -786,7 +778,6 @@ const remoteControlIconClass = computed(() => {
const isPinnedSectionCollapsed = ref(false)
const collapsedGroupIds = ref
>(new Set())
-const defaultCollapsedChatGroupIds = ref>(new Set())
const normalizedSessionSearchQuery = computed(() => sessionSearchQuery.value.trim().toLowerCase())
const matchesSessionSearch = (session: UISession) => {
if (!normalizedSessionSearchQuery.value) {
@@ -827,23 +818,26 @@ const normalizeProjectPath = (projectPath: string | null | undefined) =>
const defaultChatWorkspacePath = computed(() =>
normalizeProjectPath(projectStore.defaultChatWorkspacePath)
)
-const isChatsGroup = (group: SessionGroup) =>
- sessionStore.groupMode === 'project' &&
- (group.id === NO_PROJECT_GROUP_ID ||
- (defaultChatWorkspacePath.value.length > 0 &&
- normalizeProjectPath(group.id) === defaultChatWorkspacePath.value))
+const isChatSession = (session: UISession) => {
+ const projectPath = normalizeProjectPath(session.projectDir)
+ return (
+ projectPath.length === 0 ||
+ (defaultChatWorkspacePath.value.length > 0 && projectPath === defaultChatWorkspacePath.value)
+ )
+}
+const isWorkspaceSession = (session: UISession) => !isChatSession(session)
+const isChatProjectGroup = (group: SessionGroup) =>
+ group.id === NO_PROJECT_GROUP_ID ||
+ (defaultChatWorkspacePath.value.length > 0 &&
+ normalizeProjectPath(group.id) === defaultChatWorkspacePath.value)
const isProjectDirectoryGroup = (group: SessionGroup) =>
sessionStore.groupMode === 'project' &&
group.id !== NO_PROJECT_GROUP_ID &&
!group.labelKey &&
- !isChatsGroup(group)
+ !isChatProjectGroup(group)
const isActiveProjectDirectoryGroup = (group: SessionGroup) =>
isProjectDirectoryGroup(group) && !archivedProjectPathSet.value.has(group.id)
const getProjectGroupRank = (group: SessionGroup) => {
- if (isChatsGroup(group)) {
- return -1
- }
-
if (!isProjectDirectoryGroup(group)) {
return 2
}
@@ -883,10 +877,56 @@ const filteredGroups = computed(() => {
)
.map(({ group }) => group)
})
-const chatGroups = computed(() =>
- filteredGroups.value.filter((group) => !isProjectDirectoryGroup(group))
+const compareSidebarSessions = (left: UISession, right: UISession) => {
+ const leftUpdatedAt = Number.isFinite(left.updatedAt) ? left.updatedAt : 0
+ const rightUpdatedAt = Number.isFinite(right.updatedAt) ? right.updatedAt : 0
+ if (leftUpdatedAt !== rightUpdatedAt) {
+ return rightUpdatedAt - leftUpdatedAt
+ }
+
+ return left.title.localeCompare(right.title) || left.id.localeCompare(right.id)
+}
+const sortSidebarSessions = (sessions: UISession[]) => [...sessions].sort(compareSidebarSessions)
+const chatSessions = computed(() =>
+ sortSidebarSessions(
+ baseFilteredGroups.value.flatMap((group) => {
+ if (sessionStore.groupMode === 'project') {
+ return isChatProjectGroup(group) ? group.sessions : []
+ }
+
+ return group.sessions.filter(isChatSession)
+ })
+ )
)
-const workspaceGroups = computed(() => filteredGroups.value.filter(isProjectDirectoryGroup))
+const chatSectionGroup = computed(() => {
+ const sessions = chatSessions.value
+ if (sessions.length === 0) {
+ return null
+ }
+
+ return {
+ id: CHAT_SECTION_GROUP_ID,
+ label: 'chat.sidebar.chats',
+ labelKey: 'chat.sidebar.chats',
+ sessions
+ }
+})
+const workspaceGroups = computed(() => {
+ if (sessionStore.groupMode === 'project') {
+ return filteredGroups.value.filter(isProjectDirectoryGroup)
+ }
+
+ return baseFilteredGroups.value
+ .map((group) => ({
+ ...group,
+ sessions: sortSidebarSessions(group.sessions.filter(isWorkspaceSession))
+ }))
+ .filter((group) => group.sessions.length > 0)
+})
+const visibleGroups = computed(() => [
+ ...(chatSectionGroup.value ? [chatSectionGroup.value] : []),
+ ...workspaceGroups.value
+])
const projectReorderableGroups = computed(() =>
workspaceGroups.value.filter(isActiveProjectDirectoryGroup)
)
@@ -915,15 +955,9 @@ const deleteDialogOpen = computed({
const getGroupIdentifier = (group: SessionGroup) => group.id
-const getGroupLabel = (group: SessionGroup) =>
- isChatsGroup(group) ? t('chat.sidebar.chats') : group.labelKey ? t(group.labelKey) : group.label
-const getGroupIcon = (group: SessionGroup) => {
- if (isChatsGroup(group)) {
- return 'lucide:message-square'
- }
-
- return isGroupCollapsed(group) ? 'lucide:folder-closed' : 'lucide:folder-open'
-}
+const getGroupLabel = (group: SessionGroup) => (group.labelKey ? t(group.labelKey) : group.label)
+const getGroupIcon = (group: SessionGroup) =>
+ isGroupCollapsed(group) ? 'lucide:folder-closed' : 'lucide:folder-open'
const isGroupCollapsed = (group: SessionGroup) =>
collapsedGroupIds.value.has(getGroupIdentifier(group))
@@ -939,7 +973,7 @@ const visibleShortcutSessions = computed(() => {
sessions.push(...pinnedSessions.value)
}
- for (const group of filteredGroups.value) {
+ for (const group of visibleGroups.value) {
if (!isGroupCollapsed(group)) {
sessions.push(...group.sessions)
}
@@ -1125,7 +1159,7 @@ watch(
)
watch(
- [filteredGroups, () => sessionStore.activeSessionId],
+ [visibleGroups, () => sessionStore.activeSessionId],
([groups, activeSessionId]) => {
if (isProjectGroupDragging.value) {
return
@@ -1141,23 +1175,11 @@ watch(
group.sessions.some((session) => session.id === activeSessionId)
)
- if (activeGroup && !isChatsGroup(activeGroup)) {
+ if (activeGroup) {
nextCollapsedGroupIds.delete(getGroupIdentifier(activeGroup))
}
}
- const nextDefaultCollapsedChatGroupIds = new Set(
- [...defaultCollapsedChatGroupIds.value].filter((groupId) => validGroupIds.has(groupId))
- )
- for (const group of groups) {
- const groupId = getGroupIdentifier(group)
- if (isChatsGroup(group) && !nextDefaultCollapsedChatGroupIds.has(groupId)) {
- nextCollapsedGroupIds.add(groupId)
- nextDefaultCollapsedChatGroupIds.add(groupId)
- }
- }
- defaultCollapsedChatGroupIds.value = nextDefaultCollapsedChatGroupIds
-
const stateChanged =
nextCollapsedGroupIds.size !== collapsedGroupIds.value.size ||
[...nextCollapsedGroupIds].some((groupId) => !collapsedGroupIds.value.has(groupId))
@@ -1591,7 +1613,7 @@ const visibleSessionFingerprint = computed(() =>
[
isPinnedSectionCollapsed.value ? 'pinned:collapsed' : 'pinned:expanded',
...pinnedSessions.value.map((session) => `pinned:${session.id}`),
- ...filteredGroups.value.flatMap((group) => [
+ ...visibleGroups.value.flatMap((group) => [
`group:${getGroupIdentifier(group)}:${isGroupCollapsed(group) ? 'collapsed' : 'expanded'}`,
...(!isGroupCollapsed(group) ? group.sessions.map((session) => session.id) : [])
])
diff --git a/test/renderer/components/WindowSideBar.test.ts b/test/renderer/components/WindowSideBar.test.ts
index 1c9cc426d1..015df391ec 100644
--- a/test/renderer/components/WindowSideBar.test.ts
+++ b/test/renderer/components/WindowSideBar.test.ts
@@ -19,7 +19,14 @@ type SetupOptions = {
id: string
label: string
labelKey?: string
- sessions: Array<{ id: string; title: string; status: string; isPinned?: boolean }>
+ sessions: Array<{
+ id: string
+ title: string
+ status: string
+ isPinned?: boolean
+ projectDir?: string
+ updatedAt?: number
+ }>
}>
remoteStatus?: {
enabled: boolean
@@ -594,7 +601,8 @@ describe('WindowSideBar agent switch', () => {
{
id: 'normal-1',
title: 'Normal Session',
- status: 'none'
+ status: 'none',
+ projectDir: '/work/today'
}
]
}
@@ -642,6 +650,53 @@ describe('WindowSideBar agent switch', () => {
TEST_TIMEOUT_MS
)
+ it(
+ 'collapses and expands chat sessions from the chat header',
+ async () => {
+ const { wrapper } = await setup({
+ groupMode: 'project',
+ groups: [
+ {
+ id: '__no_project__',
+ label: 'No Project',
+ labelKey: 'common.project.none',
+ sessions: [
+ {
+ id: 'chat-1',
+ title: 'Chat Session',
+ status: 'none'
+ }
+ ]
+ }
+ ]
+ })
+
+ await wrapper.vm.$nextTick()
+
+ expect(wrapper.text()).toContain('chat.sidebar.chatSection')
+ expect(wrapper.get('[data-testid="window-sidebar-chat-icon"]').attributes('data-icon')).toBe(
+ 'lucide:message-square'
+ )
+ expect(wrapper.get('[data-session-id="chat-1"]').isVisible()).toBe(true)
+
+ await wrapper.find('[data-group-id="__chat__"]').trigger('click')
+ await wrapper.vm.$nextTick()
+
+ expect(wrapper.get('[data-group-id="__chat__"]').attributes('aria-expanded')).toBe('false')
+ expect(
+ (wrapper.get('[data-group-id="__chat__"]').element.nextElementSibling as HTMLElement).style
+ .display
+ ).toBe('none')
+
+ await wrapper.find('[data-group-id="__chat__"]').trigger('click')
+ await wrapper.vm.$nextTick()
+
+ expect(wrapper.get('[data-group-id="__chat__"]').attributes('aria-expanded')).toBe('true')
+ expect(wrapper.get('[data-session-id="chat-1"]').isVisible()).toBe(true)
+ },
+ TEST_TIMEOUT_MS
+ )
+
it(
'toggles pinned state from a session item action',
async () => {
@@ -770,7 +825,8 @@ describe('WindowSideBar agent switch', () => {
{
id: 'group-1',
title: 'Group Session 1',
- status: 'none'
+ status: 'none',
+ projectDir: '/work/today'
}
]
}
@@ -805,7 +861,8 @@ describe('WindowSideBar agent switch', () => {
{
id: 'group-1',
title: 'Group Session 1',
- status: 'none'
+ status: 'none',
+ projectDir: '/work/today'
}
]
}
@@ -1148,7 +1205,8 @@ describe('WindowSideBar agent switch', () => {
{
id: 'time-1',
title: 'Today Session',
- status: 'none'
+ status: 'none',
+ projectDir: '/work/today'
}
]
}
@@ -1394,9 +1452,10 @@ describe('WindowSideBar agent switch', () => {
expect(wrapper.text()).toContain('chat.sidebar.chatSection')
expect(wrapper.text()).toContain('chat.sidebar.workspace')
+ expect(wrapper.text()).toContain('Default Chat Session')
expect(
wrapper.findAll('button[data-group-id]').map((button) => button.attributes('data-group-id'))
- ).toEqual(['__pinned__', '/work/alpha', '/work/beta'])
+ ).toEqual(['__pinned__', '__chat__', '/work/alpha', '/work/beta'])
expect(wrapper.findAll('[aria-label="chat.sidebar.projectGroupActions"]')).toHaveLength(2)
wrapper
@@ -1462,6 +1521,7 @@ describe('WindowSideBar agent switch', () => {
expect(wrapper.text()).toContain('chat.sidebar.workspace')
expect(wrapper.text()).not.toContain('common.project.none')
expect(wrapper.find('[data-group-id="__no_project__"]').exists()).toBe(false)
+ expect(wrapper.find('[data-group-id="__chat__"]').exists()).toBe(true)
expect(wrapper.findAll('[aria-label="chat.sidebar.projectGroupActions"]')).toHaveLength(2)
wrapper
@@ -1474,6 +1534,58 @@ describe('WindowSideBar agent switch', () => {
TEST_TIMEOUT_MS
)
+ it(
+ 'keeps date grouping scoped to workspace sessions',
+ async () => {
+ const { wrapper } = await setup({
+ groupMode: 'time',
+ groups: [
+ {
+ id: 'common.time.lastWeek',
+ label: 'common.time.lastWeek',
+ labelKey: 'common.time.lastWeek',
+ sessions: [
+ {
+ id: 'chat-1',
+ title: 'Chat Session',
+ status: 'none',
+ projectDir: '',
+ updatedAt: 200
+ },
+ {
+ id: 'workspace-1',
+ title: 'Workspace Session',
+ status: 'none',
+ projectDir: '/work/alpha',
+ updatedAt: 100
+ }
+ ]
+ }
+ ]
+ })
+
+ await wrapper.vm.$nextTick()
+
+ expect(
+ wrapper.findAll('button[data-group-id]').map((button) => button.attributes('data-group-id'))
+ ).toEqual(['__chat__', 'common.time.lastWeek'])
+ expect(
+ wrapper
+ .findAll('[data-testid="sidebar-session-item"]')
+ .map((item) => item.attributes('data-session-id'))
+ ).toEqual(['chat-1', 'workspace-1'])
+
+ const html = wrapper.html()
+ expect(html.indexOf('data-group-id="__chat__"')).toBeLessThan(
+ html.indexOf('data-session-id="chat-1"')
+ )
+ expect(html.indexOf('chat.sidebar.workspace')).toBeLessThan(
+ html.indexOf('data-group-id="common.time.lastWeek"')
+ )
+ },
+ TEST_TIMEOUT_MS
+ )
+
it('does not render the chats group when it has no sessions', async () => {
const { wrapper } = await setup({
groupMode: 'project',
@@ -1729,8 +1841,8 @@ describe('WindowSideBar viewport auto-fill', () => {
label: 'common.time.today',
labelKey: 'common.time.today',
sessions: [
- { id: 'session-1', title: 'Alpha', status: 'none' },
- { id: 'session-2', title: 'Bravo', status: 'none' }
+ { id: 'session-1', title: 'Alpha', status: 'none', projectDir: '/work/today' },
+ { id: 'session-2', title: 'Bravo', status: 'none', projectDir: '/work/today' }
]
}
],
From 8832f0435426f01046fe5c6d79c13b254ad085d2 Mon Sep 17 00:00:00 2001
From: zerob13
Date: Mon, 29 Jun 2026 12:50:15 +0800
Subject: [PATCH 11/14] feat(skills): manage agent skill links
---
.../deepchat-skills-management/plan.md | 643 ++++++++++++
.../deepchat-skills-management/spec.md | 736 ++++++++++++++
.../deepchat-skills-management/tasks.md | 189 ++++
.../configPresenter/configDbStores.ts | 3 +-
src/main/presenter/skillPresenter/index.ts | 945 +++++++++++++++++-
.../presenter/skillSyncPresenter/index.ts | 809 ++++++++++++++-
src/main/routes/index.ts | 148 +++
src/renderer/api/SkillClient.ts | 87 +-
src/renderer/api/SkillSyncClient.ts | 89 ++
.../components/skills/AdoptSkillDialog.vue | 188 ++++
.../components/skills/AgentSkillTable.vue | 131 +++
.../skills/InstallFromGitDialog.vue | 251 +++++
.../skills/InstallSkillToAgentDialog.vue | 305 ++++++
.../components/skills/SkillAgentsTab.vue | 351 +++++++
.../settings/components/skills/SkillCard.vue | 64 +-
.../components/skills/SkillDetailDialog.vue | 332 ++++++
.../components/skills/SkillEditorSheet.vue | 566 -----------
.../skills/SkillImportExportTab.vue | 372 +++++++
.../components/skills/SkillsSettings.vue | 396 +++++---
src/renderer/src/i18n/da-DK/settings.json | 215 +++-
src/renderer/src/i18n/de-DE/settings.json | 215 +++-
src/renderer/src/i18n/en-US/settings.json | 215 +++-
src/renderer/src/i18n/es-ES/settings.json | 215 +++-
src/renderer/src/i18n/fa-IR/settings.json | 215 +++-
src/renderer/src/i18n/fr-FR/settings.json | 215 +++-
src/renderer/src/i18n/he-IL/settings.json | 215 +++-
src/renderer/src/i18n/id-ID/settings.json | 215 +++-
src/renderer/src/i18n/it-IT/settings.json | 215 +++-
src/renderer/src/i18n/ja-JP/settings.json | 215 +++-
src/renderer/src/i18n/ko-KR/settings.json | 215 +++-
src/renderer/src/i18n/ms-MY/settings.json | 215 +++-
src/renderer/src/i18n/pl-PL/settings.json | 215 +++-
src/renderer/src/i18n/pt-BR/settings.json | 215 +++-
src/renderer/src/i18n/ru-RU/settings.json | 215 +++-
src/renderer/src/i18n/tr-TR/settings.json | 215 +++-
src/renderer/src/i18n/vi-VN/settings.json | 215 +++-
src/renderer/src/i18n/zh-CN/settings.json | 215 +++-
src/renderer/src/i18n/zh-HK/settings.json | 215 +++-
src/renderer/src/i18n/zh-TW/settings.json | 215 +++-
src/renderer/src/stores/skillsStore.ts | 14 +-
src/shared/contracts/events/skills.events.ts | 11 +-
src/shared/contracts/routes.ts | 38 +
.../contracts/routes/skillSync.routes.ts | 108 ++
src/shared/contracts/routes/skills.routes.ts | 112 +++
src/shared/types/skill.ts | 103 ++
src/shared/types/skillManagement.ts | 73 ++
src/shared/types/skillSync.ts | 152 +++
.../skillPresenter/skillPresenter.test.ts | 500 +++++++--
.../skillSyncPresenter/index.test.ts | 517 +++++++++-
test/main/routes/contracts.test.ts | 63 ++
test/renderer/api/clients.test.ts | 316 +++++-
.../components/SkillEditorSheet.test.ts | 139 ---
.../components/SkillSyncSettings.test.ts | 929 ++++++++++++++++-
53 files changed, 12909 insertions(+), 1071 deletions(-)
create mode 100644 docs/features/deepchat-skills-management/plan.md
create mode 100644 docs/features/deepchat-skills-management/spec.md
create mode 100644 docs/features/deepchat-skills-management/tasks.md
create mode 100644 src/renderer/settings/components/skills/AdoptSkillDialog.vue
create mode 100644 src/renderer/settings/components/skills/AgentSkillTable.vue
create mode 100644 src/renderer/settings/components/skills/InstallFromGitDialog.vue
create mode 100644 src/renderer/settings/components/skills/InstallSkillToAgentDialog.vue
create mode 100644 src/renderer/settings/components/skills/SkillAgentsTab.vue
create mode 100644 src/renderer/settings/components/skills/SkillDetailDialog.vue
delete mode 100644 src/renderer/settings/components/skills/SkillEditorSheet.vue
create mode 100644 src/renderer/settings/components/skills/SkillImportExportTab.vue
create mode 100644 src/shared/types/skillManagement.ts
delete mode 100644 test/renderer/components/SkillEditorSheet.test.ts
diff --git a/docs/features/deepchat-skills-management/plan.md b/docs/features/deepchat-skills-management/plan.md
new file mode 100644
index 0000000000..98f7ec3f41
--- /dev/null
+++ b/docs/features/deepchat-skills-management/plan.md
@@ -0,0 +1,643 @@
+# DeepChat Skills Management Implementation Plan
+
+## Architecture Fit
+
+Use the existing split:
+
+- Main runtime owner: `src/main/presenter/skillPresenter/index.ts`
+- External scan/conversion owner: `src/main/presenter/skillSyncPresenter/index.ts`
+- Shared types: `src/shared/types/*`
+- Route contracts: `src/shared/contracts/routes/*`
+- Route dispatch: `src/main/routes/index.ts`
+- Renderer API clients: `src/renderer/api/*Client.ts`
+- Settings UI: `src/renderer/settings/components/skills/*`
+
+Do not create a new top-level Presenter for V1. Add small helper modules under the existing
+presenter folders where code size requires it.
+
+## Current Gaps
+
+| Gap | Current state | Needed change |
+| --- | --- | --- |
+| Database state | Runtime extension settings currently live in per-skill files under `.deepchat-meta/.json`. | Move skill management state into the application database and treat `.deepchat-meta` as legacy migration input. |
+| Library disabled state | `getMetadataList()` and `getMetadataPrompt()` expose all visible skills. | Add a Library catalog that includes disabled skills, and filter disabled skills from runtime paths. |
+| Agent ownership | `SkillSyncPresenter` scans external tools but does not classify links or ownership. | Add user-level folder-format agent management scan/classification. |
+| Adoption | Existing import copies external skills into DeepChat, but does not move agent-owned folders or create links. | Add adopt preview/execute with private backups and link creation. |
+| Link repair/remove | No DeepChat-owned link model. | Track created links in database state and only repair/remove those safely. |
+| Git install | `installFromUrl` downloads ZIP only. | Add Git clone scan/install flow with provenance, opened from the top add menu. |
+| Sync directory | Existing import/export targets registered tools, not a user-selected multi-skill repo directory. | Add native sync directory preview/execute APIs, labeled as sync directory instead of agent export. |
+| Skill details | Long descriptions currently expand list/table rows. | Add one reusable detail dialog that renders manifest data and `SKILL.md` Markdown. |
+| Settings UX | The first implementation over-split Library, Agents, Import / Export, Install, and Discover. | Collapse to Library, Agents, and Sync Directory. Folder/ZIP/URL/Git install lives under top Add Skill; install-to-agent lives on each Library row. |
+
+## Data Model
+
+Add `src/shared/types/skillManagement.ts`.
+
+```ts
+export type SkillSourceType =
+ | 'builtin'
+ | 'created'
+ | 'folder-install'
+ | 'zip-install'
+ | 'url-install'
+ | 'git-install'
+ | 'adopted'
+ | 'imported'
+
+export type SkillRepoFormat = 'single-skill' | 'multi-skill'
+
+export interface SkillManagementState {
+ version: 1
+ skills: Record
+ sync?: SkillSyncDirectoryConfig
+}
+
+export interface SkillManagementItem {
+ name: string
+ canonicalPath: string
+ deepchat: {
+ disabled: boolean
+ }
+ extension: SkillExtensionConfig
+ source: SkillSource
+ agentLinks?: Record
+}
+
+export interface SkillSource {
+ type: SkillSourceType
+ repoUrl?: string
+ repoFormat?: SkillRepoFormat
+ agentId?: string
+ originalPath?: string
+ importedFrom?: string
+ installedAt?: string
+ importedAt?: string
+ adoptedAt?: string
+}
+
+export interface AgentLinkInfo {
+ path: string
+ state: 'linked' | 'missing' | 'broken' | 'conflict' | 'permission-denied'
+ createdByDeepChat: boolean
+ linkedAt?: string
+}
+
+export interface SkillSyncDirectoryConfig {
+ skillsDirectory: string
+ layout: 'multi-skill-repo'
+ lastExportAt?: string | null
+ lastImportAt?: string | null
+}
+```
+
+Database state rules:
+
+- Store only durable state that cannot be derived cheaply from files.
+- Store V1 state in the existing application database, preferably through the DB-backed settings
+ path (`app_settings`) unless implementation proves dedicated SQL tables are needed.
+- Rebuild missing database entries from discovered DeepChat skills with `source.type = 'created'`
+ only as a fallback. Keep current built-in install behavior, but mark bundled resources as
+ `builtin` when source can be recognized.
+- Migrate legacy runtime extension sidecars from `/.deepchat-meta/.json` into
+ database state on first load.
+- After successful migration, remove the migrated legacy sidecar files. If migration fails, leave
+ legacy files untouched for retry.
+- New writes go only to the database.
+- The skills path must not be the canonical storage location for management metadata.
+- Use database transactions for multi-skill state writes.
+
+## Presenter Changes
+
+### SkillPresenter
+
+Add helpers:
+
+- `managementState.ts`: load/save/migrate database-backed skill management state.
+- `gitInstall.ts`: clone/scan/install Git repositories.
+- `importExport.ts`: native sync directory import/export.
+
+Add or extend methods on `ISkillPresenter`:
+
+- `getUnifiedSkillCatalog(): Promise`
+- `getSkillDetail(input: { name: string }): Promise`
+- `setSkillDeepChatDisabled(name: string, disabled: boolean): Promise`
+- `getSkillManagementState(): Promise`
+- `scanGitSkillRepo(input): Promise`
+- `installSkillsFromGit(input): Promise`
+- `getSkillsSyncConfig(): Promise`
+- `setSkillsSyncDirectory(input): Promise`
+- `previewSyncDirectoryExport(input): Promise`
+- `executeSyncDirectoryExport(input): Promise`
+- `previewSyncDirectoryImport(input): Promise`
+- `executeSyncDirectoryImport(input): Promise`
+
+Runtime filtering:
+
+- `getMetadataPrompt()` excludes disabled skills.
+- `loadSkillContent(name)` returns `null` for disabled skills unless an explicit internal option is
+ added later.
+- `validateSkillNames()` excludes disabled skills.
+- `getActiveSkillsAllowedTools()` inherits disabled filtering.
+- `getUnifiedSkillCatalog()` includes disabled skills for Library.
+
+Install provenance:
+
+- `installFromFolder`, `installFromZip`, and `installFromUrl` should update database source type.
+- Existing folder/ZIP/URL behavior must remain compatible.
+- Existing overwrite backup under the skills directory should be removed from the target design.
+ Normal install replacement and adoption backups both use private backup/temp locations outside
+ the skills path.
+
+### SkillSyncPresenter
+
+Keep read-only agent scan/classification inside `SkillSyncPresenter` for the first pass. Extract an
+`agentManagement.ts` helper only when adoption, repair, remove, and custom path actions make the
+method set large enough to justify another module.
+
+Methods to add to `ISkillSyncPresenter`:
+
+- `scanSkillAgents(): Promise`
+- `scanSkillAgent(input: { agentId: string }): Promise`
+- `getAgentSkillDetail(input: { agentId: string; name: string }): Promise`
+- `previewAdoptAgentSkill(input): Promise`
+- `executeAdoptAgentSkill(input): Promise`
+- `previewLinkDeepChatSkills(input): Promise`
+- `executeLinkDeepChatSkills(input): Promise`
+- `repairAgentSkillLink(input): Promise`
+- `removeAgentSkillLink(input): Promise`
+- `addCustomSkillAgentPath(input): Promise`
+
+Use `toolScanner.getAllTools()` as the registered tool source, but filter link/adopt targets to
+user-level folder-format tools:
+
+```ts
+const canManageLinks =
+ !tool.isProjectLevel &&
+ tool.filePattern === '*/SKILL.md' &&
+ tool.capabilities.supportsSubfolders
+```
+
+Classification should inspect each entry without writing:
+
+```txt
+symlink -> target missing => broken-link
+symlink -> target under skillsDir => deepchat linked
+symlink -> other target => external-link
+real dir + DeepChat same name + diff => conflict
+real dir + no DeepChat same name => agent-owned
+```
+
+Use content hashes only for conflict detection after verifying both sides have `SKILL.md`.
+
+## Route And Client Changes
+
+Extend route contracts:
+
+- `src/shared/contracts/routes/skills.routes.ts`
+- `src/shared/contracts/routes/skillSync.routes.ts`
+
+Extend Zod schemas in `src/shared/contracts/domainSchemas.ts` only for route payload validation.
+Route dispatch remains in `src/main/routes/index.ts`.
+
+Extend renderer clients:
+
+- `src/renderer/api/SkillClient.ts` for Library, Git, and sync directory calls.
+- `src/renderer/api/SkillSyncClient.ts` for agent management calls.
+
+Add event contracts only where UI needs push refresh:
+
+- `skills.catalog.changed`: add reason values for `disabled-updated`, `management-state-updated`,
+ `git-installed`, and `sync-directory-updated`.
+- Add `skillSync.agentLinks.changed` if link/adopt actions need passive refresh.
+
+Keep scan/import/export progress events unchanged.
+
+### Route API Shape
+
+Library:
+
+```ts
+export interface UnifiedSkillItem {
+ name: string
+ description: string
+ canonicalPath: string
+ sourceType: SkillSourceType
+ deepchatDisabled: boolean
+ agentLinks: Record
+ ownerPluginId?: string
+ mutable: boolean
+}
+
+export interface SkillDetail {
+ name: string
+ description: string
+ sourcePath: string
+ markdown: string
+ mutable: boolean
+}
+```
+
+Agents:
+
+```ts
+export type AgentSkillOwner = 'deepchat' | 'agent' | 'external-link' | 'broken-link' | 'unknown'
+
+export type AgentSkillStatus =
+ | 'linked'
+ | 'agent-owned'
+ | 'linked-out'
+ | 'broken-link'
+ | 'conflict'
+ | 'empty'
+
+export type AgentSkillAction =
+ | 'adopt'
+ | 'resolve-conflict'
+ | 'repair-link'
+ | 'remove-link'
+ | 'open'
+
+export interface InstalledSkillAgent {
+ id: string
+ name: string
+ skillsDir: string
+ isCustom: boolean
+ supportsLinkManagement: boolean
+ skillsCount: number
+ linkedCount: number
+ agentOwnedCount: number
+ conflictCount: number
+ brokenLinkCount: number
+ status: 'ready' | 'detected-no-skills-dir' | 'permission-denied'
+}
+
+export interface AgentSkillItem {
+ name: string
+ description?: string
+ path: string
+ owner: AgentSkillOwner
+ status: AgentSkillStatus
+ action?: AgentSkillAction
+ link?: {
+ isSymlink: boolean
+ targetPath?: string
+ targetExists?: boolean
+ targetInsideDeepChat?: boolean
+ createdByDeepChat?: boolean
+ }
+ deepchat?: {
+ exists: boolean
+ path?: string
+ disabled?: boolean
+ sameContent?: boolean
+ }
+}
+```
+
+Git install:
+
+```ts
+export interface GitSkillRepoScanResult {
+ repoUrl: string
+ repoFormat: 'single-skill' | 'multi-skill'
+ skills: Array<{
+ name: string
+ description: string
+ relativePath: string
+ conflict: boolean
+ valid: boolean
+ error?: string
+ }>
+}
+```
+
+Sync directory:
+
+```ts
+export type SyncDirectorySkillState = 'new' | 'same' | 'modified' | 'conflict' | 'invalid'
+
+export interface SyncDirectorySkillPreview {
+ name: string
+ state: SyncDirectorySkillState
+ sourcePath: string
+ targetPath: string
+ error?: string
+}
+```
+
+## File Operations
+
+Base directories:
+
+```txt
+//
+application database: skill management state
+~/.deepchat/backups/skill-adoptions////
+~/.deepchat/tmp/skill-adoptions//
+~/.deepchat/tmp/skill-installs//
+~/.deepchat/tmp/skill-imports//
+```
+
+The configured skills path is a content root only. It must not contain `.deepchat-meta`, metadata
+files, backup folders, temp folders, or rollback folders in the target design.
+
+Adoption flow:
+
+```txt
+1. Resolve tool and skill row from a fresh scan.
+2. Validate source is inside the selected agent skills directory.
+3. Resolve symlink source when adopting external-link rows.
+4. Validate `SKILL.md` and skill name.
+5. Choose target name, defaulting to `-` on conflict.
+6. Copy source content to private temp.
+7. Validate copied `SKILL.md` and hash.
+8. Move temp to `/`.
+9. Move original agent path to private backup.
+10. Create directory symlink; on Windows fallback to junction.
+11. Write database source provenance and agentLinks.
+12. Rediscover DeepChat skills and rescan the selected agent.
+```
+
+Agent directories must never receive:
+
+```txt
+*.backup
+*.old
+*.deepchat-backup-*
+.deepchat-meta
+tmp
+```
+
+## Git Install
+
+Implementation:
+
+- Use `child_process.execFile` or existing process utility with `git` directly. Do not add a Git
+ dependency.
+- Clone into `~/.deepchat/tmp/skill-installs/`.
+- Detect:
+ - root `SKILL.md` => `single-skill`
+ - `skills//SKILL.md` => `multi-skill`
+- Reuse existing skill validation and copy logic where possible.
+- Support strategies: `rename`, `overwrite`, `skip`.
+- Record `repoUrl`, `repoFormat`, and `installedAt`.
+- Always remove temp clone after install/scan completion.
+
+## Sync Directory
+
+This is separate from existing external tool import/export.
+
+Export:
+
+```txt
+/
+ README.md
+ skills/
+ /
+ SKILL.md
+ assets/
+ references/
+ scripts/
+```
+
+Import:
+
+- Scan only `/skills/*/SKILL.md`.
+- Validate each skill before preview.
+- Show state: `new`, `same`, `modified`, `conflict`, `invalid`.
+- Apply `rename`, `overwrite`, or `skip`.
+- Record `source.type = 'imported'`, `importedFrom`, and `importedAt`.
+
+## Renderer Plan
+
+Convert `SkillsSettings.vue` into three tabs and one top add menu:
+
+```txt
+SettingsPageShell
+ Actions: search where relevant, Add Skill menu
+ Tabs
+ Library
+ Agents
+ Sync Directory
+```
+
+Reuse or adapt:
+
+- Existing `SkillCard` for Library rows.
+- Existing `SkillInstallDialog` folder/ZIP/URL UI from the top Add Skill menu.
+- Existing Git install dialog logic from the top Add Skill menu.
+- Existing link/sync-to-agent backend from a single-skill Library row action.
+
+New components:
+
+- `SkillAgentsTab.vue`
+- `AgentSkillTable.vue`
+- `AdoptSkillDialog.vue`
+- `ResolveSkillConflictDialog.vue`
+- `InstallSkillToAgentDialog.vue`
+- `SkillDetailDialog.vue`
+- `SkillImportExportTab.vue`
+- `InstallFromGitDialog.vue`
+
+Keep user-facing strings in `src/renderer/src/i18n/*/settings.json`.
+
+### Renderer Style Contract
+
+Use current settings UI patterns instead of a new design system:
+
+- Shell: `SettingsPageShell`.
+- Tabs: existing shadcn tabs.
+- Tables/lists: plain bordered row groups with compact spacing.
+- Actions: `Button` with lucide/Iconify icons; destructive actions stay in menus or confirm dialogs.
+- Toggles: `Switch` for DeepChat-only enabled/disabled.
+- Selection: `Checkbox` for skill multi-select.
+- Conflict strategies: `RadioGroup`.
+- Paths: monospace text, truncated with tooltip.
+- Status: badge text plus semantic color.
+
+Recommended tab component shape:
+
+```txt
+SkillsSettings.vue
+ Add Skill menu
+ SkillInstallDialog.vue
+ InstallFromGitDialog.vue
+ SkillCard.vue
+ SkillDetailDialog.vue
+ InstallSkillToAgentDialog.vue
+ SkillAgentsTab.vue
+ AgentSkillTable.vue
+ SkillDetailDialog.vue
+ AdoptSkillDialog.vue
+ ResolveSkillConflictDialog.vue
+ CustomAgentPathDialog.vue
+ SkillImportExportTab.vue as Sync Directory
+```
+
+Description handling:
+
+```txt
+List/table row: one-line clamp or no description.
+Detail dialog: full manifest description plus rendered Markdown from SKILL.md.
+```
+
+Library row interaction:
+
+```txt
+SkillCard.vue
+ non-control area click -> SkillDetailDialog.vue
+ exposed controls:
+ [Install to Agent] InstallSkillToAgentDialog.vue
+ [switch] DeepChat enable/disable
+
+SkillDetailDialog.vue
+ preview mode: rendered SKILL.md body
+ edit mode: name (read-only), description, allowedTools, Markdown content
+ actions: Install to Agent, enable/disable, Edit/Preview, Delete with confirm, Save/Cancel
+```
+
+Loading, empty, and error states:
+
+```txt
+Loading:
+[spinner] Scanning installed agents...
+
+Empty:
+No supported agents found.
+[Refresh]
+
+Permission error:
+Cannot read ~/.claude/skills
+[Open Folder] [Refresh]
+
+Broken link:
+Target missing: ~/.deepchat/skills/foo
+[Repair] [...]
+```
+
+Do not add nested cards. A tab may have one top toolbar and one primary list/table area; dialogs are
+the only framed surfaces that may contain form sections.
+
+### File Change Range
+
+Expected source files across the full feature. Phase 2 keeps read-only agent classification in
+`SkillSyncPresenter.index.ts`; do not add a dedicated agent-management module until write actions
+make that separation useful.
+
+```txt
+src/main/presenter/skillPresenter/
+ index.ts
+ managementState.ts
+ gitInstall.ts
+ importExport.ts
+
+src/main/presenter/skillSyncPresenter/
+ index.ts
+ toolScanner.ts
+
+src/shared/types/
+ skill.ts
+ skillManagement.ts
+ skillSync.ts
+
+src/main/presenter/sqlitePresenter/tables/
+ configTables.ts
+
+src/shared/contracts/routes/
+ skills.routes.ts
+ skillSync.routes.ts
+
+src/shared/contracts/events/
+ skills.events.ts
+ skillSync.events.ts
+
+src/renderer/api/
+ SkillClient.ts
+ SkillSyncClient.ts
+
+src/renderer/settings/components/skills/
+ SkillsSettings.vue
+ SkillAgentsTab.vue
+ AgentSkillTable.vue
+ AdoptSkillDialog.vue
+ ResolveSkillConflictDialog.vue
+ InstallSkillToAgentDialog.vue
+ SkillDetailDialog.vue
+ CustomAgentPathDialog.vue
+ SkillImportExportTab.vue
+ InstallFromGitDialog.vue
+```
+
+## Security And Compatibility
+
+- Reuse `skillSyncPresenter/security.ts` path safety helpers where possible.
+- Add symlink-aware containment checks for existing and not-yet-existing paths.
+- Never follow recursive symlink loops during scan or copy.
+- Skip symlinks during copied skill content unless adopting an external-link target explicitly.
+- Enforce current skill name rules: `^[a-z0-9][a-z0-9._-]*$`.
+- Enforce current file/ZIP/folder size ceilings or stricter ceilings for new flows.
+- Keep `skillsPath` compatibility. All "DeepChat skills path" operations use
+ `configPresenter.getSkillsPath()`, not hard-coded `~/.deepchat/skills`.
+- Migrate legacy `/.deepchat-meta/*.json` sidecars into database state, then stop writing
+ new sidecar files.
+- Scans must ignore legacy `.deepchat-meta` until migration cleanup is implemented.
+- Plugin-contributed skills are read-only catalog entries and are excluded from mutable actions.
+- Detail routes must read only from the selected DeepChat skill path or the freshly scanned supported
+ agent skill path.
+
+## Test Strategy
+
+Main unit tests:
+
+- Database-backed management state load/save/migration.
+- Legacy `.deepchat-meta/.json` sidecar import into database state.
+- Assertion that new runtime extension writes do not create files under the skills path.
+- Disabled filtering in metadata prompt, `loadSkillContent`, `validateSkillNames`, and Library
+ catalog inclusion.
+- Agent scan classification for linked, agent-owned, external-link, broken-link, and conflict.
+- Adoption success, conflict rename, backup location, and no backup residue in agent directory.
+- Link create/repair/remove, including Windows junction fallback mocked path.
+- Git single-skill and multi-skill scan/install with temp cleanup.
+- Sync directory import/export preview and conflict strategies.
+- Skill detail route path safety for DeepChat and agent-owned skills.
+
+Renderer tests:
+
+- Skills tabs render as Library, Agents, and Sync Directory only.
+- Top Add Skill menu exposes folder, ZIP, URL, and Git install choices.
+- Disabled toggle calls typed client and updates UI.
+- Library row Install to Agent opens detected local agents and calls the link client for one skill.
+- Library Install to Agent shows Disconnect and calls the remove-link client when the selected agent
+ is already linked.
+- Skill card body click opens detail while exposed install/toggle controls do not trigger detail.
+- Skill detail dialog renders long `SKILL.md` content without expanding list/table rows.
+- Skill detail dialog owns edit/save and delete-with-confirm controls for mutable DeepChat skills.
+- Agents table row actions map to the right client methods.
+- Agents tab uses icon-leading agent tab buttons and has no bulk "Sync to Agent" button.
+- Git install dialog scan/select/install states from the top add menu.
+- Sync Directory preview states.
+- Discover tab and `find-skills` resource are absent.
+
+Smoke tests:
+
+- Extend existing skills read-only route smoke for new Library routes.
+- Extend skill sync smoke for agent scan routes without mutating user files.
+
+Manual checks:
+
+- macOS/Linux symlink creation.
+- Windows junction fallback.
+- Agent directory remains clean after adoption.
+
+## Delivery Order
+
+1. Database state and Library disabled state.
+2. Agents scan/classification UI with no mutations.
+3. Adoption, link, repair, and remove.
+4. Git install through the top add menu.
+5. Sync directory import/export.
+6. UX consolidation: remove Install and Discover tabs, remove `find-skills`, move install-to-agent
+ to Library rows, and add reusable skill details.
+
+This order produces a useful first slice after step 3: users can take over existing local
+folder-format agent skills without polluting agent directories.
diff --git a/docs/features/deepchat-skills-management/spec.md b/docs/features/deepchat-skills-management/spec.md
new file mode 100644
index 0000000000..debfeccb37
--- /dev/null
+++ b/docs/features/deepchat-skills-management/spec.md
@@ -0,0 +1,736 @@
+# DeepChat Skills Management
+
+## Current-State Corrections
+
+The source draft describes the right product direction, but several parts need to be corrected for
+the current codebase before implementation:
+
+- This is not a greenfield skills system. `SkillPresenter` already owns local skill discovery,
+ install/uninstall, hot reload, built-in skill installation, legacy sidecar runtime config, and
+ session activation.
+- This is not a generic "all agents are sync targets" feature. V1 link/adopt operations are only
+ valid for user-level folder-format tools that use `//SKILL.md`. Project-level
+ and single-file tools remain import/export conversion targets only.
+- Git install is not the same as current `installFromUrl`. The existing URL path downloads ZIP
+ files; Git install needs clone/scan/select/install provenance.
+- The settings UX should stay in the existing `settings-skills` route, but V1.1 must remove the
+ over-split five-tab surface. Library owns add/install-to-agent actions, Agents owns inspection and
+ adoption, and sync directory remains a separate local repository workflow.
+- A command-copy Discover tab is not useful enough to keep. Remove it and do not bundle
+ `find-skills` as a built-in skill.
+
+## User Need
+
+Users need DeepChat to act as the local control center for skills: see local DeepChat skills, disable
+them for DeepChat without deleting files, install new skills from the top add menu, install a
+specific DeepChat skill to a detected local agent from that skill row, inspect existing
+folder-format skills from installed agents, adopt those skills into DeepChat safely, and move skills
+in or out of a user-selected sync directory.
+
+## Goals
+
+- Keep DeepChat runtime skills canonical under the configured skills path, defaulting to
+ `~/.deepchat/skills`.
+- Store source provenance, DeepChat-only disabled state, runtime extension settings, sync directory
+ settings, and DeepChat-created agent links in the application database.
+- Keep the configured DeepChat skills path as a pure content directory: only skill folders and their
+ files belong under it.
+- Preserve the existing `SkillPresenter` runtime behavior while adding a Library catalog that can
+ show disabled skills.
+- Add an Agents tab that scans detected user-level folder-format tools and classifies each skill as
+ DeepChat-linked, agent-owned, external-link, broken-link, or conflict.
+- Adopt agent-owned folder-format skills by copying the skill into DeepChat, backing up the original
+ under `~/.deepchat/backups`, and replacing the agent path with a link to the DeepChat canonical
+ skill.
+- Link a selected DeepChat skill to a supported local agent from the Library row action by creating a
+ symlink or Windows junction.
+- Add Git repository installation for single-skill repos with root `SKILL.md` and multi-skill repos
+ with `skills//SKILL.md` through the top add menu.
+- Add manual import/export to a user-selected multi-skill sync directory.
+- Add a reusable skill detail dialog that clamps list/table descriptions and renders the selected
+ `SKILL.md` body as Markdown.
+- Remove the top-level Install and Discover tabs; remove the old external-tool import block from the
+ Library tab.
+
+## Existing Capabilities To Preserve
+
+- `SkillPresenter` discovers `SKILL.md` files under the configured skills path.
+- `SkillPresenter` installs from folder, ZIP, and ZIP URL.
+- `SkillPresenter` installs built-in skills from `resources/skills`.
+- `SkillPresenter` currently stores per-skill runtime extension config under `.deepchat-meta`; the
+ target design migrates this state into the database and treats `.deepchat-meta` as legacy input.
+- `SkillPresenter` watches skill file changes and publishes `skills.catalog.changed`.
+- `SkillSyncPresenter` scans registered external tools, imports external skills into DeepChat, and
+ exports DeepChat skills to external tool formats.
+- Renderer-main communication uses typed route contracts and renderer API clients.
+- Skills settings currently live at `settings-skills` in `SkillsSettings.vue`.
+
+## Directory Layout
+
+DeepChat-managed skills use the configured skills path. When the user has not changed it, the path
+is:
+
+```txt
+~/.deepchat/
+ skills/
+ skill-a/
+ SKILL.md
+ assets/
+ references/
+ scripts/
+ skill-b/
+ SKILL.md
+ backups/
+ skill-adoptions/
+ claude-code/
+ old-review/
+ 20260626-153000/
+ original/
+ SKILL.md
+ adoption.json
+ tmp/
+ skill-adoptions/
+ skill-installs/
+ skill-imports/
+```
+
+Database-backed management state:
+
+```txt
+application database
+ skill metadata/provenance
+ DeepChat-only disabled flags
+ runtime extension settings
+ agent link ownership
+ sync directory config and timestamps
+```
+
+After adoption or Library install-to-agent, supported agent directories should contain final skills
+or links only:
+
+```txt
+~/.claude/skills/
+ old-review -> ~/.deepchat/skills/old-review
+ guizang-ppt -> ~/.deepchat/skills/guizang-ppt
+```
+
+Manual sync directory layout:
+
+```txt
+~/Documents/deepchat-skills/
+ README.md
+ skills/
+ old-review/
+ SKILL.md
+ assets/
+ references/
+ scripts/
+ guizang-ppt/
+ SKILL.md
+```
+
+## Ownership Rules
+
+| Location | Owner |
+| --- | --- |
+| Real directory under configured DeepChat skills path | DeepChat |
+| Real directory under a supported agent skills path | Agent |
+| Agent path is a symlink/junction to DeepChat skills path | DeepChat |
+| Agent path is a symlink to another location | External link |
+| Agent path is a symlink/junction whose target is missing | Broken link |
+
+DeepChat runtime reads only managed DeepChat skills and plugin-contributed runtime skills. It must
+not require any metadata directory inside the skills path. Legacy `.deepchat-meta`, backups, temp
+directories, and agent backup residue must be ignored during migration/scanning.
+
+## Supported Agent Management Targets
+
+V1 link/adopt supports only user-level folder-format tools:
+
+- `claude-code`
+- `codex`
+- `cursor`
+- `opencode`
+- `goose`
+- `kilocode`
+- `copilot-user`
+
+V1 does not link/adopt project-level or single-file tools:
+
+- `cursor-project`
+- `windsurf`
+- `copilot`
+- `kiro`
+- `antigravity`
+
+Those tools remain available through the existing import/export conversion flow.
+
+## Functional Requirements
+
+### Library
+
+- Users can view all DeepChat-managed skills, including disabled skills.
+- Users can toggle a DeepChat-only disabled state.
+- Users can open a reusable skill detail dialog from each row.
+- Users can install a single DeepChat skill to a detected local agent from that skill row.
+- Users can add skills from folder, ZIP, URL, or Git repository from the top add menu.
+- The Library tab must not show the old external-tool import grid.
+- Disabled skills remain on disk and remain eligible for agent links and manual export when the user
+ explicitly includes them.
+- Disabled skills are excluded from DeepChat runtime prompt injection, automatic validation, and
+ active skill tool permissions.
+
+### Agents
+
+- Users can see detected supported agents as icon tab buttons matching the Library/external tool
+ button style.
+- Users can select an agent and see the agent's skills directory, counts, and skill rows.
+- Agent rows classify ownership and status without mutating files during scan.
+- Agent rows clamp descriptions to a short preview and expose full content through the reusable
+ skill detail dialog.
+- Agent-owned folder skills can be adopted into DeepChat after a preview and confirmation.
+- DeepChat-linked skills show link details and do not offer a primary mutation button.
+- Broken DeepChat-created links can be repaired when the canonical DeepChat skill still exists.
+- DeepChat-created links can be removed without deleting the canonical DeepChat skill.
+- Agents tab must not show a bulk "Sync to Agent" action; installing DeepChat skills to agents is a
+ Library row action.
+
+### Add Skill
+
+- Users can install selected skills from a Git repository.
+- Folder, ZIP, URL, and Git installation share the top add menu instead of a separate Install tab.
+- Git scan detects root `SKILL.md` as `single-skill`.
+- Git scan detects `skills//SKILL.md` entries as `multi-skill`.
+- Git install records source provenance in database state.
+
+### Sync Directory
+
+- Users can set a sync directory.
+- Export writes selected skills to `/skills/`.
+- Import reads selected skills from `/skills/`.
+- Import/export previews show new, same, modified, conflict, skipped, and failed items.
+- Import/export updates database sync timestamps.
+- This workflow is for local multi-skill repository backup/migration, not for installing a skill to
+ an agent.
+
+## UX Shape
+
+The existing `settings-skills` route becomes a smaller tabbed work surface:
+
+```txt
++--------------------------------------------------------------------------+
+| Skills [Search_______] [+ Add Skill] |
+| Manage DeepChat skills and local agent links. |
++--------------------------------------------------------------------------+
+| [ Library ] [ Agents ] [ Sync Directory ] |
++--------------------------------------------------------------------------+
+| active tab content |
++--------------------------------------------------------------------------+
+```
+
+Style contract:
+
+- Use the existing settings shell, shadcn controls, Iconify/lucide icons, and Tailwind utilities.
+- Keep the page dense and operational. No hero, marketing panel, gradient background, or nested
+ cards.
+- Use compact rows, 8px or smaller radius, semantic badges, and icon buttons with tooltips for
+ refresh/open/remove actions.
+- Agent selector buttons use the same icon-leading button style as the Library external tool tiles:
+ icon, name, count badge, selected border.
+- Use semantic color only as a secondary signal:
+ - Enabled/linked/success: green semantic badge.
+ - Disabled/skipped/neutral: muted badge.
+ - Conflict/warning: amber badge.
+ - Broken/failed/destructive: destructive badge.
+- Every status must also have text; color alone is not enough.
+- Long paths and descriptions truncate or clamp instead of wrapping over action controls.
+- Primary action per row goes in the right column; secondary actions go in a row menu.
+
+Top add menu:
+
+```txt
++----------------------------------+
+| + Add Skill |
++----------------------------------+
+| Folder... |
+| ZIP... |
+| URL... |
+| Git repository... |
++----------------------------------+
+```
+
+Git repository install opens from the top add menu, not from a tab:
+
+```txt
++--------------------------------------------------------------------------+
+| Install from Git |
++--------------------------------------------------------------------------+
+| Repository URL |
+| [https://github.com/op7418/guizang-ppt-skill______________] [Scan] |
+| |
+| Detected format: single-skill |
+| [x] guizang-ppt-skill No conflict |
+| |
+| Conflict strategy |
+| (*) Rename new skill ( ) Replace existing ( ) Skip existing |
+| |
+| [Cancel] [Install to DeepChat] |
++--------------------------------------------------------------------------+
+```
+
+Library tab:
+
+```txt
++--------------------------------------------------------------------------+
+| Library [Open Folder] |
++--------------------------------------------------------------------------+
+| Summary: 18 skills - 15 enabled - 3 disabled - 4 agent links |
+| |
+| [wand] guizang-ppt |
+| Create PowerPoint decks from structured plans. |
+| Git install Enabled Claude [Install to Agent] [on] |
+| |
+| [wand] frontend-design |
+| UI and UX implementation guidance. |
+| Built-in Enabled - [Install to Agent] [on] |
+| |
+| [wand] old-review |
+| Review legacy code paths. |
+| Adopted Disabled Codex [Install to Agent] [off]|
+| |
+| Empty: No skills installed. Use Add Skill to add folder, ZIP, URL, Git. |
++--------------------------------------------------------------------------+
+```
+
+Library row interaction:
+
+```txt
+Click a non-control area of a Library row -> open Skill Detail.
+The exposed hot controls are:
+
+[Install to Agent] Install to Agent
+[on/off] Enable or disable in DeepChat
+```
+
+Skill detail:
+
+```txt
++--------------------------------------------------------------------------+
+| G guizang-ppt [Install to Agent] |
+| Create PowerPoint decks from structured plans. Enabled [] |
+| /Users/.../.deepchat/skills/guizang-ppt/SKILL.md [Edit] [Delete] |
+| |
+| +----------------------------------------------------------------------+ |
+| | Rendered Markdown preview of SKILL.md without YAML frontmatter | |
+| +----------------------------------------------------------------------+ |
++--------------------------------------------------------------------------+
+
+Edit mode keeps the same dialog:
+
++--------------------------------------------------------------------------+
+| G guizang-ppt [Install to Agent] |
+| /Users/.../.deepchat/skills/guizang-ppt/SKILL.md [Preview] [Delete] |
+| |
+| Name: guizang-ppt (read-only) |
+| Description: [.........................................................] |
+| Allowed tools: [Read, Bash] |
+| Content: |
+| +----------------------------------------------------------------------+ |
+| | # guizang-ppt | |
+| | ... | |
+| +----------------------------------------------------------------------+ |
+| [Cancel] [Save] |
++--------------------------------------------------------------------------+
+```
+
+Install one skill to a detected local agent:
+
+```txt
++--------------------------------------------------+
+| Install guizang-ppt to Agent |
++--------------------------------------------------+
+| Target agent |
+| [ Claude Code ] [ OpenAI Codex ] [ Cursor ] |
+| [ OpenCode ] [ Goose ] [ Kilo Code ] |
+| |
+| Result |
+| ~/.codex/skills/guizang-ppt -> DeepChat skill |
+| |
+| Conflict strategy |
+| (*) Rename link ( ) Replace DeepChat-owned link |
+| ( ) Skip |
+| |
+| [Cancel] [Install] |
++--------------------------------------------------+
+```
+
+Library row behavior:
+
+- Enabled/disabled toggle changes only DeepChat runtime state.
+- Disabled rows remain visible and editable, but their badge is muted and activation controls are
+ disabled where runtime selection appears.
+- Built-in or plugin-owned rows do not show destructive actions unless the existing system already
+ supports that action.
+- The old external-tool import grid is removed from this tab.
+
+Agents tab:
+
+```txt
++--------------------------------------------------------------------------+
+| Agents [Refresh] |
++--------------------------------------------------------------------------+
+| [ icon Claude Code 0 ] [ icon OpenAI Codex 2 ] [ icon Cursor 0 ] |
+| [ icon OpenCode 0 ] [ icon Goose 0 ] [ icon Kilo Code 0 ] |
++--------------------------------------------------------------------------+
+| OpenAI Codex Available |
+| /Users/me/.codex/skills |
+| 2 skills - 0 linked - 2 agent owned - 0 conflict - 0 broken |
++------------------+--------------+--------------+----------+------------+
+| Skill | Owner | Status | Preview | Action |
++------------------+--------------+--------------+----------+------------+
+| hatch-pet | Codex | Agent owned | View | Adopt |
+| native-feel | Codex | Agent owned | View | Adopt |
++------------------+--------------+--------------+----------+------------+
+```
+
+Agent row rules:
+
+- Description stays clamped to one line or is omitted from the table.
+- Full description and `SKILL.md` body are shown through the reusable detail dialog.
+- The tab does not show "Sync to Agent"; linking DeepChat skills to agents starts from Library.
+
+Agent row states:
+
+```txt
+Agent owned:
++------------------+--------------+--------------+----------+------------+
+| old-review | Claude Code | Agent owned | View | Adopt |
++------------------+--------------+--------------+----------+------------+
+
+DeepChat linked:
++------------------+--------------+--------------+----------+------------+
+| guizang-ppt | DeepChat | Linked | View | ... |
++------------------+--------------+--------------+----------+------------+
+menu: Open in Finder, Remove link
+
+External link:
++------------------+--------------+--------------+----------+------------+
+| docs-writer | External | Linked out | View | Adopt |
++------------------+--------------+--------------+----------+------------+
+
+Conflict:
++------------------+--------------+--------------+----------+------------+
+| frontend-helper | Claude Code | Conflict | View | Resolve |
++------------------+--------------+--------------+----------+------------+
+
+Broken link:
++------------------+--------------+--------------+----------+------------+
+| broken-ppt | DeepChat | Broken link | View | Repair |
++------------------+--------------+--------------+----------+------------+
+```
+
+Adopt confirmation:
+
+```txt
++--------------------------------------------------+
+| Adopt Skill |
++--------------------------------------------------+
+| old-review |
+| |
+| Current location |
+| ~/.claude/skills/old-review |
+| |
+| After adoption |
+| ~/.deepchat/skills/old-review |
+| ~/.claude/skills/old-review -> DeepChat skill |
+| |
+| Backup |
+| ~/.deepchat/backups/skill-adoptions/... |
+| |
+| [Cancel] [Adopt] |
++--------------------------------------------------+
+```
+
+Conflict resolver:
+
+```txt
++--------------------------------------------------+
+| Resolve Conflict |
++--------------------------------------------------+
+| frontend-helper |
+| |
+| Agent |
+| ~/.claude/skills/frontend-helper |
+| |
+| DeepChat |
+| ~/.deepchat/skills/frontend-helper |
+| |
+| Choose action |
+| (*) Adopt as frontend-helper-claude |
+| ( ) Replace DeepChat frontend-helper |
+| ( ) Keep current state |
+| |
+| [Cancel] [Apply] |
++--------------------------------------------------+
+```
+
+Custom path:
+
+```txt
++--------------------------------------------------+
+| Add Custom Agent Path |
++--------------------------------------------------+
+| Display name |
+| [My Agent ] |
+| |
+| Skills directory |
+| [/Users/me/.my-agent/skills ] |
+| |
+| Format |
+| (*) SKILL.md folder format |
+| |
+| [Cancel] [Scan path] |
++--------------------------------------------------+
+```
+
+Reusable skill detail:
+
+```txt
++----------------------------------------------------------+
+| C |
+| ComputerUse skill [Switch] [...]|
+| Drive the user's desktop GUI through ... |
+| |
+| +------------------------------------------------------+ |
+| | Computer Use | |
+| | Rendered Markdown from SKILL.md | |
+| | ... | |
+| +------------------------------------------------------+ |
+| |
+| [Try in Chat] |
++----------------------------------------------------------+
+```
+
+The same dialog is used from Library rows and Agents rows. It receives a source descriptor and
+renders the manifest summary plus sanitized Markdown body.
+
+Sync Directory tab:
+
+```txt
++--------------------------------------------------------------------------+
+| Sync Directory |
++--------------------------------------------------------------------------+
+| Local multi-skill repository |
+| [~/Documents/deepchat-skills____________________________] [Browse] [Save] |
++--------------------------------------------------------------------------+
+| [ Export to directory ] [ Import from directory ] |
++--------------------------------------------------------------------------+
+| Export selected skills |
+| [x] guizang-ppt Enabled Git install |
+| [x] frontend-design Enabled Built-in |
+| [ ] old-review Disabled Adopted |
+| |
+| [Preview Export] [Export Now] |
++--------------------------------------------------------------------------+
+```
+
+Import preview:
+
+```txt
++--------------------------------------------------------------------------+
+| Import from ~/Documents/deepchat-skills |
++----------------------+-------------+---------------+---------------------+
+| Skill | State | Source | Action |
++----------------------+-------------+---------------+---------------------+
+| guizang-ppt | Same | sync dir | Skip |
+| frontend-design | New | sync dir | Import |
+| skill-x | Conflict | sync dir | Rename |
+| broken-skill | Invalid | sync dir | View error |
++----------------------+-------------+---------------+---------------------+
+| Conflict strategy: (*) Rename imported ( ) Replace local ( ) Skip |
+| [Cancel] [Import Selected] |
++--------------------------------------------------------------------------+
+```
+
+## Non-Goals
+
+- No automatic scheduled sync.
+- No built-in Git commit, pull, or push.
+- No marketplace search or command-copy Discover tab.
+- No project-level agent link/adopt.
+- No conversion of single-file prompt formats into linked folder-format skills during adoption.
+- No cloud sync.
+- No separate Install tab; install flows start from the top add menu.
+- No new dependency unless an existing standard library or installed dependency is insufficient.
+
+## Acceptance Criteria
+
+- Database state is created or migrated without deleting existing skills or legacy sidecar runtime
+ configs.
+- Disabling a skill persists across restart, remains visible in Library, and excludes that skill
+ from DeepChat runtime metadata prompt and active-skill validation.
+- The configured DeepChat skills path contains only skill content directories. It must not contain
+ `.deepchat-meta`, metadata files, backups, temp files, or other management metadata after
+ migration.
+- Legacy `.deepchat-meta` runtime config files are migrated into the database and removed only after
+ the database write succeeds.
+- Supported agents are shown only when detected locally.
+- Supported agents use icon-leading tab buttons with counts.
+- Project-level and single-file tools are not offered link/adopt actions.
+- Scanning an agent never creates, deletes, or moves files.
+- Agents table descriptions are clamped or omitted, and full skill content is available through the
+ reusable skill detail dialog with Markdown rendering.
+- Adopting an agent-owned skill creates `~/.deepchat/skills//SKILL.md`, stores the original
+ under `~/.deepchat/backups/skill-adoptions/...`, and replaces the agent path with a link to the
+ canonical DeepChat skill.
+- Agent skills directories do not receive backup, temp, rollback, or metadata folders.
+- Same-name conflicts default to creating a unique adopted skill name instead of overwriting the
+ existing DeepChat skill.
+- Installing one Library skill to an agent creates or repairs only DeepChat-owned links and does not
+ delete agent-owned skill directories unless the user explicitly chooses a conflict strategy.
+- The top add menu exposes folder, ZIP, URL, and Git install paths.
+- Git single-skill and multi-skill repositories install selected skills into DeepChat and write
+ `git-install` provenance.
+- Manual export creates a valid multi-skill repository layout.
+- Manual import handles new, same, modified, and conflict states before writing.
+- The old external-tool import grid, separate Install tab, Discover tab, and `find-skills` bundled
+ skill are removed from the settings surface.
+
+## Critical Acceptance Scenarios
+
+### Agent-Owned Adoption
+
+```txt
+Given ~/.claude/skills/old-review/SKILL.md exists
+And ~/.deepchat/skills/old-review does not exist
+When the user adopts old-review from Claude Code
+Then ~/.deepchat/skills/old-review/SKILL.md exists
+And ~/.claude/skills/old-review links to ~/.deepchat/skills/old-review
+And the original is backed up under ~/.deepchat/backups/skill-adoptions
+And database state source.type is adopted
+```
+
+### Clean Agent Directory
+
+```txt
+Given a user adopts ~/.claude/skills/old-review
+Then ~/.claude/skills contains old-review as a link
+And ~/.claude/skills does not contain old-review.deepchat-backup-*
+And scan shows one old-review row
+```
+
+### DeepChat Linked Display
+
+```txt
+Given ~/.claude/skills/guizang-ppt links to ~/.deepchat/skills/guizang-ppt
+Then the Agents table shows:
+Skill = guizang-ppt
+Owner = DeepChat
+Status = Linked
+Action = row menu only
+```
+
+### Conflict Adoption
+
+```txt
+Given ~/.claude/skills/frontend-helper exists
+And ~/.deepchat/skills/frontend-helper exists
+And their content hashes differ
+When the user chooses "Adopt as frontend-helper-claude"
+Then ~/.deepchat/skills/frontend-helper remains unchanged
+And ~/.deepchat/skills/frontend-helper-claude is created
+And ~/.claude/skills/frontend-helper links to the renamed DeepChat skill
+```
+
+### Git Installation
+
+```txt
+Given a repo root contains SKILL.md
+When the user opens Add Skill -> Git repository, scans, and installs it
+Then the selected skill is copied to the DeepChat skills path
+And database state source.type is git-install
+And database state source.repoFormat is single-skill
+
+Example: `https://github.com/op7418/guizang-ppt-skill` is a root `SKILL.md` repository whose
+frontmatter skill name is `guizang-ppt-skill`.
+
+Given a repo contains skills/a/SKILL.md and skills/b/SKILL.md
+When the user selects a and b
+Then both skills are installed
+And database state source.repoFormat is multi-skill
+```
+
+### Library Install To Agent
+
+```txt
+Given ~/.deepchat/skills/guizang-ppt/SKILL.md exists
+And ~/.codex/skills is detected
+When the user chooses Install to Agent from the guizang-ppt Library row
+Then ~/.codex/skills/guizang-ppt links to ~/.deepchat/skills/guizang-ppt
+And database state records the Codex agent link
+And the Agents tab later shows guizang-ppt as DeepChat linked
+
+Given guizang-ppt is already linked to ~/.codex/skills/guizang-ppt
+When the user opens Install to Agent and selects Codex
+Then the dialog shows a Disconnect action
+And Disconnect removes the DeepChat-owned Agent link
+And database state removes the Codex agent link record
+```
+
+### Library Row And Detail Interaction
+
+```txt
+Given a Library skill row is visible
+When the user clicks any non-control area of the row
+Then the Skill Detail dialog opens
+And the row does not expose a standalone View details action
+And the row keeps Install to Agent and DeepChat enable/disable as visible controls
+
+Given the Skill Detail dialog is open for a mutable skill
+When the user chooses Edit
+Then the dialog switches to editable name, description, allowed tools, and Markdown content fields
+And Delete is next to Edit/Preview inside the same dialog
+And Delete requires a second confirmation before removing the skill
+And Install to Agent and DeepChat enable/disable are also available inside the detail dialog
+```
+
+### Skill Detail Preview
+
+```txt
+Given an agent skill has a long description
+When the user views the agent row
+Then the table does not expand horizontally for the full description
+And clicking the row detail affordance opens a detail dialog
+And the dialog renders the selected SKILL.md body as Markdown
+```
+
+### Manual Export
+
+```txt
+Given sync directory is ~/Documents/deepchat-skills
+And selected skills are a and b
+When the user exports
+Then ~/Documents/deepchat-skills/skills/a/SKILL.md exists
+And ~/Documents/deepchat-skills/skills/b/SKILL.md exists
+And database sync lastExportAt is updated
+```
+
+### DeepChat-Only Disable
+
+```txt
+Given skill a exists in the DeepChat skills path
+When the user disables a in Library
+Then database state marks skill a as DeepChat-disabled
+And Library still shows a
+And getMetadataPrompt excludes a
+And existing agent links remain unchanged
+```
+
+## Resolved Assumptions
+
+- `~/.deepchat/skills` means the configured skills path when the user changed `skillsPath`.
+- Skill management database state is local-only and not automatically synchronized.
+- Existing `.deepchat-meta/.json` runtime config is legacy migration input; new writes go to
+ the database.
+- Plugin-contributed skills remain read-only runtime contributions and are not adopted, linked,
+ exported by default, or moved into database-owned management state.
diff --git a/docs/features/deepchat-skills-management/tasks.md b/docs/features/deepchat-skills-management/tasks.md
new file mode 100644
index 0000000000..7a115e310f
--- /dev/null
+++ b/docs/features/deepchat-skills-management/tasks.md
@@ -0,0 +1,189 @@
+# DeepChat Skills Management Tasks
+
+Status: Phase 1 through Phase 8 are implemented for the supported V1.1 paths. V1.1 keeps the
+working backend paths, removes the over-split Install/Discover UI, moves install-to-agent into
+Library rows, and keeps sync directory as a separate local repository workflow. Adoption still
+defaults conflicts to safe rename; destructive overwrite and custom agent path management remain
+deferred until the agent registry has a durable custom-target model.
+The standalone draft has been absorbed into this SDD folder and removed.
+
+## Phase 0: Design Contract Check
+
+- [x] Keep the `settings-skills` route as one tabbed settings surface.
+- [x] Match the ASCII layouts in `spec.md` before implementing UI.
+- [x] Use existing shadcn settings controls and lucide/Iconify icons.
+- [x] Use compact row/table layouts; do not add hero panels or nested cards.
+- [x] Add loading, empty, permission-error, conflict, broken-link, and invalid-skill states.
+- [x] Ensure every status uses text, not color alone.
+- [x] Truncate long paths and descriptions with tooltips.
+- [x] Keep all user-facing labels in i18n files.
+
+## Phase 1: Database State And Library Disable
+
+- [x] Add `src/shared/types/skillManagement.ts`.
+- [x] Add database-backed management state helper under `src/main/presenter/skillPresenter/`.
+- [x] Store source provenance, disabled state, runtime extension settings, sync config, and agent
+ links in the application database.
+- [x] Migrate legacy `/.deepchat-meta/.json` runtime configs into database state.
+- [x] Remove migrated legacy `.deepchat-meta` files after successful database write.
+- [x] Stop writing new `.deepchat-meta` files under the skills path.
+- [x] Add a test that the configured skills path remains a pure skill content directory.
+- [x] Add `getUnifiedSkillCatalog()` to `ISkillPresenter`.
+- [x] Add `setSkillDeepChatDisabled()` to `ISkillPresenter`.
+- [x] Add typed routes and `SkillClient` methods for unified catalog and disabled toggle.
+- [x] Filter disabled skills from `getMetadataPrompt()`.
+- [x] Filter disabled skills from `loadSkillContent()`.
+- [x] Filter disabled skills from `validateSkillNames()`.
+- [x] Keep disabled skills visible in the Library catalog.
+- [x] Add Library disabled toggle UI and i18n strings.
+- [x] Add unit tests for database migration, persistence, disabled filtering, and restart behavior.
+
+## Phase 2: Agents Scan And Classification
+
+- [x] Add shared agent-management types for installed agents, skill rows, owners, statuses, and
+ actions.
+- [x] Add route contracts for agent scan/list/detail.
+- [x] Keep read-only classification helpers inside `SkillSyncPresenter`; defer a separate
+ `agentManagement.ts` until adoption/repair/remove logic needs it.
+- [x] Reuse `toolScanner.getAllTools()` and filter link/adopt support to user-level
+ `*/SKILL.md` tools.
+- [x] Implement read-only installed-agent detection.
+- [x] Implement read-only skill row classification.
+- [x] Exclude project-level and single-file tools from link/adopt actions.
+- [x] Add `SkillSyncClient` methods for agent scan/list/detail.
+- [x] Add `SkillAgentsTab.vue` and `AgentSkillTable.vue`.
+- [x] Match the read-only Agents tab ASCII layout, row states, and action placement from `spec.md`;
+ keep write actions disabled until Phase 3.
+- [x] Add tests for linked, agent-owned, external-link, broken-link, and conflict classification.
+
+## Phase 3: Adoption And Agent Links
+
+- [x] Add adopt preview route and presenter method.
+- [x] Add adopt execute route and presenter method.
+- [x] Copy adoption sources through `~/.deepchat/tmp/skill-adoptions/`.
+- [x] Move original agent content to `~/.deepchat/backups/skill-adoptions/...`.
+- [x] Replace adopted agent path with a symlink or Windows junction.
+- [x] Record database source provenance and `agentLinks`.
+- [x] Add default conflict strategy: adopt as `-`.
+- [x] Add sync-to-agent preview route and presenter method.
+- [x] Add sync-to-agent execute route and presenter method.
+- [x] Add repair DeepChat-owned link route and presenter method.
+- [x] Add remove DeepChat-owned link route and presenter method.
+- [x] Add `AdoptSkillDialog.vue` and wire adopt/resolve-conflict rows to the existing adoption
+ backend.
+- [x] Match the adopt confirmation ASCII layout from `spec.md`.
+- [ ] Add destructive overwrite/keep adoption conflict strategies after the custom agent ownership
+ model is durable enough to support them safely.
+- [x] Add batch sync-to-agent backend; the original dialog was superseded by the Phase 8 Library
+ row flow.
+- [x] Match sync-to-agent dialog ASCII layout from `spec.md` for the first V1 slice.
+- [ ] Match custom-path dialog ASCII layout when custom agent targets are implemented.
+- [x] Add tests that agent directories never receive backup/temp/meta folders.
+- [x] Add renderer tests for the adopt preview/execute confirmation flow.
+- [x] Add tests for repair/remove refusing links not created by DeepChat.
+
+## Phase 4: Git Install
+
+- [x] Add Git scan route and `SkillClient` method.
+- [x] Add Git install route and `SkillClient` method.
+- [x] Clone repos to `~/.deepchat/tmp/skill-installs/`.
+- [x] Detect root `SKILL.md` as `single-skill`.
+- [x] Detect `skills//SKILL.md` entries as `multi-skill`.
+- [x] Reuse existing skill validation before copy.
+- [x] Support `rename`, `overwrite`, and `skip` conflict strategies.
+- [x] Record `git-install` provenance in database state.
+- [x] Clean temp clones on success and failure.
+- [x] Add `InstallFromGitDialog.vue` and wire it into the Install tab.
+- [x] Match the Install tab Git scan/select/conflict ASCII layout from `spec.md`.
+- [x] Add tests for single-skill, multi-skill, conflict strategy, and temp cleanup.
+
+## Phase 5: Sync Directory Import / Export
+
+- [x] Add sync directory config types and routes.
+- [x] Add `getSkillsSyncConfig()` and `setSkillsSyncDirectory()`.
+- [x] Add export preview and execute routes.
+- [x] Export selected skills to `/skills/`.
+- [x] Write `README.md` when missing.
+- [x] Exclude disabled skills by default and allow explicit inclusion.
+- [x] Add import preview and execute routes.
+- [x] Scan `/skills/*/SKILL.md` only.
+- [x] Preview `new`, `same`, `modified`, `conflict`, and `invalid`.
+- [x] Apply `rename`, `overwrite`, and `skip`.
+- [x] Record `imported` provenance and import/export timestamps.
+- [x] Add `SkillImportExportTab.vue`.
+- [x] Match export and import preview ASCII layouts from `spec.md`.
+- [x] Add tests for export layout and import conflict states.
+
+## Phase 6: Discover
+
+- [x] Add `resources/skills/find-skills/SKILL.md`.
+- [x] Confirm built-in installation picks up the new skill on first run.
+- [x] Add `SkillDiscoverTab.vue`.
+- [x] Match the Discover tab ASCII layout from `spec.md`.
+- [x] Show local `find-skills` status and command-oriented actions.
+- [x] Add i18n strings.
+- [x] Add a renderer test that the Discover tab renders through the five-tab surface coverage.
+
+## Phase 7: Settings Surface And Smoke Coverage
+
+- [x] Convert `SkillsSettings.vue` into Library, Agents, Import / Export, Install, and Discover tabs.
+- [x] Keep existing folder/ZIP/URL install behavior reachable.
+- [x] Keep existing external tool import/export behavior reachable.
+- [x] Keep existing first-launch sync prompt behavior or explicitly remove it from the UX if the new
+ tabs replace it.
+- [x] Extend `test/renderer/api/clients.test.ts` for new typed routes.
+- [x] Extend skills route smoke tests for new read-only routes.
+- [x] Extend skill sync smoke tests for read-only agent scan routes.
+- [x] Run `pnpm run format`.
+- [x] Run `pnpm run i18n`.
+- [x] Run `pnpm run lint`.
+- [x] Run targeted main and renderer tests for touched skills modules.
+
+## Phase 8: V1.1 UX Consolidation
+
+- [x] Reduce `SkillsSettings.vue` tabs to Library, Agents, and Sync Directory.
+- [x] Remove the separate Install tab and `SkillInstallTab.vue`.
+- [x] Remove the Discover tab, `SkillDiscoverTab.vue`, `resources/skills/find-skills/`, and related
+ i18n/tests.
+- [x] Keep folder, ZIP, URL, and Git installs reachable from the top Add Skill menu.
+- [x] Move Git repo install entry into the Add Skill menu and reuse the existing Git scan/install
+ backend.
+- [x] Remove the top Export action; replace that flow with per-skill Library Install to Agent.
+- [x] Remove the old external-tool import grid from the Library tab.
+- [x] Add Library row action: Install to Agent.
+- [x] Let Install to Agent choose from detected local user-level folder-format agents only.
+- [x] Reuse existing link/sync-to-agent backend for the one-skill Library row flow.
+- [x] Let Install to Agent disconnect an already linked Agent via the existing remove-link backend.
+- [x] Remove the bulk Sync to Agent button from the Agents tab.
+- [x] Change Agents selector buttons to icon-leading tab buttons with count badges.
+- [x] Clamp or omit long descriptions in the Agents skill table.
+- [x] Add reusable `SkillDetailDialog.vue` for Library and Agents rows.
+- [x] Render the selected `SKILL.md` body as Markdown inside the detail dialog.
+- [x] Make Library row non-control area open the detail dialog directly.
+- [x] Keep Library row Install to Agent and DeepChat enable/disable as exposed controls.
+- [x] Move mutable skill edit/save into `SkillDetailDialog.vue`.
+- [x] Remove the standalone `SkillEditorSheet.vue` path after merging edit into detail.
+- [x] Move mutable skill delete into `SkillDetailDialog.vue` with second confirmation.
+- [x] Keep Install to Agent and DeepChat enable/disable available inside the detail dialog.
+- [x] Add detail route/client methods for DeepChat skills and scanned agent skills.
+- [x] Rename the manual import/export UI copy to Sync Directory to separate it from agent install.
+- [x] Keep sync directory import/export backend intact for local multi-skill repository backup and
+ migration.
+- [x] Update all user-facing strings in every locale with local-language translations.
+- [x] Update renderer tests for three tabs, Add Skill Git install, Library Install to Agent, agent
+ icon tabs, detail dialog, and absence of Discover/Install tabs.
+- [x] Update main/contract tests for skill detail routes.
+- [x] Run `pnpm run format`.
+- [x] Run `pnpm run i18n`.
+- [x] Run `pnpm run lint`.
+- [x] Run targeted main and renderer tests for touched skills modules.
+
+## Deferred
+
+- [ ] Built-in Git commit/pull/push for the sync directory.
+- [ ] Automatic scheduled sync.
+- [ ] Custom agent path registry and UI.
+- [ ] Destructive overwrite/keep adoption conflict strategies.
+- [ ] Project-level agent adoption.
+- [ ] Single-file prompt adoption into folder-format skills.
+- [ ] Deep external marketplace search integration.
diff --git a/src/main/presenter/configPresenter/configDbStores.ts b/src/main/presenter/configPresenter/configDbStores.ts
index 51314a70a7..1fab300a2c 100644
--- a/src/main/presenter/configPresenter/configDbStores.ts
+++ b/src/main/presenter/configPresenter/configDbStores.ts
@@ -13,7 +13,8 @@ export const SENSITIVE_APP_SETTING_KEYS = [
'hooksNotifications',
'knowledgeConfigs',
'customPrompts',
- 'systemPrompts'
+ 'systemPrompts',
+ 'skills.managementState'
] as const
const SENSITIVE_APP_SETTING_KEY_SET = new Set(SENSITIVE_APP_SETTING_KEYS)
diff --git a/src/main/presenter/skillPresenter/index.ts b/src/main/presenter/skillPresenter/index.ts
index d3d7fdb736..5d555df09b 100644
--- a/src/main/presenter/skillPresenter/index.ts
+++ b/src/main/presenter/skillPresenter/index.ts
@@ -1,7 +1,9 @@
import { app, shell } from 'electron'
import path from 'path'
import fs from 'fs'
+import { execFile } from 'node:child_process'
import { randomUUID } from 'node:crypto'
+import { promisify } from 'node:util'
import matter from 'gray-matter'
import { unzipSync } from 'fflate'
import type { IConfigPresenter } from '@shared/presenter'
@@ -20,7 +22,18 @@ import {
SkillInstallResult,
SkillFolderNode,
SkillInstallOptions,
+ GitSkillInstallInput,
+ GitSkillRepoScanItem,
+ GitSkillRepoScanResult,
+ SkillAdoptionRegistration,
+ SkillAgentLinkRegistration,
SkillExtensionConfig,
+ SkillSyncDirectoryExportInput,
+ SkillSyncDirectoryExportPreview,
+ SkillSyncDirectoryImportInput,
+ SkillSyncDirectoryImportPreview,
+ SkillSyncDirectoryPreviewItem,
+ SkillSyncDirectoryResult,
SkillManageRequest,
SkillManageResult,
SkillDraftActionResult,
@@ -30,11 +43,21 @@ import {
SkillViewResult,
SkillLinkedFile
} from '@shared/types/skill'
+import type {
+ SkillManagementItem,
+ SkillManagementState,
+ SkillSyncDirectoryConfig,
+ SkillSource,
+ SkillSourceType,
+ UnifiedSkillItem
+} from '@shared/types/skillManagement'
import { publishDeepchatEvent } from '@/routes/publishDeepchatEvent'
import logger from '@shared/logger'
import { normalizeSkillAllowedTools } from './toolNameMapping'
import { discoverSkillMetadataInWorker, logSkillDiscoveryWorkerWarnings } from './discoveryWorker'
+const execFileAsync = promisify(execFile)
+
/**
* Skill system configuration constants
*/
@@ -112,6 +135,7 @@ const DRAFT_ALLOWED_TOP_LEVEL_DIRS = new Set(['references', 'templates', 'script
const DRAFT_CONVERSATION_ID_PATTERN = /^[A-Za-z0-9._-]+$/
const DRAFT_ID_PATTERN = /^[A-Za-z0-9._-]+$/
const DRAFT_ACTIVITY_MARKER = '.lastActivity'
+const SKILL_MANAGEMENT_STATE_KEY = 'skills.managementState'
const DRAFT_INJECTION_PATTERNS = [
/ignore\s+previous\s+instructions/i,
/disregard\s+all\s+prior/i,
@@ -282,9 +306,6 @@ export class SkillPresenter implements ISkillPresenter {
if (!fs.existsSync(this.skillsDir)) {
fs.mkdirSync(this.skillsDir, { recursive: true })
}
- if (!fs.existsSync(this.sidecarDir)) {
- fs.mkdirSync(this.sidecarDir, { recursive: true })
- }
}
/**
@@ -482,7 +503,181 @@ export class SkillPresenter implements ISkillPresenter {
}
private isSkillVisible(metadata: SkillMetadata): boolean {
- return Boolean(metadata)
+ return Boolean(metadata) && !this.isSkillDeepChatDisabled(metadata.name)
+ }
+
+ private createDefaultManagementState(): SkillManagementState {
+ return {
+ version: 1,
+ skills: {}
+ }
+ }
+
+ private getStoredManagementState(): SkillManagementState {
+ const stored = this.configPresenter.getSetting(SKILL_MANAGEMENT_STATE_KEY)
+ if (!stored || typeof stored !== 'object') {
+ return this.createDefaultManagementState()
+ }
+
+ const candidate = stored as Partial
+ const skills: Record = {}
+ for (const [name, item] of Object.entries(candidate.skills ?? {})) {
+ if (!this.isSafeSkillName(name) || !item || typeof item !== 'object') {
+ continue
+ }
+ const raw = item as Partial
+ skills[name] = {
+ name,
+ canonicalPath:
+ typeof raw.canonicalPath === 'string' && raw.canonicalPath.trim()
+ ? raw.canonicalPath
+ : path.join(this.skillsDir, name),
+ deepchat: {
+ disabled: raw.deepchat?.disabled === true
+ },
+ extension: sanitizeSkillExtensionConfig(raw.extension),
+ source: this.sanitizeSkillSource(raw.source),
+ agentLinks:
+ raw.agentLinks && typeof raw.agentLinks === 'object'
+ ? (raw.agentLinks as SkillManagementItem['agentLinks'])
+ : undefined
+ }
+ }
+
+ return {
+ version: 1,
+ skills,
+ sync: this.sanitizeSyncDirectoryConfig(candidate.sync)
+ }
+ }
+
+ private sanitizeSyncDirectoryConfig(value: unknown): SkillSyncDirectoryConfig | undefined {
+ const raw =
+ value && typeof value === 'object' ? (value as Partial) : {}
+ if (typeof raw.skillsDirectory !== 'string' || !raw.skillsDirectory.trim()) {
+ return undefined
+ }
+
+ return {
+ skillsDirectory: path.resolve(raw.skillsDirectory),
+ layout: 'multi-skill-repo',
+ lastExportAt: typeof raw.lastExportAt === 'string' ? raw.lastExportAt : null,
+ lastImportAt: typeof raw.lastImportAt === 'string' ? raw.lastImportAt : null
+ }
+ }
+
+ private saveManagementState(state: SkillManagementState): void {
+ this.configPresenter.setSetting(SKILL_MANAGEMENT_STATE_KEY, state)
+ }
+
+ private sanitizeSkillSource(value: unknown): SkillSource {
+ const raw = value && typeof value === 'object' ? (value as Partial) : {}
+ const source: SkillSource = {
+ type: this.normalizeSkillSourceType(raw.type)
+ }
+ if (typeof raw.repoUrl === 'string') source.repoUrl = raw.repoUrl
+ if (raw.repoFormat === 'single-skill' || raw.repoFormat === 'multi-skill') {
+ source.repoFormat = raw.repoFormat
+ }
+ if (typeof raw.agentId === 'string') source.agentId = raw.agentId
+ if (typeof raw.originalPath === 'string') source.originalPath = raw.originalPath
+ if (typeof raw.importedFrom === 'string') source.importedFrom = raw.importedFrom
+ if (typeof raw.installedAt === 'string') source.installedAt = raw.installedAt
+ if (typeof raw.importedAt === 'string') source.importedAt = raw.importedAt
+ if (typeof raw.adoptedAt === 'string') source.adoptedAt = raw.adoptedAt
+ return source
+ }
+
+ private normalizeSkillSourceType(value: unknown): SkillSourceType {
+ const allowed: SkillSourceType[] = [
+ 'builtin',
+ 'created',
+ 'folder-install',
+ 'zip-install',
+ 'url-install',
+ 'git-install',
+ 'adopted',
+ 'imported'
+ ]
+ return typeof value === 'string' && allowed.includes(value as SkillSourceType)
+ ? (value as SkillSourceType)
+ : 'created'
+ }
+
+ private createDefaultManagementItem(name: string): SkillManagementItem {
+ return {
+ name,
+ canonicalPath: path.join(this.skillsDir, name),
+ deepchat: {
+ disabled: false
+ },
+ extension: createDefaultSkillExtensionConfig(),
+ source: {
+ type: 'created'
+ }
+ }
+ }
+
+ private updateSkillManagementItem(
+ name: string,
+ updater: (item: SkillManagementItem) => SkillManagementItem
+ ): SkillManagementItem {
+ const state = this.getStoredManagementState()
+ const nextItem = updater(state.skills[name] ?? this.createDefaultManagementItem(name))
+ state.skills[name] = nextItem
+ this.saveManagementState(state)
+ return nextItem
+ }
+
+ private isSkillDeepChatDisabled(name: string): boolean {
+ return this.getStoredManagementState().skills[name]?.deepchat.disabled === true
+ }
+
+ async getSkillManagementState(): Promise {
+ return this.getStoredManagementState()
+ }
+
+ async setSkillDeepChatDisabled(name: string, disabled: boolean): Promise {
+ if (this.metadataCache.size === 0) {
+ await this.discoverSkills()
+ }
+ if (!this.metadataCache.has(name)) {
+ throw new Error(`Skill "${name}" not found`)
+ }
+
+ this.updateSkillManagementItem(name, (item) => ({
+ ...item,
+ canonicalPath: this.metadataCache.get(name)?.skillRoot ?? item.canonicalPath,
+ deepchat: {
+ ...item.deepchat,
+ disabled
+ }
+ }))
+ this.contentCache.delete(name)
+ publishDeepchatEvent('skills.catalog.changed', {
+ reason: 'disabled-updated',
+ name,
+ version: Date.now()
+ })
+ }
+
+ async getUnifiedSkillCatalog(): Promise {
+ if (this.metadataCache.size === 0) {
+ await this.discoverSkills()
+ }
+
+ const state = this.getStoredManagementState()
+ return this.sortSkillMetadata(Array.from(this.metadataCache.values())).map((skill) => {
+ const item = state.skills[skill.name] ?? this.createDefaultManagementItem(skill.name)
+ return {
+ ...skill,
+ canonicalPath: item.canonicalPath || skill.skillRoot,
+ sourceType: item.source.type,
+ deepchatDisabled: item.deepchat.disabled,
+ agentLinks: item.agentLinks ?? {},
+ mutable: !skill.ownerPluginId
+ }
+ })
}
private sortSkillMetadata(skills: SkillMetadata[]): SkillMetadata[] {
@@ -1080,7 +1275,7 @@ export class SkillPresenter implements ISkillPresenter {
continue
}
- const result = await this.installFromDirectory(skillDir, { overwrite: false })
+ const result = await this.installFromDirectory(skillDir, { overwrite: false }, 'builtin')
if (!result.success && result.error?.includes('already exists')) {
continue
}
@@ -1140,7 +1335,7 @@ export class SkillPresenter implements ISkillPresenter {
folderPath: string,
options?: SkillInstallOptions
): Promise {
- return this.installFromDirectory(folderPath, options)
+ return this.installFromDirectory(folderPath, options, 'folder-install')
}
/**
@@ -1161,7 +1356,7 @@ export class SkillPresenter implements ISkillPresenter {
if (!skillDir) {
return { success: false, error: 'SKILL.md not found in zip archive' }
}
- return await this.installFromDirectory(skillDir, options)
+ return await this.installFromDirectory(skillDir, options, 'zip-install')
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
return { success: false, error: errorMsg, errorCode: 'io_error' }
@@ -1177,7 +1372,17 @@ export class SkillPresenter implements ISkillPresenter {
const tempZipPath = path.join(app.getPath('temp'), `deepchat-skill-${Date.now()}.zip`)
try {
await this.downloadSkillZip(url, tempZipPath)
- return await this.installFromZip(tempZipPath, options)
+ const result = await this.installFromZip(tempZipPath, options)
+ if (result.success && result.skillName) {
+ this.updateSkillManagementItem(result.skillName, (item) => ({
+ ...item,
+ source: {
+ type: 'url-install',
+ installedAt: new Date().toISOString()
+ }
+ }))
+ }
+ return result
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
return { success: false, error: errorMsg, errorCode: 'io_error' }
@@ -1188,6 +1393,277 @@ export class SkillPresenter implements ISkillPresenter {
}
}
+ async scanGitSkillRepo(repoUrl: string): Promise {
+ const normalizedRepoUrl = repoUrl.trim()
+ if (!normalizedRepoUrl) {
+ throw new Error('Git repository URL is required')
+ }
+
+ const cloneDir = await this.cloneGitSkillRepo(normalizedRepoUrl)
+ try {
+ return await this.scanGitSkillRepoDirectory(normalizedRepoUrl, cloneDir)
+ } finally {
+ fs.rmSync(cloneDir, { recursive: true, force: true })
+ }
+ }
+
+ async installSkillsFromGit(input: GitSkillInstallInput): Promise {
+ const repoUrl = input.repoUrl.trim()
+ const selected = new Set(input.skillNames)
+ const strategy = input.strategy ?? 'rename'
+ if (!repoUrl || selected.size === 0) {
+ return []
+ }
+
+ const cloneDir = await this.cloneGitSkillRepo(repoUrl)
+ try {
+ const scan = await this.scanGitSkillRepoDirectory(repoUrl, cloneDir)
+ const selectedItems = scan.skills.filter((item) => selected.has(item.name))
+ const results: SkillInstallResult[] = []
+
+ for (const item of selectedItems) {
+ if (!item.valid) {
+ results.push({
+ success: false,
+ skillName: item.name,
+ error: item.error ?? 'Invalid skill',
+ errorCode: 'invalid_skill'
+ })
+ continue
+ }
+
+ if (item.conflict && strategy === 'skip') {
+ results.push({
+ success: false,
+ skillName: item.name,
+ existingSkillName: item.name,
+ error: `Skill "${item.name}" already exists`,
+ errorCode: 'conflict'
+ })
+ continue
+ }
+
+ const sourceDir =
+ scan.repoFormat === 'single-skill'
+ ? cloneDir
+ : path.join(cloneDir, item.relativePath.replace(/\/SKILL\.md$/, ''))
+ const targetName =
+ item.conflict && strategy === 'rename' ? this.createUniqueSkillName(item.name) : item.name
+ const result = await this.installFromDirectory(
+ sourceDir,
+ { overwrite: item.conflict && strategy === 'overwrite' },
+ 'git-install',
+ {
+ repoUrl,
+ repoFormat: scan.repoFormat,
+ installedAt: new Date().toISOString()
+ },
+ targetName
+ )
+ results.push(result)
+ }
+
+ if (results.some((result) => result.success)) {
+ publishDeepchatEvent('skills.catalog.changed', {
+ reason: 'git-installed',
+ version: Date.now()
+ })
+ }
+
+ return results
+ } catch (error) {
+ const errorMsg = error instanceof Error ? error.message : String(error)
+ return [{ success: false, error: errorMsg, errorCode: 'io_error' }]
+ } finally {
+ fs.rmSync(cloneDir, { recursive: true, force: true })
+ }
+ }
+
+ async getSkillsSyncConfig(): Promise {
+ return this.getStoredManagementState().sync ?? null
+ }
+
+ async setSkillsSyncDirectory(input: {
+ skillsDirectory: string
+ }): Promise {
+ const skillsDirectory = path.resolve(input.skillsDirectory.trim())
+ const config: SkillSyncDirectoryConfig = {
+ skillsDirectory,
+ layout: 'multi-skill-repo',
+ lastExportAt: null,
+ lastImportAt: null
+ }
+
+ fs.mkdirSync(path.join(skillsDirectory, 'skills'), { recursive: true })
+ const state = this.getStoredManagementState()
+ state.sync = {
+ ...state.sync,
+ ...config
+ }
+ this.saveManagementState(state)
+ publishDeepchatEvent('skills.catalog.changed', {
+ reason: 'sync-directory-updated',
+ version: Date.now()
+ })
+ return state.sync
+ }
+
+ async previewSyncDirectoryExport(
+ input: SkillSyncDirectoryExportInput
+ ): Promise {
+ const config = this.requireSyncDirectoryConfig()
+ const selected = new Set(input.skillNames)
+ const skills = (await this.getUnifiedSkillCatalog()).filter((skill) => {
+ if (!selected.has(skill.name)) return false
+ return input.includeDisabled === true || !skill.deepchatDisabled
+ })
+
+ return {
+ skillsDirectory: config.skillsDirectory,
+ items: skills.map((skill) => {
+ const targetPath = path.join(config.skillsDirectory, 'skills', skill.name)
+ if (!skill.mutable || !fs.existsSync(path.join(skill.skillRoot, 'SKILL.md'))) {
+ return {
+ name: skill.name,
+ state: 'invalid',
+ sourcePath: skill.skillRoot,
+ targetPath,
+ error: 'Skill cannot be exported'
+ }
+ }
+ return {
+ name: skill.name,
+ state: this.resolveExportPreviewState(skill.skillRoot, targetPath),
+ sourcePath: skill.skillRoot,
+ targetPath
+ }
+ })
+ }
+ }
+
+ async executeSyncDirectoryExport(
+ input: SkillSyncDirectoryExportInput
+ ): Promise {
+ const preview = await this.previewSyncDirectoryExport(input)
+ let exported = 0
+ let skipped = 0
+ const failed: Array<{ skillName: string; reason: string }> = []
+
+ fs.mkdirSync(path.join(preview.skillsDirectory, 'skills'), { recursive: true })
+ this.ensureSyncDirectoryReadme(preview.skillsDirectory)
+
+ for (const item of preview.items) {
+ if (item.state === 'invalid') {
+ skipped += 1
+ failed.push({ skillName: item.name, reason: item.error ?? 'Invalid skill' })
+ continue
+ }
+
+ try {
+ fs.rmSync(item.targetPath, { recursive: true, force: true })
+ this.copyDirectory(item.sourcePath, item.targetPath)
+ exported += 1
+ } catch (error) {
+ failed.push({
+ skillName: item.name,
+ reason: error instanceof Error ? error.message : String(error)
+ })
+ }
+ }
+
+ if (exported > 0) {
+ this.updateSyncDirectoryConfig({ lastExportAt: new Date().toISOString() })
+ }
+
+ return {
+ success: failed.length === 0,
+ exported,
+ skipped,
+ failed
+ }
+ }
+
+ async previewSyncDirectoryImport(): Promise {
+ const config = this.requireSyncDirectoryConfig()
+ const skillsRoot = path.join(config.skillsDirectory, 'skills')
+ const items: SkillSyncDirectoryPreviewItem[] = []
+ if (!fs.existsSync(skillsRoot)) {
+ return { skillsDirectory: config.skillsDirectory, items }
+ }
+
+ for (const entry of fs.readdirSync(skillsRoot, { withFileTypes: true })) {
+ if (!entry.isDirectory()) continue
+ const sourcePath = path.join(skillsRoot, entry.name)
+ const targetPath = path.join(this.skillsDir, entry.name)
+ items.push(this.createImportPreviewItem(sourcePath, targetPath))
+ }
+
+ return {
+ skillsDirectory: config.skillsDirectory,
+ items: items.sort((left, right) => left.name.localeCompare(right.name))
+ }
+ }
+
+ async executeSyncDirectoryImport(
+ input: SkillSyncDirectoryImportInput
+ ): Promise {
+ const preview = await this.previewSyncDirectoryImport()
+ const selected = new Set(input.skillNames)
+ const strategy = input.strategy ?? 'rename'
+ let imported = 0
+ let skipped = 0
+ const failed: Array<{ skillName: string; reason: string }> = []
+
+ for (const item of preview.items.filter((candidate) => selected.has(candidate.name))) {
+ if (item.state === 'invalid' || item.state === 'same') {
+ skipped += 1
+ if (item.state === 'invalid') {
+ failed.push({ skillName: item.name, reason: item.error ?? 'Invalid skill' })
+ }
+ continue
+ }
+
+ if ((item.state === 'conflict' || item.state === 'modified') && strategy === 'skip') {
+ skipped += 1
+ continue
+ }
+
+ const targetName =
+ (item.state === 'conflict' || item.state === 'modified') && strategy === 'rename'
+ ? this.createUniqueSkillName(item.name)
+ : item.name
+ const result = await this.installFromDirectory(
+ item.sourcePath,
+ { overwrite: strategy === 'overwrite' },
+ 'imported',
+ {
+ importedFrom: item.sourcePath,
+ importedAt: new Date().toISOString()
+ },
+ targetName
+ )
+ if (result.success) {
+ imported += 1
+ } else {
+ failed.push({
+ skillName: item.name,
+ reason: result.error ?? 'Import failed'
+ })
+ }
+ }
+
+ if (imported > 0) {
+ this.updateSyncDirectoryConfig({ lastImportAt: new Date().toISOString() })
+ }
+
+ return {
+ success: failed.length === 0,
+ imported,
+ skipped,
+ failed
+ }
+ }
+
async registerPluginSkill(input: {
ownerPluginId: string
id: string
@@ -1212,6 +1688,90 @@ export class SkillPresenter implements ISkillPresenter {
}
}
+ async registerAdoptedSkill(input: SkillAdoptionRegistration): Promise {
+ const skillRoot = path.resolve(input.canonicalPath)
+ const metadata = await this.parseSkillMetadata(path.join(skillRoot, 'SKILL.md'), input.name)
+ if (!metadata || metadata.name !== input.name) {
+ throw new Error(`Adopted skill "${input.name}" is invalid`)
+ }
+
+ this.metadataCache.set(input.name, metadata)
+ this.contentCache.delete(input.name)
+ this.updateSkillManagementItem(input.name, (item) => ({
+ ...item,
+ canonicalPath: skillRoot,
+ source: {
+ type: 'adopted',
+ agentId: input.agentId,
+ originalPath: input.originalPath,
+ adoptedAt: new Date().toISOString()
+ },
+ agentLinks: {
+ ...item.agentLinks,
+ [input.agentId]: {
+ path: input.agentPath,
+ state: 'linked',
+ createdByDeepChat: true,
+ linkedAt: new Date().toISOString()
+ }
+ }
+ }))
+
+ publishDeepchatEvent('skills.catalog.changed', {
+ reason: 'installed',
+ name: input.name,
+ skill: metadata,
+ version: Date.now()
+ })
+ }
+
+ async registerAgentSkillLink(input: SkillAgentLinkRegistration): Promise {
+ if (this.metadataCache.size === 0) {
+ await this.discoverSkills()
+ }
+ const metadata = this.metadataCache.get(input.skillName)
+ if (!metadata) {
+ throw new Error(`Skill "${input.skillName}" not found`)
+ }
+
+ this.updateSkillManagementItem(input.skillName, (item) => ({
+ ...item,
+ canonicalPath: metadata.skillRoot,
+ agentLinks: {
+ ...item.agentLinks,
+ [input.agentId]: {
+ path: input.agentPath,
+ state: 'linked',
+ createdByDeepChat: true,
+ linkedAt: new Date().toISOString()
+ }
+ }
+ }))
+
+ publishDeepchatEvent('skills.catalog.changed', {
+ reason: 'management-state-updated',
+ name: input.skillName,
+ version: Date.now()
+ })
+ }
+
+ async removeAgentSkillLink(input: { skillName: string; agentId: string }): Promise {
+ this.updateSkillManagementItem(input.skillName, (item) => {
+ const agentLinks = { ...item.agentLinks }
+ delete agentLinks[input.agentId]
+ return {
+ ...item,
+ agentLinks: Object.keys(agentLinks).length > 0 ? agentLinks : undefined
+ }
+ })
+
+ publishDeepchatEvent('skills.catalog.changed', {
+ reason: 'management-state-updated',
+ name: input.skillName,
+ version: Date.now()
+ })
+ }
+
async unregisterPluginSkillsByOwner(ownerPluginId: string): Promise {
let changed = false
for (const [key, contribution] of this.pluginSkillContributions.entries()) {
@@ -1233,7 +1793,10 @@ export class SkillPresenter implements ISkillPresenter {
private async installFromDirectory(
folderPath: string,
- options?: SkillInstallOptions
+ options?: SkillInstallOptions,
+ sourceType: SkillSourceType = 'folder-install',
+ sourcePatch: Partial = {},
+ targetName?: string
): Promise {
try {
this.ensureSkillsDir()
@@ -1285,15 +1848,24 @@ export class SkillPresenter implements ISkillPresenter {
}
}
- const targetDir = path.join(this.skillsDir, skillName)
+ const finalSkillName = targetName?.trim() || skillName
+ if (!this.isSafeSkillName(finalSkillName)) {
+ return {
+ success: false,
+ error: 'Invalid target skill name',
+ errorCode: 'invalid_skill'
+ }
+ }
+
+ const targetDir = path.join(this.skillsDir, finalSkillName)
const resolvedTarget = path.resolve(targetDir)
if (resolvedSource === resolvedTarget) {
return {
success: false,
- error: `Skill "${skillName}" already exists`,
+ error: `Skill "${finalSkillName}" already exists`,
errorCode: 'conflict',
- existingSkillName: skillName
+ existingSkillName: finalSkillName
}
}
@@ -1313,36 +1885,51 @@ export class SkillPresenter implements ISkillPresenter {
if (!options?.overwrite) {
return {
success: false,
- error: `Skill "${skillName}" already exists`,
+ error: `Skill "${finalSkillName}" already exists`,
errorCode: 'conflict',
- existingSkillName: skillName
+ existingSkillName: finalSkillName
}
}
- const replaceResult = this.prepareExistingSkillTargetForInstall(skillName, resolvedTarget)
+ const replaceResult = this.prepareExistingSkillTargetForInstall(
+ finalSkillName,
+ resolvedTarget
+ )
if (replaceResult) {
return replaceResult
}
- this.metadataCache.delete(skillName)
- this.contentCache.delete(skillName)
+ this.metadataCache.delete(finalSkillName)
+ this.contentCache.delete(finalSkillName)
}
this.copyDirectory(resolvedSource, resolvedTarget)
+ if (finalSkillName !== skillName) {
+ this.rewriteSkillManifestName(resolvedTarget, finalSkillName)
+ }
const metadata = await this.parseSkillMetadata(
path.join(resolvedTarget, 'SKILL.md'),
- skillName
+ finalSkillName
)
if (metadata) {
- this.metadataCache.set(skillName, metadata)
- }
+ this.metadataCache.set(finalSkillName, metadata)
+ }
+ this.updateSkillManagementItem(finalSkillName, (item) => ({
+ ...item,
+ canonicalPath: resolvedTarget,
+ source: {
+ type: sourceType,
+ installedAt: new Date().toISOString(),
+ ...sourcePatch
+ }
+ }))
publishDeepchatEvent('skills.catalog.changed', {
reason: 'installed',
- name: skillName,
+ name: finalSkillName,
version: Date.now()
})
- return { success: true, skillName }
+ return { success: true, skillName: finalSkillName, targetPath: resolvedTarget }
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
return { success: false, error: errorMsg, errorCode: 'io_error' }
@@ -1372,16 +1959,25 @@ export class SkillPresenter implements ISkillPresenter {
private backupExistingSkill(skillName: string): string {
const sourceDir = path.join(this.skillsDir, skillName)
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
- let backupDir = path.join(this.skillsDir, `${skillName}.backup-${timestamp}`)
+ const backupRoot = path.join(app.getPath('home'), '.deepchat', 'backups', 'skill-installs')
+ fs.mkdirSync(backupRoot, { recursive: true })
+ let backupDir = path.join(backupRoot, `${skillName}-${timestamp}`)
let counter = 0
while (fs.existsSync(backupDir)) {
counter += 1
- backupDir = path.join(this.skillsDir, `${skillName}.backup-${timestamp}-${counter}`)
+ backupDir = path.join(backupRoot, `${skillName}-${timestamp}-${counter}`)
}
fs.renameSync(sourceDir, backupDir)
return backupDir
}
+ private rewriteSkillManifestName(skillDir: string, name: string): void {
+ const skillPath = path.join(skillDir, 'SKILL.md')
+ const raw = fs.readFileSync(skillPath, 'utf-8')
+ const parsed = matter(raw)
+ fs.writeFileSync(skillPath, matter.stringify(parsed.content, { ...parsed.data, name }), 'utf-8')
+ }
+
private createTargetLockedFailure(
skillName: string,
targetPath: string,
@@ -1545,6 +2141,235 @@ export class SkillPresenter implements ISkillPresenter {
}
}
+ private async cloneGitSkillRepo(repoUrl: string): Promise {
+ const operationRoot = path.join(app.getPath('home'), '.deepchat', 'tmp', 'skill-installs')
+ fs.mkdirSync(operationRoot, { recursive: true })
+ const cloneDir = path.join(operationRoot, `${Date.now()}-${randomUUID()}`)
+ try {
+ await execFileAsync('git', ['clone', '--depth', '1', repoUrl, cloneDir], {
+ timeout: SKILL_CONFIG.DOWNLOAD_TIMEOUT
+ })
+ return cloneDir
+ } catch (error) {
+ fs.rmSync(cloneDir, { recursive: true, force: true })
+ const errorMsg = error instanceof Error ? error.message : String(error)
+ throw new Error(`Failed to clone Git repository: ${errorMsg}`)
+ }
+ }
+
+ private async scanGitSkillRepoDirectory(
+ repoUrl: string,
+ repoRoot: string
+ ): Promise {
+ const rootSkill = path.join(repoRoot, 'SKILL.md')
+ if (fs.existsSync(rootSkill)) {
+ return {
+ repoUrl,
+ repoFormat: 'single-skill',
+ skills: [this.createGitScanItem(repoRoot, 'SKILL.md')]
+ }
+ }
+
+ const skillsRoot = path.join(repoRoot, 'skills')
+ const skills = fs.existsSync(skillsRoot)
+ ? fs
+ .readdirSync(skillsRoot, { withFileTypes: true })
+ .filter((entry) => entry.isDirectory())
+ .map((entry) =>
+ this.createGitScanItem(
+ path.join(skillsRoot, entry.name),
+ path.join('skills', entry.name, 'SKILL.md')
+ )
+ )
+ : []
+
+ return {
+ repoUrl,
+ repoFormat: 'multi-skill',
+ skills: skills.sort((left, right) => left.name.localeCompare(right.name))
+ }
+ }
+
+ private createGitScanItem(skillDir: string, relativePath: string): GitSkillRepoScanItem {
+ const summary = this.readSkillManifestSummary(skillDir)
+ if (!summary.valid) {
+ return {
+ name: path.basename(skillDir),
+ description: '',
+ relativePath,
+ conflict: false,
+ valid: false,
+ error: summary.error
+ }
+ }
+
+ return {
+ name: summary.name,
+ description: summary.description,
+ relativePath,
+ conflict: fs.existsSync(path.join(this.skillsDir, summary.name)),
+ valid: true
+ }
+ }
+
+ private readSkillManifestSummary(
+ skillDir: string
+ ): { valid: true; name: string; description: string } | { valid: false; error: string } {
+ const skillPath = path.join(skillDir, 'SKILL.md')
+ if (!fs.existsSync(skillPath)) {
+ return { valid: false, error: 'SKILL.md not found' }
+ }
+
+ try {
+ const content = fs.readFileSync(skillPath, 'utf-8')
+ const { data } = matter(content)
+ const name = typeof data.name === 'string' ? data.name.trim() : ''
+ const description = typeof data.description === 'string' ? data.description.trim() : ''
+ if (!name || !description || !this.isSafeSkillName(name)) {
+ return { valid: false, error: 'Invalid SKILL.md frontmatter' }
+ }
+ return { valid: true, name, description }
+ } catch (error) {
+ return { valid: false, error: error instanceof Error ? error.message : String(error) }
+ }
+ }
+
+ private createUniqueSkillName(baseName: string): string {
+ let counter = 1
+ let candidate = `${baseName}-${counter}`
+ while (fs.existsSync(path.join(this.skillsDir, candidate))) {
+ counter += 1
+ candidate = `${baseName}-${counter}`
+ }
+ return candidate
+ }
+
+ private requireSyncDirectoryConfig(): SkillSyncDirectoryConfig {
+ const config = this.getStoredManagementState().sync
+ if (!config) {
+ throw new Error('Skills sync directory is not configured')
+ }
+ return config
+ }
+
+ private updateSyncDirectoryConfig(patch: Partial): void {
+ const state = this.getStoredManagementState()
+ if (!state.sync) {
+ throw new Error('Skills sync directory is not configured')
+ }
+ state.sync = {
+ ...state.sync,
+ ...patch
+ }
+ this.saveManagementState(state)
+ publishDeepchatEvent('skills.catalog.changed', {
+ reason: 'sync-directory-updated',
+ version: Date.now()
+ })
+ }
+
+ private ensureSyncDirectoryReadme(syncDirectory: string): void {
+ const readmePath = path.join(syncDirectory, 'README.md')
+ if (!fs.existsSync(readmePath)) {
+ fs.writeFileSync(
+ readmePath,
+ '# DeepChat Skills\n\nThis directory stores portable DeepChat skills under `skills/`.\n',
+ 'utf-8'
+ )
+ }
+ }
+
+ private resolveExportPreviewState(
+ sourcePath: string,
+ targetPath: string
+ ): SkillSyncDirectoryPreviewItem['state'] {
+ if (!fs.existsSync(targetPath)) {
+ return 'new'
+ }
+ return this.areSkillDirectoriesSame(sourcePath, targetPath) ? 'same' : 'modified'
+ }
+
+ private createImportPreviewItem(
+ sourcePath: string,
+ fallbackTargetPath: string
+ ): SkillSyncDirectoryPreviewItem {
+ const summary = this.readSkillManifestSummary(sourcePath)
+ if (!summary.valid) {
+ return {
+ name: path.basename(sourcePath),
+ state: 'invalid',
+ sourcePath,
+ targetPath: fallbackTargetPath,
+ error: summary.error
+ }
+ }
+
+ const targetPath = path.join(this.skillsDir, summary.name)
+ if (!fs.existsSync(targetPath)) {
+ return {
+ name: summary.name,
+ state: 'new',
+ sourcePath,
+ targetPath
+ }
+ }
+
+ if (this.areSkillDirectoriesSame(sourcePath, targetPath)) {
+ return {
+ name: summary.name,
+ state: 'same',
+ sourcePath,
+ targetPath
+ }
+ }
+
+ const existingSource = this.getStoredManagementState().skills[summary.name]?.source
+ const state =
+ existingSource?.type === 'imported' && existingSource.importedFrom === sourcePath
+ ? 'modified'
+ : 'conflict'
+ return {
+ name: summary.name,
+ state,
+ sourcePath,
+ targetPath
+ }
+ }
+
+ private areSkillDirectoriesSame(left: string, right: string): boolean {
+ try {
+ return this.createSkillDirectorySnapshot(left) === this.createSkillDirectorySnapshot(right)
+ } catch {
+ return false
+ }
+ }
+
+ private createSkillDirectorySnapshot(root: string): string {
+ return this.collectSkillDirectoryFiles(root)
+ .sort()
+ .map((relativePath) => {
+ const content = fs.readFileSync(path.join(root, relativePath)).toString('base64')
+ return `${relativePath}\0${content}`
+ })
+ .join('\0')
+ }
+
+ private collectSkillDirectoryFiles(root: string, current: string = root): string[] {
+ const files: string[] = []
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
+ if (entry.isSymbolicLink() || entry.name === SKILL_CONFIG.SIDECAR_DIR) {
+ continue
+ }
+ const fullPath = path.join(current, entry.name)
+ if (entry.isDirectory()) {
+ files.push(...this.collectSkillDirectoryFiles(root, fullPath))
+ } else {
+ files.push(path.relative(root, fullPath))
+ }
+ }
+ return files
+ }
+
/**
* Uninstall a skill
*/
@@ -1584,9 +2409,9 @@ export class SkillPresenter implements ISkillPresenter {
private cleanupUninstalledSkillState(name: string): void {
if (this.isSafeSkillName(name)) {
try {
- this.deleteSkillExtension(name)
+ this.deleteSkillManagementItem(name)
} catch (error) {
- logger.warn('[SkillPresenter] Failed to delete skill sidecar after uninstall', {
+ logger.warn('[SkillPresenter] Failed to delete skill management state after uninstall', {
name,
error
})
@@ -1642,15 +2467,17 @@ export class SkillPresenter implements ISkillPresenter {
return { success: false, error: `Skill "${name}" not found` }
}
- const sidecarPath = this.getSidecarPath(name)
const previousSkillContent = fs.readFileSync(metadata.path, 'utf-8')
- const hadSidecar = fs.existsSync(sidecarPath)
- const previousSidecarContent = hadSidecar ? fs.readFileSync(sidecarPath, 'utf-8') : null
+ const previousState = this.getStoredManagementState()
const sanitized = sanitizeSkillExtensionConfig(config)
try {
fs.writeFileSync(metadata.path, content, 'utf-8')
- fs.writeFileSync(sidecarPath, JSON.stringify(sanitized, null, 2), 'utf-8')
+ this.updateSkillManagementItem(name, (item) => ({
+ ...item,
+ canonicalPath: metadata.skillRoot,
+ extension: sanitized
+ }))
this.contentCache.delete(name)
const newMetadata = await this.parseSkillMetadata(metadata.path, name)
@@ -1664,11 +2491,7 @@ export class SkillPresenter implements ISkillPresenter {
try {
fs.writeFileSync(metadata.path, previousSkillContent, 'utf-8')
- if (hadSidecar && previousSidecarContent !== null) {
- fs.writeFileSync(sidecarPath, previousSidecarContent, 'utf-8')
- } else if (fs.existsSync(sidecarPath)) {
- fs.rmSync(sidecarPath, { force: true })
- }
+ this.saveManagementState(previousState)
} catch (rollbackError) {
const rollbackMessage =
rollbackError instanceof Error ? rollbackError.message : String(rollbackError)
@@ -1776,14 +2599,36 @@ export class SkillPresenter implements ISkillPresenter {
async getSkillExtension(name: string): Promise {
this.ensureSkillsDir()
+ const item = this.getStoredManagementState().skills[name]
+ if (item) {
+ return sanitizeSkillExtensionConfig(item.extension)
+ }
+
+ return await this.migrateLegacySkillExtension(name)
+ }
+
+ private async migrateLegacySkillExtension(name: string): Promise {
const sidecarPath = this.getSidecarPath(name)
if (!(await this.pathExists(sidecarPath))) {
return createDefaultSkillExtensionConfig()
}
-
try {
const content = await fs.promises.readFile(sidecarPath, 'utf-8')
- return sanitizeSkillExtensionConfig(JSON.parse(content))
+ const config = sanitizeSkillExtensionConfig(JSON.parse(content))
+ this.updateSkillManagementItem(name, (item) => ({
+ ...item,
+ extension: config
+ }))
+ try {
+ fs.rmSync(sidecarPath, { force: true })
+ this.removeLegacySidecarDirIfEmpty()
+ } catch (cleanupError) {
+ logger.warn('[SkillPresenter] Failed to remove migrated skill sidecar', {
+ name,
+ error: cleanupError
+ })
+ }
+ return config
} catch (error) {
logger.warn('[SkillPresenter] Failed to read skill sidecar, using defaults', {
name,
@@ -1793,6 +2638,16 @@ export class SkillPresenter implements ISkillPresenter {
}
}
+ private removeLegacySidecarDirIfEmpty(): void {
+ try {
+ if (fs.existsSync(this.sidecarDir) && fs.readdirSync(this.sidecarDir).length === 0) {
+ fs.rmSync(this.sidecarDir, { force: true, recursive: false })
+ }
+ } catch {
+ // Keep legacy residue for the next migration attempt.
+ }
+ }
+
async saveSkillExtension(name: string, config: SkillExtensionConfig): Promise {
this.ensureSkillsDir()
if (this.metadataCache.size === 0) {
@@ -1804,7 +2659,12 @@ export class SkillPresenter implements ISkillPresenter {
}
const sanitized = sanitizeSkillExtensionConfig(config)
- fs.writeFileSync(this.getSidecarPath(name), JSON.stringify(sanitized, null, 2), 'utf-8')
+ const metadata = this.metadataCache.get(name)
+ this.updateSkillManagementItem(name, (item) => ({
+ ...item,
+ canonicalPath: metadata?.skillRoot ?? item.canonicalPath,
+ extension: sanitized
+ }))
this.contentCache.delete(name)
}
@@ -2236,10 +3096,11 @@ export class SkillPresenter implements ISkillPresenter {
return path.join(this.sidecarDir, `${name}.json`)
}
- private deleteSkillExtension(name: string): void {
- const sidecarPath = this.getSidecarPath(name)
- if (fs.existsSync(sidecarPath)) {
- fs.rmSync(sidecarPath, { force: true })
+ private deleteSkillManagementItem(name: string): void {
+ const state = this.getStoredManagementState()
+ if (state.skills[name]) {
+ delete state.skills[name]
+ this.saveManagementState(state)
}
}
diff --git a/src/main/presenter/skillSyncPresenter/index.ts b/src/main/presenter/skillSyncPresenter/index.ts
index 292aa2e48e..05ba17e129 100644
--- a/src/main/presenter/skillSyncPresenter/index.ts
+++ b/src/main/presenter/skillSyncPresenter/index.ts
@@ -11,7 +11,9 @@ import logger from '@shared/logger'
import * as fs from 'fs'
import * as path from 'path'
+import { randomUUID } from 'node:crypto'
import { app } from 'electron'
+import matter from 'gray-matter'
import type {
ISkillSyncPresenter,
ExternalToolConfig,
@@ -22,17 +24,38 @@ import type {
CanonicalSkill,
ExternalSkillInfo,
ScanCache,
- NewDiscovery
+ NewDiscovery,
+ InstalledSkillAgent,
+ InstalledSkillAgentDetail,
+ AgentSkillItem,
+ AdoptAgentSkillInput,
+ AdoptAgentSkillPreview,
+ AdoptAgentSkillResult,
+ AgentSkillLinkInput,
+ LinkDeepChatSkillResult,
+ LinkDeepChatSkillsInput,
+ LinkDeepChatSkillsPreview,
+ LinkDeepChatSkillsResult,
+ SkillDetail
} from '@shared/types/skillSync'
import { ConflictStrategy } from '@shared/types/skillSync'
+import type { UnifiedSkillItem } from '@shared/types/skillManagement'
import type { ISkillPresenter, IConfigPresenter } from '@shared/presenter'
import { toolScanner, resolveSkillsDir } from './toolScanner'
import { formatConverter } from './formatConverter'
import type { SyncContext } from './types'
import { publishDeepchatEvent } from '@/routes/publishDeepchatEvent'
-import { isValidToolId, isValidConflictStrategy, checkWritePermission } from './security'
+import {
+ isValidToolId,
+ isValidConflictStrategy,
+ checkWritePermission,
+ checkReadPermission,
+ isFilenameSafe
+} from './security'
import { scanAndDetectDiscoveriesInWorker, scanExternalToolsInWorker } from './scanWorker'
+const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/
+
type SkillSyncEventName =
| 'skillSync.discoveries.changed'
| 'skillSync.scan.started'
@@ -734,6 +757,354 @@ export class SkillSyncPresenter implements ISkillSyncPresenter {
return toolScanner.getAllTools()
}
+ async scanSkillAgents(): Promise {
+ const results = await this.scanExternalToolsWithFallback()
+ const resultByTool = new Map(results.map((result) => [result.toolId, result]))
+ const agents: InstalledSkillAgent[] = []
+
+ for (const tool of this.getManageableAgentTools()) {
+ const result =
+ resultByTool.get(tool.id) ??
+ (await toolScanner.scanTool(tool.id, this.syncContext.projectRoot))
+ const detail = await this.buildAgentDetail(tool, result)
+ const { skills: _skills, ...summary } = detail
+ agents.push(summary)
+ }
+
+ return agents
+ }
+
+ async scanSkillAgent(input: { agentId: string }): Promise {
+ const tool = toolScanner.getTool(input.agentId)
+ if (!tool || !this.canManageAgentLinks(tool)) {
+ return {
+ id: input.agentId,
+ name: input.agentId,
+ skillsDir: '',
+ isCustom: false,
+ supportsLinkManagement: false,
+ skillsCount: 0,
+ linkedCount: 0,
+ agentOwnedCount: 0,
+ conflictCount: 0,
+ brokenLinkCount: 0,
+ status: 'detected-no-skills-dir',
+ skills: []
+ }
+ }
+
+ return this.buildAgentDetail(
+ tool,
+ await toolScanner.scanTool(tool.id, this.syncContext.projectRoot)
+ )
+ }
+
+ async getAgentSkillDetail(input: { agentId: string; skillName: string }): Promise {
+ const detail = await this.scanSkillAgent({ agentId: input.agentId })
+ const skill = detail.skills.find((item) => item.name === input.skillName)
+ if (!skill) {
+ throw new Error(`Skill "${input.skillName}" not found in ${detail.name}`)
+ }
+
+ const markdownPath = path.join(skill.path, 'SKILL.md')
+ const markdown = await fs.promises.readFile(markdownPath, 'utf-8')
+ return {
+ name: skill.name,
+ description: skill.description ?? '',
+ sourcePath: markdownPath,
+ markdown,
+ mutable: skill.owner !== 'broken-link'
+ }
+ }
+
+ async previewAdoptAgentSkill(input: AdoptAgentSkillInput): Promise {
+ const adoption = await this.resolveAdoptionSource(input)
+ const source = await this.readAdoptableSkill(adoption.sourcePath)
+ if (source.name !== adoption.skill.name) {
+ throw new Error(`SKILL.md name "${source.name}" does not match "${adoption.skill.name}"`)
+ }
+
+ const skillsDir = path.resolve(await this.skillPresenter.getSkillsDir())
+ const deepchatSkills = await this.skillPresenter.getUnifiedSkillCatalog()
+ const deepchatNames = new Set(deepchatSkills.map((skill) => skill.name))
+ const hasConflict =
+ deepchatNames.has(source.name) || (await this.pathExists(path.join(skillsDir, source.name)))
+ const targetName =
+ input.targetName ??
+ (hasConflict
+ ? await this.generateAdoptionTargetName(
+ `${source.name}-${input.agentId}`,
+ skillsDir,
+ deepchatNames
+ )
+ : source.name)
+
+ this.assertValidDeepChatSkillName(targetName)
+ if (
+ deepchatNames.has(targetName) ||
+ (await this.pathExists(path.join(skillsDir, targetName)))
+ ) {
+ throw new Error(`Skill "${targetName}" already exists`)
+ }
+
+ const dataRoot = path.dirname(skillsDir)
+ const targetPath = path.join(skillsDir, targetName)
+
+ return {
+ agentId: input.agentId,
+ agentName: adoption.agent.name,
+ skillName: adoption.skill.name,
+ targetName,
+ sourcePath: adoption.sourcePath,
+ agentPath: adoption.agentPath,
+ targetPath,
+ backupRoot: path.join(
+ dataRoot,
+ 'backups',
+ 'skill-adoptions',
+ input.agentId,
+ adoption.skill.name
+ ),
+ conflict: hasConflict,
+ warnings: targetName === source.name ? [] : [`Skill will be adopted as "${targetName}"`]
+ }
+ }
+
+ async executeAdoptAgentSkill(input: AdoptAgentSkillInput): Promise {
+ let tempPath = ''
+ let targetCreated = false
+ let originalMoved = false
+ let preview: AdoptAgentSkillPreview | undefined
+ let backupPath = ''
+
+ try {
+ preview = await this.previewAdoptAgentSkill(input)
+ const operationId = `${Date.now()}-${randomUUID()}`
+ const dataRoot = path.dirname(path.resolve(await this.skillPresenter.getSkillsDir()))
+ tempPath = path.join(dataRoot, 'tmp', 'skill-adoptions', operationId)
+ backupPath = path.join(preview.backupRoot, operationId)
+
+ await fs.promises.mkdir(path.dirname(tempPath), { recursive: true })
+ await fs.promises.mkdir(path.dirname(backupPath), { recursive: true })
+ await this.prepareAdoptionTemp(preview.sourcePath, tempPath, preview.targetName)
+
+ if (await this.pathExists(preview.targetPath)) {
+ throw new Error(`Skill "${preview.targetName}" already exists`)
+ }
+
+ await fs.promises.mkdir(path.dirname(preview.targetPath), { recursive: true })
+ await fs.promises.rename(tempPath, preview.targetPath)
+ targetCreated = true
+
+ try {
+ await fs.promises.rename(preview.agentPath, backupPath)
+ originalMoved = true
+ await this.createDirectoryLink(preview.targetPath, preview.agentPath)
+ } catch (error) {
+ if (originalMoved && !(await this.pathExists(preview.agentPath))) {
+ await fs.promises.rename(backupPath, preview.agentPath).catch(() => undefined)
+ }
+ if (targetCreated) {
+ await fs.promises.rm(preview.targetPath, { recursive: true, force: true })
+ }
+ throw error
+ }
+
+ await this.skillPresenter.registerAdoptedSkill({
+ name: preview.targetName,
+ canonicalPath: preview.targetPath,
+ agentId: preview.agentId,
+ agentPath: preview.agentPath,
+ originalPath: preview.sourcePath
+ })
+
+ return {
+ success: true,
+ skillName: preview.targetName,
+ targetPath: preview.targetPath,
+ agentPath: preview.agentPath,
+ backupPath
+ }
+ } catch (error) {
+ if (tempPath) {
+ await fs.promises.rm(tempPath, { recursive: true, force: true }).catch(() => undefined)
+ }
+ return {
+ success: false,
+ skillName: preview?.targetName,
+ targetPath: preview?.targetPath,
+ agentPath: preview?.agentPath,
+ backupPath: backupPath || undefined,
+ error: error instanceof Error ? error.message : String(error)
+ }
+ }
+ }
+
+ async previewLinkDeepChatSkills(
+ input: LinkDeepChatSkillsInput
+ ): Promise {
+ const tool = this.resolveManageableAgentTool(input.agentId)
+ const detail = await this.scanSkillAgent({ agentId: input.agentId })
+ const skillsDir = detail.skillsDir || resolveSkillsDir(tool, this.syncContext.projectRoot)
+ const existingByName = new Map(detail.skills.map((skill) => [skill.name, skill]))
+ const deepchatByName = new Map(
+ (await this.skillPresenter.getUnifiedSkillCatalog()).map((skill) => [skill.name, skill])
+ )
+
+ return {
+ agentId: input.agentId,
+ agentName: tool.name,
+ skillsDir,
+ items: await Promise.all(
+ [...new Set(input.skillNames)].map(async (skillName) => {
+ this.assertValidDeepChatSkillName(skillName)
+ const deepchat = deepchatByName.get(skillName)
+ const targetPath = path.join(skillsDir, skillName)
+ if (!deepchat) {
+ return {
+ skillName,
+ targetPath,
+ status: 'missing',
+ message: `Skill "${skillName}" not found in DeepChat`
+ }
+ }
+
+ const existing = existingByName.get(skillName)
+ if (!existing) {
+ return {
+ skillName,
+ sourcePath: deepchat.skillRoot,
+ targetPath,
+ status: 'ready'
+ }
+ }
+
+ if (
+ existing.status === 'linked' &&
+ existing.link?.targetPath &&
+ path.resolve(existing.link.targetPath) === path.resolve(deepchat.skillRoot)
+ ) {
+ return {
+ skillName,
+ sourcePath: deepchat.skillRoot,
+ targetPath,
+ status: 'already-linked'
+ }
+ }
+
+ return {
+ skillName,
+ sourcePath: deepchat.skillRoot,
+ targetPath,
+ status: 'conflict',
+ message: `Agent path already exists: ${targetPath}`
+ }
+ })
+ )
+ }
+ }
+
+ async executeLinkDeepChatSkills(
+ input: LinkDeepChatSkillsInput
+ ): Promise {
+ const preview = await this.previewLinkDeepChatSkills(input)
+ const result: LinkDeepChatSkillsResult = {
+ success: true,
+ linked: 0,
+ skipped: 0,
+ failed: []
+ }
+
+ await fs.promises.mkdir(preview.skillsDir, { recursive: true })
+ if (!(await checkWritePermission(preview.skillsDir))) {
+ return {
+ success: false,
+ linked: 0,
+ skipped: 0,
+ failed: input.skillNames.map((skillName) => ({
+ skillName,
+ reason: `No write permission for: ${preview.skillsDir}`
+ }))
+ }
+ }
+
+ for (const item of preview.items) {
+ if (item.status === 'already-linked') {
+ result.skipped += 1
+ continue
+ }
+ if (item.status !== 'ready' || !item.sourcePath) {
+ result.skipped += 1
+ continue
+ }
+
+ try {
+ await this.createDirectoryLink(item.sourcePath, item.targetPath)
+ await this.skillPresenter.registerAgentSkillLink({
+ skillName: item.skillName,
+ agentId: input.agentId,
+ agentPath: item.targetPath
+ })
+ result.linked += 1
+ } catch (error) {
+ result.failed.push({
+ skillName: item.skillName,
+ reason: error instanceof Error ? error.message : String(error)
+ })
+ }
+ }
+
+ result.success = result.failed.length === 0
+ return result
+ }
+
+ async repairAgentSkillLink(input: AgentSkillLinkInput): Promise {
+ try {
+ const link = await this.resolveDeepChatOwnedAgentLink(input)
+ await this.assertAgentPathIsLinkOrMissing(link.agentPath)
+ await fs.promises.rm(link.agentPath, { recursive: true, force: true })
+ await this.createDirectoryLink(link.targetPath, link.agentPath)
+ await this.skillPresenter.registerAgentSkillLink({
+ skillName: input.skillName,
+ agentId: input.agentId,
+ agentPath: link.agentPath
+ })
+ return {
+ success: true,
+ skillName: input.skillName,
+ agentPath: link.agentPath,
+ targetPath: link.targetPath
+ }
+ } catch (error) {
+ return {
+ success: false,
+ skillName: input.skillName,
+ error: error instanceof Error ? error.message : String(error)
+ }
+ }
+ }
+
+ async removeAgentSkillLink(input: AgentSkillLinkInput): Promise {
+ try {
+ const link = await this.resolveDeepChatOwnedAgentLink(input)
+ await this.assertAgentPathIsLinkOrMissing(link.agentPath)
+ await fs.promises.rm(link.agentPath, { recursive: true, force: true })
+ await this.skillPresenter.removeAgentSkillLink(input)
+ return {
+ success: true,
+ skillName: input.skillName,
+ agentPath: link.agentPath,
+ targetPath: link.targetPath
+ }
+ } catch (error) {
+ return {
+ success: false,
+ skillName: input.skillName,
+ error: error instanceof Error ? error.message : String(error)
+ }
+ }
+ }
+
/**
* Check if a tool's directory exists
*/
@@ -753,6 +1124,440 @@ export class SkillSyncPresenter implements ISkillSyncPresenter {
// Private Helper Methods
// ============================================================================
+ private async resolveAdoptionSource(input: AdoptAgentSkillInput): Promise<{
+ agent: InstalledSkillAgentDetail
+ skill: AgentSkillItem
+ sourcePath: string
+ agentPath: string
+ }> {
+ const tool = toolScanner.getTool(input.agentId)
+ if (!tool || !this.canManageAgentLinks(tool)) {
+ throw new Error(`Agent "${input.agentId}" does not support skill adoption`)
+ }
+
+ const agent = await this.scanSkillAgent({ agentId: input.agentId })
+ const skill = agent.skills.find((item) => item.name === input.skillName)
+ if (!skill) {
+ throw new Error(`Skill "${input.skillName}" not found in ${agent.name}`)
+ }
+ if (!['agent-owned', 'linked-out', 'conflict'].includes(skill.status)) {
+ throw new Error(`Skill "${input.skillName}" cannot be adopted from status "${skill.status}"`)
+ }
+ if (!this.isInsideDirectory(skill.path, agent.skillsDir)) {
+ throw new Error(`Agent path escapes skills directory: ${skill.path}`)
+ }
+
+ const sourcePath = skill.status === 'linked-out' ? skill.link?.targetPath : skill.path
+ if (!sourcePath) {
+ throw new Error(`Skill "${input.skillName}" source path is unavailable`)
+ }
+ if (!(await checkReadPermission(sourcePath))) {
+ throw new Error(`No read permission for: ${sourcePath}`)
+ }
+
+ return {
+ agent,
+ skill,
+ sourcePath,
+ agentPath: skill.path
+ }
+ }
+
+ private resolveManageableAgentTool(agentId: string): ExternalToolConfig {
+ const tool = toolScanner.getTool(agentId)
+ if (!tool || !this.canManageAgentLinks(tool)) {
+ throw new Error(`Agent "${agentId}" does not support skill links`)
+ }
+ return tool
+ }
+
+ private async resolveDeepChatOwnedAgentLink(input: AgentSkillLinkInput): Promise<{
+ agentPath: string
+ targetPath: string
+ }> {
+ this.assertValidDeepChatSkillName(input.skillName)
+ const tool = this.resolveManageableAgentTool(input.agentId)
+ const skillsDir = resolveSkillsDir(tool, this.syncContext.projectRoot)
+ const state = await this.skillPresenter.getSkillManagementState()
+ const link = state.skills[input.skillName]?.agentLinks?.[input.agentId]
+ if (!link?.createdByDeepChat) {
+ throw new Error(`Link for "${input.skillName}" was not created by DeepChat`)
+ }
+
+ const deepchat = (await this.skillPresenter.getUnifiedSkillCatalog()).find(
+ (skill) => skill.name === input.skillName
+ )
+ if (!deepchat || !(await this.pathExists(deepchat.skillRoot))) {
+ throw new Error(`DeepChat skill "${input.skillName}" not found`)
+ }
+
+ if (!this.isInsideDirectory(link.path, skillsDir)) {
+ throw new Error(`Agent link path escapes skills directory: ${link.path}`)
+ }
+
+ return {
+ agentPath: link.path,
+ targetPath: deepchat.skillRoot
+ }
+ }
+
+ private async assertAgentPathIsLinkOrMissing(agentPath: string): Promise {
+ try {
+ await fs.promises.readlink(agentPath)
+ return
+ } catch {
+ if (await this.pathExists(agentPath)) {
+ throw new Error(`Agent path is not a link: ${agentPath}`)
+ }
+ }
+ }
+
+ private async readAdoptableSkill(skillRoot: string): Promise<{
+ name: string
+ description: string
+ parsed: matter.GrayMatterFile
+ }> {
+ const skillPath = path.join(skillRoot, 'SKILL.md')
+ const content = await fs.promises.readFile(skillPath, 'utf-8')
+ const parsed = matter(content)
+ const name = typeof parsed.data.name === 'string' ? parsed.data.name.trim() : ''
+ const description =
+ typeof parsed.data.description === 'string' ? parsed.data.description.trim() : ''
+ this.assertValidDeepChatSkillName(name)
+ if (!description) {
+ throw new Error('Skill description not found in SKILL.md frontmatter')
+ }
+ return { name, description, parsed }
+ }
+
+ private assertValidDeepChatSkillName(name: string): void {
+ if (!SKILL_NAME_PATTERN.test(name) || name.includes('/') || name.includes('\\')) {
+ throw new Error(`Invalid skill name: ${name}`)
+ }
+ }
+
+ private async generateAdoptionTargetName(
+ baseName: string,
+ skillsDir: string,
+ existingNames: Set
+ ): Promise {
+ this.assertValidDeepChatSkillName(baseName)
+ let candidate = baseName
+ let counter = 2
+ while (
+ existingNames.has(candidate) ||
+ (await this.pathExists(path.join(skillsDir, candidate)))
+ ) {
+ candidate = `${baseName}-${counter}`
+ counter += 1
+ }
+ return candidate
+ }
+
+ private async prepareAdoptionTemp(
+ sourcePath: string,
+ tempPath: string,
+ targetName: string
+ ): Promise {
+ await fs.promises.rm(tempPath, { recursive: true, force: true })
+ await this.copyDirectoryWithoutSymlinks(sourcePath, tempPath)
+ const copied = await this.readAdoptableSkill(tempPath)
+ if (copied.name !== targetName) {
+ copied.parsed.data.name = targetName
+ await fs.promises.writeFile(
+ path.join(tempPath, 'SKILL.md'),
+ matter.stringify(copied.parsed.content, copied.parsed.data),
+ 'utf-8'
+ )
+ }
+ }
+
+ private async copyDirectoryWithoutSymlinks(
+ sourcePath: string,
+ targetPath: string
+ ): Promise {
+ await fs.promises.mkdir(targetPath, { recursive: true })
+ const entries = await fs.promises.readdir(sourcePath, { withFileTypes: true })
+ for (const entry of entries) {
+ if (entry.isSymbolicLink() || entry.name === '.deepchat-meta') {
+ continue
+ }
+ const sourceEntry = path.join(sourcePath, entry.name)
+ const targetEntry = path.join(targetPath, entry.name)
+ if (entry.isDirectory()) {
+ await this.copyDirectoryWithoutSymlinks(sourceEntry, targetEntry)
+ } else if (entry.isFile()) {
+ await fs.promises.copyFile(sourceEntry, targetEntry)
+ }
+ }
+ }
+
+ private async createDirectoryLink(targetPath: string, linkPath: string): Promise {
+ await fs.promises.symlink(
+ targetPath,
+ linkPath,
+ process.platform === 'win32' ? 'junction' : 'dir'
+ )
+ }
+
+ private getManageableAgentTools(): ExternalToolConfig[] {
+ return toolScanner.getAllTools().filter((tool) => this.canManageAgentLinks(tool))
+ }
+
+ private canManageAgentLinks(tool: ExternalToolConfig): boolean {
+ return (
+ !tool.isProjectLevel &&
+ tool.filePattern === '*/SKILL.md' &&
+ tool.capabilities.supportsSubfolders
+ )
+ }
+
+ private async buildAgentDetail(
+ tool: ExternalToolConfig,
+ result: ScanResult
+ ): Promise {
+ if (!result.available) {
+ return this.createAgentDetail(
+ tool,
+ result.skillsDir || tool.skillsDir,
+ 'detected-no-skills-dir',
+ []
+ )
+ }
+
+ const skills = await this.classifyAgentSkills(result)
+ const status = skills.some((skill) => skill.status === 'empty') ? 'permission-denied' : 'ready'
+ return this.createAgentDetail(
+ tool,
+ result.skillsDir,
+ status,
+ skills.filter((skill) => skill.status !== 'empty')
+ )
+ }
+
+ private createAgentDetail(
+ tool: ExternalToolConfig,
+ skillsDir: string,
+ status: InstalledSkillAgent['status'],
+ skills: AgentSkillItem[]
+ ): InstalledSkillAgentDetail {
+ return {
+ id: tool.id,
+ name: tool.name,
+ skillsDir,
+ isCustom: false,
+ supportsLinkManagement: this.canManageAgentLinks(tool),
+ skillsCount: skills.length,
+ linkedCount: skills.filter((skill) => skill.status === 'linked').length,
+ agentOwnedCount: skills.filter((skill) => skill.status === 'agent-owned').length,
+ conflictCount: skills.filter((skill) => skill.status === 'conflict').length,
+ brokenLinkCount: skills.filter((skill) => skill.status === 'broken-link').length,
+ status,
+ skills
+ }
+ }
+
+ private async classifyAgentSkills(result: ScanResult): Promise {
+ const deepchatSkills = await this.skillPresenter.getUnifiedSkillCatalog()
+ const deepchatByName = new Map(deepchatSkills.map((skill) => [skill.name, skill]))
+ const deepchatSkillsDir = path.resolve(await this.skillPresenter.getSkillsDir())
+ const scannedByPath = new Map(result.skills.map((skill) => [path.resolve(skill.path), skill]))
+
+ let entries: fs.Dirent[]
+ try {
+ entries = await fs.promises.readdir(result.skillsDir, { withFileTypes: true })
+ } catch (error) {
+ const code = typeof error === 'object' && error ? (error as { code?: unknown }).code : null
+ if (code === 'EACCES' || code === 'EPERM') {
+ return [
+ {
+ name: result.toolId,
+ path: result.skillsDir,
+ owner: 'unknown',
+ status: 'empty'
+ }
+ ]
+ }
+ return []
+ }
+
+ const skills: AgentSkillItem[] = []
+ for (const entry of entries) {
+ if (!isFilenameSafe(entry.name) || (!entry.isDirectory() && !entry.isSymbolicLink())) {
+ continue
+ }
+
+ const entryPath = path.join(result.skillsDir, entry.name)
+ if (entry.isSymbolicLink()) {
+ skills.push(
+ await this.classifyAgentSkillLink(
+ result.toolId,
+ entry.name,
+ entryPath,
+ deepchatSkillsDir,
+ deepchatByName
+ )
+ )
+ continue
+ }
+
+ const scanInfo = scannedByPath.get(path.resolve(entryPath))
+ if (!scanInfo) {
+ continue
+ }
+ skills.push(await this.classifyAgentSkillDirectory(scanInfo, deepchatByName))
+ }
+
+ return skills.sort((left, right) => left.name.localeCompare(right.name))
+ }
+
+ private async classifyAgentSkillDirectory(
+ skill: ExternalSkillInfo,
+ deepchatByName: Map
+ ): Promise {
+ const deepchat = deepchatByName.get(skill.name)
+ if (!deepchat) {
+ return {
+ name: skill.name,
+ description: skill.description,
+ path: skill.path,
+ owner: 'agent',
+ status: 'agent-owned',
+ action: 'adopt',
+ deepchat: { exists: false }
+ }
+ }
+
+ const sameContent = await this.hasSameSkillContent(skill.path, deepchat.skillRoot)
+ return {
+ name: skill.name,
+ description: skill.description || deepchat.description,
+ path: skill.path,
+ owner: 'agent',
+ status: sameContent ? 'agent-owned' : 'conflict',
+ action: sameContent ? 'adopt' : 'resolve-conflict',
+ deepchat: {
+ exists: true,
+ path: deepchat.skillRoot,
+ disabled: deepchat.deepchatDisabled,
+ sameContent
+ }
+ }
+ }
+
+ private async classifyAgentSkillLink(
+ agentId: string,
+ name: string,
+ linkPath: string,
+ deepchatSkillsDir: string,
+ deepchatByName: Map
+ ): Promise {
+ const targetPath = await this.readResolvedLinkTarget(linkPath)
+ const targetExists = targetPath ? await this.pathExists(targetPath) : false
+ const targetInsideDeepChat = Boolean(
+ targetPath && this.isInsideDirectory(targetPath, deepchatSkillsDir)
+ )
+ const deepchat = deepchatByName.get(name)
+ const createdByDeepChat =
+ deepchat?.agentLinks[agentId]?.createdByDeepChat === true &&
+ path.resolve(deepchat.agentLinks[agentId].path) === path.resolve(linkPath)
+
+ if (!targetExists) {
+ return {
+ name,
+ path: linkPath,
+ owner: 'broken-link',
+ status: 'broken-link',
+ action: createdByDeepChat ? 'repair-link' : undefined,
+ link: {
+ isSymlink: true,
+ targetPath,
+ targetExists: false,
+ targetInsideDeepChat,
+ createdByDeepChat
+ },
+ deepchat: deepchat
+ ? { exists: true, path: deepchat.skillRoot, disabled: deepchat.deepchatDisabled }
+ : { exists: false }
+ }
+ }
+
+ if (targetInsideDeepChat) {
+ return {
+ name,
+ description: deepchat?.description,
+ path: linkPath,
+ owner: 'deepchat',
+ status: 'linked',
+ action: createdByDeepChat ? 'remove-link' : undefined,
+ link: {
+ isSymlink: true,
+ targetPath,
+ targetExists: true,
+ targetInsideDeepChat: true,
+ createdByDeepChat
+ },
+ deepchat: deepchat
+ ? { exists: true, path: deepchat.skillRoot, disabled: deepchat.deepchatDisabled }
+ : { exists: false }
+ }
+ }
+
+ return {
+ name,
+ path: linkPath,
+ owner: 'external-link',
+ status: 'linked-out',
+ action: 'adopt',
+ link: {
+ isSymlink: true,
+ targetPath,
+ targetExists: true,
+ targetInsideDeepChat: false
+ },
+ deepchat: deepchat
+ ? { exists: true, path: deepchat.skillRoot, disabled: deepchat.deepchatDisabled }
+ : { exists: false }
+ }
+ }
+
+ private async readResolvedLinkTarget(linkPath: string): Promise {
+ try {
+ const rawTarget = await fs.promises.readlink(linkPath)
+ return path.isAbsolute(rawTarget)
+ ? path.resolve(rawTarget)
+ : path.resolve(path.dirname(linkPath), rawTarget)
+ } catch {
+ return undefined
+ }
+ }
+
+ private async pathExists(targetPath: string): Promise {
+ try {
+ await fs.promises.access(targetPath, fs.constants.F_OK)
+ return true
+ } catch {
+ return false
+ }
+ }
+
+ private isInsideDirectory(targetPath: string, parentPath: string): boolean {
+ const relative = path.relative(parentPath, path.resolve(targetPath))
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
+ }
+
+ private async hasSameSkillContent(leftRoot: string, rightRoot: string): Promise {
+ try {
+ const [left, right] = await Promise.all([
+ fs.promises.readFile(path.join(leftRoot, 'SKILL.md'), 'utf-8'),
+ fs.promises.readFile(path.join(rightRoot, 'SKILL.md'), 'utf-8')
+ ])
+ return left === right
+ } catch {
+ return false
+ }
+ }
+
/**
* Parse an external skill file
*/
diff --git a/src/main/routes/index.ts b/src/main/routes/index.ts
index 3dc52a097b..ec3ce24c95 100644
--- a/src/main/routes/index.ts
+++ b/src/main/routes/index.ts
@@ -270,25 +270,44 @@ import {
skillsGetDirectoryRoute,
skillsGetExtensionRoute,
skillsGetFolderTreeRoute,
+ skillsGetSyncConfigRoute,
+ skillsExecuteSyncDirectoryExportRoute,
+ skillsExecuteSyncDirectoryImportRoute,
+ skillsInstallFromGitRoute,
skillsInstallFromFolderRoute,
skillsInstallFromUrlRoute,
skillsInstallFromZipRoute,
+ skillsListCatalogRoute,
skillsListMetadataRoute,
skillsListScriptsRoute,
skillsOpenFolderRoute,
+ skillsPreviewSyncDirectoryExportRoute,
+ skillsPreviewSyncDirectoryImportRoute,
skillsReadFileRoute,
+ skillsScanGitRepoRoute,
skillsSaveExtensionRoute,
skillsSaveWithExtensionRoute,
skillsSetActiveRoute,
+ skillsSetDisabledRoute,
+ skillsSetSyncDirectoryRoute,
skillsUninstallRoute,
skillsUpdateFileRoute,
skillSyncAcknowledgeDiscoveriesRoute,
+ skillSyncExecuteAdoptAgentSkillRoute,
skillSyncExecuteExportRoute,
skillSyncExecuteImportRoute,
+ skillSyncExecuteLinkDeepChatSkillsRoute,
+ skillSyncGetAgentDetailRoute,
+ skillSyncGetAgentSkillDetailRoute,
skillSyncGetNewDiscoveriesRoute,
skillSyncGetRegisteredToolsRoute,
+ skillSyncPreviewAdoptAgentSkillRoute,
skillSyncPreviewExportRoute,
skillSyncPreviewImportRoute,
+ skillSyncPreviewLinkDeepChatSkillsRoute,
+ skillSyncRemoveAgentSkillLinkRoute,
+ skillSyncRepairAgentSkillLinkRoute,
+ skillSyncScanAgentsRoute,
skillSyncScanExternalToolsRoute,
syncGetBackupStatusRoute,
syncImportRoute,
@@ -2986,6 +3005,21 @@ export async function dispatchDeepchatRoute(
})
}
+ case skillsListCatalogRoute.name: {
+ return await runTrackedRouteTask(runtime, routeName, context, async () => {
+ skillsListCatalogRoute.input.parse(rawInput)
+ const skills = await runtime.skillPresenter.getUnifiedSkillCatalog()
+ return skillsListCatalogRoute.output.parse({ skills })
+ })
+ }
+
+ case skillsSetDisabledRoute.name: {
+ const input = skillsSetDisabledRoute.input.parse(rawInput)
+ await runtime.skillPresenter.setSkillDeepChatDisabled(input.name, input.disabled)
+ recordSkillUpdatedActivity(runtime, input.name, 'skill-disabled-state')
+ return skillsSetDisabledRoute.output.parse({ saved: true })
+ }
+
case skillsGetDirectoryRoute.name: {
skillsGetDirectoryRoute.input.parse(rawInput)
const path = await runtime.skillPresenter.getSkillsDir()
@@ -3019,6 +3053,57 @@ export async function dispatchDeepchatRoute(
return skillsInstallFromUrlRoute.output.parse({ result })
}
+ case skillsScanGitRepoRoute.name: {
+ const input = skillsScanGitRepoRoute.input.parse(rawInput)
+ const result = await runtime.skillPresenter.scanGitSkillRepo(input.repoUrl)
+ return skillsScanGitRepoRoute.output.parse({ result })
+ }
+
+ case skillsInstallFromGitRoute.name: {
+ const input = skillsInstallFromGitRoute.input.parse(rawInput)
+ const results = await runtime.skillPresenter.installSkillsFromGit(input)
+ if (results.some(didSkillOperationSucceed)) {
+ recordSkillSettingsActivity(runtime, 'created', 'skill Git source')
+ }
+ return skillsInstallFromGitRoute.output.parse({ results })
+ }
+
+ case skillsGetSyncConfigRoute.name: {
+ skillsGetSyncConfigRoute.input.parse(rawInput)
+ const config = await runtime.skillPresenter.getSkillsSyncConfig()
+ return skillsGetSyncConfigRoute.output.parse({ config })
+ }
+
+ case skillsSetSyncDirectoryRoute.name: {
+ const input = skillsSetSyncDirectoryRoute.input.parse(rawInput)
+ const config = await runtime.skillPresenter.setSkillsSyncDirectory(input)
+ return skillsSetSyncDirectoryRoute.output.parse({ config })
+ }
+
+ case skillsPreviewSyncDirectoryExportRoute.name: {
+ const input = skillsPreviewSyncDirectoryExportRoute.input.parse(rawInput)
+ const preview = await runtime.skillPresenter.previewSyncDirectoryExport(input)
+ return skillsPreviewSyncDirectoryExportRoute.output.parse({ preview })
+ }
+
+ case skillsExecuteSyncDirectoryExportRoute.name: {
+ const input = skillsExecuteSyncDirectoryExportRoute.input.parse(rawInput)
+ const result = await runtime.skillPresenter.executeSyncDirectoryExport(input)
+ return skillsExecuteSyncDirectoryExportRoute.output.parse({ result })
+ }
+
+ case skillsPreviewSyncDirectoryImportRoute.name: {
+ skillsPreviewSyncDirectoryImportRoute.input.parse(rawInput)
+ const preview = await runtime.skillPresenter.previewSyncDirectoryImport()
+ return skillsPreviewSyncDirectoryImportRoute.output.parse({ preview })
+ }
+
+ case skillsExecuteSyncDirectoryImportRoute.name: {
+ const input = skillsExecuteSyncDirectoryImportRoute.input.parse(rawInput)
+ const result = await runtime.skillPresenter.executeSyncDirectoryImport(input)
+ return skillsExecuteSyncDirectoryImportRoute.output.parse({ result })
+ }
+
case skillsUninstallRoute.name: {
const input = skillsUninstallRoute.input.parse(rawInput)
const result = await runtime.skillPresenter.uninstallSkill(input.name)
@@ -3143,6 +3228,69 @@ export async function dispatchDeepchatRoute(
})
}
+ case skillSyncScanAgentsRoute.name: {
+ skillSyncScanAgentsRoute.input.parse(rawInput)
+ return skillSyncScanAgentsRoute.output.parse({
+ agents: await runtime.skillSyncPresenter.scanSkillAgents()
+ })
+ }
+
+ case skillSyncGetAgentDetailRoute.name: {
+ const input = skillSyncGetAgentDetailRoute.input.parse(rawInput)
+ return skillSyncGetAgentDetailRoute.output.parse({
+ agent: await runtime.skillSyncPresenter.scanSkillAgent({ agentId: input.agentId })
+ })
+ }
+
+ case skillSyncGetAgentSkillDetailRoute.name: {
+ const input = skillSyncGetAgentSkillDetailRoute.input.parse(rawInput)
+ return skillSyncGetAgentSkillDetailRoute.output.parse({
+ detail: await runtime.skillSyncPresenter.getAgentSkillDetail(input)
+ })
+ }
+
+ case skillSyncPreviewAdoptAgentSkillRoute.name: {
+ const input = skillSyncPreviewAdoptAgentSkillRoute.input.parse(rawInput)
+ return skillSyncPreviewAdoptAgentSkillRoute.output.parse({
+ preview: await runtime.skillSyncPresenter.previewAdoptAgentSkill(input)
+ })
+ }
+
+ case skillSyncExecuteAdoptAgentSkillRoute.name: {
+ const input = skillSyncExecuteAdoptAgentSkillRoute.input.parse(rawInput)
+ return skillSyncExecuteAdoptAgentSkillRoute.output.parse({
+ result: await runtime.skillSyncPresenter.executeAdoptAgentSkill(input)
+ })
+ }
+
+ case skillSyncPreviewLinkDeepChatSkillsRoute.name: {
+ const input = skillSyncPreviewLinkDeepChatSkillsRoute.input.parse(rawInput)
+ return skillSyncPreviewLinkDeepChatSkillsRoute.output.parse({
+ preview: await runtime.skillSyncPresenter.previewLinkDeepChatSkills(input)
+ })
+ }
+
+ case skillSyncExecuteLinkDeepChatSkillsRoute.name: {
+ const input = skillSyncExecuteLinkDeepChatSkillsRoute.input.parse(rawInput)
+ return skillSyncExecuteLinkDeepChatSkillsRoute.output.parse({
+ result: await runtime.skillSyncPresenter.executeLinkDeepChatSkills(input)
+ })
+ }
+
+ case skillSyncRepairAgentSkillLinkRoute.name: {
+ const input = skillSyncRepairAgentSkillLinkRoute.input.parse(rawInput)
+ return skillSyncRepairAgentSkillLinkRoute.output.parse({
+ result: await runtime.skillSyncPresenter.repairAgentSkillLink(input)
+ })
+ }
+
+ case skillSyncRemoveAgentSkillLinkRoute.name: {
+ const input = skillSyncRemoveAgentSkillLinkRoute.input.parse(rawInput)
+ return skillSyncRemoveAgentSkillLinkRoute.output.parse({
+ result: await runtime.skillSyncPresenter.removeAgentSkillLink(input)
+ })
+ }
+
case skillSyncPreviewImportRoute.name: {
const input = skillSyncPreviewImportRoute.input.parse(rawInput)
return skillSyncPreviewImportRoute.output.parse({
diff --git a/src/renderer/api/SkillClient.ts b/src/renderer/api/SkillClient.ts
index 498a849e1a..3ab0d3a0df 100644
--- a/src/renderer/api/SkillClient.ts
+++ b/src/renderer/api/SkillClient.ts
@@ -5,20 +5,36 @@ import {
skillsGetDirectoryRoute,
skillsGetExtensionRoute,
skillsGetFolderTreeRoute,
+ skillsGetSyncConfigRoute,
+ skillsExecuteSyncDirectoryExportRoute,
+ skillsExecuteSyncDirectoryImportRoute,
+ skillsInstallFromGitRoute,
skillsInstallFromFolderRoute,
skillsInstallFromUrlRoute,
skillsInstallFromZipRoute,
+ skillsListCatalogRoute,
skillsListMetadataRoute,
skillsListScriptsRoute,
skillsOpenFolderRoute,
+ skillsPreviewSyncDirectoryExportRoute,
+ skillsPreviewSyncDirectoryImportRoute,
skillsReadFileRoute,
+ skillsScanGitRepoRoute,
skillsSaveExtensionRoute,
skillsSaveWithExtensionRoute,
skillsSetActiveRoute,
+ skillsSetDisabledRoute,
+ skillsSetSyncDirectoryRoute,
skillsUninstallRoute,
skillsUpdateFileRoute
} from '@shared/contracts/routes'
-import type { SkillExtensionConfig, SkillInstallOptions } from '@shared/types/skill'
+import type {
+ GitSkillInstallInput,
+ SkillExtensionConfig,
+ SkillInstallOptions,
+ SkillSyncDirectoryExportInput,
+ SkillSyncDirectoryImportInput
+} from '@shared/types/skill'
import { getDeepchatBridge } from './core'
export function createSkillClient(bridge: DeepchatBridge = getDeepchatBridge()) {
@@ -27,6 +43,11 @@ export function createSkillClient(bridge: DeepchatBridge = getDeepchatBridge())
return result.skills
}
+ async function getUnifiedSkillCatalog() {
+ const result = await bridge.invoke(skillsListCatalogRoute.name, {})
+ return result.skills
+ }
+
async function getSkillsDir() {
const result = await bridge.invoke(skillsGetDirectoryRoute.name, {})
return result.path
@@ -56,6 +77,46 @@ export function createSkillClient(bridge: DeepchatBridge = getDeepchatBridge())
return result.result
}
+ async function scanGitSkillRepo(repoUrl: string) {
+ const result = await bridge.invoke(skillsScanGitRepoRoute.name, { repoUrl })
+ return result.result
+ }
+
+ async function installFromGit(input: GitSkillInstallInput) {
+ const result = await bridge.invoke(skillsInstallFromGitRoute.name, input)
+ return result.results
+ }
+
+ async function getSkillsSyncConfig() {
+ const result = await bridge.invoke(skillsGetSyncConfigRoute.name, {})
+ return result.config
+ }
+
+ async function setSkillsSyncDirectory(skillsDirectory: string) {
+ const result = await bridge.invoke(skillsSetSyncDirectoryRoute.name, { skillsDirectory })
+ return result.config
+ }
+
+ async function previewSyncDirectoryExport(input: SkillSyncDirectoryExportInput) {
+ const result = await bridge.invoke(skillsPreviewSyncDirectoryExportRoute.name, input)
+ return result.preview
+ }
+
+ async function executeSyncDirectoryExport(input: SkillSyncDirectoryExportInput) {
+ const result = await bridge.invoke(skillsExecuteSyncDirectoryExportRoute.name, input)
+ return result.result
+ }
+
+ async function previewSyncDirectoryImport() {
+ const result = await bridge.invoke(skillsPreviewSyncDirectoryImportRoute.name, {})
+ return result.preview
+ }
+
+ async function executeSyncDirectoryImport(input: SkillSyncDirectoryImportInput) {
+ const result = await bridge.invoke(skillsExecuteSyncDirectoryImportRoute.name, input)
+ return result.result
+ }
+
async function uninstallSkill(name: string) {
const result = await bridge.invoke(skillsUninstallRoute.name, { name })
return result.result
@@ -102,6 +163,10 @@ export function createSkillClient(bridge: DeepchatBridge = getDeepchatBridge())
await bridge.invoke(skillsSaveExtensionRoute.name, { name, config })
}
+ async function setSkillDisabled(name: string, disabled: boolean) {
+ await bridge.invoke(skillsSetDisabledRoute.name, { name, disabled })
+ }
+
async function listSkillScripts(name: string) {
const result = await bridge.invoke(skillsListScriptsRoute.name, { name })
return result.scripts
@@ -122,7 +187,15 @@ export function createSkillClient(bridge: DeepchatBridge = getDeepchatBridge())
function onCatalogChanged(
listener: (payload: {
- reason: 'discovered' | 'installed' | 'uninstalled' | 'metadata-updated'
+ reason:
+ | 'discovered'
+ | 'installed'
+ | 'uninstalled'
+ | 'metadata-updated'
+ | 'disabled-updated'
+ | 'management-state-updated'
+ | 'git-installed'
+ | 'sync-directory-updated'
name?: string
version: number
}) => void
@@ -143,10 +216,19 @@ export function createSkillClient(bridge: DeepchatBridge = getDeepchatBridge())
return {
getMetadataList,
+ getUnifiedSkillCatalog,
getSkillsDir,
installFromFolder,
installFromZip,
installFromUrl,
+ scanGitSkillRepo,
+ installFromGit,
+ getSkillsSyncConfig,
+ setSkillsSyncDirectory,
+ previewSyncDirectoryExport,
+ executeSyncDirectoryExport,
+ previewSyncDirectoryImport,
+ executeSyncDirectoryImport,
uninstallSkill,
readSkillFile,
updateSkillFile,
@@ -155,6 +237,7 @@ export function createSkillClient(bridge: DeepchatBridge = getDeepchatBridge())
openSkillsFolder,
getSkillExtension,
saveSkillExtension,
+ setSkillDisabled,
listSkillScripts,
getActiveSkills,
setActiveSkills,
diff --git a/src/renderer/api/SkillSyncClient.ts b/src/renderer/api/SkillSyncClient.ts
index 6e984fd588..86e452376b 100644
--- a/src/renderer/api/SkillSyncClient.ts
+++ b/src/renderer/api/SkillSyncClient.ts
@@ -13,21 +13,41 @@ import {
import {
type DeepchatRouteInput,
skillSyncAcknowledgeDiscoveriesRoute,
+ skillSyncExecuteAdoptAgentSkillRoute,
skillSyncExecuteExportRoute,
skillSyncExecuteImportRoute,
+ skillSyncExecuteLinkDeepChatSkillsRoute,
+ skillSyncGetAgentDetailRoute,
+ skillSyncGetAgentSkillDetailRoute,
skillSyncGetNewDiscoveriesRoute,
skillSyncGetRegisteredToolsRoute,
+ skillSyncPreviewAdoptAgentSkillRoute,
skillSyncPreviewExportRoute,
skillSyncPreviewImportRoute,
+ skillSyncPreviewLinkDeepChatSkillsRoute,
+ skillSyncRemoveAgentSkillLinkRoute,
+ skillSyncRepairAgentSkillLinkRoute,
+ skillSyncScanAgentsRoute,
skillSyncScanExternalToolsRoute
} from '@shared/contracts/routes'
import type {
+ AgentSkillLinkInput,
ConflictStrategy,
+ AdoptAgentSkillInput,
+ AdoptAgentSkillPreview,
+ AdoptAgentSkillResult,
ExportPreview,
ExternalToolConfig,
ImportPreview,
+ InstalledSkillAgent,
+ InstalledSkillAgentDetail,
+ LinkDeepChatSkillResult,
+ LinkDeepChatSkillsInput,
+ LinkDeepChatSkillsPreview,
+ LinkDeepChatSkillsResult,
NewDiscovery,
ScanResult,
+ SkillDetail,
SyncResult
} from '@shared/types/skillSync'
import { getDeepchatBridge } from './core'
@@ -53,6 +73,66 @@ export function createSkillSyncClient(bridge: DeepchatBridge = getDeepchatBridge
return result.tools as ExternalToolConfig[]
}
+ async function scanAgents(): Promise {
+ const result = await bridge.invoke(skillSyncScanAgentsRoute.name, {})
+ return result.agents as InstalledSkillAgent[]
+ }
+
+ async function getAgentDetail(agentId: string): Promise {
+ const result = await bridge.invoke(skillSyncGetAgentDetailRoute.name, { agentId })
+ return result.agent as InstalledSkillAgentDetail
+ }
+
+ async function getAgentSkillDetail(agentId: string, skillName: string): Promise {
+ const result = await bridge.invoke(skillSyncGetAgentSkillDetailRoute.name, {
+ agentId,
+ skillName
+ })
+ return result.detail as SkillDetail
+ }
+
+ async function previewAdoptAgentSkill(
+ input: AdoptAgentSkillInput
+ ): Promise {
+ const result = await bridge.invoke(skillSyncPreviewAdoptAgentSkillRoute.name, input)
+ return result.preview as AdoptAgentSkillPreview
+ }
+
+ async function executeAdoptAgentSkill(
+ input: AdoptAgentSkillInput
+ ): Promise {
+ const result = await bridge.invoke(skillSyncExecuteAdoptAgentSkillRoute.name, input)
+ return result.result as AdoptAgentSkillResult
+ }
+
+ async function previewLinkDeepChatSkills(
+ input: LinkDeepChatSkillsInput
+ ): Promise {
+ const result = await bridge.invoke(skillSyncPreviewLinkDeepChatSkillsRoute.name, input)
+ return result.preview as LinkDeepChatSkillsPreview
+ }
+
+ async function executeLinkDeepChatSkills(
+ input: LinkDeepChatSkillsInput
+ ): Promise {
+ const result = await bridge.invoke(skillSyncExecuteLinkDeepChatSkillsRoute.name, input)
+ return result.result as LinkDeepChatSkillsResult
+ }
+
+ async function repairAgentSkillLink(
+ input: AgentSkillLinkInput
+ ): Promise {
+ const result = await bridge.invoke(skillSyncRepairAgentSkillLinkRoute.name, input)
+ return result.result as LinkDeepChatSkillResult
+ }
+
+ async function removeAgentSkillLink(
+ input: AgentSkillLinkInput
+ ): Promise {
+ const result = await bridge.invoke(skillSyncRemoveAgentSkillLinkRoute.name, input)
+ return result.result as LinkDeepChatSkillResult
+ }
+
async function previewImport(toolId: string, skillNames: string[]): Promise {
const result = await bridge.invoke(skillSyncPreviewImportRoute.name, {
toolId,
@@ -163,6 +243,15 @@ export function createSkillSyncClient(bridge: DeepchatBridge = getDeepchatBridge
getNewDiscoveries,
acknowledgeDiscoveries,
getRegisteredTools,
+ scanAgents,
+ getAgentDetail,
+ getAgentSkillDetail,
+ previewAdoptAgentSkill,
+ executeAdoptAgentSkill,
+ previewLinkDeepChatSkills,
+ executeLinkDeepChatSkills,
+ repairAgentSkillLink,
+ removeAgentSkillLink,
previewImport,
executeImport,
previewExport,
diff --git a/src/renderer/settings/components/skills/AdoptSkillDialog.vue b/src/renderer/settings/components/skills/AdoptSkillDialog.vue
new file mode 100644
index 0000000000..f67907e6f3
--- /dev/null
+++ b/src/renderer/settings/components/skills/AdoptSkillDialog.vue
@@ -0,0 +1,188 @@
+
+
+
+
+
diff --git a/src/renderer/settings/components/skills/AgentSkillTable.vue b/src/renderer/settings/components/skills/AgentSkillTable.vue
new file mode 100644
index 0000000000..11f7ac2c3b
--- /dev/null
+++ b/src/renderer/settings/components/skills/AgentSkillTable.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+ {{ t('settings.skills.agents.table.skill') }}
+ {{ t('settings.skills.agents.table.owner') }}
+ {{ t('settings.skills.agents.table.status') }}
+ {{ t('settings.skills.agents.table.preview') }}
+
+ {{ t('settings.skills.agents.table.action') }}
+
+
+
+
+
+
+ {{ t('settings.skills.agents.emptySkills') }}
+
+
+
+
+
+ {{ skill.name }}
+
+
+
+ {{ ownerLabel(skill.owner) }}
+
+
+
+ {{ statusLabel(skill.status) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/settings/components/skills/InstallFromGitDialog.vue b/src/renderer/settings/components/skills/InstallFromGitDialog.vue
new file mode 100644
index 0000000000..3be781e13c
--- /dev/null
+++ b/src/renderer/settings/components/skills/InstallFromGitDialog.vue
@@ -0,0 +1,251 @@
+
+
+
+
+
diff --git a/src/renderer/settings/components/skills/InstallSkillToAgentDialog.vue b/src/renderer/settings/components/skills/InstallSkillToAgentDialog.vue
new file mode 100644
index 0000000000..2c575d00cd
--- /dev/null
+++ b/src/renderer/settings/components/skills/InstallSkillToAgentDialog.vue
@@ -0,0 +1,305 @@
+
+
+
+
+
diff --git a/src/renderer/settings/components/skills/SkillAgentsTab.vue b/src/renderer/settings/components/skills/SkillAgentsTab.vue
new file mode 100644
index 0000000000..5e89e3bdf1
--- /dev/null
+++ b/src/renderer/settings/components/skills/SkillAgentsTab.vue
@@ -0,0 +1,351 @@
+
+
+
+
+
{{ t('settings.skills.agents.title') }}
+
+ {{ t('settings.skills.agents.summary', { count: agents.length }) }}
+
+
+
+
+
+
+
+
+
{{ t('settings.skills.agents.loadFailed') }}
+
{{ error }}
+
+
+
+
+
+
+
+ {{ t('settings.skills.agents.empty') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ selectedAgent.name }}
+
+ {{ t(`settings.skills.agents.agentStatus.${selectedAgent.status}`) }}
+
+
+
+ {{ selectedAgent.skillsDir }}
+
+
+
+
+ {{
+ t('settings.skills.agents.counts.skills', { count: selectedAgent.skillsCount })
+ }}
+ {{
+ t('settings.skills.agents.counts.linked', { count: selectedAgent.linkedCount })
+ }}
+
+ {{
+ t('settings.skills.agents.counts.agentOwned', {
+ count: selectedAgent.agentOwnedCount
+ })
+ }}
+
+ {{
+ t('settings.skills.agents.counts.conflicts', { count: selectedAgent.conflictCount })
+ }}
+ {{
+ t('settings.skills.agents.counts.broken', { count: selectedAgent.brokenLinkCount })
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/settings/components/skills/SkillCard.vue b/src/renderer/settings/components/skills/SkillCard.vue
index 177d79b8c0..b9c5beb492 100644
--- a/src/renderer/settings/components/skills/SkillCard.vue
+++ b/src/renderer/settings/components/skills/SkillCard.vue
@@ -1,8 +1,11 @@
@@ -24,58 +27,77 @@
{{ runtimeSummary }}
+
+ {{
+ skill.deepchatDisabled
+ ? t('settings.skills.card.disabled')
+ : t('settings.skills.card.enabled')
+ }}
+
-
-
+
+
diff --git a/src/renderer/settings/components/skills/SkillEditorSheet.vue b/src/renderer/settings/components/skills/SkillEditorSheet.vue
deleted file mode 100644
index ec2a37e941..0000000000
--- a/src/renderer/settings/components/skills/SkillEditorSheet.vue
+++ /dev/null
@@ -1,566 +0,0 @@
-
-
-
-
- {{ t('settings.skills.edit.title') }}
-
- {{ skill?.name }}
-
-
-
-
-
-
-
-
{{ t('settings.skills.edit.name') }}
-
-
- {{ t('settings.skills.edit.nameHint') }}
-
-
-
-
- {{ t('settings.skills.edit.description') }}
-
-
-
-
-
{{ t('settings.skills.edit.allowedTools') }}
-
-
- {{ t('settings.skills.edit.allowedToolsHint') }}
-
-
-
-
- {{ t('settings.skills.edit.content') }}
-
-
-
-
-
-
-
-
-
{{ t('settings.skills.edit.runtimeTitle') }}
-
- {{ t('settings.skills.edit.runtimeHint') }}
-
-
-
-
-
- {{ t('settings.skills.edit.pythonRuntime') }}
-
-
-
-
- {{ t('settings.skills.edit.nodeRuntime') }}
-
-
-
-
-
-
-
-
-
- {{ t('settings.skills.edit.envTitle') }}
-
-
-
-
-
-
- {{ t('settings.skills.edit.envWarning') }}
-
-
-
-
-
-
-
-
{{ t('settings.skills.edit.scriptsTitle') }}
-
- {{ t('settings.skills.edit.scriptsHint') }}
-
-
-
-
- {{ t('settings.skills.edit.noScripts') }}
-
-
-
-
-
-
-
- {{ script.relativePath }}
-
-
-
- {{ script.runtime }}
-
-
- {{ script.absolutePath }}
-
-
-
-
-
- {{ t('settings.skills.edit.scriptEnabled') }}
-
-
-
-
-
-
- {{ t('settings.skills.edit.scriptDescription') }}
-
-
-
-
-
-
-
-
-
-
{{ t('settings.skills.edit.files') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/renderer/settings/components/skills/SkillImportExportTab.vue b/src/renderer/settings/components/skills/SkillImportExportTab.vue
new file mode 100644
index 0000000000..8b3ecf0c77
--- /dev/null
+++ b/src/renderer/settings/components/skills/SkillImportExportTab.vue
@@ -0,0 +1,372 @@
+
+
+
+
{{ t('settings.skills.importExport.directory') }}
+
+
+
+
+
+
+
+
+
+ {{ t('settings.skills.importExport.export') }}
+ {{ t('settings.skills.importExport.import') }}
+
+
+
+
+
+
+
+
+ {{ skill.name }}
+
+
+ {{ skill.description }}
+
+
+
+ {{
+ skill.deepchatDisabled
+ ? t('settings.skills.card.disabled')
+ : t('settings.skills.card.enabled')
+ }}
+
+
+
+ {{ t('settings.skills.empty') }}
+
+
+
+
+
+ {{ t('settings.skills.importExport.includeDisabled') }}
+
+
+
+
+
{{ item.name }}
+
+ {{ t(`settings.skills.importExport.state.${item.state}`) }}
+
+
+
+ {{ t('settings.skills.importExport.noExportPreview') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+ {{ item.sourcePath }}
+
+ {{ item.error }}
+
+
+ {{ t(`settings.skills.importExport.state.${item.state}`) }}
+
+
+
+ {{ t('settings.skills.importExport.noImportPreview') }}
+
+
+
+
+
{{ t('settings.skills.importExport.strategy') }}
+
+
+
+ {{ t('settings.skills.importExport.rename') }}
+
+
+
+ {{ t('settings.skills.importExport.overwrite') }}
+
+
+
+ {{ t('settings.skills.importExport.skip') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/settings/components/skills/SkillsSettings.vue b/src/renderer/settings/components/skills/SkillsSettings.vue
index 16fe17b024..701e35c675 100644
--- a/src/renderer/settings/components/skills/SkillsSettings.vue
+++ b/src/renderer/settings/components/skills/SkillsSettings.vue
@@ -6,7 +6,7 @@
data-testid="settings-skills-page"
>
-
+
-
-
+
+
+
+
+
+
+
+ {{ t('settings.skills.install.basicTitle') }}
+
+
+
+ {{ t('settings.skills.git.menuItem') }}
+
+
+
-
-
-
- {{ t('settings.skills.draftSuggestions.title') }}
+
+
+ {{ t('settings.skills.tabs.library') }}
+ {{ t('settings.skills.tabs.agents') }}
+
+ {{ t('settings.skills.tabs.syncDirectory') }}
+
+
+
+
+
+
+
+ {{ t('settings.skills.draftSuggestions.title') }}
+
+
+ {{ t('settings.skills.draftSuggestions.description') }}
+
+
+
-
- {{ t('settings.skills.draftSuggestions.description') }}
-
-
-
-
-
-
-
+
-
+
-
-
-
-
-
-
+
+
+
+ {{ searchQuery ? t('settings.skills.noResults') : t('settings.skills.empty') }}
+
+
+ {{ t('settings.skills.emptyHint') }}
+
-
-
-
-
-
- {{ searchQuery ? t('settings.skills.noResults') : t('settings.skills.empty') }}
-
-
- {{ t('settings.skills.emptyHint') }}
-
-
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
- {{ t('settings.skills.delete.title') }}
-
- {{ t('settings.skills.delete.description', { name: deletingSkill?.name }) }}
-
-
-
- {{ t('common.cancel') }}
-
- {{ t('common.delete') }}
-
-
-
-
-
-
-
+
skillsGuide.showGuide.value && Boolean(sk
const { skills, skillExtensions, skillScripts, loading } = storeToRefs(skillsStore)
// Search
+const activeTab = ref('library')
const searchQuery = ref('')
const draftSuggestionsEnabled = ref(false)
const filteredSkills = computed(() => {
@@ -213,23 +234,13 @@ const filteredSkills = computed(() => {
// Install dialog
const installDialogOpen = ref(false)
-
-// Sync dialog
-const syncDialogOpen = ref(false)
-const syncMode = ref<'import' | 'export'>('import')
-
-const openSyncDialog = (mode: 'import' | 'export') => {
- syncMode.value = mode
- syncDialogOpen.value = true
-}
-
-// Editor
-const editorOpen = ref(false)
-const editingSkill = ref(null)
-
-// Delete dialog
-const deleteDialogOpen = ref(false)
-const deletingSkill = ref(null)
+const gitDialogOpen = ref(false)
+const installToAgentOpen = ref(false)
+const installingToAgentSkill = ref(null)
+const detailDialogOpen = ref(false)
+const skillDetail = ref(null)
+const selectedDetailSkill = ref(null)
+const detailSaving = ref(false)
const router = useRouter()
@@ -251,10 +262,6 @@ const handleSkillsGuidePrimary = async () => {
})
}
-const handleSkillsGuideTargetInteract = async () => {
- await handleSkillsGuidePrimary()
-}
-
const handleSkillsGuideBack = async () => {
const state = await skillsGuide.activatePreviousStep()
await continueGuidedOnboardingFromSettings({
@@ -306,20 +313,114 @@ const setupEventListeners = () => {
eventCleanup.value = skillClient.onCatalogChanged(handleSkillEvent)
}
-const openEditor = (skill: SkillMetadata) => {
- editingSkill.value = skill
- editorOpen.value = true
+const openSkillDetail = async (skill: UnifiedSkillItem) => {
+ try {
+ selectedDetailSkill.value = skill
+ skillDetail.value = {
+ name: skill.name,
+ description: skill.description,
+ sourcePath: skill.path,
+ markdown: await skillClient.readSkillFile(skill.name),
+ mutable: skill.mutable
+ }
+ detailDialogOpen.value = true
+ } catch (cause) {
+ toast({
+ title: t('settings.skills.detail.failed'),
+ description: cause instanceof Error ? cause.message : String(cause),
+ variant: 'destructive'
+ })
+ }
+}
+
+const openInstallToAgent = (skill: UnifiedSkillItem) => {
+ installingToAgentSkill.value = skill
+ installToAgentOpen.value = true
+}
+
+const toggleSkillDisabled = async (skill: UnifiedSkillItem, disabled: boolean) => {
+ try {
+ await skillsStore.setSkillDisabled(skill.name, disabled)
+ toast({
+ title: disabled ? t('settings.skills.disable.success') : t('settings.skills.enable.success'),
+ description: disabled
+ ? t('settings.skills.disable.successMessage', { name: skill.name })
+ : t('settings.skills.enable.successMessage', { name: skill.name })
+ })
+ return true
+ } catch (e) {
+ toast({
+ title: disabled ? t('settings.skills.disable.failed') : t('settings.skills.enable.failed'),
+ description: e instanceof Error ? e.message : String(e),
+ variant: 'destructive'
+ })
+ return false
+ }
}
-const confirmDelete = (skill: SkillMetadata) => {
- deletingSkill.value = skill
- deleteDialogOpen.value = true
+const createDefaultExtension = (): SkillExtensionConfig => ({
+ version: 1,
+ env: {},
+ runtimePolicy: {
+ python: 'auto',
+ node: 'auto'
+ },
+ scriptOverrides: {}
+})
+
+const handleDetailSave = async (content: string) => {
+ const skill = selectedDetailSkill.value
+ if (!skill) return
+
+ detailSaving.value = true
+ try {
+ const result = await skillsStore.saveSkillWithExtension(
+ skill.name,
+ content,
+ skillExtensions.value[skill.name] ?? createDefaultExtension()
+ )
+
+ if (!result.success) {
+ toast({
+ title: t('settings.skills.edit.failed'),
+ description: result.error,
+ variant: 'destructive'
+ })
+ return
+ }
+
+ toast({
+ title: t('settings.skills.edit.success')
+ })
+ detailDialogOpen.value = false
+ skillDetail.value = null
+ selectedDetailSkill.value = null
+ } finally {
+ detailSaving.value = false
+ }
}
-const handleDelete = async () => {
- if (!deletingSkill.value) return
+const handleDetailToggleDisabled = async (disabled: boolean) => {
+ if (!selectedDetailSkill.value) return
+ const success = await toggleSkillDisabled(selectedDetailSkill.value, disabled)
+ if (success && selectedDetailSkill.value) {
+ selectedDetailSkill.value = {
+ ...selectedDetailSkill.value,
+ deepchatDisabled: disabled
+ }
+ }
+}
- const name = deletingSkill.value.name
+const handleDetailInstallToAgent = () => {
+ if (!selectedDetailSkill.value) return
+ openInstallToAgent(selectedDetailSkill.value)
+ detailDialogOpen.value = false
+}
+
+const handleDetailDelete = async () => {
+ if (!selectedDetailSkill.value) return
+
+ const name = selectedDetailSkill.value.name
const result = await skillsStore.uninstallSkill(name)
if (result.success) {
@@ -335,8 +436,9 @@ const handleDelete = async () => {
})
}
- deleteDialogOpen.value = false
- deletingSkill.value = null
+ detailDialogOpen.value = false
+ skillDetail.value = null
+ selectedDetailSkill.value = null
}
const handleInstalled = () => {
@@ -349,35 +451,7 @@ const handleDraftSuggestionsToggle = async (nextValue: boolean | string) => {
await configClient.setSkillDraftSuggestionsEnabled(normalized)
}
-const handleSaved = () => {
- skillsStore.loadSkills()
-}
-
const handleSyncCompleted = () => {
skillsStore.loadSkills()
}
-
-const handleQuickImport = (_toolId: string, _skills: string[]) => {
- // Open sync dialog in import mode with the specified tool preselected
- syncMode.value = 'import'
- syncDialogOpen.value = true
- // Note: The SkillSyncDialog will need to handle the preselected tool
- // For now, we just open it in import mode
-}
-
-const handleImportNew = () => {
- // Open sync dialog in import mode for new discoveries
- syncMode.value = 'import'
- syncDialogOpen.value = true
-}
-
-const handlePromptImport = (_toolIds: string[]) => {
- // Open sync dialog in import mode
- syncMode.value = 'import'
- syncDialogOpen.value = true
-}
-
-const handlePromptClose = () => {
- // Dialog closed without action
-}
diff --git a/src/renderer/src/i18n/da-DK/settings.json b/src/renderer/src/i18n/da-DK/settings.json
index 59df5fc858..c2c99d7ecb 100644
--- a/src/renderer/src/i18n/da-DK/settings.json
+++ b/src/renderer/src/i18n/da-DK/settings.json
@@ -1754,7 +1754,7 @@
"pythonRuntime": "Python Runtime",
"nodeRuntime": "Node Runtime",
"envTitle": "Environment Variables",
- "envWarning": "Masked in the UI only. Values are stored as plain text in the skill sidecar file.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Bundled Scripts",
"scriptsHint": "Only scripts under scripts/ can be run via skill_run.",
"noScripts": "No runnable scripts detected",
@@ -1787,7 +1787,9 @@
"title": "Installer skill",
"urlHint": "Indtast downloadlinket til skill-pakken",
"urlPlaceholder": "Indtast skill ZIP-downloadadresse",
- "zipHint": "Klik eller træk en ZIP-fil hertil"
+ "zipHint": "Klik eller træk en ZIP-fil hertil",
+ "basicTitle": "Mappe, ZIP eller URL",
+ "basicDescription": "Brug den eksisterende installer til lokale mapper, ZIP-pakker og ZIP-downloadlinks."
},
"noResults": "Ingen matchende skill fundet",
"openFolder": "Åbn mappe",
@@ -1887,7 +1889,214 @@
"scripts": "{count} scripts",
"env": "{count} env",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "Vis detaljer",
+ "edit": "Rediger",
+ "installToAgent": "Installer i Agent"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "Synkroniseringsmappe"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "Forhandsvisning"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "Vis"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Scan et Git-repository og installer valgte skills i DeepChat.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "Scan",
+ "detectedFormat": "Registreret format:",
+ "format": {
+ "single-skill": "Enkelt skill",
+ "multi-skill": "Flere skills"
+ },
+ "selectedCount": "{count} valgt",
+ "empty": "Ingen gyldige skills fundet.",
+ "conflict": "Konflikt",
+ "invalid": "Ugyldig",
+ "strategy": "Konfliktstrategi",
+ "rename": "Omdøb nyt skill",
+ "overwrite": "Erstat eksisterende",
+ "skip": "Spring eksisterende over",
+ "install": "Installer i DeepChat",
+ "failed": "Git-installation mislykkedes",
+ "success": "Git-installation færdig",
+ "successMessage": "{count} installeret, {failed} mislykkedes",
+ "menuItem": "Git-repository..."
+ },
+ "importExport": {
+ "directory": "Lokal multi-skill synkroniseringsmappe",
+ "browse": "Gennemse",
+ "save": "Gem",
+ "saved": "Synkroniseringsmappe gemt",
+ "export": "Eksporter til mappe",
+ "import": "Importer fra mappe",
+ "includeDisabled": "Medtag deaktiverede skills",
+ "previewExport": "Forhåndsvis eksport",
+ "exportNow": "Eksporter nu",
+ "previewImport": "Forhåndsvis import",
+ "importSelected": "Importer valgte",
+ "noExportPreview": "Forhåndsvis eksport for at se målstatus.",
+ "noImportPreview": "Forhåndsvis import for at se tilgængelige skills.",
+ "strategy": "Konfliktstrategi",
+ "rename": "Omdøb importeret",
+ "overwrite": "Erstat lokal",
+ "skip": "Spring eksisterende over",
+ "exported": "Eksport færdig",
+ "imported": "Import færdig",
+ "result": "{count} færdig, {failed} mislykkedes",
+ "state": {
+ "new": "Ny",
+ "same": "Samme",
+ "modified": "Ændret",
+ "conflict": "Konflikt",
+ "invalid": "Ugyldig"
+ }
+ },
+ "detail": {
+ "failed": "Kunne ikke indlaese skill-detaljer",
+ "noDescription": "Ingen beskrivelse",
+ "empty": "Intet indhold til forhandsvisning",
+ "installToAgent": "Installer i Agent",
+ "enabled": "Aktiveret",
+ "disabled": "Deaktiveret",
+ "enable": "Aktivér i DeepChat",
+ "disable": "Deaktivér i DeepChat",
+ "preview": "Forhåndsvisning",
+ "edit": "Rediger",
+ "delete": "Slet",
+ "confirmDeleteTitle": "Slet skill",
+ "confirmDeleteDescription": "Slet skill \"{name}\"? Denne handling kan ikke fortrydes."
+ },
+ "installToAgent": {
+ "title": "Installer {name} i Agent",
+ "description": "Opretter et DeepChat-ejet link for denne skill i en lokal agents skills-mappe.",
+ "failed": "Kunne ikke installere i Agent",
+ "emptyAgents": "Ingen tilgaengelige lokale agents fundet.",
+ "target": "Mal-Agent",
+ "preview": "Forhandsvisning",
+ "loadingPreview": "Forbereder linkforhandsvisning...",
+ "noPreview": "Vaelg en agent for at se forhandsvisningen.",
+ "install": "Installer",
+ "success": "Installeret i Agent",
+ "successMessage": "{name} blev linket til mal-agenten.",
+ "disconnect": "Afbryd",
+ "disconnectSuccess": "Afbrudt fra Agent",
+ "disconnectSuccessMessage": "{name} er ikke længere linket til mål-Agent.",
+ "disconnectFailed": "Kunne ikke afbryde fra Agent"
}
},
"notificationsHooks": {
diff --git a/src/renderer/src/i18n/de-DE/settings.json b/src/renderer/src/i18n/de-DE/settings.json
index 1fa3f2136c..ee6ad9136f 100644
--- a/src/renderer/src/i18n/de-DE/settings.json
+++ b/src/renderer/src/i18n/de-DE/settings.json
@@ -1910,7 +1910,9 @@
"success": "Installation erfolgreich",
"successMessage": "skill {name} wurde erfolgreich installiert",
"failed": "Installation fehlgeschlagen",
- "dragInvalid": "Inhalt nicht unterstützt. Ziehen Sie einen skill-Ordner oder eine einzelne .zip-Datei."
+ "dragInvalid": "Inhalt nicht unterstützt. Ziehen Sie einen skill-Ordner oder eine einzelne .zip-Datei.",
+ "basicTitle": "Ordner, ZIP oder URL",
+ "basicDescription": "Nutze den vorhandenen Installer für lokale Ordner, ZIP-Pakete und ZIP-Download-URLs."
},
"delete": {
"title": "skill löschen",
@@ -1923,7 +1925,24 @@
"scripts": "{count} Skripte",
"env": "{count} Umgebungsvariablen",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "Details anzeigen",
+ "edit": "Bearbeiten",
+ "installToAgent": "In Agent installieren"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
},
"edit": {
"title": "skill bearbeiten",
@@ -1945,7 +1964,7 @@
"pythonRuntime": "Python-Runtime",
"nodeRuntime": "Node-Runtime",
"envTitle": "Umgebungsvariablen",
- "envWarning": "Nur in der UI maskiert; tatsächlich als Klartext in die skill sidecar-Datei geschrieben.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Integrierte Skripte",
"scriptsHint": "Nur Skripte unter scripts/ werden über skill_run für agent verfügbar gemacht.",
"noScripts": "Keine ausführbaren Skripte gefunden",
@@ -2054,6 +2073,196 @@
"dontShowAgain": "Diesen Hinweis nicht mehr anzeigen",
"skip": "Überspringen",
"importSelected": "Ausgewählte importieren"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "Sync-Verzeichnis"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "Vorschau"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "Anzeigen"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Ein Git-Repository scannen und ausgewählte Skills in DeepChat installieren.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "Scannen",
+ "detectedFormat": "Erkanntes Format:",
+ "format": {
+ "single-skill": "Einzelner Skill",
+ "multi-skill": "Mehrere Skills"
+ },
+ "selectedCount": "{count} ausgewählt",
+ "empty": "Keine gültigen Skills gefunden.",
+ "conflict": "Konflikt",
+ "invalid": "Ungültig",
+ "strategy": "Konfliktstrategie",
+ "rename": "Neuen Skill umbenennen",
+ "overwrite": "Vorhandenen ersetzen",
+ "skip": "Vorhandenen überspringen",
+ "install": "In DeepChat installieren",
+ "failed": "Git-Installation fehlgeschlagen",
+ "success": "Git-Installation abgeschlossen",
+ "successMessage": "{count} installiert, {failed} fehlgeschlagen",
+ "menuItem": "Git-Repository..."
+ },
+ "importExport": {
+ "directory": "Lokales Multi-Skill-Sync-Verzeichnis",
+ "browse": "Durchsuchen",
+ "save": "Speichern",
+ "saved": "Synchronisationsordner gespeichert",
+ "export": "In Verzeichnis exportieren",
+ "import": "Aus Verzeichnis importieren",
+ "includeDisabled": "Deaktivierte Skills einschließen",
+ "previewExport": "Exportvorschau",
+ "exportNow": "Jetzt exportieren",
+ "previewImport": "Importvorschau",
+ "importSelected": "Auswahl importieren",
+ "noExportPreview": "Exportvorschau starten, um Zielstatus zu sehen.",
+ "noImportPreview": "Importvorschau starten, um verfügbare Skills zu sehen.",
+ "strategy": "Konfliktstrategie",
+ "rename": "Import umbenennen",
+ "overwrite": "Lokalen ersetzen",
+ "skip": "Vorhandenen überspringen",
+ "exported": "Export abgeschlossen",
+ "imported": "Import abgeschlossen",
+ "result": "{count} abgeschlossen, {failed} fehlgeschlagen",
+ "state": {
+ "new": "Neu",
+ "same": "Gleich",
+ "modified": "Geändert",
+ "conflict": "Konflikt",
+ "invalid": "Ungültig"
+ }
+ },
+ "detail": {
+ "failed": "Skill-Details konnten nicht geladen werden",
+ "noDescription": "Keine Beschreibung",
+ "empty": "Kein Vorschauinhalt",
+ "installToAgent": "In Agent installieren",
+ "enabled": "Aktiviert",
+ "disabled": "Deaktiviert",
+ "enable": "In DeepChat aktivieren",
+ "disable": "In DeepChat deaktivieren",
+ "preview": "Vorschau",
+ "edit": "Bearbeiten",
+ "delete": "Löschen",
+ "confirmDeleteTitle": "skill löschen",
+ "confirmDeleteDescription": "skill „{name}“ löschen? Diese Aktion kann nicht rückgängig gemacht werden."
+ },
+ "installToAgent": {
+ "title": "{name} in Agent installieren",
+ "description": "Erstellt einen von DeepChat verwalteten Link fuer diesen Skill im lokalen Skills-Verzeichnis des Agents.",
+ "failed": "Installation in Agent fehlgeschlagen",
+ "emptyAgents": "Keine verfuegbaren lokalen Agents erkannt.",
+ "target": "Ziel-Agent",
+ "preview": "Vorschau",
+ "loadingPreview": "Link-Vorschau wird vorbereitet...",
+ "noPreview": "Waehle einen Agent aus, um die Vorschau zu sehen.",
+ "install": "Installieren",
+ "success": "In Agent installiert",
+ "successMessage": "{name} wurde mit dem Ziel-Agent verknuepft.",
+ "disconnect": "Trennen",
+ "disconnectSuccess": "Von Agent getrennt",
+ "disconnectSuccessMessage": "{name} ist nicht mehr mit dem Ziel-Agent verknüpft.",
+ "disconnectFailed": "Trennen vom Agent fehlgeschlagen"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json
index bd6ce6eaea..28eb2e44ec 100644
--- a/src/renderer/src/i18n/en-US/settings.json
+++ b/src/renderer/src/i18n/en-US/settings.json
@@ -1970,7 +1970,9 @@
"success": "Installation Successful",
"successMessage": "Skill {name} has been installed successfully",
"failed": "Installation Failed",
- "dragInvalid": "Unsupported drop. Drag a skill folder or a single .zip file."
+ "dragInvalid": "Unsupported drop. Drag a skill folder or a single .zip file.",
+ "basicTitle": "Folder, ZIP, or URL",
+ "basicDescription": "Use the existing installer for local folders, ZIP packages, and ZIP download URLs."
},
"delete": {
"title": "Delete Skill",
@@ -1983,7 +1985,24 @@
"scripts": "{count} scripts",
"env": "{count} env",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "View details",
+ "edit": "Edit",
+ "installToAgent": "Install to Agent"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
},
"edit": {
"title": "Edit Skill",
@@ -2005,7 +2024,7 @@
"pythonRuntime": "Python Runtime",
"nodeRuntime": "Node Runtime",
"envTitle": "Environment Variables",
- "envWarning": "Masked in the UI only. Values are stored as plain text in the skill sidecar file.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Bundled Scripts",
"scriptsHint": "Only scripts under scripts/ can be run via skill_run.",
"noScripts": "No runnable scripts detected",
@@ -2114,6 +2133,196 @@
"dontShowAgain": "Don't show this again",
"skip": "Skip",
"importSelected": "Import Selected"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "Sync Directory"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "syncToAgent": "Sync to Agent",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "Preview"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "View"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Scan a Git repository and install selected skills into DeepChat.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "Scan",
+ "detectedFormat": "Detected format:",
+ "format": {
+ "single-skill": "Single skill",
+ "multi-skill": "Multi-skill"
+ },
+ "selectedCount": "{count} selected",
+ "empty": "No valid skills found.",
+ "conflict": "Conflict",
+ "invalid": "Invalid",
+ "strategy": "Conflict strategy",
+ "rename": "Rename new skill",
+ "overwrite": "Replace existing",
+ "skip": "Skip existing",
+ "install": "Install to DeepChat",
+ "failed": "Git install failed",
+ "success": "Git install complete",
+ "successMessage": "{count} installed, {failed} failed",
+ "menuItem": "Git repository..."
+ },
+ "importExport": {
+ "directory": "Local multi-skill sync directory",
+ "browse": "Browse",
+ "save": "Save",
+ "saved": "Sync directory saved",
+ "export": "Export to directory",
+ "import": "Import from directory",
+ "includeDisabled": "Include disabled skills",
+ "previewExport": "Preview Export",
+ "exportNow": "Export Now",
+ "previewImport": "Preview Import",
+ "importSelected": "Import Selected",
+ "noExportPreview": "Preview export to see target states.",
+ "noImportPreview": "Preview import to see available skills.",
+ "strategy": "Conflict strategy",
+ "rename": "Rename imported",
+ "overwrite": "Replace local",
+ "skip": "Skip existing",
+ "exported": "Export complete",
+ "imported": "Import complete",
+ "result": "{count} completed, {failed} failed",
+ "state": {
+ "new": "New",
+ "same": "Same",
+ "modified": "Modified",
+ "conflict": "Conflict",
+ "invalid": "Invalid"
+ }
+ },
+ "detail": {
+ "failed": "Failed to load skill details",
+ "noDescription": "No description",
+ "empty": "No preview content",
+ "installToAgent": "Install to Agent",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "preview": "Preview",
+ "edit": "Edit",
+ "delete": "Delete",
+ "confirmDeleteTitle": "Delete skill",
+ "confirmDeleteDescription": "Delete skill \"{name}\"? This action cannot be undone."
+ },
+ "installToAgent": {
+ "title": "Install {name} to Agent",
+ "description": "Create a DeepChat-owned link for this skill in a local agent skills directory.",
+ "failed": "Failed to install to Agent",
+ "emptyAgents": "No available local agents detected.",
+ "target": "Target Agent",
+ "preview": "Preview",
+ "loadingPreview": "Preparing link preview...",
+ "noPreview": "Select an agent to view the preview.",
+ "install": "Install",
+ "success": "Installed to Agent",
+ "successMessage": "{name} was linked to the target agent.",
+ "disconnect": "Disconnect",
+ "disconnectSuccess": "Disconnected from Agent",
+ "disconnectSuccessMessage": "{name} is no longer linked to the target agent.",
+ "disconnectFailed": "Failed to disconnect from Agent"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/es-ES/settings.json b/src/renderer/src/i18n/es-ES/settings.json
index 190786ac6a..373f75f931 100644
--- a/src/renderer/src/i18n/es-ES/settings.json
+++ b/src/renderer/src/i18n/es-ES/settings.json
@@ -1910,7 +1910,9 @@
"success": "Instalación exitosa",
"successMessage": "La Skill {name} se ha instalado correctamente",
"failed": "Instalación fallida",
- "dragInvalid": "Contenido no admitido. Arrastra una carpeta de Skill o un único archivo .zip."
+ "dragInvalid": "Contenido no admitido. Arrastra una carpeta de Skill o un único archivo .zip.",
+ "basicTitle": "Carpeta, ZIP o URL",
+ "basicDescription": "Usa el instalador existente para carpetas locales, paquetes ZIP y URL de descarga ZIP."
},
"delete": {
"title": "Eliminar Skill",
@@ -1923,7 +1925,24 @@
"scripts": "{count} guiones",
"env": "{count} entorno",
"pythonShort": "Py",
- "nodeShort": "Nodo"
+ "nodeShort": "Nodo",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "Ver detalles",
+ "edit": "Editar",
+ "installToAgent": "Instalar en Agent"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
},
"edit": {
"title": "Editar Skill",
@@ -1945,7 +1964,7 @@
"pythonRuntime": "Python Tiempo de ejecución",
"nodeRuntime": "Tiempo de ejecución del nodo",
"envTitle": "Variables de entorno",
- "envWarning": "Solo se enmascaran en la UI. Los valores se guardan como texto plano en el archivo sidecar de la Skill.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Guiones incluidos",
"scriptsHint": "Solo los scripts de scripts/ se exponen al Agent mediante skill_run.",
"noScripts": "No se detectaron scripts ejecutables",
@@ -2054,6 +2073,196 @@
"dontShowAgain": "No vuelvas a mostrar esto",
"skip": "Saltar",
"importSelected": "Importar seleccionado"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "Directorio de sincronizacion"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "Vista previa"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "Ver"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Escanea un repositorio Git e instala las skills seleccionadas en DeepChat.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "Escanear",
+ "detectedFormat": "Formato detectado:",
+ "format": {
+ "single-skill": "Skill única",
+ "multi-skill": "Varias skills"
+ },
+ "selectedCount": "{count} seleccionadas",
+ "empty": "No se encontraron skills válidas.",
+ "conflict": "Conflicto",
+ "invalid": "No válida",
+ "strategy": "Estrategia de conflicto",
+ "rename": "Renombrar skill nueva",
+ "overwrite": "Reemplazar existente",
+ "skip": "Omitir existente",
+ "install": "Instalar en DeepChat",
+ "failed": "Falló la instalación Git",
+ "success": "Instalación Git completada",
+ "successMessage": "{count} instaladas, {failed} fallidas",
+ "menuItem": "Repositorio Git..."
+ },
+ "importExport": {
+ "directory": "Directorio local de sincronizacion multi-skill",
+ "browse": "Examinar",
+ "save": "Guardar",
+ "saved": "Directorio de sincronización guardado",
+ "export": "Exportar al directorio",
+ "import": "Importar desde el directorio",
+ "includeDisabled": "Incluir skills deshabilitadas",
+ "previewExport": "Previsualizar exportación",
+ "exportNow": "Exportar ahora",
+ "previewImport": "Previsualizar importación",
+ "importSelected": "Importar selección",
+ "noExportPreview": "Previsualiza la exportación para ver el estado de destino.",
+ "noImportPreview": "Previsualiza la importación para ver las skills disponibles.",
+ "strategy": "Estrategia de conflicto",
+ "rename": "Renombrar importada",
+ "overwrite": "Reemplazar local",
+ "skip": "Omitir existente",
+ "exported": "Exportación completada",
+ "imported": "Importación completada",
+ "result": "{count} completadas, {failed} fallidas",
+ "state": {
+ "new": "Nueva",
+ "same": "Igual",
+ "modified": "Modificada",
+ "conflict": "Conflicto",
+ "invalid": "No válida"
+ }
+ },
+ "detail": {
+ "failed": "No se pudieron cargar los detalles del skill",
+ "noDescription": "Sin descripcion",
+ "empty": "No hay contenido para previsualizar",
+ "installToAgent": "Instalar en Agent",
+ "enabled": "Activado",
+ "disabled": "Desactivado",
+ "enable": "Activar en DeepChat",
+ "disable": "Desactivar en DeepChat",
+ "preview": "Vista previa",
+ "edit": "Editar",
+ "delete": "Eliminar",
+ "confirmDeleteTitle": "Eliminar skill",
+ "confirmDeleteDescription": "¿Eliminar el skill \"{name}\"? Esta acción no se puede deshacer."
+ },
+ "installToAgent": {
+ "title": "Instalar {name} en Agent",
+ "description": "Crea un enlace propiedad de DeepChat para este skill en el directorio skills de un agente local.",
+ "failed": "Error al instalar en Agent",
+ "emptyAgents": "No se detectaron agentes locales disponibles.",
+ "target": "Agent de destino",
+ "preview": "Vista previa",
+ "loadingPreview": "Preparando vista previa del enlace...",
+ "noPreview": "Selecciona un agente para ver la vista previa.",
+ "install": "Instalar",
+ "success": "Instalado en Agent",
+ "successMessage": "{name} se enlazo al agente de destino.",
+ "disconnect": "Desconectar",
+ "disconnectSuccess": "Desconectado de Agent",
+ "disconnectSuccessMessage": "{name} ya no está enlazado al Agent de destino.",
+ "disconnectFailed": "No se pudo desconectar de Agent"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/fa-IR/settings.json b/src/renderer/src/i18n/fa-IR/settings.json
index 14f4579922..50d1ee4bae 100644
--- a/src/renderer/src/i18n/fa-IR/settings.json
+++ b/src/renderer/src/i18n/fa-IR/settings.json
@@ -1821,7 +1821,7 @@
"pythonRuntime": "Python Runtime",
"nodeRuntime": "Node Runtime",
"envTitle": "Environment Variables",
- "envWarning": "Masked in the UI only. Values are stored as plain text in the skill sidecar file.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Bundled Scripts",
"scriptsHint": "Only scripts under scripts/ can be run via skill_run.",
"noScripts": "No runnable scripts detected",
@@ -1854,7 +1854,9 @@
"title": "نصب مهارت",
"urlHint": "لینک دانلود بسته skill را وارد کنید",
"urlPlaceholder": "آدرس دانلود ZIP skill را وارد کنید",
- "zipHint": "برای انتخاب فایل ZIP کلیک کنید یا آن را به اینجا بکشید"
+ "zipHint": "برای انتخاب فایل ZIP کلیک کنید یا آن را به اینجا بکشید",
+ "basicTitle": "پوشه، ZIP یا URL",
+ "basicDescription": "از نصبکننده موجود برای پوشههای محلی، بستههای ZIP و URL دانلود ZIP استفاده کنید."
},
"noResults": "مهارت مطابقی یافت نشد",
"openFolder": "باز کردن پوشه",
@@ -1954,7 +1956,214 @@
"scripts": "{count} scripts",
"env": "{count} env",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "مشاهده جزئیات",
+ "edit": "ویرایش",
+ "installToAgent": "نصب در Agent"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "پوشه همگام سازی"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "پیش نمایش"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "مشاهده"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "یک مخزن Git را اسکن کنید و skillهای انتخابشده را در DeepChat نصب کنید.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "اسکن",
+ "detectedFormat": "قالب تشخیصدادهشده:",
+ "format": {
+ "single-skill": "یک skill",
+ "multi-skill": "چند skill"
+ },
+ "selectedCount": "{count} انتخابشده",
+ "empty": "skill معتبری پیدا نشد.",
+ "conflict": "تداخل",
+ "invalid": "نامعتبر",
+ "strategy": "راهبرد تداخل",
+ "rename": "تغییر نام skill جدید",
+ "overwrite": "جایگزینی موجود",
+ "skip": "رد کردن موجود",
+ "install": "نصب در DeepChat",
+ "failed": "نصب Git ناموفق بود",
+ "success": "نصب Git کامل شد",
+ "successMessage": "{count} نصب شد، {failed} ناموفق",
+ "menuItem": "مخزن Git..."
+ },
+ "importExport": {
+ "directory": "پوشه همگام سازی محلی چند skill",
+ "browse": "مرور",
+ "save": "ذخیره",
+ "saved": "پوشه همگامسازی ذخیره شد",
+ "export": "خروجی به پوشه",
+ "import": "ورود از پوشه",
+ "includeDisabled": "شامل skillهای غیرفعال",
+ "previewExport": "پیشنمایش برونبری",
+ "exportNow": "برونبری اکنون",
+ "previewImport": "پیشنمایش درونریزی",
+ "importSelected": "درونریزی انتخابشدهها",
+ "noExportPreview": "برای دیدن وضعیت مقصد، برونبری را پیشنمایش کنید.",
+ "noImportPreview": "برای دیدن skillهای موجود، درونریزی را پیشنمایش کنید.",
+ "strategy": "راهبرد تداخل",
+ "rename": "تغییر نام مورد درونریزی",
+ "overwrite": "جایگزینی محلی",
+ "skip": "رد کردن موجود",
+ "exported": "برونبری کامل شد",
+ "imported": "درونریزی کامل شد",
+ "result": "{count} کامل شد، {failed} ناموفق",
+ "state": {
+ "new": "جدید",
+ "same": "یکسان",
+ "modified": "تغییریافته",
+ "conflict": "تداخل",
+ "invalid": "نامعتبر"
+ }
+ },
+ "detail": {
+ "failed": "بارگیری جزئیات skill ناموفق بود",
+ "noDescription": "بدون توضیح",
+ "empty": "محتوایی برای پیش نمایش نیست",
+ "installToAgent": "نصب در Agent",
+ "enabled": "فعال",
+ "disabled": "غیرفعال",
+ "enable": "فعالسازی در DeepChat",
+ "disable": "غیرفعالسازی در DeepChat",
+ "preview": "پیشنمایش",
+ "edit": "ویرایش",
+ "delete": "حذف",
+ "confirmDeleteTitle": "حذف skill",
+ "confirmDeleteDescription": "skill «{name}» حذف شود؟ این عمل قابل بازگشت نیست."
+ },
+ "installToAgent": {
+ "title": "نصب {name} در Agent",
+ "description": "برای این skill در پوشه skills یک agent محلی، پیوند تحت مالکیت DeepChat ایجاد می کند.",
+ "failed": "نصب در Agent ناموفق بود",
+ "emptyAgents": "هیچ agent محلی قابل استفاده ای پیدا نشد.",
+ "target": "Agent مقصد",
+ "preview": "پیش نمایش",
+ "loadingPreview": "در حال آماده سازی پیش نمایش پیوند...",
+ "noPreview": "برای دیدن پیش نمایش یک agent انتخاب کنید.",
+ "install": "نصب",
+ "success": "در Agent نصب شد",
+ "successMessage": "{name} به agent مقصد پیوند شد.",
+ "disconnect": "قطع اتصال",
+ "disconnectSuccess": "از Agent قطع شد",
+ "disconnectSuccessMessage": "{name} دیگر به Agent مقصد متصل نیست.",
+ "disconnectFailed": "قطع اتصال از Agent ناموفق بود"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/fr-FR/settings.json b/src/renderer/src/i18n/fr-FR/settings.json
index dfa82a9868..a5de18806b 100644
--- a/src/renderer/src/i18n/fr-FR/settings.json
+++ b/src/renderer/src/i18n/fr-FR/settings.json
@@ -1821,7 +1821,7 @@
"pythonRuntime": "Python Runtime",
"nodeRuntime": "Node Runtime",
"envTitle": "Environment Variables",
- "envWarning": "Masked in the UI only. Values are stored as plain text in the skill sidecar file.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Bundled Scripts",
"scriptsHint": "Only scripts under scripts/ can be run via skill_run.",
"noScripts": "No runnable scripts detected",
@@ -1854,7 +1854,9 @@
"title": "Installer skill",
"urlHint": "Entrez le lien de téléchargement du package skill",
"urlPlaceholder": "Saisir l'adresse de téléchargement du fichier ZIP skill",
- "zipHint": "Cliquez ou glissez un fichier ZIP ici"
+ "zipHint": "Cliquez ou glissez un fichier ZIP ici",
+ "basicTitle": "Dossier, ZIP ou URL",
+ "basicDescription": "Utilisez l’installateur existant pour les dossiers locaux, les ZIP et les URL de téléchargement ZIP."
},
"noResults": "Aucune compétence correspondante trouvée",
"openFolder": "Ouvrir le dossier",
@@ -1954,7 +1956,214 @@
"scripts": "{count} scripts",
"env": "{count} env",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "Voir les details",
+ "edit": "Modifier",
+ "installToAgent": "Installer dans Agent"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "Dossier de sync"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "Apercu"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "Voir"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Analysez un dépôt Git et installez les skills sélectionnés dans DeepChat.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "Analyser",
+ "detectedFormat": "Format détecté :",
+ "format": {
+ "single-skill": "Skill unique",
+ "multi-skill": "Multi-skill"
+ },
+ "selectedCount": "{count} sélectionné(s)",
+ "empty": "Aucun skill valide trouvé.",
+ "conflict": "Conflit",
+ "invalid": "Invalide",
+ "strategy": "Stratégie de conflit",
+ "rename": "Renommer le nouveau skill",
+ "overwrite": "Remplacer l’existant",
+ "skip": "Ignorer l’existant",
+ "install": "Installer dans DeepChat",
+ "failed": "Échec de l’installation Git",
+ "success": "Installation Git terminée",
+ "successMessage": "{count} installé(s), {failed} échec(s)",
+ "menuItem": "Depot Git..."
+ },
+ "importExport": {
+ "directory": "Dossier local de sync multi-skill",
+ "browse": "Parcourir",
+ "save": "Enregistrer",
+ "saved": "Dossier de synchronisation enregistré",
+ "export": "Exporter vers le dossier",
+ "import": "Importer depuis le dossier",
+ "includeDisabled": "Inclure les skills désactivés",
+ "previewExport": "Prévisualiser l’export",
+ "exportNow": "Exporter maintenant",
+ "previewImport": "Prévisualiser l’import",
+ "importSelected": "Importer la sélection",
+ "noExportPreview": "Prévisualisez l’export pour voir les états de destination.",
+ "noImportPreview": "Prévisualisez l’import pour voir les skills disponibles.",
+ "strategy": "Stratégie de conflit",
+ "rename": "Renommer l’import",
+ "overwrite": "Remplacer le local",
+ "skip": "Ignorer l’existant",
+ "exported": "Export terminé",
+ "imported": "Import terminé",
+ "result": "{count} terminé(s), {failed} échec(s)",
+ "state": {
+ "new": "Nouveau",
+ "same": "Identique",
+ "modified": "Modifié",
+ "conflict": "Conflit",
+ "invalid": "Invalide"
+ }
+ },
+ "detail": {
+ "failed": "Echec du chargement des details du skill",
+ "noDescription": "Aucune description",
+ "empty": "Aucun contenu a previsualiser",
+ "installToAgent": "Installer dans Agent",
+ "enabled": "Activé",
+ "disabled": "Désactivé",
+ "enable": "Activer dans DeepChat",
+ "disable": "Désactiver dans DeepChat",
+ "preview": "Aperçu",
+ "edit": "Modifier",
+ "delete": "Supprimer",
+ "confirmDeleteTitle": "Supprimer le skill",
+ "confirmDeleteDescription": "Supprimer le skill « {name} » ? Cette action est irréversible."
+ },
+ "installToAgent": {
+ "title": "Installer {name} dans Agent",
+ "description": "Cree un lien gere par DeepChat pour ce skill dans le dossier skills d un agent local.",
+ "failed": "Echec de l installation dans Agent",
+ "emptyAgents": "Aucun agent local disponible detecte.",
+ "target": "Agent cible",
+ "preview": "Apercu",
+ "loadingPreview": "Preparation de l apercu du lien...",
+ "noPreview": "Selectionnez un agent pour voir l apercu.",
+ "install": "Installer",
+ "success": "Installe dans Agent",
+ "successMessage": "{name} a ete lie a l agent cible.",
+ "disconnect": "Déconnecter",
+ "disconnectSuccess": "Déconnecté de Agent",
+ "disconnectSuccessMessage": "{name} n’est plus lié à l’Agent cible.",
+ "disconnectFailed": "Échec de la déconnexion de Agent"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/he-IL/settings.json b/src/renderer/src/i18n/he-IL/settings.json
index 3b446fc00c..e96a40994e 100644
--- a/src/renderer/src/i18n/he-IL/settings.json
+++ b/src/renderer/src/i18n/he-IL/settings.json
@@ -1821,7 +1821,7 @@
"pythonRuntime": "זמן ריצה של Python",
"nodeRuntime": "זמן ריצה של Node",
"envTitle": "משתני סביבה",
- "envWarning": "הערכים מוסתרים רק בממשק המשתמש; הם נשמרים כטקסט רגיל בקובץ ה-sidecar של הכישור.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "סקריפטים מצורפים",
"scriptsHint": "רק סקריפטים תחת scripts/ ניתנים להרצה דרך skill_run.",
"noScripts": "לא זוהו סקריפטים ניתנים להרצה",
@@ -1854,7 +1854,9 @@
"title": "התקן skill",
"urlHint": "הזן את קישור ההורדה של חבילת ה-skill",
"urlPlaceholder": "הזן כתובת הורדת skill ZIP",
- "zipHint": "לחץ או גרור לכאן קובץ ZIP"
+ "zipHint": "לחץ או גרור לכאן קובץ ZIP",
+ "basicTitle": "תיקייה, ZIP או URL",
+ "basicDescription": "השתמש במתקין הקיים עבור תיקיות מקומיות, חבילות ZIP וכתובות הורדה של ZIP."
},
"noResults": "לא נמצא כישור תואם",
"openFolder": "פתח תיקייה",
@@ -1954,7 +1956,214 @@
"scripts": "{count} סקריפטים",
"env": "{count} משתני סביבה",
"pythonShort": "פייתון",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "הצג פרטים",
+ "edit": "עריכה",
+ "installToAgent": "התקן ב-Agent"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "תיקיית סנכרון"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "תצוגה מקדימה"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "הצג"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "סרוק מאגר Git והתקן את ה-skills שנבחרו ב-DeepChat.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "סריקה",
+ "detectedFormat": "פורמט שזוהה:",
+ "format": {
+ "single-skill": "skill יחיד",
+ "multi-skill": "כמה skills"
+ },
+ "selectedCount": "{count} נבחרו",
+ "empty": "לא נמצאו skills תקפים.",
+ "conflict": "התנגשות",
+ "invalid": "לא תקין",
+ "strategy": "אסטרטגיית התנגשות",
+ "rename": "שנה שם ל-skill החדש",
+ "overwrite": "החלף קיים",
+ "skip": "דלג על קיים",
+ "install": "התקן ב-DeepChat",
+ "failed": "התקנת Git נכשלה",
+ "success": "התקנת Git הושלמה",
+ "successMessage": "{count} הותקנו, {failed} נכשלו",
+ "menuItem": "מאגר Git..."
+ },
+ "importExport": {
+ "directory": "תיקיית סנכרון multi-skill מקומית",
+ "browse": "עיון",
+ "save": "שמירה",
+ "saved": "תיקיית הסנכרון נשמרה",
+ "export": "ייצא לתיקייה",
+ "import": "ייבא מתיקייה",
+ "includeDisabled": "כלול skills מושבתים",
+ "previewExport": "תצוגה מקדימה לייצוא",
+ "exportNow": "ייצא עכשיו",
+ "previewImport": "תצוגה מקדימה לייבוא",
+ "importSelected": "ייבא נבחרים",
+ "noExportPreview": "הצג תצוגה מקדימה של הייצוא כדי לראות מצבי יעד.",
+ "noImportPreview": "הצג תצוגה מקדימה של הייבוא כדי לראות skills זמינים.",
+ "strategy": "אסטרטגיית התנגשות",
+ "rename": "שנה שם לייבוא",
+ "overwrite": "החלף מקומי",
+ "skip": "דלג על קיים",
+ "exported": "הייצוא הושלם",
+ "imported": "הייבוא הושלם",
+ "result": "{count} הושלמו, {failed} נכשלו",
+ "state": {
+ "new": "חדש",
+ "same": "זהה",
+ "modified": "שונה",
+ "conflict": "התנגשות",
+ "invalid": "לא תקין"
+ }
+ },
+ "detail": {
+ "failed": "טעינת פרטי skill נכשלה",
+ "noDescription": "אין תיאור",
+ "empty": "אין תוכן לתצוגה מקדימה",
+ "installToAgent": "התקנה ל-Agent",
+ "enabled": "מופעל",
+ "disabled": "מושבת",
+ "enable": "הפעלה ב-DeepChat",
+ "disable": "השבתה ב-DeepChat",
+ "preview": "תצוגה מקדימה",
+ "edit": "עריכה",
+ "delete": "מחיקה",
+ "confirmDeleteTitle": "מחיקת skill",
+ "confirmDeleteDescription": "למחוק את skill \"{name}\"? לא ניתן לבטל פעולה זו."
+ },
+ "installToAgent": {
+ "title": "התקן את {name} ב-Agent",
+ "description": "יוצר קישור בבעלות DeepChat עבור skill זה בתיקיית skills של agent מקומי.",
+ "failed": "התקנה ב-Agent נכשלה",
+ "emptyAgents": "לא זוהו agents מקומיים זמינים.",
+ "target": "Agent יעד",
+ "preview": "תצוגה מקדימה",
+ "loadingPreview": "מכין תצוגה מקדימה של הקישור...",
+ "noPreview": "בחר agent כדי לראות תצוגה מקדימה.",
+ "install": "התקן",
+ "success": "הותקן ב-Agent",
+ "successMessage": "{name} קושר ל-agent היעד.",
+ "disconnect": "ניתוק",
+ "disconnectSuccess": "נותק מ-Agent",
+ "disconnectSuccessMessage": "{name} כבר לא מקושר ל-Agent היעד.",
+ "disconnectFailed": "הניתוק מ-Agent נכשל"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/id-ID/settings.json b/src/renderer/src/i18n/id-ID/settings.json
index 342dfd31a3..f0f5edb6b1 100644
--- a/src/renderer/src/i18n/id-ID/settings.json
+++ b/src/renderer/src/i18n/id-ID/settings.json
@@ -1910,7 +1910,9 @@
"success": "Instalasi berhasil",
"successMessage": "skill {name} berhasil diinstal",
"failed": "Instalasi gagal",
- "dragInvalid": "Konten tidak didukung. Seret folder skill atau satu file .zip."
+ "dragInvalid": "Konten tidak didukung. Seret folder skill atau satu file .zip.",
+ "basicTitle": "Folder, ZIP, atau URL",
+ "basicDescription": "Gunakan penginstal yang ada untuk folder lokal, paket ZIP, dan URL unduhan ZIP."
},
"delete": {
"title": "Hapus skill",
@@ -1923,7 +1925,24 @@
"scripts": "skrip {count}",
"env": "{count} variabel lingkungan",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "Lihat detail",
+ "edit": "Edit",
+ "installToAgent": "Instal ke Agent"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
},
"edit": {
"title": "Sunting skill",
@@ -1945,7 +1964,7 @@
"pythonRuntime": "Python waktu proses",
"nodeRuntime": "Waktu proses simpul",
"envTitle": "variabel lingkungan",
- "envWarning": "Hanya tampilan topeng yang dilakukan di UI, dan file sespan skill sebenarnya akan ditulis dalam teks biasa.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Skrip bawaan",
"scriptsHint": "Hanya skrip di bawah scripts/ yang akan diekspos ke agent melalui skill_run.",
"noScripts": "Tidak ditemukan skrip yang dapat dijalankan",
@@ -2054,6 +2073,196 @@
"dontShowAgain": "Jangan tampilkan prompt ini lagi",
"skip": "lewati",
"importSelected": "Impor dipilih"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "Direktori sinkronisasi"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "Pratinjau"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "Lihat"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Pindai repositori Git dan instal skill yang dipilih ke DeepChat.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "Pindai",
+ "detectedFormat": "Format terdeteksi:",
+ "format": {
+ "single-skill": "Skill tunggal",
+ "multi-skill": "Multi-skill"
+ },
+ "selectedCount": "{count} dipilih",
+ "empty": "Tidak ada skill valid ditemukan.",
+ "conflict": "Konflik",
+ "invalid": "Tidak valid",
+ "strategy": "Strategi konflik",
+ "rename": "Ganti nama skill baru",
+ "overwrite": "Ganti yang ada",
+ "skip": "Lewati yang ada",
+ "install": "Instal ke DeepChat",
+ "failed": "Instalasi Git gagal",
+ "success": "Instalasi Git selesai",
+ "successMessage": "{count} terinstal, {failed} gagal",
+ "menuItem": "Repositori Git..."
+ },
+ "importExport": {
+ "directory": "Direktori sinkronisasi multi-skill lokal",
+ "browse": "Jelajahi",
+ "save": "Simpan",
+ "saved": "Direktori sinkronisasi disimpan",
+ "export": "Ekspor ke direktori",
+ "import": "Impor dari direktori",
+ "includeDisabled": "Sertakan skill yang dinonaktifkan",
+ "previewExport": "Pratinjau Ekspor",
+ "exportNow": "Ekspor Sekarang",
+ "previewImport": "Pratinjau Impor",
+ "importSelected": "Impor yang Dipilih",
+ "noExportPreview": "Pratinjau ekspor untuk melihat status tujuan.",
+ "noImportPreview": "Pratinjau impor untuk melihat skill yang tersedia.",
+ "strategy": "Strategi konflik",
+ "rename": "Ganti nama impor",
+ "overwrite": "Ganti lokal",
+ "skip": "Lewati yang ada",
+ "exported": "Ekspor selesai",
+ "imported": "Impor selesai",
+ "result": "{count} selesai, {failed} gagal",
+ "state": {
+ "new": "Baru",
+ "same": "Sama",
+ "modified": "Diubah",
+ "conflict": "Konflik",
+ "invalid": "Tidak valid"
+ }
+ },
+ "detail": {
+ "failed": "Gagal memuat detail skill",
+ "noDescription": "Tidak ada deskripsi",
+ "empty": "Tidak ada konten pratinjau",
+ "installToAgent": "Instal ke Agent",
+ "enabled": "Aktif",
+ "disabled": "Nonaktif",
+ "enable": "Aktifkan di DeepChat",
+ "disable": "Nonaktifkan di DeepChat",
+ "preview": "Pratinjau",
+ "edit": "Edit",
+ "delete": "Hapus",
+ "confirmDeleteTitle": "Hapus skill",
+ "confirmDeleteDescription": "Hapus skill \"{name}\"? Tindakan ini tidak dapat dibatalkan."
+ },
+ "installToAgent": {
+ "title": "Instal {name} ke Agent",
+ "description": "Membuat tautan milik DeepChat untuk skill ini di direktori skills agent lokal.",
+ "failed": "Gagal menginstal ke Agent",
+ "emptyAgents": "Tidak ada agent lokal yang tersedia terdeteksi.",
+ "target": "Agent tujuan",
+ "preview": "Pratinjau",
+ "loadingPreview": "Menyiapkan pratinjau tautan...",
+ "noPreview": "Pilih agent untuk melihat pratinjau.",
+ "install": "Instal",
+ "success": "Terinstal ke Agent",
+ "successMessage": "{name} telah ditautkan ke agent tujuan.",
+ "disconnect": "Putuskan",
+ "disconnectSuccess": "Terputus dari Agent",
+ "disconnectSuccessMessage": "{name} tidak lagi tertaut ke Agent target.",
+ "disconnectFailed": "Gagal memutus dari Agent"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/it-IT/settings.json b/src/renderer/src/i18n/it-IT/settings.json
index 2bbe4cc158..4312b789f7 100644
--- a/src/renderer/src/i18n/it-IT/settings.json
+++ b/src/renderer/src/i18n/it-IT/settings.json
@@ -1910,7 +1910,9 @@
"success": "Installazione riuscita",
"successMessage": "skill {name} installata correttamente",
"failed": "Installazione non riuscita",
- "dragInvalid": "Contenuto non supportato. Trascina una cartella skill o un singolo file .zip."
+ "dragInvalid": "Contenuto non supportato. Trascina una cartella skill o un singolo file .zip.",
+ "basicTitle": "Cartella, ZIP o URL",
+ "basicDescription": "Usa l’installer esistente per cartelle locali, pacchetti ZIP e URL di download ZIP."
},
"delete": {
"title": "Elimina skill",
@@ -1923,7 +1925,24 @@
"scripts": "{count} script",
"env": "{count} variabili d'ambiente",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "Vedi dettagli",
+ "edit": "Modifica",
+ "installToAgent": "Installa in Agent"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
},
"edit": {
"title": "Modifica skill",
@@ -1945,7 +1964,7 @@
"pythonRuntime": "Runtime Python",
"nodeRuntime": "Runtime Node",
"envTitle": "Variabili d'ambiente",
- "envWarning": "Mascherate solo nella UI; in realtà verranno scritte in chiaro nel file sidecar della skill.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Script integrati",
"scriptsHint": "Solo gli script sotto scripts/ saranno esposti all'agent tramite skill_run.",
"noScripts": "Nessuno script eseguibile trovato",
@@ -2054,6 +2073,196 @@
"dontShowAgain": "Non mostrare più questo avviso",
"skip": "Salta",
"importSelected": "Importa selezionate"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "Directory di sincronizzazione"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "Anteprima"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "Vedi"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Scansiona un repository Git e installa le skill selezionate in DeepChat.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "Scansiona",
+ "detectedFormat": "Formato rilevato:",
+ "format": {
+ "single-skill": "Skill singola",
+ "multi-skill": "Multi-skill"
+ },
+ "selectedCount": "{count} selezionate",
+ "empty": "Nessuna skill valida trovata.",
+ "conflict": "Conflitto",
+ "invalid": "Non valida",
+ "strategy": "Strategia conflitto",
+ "rename": "Rinomina nuova skill",
+ "overwrite": "Sostituisci esistente",
+ "skip": "Salta esistente",
+ "install": "Installa in DeepChat",
+ "failed": "Installazione Git non riuscita",
+ "success": "Installazione Git completata",
+ "successMessage": "{count} installate, {failed} non riuscite",
+ "menuItem": "Repository Git..."
+ },
+ "importExport": {
+ "directory": "Directory locale di sincronizzazione multi-skill",
+ "browse": "Sfoglia",
+ "save": "Salva",
+ "saved": "Directory di sincronizzazione salvata",
+ "export": "Esporta nella directory",
+ "import": "Importa dalla directory",
+ "includeDisabled": "Includi skill disabilitate",
+ "previewExport": "Anteprima esportazione",
+ "exportNow": "Esporta ora",
+ "previewImport": "Anteprima importazione",
+ "importSelected": "Importa selezionate",
+ "noExportPreview": "Apri l’anteprima esportazione per vedere gli stati di destinazione.",
+ "noImportPreview": "Apri l’anteprima importazione per vedere le skill disponibili.",
+ "strategy": "Strategia conflitto",
+ "rename": "Rinomina importata",
+ "overwrite": "Sostituisci locale",
+ "skip": "Salta esistente",
+ "exported": "Esportazione completata",
+ "imported": "Importazione completata",
+ "result": "{count} completate, {failed} non riuscite",
+ "state": {
+ "new": "Nuova",
+ "same": "Uguale",
+ "modified": "Modificata",
+ "conflict": "Conflitto",
+ "invalid": "Non valida"
+ }
+ },
+ "detail": {
+ "failed": "Impossibile caricare i dettagli dello skill",
+ "noDescription": "Nessuna descrizione",
+ "empty": "Nessun contenuto da visualizzare",
+ "installToAgent": "Installa in Agent",
+ "enabled": "Attivo",
+ "disabled": "Disattivato",
+ "enable": "Attiva in DeepChat",
+ "disable": "Disattiva in DeepChat",
+ "preview": "Anteprima",
+ "edit": "Modifica",
+ "delete": "Elimina",
+ "confirmDeleteTitle": "Elimina skill",
+ "confirmDeleteDescription": "Eliminare lo skill \"{name}\"? Questa azione non può essere annullata."
+ },
+ "installToAgent": {
+ "title": "Installa {name} in Agent",
+ "description": "Crea un link gestito da DeepChat per questo skill nella directory skills di un agent locale.",
+ "failed": "Installazione in Agent non riuscita",
+ "emptyAgents": "Nessun agent locale disponibile rilevato.",
+ "target": "Agent di destinazione",
+ "preview": "Anteprima",
+ "loadingPreview": "Preparazione anteprima del link...",
+ "noPreview": "Seleziona un agent per vedere l anteprima.",
+ "install": "Installa",
+ "success": "Installato in Agent",
+ "successMessage": "{name} e stato collegato all agent di destinazione.",
+ "disconnect": "Disconnetti",
+ "disconnectSuccess": "Disconnesso da Agent",
+ "disconnectSuccessMessage": "{name} non è più collegato all’Agent di destinazione.",
+ "disconnectFailed": "Disconnessione da Agent non riuscita"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/ja-JP/settings.json b/src/renderer/src/i18n/ja-JP/settings.json
index e15c666203..715e9b0ad8 100644
--- a/src/renderer/src/i18n/ja-JP/settings.json
+++ b/src/renderer/src/i18n/ja-JP/settings.json
@@ -1821,7 +1821,7 @@
"pythonRuntime": "Python Runtime",
"nodeRuntime": "Node Runtime",
"envTitle": "Environment Variables",
- "envWarning": "Masked in the UI only. Values are stored as plain text in the skill sidecar file.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Bundled Scripts",
"scriptsHint": "Only scripts under scripts/ can be run via skill_run.",
"noScripts": "No runnable scripts detected",
@@ -1854,7 +1854,9 @@
"title": "スキルをインストール",
"urlHint": "スキルパッケージのダウンロードリンクを入力",
"urlPlaceholder": "skill ZIP ダウンロードアドレスを入力",
- "zipHint": "ZIP ファイルをクリックまたはドラッグ"
+ "zipHint": "ZIP ファイルをクリックまたはドラッグ",
+ "basicTitle": "フォルダー、ZIP、URL",
+ "basicDescription": "既存のインストーラーでローカルフォルダー、ZIP パッケージ、ZIP ダウンロード URL を追加します。"
},
"noResults": "一致するスキルが見つかりません",
"openFolder": "フォルダを開く",
@@ -1954,7 +1956,214 @@
"scripts": "{count} scripts",
"env": "{count} env",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "詳細を表示",
+ "edit": "編集",
+ "installToAgent": "Agent にインストール"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "同期ディレクトリ"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "プレビュー"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "表示"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Git リポジトリをスキャンし、選択した skill を DeepChat にインストールします。",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "スキャン",
+ "detectedFormat": "検出形式:",
+ "format": {
+ "single-skill": "単一 skill",
+ "multi-skill": "複数 skill"
+ },
+ "selectedCount": "{count} 件選択",
+ "empty": "有効な skill が見つかりません。",
+ "conflict": "競合",
+ "invalid": "無効",
+ "strategy": "競合時の処理",
+ "rename": "新しい skill をリネーム",
+ "overwrite": "既存を置換",
+ "skip": "既存をスキップ",
+ "install": "DeepChat にインストール",
+ "failed": "Git インストールに失敗しました",
+ "success": "Git インストール完了",
+ "successMessage": "{count} 件インストール、{failed} 件失敗",
+ "menuItem": "Git リポジトリ..."
+ },
+ "importExport": {
+ "directory": "ローカル multi-skill 同期ディレクトリ",
+ "browse": "参照",
+ "save": "保存",
+ "saved": "同期ディレクトリを保存しました",
+ "export": "ディレクトリへエクスポート",
+ "import": "ディレクトリからインポート",
+ "includeDisabled": "無効な skill を含める",
+ "previewExport": "エクスポートをプレビュー",
+ "exportNow": "今すぐエクスポート",
+ "previewImport": "インポートをプレビュー",
+ "importSelected": "選択をインポート",
+ "noExportPreview": "エクスポートのプレビューで宛先状態を確認します。",
+ "noImportPreview": "インポートのプレビューで利用可能な skill を確認します。",
+ "strategy": "競合時の処理",
+ "rename": "インポート名を変更",
+ "overwrite": "ローカルを置換",
+ "skip": "既存をスキップ",
+ "exported": "エクスポート完了",
+ "imported": "インポート完了",
+ "result": "{count} 件完了、{failed} 件失敗",
+ "state": {
+ "new": "新規",
+ "same": "同一",
+ "modified": "変更あり",
+ "conflict": "競合",
+ "invalid": "無効"
+ }
+ },
+ "detail": {
+ "failed": "skill の詳細を読み込めませんでした",
+ "noDescription": "説明なし",
+ "empty": "プレビューできる内容がありません",
+ "installToAgent": "Agent にインストール",
+ "enabled": "有効",
+ "disabled": "無効",
+ "enable": "DeepChat で有効化",
+ "disable": "DeepChat で無効化",
+ "preview": "プレビュー",
+ "edit": "編集",
+ "delete": "削除",
+ "confirmDeleteTitle": "skill を削除",
+ "confirmDeleteDescription": "skill「{name}」を削除しますか?この操作は元に戻せません。"
+ },
+ "installToAgent": {
+ "title": "{name} を Agent にインストール",
+ "description": "この skill の DeepChat 所有リンクをローカル Agent の skills ディレクトリに作成します。",
+ "failed": "Agent へのインストールに失敗しました",
+ "emptyAgents": "利用可能なローカル Agent が見つかりません。",
+ "target": "対象 Agent",
+ "preview": "プレビュー",
+ "loadingPreview": "リンクプレビューを準備中...",
+ "noPreview": "Agent を選択するとプレビューを表示します。",
+ "install": "インストール",
+ "success": "Agent にインストールしました",
+ "successMessage": "{name} を対象 Agent にリンクしました。",
+ "disconnect": "解除",
+ "disconnectSuccess": "Agent から解除しました",
+ "disconnectSuccessMessage": "{name} の対象 Agent へのリンクを解除しました。",
+ "disconnectFailed": "Agent からの解除に失敗しました"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/ko-KR/settings.json b/src/renderer/src/i18n/ko-KR/settings.json
index 687c35e87d..c15839ece0 100644
--- a/src/renderer/src/i18n/ko-KR/settings.json
+++ b/src/renderer/src/i18n/ko-KR/settings.json
@@ -1821,7 +1821,7 @@
"pythonRuntime": "Python Runtime",
"nodeRuntime": "Node Runtime",
"envTitle": "Environment Variables",
- "envWarning": "Masked in the UI only. Values are stored as plain text in the skill sidecar file.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Bundled Scripts",
"scriptsHint": "Only scripts under scripts/ can be run via skill_run.",
"noScripts": "No runnable scripts detected",
@@ -1854,7 +1854,9 @@
"title": "스킬 설치",
"urlHint": "스킬 패키지의 다운로드 링크 입력",
"urlPlaceholder": "스킬 ZIP 다운로드 주소 입력",
- "zipHint": "ZIP 파일을 클릭하거나 여기로 드래그하세요"
+ "zipHint": "ZIP 파일을 클릭하거나 여기로 드래그하세요",
+ "basicTitle": "폴더, ZIP 또는 URL",
+ "basicDescription": "기존 설치 도구로 로컬 폴더, ZIP 패키지, ZIP 다운로드 URL을 추가합니다."
},
"noResults": "일치하는 skill을 찾을 수 없습니다.",
"sync": {
@@ -1954,7 +1956,214 @@
"scripts": "{count} scripts",
"env": "{count} env",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "자세히 보기",
+ "edit": "편집",
+ "installToAgent": "Agent에 설치"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "동기화 디렉터리"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "미리보기"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "보기"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Git 저장소를 스캔하고 선택한 skill을 DeepChat에 설치합니다.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "스캔",
+ "detectedFormat": "감지된 형식:",
+ "format": {
+ "single-skill": "단일 skill",
+ "multi-skill": "다중 skill"
+ },
+ "selectedCount": "{count}개 선택됨",
+ "empty": "유효한 skill을 찾지 못했습니다.",
+ "conflict": "충돌",
+ "invalid": "유효하지 않음",
+ "strategy": "충돌 전략",
+ "rename": "새 skill 이름 변경",
+ "overwrite": "기존 항목 교체",
+ "skip": "기존 항목 건너뛰기",
+ "install": "DeepChat에 설치",
+ "failed": "Git 설치 실패",
+ "success": "Git 설치 완료",
+ "successMessage": "{count}개 설치, {failed}개 실패",
+ "menuItem": "Git 저장소..."
+ },
+ "importExport": {
+ "directory": "로컬 multi-skill 동기화 디렉터리",
+ "browse": "찾아보기",
+ "save": "저장",
+ "saved": "동기화 디렉터리가 저장되었습니다",
+ "export": "디렉터리로 내보내기",
+ "import": "디렉터리에서 가져오기",
+ "includeDisabled": "비활성 skill 포함",
+ "previewExport": "내보내기 미리보기",
+ "exportNow": "지금 내보내기",
+ "previewImport": "가져오기 미리보기",
+ "importSelected": "선택 항목 가져오기",
+ "noExportPreview": "내보내기 미리보기로 대상 상태를 확인하세요.",
+ "noImportPreview": "가져오기 미리보기로 사용 가능한 skill을 확인하세요.",
+ "strategy": "충돌 전략",
+ "rename": "가져온 항목 이름 변경",
+ "overwrite": "로컬 항목 교체",
+ "skip": "기존 항목 건너뛰기",
+ "exported": "내보내기 완료",
+ "imported": "가져오기 완료",
+ "result": "{count}개 완료, {failed}개 실패",
+ "state": {
+ "new": "새 항목",
+ "same": "같음",
+ "modified": "수정됨",
+ "conflict": "충돌",
+ "invalid": "유효하지 않음"
+ }
+ },
+ "detail": {
+ "failed": "skill 세부 정보를 불러오지 못했습니다",
+ "noDescription": "설명 없음",
+ "empty": "미리 볼 내용이 없습니다",
+ "installToAgent": "Agent에 설치",
+ "enabled": "활성화됨",
+ "disabled": "비활성화됨",
+ "enable": "DeepChat에서 활성화",
+ "disable": "DeepChat에서 비활성화",
+ "preview": "미리보기",
+ "edit": "편집",
+ "delete": "삭제",
+ "confirmDeleteTitle": "skill 삭제",
+ "confirmDeleteDescription": "skill \"{name}\"을(를) 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
+ },
+ "installToAgent": {
+ "title": "{name}을 Agent에 설치",
+ "description": "이 skill에 대한 DeepChat 소유 링크를 로컬 Agent skills 디렉터리에 만듭니다.",
+ "failed": "Agent 설치 실패",
+ "emptyAgents": "사용 가능한 로컬 Agent를 찾지 못했습니다.",
+ "target": "대상 Agent",
+ "preview": "미리보기",
+ "loadingPreview": "링크 미리보기를 준비하는 중...",
+ "noPreview": "Agent를 선택하면 미리보기가 표시됩니다.",
+ "install": "설치",
+ "success": "Agent에 설치됨",
+ "successMessage": "{name}이 대상 Agent에 연결되었습니다.",
+ "disconnect": "연결 해제",
+ "disconnectSuccess": "Agent에서 연결 해제됨",
+ "disconnectSuccessMessage": "{name}의 대상 Agent 링크가 해제되었습니다.",
+ "disconnectFailed": "Agent 연결 해제 실패"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/ms-MY/settings.json b/src/renderer/src/i18n/ms-MY/settings.json
index e4c9e29835..267f240433 100644
--- a/src/renderer/src/i18n/ms-MY/settings.json
+++ b/src/renderer/src/i18n/ms-MY/settings.json
@@ -1910,7 +1910,9 @@
"success": "Pemasangan berjaya",
"successMessage": "skill {name} telah berjaya dipasang",
"failed": "Pemasangan gagal",
- "dragInvalid": "Kandungan tidak disokong. Seret folder skill atau satu fail .zip."
+ "dragInvalid": "Kandungan tidak disokong. Seret folder skill atau satu fail .zip.",
+ "basicTitle": "Folder, ZIP atau URL",
+ "basicDescription": "Gunakan pemasang sedia ada untuk folder setempat, pakej ZIP dan URL muat turun ZIP."
},
"delete": {
"title": "Alih keluar skill",
@@ -1923,7 +1925,24 @@
"scripts": "Skrip {count}",
"env": "Pembolehubah persekitaran {count}",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "Lihat butiran",
+ "edit": "Edit",
+ "installToAgent": "Pasang ke Agent"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
},
"edit": {
"title": "Edit skill",
@@ -1945,7 +1964,7 @@
"pythonRuntime": "Masa jalan Python",
"nodeRuntime": "Masa jalan Node",
"envTitle": "pembolehubah persekitaran",
- "envWarning": "Hanya paparan topeng dilakukan dalam UI, dan fail skill sidecar sebenarnya akan ditulis dalam teks biasa.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Skrip terbina dalam",
"scriptsHint": "Hanya skrip di bawah scripts/ akan didedahkan kepada agent melalui skill_run.",
"noScripts": "Tiada skrip runnable ditemui",
@@ -2054,6 +2073,196 @@
"dontShowAgain": "Jangan tunjukkan ini lagi",
"skip": "melompat ke atas",
"importSelected": "Import dipilih"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "Direktori segerak"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "Pratonton"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "Lihat"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Imbas repositori Git dan pasang skill yang dipilih ke DeepChat.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "Imbas",
+ "detectedFormat": "Format dikesan:",
+ "format": {
+ "single-skill": "Skill tunggal",
+ "multi-skill": "Berbilang skill"
+ },
+ "selectedCount": "{count} dipilih",
+ "empty": "Tiada skill sah ditemui.",
+ "conflict": "Konflik",
+ "invalid": "Tidak sah",
+ "strategy": "Strategi konflik",
+ "rename": "Namakan semula skill baharu",
+ "overwrite": "Ganti yang sedia ada",
+ "skip": "Langkau yang sedia ada",
+ "install": "Pasang ke DeepChat",
+ "failed": "Pemasangan Git gagal",
+ "success": "Pemasangan Git selesai",
+ "successMessage": "{count} dipasang, {failed} gagal",
+ "menuItem": "Repositori Git..."
+ },
+ "importExport": {
+ "directory": "Direktori segerak multi-skill setempat",
+ "browse": "Semak imbas",
+ "save": "Simpan",
+ "saved": "Direktori penyegerakan disimpan",
+ "export": "Eksport ke direktori",
+ "import": "Import dari direktori",
+ "includeDisabled": "Sertakan skill dinyahdayakan",
+ "previewExport": "Pratonton Eksport",
+ "exportNow": "Eksport Sekarang",
+ "previewImport": "Pratonton Import",
+ "importSelected": "Import yang Dipilih",
+ "noExportPreview": "Pratonton eksport untuk melihat status sasaran.",
+ "noImportPreview": "Pratonton import untuk melihat skill tersedia.",
+ "strategy": "Strategi konflik",
+ "rename": "Namakan semula import",
+ "overwrite": "Ganti setempat",
+ "skip": "Langkau yang sedia ada",
+ "exported": "Eksport selesai",
+ "imported": "Import selesai",
+ "result": "{count} selesai, {failed} gagal",
+ "state": {
+ "new": "Baharu",
+ "same": "Sama",
+ "modified": "Diubah",
+ "conflict": "Konflik",
+ "invalid": "Tidak sah"
+ }
+ },
+ "detail": {
+ "failed": "Gagal memuat butiran skill",
+ "noDescription": "Tiada penerangan",
+ "empty": "Tiada kandungan pratonton",
+ "installToAgent": "Pasang ke Agent",
+ "enabled": "Didayakan",
+ "disabled": "Dilumpuhkan",
+ "enable": "Dayakan dalam DeepChat",
+ "disable": "Lumpuhkan dalam DeepChat",
+ "preview": "Pratonton",
+ "edit": "Edit",
+ "delete": "Padam",
+ "confirmDeleteTitle": "Padam skill",
+ "confirmDeleteDescription": "Padam skill \"{name}\"? Tindakan ini tidak boleh dibuat asal."
+ },
+ "installToAgent": {
+ "title": "Pasang {name} ke Agent",
+ "description": "Mencipta pautan milik DeepChat untuk skill ini dalam direktori skills agent setempat.",
+ "failed": "Gagal memasang ke Agent",
+ "emptyAgents": "Tiada agent setempat tersedia dikesan.",
+ "target": "Agent sasaran",
+ "preview": "Pratonton",
+ "loadingPreview": "Menyediakan pratonton pautan...",
+ "noPreview": "Pilih agent untuk melihat pratonton.",
+ "install": "Pasang",
+ "success": "Dipasang ke Agent",
+ "successMessage": "{name} telah dipautkan ke agent sasaran.",
+ "disconnect": "Putuskan",
+ "disconnectSuccess": "Diputuskan daripada Agent",
+ "disconnectSuccessMessage": "{name} tidak lagi dipautkan kepada Agent sasaran.",
+ "disconnectFailed": "Gagal memutuskan daripada Agent"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/pl-PL/settings.json b/src/renderer/src/i18n/pl-PL/settings.json
index f60f64deb1..a2333a690c 100644
--- a/src/renderer/src/i18n/pl-PL/settings.json
+++ b/src/renderer/src/i18n/pl-PL/settings.json
@@ -1910,7 +1910,9 @@
"success": "Instalacja pomyślna",
"successMessage": "Umiejętność {name} została pomyślnie zainstalowana",
"failed": "Instalacja nie powiodła się",
- "dragInvalid": "Nieobsługiwana zawartość. Przeciągnij folder umiejętności lub pojedynczy plik .zip."
+ "dragInvalid": "Nieobsługiwana zawartość. Przeciągnij folder umiejętności lub pojedynczy plik .zip.",
+ "basicTitle": "Folder, ZIP lub URL",
+ "basicDescription": "Użyj istniejącego instalatora dla lokalnych folderów, paczek ZIP i adresów URL ZIP."
},
"delete": {
"title": "Usuń umiejętność",
@@ -1923,7 +1925,24 @@
"scripts": "Skrypty {count}",
"env": "środowisko {count}",
"pythonShort": "P",
- "nodeShort": "Węzeł"
+ "nodeShort": "Węzeł",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "Pokaż szczegóły",
+ "edit": "Edytuj",
+ "installToAgent": "Zainstaluj w Agent"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
},
"edit": {
"title": "Edytuj umiejętność",
@@ -1945,7 +1964,7 @@
"pythonRuntime": "Środowisko wykonawcze Pythona",
"nodeRuntime": "Środowisko wykonawcze węzła",
"envTitle": "Zmienne środowiskowe",
- "envWarning": "Maskowane tylko w interfejsie użytkownika. Wartości są przechowywane w postaci zwykłego tekstu w pliku pomocniczym umiejętności.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Dołączone skrypty",
"scriptsHint": "Tylko skrypty znajdujące się w sekcji scripts/ mogą być uruchamiane poprzez Skill_run.",
"noScripts": "Nie wykryto żadnych uruchamialnych skryptów",
@@ -2054,6 +2073,196 @@
"dontShowAgain": "Nie pokazuj tego więcej",
"skip": "Pomiń",
"importSelected": "Importuj wybrane"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "Katalog synchronizacji"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "Podgląd"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "Pokaż"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Skanuj repozytorium Git i instaluj wybrane skills w DeepChat.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "Skanuj",
+ "detectedFormat": "Wykryty format:",
+ "format": {
+ "single-skill": "Pojedynczy skill",
+ "multi-skill": "Wiele skills"
+ },
+ "selectedCount": "Wybrano {count}",
+ "empty": "Nie znaleziono prawidłowych skills.",
+ "conflict": "Konflikt",
+ "invalid": "Nieprawidłowy",
+ "strategy": "Strategia konfliktu",
+ "rename": "Zmień nazwę nowego skill",
+ "overwrite": "Zastąp istniejący",
+ "skip": "Pomiń istniejący",
+ "install": "Instaluj w DeepChat",
+ "failed": "Instalacja Git nie powiodła się",
+ "success": "Instalacja Git zakończona",
+ "successMessage": "Zainstalowano {count}, niepowodzeń {failed}",
+ "menuItem": "Repozytorium Git..."
+ },
+ "importExport": {
+ "directory": "Lokalny katalog synchronizacji multi-skill",
+ "browse": "Przeglądaj",
+ "save": "Zapisz",
+ "saved": "Katalog synchronizacji zapisany",
+ "export": "Eksportuj do katalogu",
+ "import": "Importuj z katalogu",
+ "includeDisabled": "Uwzględnij wyłączone skills",
+ "previewExport": "Podgląd eksportu",
+ "exportNow": "Eksportuj teraz",
+ "previewImport": "Podgląd importu",
+ "importSelected": "Importuj wybrane",
+ "noExportPreview": "Wyświetl podgląd eksportu, aby zobaczyć stany celu.",
+ "noImportPreview": "Wyświetl podgląd importu, aby zobaczyć dostępne skills.",
+ "strategy": "Strategia konfliktu",
+ "rename": "Zmień nazwę importu",
+ "overwrite": "Zastąp lokalny",
+ "skip": "Pomiń istniejący",
+ "exported": "Eksport zakończony",
+ "imported": "Import zakończony",
+ "result": "Ukończono {count}, niepowodzeń {failed}",
+ "state": {
+ "new": "Nowy",
+ "same": "Taki sam",
+ "modified": "Zmieniony",
+ "conflict": "Konflikt",
+ "invalid": "Nieprawidłowy"
+ }
+ },
+ "detail": {
+ "failed": "Nie udało się wczytać szczegółów skill",
+ "noDescription": "Brak opisu",
+ "empty": "Brak treści do podglądu",
+ "installToAgent": "Zainstaluj w Agent",
+ "enabled": "Włączone",
+ "disabled": "Wyłączone",
+ "enable": "Włącz w DeepChat",
+ "disable": "Wyłącz w DeepChat",
+ "preview": "Podgląd",
+ "edit": "Edytuj",
+ "delete": "Usuń",
+ "confirmDeleteTitle": "Usuń skill",
+ "confirmDeleteDescription": "Usunąć skill „{name}”? Tej akcji nie można cofnąć."
+ },
+ "installToAgent": {
+ "title": "Zainstaluj {name} w Agent",
+ "description": "Tworzy link zarządzany przez DeepChat dla tego skill w lokalnym katalogu skills agenta.",
+ "failed": "Instalacja w Agent nie powiodła się",
+ "emptyAgents": "Nie wykryto dostępnych lokalnych agentów.",
+ "target": "Docelowy Agent",
+ "preview": "Podgląd",
+ "loadingPreview": "Przygotowywanie podglądu linku...",
+ "noPreview": "Wybierz agenta, aby zobaczyć podgląd.",
+ "install": "Zainstaluj",
+ "success": "Zainstalowano w Agent",
+ "successMessage": "{name} połączono z docelowym agentem.",
+ "disconnect": "Odłącz",
+ "disconnectSuccess": "Odłączono od Agent",
+ "disconnectSuccessMessage": "{name} nie jest już połączony z docelowym Agent.",
+ "disconnectFailed": "Nie udało się odłączyć od Agent"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/pt-BR/settings.json b/src/renderer/src/i18n/pt-BR/settings.json
index e4250b0ea8..53e4377198 100644
--- a/src/renderer/src/i18n/pt-BR/settings.json
+++ b/src/renderer/src/i18n/pt-BR/settings.json
@@ -1821,7 +1821,7 @@
"pythonRuntime": "Python Runtime",
"nodeRuntime": "Node Runtime",
"envTitle": "Environment Variables",
- "envWarning": "Masked in the UI only. Values are stored as plain text in the skill sidecar file.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Bundled Scripts",
"scriptsHint": "Only scripts under scripts/ can be run via skill_run.",
"noScripts": "No runnable scripts detected",
@@ -1854,7 +1854,9 @@
"title": "Instalar skill",
"urlHint": "Digite o link de download do pacote skill",
"urlPlaceholder": "Digite o endereço de download do skill ZIP",
- "zipHint": "Clique ou arraste um arquivo ZIP aqui"
+ "zipHint": "Clique ou arraste um arquivo ZIP aqui",
+ "basicTitle": "Pasta, ZIP ou URL",
+ "basicDescription": "Use o instalador existente para pastas locais, pacotes ZIP e URLs de download ZIP."
},
"noResults": "Skill correspondente não encontrada",
"openFolder": "Abrir pasta",
@@ -1954,7 +1956,214 @@
"scripts": "{count} scripts",
"env": "{count} env",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "Ver detalhes",
+ "edit": "Editar",
+ "installToAgent": "Instalar no Agent"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "Diretorio de sincronizacao"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "Previa"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "Ver"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Verifique um repositório Git e instale as skills selecionadas no DeepChat.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "Verificar",
+ "detectedFormat": "Formato detectado:",
+ "format": {
+ "single-skill": "Skill única",
+ "multi-skill": "Multi-skill"
+ },
+ "selectedCount": "{count} selecionadas",
+ "empty": "Nenhuma skill válida encontrada.",
+ "conflict": "Conflito",
+ "invalid": "Inválida",
+ "strategy": "Estratégia de conflito",
+ "rename": "Renomear nova skill",
+ "overwrite": "Substituir existente",
+ "skip": "Ignorar existente",
+ "install": "Instalar no DeepChat",
+ "failed": "Falha na instalação Git",
+ "success": "Instalação Git concluída",
+ "successMessage": "{count} instaladas, {failed} falharam",
+ "menuItem": "Repositorio Git..."
+ },
+ "importExport": {
+ "directory": "Diretorio local de sincronizacao multi-skill",
+ "browse": "Procurar",
+ "save": "Salvar",
+ "saved": "Diretório de sincronização salvo",
+ "export": "Exportar para diretorio",
+ "import": "Importar do diretorio",
+ "includeDisabled": "Incluir skills desativadas",
+ "previewExport": "Pré-visualizar exportação",
+ "exportNow": "Exportar agora",
+ "previewImport": "Pré-visualizar importação",
+ "importSelected": "Importar selecionadas",
+ "noExportPreview": "Pré-visualize a exportação para ver os estados de destino.",
+ "noImportPreview": "Pré-visualize a importação para ver as skills disponíveis.",
+ "strategy": "Estratégia de conflito",
+ "rename": "Renomear importada",
+ "overwrite": "Substituir local",
+ "skip": "Ignorar existente",
+ "exported": "Exportação concluída",
+ "imported": "Importação concluída",
+ "result": "{count} concluídas, {failed} falharam",
+ "state": {
+ "new": "Nova",
+ "same": "Igual",
+ "modified": "Modificada",
+ "conflict": "Conflito",
+ "invalid": "Inválida"
+ }
+ },
+ "detail": {
+ "failed": "Falha ao carregar detalhes do skill",
+ "noDescription": "Sem descricao",
+ "empty": "Sem conteudo para visualizar",
+ "installToAgent": "Instalar no Agent",
+ "enabled": "Ativado",
+ "disabled": "Desativado",
+ "enable": "Ativar no DeepChat",
+ "disable": "Desativar no DeepChat",
+ "preview": "Prévia",
+ "edit": "Editar",
+ "delete": "Excluir",
+ "confirmDeleteTitle": "Excluir skill",
+ "confirmDeleteDescription": "Excluir o skill \"{name}\"? Esta ação não pode ser desfeita."
+ },
+ "installToAgent": {
+ "title": "Instalar {name} no Agent",
+ "description": "Cria um link gerenciado pelo DeepChat para este skill no diretorio skills de um agent local.",
+ "failed": "Falha ao instalar no Agent",
+ "emptyAgents": "Nenhum agent local disponivel detectado.",
+ "target": "Agent de destino",
+ "preview": "Previa",
+ "loadingPreview": "Preparando previa do link...",
+ "noPreview": "Selecione um agent para ver a previa.",
+ "install": "Instalar",
+ "success": "Instalado no Agent",
+ "successMessage": "{name} foi vinculado ao agent de destino.",
+ "disconnect": "Desconectar",
+ "disconnectSuccess": "Desconectado do Agent",
+ "disconnectSuccessMessage": "{name} não está mais vinculado ao Agent de destino.",
+ "disconnectFailed": "Falha ao desconectar do Agent"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/ru-RU/settings.json b/src/renderer/src/i18n/ru-RU/settings.json
index 73dd2d024c..aa4c329a7f 100644
--- a/src/renderer/src/i18n/ru-RU/settings.json
+++ b/src/renderer/src/i18n/ru-RU/settings.json
@@ -1821,7 +1821,7 @@
"pythonRuntime": "Python Runtime",
"nodeRuntime": "Node Runtime",
"envTitle": "Environment Variables",
- "envWarning": "Masked in the UI only. Values are stored as plain text in the skill sidecar file.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Bundled Scripts",
"scriptsHint": "Only scripts under scripts/ can be run via skill_run.",
"noScripts": "No runnable scripts detected",
@@ -1854,7 +1854,9 @@
"successMessage": "Навык {name} успешно установлен",
"tabUrl": "URL",
"tabZip": "ZIP",
- "urlHint": "Введите ссылку для скачивания пакета skill"
+ "urlHint": "Введите ссылку для скачивания пакета skill",
+ "basicTitle": "Папка, ZIP или URL",
+ "basicDescription": "Используйте существующий установщик для локальных папок, ZIP-пакетов и URL загрузки ZIP."
},
"noResults": "Не найдено совпадающего навыка",
"openFolder": "Открыть папку",
@@ -1954,7 +1956,214 @@
"scripts": "{count} scripts",
"env": "{count} env",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "Показать детали",
+ "edit": "Редактировать",
+ "installToAgent": "Установить в Agent"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "Каталог синхронизации"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "Предпросмотр"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "Открыть"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Сканируйте Git-репозиторий и установите выбранные skills в DeepChat.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "Сканировать",
+ "detectedFormat": "Обнаруженный формат:",
+ "format": {
+ "single-skill": "Один skill",
+ "multi-skill": "Несколько skills"
+ },
+ "selectedCount": "Выбрано: {count}",
+ "empty": "Действительные skills не найдены.",
+ "conflict": "Конфликт",
+ "invalid": "Недействительно",
+ "strategy": "Стратегия конфликта",
+ "rename": "Переименовать новый skill",
+ "overwrite": "Заменить существующий",
+ "skip": "Пропустить существующий",
+ "install": "Установить в DeepChat",
+ "failed": "Ошибка установки Git",
+ "success": "Установка Git завершена",
+ "successMessage": "Установлено: {count}, ошибок: {failed}",
+ "menuItem": "Git-репозиторий..."
+ },
+ "importExport": {
+ "directory": "Локальный каталог синхронизации multi-skill",
+ "browse": "Обзор",
+ "save": "Сохранить",
+ "saved": "Каталог синхронизации сохранен",
+ "export": "Экспорт в каталог",
+ "import": "Импорт из каталога",
+ "includeDisabled": "Включить отключенные skills",
+ "previewExport": "Предпросмотр экспорта",
+ "exportNow": "Экспортировать",
+ "previewImport": "Предпросмотр импорта",
+ "importSelected": "Импортировать выбранное",
+ "noExportPreview": "Запустите предпросмотр экспорта, чтобы увидеть состояния цели.",
+ "noImportPreview": "Запустите предпросмотр импорта, чтобы увидеть доступные skills.",
+ "strategy": "Стратегия конфликта",
+ "rename": "Переименовать импорт",
+ "overwrite": "Заменить локальный",
+ "skip": "Пропустить существующий",
+ "exported": "Экспорт завершен",
+ "imported": "Импорт завершен",
+ "result": "Завершено: {count}, ошибок: {failed}",
+ "state": {
+ "new": "Новый",
+ "same": "Совпадает",
+ "modified": "Изменен",
+ "conflict": "Конфликт",
+ "invalid": "Недействителен"
+ }
+ },
+ "detail": {
+ "failed": "Не удалось загрузить детали skill",
+ "noDescription": "Нет описания",
+ "empty": "Нет содержимого для предпросмотра",
+ "installToAgent": "Установить в Agent",
+ "enabled": "Включено",
+ "disabled": "Отключено",
+ "enable": "Включить в DeepChat",
+ "disable": "Отключить в DeepChat",
+ "preview": "Предпросмотр",
+ "edit": "Изменить",
+ "delete": "Удалить",
+ "confirmDeleteTitle": "Удалить skill",
+ "confirmDeleteDescription": "Удалить skill «{name}»? Это действие нельзя отменить."
+ },
+ "installToAgent": {
+ "title": "Установить {name} в Agent",
+ "description": "Создает ссылку DeepChat для этого skill в локальном каталоге skills агента.",
+ "failed": "Не удалось установить в Agent",
+ "emptyAgents": "Доступные локальные агенты не найдены.",
+ "target": "Целевой Agent",
+ "preview": "Предпросмотр",
+ "loadingPreview": "Подготовка предпросмотра ссылки...",
+ "noPreview": "Выберите агента для предпросмотра.",
+ "install": "Установить",
+ "success": "Установлено в Agent",
+ "successMessage": "{name} связан с целевым агентом.",
+ "disconnect": "Отключить",
+ "disconnectSuccess": "Отключено от Agent",
+ "disconnectSuccessMessage": "{name} больше не связан с целевым Agent.",
+ "disconnectFailed": "Не удалось отключить от Agent"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/tr-TR/settings.json b/src/renderer/src/i18n/tr-TR/settings.json
index fee39cedee..2a5b09cb18 100644
--- a/src/renderer/src/i18n/tr-TR/settings.json
+++ b/src/renderer/src/i18n/tr-TR/settings.json
@@ -1910,7 +1910,9 @@
"success": "Kurulum Başarılı",
"successMessage": "Skill {name} başarıyla kuruldu",
"failed": "Kurulum Başarısız",
- "dragInvalid": "Desteklenmeyen içerik. Bir skill klasörü veya tek bir .zip dosyası sürükleyin."
+ "dragInvalid": "Desteklenmeyen içerik. Bir skill klasörü veya tek bir .zip dosyası sürükleyin.",
+ "basicTitle": "Klasör, ZIP veya URL",
+ "basicDescription": "Yerel klasörler, ZIP paketleri ve ZIP indirme URL’leri için mevcut yükleyiciyi kullanın."
},
"delete": {
"title": "Skill'yi sil",
@@ -1923,7 +1925,24 @@
"scripts": "{count} komut dosyaları",
"env": "{count} ortam",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "Ayrintilari gor",
+ "edit": "Duzenle",
+ "installToAgent": "Agent a yukle"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
},
"edit": {
"title": "Skill'yi düzenleyin",
@@ -1945,7 +1964,7 @@
"pythonRuntime": "Python Çalışma Zamanı",
"nodeRuntime": "Node Çalışma Zamanı",
"envTitle": "Ortam Değişkenleri",
- "envWarning": "Yalnızca kullanıcı arayüzünde maskelenmiştir. Değerler skill sepet dosyasında düz metin olarak saklanır.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Paketlenmiş Komut Dosyaları",
"scriptsHint": "Skill_run aracılığıyla yalnızca scripts/ altındaki komut dosyaları çalıştırılabilir.",
"noScripts": "Çalıştırılabilir komut dosyası algılanmadı",
@@ -2054,6 +2073,196 @@
"dontShowAgain": "Bunu bir daha gösterme",
"skip": "Atlamak",
"importSelected": "Seçileni İçe Aktar"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "Senkron dizini"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "Onizleme"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "Gor"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Bir Git deposunu tara ve seçili skill’leri DeepChat’e yükle.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "Tara",
+ "detectedFormat": "Algılanan biçim:",
+ "format": {
+ "single-skill": "Tek skill",
+ "multi-skill": "Çoklu skill"
+ },
+ "selectedCount": "{count} seçildi",
+ "empty": "Geçerli skill bulunamadı.",
+ "conflict": "Çakışma",
+ "invalid": "Geçersiz",
+ "strategy": "Çakışma stratejisi",
+ "rename": "Yeni skill’i yeniden adlandır",
+ "overwrite": "Mevcut olanı değiştir",
+ "skip": "Mevcut olanı atla",
+ "install": "DeepChat’e yükle",
+ "failed": "Git yüklemesi başarısız",
+ "success": "Git yüklemesi tamamlandı",
+ "successMessage": "{count} yüklendi, {failed} başarısız",
+ "menuItem": "Git deposu..."
+ },
+ "importExport": {
+ "directory": "Yerel multi-skill senkron dizini",
+ "browse": "Gözat",
+ "save": "Kaydet",
+ "saved": "Eşitleme dizini kaydedildi",
+ "export": "Dizine aktar",
+ "import": "Dizinden ice aktar",
+ "includeDisabled": "Devre dışı skill’leri dahil et",
+ "previewExport": "Dışa aktarmayı önizle",
+ "exportNow": "Şimdi dışa aktar",
+ "previewImport": "İçe aktarmayı önizle",
+ "importSelected": "Seçilenleri içe aktar",
+ "noExportPreview": "Hedef durumlarını görmek için dışa aktarmayı önizleyin.",
+ "noImportPreview": "Kullanılabilir skill’leri görmek için içe aktarmayı önizleyin.",
+ "strategy": "Çakışma stratejisi",
+ "rename": "İçe aktarılanı yeniden adlandır",
+ "overwrite": "Yereli değiştir",
+ "skip": "Mevcut olanı atla",
+ "exported": "Dışa aktarma tamamlandı",
+ "imported": "İçe aktarma tamamlandı",
+ "result": "{count} tamamlandı, {failed} başarısız",
+ "state": {
+ "new": "Yeni",
+ "same": "Aynı",
+ "modified": "Değiştirildi",
+ "conflict": "Çakışma",
+ "invalid": "Geçersiz"
+ }
+ },
+ "detail": {
+ "failed": "Skill ayrintilari yuklenemedi",
+ "noDescription": "Aciklama yok",
+ "empty": "Onizlenecek icerik yok",
+ "installToAgent": "Agent içine yükle",
+ "enabled": "Etkin",
+ "disabled": "Devre dışı",
+ "enable": "DeepChat içinde etkinleştir",
+ "disable": "DeepChat içinde devre dışı bırak",
+ "preview": "Önizleme",
+ "edit": "Düzenle",
+ "delete": "Sil",
+ "confirmDeleteTitle": "skill sil",
+ "confirmDeleteDescription": "\"{name}\" skill silinsin mi? Bu işlem geri alınamaz."
+ },
+ "installToAgent": {
+ "title": "{name} skillini Agent a yukle",
+ "description": "Bu skill icin yerel agent skills dizininde DeepChat sahipli bir baglanti olusturur.",
+ "failed": "Agent a yukleme basarisiz",
+ "emptyAgents": "Kullanilabilir yerel agent bulunamadi.",
+ "target": "Hedef Agent",
+ "preview": "Onizleme",
+ "loadingPreview": "Baglanti onizlemesi hazirlaniyor...",
+ "noPreview": "Onizleme icin bir agent secin.",
+ "install": "Yukle",
+ "success": "Agent a yuklendi",
+ "successMessage": "{name} hedef agent a baglandi.",
+ "disconnect": "Bağlantıyı kes",
+ "disconnectSuccess": "Agent bağlantısı kesildi",
+ "disconnectSuccessMessage": "{name} artık hedef Agent ile bağlantılı değil.",
+ "disconnectFailed": "Agent bağlantısı kesilemedi"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/vi-VN/settings.json b/src/renderer/src/i18n/vi-VN/settings.json
index 28a6dfaa54..2bf9f62e54 100644
--- a/src/renderer/src/i18n/vi-VN/settings.json
+++ b/src/renderer/src/i18n/vi-VN/settings.json
@@ -1910,7 +1910,9 @@
"success": "Cài đặt thành công",
"successMessage": "Kỹ năng {name} đã được cài đặt thành công",
"failed": "Cài đặt không thành công",
- "dragInvalid": "Nội dung không được hỗ trợ. Hãy kéo một thư mục kỹ năng hoặc một tệp .zip."
+ "dragInvalid": "Nội dung không được hỗ trợ. Hãy kéo một thư mục kỹ năng hoặc một tệp .zip.",
+ "basicTitle": "Thư mục, ZIP hoặc URL",
+ "basicDescription": "Dùng trình cài đặt hiện có cho thư mục cục bộ, gói ZIP và URL tải ZIP."
},
"delete": {
"title": "Xóa kỹ năng",
@@ -1923,7 +1925,24 @@
"scripts": "Tập lệnh {count}",
"env": "{count} môi trường",
"pythonShort": "Py",
- "nodeShort": "nút"
+ "nodeShort": "nút",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "enable": "Enable in DeepChat",
+ "disable": "Disable in DeepChat",
+ "viewDetails": "Xem chi tiet",
+ "edit": "Chinh sua",
+ "installToAgent": "Cai vao Agent"
+ },
+ "enable": {
+ "success": "Skill Enabled",
+ "successMessage": "Skill {name} is enabled in DeepChat",
+ "failed": "Enable Failed"
+ },
+ "disable": {
+ "success": "Skill Disabled",
+ "successMessage": "Skill {name} is disabled in DeepChat",
+ "failed": "Disable Failed"
},
"edit": {
"title": "Chỉnh sửa kỹ năng",
@@ -1945,7 +1964,7 @@
"pythonRuntime": "Thời gian chạy Python",
"nodeRuntime": "Thời gian chạy nút",
"envTitle": "Biến môi trường",
- "envWarning": "Chỉ ẩn trong giao diện người dùng. Các giá trị được lưu trữ dưới dạng văn bản thuần túy trong tệp sidecar kỹ năng.",
+ "envWarning": "Masked in the UI only. Values are stored as plain text in the application database.",
"scriptsTitle": "Tập lệnh đi kèm",
"scriptsHint": "Chỉ các tập lệnh trong scripts/ mới có thể chạy qua Skill_run.",
"noScripts": "Không phát hiện thấy tập lệnh có thể chạy nào",
@@ -2054,6 +2073,196 @@
"dontShowAgain": "Đừng hiển thị lại điều này",
"skip": "Bỏ qua",
"importSelected": "Nhập đã chọn"
+ },
+ "tabs": {
+ "library": "Library",
+ "agents": "Agents",
+ "syncDirectory": "Thu muc dong bo"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} supported agent(s)",
+ "refresh": "Refresh",
+ "empty": "No supported agents found.",
+ "emptySkills": "No skills found for this agent.",
+ "loadFailed": "Failed to load agent skills",
+ "conflictCount": "{count} conflict",
+ "table": {
+ "skill": "Skill",
+ "owner": "Owner",
+ "status": "Status",
+ "action": "Action",
+ "preview": "Xem truoc"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "External",
+ "broken-link": "Broken link",
+ "unknown": "Unknown"
+ },
+ "status": {
+ "linked": "Linked",
+ "agent-owned": "Agent owned",
+ "linked-out": "Linked out",
+ "broken-link": "Broken link",
+ "conflict": "Conflict",
+ "empty": "Empty"
+ },
+ "actions": {
+ "adopt": "Adopt",
+ "resolve-conflict": "Resolve",
+ "repair-link": "Repair",
+ "remove-link": "Remove",
+ "open": "Open",
+ "pending": "Write actions are enabled in a later phase.",
+ "view": "Xem"
+ },
+ "agentStatus": {
+ "ready": "Ready",
+ "detected-no-skills-dir": "No skills directory",
+ "permission-denied": "Permission denied"
+ },
+ "counts": {
+ "skills": "{count} skill(s)",
+ "linked": "{count} linked",
+ "agentOwned": "{count} agent owned",
+ "conflicts": "{count} conflict(s)",
+ "broken": "{count} broken"
+ },
+ "adoptDialog": {
+ "adoptTitle": "Adopt Skill",
+ "conflictTitle": "Resolve Conflict",
+ "adoptDescription": "Copy this skill into DeepChat, back up the original, and replace the agent entry with a link.",
+ "conflictDescription": "{skill} already exists in DeepChat. The default action keeps the current DeepChat skill and adopts this agent skill as a renamed copy.",
+ "loading": "Preparing adoption preview...",
+ "previewFailed": "Failed to prepare adoption preview",
+ "executeFailed": "Failed to adopt skill",
+ "currentLocation": "Current location",
+ "afterAdoption": "After adoption",
+ "backup": "Backup",
+ "chooseAction": "Choose action",
+ "adoptAs": "Adopt as {name}",
+ "replaceDeepChat": "Replace existing DeepChat skill",
+ "keepCurrent": "Keep current state",
+ "unsupportedStrategies": "Replace and keep strategies are not available in this build.",
+ "warnings": "Warnings",
+ "apply": "Apply",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill adopted",
+ "successDescription": "{name} is now managed by DeepChat."
+ },
+ "syncToAgent": "Sync to Agent",
+ "syncDialog": {
+ "title": "Sync to {agent}",
+ "description": "Create DeepChat-owned links for selected skills. Existing agent content is skipped.",
+ "target": "Target",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "No linkable DeepChat skills.",
+ "preview": "Preview",
+ "loading": "Preparing link preview...",
+ "noSelection": "Select at least one skill.",
+ "apply": "Apply",
+ "failed": "Failed to sync skills",
+ "successTitle": "Skills synced",
+ "successDescription": "{count} skill(s) linked.",
+ "status": {
+ "ready": "Ready",
+ "already-linked": "Already linked",
+ "conflict": "Conflict",
+ "missing": "Missing"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "Link repaired",
+ "removeSuccess": "Link removed",
+ "successDescription": "{name} updated.",
+ "failed": "Failed to update link"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "Quét kho Git và cài các skill đã chọn vào DeepChat.",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "Quét",
+ "detectedFormat": "Định dạng phát hiện:",
+ "format": {
+ "single-skill": "Một skill",
+ "multi-skill": "Nhiều skill"
+ },
+ "selectedCount": "Đã chọn {count}",
+ "empty": "Không tìm thấy skill hợp lệ.",
+ "conflict": "Xung đột",
+ "invalid": "Không hợp lệ",
+ "strategy": "Chiến lược xung đột",
+ "rename": "Đổi tên skill mới",
+ "overwrite": "Thay thế mục hiện có",
+ "skip": "Bỏ qua mục hiện có",
+ "install": "Cài vào DeepChat",
+ "failed": "Cài đặt Git thất bại",
+ "success": "Cài đặt Git hoàn tất",
+ "successMessage": "Đã cài {count}, thất bại {failed}",
+ "menuItem": "Kho Git..."
+ },
+ "importExport": {
+ "directory": "Thu muc dong bo multi-skill cuc bo",
+ "browse": "Duyệt",
+ "save": "Lưu",
+ "saved": "Đã lưu thư mục đồng bộ",
+ "export": "Xuat vao thu muc",
+ "import": "Nhap tu thu muc",
+ "includeDisabled": "Bao gồm skill đã tắt",
+ "previewExport": "Xem trước xuất",
+ "exportNow": "Xuất ngay",
+ "previewImport": "Xem trước nhập",
+ "importSelected": "Nhập mục đã chọn",
+ "noExportPreview": "Xem trước xuất để xem trạng thái đích.",
+ "noImportPreview": "Xem trước nhập để xem các skill có sẵn.",
+ "strategy": "Chiến lược xung đột",
+ "rename": "Đổi tên mục nhập",
+ "overwrite": "Thay thế cục bộ",
+ "skip": "Bỏ qua mục hiện có",
+ "exported": "Xuất hoàn tất",
+ "imported": "Nhập hoàn tất",
+ "result": "Hoàn tất {count}, thất bại {failed}",
+ "state": {
+ "new": "Mới",
+ "same": "Giống nhau",
+ "modified": "Đã sửa",
+ "conflict": "Xung đột",
+ "invalid": "Không hợp lệ"
+ }
+ },
+ "detail": {
+ "failed": "Khong tai duoc chi tiet skill",
+ "noDescription": "Khong co mo ta",
+ "empty": "Khong co noi dung xem truoc",
+ "installToAgent": "Cài vào Agent",
+ "enabled": "Đã bật",
+ "disabled": "Đã tắt",
+ "enable": "Bật trong DeepChat",
+ "disable": "Tắt trong DeepChat",
+ "preview": "Xem trước",
+ "edit": "Chỉnh sửa",
+ "delete": "Xóa",
+ "confirmDeleteTitle": "Xóa skill",
+ "confirmDeleteDescription": "Xóa skill \"{name}\"? Không thể hoàn tác thao tác này."
+ },
+ "installToAgent": {
+ "title": "Cai {name} vao Agent",
+ "description": "Tao lien ket do DeepChat quan ly cho skill nay trong thu muc skills cua agent cuc bo.",
+ "failed": "Cai vao Agent that bai",
+ "emptyAgents": "Khong phat hien agent cuc bo kha dung.",
+ "target": "Agent dich",
+ "preview": "Xem truoc",
+ "loadingPreview": "Dang chuan bi xem truoc lien ket...",
+ "noPreview": "Chon agent de xem truoc.",
+ "install": "Cai dat",
+ "success": "Da cai vao Agent",
+ "successMessage": "{name} da duoc lien ket voi agent dich.",
+ "disconnect": "Ngắt kết nối",
+ "disconnectSuccess": "Đã ngắt khỏi Agent",
+ "disconnectSuccessMessage": "{name} không còn được liên kết với Agent đích.",
+ "disconnectFailed": "Không thể ngắt khỏi Agent"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json
index 81e96d6ffb..528d09cb3b 100644
--- a/src/renderer/src/i18n/zh-CN/settings.json
+++ b/src/renderer/src/i18n/zh-CN/settings.json
@@ -1970,7 +1970,9 @@
"success": "安装成功",
"successMessage": "skill {name} 已成功安装",
"failed": "安装失败",
- "dragInvalid": "不支持的拖拽内容,请拖入 skill 文件夹或单个 .zip 文件。"
+ "dragInvalid": "不支持的拖拽内容,请拖入 skill 文件夹或单个 .zip 文件。",
+ "basicTitle": "文件夹、ZIP 或 URL",
+ "basicDescription": "使用现有安装器导入本地文件夹、ZIP 包和 ZIP 下载链接。"
},
"delete": {
"title": "删除 skill",
@@ -1983,7 +1985,24 @@
"scripts": "{count} 个脚本",
"env": "{count} 个环境变量",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "已启用",
+ "disabled": "已禁用",
+ "enable": "在 DeepChat 中启用",
+ "disable": "在 DeepChat 中禁用",
+ "viewDetails": "查看详情",
+ "edit": "编辑",
+ "installToAgent": "安装到 Agent"
+ },
+ "enable": {
+ "success": "已启用 skill",
+ "successMessage": "skill {name} 已在 DeepChat 中启用",
+ "failed": "启用失败"
+ },
+ "disable": {
+ "success": "已禁用 skill",
+ "successMessage": "skill {name} 已在 DeepChat 中禁用",
+ "failed": "禁用失败"
},
"edit": {
"title": "编辑 skill",
@@ -2005,7 +2024,7 @@
"pythonRuntime": "Python 运行时",
"nodeRuntime": "Node 运行时",
"envTitle": "环境变量",
- "envWarning": "仅在 UI 中做遮罩显示,实际会以明文写入 skill sidecar 文件。",
+ "envWarning": "仅在 UI 中做遮罩显示,实际会以明文写入应用数据库。",
"scriptsTitle": "内置脚本",
"scriptsHint": "只有 scripts/ 下的脚本会通过 skill_run 暴露给 agent。",
"noScripts": "未发现可运行脚本",
@@ -2114,6 +2133,196 @@
"dontShowAgain": "不再显示此提示",
"skip": "跳过",
"importSelected": "导入所选"
+ },
+ "tabs": {
+ "library": "技能库",
+ "agents": "Agents",
+ "syncDirectory": "同步目录"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} 个支持的 Agent",
+ "refresh": "刷新",
+ "syncToAgent": "同步到 Agent",
+ "empty": "未检测到支持的 Agent。",
+ "emptySkills": "该 Agent 暂无 skill。",
+ "loadFailed": "加载 Agent skills 失败",
+ "conflictCount": "{count} 冲突",
+ "table": {
+ "skill": "Skill",
+ "owner": "归属",
+ "status": "状态",
+ "action": "操作",
+ "preview": "预览"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "外部链接",
+ "broken-link": "断开的链接",
+ "unknown": "未知"
+ },
+ "status": {
+ "linked": "已链接",
+ "agent-owned": "Agent 所有",
+ "linked-out": "链接到外部",
+ "broken-link": "链接断开",
+ "conflict": "冲突",
+ "empty": "空"
+ },
+ "actions": {
+ "adopt": "接管",
+ "resolve-conflict": "解决",
+ "repair-link": "修复",
+ "remove-link": "移除",
+ "open": "打开",
+ "pending": "写入操作将在后续阶段启用。",
+ "view": "查看"
+ },
+ "agentStatus": {
+ "ready": "可用",
+ "detected-no-skills-dir": "无 skills 目录",
+ "permission-denied": "无权限"
+ },
+ "counts": {
+ "skills": "{count} 个 skill",
+ "linked": "{count} 已链接",
+ "agentOwned": "{count} Agent 所有",
+ "conflicts": "{count} 冲突",
+ "broken": "{count} 断开"
+ },
+ "adoptDialog": {
+ "adoptTitle": "接管 Skill",
+ "conflictTitle": "解决冲突",
+ "adoptDescription": "将该 skill 复制到 DeepChat,备份原始内容,并把 Agent 入口替换为链接。",
+ "conflictDescription": "{skill} 已存在于 DeepChat。默认操作会保留当前 DeepChat skill,并将该 Agent skill 以重命名副本接管。",
+ "loading": "正在准备接管预览...",
+ "previewFailed": "准备接管预览失败",
+ "executeFailed": "接管 skill 失败",
+ "currentLocation": "当前位置",
+ "afterAdoption": "接管后",
+ "backup": "备份",
+ "chooseAction": "选择操作",
+ "adoptAs": "接管为 {name}",
+ "replaceDeepChat": "替换现有 DeepChat skill",
+ "keepCurrent": "保持当前状态",
+ "unsupportedStrategies": "当前版本暂不支持替换和保持策略。",
+ "warnings": "警告",
+ "apply": "应用",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill 已接管",
+ "successDescription": "{name} 现在由 DeepChat 管理。"
+ },
+ "syncDialog": {
+ "title": "同步到 {agent}",
+ "description": "为所选 skill 创建 DeepChat 拥有的链接。已有 Agent 内容会被跳过。",
+ "target": "目标",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "没有可链接的 DeepChat skill。",
+ "preview": "预览",
+ "loading": "正在准备链接预览...",
+ "noSelection": "至少选择一个 skill。",
+ "apply": "应用",
+ "failed": "同步 skills 失败",
+ "successTitle": "Skills 已同步",
+ "successDescription": "已链接 {count} 个 skill。",
+ "status": {
+ "ready": "可用",
+ "already-linked": "已链接",
+ "conflict": "冲突",
+ "missing": "缺失"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "链接已修复",
+ "removeSuccess": "链接已移除",
+ "successDescription": "{name} 已更新。",
+ "failed": "更新链接失败"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "扫描 Git 仓库,并将选中的 skills 安装到 DeepChat。",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "扫描",
+ "detectedFormat": "检测到格式:",
+ "format": {
+ "single-skill": "单个 skill",
+ "multi-skill": "多 skill"
+ },
+ "selectedCount": "已选 {count} 个",
+ "empty": "没有找到有效 skill。",
+ "conflict": "冲突",
+ "invalid": "无效",
+ "strategy": "冲突策略",
+ "rename": "重命名新 skill",
+ "overwrite": "替换现有项",
+ "skip": "跳过现有项",
+ "install": "安装到 DeepChat",
+ "failed": "Git 安装失败",
+ "success": "Git 安装完成",
+ "successMessage": "已安装 {count} 个,失败 {failed} 个",
+ "menuItem": "Git 仓库..."
+ },
+ "importExport": {
+ "directory": "本地多 skill 同步目录",
+ "browse": "浏览",
+ "save": "保存",
+ "saved": "同步目录已保存",
+ "export": "导出到目录",
+ "import": "从目录导入",
+ "includeDisabled": "包含已禁用 skills",
+ "previewExport": "预览导出",
+ "exportNow": "立即导出",
+ "previewImport": "预览导入",
+ "importSelected": "导入选中项",
+ "noExportPreview": "先预览导出以查看目标状态。",
+ "noImportPreview": "先预览导入以查看可用 skills。",
+ "strategy": "冲突策略",
+ "rename": "重命名导入项",
+ "overwrite": "替换本地项",
+ "skip": "跳过现有项",
+ "exported": "导出完成",
+ "imported": "导入完成",
+ "result": "完成 {count} 个,失败 {failed} 个",
+ "state": {
+ "new": "新增",
+ "same": "相同",
+ "modified": "已修改",
+ "conflict": "冲突",
+ "invalid": "无效"
+ }
+ },
+ "detail": {
+ "failed": "读取 skill 详情失败",
+ "noDescription": "暂无描述",
+ "empty": "暂无可预览内容",
+ "installToAgent": "安装到 Agent",
+ "enabled": "已启用",
+ "disabled": "已禁用",
+ "enable": "在 DeepChat 中启用",
+ "disable": "在 DeepChat 中禁用",
+ "preview": "预览",
+ "edit": "编辑",
+ "delete": "删除",
+ "confirmDeleteTitle": "删除 skill",
+ "confirmDeleteDescription": "确定要删除 skill「{name}」吗?此操作无法撤销。"
+ },
+ "installToAgent": {
+ "title": "安装 {name} 到 Agent",
+ "description": "为这个 skill 在本机 Agent skills 目录下创建 DeepChat 拥有的链接。",
+ "failed": "安装到 Agent 失败",
+ "emptyAgents": "未检测到可用的本机 Agent。",
+ "target": "目标 Agent",
+ "preview": "预览",
+ "loadingPreview": "正在准备链接预览...",
+ "noPreview": "选择一个 Agent 后查看预览。",
+ "install": "安装",
+ "success": "已安装到 Agent",
+ "successMessage": "{name} 已链接到目标 Agent。",
+ "disconnect": "断开",
+ "disconnectSuccess": "已从 Agent 断开",
+ "disconnectSuccessMessage": "{name} 已从目标 Agent 断开。",
+ "disconnectFailed": "从 Agent 断开失败"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/zh-HK/settings.json b/src/renderer/src/i18n/zh-HK/settings.json
index 7dcf2795ae..f400fabd08 100644
--- a/src/renderer/src/i18n/zh-HK/settings.json
+++ b/src/renderer/src/i18n/zh-HK/settings.json
@@ -1821,7 +1821,7 @@
"pythonRuntime": "Python 執行環境",
"nodeRuntime": "Node 執行環境",
"envTitle": "環境變量",
- "envWarning": "僅在 UI 中遮罩顯示,實際會以明文寫入 skill sidecar 檔案。",
+ "envWarning": "僅在 UI 中遮罩顯示,實際會以明文寫入應用程式資料庫。",
"scriptsTitle": "內置腳本",
"scriptsHint": "只有 scripts/ 目錄下的腳本會通過 skill_run 暴露給 agent。",
"noScripts": "未發現可執行腳本",
@@ -1854,7 +1854,9 @@
"title": "安裝 skill",
"urlHint": "輸入 skill 包的下載連結",
"urlPlaceholder": "輸入 skill ZIP 下載地址",
- "zipHint": "點擊或拖放 ZIP 檔案到此處"
+ "zipHint": "點擊或拖放 ZIP 檔案到此處",
+ "basicTitle": "資料夾、ZIP 或 URL",
+ "basicDescription": "使用現有安裝器匯入本機資料夾、ZIP 套件和 ZIP 下載連結。"
},
"noResults": "未找到匹配的 skill",
"openFolder": "開啟資料夾",
@@ -1954,7 +1956,214 @@
"scripts": "{count} 個腳本",
"env": "{count} 個環境變量",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "已啟用",
+ "disabled": "已停用",
+ "enable": "在 DeepChat 中啟用",
+ "disable": "在 DeepChat 中停用",
+ "viewDetails": "查看詳情",
+ "edit": "編輯",
+ "installToAgent": "安裝到 Agent"
+ },
+ "enable": {
+ "success": "已啟用 skill",
+ "successMessage": "skill {name} 已在 DeepChat 中啟用",
+ "failed": "啟用失敗"
+ },
+ "disable": {
+ "success": "已停用 skill",
+ "successMessage": "skill {name} 已在 DeepChat 中停用",
+ "failed": "停用失敗"
+ },
+ "tabs": {
+ "library": "技能库",
+ "agents": "Agents",
+ "syncDirectory": "同步目錄"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} 个支持的 Agent",
+ "refresh": "刷新",
+ "empty": "未检测到支持的 Agent。",
+ "emptySkills": "该 Agent 暂无 skill。",
+ "loadFailed": "加载 Agent skills 失败",
+ "conflictCount": "{count} 冲突",
+ "table": {
+ "skill": "Skill",
+ "owner": "归属",
+ "status": "状态",
+ "action": "操作",
+ "preview": "預覽"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "外部链接",
+ "broken-link": "断开的链接",
+ "unknown": "未知"
+ },
+ "status": {
+ "linked": "已链接",
+ "agent-owned": "Agent 所有",
+ "linked-out": "链接到外部",
+ "broken-link": "链接断开",
+ "conflict": "冲突",
+ "empty": "空"
+ },
+ "actions": {
+ "adopt": "接管",
+ "resolve-conflict": "解决",
+ "repair-link": "修复",
+ "remove-link": "移除",
+ "open": "打开",
+ "pending": "写入操作将在后续阶段启用。",
+ "view": "查看"
+ },
+ "agentStatus": {
+ "ready": "可用",
+ "detected-no-skills-dir": "无 skills 目录",
+ "permission-denied": "无权限"
+ },
+ "counts": {
+ "skills": "{count} 个 skill",
+ "linked": "{count} 已链接",
+ "agentOwned": "{count} Agent 所有",
+ "conflicts": "{count} 冲突",
+ "broken": "{count} 断开"
+ },
+ "adoptDialog": {
+ "adoptTitle": "接管 Skill",
+ "conflictTitle": "解决冲突",
+ "adoptDescription": "将该 skill 复制到 DeepChat,备份原始内容,并把 Agent 入口替换为链接。",
+ "conflictDescription": "{skill} 已存在于 DeepChat。默认操作会保留当前 DeepChat skill,并将该 Agent skill 以重命名副本接管。",
+ "loading": "正在准备接管预览...",
+ "previewFailed": "准备接管预览失败",
+ "executeFailed": "接管 skill 失败",
+ "currentLocation": "当前位置",
+ "afterAdoption": "接管后",
+ "backup": "备份",
+ "chooseAction": "选择操作",
+ "adoptAs": "接管为 {name}",
+ "replaceDeepChat": "替换现有 DeepChat skill",
+ "keepCurrent": "保持当前状态",
+ "unsupportedStrategies": "当前版本暂不支持替换和保持策略。",
+ "warnings": "警告",
+ "apply": "应用",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill 已接管",
+ "successDescription": "{name} 现在由 DeepChat 管理。"
+ },
+ "syncToAgent": "同步到 Agent",
+ "syncDialog": {
+ "title": "同步到 {agent}",
+ "description": "为所选 skill 创建 DeepChat 拥有的链接。已有 Agent 内容会被跳过。",
+ "target": "目标",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "没有可链接的 DeepChat skill。",
+ "preview": "预览",
+ "loading": "正在准备链接预览...",
+ "noSelection": "至少选择一个 skill。",
+ "apply": "应用",
+ "failed": "同步 skills 失败",
+ "successTitle": "Skills 已同步",
+ "successDescription": "已链接 {count} 个 skill。",
+ "status": {
+ "ready": "可用",
+ "already-linked": "已链接",
+ "conflict": "冲突",
+ "missing": "缺失"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "链接已修复",
+ "removeSuccess": "链接已移除",
+ "successDescription": "{name} 已更新。",
+ "failed": "更新链接失败"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "掃描 Git 儲存庫,並將已選 skills 安裝到 DeepChat。",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "掃描",
+ "detectedFormat": "偵測到格式:",
+ "format": {
+ "single-skill": "單一 skill",
+ "multi-skill": "多 skill"
+ },
+ "selectedCount": "已選 {count} 個",
+ "empty": "找不到有效 skill。",
+ "conflict": "衝突",
+ "invalid": "無效",
+ "strategy": "衝突策略",
+ "rename": "重新命名新 skill",
+ "overwrite": "取代現有項目",
+ "skip": "略過現有項目",
+ "install": "安裝到 DeepChat",
+ "failed": "Git 安裝失敗",
+ "success": "Git 安裝完成",
+ "successMessage": "已安裝 {count} 個,失敗 {failed} 個",
+ "menuItem": "Git 倉庫..."
+ },
+ "importExport": {
+ "directory": "本機多 skill 同步目錄",
+ "browse": "瀏覽",
+ "save": "儲存",
+ "saved": "同步目錄已儲存",
+ "export": "匯出到目錄",
+ "import": "從目錄匯入",
+ "includeDisabled": "包含已停用 skills",
+ "previewExport": "預覽匯出",
+ "exportNow": "立即匯出",
+ "previewImport": "預覽匯入",
+ "importSelected": "匯入已選項目",
+ "noExportPreview": "先預覽匯出以查看目標狀態。",
+ "noImportPreview": "先預覽匯入以查看可用 skills。",
+ "strategy": "衝突策略",
+ "rename": "重新命名匯入項目",
+ "overwrite": "取代本機項目",
+ "skip": "略過現有項目",
+ "exported": "匯出完成",
+ "imported": "匯入完成",
+ "result": "完成 {count} 個,失敗 {failed} 個",
+ "state": {
+ "new": "新增",
+ "same": "相同",
+ "modified": "已修改",
+ "conflict": "衝突",
+ "invalid": "無效"
+ }
+ },
+ "detail": {
+ "failed": "讀取 skill 詳情失敗",
+ "noDescription": "暫無描述",
+ "empty": "暫無可預覽內容",
+ "installToAgent": "安裝到 Agent",
+ "enabled": "已啟用",
+ "disabled": "已停用",
+ "enable": "在 DeepChat 中啟用",
+ "disable": "在 DeepChat 中停用",
+ "preview": "預覽",
+ "edit": "編輯",
+ "delete": "刪除",
+ "confirmDeleteTitle": "刪除 skill",
+ "confirmDeleteDescription": "確定要刪除 skill「{name}」嗎?此操作無法復原。"
+ },
+ "installToAgent": {
+ "title": "安裝 {name} 到 Agent",
+ "description": "為這個 skill 在本機 Agent skills 目錄建立 DeepChat 擁有的連結。",
+ "failed": "安裝到 Agent 失敗",
+ "emptyAgents": "未偵測到可用的本機 Agent。",
+ "target": "目標 Agent",
+ "preview": "預覽",
+ "loadingPreview": "正在準備連結預覽...",
+ "noPreview": "選擇一個 Agent 後查看預覽。",
+ "install": "安裝",
+ "success": "已安裝到 Agent",
+ "successMessage": "{name} 已連結到目標 Agent。",
+ "disconnect": "斷開",
+ "disconnectSuccess": "已從 Agent 斷開",
+ "disconnectSuccessMessage": "{name} 已從目標 Agent 斷開。",
+ "disconnectFailed": "從 Agent 斷開失敗"
}
},
"environments": {
diff --git a/src/renderer/src/i18n/zh-TW/settings.json b/src/renderer/src/i18n/zh-TW/settings.json
index f579a78286..397ccb7d4c 100644
--- a/src/renderer/src/i18n/zh-TW/settings.json
+++ b/src/renderer/src/i18n/zh-TW/settings.json
@@ -1821,7 +1821,7 @@
"pythonRuntime": "Python 執行環境",
"nodeRuntime": "Node 執行環境",
"envTitle": "環境變數",
- "envWarning": "僅在 UI 中遮罩顯示,實際會以明文寫入 skill sidecar 檔案。",
+ "envWarning": "僅在 UI 中遮罩顯示,實際會以明文寫入應用程式資料庫。",
"scriptsTitle": "內建腳本",
"scriptsHint": "只有 scripts/ 目錄下的腳本會透過 skill_run 暴露給 agent。",
"noScripts": "未發現可執行腳本",
@@ -1854,7 +1854,9 @@
"title": "安裝 skill",
"urlHint": "輸入 skill 包的下載連結",
"urlPlaceholder": "輸入 skill ZIP 下載地址",
- "zipHint": "點擊或拖放 ZIP 檔案到此處"
+ "zipHint": "點擊或拖放 ZIP 檔案到此處",
+ "basicTitle": "資料夾、ZIP 或 URL",
+ "basicDescription": "使用現有安裝器匯入本機資料夾、ZIP 套件和 ZIP 下載連結。"
},
"noResults": "未找到匹配的 skill",
"openFolder": "開啟資料夾",
@@ -1954,7 +1956,214 @@
"scripts": "{count} 個腳本",
"env": "{count} 個環境變數",
"pythonShort": "Py",
- "nodeShort": "Node"
+ "nodeShort": "Node",
+ "enabled": "已啟用",
+ "disabled": "已停用",
+ "enable": "在 DeepChat 中啟用",
+ "disable": "在 DeepChat 中停用",
+ "viewDetails": "檢視詳細資訊",
+ "edit": "編輯",
+ "installToAgent": "安裝到 Agent"
+ },
+ "enable": {
+ "success": "已啟用 skill",
+ "successMessage": "skill {name} 已在 DeepChat 中啟用",
+ "failed": "啟用失敗"
+ },
+ "disable": {
+ "success": "已停用 skill",
+ "successMessage": "skill {name} 已在 DeepChat 中停用",
+ "failed": "停用失敗"
+ },
+ "tabs": {
+ "library": "技能库",
+ "agents": "Agents",
+ "syncDirectory": "同步目錄"
+ },
+ "agents": {
+ "title": "Agents",
+ "summary": "{count} 个支持的 Agent",
+ "refresh": "刷新",
+ "empty": "未检测到支持的 Agent。",
+ "emptySkills": "该 Agent 暂无 skill。",
+ "loadFailed": "加载 Agent skills 失败",
+ "conflictCount": "{count} 冲突",
+ "table": {
+ "skill": "Skill",
+ "owner": "归属",
+ "status": "状态",
+ "action": "操作",
+ "preview": "預覽"
+ },
+ "owner": {
+ "deepchat": "DeepChat",
+ "external-link": "外部链接",
+ "broken-link": "断开的链接",
+ "unknown": "未知"
+ },
+ "status": {
+ "linked": "已链接",
+ "agent-owned": "Agent 所有",
+ "linked-out": "链接到外部",
+ "broken-link": "链接断开",
+ "conflict": "冲突",
+ "empty": "空"
+ },
+ "actions": {
+ "adopt": "接管",
+ "resolve-conflict": "解决",
+ "repair-link": "修复",
+ "remove-link": "移除",
+ "open": "打开",
+ "pending": "写入操作将在后续阶段启用。",
+ "view": "檢視"
+ },
+ "agentStatus": {
+ "ready": "可用",
+ "detected-no-skills-dir": "无 skills 目录",
+ "permission-denied": "无权限"
+ },
+ "counts": {
+ "skills": "{count} 个 skill",
+ "linked": "{count} 已链接",
+ "agentOwned": "{count} Agent 所有",
+ "conflicts": "{count} 冲突",
+ "broken": "{count} 断开"
+ },
+ "adoptDialog": {
+ "adoptTitle": "接管 Skill",
+ "conflictTitle": "解决冲突",
+ "adoptDescription": "将该 skill 复制到 DeepChat,备份原始内容,并把 Agent 入口替换为链接。",
+ "conflictDescription": "{skill} 已存在于 DeepChat。默认操作会保留当前 DeepChat skill,并将该 Agent skill 以重命名副本接管。",
+ "loading": "正在准备接管预览...",
+ "previewFailed": "准备接管预览失败",
+ "executeFailed": "接管 skill 失败",
+ "currentLocation": "当前位置",
+ "afterAdoption": "接管后",
+ "backup": "备份",
+ "chooseAction": "选择操作",
+ "adoptAs": "接管为 {name}",
+ "replaceDeepChat": "替换现有 DeepChat skill",
+ "keepCurrent": "保持当前状态",
+ "unsupportedStrategies": "当前版本暂不支持替换和保持策略。",
+ "warnings": "警告",
+ "apply": "应用",
+ "linkArrow": "-> DeepChat skill",
+ "successTitle": "Skill 已接管",
+ "successDescription": "{name} 现在由 DeepChat 管理。"
+ },
+ "syncToAgent": "同步到 Agent",
+ "syncDialog": {
+ "title": "同步到 {agent}",
+ "description": "为所选 skill 创建 DeepChat 拥有的链接。已有 Agent 内容会被跳过。",
+ "target": "目标",
+ "deepchatSkills": "DeepChat skills",
+ "empty": "没有可链接的 DeepChat skill。",
+ "preview": "预览",
+ "loading": "正在准备链接预览...",
+ "noSelection": "至少选择一个 skill。",
+ "apply": "应用",
+ "failed": "同步 skills 失败",
+ "successTitle": "Skills 已同步",
+ "successDescription": "已链接 {count} 个 skill。",
+ "status": {
+ "ready": "可用",
+ "already-linked": "已链接",
+ "conflict": "冲突",
+ "missing": "缺失"
+ }
+ },
+ "linkAction": {
+ "repairSuccess": "链接已修复",
+ "removeSuccess": "链接已移除",
+ "successDescription": "{name} 已更新。",
+ "failed": "更新链接失败"
+ }
+ },
+ "git": {
+ "title": "Git",
+ "description": "掃描 Git 儲存庫,並將選取的 skills 安裝到 DeepChat。",
+ "placeholder": "https://github.com/user/repo",
+ "scan": "掃描",
+ "detectedFormat": "偵測到格式:",
+ "format": {
+ "single-skill": "單一 skill",
+ "multi-skill": "多 skill"
+ },
+ "selectedCount": "已選 {count} 個",
+ "empty": "找不到有效 skill。",
+ "conflict": "衝突",
+ "invalid": "無效",
+ "strategy": "衝突策略",
+ "rename": "重新命名新 skill",
+ "overwrite": "取代現有項目",
+ "skip": "略過現有項目",
+ "install": "安裝到 DeepChat",
+ "failed": "Git 安裝失敗",
+ "success": "Git 安裝完成",
+ "successMessage": "已安裝 {count} 個,失敗 {failed} 個",
+ "menuItem": "Git 儲存庫..."
+ },
+ "importExport": {
+ "directory": "本機多 skill 同步目錄",
+ "browse": "瀏覽",
+ "save": "儲存",
+ "saved": "同步目錄已儲存",
+ "export": "匯出到目錄",
+ "import": "從目錄匯入",
+ "includeDisabled": "包含已停用 skills",
+ "previewExport": "預覽匯出",
+ "exportNow": "立即匯出",
+ "previewImport": "預覽匯入",
+ "importSelected": "匯入選取項目",
+ "noExportPreview": "先預覽匯出以查看目標狀態。",
+ "noImportPreview": "先預覽匯入以查看可用 skills。",
+ "strategy": "衝突策略",
+ "rename": "重新命名匯入項目",
+ "overwrite": "取代本機項目",
+ "skip": "略過現有項目",
+ "exported": "匯出完成",
+ "imported": "匯入完成",
+ "result": "完成 {count} 個,失敗 {failed} 個",
+ "state": {
+ "new": "新增",
+ "same": "相同",
+ "modified": "已修改",
+ "conflict": "衝突",
+ "invalid": "無效"
+ }
+ },
+ "detail": {
+ "failed": "讀取 skill 詳細資訊失敗",
+ "noDescription": "暫無描述",
+ "empty": "暫無可預覽內容",
+ "installToAgent": "安裝到 Agent",
+ "enabled": "已啟用",
+ "disabled": "已停用",
+ "enable": "在 DeepChat 中啟用",
+ "disable": "在 DeepChat 中停用",
+ "preview": "預覽",
+ "edit": "編輯",
+ "delete": "刪除",
+ "confirmDeleteTitle": "刪除 skill",
+ "confirmDeleteDescription": "確定要刪除 skill「{name}」嗎?此操作無法復原。"
+ },
+ "installToAgent": {
+ "title": "安裝 {name} 到 Agent",
+ "description": "為這個 skill 在本機 Agent skills 目錄建立 DeepChat 擁有的連結。",
+ "failed": "安裝到 Agent 失敗",
+ "emptyAgents": "未偵測到可用的本機 Agent。",
+ "target": "目標 Agent",
+ "preview": "預覽",
+ "loadingPreview": "正在準備連結預覽...",
+ "noPreview": "選擇一個 Agent 後檢視預覽。",
+ "install": "安裝",
+ "success": "已安裝到 Agent",
+ "successMessage": "{name} 已連結到目標 Agent。",
+ "disconnect": "斷開",
+ "disconnectSuccess": "已從 Agent 斷開",
+ "disconnectSuccessMessage": "{name} 已從目標 Agent 斷開。",
+ "disconnectFailed": "從 Agent 斷開失敗"
}
},
"environments": {
diff --git a/src/renderer/src/stores/skillsStore.ts b/src/renderer/src/stores/skillsStore.ts
index 574d4bc270..ada502d6d7 100644
--- a/src/renderer/src/stores/skillsStore.ts
+++ b/src/renderer/src/stores/skillsStore.ts
@@ -2,11 +2,11 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { createSkillClient } from '@api/SkillClient'
import type {
- SkillMetadata,
SkillInstallResult,
SkillExtensionConfig,
SkillScriptDescriptor
} from '@shared/types/skill'
+import type { UnifiedSkillItem } from '@shared/types/skillManagement'
function createDefaultSkillExtension(): SkillExtensionConfig {
return {
@@ -24,7 +24,7 @@ export const useSkillsStore = defineStore('skills', () => {
const skillClient = createSkillClient()
let catalogListenerRegistered = false
- const skills = ref([])
+ const skills = ref([])
const skillExtensions = ref>({})
const skillScripts = ref>({})
const loading = ref(false)
@@ -60,7 +60,7 @@ export const useSkillsStore = defineStore('skills', () => {
}
}
- const loadSkillRuntimeData = async (items: SkillMetadata[] = skills.value) => {
+ const loadSkillRuntimeData = async (items: UnifiedSkillItem[] = skills.value) => {
const nextExtensions: Record = {}
const nextScripts: Record = {}
@@ -89,7 +89,7 @@ export const useSkillsStore = defineStore('skills', () => {
loading.value = true
error.value = null
try {
- const nextSkills = await skillClient.getMetadataList()
+ const nextSkills = await skillClient.getUnifiedSkillCatalog()
skills.value = nextSkills
await loadSkillRuntimeData(nextSkills)
} catch (e) {
@@ -187,6 +187,11 @@ export const useSkillsStore = defineStore('skills', () => {
await loadSkillRuntime(name)
}
+ const setSkillDisabled = async (name: string, disabled: boolean): Promise => {
+ await skillClient.setSkillDisabled(name, disabled)
+ await loadSkills()
+ }
+
const saveSkillWithExtension = async (
name: string,
content: string,
@@ -233,6 +238,7 @@ export const useSkillsStore = defineStore('skills', () => {
openSkillsFolder,
updateSkillFile,
saveSkillExtension,
+ setSkillDisabled,
saveSkillWithExtension,
getSkillFolderTree
}
diff --git a/src/shared/contracts/events/skills.events.ts b/src/shared/contracts/events/skills.events.ts
index f12ed0385b..b6f4976a27 100644
--- a/src/shared/contracts/events/skills.events.ts
+++ b/src/shared/contracts/events/skills.events.ts
@@ -7,7 +7,16 @@ const SkillMetadataSchema = z.custom()
export const skillsCatalogChangedEvent = defineEventContract({
name: 'skills.catalog.changed',
payload: z.object({
- reason: z.enum(['discovered', 'installed', 'uninstalled', 'metadata-updated']),
+ reason: z.enum([
+ 'discovered',
+ 'installed',
+ 'uninstalled',
+ 'metadata-updated',
+ 'disabled-updated',
+ 'management-state-updated',
+ 'git-installed',
+ 'sync-directory-updated'
+ ]),
name: z.string().optional(),
skill: SkillMetadataSchema.optional(),
skills: z.array(SkillMetadataSchema).optional(),
diff --git a/src/shared/contracts/routes.ts b/src/shared/contracts/routes.ts
index 4a1cbf4781..1b7d7db74d 100644
--- a/src/shared/contracts/routes.ts
+++ b/src/shared/contracts/routes.ts
@@ -375,27 +375,46 @@ import {
skillsGetDirectoryRoute,
skillsGetExtensionRoute,
skillsGetFolderTreeRoute,
+ skillsGetSyncConfigRoute,
+ skillsExecuteSyncDirectoryExportRoute,
+ skillsExecuteSyncDirectoryImportRoute,
+ skillsInstallFromGitRoute,
skillsInstallFromFolderRoute,
skillsInstallFromUrlRoute,
skillsInstallFromZipRoute,
+ skillsListCatalogRoute,
skillsListMetadataRoute,
skillsListScriptsRoute,
skillsOpenFolderRoute,
+ skillsPreviewSyncDirectoryExportRoute,
+ skillsPreviewSyncDirectoryImportRoute,
skillsReadFileRoute,
+ skillsScanGitRepoRoute,
skillsSaveExtensionRoute,
skillsSaveWithExtensionRoute,
skillsSetActiveRoute,
+ skillsSetDisabledRoute,
+ skillsSetSyncDirectoryRoute,
skillsUninstallRoute,
skillsUpdateFileRoute
} from './routes/skills.routes'
import {
skillSyncAcknowledgeDiscoveriesRoute,
+ skillSyncExecuteAdoptAgentSkillRoute,
skillSyncExecuteExportRoute,
skillSyncExecuteImportRoute,
+ skillSyncExecuteLinkDeepChatSkillsRoute,
+ skillSyncGetAgentDetailRoute,
+ skillSyncGetAgentSkillDetailRoute,
skillSyncGetNewDiscoveriesRoute,
skillSyncGetRegisteredToolsRoute,
+ skillSyncPreviewAdoptAgentSkillRoute,
skillSyncPreviewExportRoute,
skillSyncPreviewImportRoute,
+ skillSyncPreviewLinkDeepChatSkillsRoute,
+ skillSyncRemoveAgentSkillLinkRoute,
+ skillSyncRepairAgentSkillLinkRoute,
+ skillSyncScanAgentsRoute,
skillSyncScanExternalToolsRoute
} from './routes/skillSync.routes'
import {
@@ -837,10 +856,19 @@ const DEEPCHAT_ROUTE_CATALOG_PART_5 = {
[memoryRejectPersonaDraftRoute.name]: memoryRejectPersonaDraftRoute,
[memorySetPersonaAnchorRoute.name]: memorySetPersonaAnchorRoute,
[skillsListMetadataRoute.name]: skillsListMetadataRoute,
+ [skillsListCatalogRoute.name]: skillsListCatalogRoute,
[skillsGetDirectoryRoute.name]: skillsGetDirectoryRoute,
[skillsInstallFromFolderRoute.name]: skillsInstallFromFolderRoute,
[skillsInstallFromZipRoute.name]: skillsInstallFromZipRoute,
[skillsInstallFromUrlRoute.name]: skillsInstallFromUrlRoute,
+ [skillsScanGitRepoRoute.name]: skillsScanGitRepoRoute,
+ [skillsInstallFromGitRoute.name]: skillsInstallFromGitRoute,
+ [skillsGetSyncConfigRoute.name]: skillsGetSyncConfigRoute,
+ [skillsSetSyncDirectoryRoute.name]: skillsSetSyncDirectoryRoute,
+ [skillsPreviewSyncDirectoryExportRoute.name]: skillsPreviewSyncDirectoryExportRoute,
+ [skillsExecuteSyncDirectoryExportRoute.name]: skillsExecuteSyncDirectoryExportRoute,
+ [skillsPreviewSyncDirectoryImportRoute.name]: skillsPreviewSyncDirectoryImportRoute,
+ [skillsExecuteSyncDirectoryImportRoute.name]: skillsExecuteSyncDirectoryImportRoute,
[skillsUninstallRoute.name]: skillsUninstallRoute,
[skillsReadFileRoute.name]: skillsReadFileRoute,
[skillsUpdateFileRoute.name]: skillsUpdateFileRoute,
@@ -852,10 +880,20 @@ const DEEPCHAT_ROUTE_CATALOG_PART_5 = {
[skillsListScriptsRoute.name]: skillsListScriptsRoute,
[skillsGetActiveRoute.name]: skillsGetActiveRoute,
[skillsSetActiveRoute.name]: skillsSetActiveRoute,
+ [skillsSetDisabledRoute.name]: skillsSetDisabledRoute,
[skillSyncScanExternalToolsRoute.name]: skillSyncScanExternalToolsRoute,
[skillSyncGetNewDiscoveriesRoute.name]: skillSyncGetNewDiscoveriesRoute,
[skillSyncAcknowledgeDiscoveriesRoute.name]: skillSyncAcknowledgeDiscoveriesRoute,
[skillSyncGetRegisteredToolsRoute.name]: skillSyncGetRegisteredToolsRoute,
+ [skillSyncScanAgentsRoute.name]: skillSyncScanAgentsRoute,
+ [skillSyncGetAgentDetailRoute.name]: skillSyncGetAgentDetailRoute,
+ [skillSyncGetAgentSkillDetailRoute.name]: skillSyncGetAgentSkillDetailRoute,
+ [skillSyncPreviewAdoptAgentSkillRoute.name]: skillSyncPreviewAdoptAgentSkillRoute,
+ [skillSyncExecuteAdoptAgentSkillRoute.name]: skillSyncExecuteAdoptAgentSkillRoute,
+ [skillSyncPreviewLinkDeepChatSkillsRoute.name]: skillSyncPreviewLinkDeepChatSkillsRoute,
+ [skillSyncExecuteLinkDeepChatSkillsRoute.name]: skillSyncExecuteLinkDeepChatSkillsRoute,
+ [skillSyncRepairAgentSkillLinkRoute.name]: skillSyncRepairAgentSkillLinkRoute,
+ [skillSyncRemoveAgentSkillLinkRoute.name]: skillSyncRemoveAgentSkillLinkRoute,
[skillSyncPreviewImportRoute.name]: skillSyncPreviewImportRoute,
[skillSyncExecuteImportRoute.name]: skillSyncExecuteImportRoute,
[skillSyncPreviewExportRoute.name]: skillSyncPreviewExportRoute,
diff --git a/src/shared/contracts/routes/skillSync.routes.ts b/src/shared/contracts/routes/skillSync.routes.ts
index 713eff12cc..616e184f58 100644
--- a/src/shared/contracts/routes/skillSync.routes.ts
+++ b/src/shared/contracts/routes/skillSync.routes.ts
@@ -1,5 +1,15 @@
import { z } from 'zod'
import { defineRouteContract } from '../common'
+import type {
+ AdoptAgentSkillPreview,
+ AdoptAgentSkillResult,
+ LinkDeepChatSkillResult,
+ LinkDeepChatSkillsPreview,
+ LinkDeepChatSkillsResult,
+ InstalledSkillAgent,
+ InstalledSkillAgentDetail,
+ SkillDetail
+} from '../../types/skillSync'
import {
SkillSyncConflictStrategySchema,
SkillSyncExportPreviewSchema,
@@ -14,6 +24,27 @@ const ToolIdSchema = z.string().min(1)
const SkillNameSchema = z.string().min(1)
const ConflictStrategiesSchema = z.record(z.string(), SkillSyncConflictStrategySchema)
const ExportOptionsSchema = z.record(z.string(), z.unknown()).optional()
+const InstalledSkillAgentSchema = z.custom()
+const InstalledSkillAgentDetailSchema = z.custom()
+const SkillDetailSchema = z.custom()
+const AdoptAgentSkillPreviewSchema = z.custom()
+const AdoptAgentSkillResultSchema = z.custom()
+const AdoptAgentSkillInputSchema = z.object({
+ agentId: ToolIdSchema,
+ skillName: SkillNameSchema,
+ targetName: SkillNameSchema.optional()
+})
+const LinkDeepChatSkillsPreviewSchema = z.custom()
+const LinkDeepChatSkillsResultSchema = z.custom()
+const LinkDeepChatSkillResultSchema = z.custom()
+const LinkDeepChatSkillsInputSchema = z.object({
+ agentId: ToolIdSchema,
+ skillNames: z.array(SkillNameSchema)
+})
+const AgentSkillLinkInputSchema = z.object({
+ agentId: ToolIdSchema,
+ skillName: SkillNameSchema
+})
export const skillSyncScanExternalToolsRoute = defineRouteContract({
name: 'skillSync.scanExternalTools',
@@ -47,6 +78,83 @@ export const skillSyncGetRegisteredToolsRoute = defineRouteContract({
})
})
+export const skillSyncScanAgentsRoute = defineRouteContract({
+ name: 'skillSync.scanAgents',
+ input: z.object({}).default({}),
+ output: z.object({
+ agents: z.array(InstalledSkillAgentSchema)
+ })
+})
+
+export const skillSyncGetAgentDetailRoute = defineRouteContract({
+ name: 'skillSync.getAgentDetail',
+ input: z.object({
+ agentId: ToolIdSchema
+ }),
+ output: z.object({
+ agent: InstalledSkillAgentDetailSchema
+ })
+})
+
+export const skillSyncGetAgentSkillDetailRoute = defineRouteContract({
+ name: 'skillSync.getAgentSkillDetail',
+ input: z.object({
+ agentId: ToolIdSchema,
+ skillName: SkillNameSchema
+ }),
+ output: z.object({
+ detail: SkillDetailSchema
+ })
+})
+
+export const skillSyncPreviewAdoptAgentSkillRoute = defineRouteContract({
+ name: 'skillSync.previewAdoptAgentSkill',
+ input: AdoptAgentSkillInputSchema,
+ output: z.object({
+ preview: AdoptAgentSkillPreviewSchema
+ })
+})
+
+export const skillSyncExecuteAdoptAgentSkillRoute = defineRouteContract({
+ name: 'skillSync.executeAdoptAgentSkill',
+ input: AdoptAgentSkillInputSchema,
+ output: z.object({
+ result: AdoptAgentSkillResultSchema
+ })
+})
+
+export const skillSyncPreviewLinkDeepChatSkillsRoute = defineRouteContract({
+ name: 'skillSync.previewLinkDeepChatSkills',
+ input: LinkDeepChatSkillsInputSchema,
+ output: z.object({
+ preview: LinkDeepChatSkillsPreviewSchema
+ })
+})
+
+export const skillSyncExecuteLinkDeepChatSkillsRoute = defineRouteContract({
+ name: 'skillSync.executeLinkDeepChatSkills',
+ input: LinkDeepChatSkillsInputSchema,
+ output: z.object({
+ result: LinkDeepChatSkillsResultSchema
+ })
+})
+
+export const skillSyncRepairAgentSkillLinkRoute = defineRouteContract({
+ name: 'skillSync.repairAgentSkillLink',
+ input: AgentSkillLinkInputSchema,
+ output: z.object({
+ result: LinkDeepChatSkillResultSchema
+ })
+})
+
+export const skillSyncRemoveAgentSkillLinkRoute = defineRouteContract({
+ name: 'skillSync.removeAgentSkillLink',
+ input: AgentSkillLinkInputSchema,
+ output: z.object({
+ result: LinkDeepChatSkillResultSchema
+ })
+})
+
export const skillSyncPreviewImportRoute = defineRouteContract({
name: 'skillSync.previewImport',
input: z.object({
diff --git a/src/shared/contracts/routes/skills.routes.ts b/src/shared/contracts/routes/skills.routes.ts
index 57a4695938..5ce7cc87d2 100644
--- a/src/shared/contracts/routes/skills.routes.ts
+++ b/src/shared/contracts/routes/skills.routes.ts
@@ -1,17 +1,29 @@
import { z } from 'zod'
import type {
+ GitSkillRepoScanResult,
SkillExtensionConfig,
SkillFolderNode,
+ SkillSyncDirectoryExportPreview,
+ SkillSyncDirectoryImportPreview,
+ SkillSyncDirectoryResult,
SkillInstallOptions,
SkillInstallResult,
SkillMetadata,
SkillScriptDescriptor
} from '@shared/types/skill'
+import type { SkillSyncDirectoryConfig, UnifiedSkillItem } from '@shared/types/skillManagement'
import { EntityIdSchema, defineRouteContract } from '../common'
const SkillMetadataSchema = z.custom()
+const UnifiedSkillItemSchema = z.custom()
const SkillInstallOptionsSchema = z.custom().optional()
const SkillInstallResultSchema = z.custom()
+const SkillInstallConflictStrategySchema = z.enum(['rename', 'overwrite', 'skip']).optional()
+const GitSkillRepoScanResultSchema = z.custom()
+const SkillSyncDirectoryConfigSchema = z.custom().nullable()
+const SkillSyncDirectoryExportPreviewSchema = z.custom()
+const SkillSyncDirectoryImportPreviewSchema = z.custom()
+const SkillSyncDirectoryResultSchema = z.custom()
const SkillFolderNodeSchema = z.custom()
const SkillExtensionConfigSchema = z.custom()
const SkillScriptDescriptorSchema = z.custom()
@@ -24,6 +36,25 @@ export const skillsListMetadataRoute = defineRouteContract({
})
})
+export const skillsListCatalogRoute = defineRouteContract({
+ name: 'skills.listCatalog',
+ input: z.object({}),
+ output: z.object({
+ skills: z.array(UnifiedSkillItemSchema)
+ })
+})
+
+export const skillsSetDisabledRoute = defineRouteContract({
+ name: 'skills.setDisabled',
+ input: z.object({
+ name: z.string().min(1),
+ disabled: z.boolean()
+ }),
+ output: z.object({
+ saved: z.literal(true)
+ })
+})
+
export const skillsGetDirectoryRoute = defineRouteContract({
name: 'skills.getDirectory',
input: z.object({}),
@@ -65,6 +96,87 @@ export const skillsInstallFromUrlRoute = defineRouteContract({
})
})
+export const skillsScanGitRepoRoute = defineRouteContract({
+ name: 'skills.scanGitRepo',
+ input: z.object({
+ repoUrl: z.string().min(1)
+ }),
+ output: z.object({
+ result: GitSkillRepoScanResultSchema
+ })
+})
+
+export const skillsInstallFromGitRoute = defineRouteContract({
+ name: 'skills.installFromGit',
+ input: z.object({
+ repoUrl: z.string().min(1),
+ skillNames: z.array(z.string().min(1)),
+ strategy: SkillInstallConflictStrategySchema
+ }),
+ output: z.object({
+ results: z.array(SkillInstallResultSchema)
+ })
+})
+
+export const skillsGetSyncConfigRoute = defineRouteContract({
+ name: 'skills.getSyncConfig',
+ input: z.object({}),
+ output: z.object({
+ config: SkillSyncDirectoryConfigSchema
+ })
+})
+
+export const skillsSetSyncDirectoryRoute = defineRouteContract({
+ name: 'skills.setSyncDirectory',
+ input: z.object({
+ skillsDirectory: z.string().min(1)
+ }),
+ output: z.object({
+ config: z.custom()
+ })
+})
+
+export const skillsPreviewSyncDirectoryExportRoute = defineRouteContract({
+ name: 'skills.previewSyncDirectoryExport',
+ input: z.object({
+ skillNames: z.array(z.string().min(1)),
+ includeDisabled: z.boolean().optional()
+ }),
+ output: z.object({
+ preview: SkillSyncDirectoryExportPreviewSchema
+ })
+})
+
+export const skillsExecuteSyncDirectoryExportRoute = defineRouteContract({
+ name: 'skills.executeSyncDirectoryExport',
+ input: z.object({
+ skillNames: z.array(z.string().min(1)),
+ includeDisabled: z.boolean().optional()
+ }),
+ output: z.object({
+ result: SkillSyncDirectoryResultSchema
+ })
+})
+
+export const skillsPreviewSyncDirectoryImportRoute = defineRouteContract({
+ name: 'skills.previewSyncDirectoryImport',
+ input: z.object({}),
+ output: z.object({
+ preview: SkillSyncDirectoryImportPreviewSchema
+ })
+})
+
+export const skillsExecuteSyncDirectoryImportRoute = defineRouteContract({
+ name: 'skills.executeSyncDirectoryImport',
+ input: z.object({
+ skillNames: z.array(z.string().min(1)),
+ strategy: SkillInstallConflictStrategySchema
+ }),
+ output: z.object({
+ result: SkillSyncDirectoryResultSchema
+ })
+})
+
export const skillsUninstallRoute = defineRouteContract({
name: 'skills.uninstall',
input: z.object({
diff --git a/src/shared/types/skill.ts b/src/shared/types/skill.ts
index 2650593995..a0a3d9d01d 100644
--- a/src/shared/types/skill.ts
+++ b/src/shared/types/skill.ts
@@ -6,6 +6,12 @@
* (metadata first, full content on activation) and hot-reloading.
*/
+import type {
+ SkillManagementState,
+ SkillSyncDirectoryConfig,
+ UnifiedSkillItem
+} from './skillManagement'
+
/**
* Skill metadata extracted from SKILL.md frontmatter.
* Always kept in memory for quick access and semantic matching.
@@ -91,6 +97,83 @@ export interface SkillInstallOptions {
overwrite?: boolean
}
+export type SkillInstallConflictStrategy = 'rename' | 'overwrite' | 'skip'
+
+export type GitSkillRepoFormat = 'single-skill' | 'multi-skill'
+
+export interface GitSkillRepoScanItem {
+ name: string
+ description: string
+ relativePath: string
+ conflict: boolean
+ valid: boolean
+ error?: string
+}
+
+export interface GitSkillRepoScanResult {
+ repoUrl: string
+ repoFormat: GitSkillRepoFormat
+ skills: GitSkillRepoScanItem[]
+}
+
+export interface GitSkillInstallInput {
+ repoUrl: string
+ skillNames: string[]
+ strategy?: SkillInstallConflictStrategy
+}
+
+export type SyncDirectorySkillState = 'new' | 'same' | 'modified' | 'conflict' | 'invalid'
+
+export interface SkillSyncDirectoryPreviewItem {
+ name: string
+ state: SyncDirectorySkillState
+ sourcePath: string
+ targetPath: string
+ error?: string
+}
+
+export interface SkillSyncDirectoryExportInput {
+ skillNames: string[]
+ includeDisabled?: boolean
+}
+
+export interface SkillSyncDirectoryImportInput {
+ skillNames: string[]
+ strategy?: SkillInstallConflictStrategy
+}
+
+export interface SkillSyncDirectoryExportPreview {
+ skillsDirectory: string
+ items: SkillSyncDirectoryPreviewItem[]
+}
+
+export interface SkillSyncDirectoryImportPreview {
+ skillsDirectory: string
+ items: SkillSyncDirectoryPreviewItem[]
+}
+
+export interface SkillSyncDirectoryResult {
+ success: boolean
+ exported?: number
+ imported?: number
+ skipped: number
+ failed: Array<{ skillName: string; reason: string }>
+}
+
+export interface SkillAdoptionRegistration {
+ name: string
+ canonicalPath: string
+ agentId: string
+ agentPath: string
+ originalPath: string
+}
+
+export interface SkillAgentLinkRegistration {
+ skillName: string
+ agentId: string
+ agentPath: string
+}
+
/**
* Folder tree node for displaying skill directory structure
*/
@@ -185,7 +268,10 @@ export interface ISkillPresenter {
getSkillsDir(): Promise
discoverSkills(): Promise
getMetadataList(): Promise
+ getUnifiedSkillCatalog(): Promise
getMetadataPrompt(): Promise
+ getSkillManagementState(): Promise
+ setSkillDeepChatDisabled(name: string, disabled: boolean): Promise
// Content loading
loadSkillContent(name: string): Promise
@@ -206,6 +292,23 @@ export interface ISkillPresenter {
installFromFolder(folderPath: string, options?: SkillInstallOptions): Promise
installFromZip(zipPath: string, options?: SkillInstallOptions): Promise
installFromUrl(url: string, options?: SkillInstallOptions): Promise
+ scanGitSkillRepo(repoUrl: string): Promise
+ installSkillsFromGit(input: GitSkillInstallInput): Promise
+ getSkillsSyncConfig(): Promise
+ setSkillsSyncDirectory(input: { skillsDirectory: string }): Promise
+ previewSyncDirectoryExport(
+ input: SkillSyncDirectoryExportInput
+ ): Promise
+ executeSyncDirectoryExport(
+ input: SkillSyncDirectoryExportInput
+ ): Promise
+ previewSyncDirectoryImport(): Promise
+ executeSyncDirectoryImport(
+ input: SkillSyncDirectoryImportInput
+ ): Promise
+ registerAdoptedSkill(input: SkillAdoptionRegistration): Promise
+ registerAgentSkillLink(input: SkillAgentLinkRegistration): Promise
+ removeAgentSkillLink(input: { skillName: string; agentId: string }): Promise
uninstallSkill(name: string): Promise
registerPluginSkill?(input: {
ownerPluginId: string
diff --git a/src/shared/types/skillManagement.ts b/src/shared/types/skillManagement.ts
new file mode 100644
index 0000000000..68816c49ff
--- /dev/null
+++ b/src/shared/types/skillManagement.ts
@@ -0,0 +1,73 @@
+import type { SkillExtensionConfig } from './skill'
+
+export type SkillSourceType =
+ | 'builtin'
+ | 'created'
+ | 'folder-install'
+ | 'zip-install'
+ | 'url-install'
+ | 'git-install'
+ | 'adopted'
+ | 'imported'
+
+export type SkillRepoFormat = 'single-skill' | 'multi-skill'
+
+export interface SkillSource {
+ type: SkillSourceType
+ repoUrl?: string
+ repoFormat?: SkillRepoFormat
+ agentId?: string
+ originalPath?: string
+ importedFrom?: string
+ installedAt?: string
+ importedAt?: string
+ adoptedAt?: string
+}
+
+export interface AgentLinkInfo {
+ path: string
+ state: 'linked' | 'missing' | 'broken' | 'conflict' | 'permission-denied'
+ createdByDeepChat: boolean
+ linkedAt?: string
+}
+
+export interface SkillManagementItem {
+ name: string
+ canonicalPath: string
+ deepchat: {
+ disabled: boolean
+ }
+ extension: SkillExtensionConfig
+ source: SkillSource
+ agentLinks?: Record
+}
+
+export interface SkillSyncDirectoryConfig {
+ skillsDirectory: string
+ layout: 'multi-skill-repo'
+ lastExportAt?: string | null
+ lastImportAt?: string | null
+}
+
+export interface SkillManagementState {
+ version: 1
+ skills: Record
+ sync?: SkillSyncDirectoryConfig
+}
+
+export interface UnifiedSkillItem {
+ name: string
+ description: string
+ path: string
+ skillRoot: string
+ category?: string | null
+ platforms?: string[]
+ metadata?: Record
+ allowedTools?: string[]
+ ownerPluginId?: string
+ canonicalPath: string
+ sourceType: SkillSourceType
+ deepchatDisabled: boolean
+ agentLinks: Record
+ mutable: boolean
+}
diff --git a/src/shared/types/skillSync.ts b/src/shared/types/skillSync.ts
index e71dcbc60f..ab629cd5d6 100644
--- a/src/shared/types/skillSync.ts
+++ b/src/shared/types/skillSync.ts
@@ -164,6 +164,140 @@ export interface ScanResult {
error?: string
}
+export type AgentSkillOwner = 'deepchat' | 'agent' | 'external-link' | 'broken-link' | 'unknown'
+
+export type AgentSkillStatus =
+ | 'linked'
+ | 'agent-owned'
+ | 'linked-out'
+ | 'broken-link'
+ | 'conflict'
+ | 'empty'
+
+export type AgentSkillAction = 'adopt' | 'resolve-conflict' | 'repair-link' | 'remove-link' | 'open'
+
+export interface AgentSkillLinkInfo {
+ isSymlink: boolean
+ targetPath?: string
+ targetExists?: boolean
+ targetInsideDeepChat?: boolean
+ createdByDeepChat?: boolean
+}
+
+export interface AgentSkillDeepChatInfo {
+ exists: boolean
+ path?: string
+ disabled?: boolean
+ sameContent?: boolean
+}
+
+export interface AgentSkillItem {
+ name: string
+ description?: string
+ path: string
+ owner: AgentSkillOwner
+ status: AgentSkillStatus
+ action?: AgentSkillAction
+ link?: AgentSkillLinkInfo
+ deepchat?: AgentSkillDeepChatInfo
+}
+
+export interface SkillDetail {
+ name: string
+ description: string
+ sourcePath: string
+ markdown: string
+ mutable: boolean
+}
+
+export interface InstalledSkillAgent {
+ id: string
+ name: string
+ skillsDir: string
+ isCustom: boolean
+ supportsLinkManagement: boolean
+ skillsCount: number
+ linkedCount: number
+ agentOwnedCount: number
+ conflictCount: number
+ brokenLinkCount: number
+ status: 'ready' | 'detected-no-skills-dir' | 'permission-denied'
+}
+
+export interface InstalledSkillAgentDetail extends InstalledSkillAgent {
+ skills: AgentSkillItem[]
+}
+
+export interface AdoptAgentSkillInput {
+ agentId: string
+ skillName: string
+ targetName?: string
+}
+
+export interface AdoptAgentSkillPreview {
+ agentId: string
+ agentName: string
+ skillName: string
+ targetName: string
+ sourcePath: string
+ agentPath: string
+ targetPath: string
+ backupRoot: string
+ conflict: boolean
+ warnings: string[]
+}
+
+export interface AdoptAgentSkillResult {
+ success: boolean
+ skillName?: string
+ targetPath?: string
+ agentPath?: string
+ backupPath?: string
+ error?: string
+}
+
+export interface LinkDeepChatSkillsInput {
+ agentId: string
+ skillNames: string[]
+}
+
+export type LinkDeepChatSkillPreviewStatus = 'ready' | 'already-linked' | 'conflict' | 'missing'
+
+export interface LinkDeepChatSkillPreviewItem {
+ skillName: string
+ sourcePath?: string
+ targetPath: string
+ status: LinkDeepChatSkillPreviewStatus
+ message?: string
+}
+
+export interface LinkDeepChatSkillsPreview {
+ agentId: string
+ agentName: string
+ skillsDir: string
+ items: LinkDeepChatSkillPreviewItem[]
+}
+
+export interface LinkDeepChatSkillResult {
+ success: boolean
+ skillName?: string
+ agentPath?: string
+ targetPath?: string
+ error?: string
+}
+
+export interface LinkDeepChatSkillsResult {
+ success: boolean
+ linked: number
+ skipped: number
+ failed: Array<{ skillName: string; reason: string }>
+}
+
+export interface AgentSkillLinkInput {
+ agentId: string
+ skillName: string
+}
+
/**
* Conflict handling strategy
*/
@@ -444,6 +578,24 @@ export interface ISkillSyncPresenter {
*/
getRegisteredTools(): ExternalToolConfig[]
+ scanSkillAgents(): Promise
+
+ scanSkillAgent(input: { agentId: string }): Promise
+
+ getAgentSkillDetail(input: { agentId: string; skillName: string }): Promise
+
+ previewAdoptAgentSkill(input: AdoptAgentSkillInput): Promise
+
+ executeAdoptAgentSkill(input: AdoptAgentSkillInput): Promise
+
+ previewLinkDeepChatSkills(input: LinkDeepChatSkillsInput): Promise
+
+ executeLinkDeepChatSkills(input: LinkDeepChatSkillsInput): Promise
+
+ repairAgentSkillLink(input: AgentSkillLinkInput): Promise