From b8eafcdb67ac083c451c6407192ebe380ff4ffea Mon Sep 17 00:00:00 2001 From: xiaomo Date: Thu, 16 Jul 2026 17:20:03 +0800 Subject: [PATCH 1/6] refactor(ui): align UI with shadcn and VueUse (#1985) * refactor(ui): align UI with shadcn and VueUse Prefer shadcn Spinner/Skeleton/Empty/Dialog/Select and VueUse helpers across settings and renderer loading surfaces. * fix(i18n): localize model capability tooltips Translate settings.modelConfigItem.capability for non-English locales. * fix(ui): address PR review clipboard and i18n issues Restore writeText clipboard paths, guard memory search stale runs, and localize ArtifactThinking loading text. --------- Co-authored-by: zhangmo8 --- AGENTS.md | 7 ++ .../shadcn-vueuse-alignment/plan.md | 42 +++++++ .../shadcn-vueuse-alignment/spec.md | 52 +++++++++ .../shadcn-vueuse-alignment/tasks.md | 34 ++++++ package.json | 4 +- src/renderer/settings/App.vue | 18 +-- .../settings/components/AboutUsSettings.vue | 12 +- .../settings/components/AcpDebugDialog.vue | 69 ++++++----- .../settings/components/AcpSettings.vue | 16 +-- .../components/BuiltinKnowledgeSettings.vue | 3 +- .../settings/components/CronJobsSettings.vue | 7 +- .../settings/components/DashboardSettings.vue | 8 +- .../settings/components/DataSettings.vue | 36 ++---- .../components/EnvironmentsSettings.vue | 8 +- .../components/GitHubCopilotOAuth.vue | 13 +-- .../settings/components/GrokOAuth.vue | 11 +- .../settings/components/KnowledgeFile.vue | 27 ++--- .../settings/components/KnowledgeFileItem.vue | 3 +- .../settings/components/McpBuiltinMarket.vue | 29 ++--- .../settings/components/McpSettings.vue | 18 +-- .../settings/components/MemoryEmptyState.vue | 67 ++++++----- .../settings/components/MemoryListView.vue | 36 +++--- .../components/ModelProviderSettings.vue | 25 ++-- .../settings/components/ModelScopeMcpSync.vue | 5 +- .../components/NotificationsHooksSettings.vue | 12 +- .../settings/components/OpenAICodexOAuth.vue | 19 +-- .../settings/components/ProviderApiConfig.vue | 12 +- .../components/ProviderConfigImportDialog.vue | 5 +- .../settings/components/ProviderModelList.vue | 48 +++----- .../components/ProviderModelManager.vue | 12 +- .../components/ProviderRateLimitConfig.vue | 22 ++-- .../settings/components/RemoteSettings.vue | 63 +++++----- .../settings/components/ShortcutSettings.vue | 6 +- .../components/skills/AdoptSkillDialog.vue | 10 +- .../skills/InstallFromGitDialog.vue | 15 +-- .../skills/InstallSkillToAgentDialog.vue | 28 +++-- .../components/skills/SkillAgentsTab.vue | 32 ++--- .../components/skills/SkillDetailDialog.vue | 3 +- .../components/skills/SkillFolderTree.vue | 6 +- .../skills/SkillImportExportTab.vue | 36 ++---- .../components/skills/SkillInstallDialog.vue | 19 ++- .../skills/SkillSyncDialog/ExportWizard.vue | 15 +-- .../skills/SkillSyncDialog/ImportWizard.vue | 11 +- .../skills/SkillSyncDialog/ToolSelector.vue | 20 ++-- .../components/skills/SkillsSettings.vue | 43 ++++--- .../components/skills/SyncStatusCard.vue | 8 +- .../components/skills/SyncStatusSection.vue | 35 +++--- src/renderer/src/App.vue | 20 ++-- .../src/components/ScrollablePopover.vue | 34 ------ src/renderer/src/components/WindowSideBar.vue | 52 ++++++--- .../components/agent/AgentTransferDialog.vue | 63 ++++++---- .../components/artifacts/ArtifactPreview.vue | 7 +- .../components/artifacts/ArtifactThinking.vue | 12 +- .../src/components/artifacts/SvgArtifact.vue | 3 +- .../components/artifacts/ToolCallPreview.vue | 6 +- .../components/chat-input/SkillsIndicator.vue | 5 +- .../src/components/chat/ChatInputToolbar.vue | 15 +-- .../components/chat/ChatSessionSkeleton.vue | 38 +++--- .../src/components/chat/ChatStatusBar.vue | 14 +-- .../mcp-config/components/McpJsonViewer.vue | 6 +- .../mcp-config/components/McpPromptPanel.vue | 11 +- .../components/McpResourceViewer.vue | 11 +- .../mcp-config/components/McpServerCard.vue | 7 +- .../mcp-config/components/McpServers.vue | 23 +--- .../mcp-config/components/McpToolPanel.vue | 8 +- .../src/components/mcp/McpSamplingDialog.vue | 9 +- .../components/message/MessageBlockAudio.vue | 5 +- .../components/message/MessageBlockImage.vue | 5 +- .../components/message/MessageBlockVideo.vue | 5 +- .../message/MessageItemPlaceholder.vue | 14 ++- .../src/components/message/MessageToolbar.vue | 5 +- .../src/components/popup/TranslatePopup.vue | 11 +- .../components/settings/ModelCheckDialog.vue | 10 +- .../components/settings/ModelConfigItem.vue | 110 ++++++++++-------- .../sidepanel/viewer/WorkspaceCodePane.vue | 42 +------ .../components/spotlight/SpotlightOverlay.vue | 8 +- .../src/composables/useArtifactExport.ts | 4 +- src/renderer/src/i18n/da-DK/artifacts.json | 1 + src/renderer/src/i18n/da-DK/settings.json | 8 +- src/renderer/src/i18n/de-DE/artifacts.json | 1 + src/renderer/src/i18n/de-DE/settings.json | 8 +- src/renderer/src/i18n/en-US/artifacts.json | 1 + src/renderer/src/i18n/en-US/settings.json | 8 +- src/renderer/src/i18n/es-ES/artifacts.json | 1 + src/renderer/src/i18n/es-ES/settings.json | 8 +- src/renderer/src/i18n/fa-IR/artifacts.json | 4 +- src/renderer/src/i18n/fa-IR/settings.json | 8 +- src/renderer/src/i18n/fr-FR/artifacts.json | 4 +- src/renderer/src/i18n/fr-FR/settings.json | 8 +- src/renderer/src/i18n/he-IL/artifacts.json | 1 + src/renderer/src/i18n/he-IL/settings.json | 8 +- src/renderer/src/i18n/id-ID/artifacts.json | 1 + src/renderer/src/i18n/id-ID/settings.json | 8 +- src/renderer/src/i18n/it-IT/artifacts.json | 1 + src/renderer/src/i18n/it-IT/settings.json | 8 +- src/renderer/src/i18n/ja-JP/artifacts.json | 4 +- src/renderer/src/i18n/ja-JP/settings.json | 8 +- src/renderer/src/i18n/ko-KR/artifacts.json | 1 + src/renderer/src/i18n/ko-KR/settings.json | 8 +- src/renderer/src/i18n/ms-MY/artifacts.json | 1 + src/renderer/src/i18n/ms-MY/settings.json | 8 +- src/renderer/src/i18n/pl-PL/artifacts.json | 1 + src/renderer/src/i18n/pl-PL/settings.json | 8 +- src/renderer/src/i18n/pt-BR/artifacts.json | 1 + src/renderer/src/i18n/pt-BR/settings.json | 8 +- src/renderer/src/i18n/ru-RU/artifacts.json | 4 +- src/renderer/src/i18n/ru-RU/settings.json | 8 +- src/renderer/src/i18n/tr-TR/artifacts.json | 1 + src/renderer/src/i18n/tr-TR/settings.json | 8 +- src/renderer/src/i18n/vi-VN/artifacts.json | 1 + src/renderer/src/i18n/vi-VN/settings.json | 8 +- src/renderer/src/i18n/zh-CN/artifacts.json | 1 + src/renderer/src/i18n/zh-CN/settings.json | 8 +- src/renderer/src/i18n/zh-HK/artifacts.json | 1 + src/renderer/src/i18n/zh-HK/settings.json | 8 +- src/renderer/src/i18n/zh-TW/artifacts.json | 1 + src/renderer/src/i18n/zh-TW/settings.json | 8 +- .../src/pages/plugins/PluginsCatalogPage.vue | 4 +- src/renderer/src/stores/providerStore.ts | 31 +++-- src/renderer/src/stores/ui/sidepanel.ts | 12 +- src/types/i18n.d.ts | 27 +++++ 121 files changed, 1058 insertions(+), 801 deletions(-) create mode 100644 docs/architecture/shadcn-vueuse-alignment/plan.md create mode 100644 docs/architecture/shadcn-vueuse-alignment/spec.md create mode 100644 docs/architecture/shadcn-vueuse-alignment/tasks.md delete mode 100644 src/renderer/src/components/ScrollablePopover.vue diff --git a/AGENTS.md b/AGENTS.md index 6516fc312c..b567bf0434 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,13 @@ - OxLint for JS/TS; the tracked `commit-msg` hook runs `commitlint`. - Names: Vue components PascalCase (`ChatInput.vue`); variables/functions `camelCase`; types/classes `PascalCase`; constants `SCREAMING_SNAKE_CASE`. +## UI primitives: shadcn-vue + VueUse first +- For renderer UI changes, when product/interaction/performance needs are met, **prefer shadcn-vue** under `src/shadcn/components/ui` via `@shadcn/components/ui/*` (Button, Dialog, Select, Switch, Skeleton, Spinner, Empty, Alert, Badge, Field, etc.). Do not hand-roll equivalent spinners, pulse skeletons, empty states, or form controls. +- Prefer **VueUse** (`@vueuse/core`) for mechanical browser utilities: `useEventListener`, `useDebounceFn` / `refDebounced`, `useWindowSize`, `useResizeObserver`, `useIntervalFn`, `useStorage`, etc. +- Missing shadcn pieces: `pnpm dlx shadcn-vue@latest docs ` then `add` (see `components.json` and `pnpm run update-shadcn`). Never invent registry flags; do not `--overwrite` without explicit approval. +- Custom UI is allowed only when shadcn cannot cover the semantic (virtual-list measurement, spotlight/onboarding overlays, domain message/artifact chrome, native truncate `title=`). Document the exception in the PR. +- Details: [docs/architecture/shadcn-vueuse-alignment/spec.md](docs/architecture/shadcn-vueuse-alignment/spec.md). + ## Testing Guidelines - Framework: Vitest (+ jsdom) and Vue Test Utils. - Location mirrors source under `test/main/**` and `test/renderer/**`. diff --git a/docs/architecture/shadcn-vueuse-alignment/plan.md b/docs/architecture/shadcn-vueuse-alignment/plan.md new file mode 100644 index 0000000000..2698dd8551 --- /dev/null +++ b/docs/architecture/shadcn-vueuse-alignment/plan.md @@ -0,0 +1,42 @@ +# Plan: shadcn-vue + VueUse Alignment + +## Approach + +Incremental, high-ROI waves. Wave 1 is independent and mergeable. + +### Wave 1 + +1. Document standing rule in this SDD + `Agents.md`. +2. Skeleton: chat placeholders, session skeleton, sidebar list loading, key settings page skeletons. +3. Spinner: model check dialog CSS spinner, media blocks, MCP/artifact/popup loaders, button loading patterns. +4. Empty: Memory empty state, sidebar empty session list. +5. VueUse: `useWindowSize` in sidepanel store, `refDebounced` in provider model search, low-risk `useEventListener` / `useIntervalFn`. +6. Remove dead `ScrollablePopover.vue`. + +### Wave 2+ + +Form primitives (`AgentTransferDialog`), Tooltip migration, remaining settings skeletons/empties, clipboard, resize observers — see repository session plan if present. + +## Interfaces / reuse + +| Need | Use | +|------|-----| +| Loading icon | `@shadcn/components/ui/spinner` | +| Placeholder blocks | `@shadcn/components/ui/skeleton` | +| Empty states | `@shadcn/components/ui/empty` | +| Button loading | `Button` + `Spinner` + `disabled` + `data-icon` | +| Window size | `useWindowSize` from `@vueuse/core` | +| Debounced ref | `refDebounced` from `@vueuse/core` | +| Event listeners | `useEventListener` from `@vueuse/core` | +| Polling | `useIntervalFn` from `@vueuse/core` | + +## Compatibility + +- Preserve `data-testid` attributes. +- Keep debounce delays and clamp logic equivalent. +- Do not change IPC or store public APIs except internal listener implementation. + +## Test strategy + +- Lint / format / i18n / typecheck on changed code. +- Manual smoke: chat skeleton, model check dialog, memory empty, sidebar empty/loading, sidepanel resize, model search debounce. diff --git a/docs/architecture/shadcn-vueuse-alignment/spec.md b/docs/architecture/shadcn-vueuse-alignment/spec.md new file mode 100644 index 0000000000..57860ee69f --- /dev/null +++ b/docs/architecture/shadcn-vueuse-alignment/spec.md @@ -0,0 +1,52 @@ +# Spec: shadcn-vue + VueUse UI Alignment + +## Goal + +Align DeepChat renderer UI with the existing shadcn-vue design system (`src/shadcn/components/ui`) and VueUse (`@vueuse/core`), so feedback primitives (Spinner / Skeleton / Empty), form controls, and mechanical browser utilities stop being reimplemented ad hoc. + +## Standing rule (non-negotiable) + +**For all future UI-related changes, when product, interaction, and performance requirements are met, prefer shadcn-vue components over custom markup. Prefer VueUse for mechanical browser/reactivity utilities.** + +Decision order: + +1. Does an installed shadcn component cover the need? → compose it. +2. If not installed → `pnpm dlx shadcn-vue@latest docs/search/add`, then compose. +3. If still not a fit (special interaction, performance-critical path, or non-modal overlay) → custom implementation, and document why in the PR. + +Exceptions that may stay custom: + +- Virtual list measurement and chat scroll windowing +- Spotlight / guided onboarding overlays +- Agent progress and tool-interaction floating layers +- Native truncate tooltips via `title=` +- Domain-specific message, artifact, and editor surfaces that only wrap primitives + +## Acceptance criteria + +- Wave 1 touchpoints use `Spinner`, `Skeleton`, and `Empty` instead of hand-rolled CSS loaders / pulse blocks / empty markup where listed in `tasks.md`. +- VueUse replacements preserve existing behavior (resize clamp, debounce timing, listener cleanup). +- No shadcn component overwrite without explicit approval. +- `Agents.md` documents the standing rule. +- format / i18n / lint pass for changed surfaces. + +## Constraints + +- Import alias: `@shadcn/components/ui/*` +- Package runner: `pnpm dlx shadcn-vue@latest` +- Icon library in business code remains primarily `@iconify/vue`; Spinner may keep `@lucide/vue` from the shadcn component source. +- Do not rewrite the whole app’s `space-y` or icon system in this goal. +- Do not upgrade `@vueuse/core` unless a required API is missing (14.3.0 is sufficient for Wave 1). + +## Non-goals + +- Full redesign of settings or chat chrome +- Migrating Spotlight / AcpDebugDialog overlays in Wave 1 +- Replacing domain components with shadcn “lookalikes” +- Mass i18n or dark-mode token rewrite beyond touched files + +## References + +- [unovue/shadcn-vue skill](https://github.com/unovue/shadcn-vue/tree/dev/skills/shadcn-vue) +- Project `components.json` +- Example usages: `MessageItemAssistant.vue` (Spinner), `SettingsOverview.vue` (Empty) diff --git a/docs/architecture/shadcn-vueuse-alignment/tasks.md b/docs/architecture/shadcn-vueuse-alignment/tasks.md new file mode 100644 index 0000000000..d1bf4eebfc --- /dev/null +++ b/docs/architecture/shadcn-vueuse-alignment/tasks.md @@ -0,0 +1,34 @@ +# Tasks: shadcn-vue + VueUse Alignment + +## Wave 1 + +- [x] Write `spec.md` / `plan.md` / `tasks.md` +- [x] Update `Agents.md` standing rule (shadcn + VueUse prefer) +- [x] Skeleton: `MessageItemPlaceholder.vue`, `ChatSessionSkeleton.vue`, `WindowSideBar.vue`, settings pages +- [x] Spinner: `ModelCheckDialog.vue`, media blocks, MCP/artifact/popup loaders +- [x] Empty: `MemoryEmptyState.vue`, `WindowSideBar.vue` empty list +- [x] VueUse: `sidepanel.ts`, `ProviderModelList.vue`, selected listeners/intervals +- [x] Delete dead `ScrollablePopover.vue` +- [x] `pnpm run format` / `i18n` / `lint` + +## Wave 2 + +- [x] `AgentTransferDialog` RadioGroup + Select +- [x] `ModelConfigItem` Tooltip + i18n capability keys +- [x] Skills/Knowledge Spinner/Skeleton/Empty batch (agents, sync, git install, folder tree, knowledge search) +- [x] VueUse: MemoryListView debounce, WorkspaceCodePane `useResizeObserver`, clipboard helpers +- [x] Remaining settings Spinner (AcpSettings, SkillInstallDialog, Export/Import wizards, ImportExport tab, etc.) + +## Wave 3 + +- [x] `AcpDebugDialog` → Dialog (fullscreen) + Spinner/Empty; skipped upstream overwrite +- [x] `providerStore` order sync → `useDebounceFn` +- [ ] Optional upstream shadcn refresh via CLI smart merge (blocked by Node fetch/proxy; deferred) + +## Wave 4 + +- [x] Settings remaining Spinner: `App`, `DataSettings`, `RemoteSettings`, OAuth (GitHub/Grok/Codex), MCP market/settings, provider list/manager/api/config import, Dashboard/Environments/About/Shortcuts/Cron/Notifications/BuiltinKnowledge/KnowledgeFileItem/ModelScope +- [x] Main renderer remaining Spinner: `ChatStatusBar`, `ChatInputToolbar`, `SkillsIndicator`, artifacts (`Svg`/`Preview`/`ToolCall`), `PluginsCatalogPage`, `McpServerCard`, `SpotlightOverlay` empty/loading +- [x] Fix `MemoryListView` `useDebounceFn().cancel()` (VueUse 14 has no cancel API; requestId stale-guard) +- [ ] Optional: remaining `animate-pulse` skeletons → `Skeleton` (Dashboard plugins pages etc., separate from Spinner) +- [ ] Optional: domain status metadata (`useAgentPlanStatus` loader icon class) if product wants Spinner there diff --git a/package.json b/package.json index 56714a5562..a4803c0a59 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "@iconify-json/vscode-icons": "^1.2.49", "@iconify/vue": "^5.0.1", "@lingual/i18n-check": "0.8.12", - "@lucide/vue": "^1.22.0", + "@lucide/vue": "^1.24.0", "@pinia/colada": "^0.20.0", "@playwright/test": "^1.60.0", "@tailwindcss/typography": "^0.5.19", @@ -204,7 +204,7 @@ "oxlint": "^1.71.0", "picocolors": "^1.1.1", "pinia": "^3.0.4", - "reka-ui": "^2.9.7", + "reka-ui": "^2.10.1", "stream-monaco": "^0.0.49", "tailwind-merge": "^3.6.0", "tailwind-scrollbar-hide": "^4.0.0", diff --git a/src/renderer/settings/App.vue b/src/renderer/settings/App.vue index 7fd8054a0a..08433652c3 100644 --- a/src/renderer/settings/App.vue +++ b/src/renderer/settings/App.vue @@ -51,13 +51,11 @@ @focus="prefetchSetting(setting.name)" @click="handleClick(setting)" > - + {{ t(setting.title) }} @@ -93,7 +91,7 @@ import { Icon } from '@iconify/vue' import { useRouter, useRoute, RouterView } from 'vue-router' import { onMounted, onBeforeUnmount, Ref, ref, watch, computed, nextTick, unref } from 'vue' import { useI18n } from 'vue-i18n' -import { useTitle } from '@vueuse/core' +import { useEventListener, useTitle } from '@vueuse/core' import { createConfigClient } from '@api/ConfigClient' import { createDeviceClient } from '@api/DeviceClient' import { createWindowClient } from '@api/WindowClient' @@ -106,6 +104,7 @@ import { Button } from '@shadcn/components/ui/button' import ModelCheckDialog from '@/components/settings/ModelCheckDialog.vue' import { useDeviceVersion } from '../src/composables/useDeviceVersion' import { Toaster } from '@shadcn/components/ui/sonner' +import { Spinner } from '@shadcn/components/ui/spinner' import 'vue-sonner/style.css' import { useToast } from '@/components/use-toast' import { useThemeStore } from '@/stores/theme' @@ -699,12 +698,14 @@ onMounted(async () => { } markStartupInteractive() - window.addEventListener('focus', handleWindowFocus) await syncPendingProviderInstall() notifySettingsReady() logSettingsStartup('settings window ready IPC sent') }) +// Same focus handler as before; VueUse manages lifecycle cleanup. +useEventListener(window, 'focus', handleWindowFocus) + const closeWindow = async () => { await windowClient.closeSettings() } @@ -718,7 +719,6 @@ onBeforeUnmount(() => { cleanupSettingsNavigate() cleanupSettingsProviderInstall() settingsEventCleanups.splice(0).forEach((cleanup) => cleanup()) - window.removeEventListener('focus', handleWindowFocus) cleanupMcpDeeplink() }) diff --git a/src/renderer/settings/components/AboutUsSettings.vue b/src/renderer/settings/components/AboutUsSettings.vue index 1d5f5bf328..45ec8486b3 100644 --- a/src/renderer/settings/components/AboutUsSettings.vue +++ b/src/renderer/settings/components/AboutUsSettings.vue @@ -189,13 +189,12 @@ :disabled="upgrade.isChecking || upgrade.isDownloading || upgrade.isRestarting" @click="handlePrimaryAction" > - +