From a26fc9a08073c198f16328748c50826f93bbe1bb Mon Sep 17 00:00:00 2001 From: zerob13 Date: Tue, 30 Jun 2026 12:59:54 +0800 Subject: [PATCH 1/5] fix(settings): add minimum window size --- .../dead-code-cleanup-2026-06/plan.md | 23 ++ .../dead-code-cleanup-2026-06/spec.md | 34 +++ .../dead-code-cleanup-2026-06/tasks.md | 7 + docs/issues/settings-window-overflow/plan.md | 37 ++++ docs/issues/settings-window-overflow/spec.md | 30 +++ docs/issues/settings-window-overflow/tasks.md | 6 + src/main/presenter/windowPresenter/index.ts | 2 + .../workspacePresenter/directoryReader.ts | 60 ------ .../src/composables/usePageCapture.example.ts | 196 ------------------ test/main/presenter/windowPresenter.test.ts | 17 ++ test/setup.ts | 12 ++ 11 files changed, 168 insertions(+), 256 deletions(-) create mode 100644 docs/architecture/dead-code-cleanup-2026-06/plan.md create mode 100644 docs/architecture/dead-code-cleanup-2026-06/spec.md create mode 100644 docs/architecture/dead-code-cleanup-2026-06/tasks.md create mode 100644 docs/issues/settings-window-overflow/plan.md create mode 100644 docs/issues/settings-window-overflow/spec.md create mode 100644 docs/issues/settings-window-overflow/tasks.md delete mode 100644 src/renderer/src/composables/usePageCapture.example.ts diff --git a/docs/architecture/dead-code-cleanup-2026-06/plan.md b/docs/architecture/dead-code-cleanup-2026-06/plan.md new file mode 100644 index 0000000000..051d672a3d --- /dev/null +++ b/docs/architecture/dead-code-cleanup-2026-06/plan.md @@ -0,0 +1,23 @@ +# Dead Code Cleanup 2026-06 - Plan + +## Fix Strategy + +Delete only the two confirmed dead-code targets. + +## Affected Files + +- `src/main/presenter/workspacePresenter/directoryReader.ts` +- `src/renderer/src/composables/usePageCapture.example.ts` + +## Steps + +1. Remove `readDirectoryTree`. +2. Delete `usePageCapture.example.ts`. +3. Re-run targeted `rg` checks. +4. Run `pnpm run typecheck`. +5. Run `pnpm run lint`. + +## Risk + +Low. The only risk is an undocumented external import path. The targeted `rg` check is enough for this repo. + diff --git a/docs/architecture/dead-code-cleanup-2026-06/spec.md b/docs/architecture/dead-code-cleanup-2026-06/spec.md new file mode 100644 index 0000000000..7aa719c76a --- /dev/null +++ b/docs/architecture/dead-code-cleanup-2026-06/spec.md @@ -0,0 +1,34 @@ +# Dead Code Cleanup 2026-06 - Spec + +## Problem + +Two confirmed dead-code candidates remain: + +- `readDirectoryTree` in `src/main/presenter/workspacePresenter/directoryReader.ts` +- `src/renderer/src/composables/usePageCapture.example.ts` + +They are not used by production code or tests except for self-reference/commented example references. + +## Goal + +Remove confirmed dead code without changing runtime behavior. + +## Evidence + +- `rg readDirectoryTree src test docs` only finds the deprecated function and its recursive self-call. +- Production message capture imports `@/composables/message/useMessageCapture`, not `usePageCapture.example`. +- `usePageCapture.example.ts` is an example file under `src/renderer/src`, which keeps it in source search/typecheck scope. + +## Acceptance Criteria + +1. `rg readDirectoryTree src test docs` returns no live production/test references after cleanup. +2. `rg usePageCapture.example src test docs` returns no live production/test references after cleanup. +3. No runtime imports are changed except removing dead exports/files. +4. Typecheck and lint pass. + +## Non-Goals + +- Do not run a broad dead-code sweep. +- Do not refactor workspace directory loading. +- Do not rewrite page capture implementation. + diff --git a/docs/architecture/dead-code-cleanup-2026-06/tasks.md b/docs/architecture/dead-code-cleanup-2026-06/tasks.md new file mode 100644 index 0000000000..702c95530a --- /dev/null +++ b/docs/architecture/dead-code-cleanup-2026-06/tasks.md @@ -0,0 +1,7 @@ +# Dead Code Cleanup 2026-06 - Tasks + +- [x] Remove `readDirectoryTree`. +- [x] Delete `src/renderer/src/composables/usePageCapture.example.ts`. +- [x] Verify both symbols/paths are gone with `rg`. +- [x] Run `pnpm run typecheck`. +- [x] Run `pnpm run lint`. diff --git a/docs/issues/settings-window-overflow/plan.md b/docs/issues/settings-window-overflow/plan.md new file mode 100644 index 0000000000..4b27d3a8f2 --- /dev/null +++ b/docs/issues/settings-window-overflow/plan.md @@ -0,0 +1,37 @@ +# Settings Window Overflow - Plan + +## Fix Strategy + +Use the smallest fix that covers the bug: set `minWidth` and `minHeight` on the Settings `BrowserWindow`. + +This avoids touching dozens of Settings row components. Responsive row cleanup can happen later when those components are edited for other reasons. + +## Proposed Bounds + +Use a conservative desktop Settings minimum: + +- `minWidth`: 900 +- `minHeight`: 640 + +The width covers: + +```text +Settings minimum 900px +| sidebar 240px | content 660px | + +Current widest common row: +| label 220px | gap | control 320px | = about 552px before page padding +``` + +## Affected Files + +- `src/main/presenter/windowPresenter/index.ts` +- `test/main/presenter/windowPresenter.test.ts` only if the existing BrowserWindow option test can assert minimum bounds cheaply. + +## Test Strategy + +1. Add or update one window presenter test that verifies Settings BrowserWindow receives `minWidth` and `minHeight`. +2. Run `pnpm run typecheck`. +3. Run `pnpm run lint`. +4. If touching renderer layout later, add a Playwright/E2E narrow-width check then; not needed for this minimum-window fix. + diff --git a/docs/issues/settings-window-overflow/spec.md b/docs/issues/settings-window-overflow/spec.md new file mode 100644 index 0000000000..0e0df8f9a7 --- /dev/null +++ b/docs/issues/settings-window-overflow/spec.md @@ -0,0 +1,30 @@ +# Settings Window Overflow - Spec + +## Problem + +Settings can be resized below the width assumed by several fixed-width rows. The window state persists user size, but `createSettingsWindow` sets no minimum size. A narrow Settings window can squeeze the fixed `w-60` sidebar plus rows that combine `min-w-[220px]` labels and `w-[320px]` controls. + +## Goal + +Prevent Settings content from overflowing horizontally at the smallest supported Settings window size. + +## Evidence + +- `src/main/presenter/windowPresenter/index.ts` creates the Settings `BrowserWindow` with default size only. +- `src/renderer/settings/App.vue` uses a fixed `w-60` sidebar. +- `src/renderer/settings/components/common/DefaultModelSettingsSection.vue` and `ProxySettingsSection.vue` use fixed 220px labels plus 320px controls. +- `src/renderer/settings/components/MemoryConfigPanel.vue` already shows the preferred responsive control pattern: `w-full min-w-0 ... md:w-[320px]`. + +## Acceptance Criteria + +1. A Settings window cannot be resized or restored narrower than the width required by current fixed rows. +2. At the minimum width, Settings pages do not show horizontal document overflow. +3. No new user-facing strings are introduced. +4. Existing Settings window state restore behavior still works for valid saved sizes. + +## Non-Goals + +- Do not redesign Settings layout. +- Do not rewrite every Settings row in this pass. +- Do not change main-window Plugins pages. + diff --git a/docs/issues/settings-window-overflow/tasks.md b/docs/issues/settings-window-overflow/tasks.md new file mode 100644 index 0000000000..1c732ecc87 --- /dev/null +++ b/docs/issues/settings-window-overflow/tasks.md @@ -0,0 +1,6 @@ +# Settings Window Overflow - Tasks + +- [x] Add Settings `BrowserWindow` minimum size. +- [x] Add the smallest relevant window presenter assertion, if current mocks expose options cleanly. +- [x] Verify with `pnpm run typecheck`. +- [x] Verify with `pnpm run lint`. diff --git a/src/main/presenter/windowPresenter/index.ts b/src/main/presenter/windowPresenter/index.ts index 483c91fcde..726487dca5 100644 --- a/src/main/presenter/windowPresenter/index.ts +++ b/src/main/presenter/windowPresenter/index.ts @@ -1313,6 +1313,8 @@ export class WindowPresenter implements IWindowPresenter { y: settingsWindowState.y, width: settingsWindowState.width, height: settingsWindowState.height, + minWidth: 900, + minHeight: 640, show: false, autoHideMenuBar: true, fullscreenable: false, diff --git a/src/main/presenter/workspacePresenter/directoryReader.ts b/src/main/presenter/workspacePresenter/directoryReader.ts index b23ab436d5..7a1f9723ae 100644 --- a/src/main/presenter/workspacePresenter/directoryReader.ts +++ b/src/main/presenter/workspacePresenter/directoryReader.ts @@ -71,63 +71,3 @@ export async function readDirectoryShallow(dirPath: string): Promise { - // Boundary check: depth limit - if (currentDepth >= maxDepth) { - return [] - } - - try { - const entries = await fs.readdir(dirPath, { withFileTypes: true }) - const nodes: WorkspaceFileNode[] = [] - - for (const entry of entries) { - // Skip ignored files/directories - if (IGNORED_PATTERNS.includes(entry.name)) { - continue - } - - // Skip hidden files (starting with .) - if (entry.name.startsWith('.')) { - continue - } - - const fullPath = path.join(dirPath, entry.name) - const node: WorkspaceFileNode = { - name: entry.name, - path: fullPath, - isDirectory: entry.isDirectory() - } - - // Recursively read subdirectories - if (entry.isDirectory()) { - node.children = await readDirectoryTree(fullPath, currentDepth + 1, maxDepth) - node.expanded = false // Default collapsed - } - - nodes.push(node) - } - - // Sort: directories first, files second, same type sorted by name - return nodes.sort((a, b) => { - if (a.isDirectory !== b.isDirectory) { - return a.isDirectory ? -1 : 1 - } - return a.name.localeCompare(b.name) - }) - } catch (error) { - console.error(`[Workspace] Failed to read directory ${dirPath}:`, error) - return [] - } -} diff --git a/src/renderer/src/composables/usePageCapture.example.ts b/src/renderer/src/composables/usePageCapture.example.ts deleted file mode 100644 index 8f65c62603..0000000000 --- a/src/renderer/src/composables/usePageCapture.example.ts +++ /dev/null @@ -1,196 +0,0 @@ -// usePageCapture 使用示例 - -import { createDeviceClient } from '@api/DeviceClient' -import { usePageCapture, createCapturePresets } from '@/composables/usePageCapture' -import { useI18n } from 'vue-i18n' -import { ref } from 'vue' - -// 在组件中使用示例 -export function useMessageCapture() { - const { t } = useI18n() - const { isCapturing, captureAndCopy } = usePageCapture() - const deviceClient = createDeviceClient() - const appVersion = ref('') - - // 初始化应用版本 - const initAppVersion = async () => { - appVersion.value = await deviceClient.getAppVersion() - } - - // 获取水印配置 - const getWatermarkConfig = (isDark: boolean, modelName?: string, providerName?: string) => ({ - isDark, - version: appVersion.value, - texts: { - brand: 'DeepChat', - tip: t('common.watermarkTip'), - model: modelName, - provider: providerName - } - }) - - // 计算单个消息组范围(用户消息 + 助手消息) - const calculateMessageGroupRect = (messageNode: HTMLElement, parentId?: string) => { - const userMessageElement = parentId - ? (document.querySelector(`[data-message-id="${parentId}"]`) as HTMLElement) - : null - - if (!userMessageElement || !messageNode) { - if (messageNode) { - const rect = messageNode.getBoundingClientRect() - return { - x: Math.round(rect.x), - y: Math.round(rect.y), - width: Math.round(rect.width), - height: Math.round(rect.height) - } - } - return null - } - - const userRect = userMessageElement.getBoundingClientRect() - const assistantRect = messageNode.getBoundingClientRect() - - const left = Math.min(userRect.left, assistantRect.left) - const top = Math.min(userRect.top, assistantRect.top) - const right = Math.max(userRect.right, assistantRect.right) - const bottom = Math.max(userRect.bottom, assistantRect.bottom) - - return { - x: Math.round(left), - y: Math.round(top), - width: Math.round(right - left), - height: Math.round(bottom - top) - } - } - - // 计算从顶部到当前消息的范围 - const calculateFromTopToCurrentRect = (currentMessageNode: HTMLElement) => { - const container = document.querySelector('.message-list-container') - if (!container || !currentMessageNode) return null - - const allMessages = container.querySelectorAll('[data-message-id]') - if (allMessages.length === 0) return null - - const firstMessage = allMessages[0] as HTMLElement - const currentRect = currentMessageNode.getBoundingClientRect() - const firstRect = firstMessage.getBoundingClientRect() - - const left = Math.min(firstRect.left, currentRect.left) - const top = Math.min(firstRect.top, currentRect.top) - const right = Math.max(firstRect.right, currentRect.right) - const bottom = Math.max(firstRect.bottom, currentRect.bottom) - - return { - x: Math.round(left), - y: Math.round(top), - width: Math.round(right - left), - height: Math.round(bottom - top) - } - } - - // 截图单个消息组 - const captureMessageGroup = async ( - messageNode: HTMLElement, - parentId: string | undefined, - isDark: boolean, - modelName?: string, - providerName?: string - ) => { - return await captureAndCopy({ - container: '.message-list-container', - getTargetRect: () => calculateMessageGroupRect(messageNode, parentId), - watermark: getWatermarkConfig(isDark, modelName, providerName) - }) - } - - // 截图从顶部到当前消息 - const captureFromTopToCurrent = async ( - currentMessageNode: HTMLElement, - isDark: boolean, - modelName?: string, - providerName?: string - ) => { - return await captureAndCopy({ - container: '.message-list-container', - getTargetRect: () => calculateFromTopToCurrentRect(currentMessageNode), - watermark: getWatermarkConfig(isDark, modelName, providerName) - }) - } - - // 使用预设配置截取整个会话 - const captureFullConversation = async ( - isDark: boolean, - modelName?: string, - providerName?: string - ) => { - const { captureFullConversation } = createCapturePresets() - const config = captureFullConversation(getWatermarkConfig(isDark, modelName, providerName)) - return await captureAndCopy(config) - } - - // 使用预设配置截取消息范围 - const captureMessageRange = async ( - startMessageId: string, - endMessageId: string, - isDark: boolean, - modelName?: string, - providerName?: string - ) => { - const { captureMessageRange } = createCapturePresets() - const config = captureMessageRange( - startMessageId, - endMessageId, - getWatermarkConfig(isDark, modelName, providerName) - ) - return await captureAndCopy(config) - } - - return { - isCapturing, - initAppVersion, - captureMessageGroup, - captureFromTopToCurrent, - captureFullConversation, - captureMessageRange - } -} - -// 在 MessageItemAssistant.vue 中的使用示例 -/* - -*/ diff --git a/test/main/presenter/windowPresenter.test.ts b/test/main/presenter/windowPresenter.test.ts index 4f5c80e61a..5624b148b5 100644 --- a/test/main/presenter/windowPresenter.test.ts +++ b/test/main/presenter/windowPresenter.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { BrowserWindow } from 'electron' import { SETTINGS_EVENTS } from '@/events' vi.mock('electron-window-state', () => ({ @@ -148,4 +149,20 @@ describe('WindowPresenter settings navigation queue', () => { ;(presenter as any).handleSettingsWindowNavigationStart(9, true, false) expect((presenter as any).settingsWindowReady).toBe(false) }) + + it('sets a minimum size for the settings window', async () => { + const { WindowPresenter } = await import('@/presenter/windowPresenter') + const presenter = new WindowPresenter({ + getContentProtectionEnabled: vi.fn(() => false) + } as any) + + await presenter.createSettingsWindow() + + expect(BrowserWindow).toHaveBeenCalledWith( + expect.objectContaining({ + minWidth: 900, + minHeight: 640 + }) + ) + }) }) diff --git a/test/setup.ts b/test/setup.ts index c9a661dfa6..ddb7b02cca 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -135,21 +135,33 @@ vi.mock('electron', () => ({ isReady: vi.fn(() => true) }, BrowserWindow: vi.fn(() => ({ + id: 1, loadURL: vi.fn(), loadFile: vi.fn(), on: vi.fn(), webContents: { + id: 2, send: vi.fn(), on: vi.fn(), setWindowOpenHandler: vi.fn(), + setBackgroundThrottling: vi.fn(), + setFrameRate: vi.fn(), + openDevTools: vi.fn(), isDestroyed: vi.fn(() => false) }, isDestroyed: vi.fn(() => false), + setContentProtection: vi.fn(), + setBackgroundColor: vi.fn(), + setHiddenInMissionControl: vi.fn(), + setSkipTaskbar: vi.fn(), close: vi.fn(), show: vi.fn(), focus: vi.fn(), hide: vi.fn() })), + nativeImage: { + createFromPath: vi.fn(() => ({})) + }, ipcMain: { on: vi.fn(), handle: vi.fn(), From 76aac9caca5904454dec7a5e4f48d20a565c8481 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Tue, 30 Jun 2026 13:13:55 +0800 Subject: [PATCH 2/5] feat(sidebar): add workspace chat actions --- .../sidebar-chat-section-actions/plan.md | 30 ++++++ .../sidebar-chat-section-actions/spec.md | 51 ++++++++++ .../sidebar-chat-section-actions/tasks.md | 13 +++ src/renderer/src/components/WindowSideBar.vue | 69 ++++++++++---- .../renderer/components/WindowSideBar.test.ts | 93 +++++++++++++++++-- 5 files changed, 226 insertions(+), 30 deletions(-) create mode 100644 docs/features/sidebar-chat-section-actions/plan.md create mode 100644 docs/features/sidebar-chat-section-actions/spec.md create mode 100644 docs/features/sidebar-chat-section-actions/tasks.md diff --git a/docs/features/sidebar-chat-section-actions/plan.md b/docs/features/sidebar-chat-section-actions/plan.md new file mode 100644 index 0000000000..8d7e9df2d8 --- /dev/null +++ b/docs/features/sidebar-chat-section-actions/plan.md @@ -0,0 +1,30 @@ +# Sidebar Section New Chat Actions - Plan + +## Fix Strategy + +Reuse the existing sidebar new-chat handler after setting `projectStore.selectedProject` to the workspace implied by the clicked row. + +This keeps the change local to the sidebar and avoids adding route query state. + +## Affected Files + +- `src/renderer/src/components/WindowSideBar.vue` +- `test/renderer/components/WindowSideBar.test.ts` + +## Implementation Plan + +1. Remove the Chat section leading icon markup and the now-unused icon constant. +2. Make the Chat row background include the action area and place the Chat `+` inside the row. +3. Add a small helper that selects a project path, then calls the existing new-chat handler. +4. Use the default Chat workspace path for the Chat `+`, falling back to explicit no-project chat when unavailable. +5. Add Project folder `+` buttons before the existing `...` button only for project directory groups. +6. Add click isolation on `+` buttons so they do not toggle their section. +7. Update the sidebar component test to assert no leading Chat icon, preserved collapse behavior, workspace selection, and no Project `+` in time grouping. + +## Test Strategy + +1. Run `pnpm vitest run test/renderer/components/WindowSideBar.test.ts`. +2. Run `pnpm run format`. +3. Run `pnpm run i18n`. +4. Run `pnpm run lint`. +5. Run `pnpm run typecheck`. diff --git a/docs/features/sidebar-chat-section-actions/spec.md b/docs/features/sidebar-chat-section-actions/spec.md new file mode 100644 index 0000000000..4805fc030f --- /dev/null +++ b/docs/features/sidebar-chat-section-actions/spec.md @@ -0,0 +1,51 @@ +# Sidebar Section New Chat Actions - Spec + +## Problem + +The sidebar Chat section header still shows a leading message icon and has no local new-chat action. Project folder headers also require the same local new-chat affordance, but only when the sidebar is grouped by project folders. + +## Goal + +Make Chat and Project folder headers use the same compact row interaction: hover covers the whole row, `+` appears inside the row action area, and new chats start with the matching workspace selected. + +## UI Shape + +Chat: + +```text +Chat [+] + Chat session + Chat session +``` + +Project folder mode: + +```text +folder design [+] [...] + Project session +``` + +Time grouping: + +```text +Today + Project session +``` + +## Acceptance Criteria + +1. The Chat section title no longer renders a leading icon. +2. Clicking the Chat title still collapses and expands Chat sessions. +3. Hovering the Chat row covers the title and `+` action area as one row. +4. Clicking the Chat `+` selects the Chat workspace and starts a new chat through the existing route/new-conversation path. +5. Clicking the Chat `+` does not toggle the Chat section collapse state. +6. Project folder rows show a `+` action before the existing `...` action when grouped by project. +7. Clicking a Project folder `+` selects that project workspace and starts a new chat. +8. Time-grouped rows do not show folder `+` actions. +9. No new user-facing strings are introduced. + +## Non-Goals + +- Do not redesign Workspace grouping. +- Do not change how sessions are persisted or created from `NewThreadPage`. +- Do not change Remote features. diff --git a/docs/features/sidebar-chat-section-actions/tasks.md b/docs/features/sidebar-chat-section-actions/tasks.md new file mode 100644 index 0000000000..2578f6de7d --- /dev/null +++ b/docs/features/sidebar-chat-section-actions/tasks.md @@ -0,0 +1,13 @@ +# Sidebar Chat Section Actions - Tasks + +- [x] Add SDD spec, plan, and tasks. +- [x] Update Chat section header layout and action. +- [x] Update sidebar tests for the new Chat header behavior. +- [x] Run targeted sidebar tests. +- [x] Run format, i18n, lint, and typecheck. +- [x] Update SDD for Chat workspace selection and Project folder actions. +- [x] Make Chat row hover include the `+` action area. +- [x] Select Chat workspace before Chat `+` starts a new conversation. +- [x] Add Project folder `+` actions in project grouping only. +- [x] Update sidebar tests for workspace selection and Project folder actions. +- [x] Rerun targeted sidebar tests and required checks. diff --git a/src/renderer/src/components/WindowSideBar.vue b/src/renderer/src/components/WindowSideBar.vue index cd6a6d1bd6..6d8015db8e 100644 --- a/src/renderer/src/components/WindowSideBar.vue +++ b/src/renderer/src/components/WindowSideBar.vue @@ -282,25 +282,35 @@
- + + + + + + {{ t('common.newChat') }} + +
+ + + + + {{ t('common.newChat') }} + + @@ -545,7 +570,6 @@ 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' @@ -1276,6 +1300,11 @@ const handleNewChat = async () => { } } +const handleNewChatForProject = async (projectPath: string | null) => { + projectStore.selectProject(projectPath, 'manual') + await handleNewChat() +} + const handleAgentSelect = async (id: string | null) => { if (collapsed.value) { sidebarStore.setCollapsed(false) diff --git a/test/renderer/components/WindowSideBar.test.ts b/test/renderer/components/WindowSideBar.test.ts index 015df391ec..56bc1988b4 100644 --- a/test/renderer/components/WindowSideBar.test.ts +++ b/test/renderer/components/WindowSideBar.test.ts @@ -225,7 +225,10 @@ const setup = async (options: SetupOptions = {}) => { archivedEnvironments: options.archivedProjectEnvironments ?? [], defaultChatWorkspacePath: options.defaultChatWorkspacePath ?? null, fetchEnvironments: vi.fn().mockResolvedValue(undefined), - reorderEnvironments: vi.fn().mockResolvedValue(undefined) + reorderEnvironments: vi.fn().mockResolvedValue(undefined), + selectProject: vi.fn((path: string | null, source?: string) => { + operations.push(`project:${path ?? 'none'}:${source ?? 'default'}`) + }) }) const spotlightStore = reactive({ open: false, @@ -653,13 +656,14 @@ describe('WindowSideBar agent switch', () => { it( 'collapses and expands chat sessions from the chat header', async () => { - const { wrapper } = await setup({ + const { wrapper, router, sessionStore, projectStore } = await setup({ + currentRouteName: 'plugins', + defaultChatWorkspacePath: '/Users/test/Documents/DeepChat', groupMode: 'project', groups: [ { - id: '__no_project__', - label: 'No Project', - labelKey: 'common.project.none', + id: '/Users/test/Documents/DeepChat', + label: 'DeepChat', sessions: [ { id: 'chat-1', @@ -674,18 +678,29 @@ describe('WindowSideBar agent switch', () => { 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.find('[data-testid="window-sidebar-chat-icon"]').exists()).toBe(false) expect(wrapper.get('[data-session-id="chat-1"]').isVisible()).toBe(true) + await wrapper.get('[data-testid="window-sidebar-chat-new-button"]').trigger('click') + await flushPromises() + + expect(projectStore.selectProject).toHaveBeenCalledWith( + '/Users/test/Documents/DeepChat', + 'manual' + ) + expect(router.push).toHaveBeenCalledWith({ name: 'chat' }) + expect(sessionStore.startNewConversation).toHaveBeenCalledWith({ refresh: true }) + expect(wrapper.get('[data-group-id="__chat__"]').attributes('aria-expanded')).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 + ( + wrapper.get('[data-group-id="__chat__"]').element.parentElement + ?.nextElementSibling as HTMLElement + ).style.display ).toBe('none') await wrapper.find('[data-group-id="__chat__"]').trigger('click') @@ -697,6 +712,64 @@ describe('WindowSideBar agent switch', () => { TEST_TIMEOUT_MS ) + it( + 'starts new conversations from project folder headers only in project grouping', + async () => { + const { wrapper, projectStore, router, sessionStore } = await setup({ + currentRouteName: 'plugins', + groupMode: 'project', + groups: [ + { + id: '/work/design', + label: 'design', + sessions: [ + { + id: 'project-design', + title: 'Design Session', + status: 'none', + projectDir: '/work/design' + } + ] + } + ] + }) + + await wrapper.vm.$nextTick() + + await wrapper.get('[data-testid="window-sidebar-project-new-button"]').trigger('click') + await flushPromises() + + expect(projectStore.selectProject).toHaveBeenCalledWith('/work/design', 'manual') + expect(router.push).toHaveBeenCalledWith({ name: 'chat' }) + expect(sessionStore.startNewConversation).toHaveBeenCalledWith({ refresh: true }) + + const { wrapper: timeWrapper } = await setup({ + groupMode: 'time', + groups: [ + { + id: 'today', + label: 'Today', + sessions: [ + { + id: 'time-project', + title: 'Time Project Session', + status: 'none', + projectDir: '/work/design' + } + ] + } + ] + }) + + await timeWrapper.vm.$nextTick() + + expect(timeWrapper.find('[data-testid="window-sidebar-project-new-button"]').exists()).toBe( + false + ) + }, + TEST_TIMEOUT_MS + ) + it( 'toggles pinned state from a session item action', async () => { From 8eac59527fd00dbfeb906c8dc87ba0d0207cfa95 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Tue, 30 Jun 2026 13:25:18 +0800 Subject: [PATCH 3/5] docs(sdd): remove completed plans --- .../dead-code-cleanup-2026-06/plan.md | 23 ------------ .../dead-code-cleanup-2026-06/tasks.md | 7 ---- .../sidebar-chat-section-actions/plan.md | 30 --------------- .../sidebar-chat-section-actions/tasks.md | 13 ------- docs/issues/settings-window-overflow/plan.md | 37 ------------------- docs/issues/settings-window-overflow/tasks.md | 6 --- 6 files changed, 116 deletions(-) delete mode 100644 docs/architecture/dead-code-cleanup-2026-06/plan.md delete mode 100644 docs/architecture/dead-code-cleanup-2026-06/tasks.md delete mode 100644 docs/features/sidebar-chat-section-actions/plan.md delete mode 100644 docs/features/sidebar-chat-section-actions/tasks.md delete mode 100644 docs/issues/settings-window-overflow/plan.md delete mode 100644 docs/issues/settings-window-overflow/tasks.md diff --git a/docs/architecture/dead-code-cleanup-2026-06/plan.md b/docs/architecture/dead-code-cleanup-2026-06/plan.md deleted file mode 100644 index 051d672a3d..0000000000 --- a/docs/architecture/dead-code-cleanup-2026-06/plan.md +++ /dev/null @@ -1,23 +0,0 @@ -# Dead Code Cleanup 2026-06 - Plan - -## Fix Strategy - -Delete only the two confirmed dead-code targets. - -## Affected Files - -- `src/main/presenter/workspacePresenter/directoryReader.ts` -- `src/renderer/src/composables/usePageCapture.example.ts` - -## Steps - -1. Remove `readDirectoryTree`. -2. Delete `usePageCapture.example.ts`. -3. Re-run targeted `rg` checks. -4. Run `pnpm run typecheck`. -5. Run `pnpm run lint`. - -## Risk - -Low. The only risk is an undocumented external import path. The targeted `rg` check is enough for this repo. - diff --git a/docs/architecture/dead-code-cleanup-2026-06/tasks.md b/docs/architecture/dead-code-cleanup-2026-06/tasks.md deleted file mode 100644 index 702c95530a..0000000000 --- a/docs/architecture/dead-code-cleanup-2026-06/tasks.md +++ /dev/null @@ -1,7 +0,0 @@ -# Dead Code Cleanup 2026-06 - Tasks - -- [x] Remove `readDirectoryTree`. -- [x] Delete `src/renderer/src/composables/usePageCapture.example.ts`. -- [x] Verify both symbols/paths are gone with `rg`. -- [x] Run `pnpm run typecheck`. -- [x] Run `pnpm run lint`. diff --git a/docs/features/sidebar-chat-section-actions/plan.md b/docs/features/sidebar-chat-section-actions/plan.md deleted file mode 100644 index 8d7e9df2d8..0000000000 --- a/docs/features/sidebar-chat-section-actions/plan.md +++ /dev/null @@ -1,30 +0,0 @@ -# Sidebar Section New Chat Actions - Plan - -## Fix Strategy - -Reuse the existing sidebar new-chat handler after setting `projectStore.selectedProject` to the workspace implied by the clicked row. - -This keeps the change local to the sidebar and avoids adding route query state. - -## Affected Files - -- `src/renderer/src/components/WindowSideBar.vue` -- `test/renderer/components/WindowSideBar.test.ts` - -## Implementation Plan - -1. Remove the Chat section leading icon markup and the now-unused icon constant. -2. Make the Chat row background include the action area and place the Chat `+` inside the row. -3. Add a small helper that selects a project path, then calls the existing new-chat handler. -4. Use the default Chat workspace path for the Chat `+`, falling back to explicit no-project chat when unavailable. -5. Add Project folder `+` buttons before the existing `...` button only for project directory groups. -6. Add click isolation on `+` buttons so they do not toggle their section. -7. Update the sidebar component test to assert no leading Chat icon, preserved collapse behavior, workspace selection, and no Project `+` in time grouping. - -## Test Strategy - -1. Run `pnpm vitest run test/renderer/components/WindowSideBar.test.ts`. -2. Run `pnpm run format`. -3. Run `pnpm run i18n`. -4. Run `pnpm run lint`. -5. Run `pnpm run typecheck`. diff --git a/docs/features/sidebar-chat-section-actions/tasks.md b/docs/features/sidebar-chat-section-actions/tasks.md deleted file mode 100644 index 2578f6de7d..0000000000 --- a/docs/features/sidebar-chat-section-actions/tasks.md +++ /dev/null @@ -1,13 +0,0 @@ -# Sidebar Chat Section Actions - Tasks - -- [x] Add SDD spec, plan, and tasks. -- [x] Update Chat section header layout and action. -- [x] Update sidebar tests for the new Chat header behavior. -- [x] Run targeted sidebar tests. -- [x] Run format, i18n, lint, and typecheck. -- [x] Update SDD for Chat workspace selection and Project folder actions. -- [x] Make Chat row hover include the `+` action area. -- [x] Select Chat workspace before Chat `+` starts a new conversation. -- [x] Add Project folder `+` actions in project grouping only. -- [x] Update sidebar tests for workspace selection and Project folder actions. -- [x] Rerun targeted sidebar tests and required checks. diff --git a/docs/issues/settings-window-overflow/plan.md b/docs/issues/settings-window-overflow/plan.md deleted file mode 100644 index 4b27d3a8f2..0000000000 --- a/docs/issues/settings-window-overflow/plan.md +++ /dev/null @@ -1,37 +0,0 @@ -# Settings Window Overflow - Plan - -## Fix Strategy - -Use the smallest fix that covers the bug: set `minWidth` and `minHeight` on the Settings `BrowserWindow`. - -This avoids touching dozens of Settings row components. Responsive row cleanup can happen later when those components are edited for other reasons. - -## Proposed Bounds - -Use a conservative desktop Settings minimum: - -- `minWidth`: 900 -- `minHeight`: 640 - -The width covers: - -```text -Settings minimum 900px -| sidebar 240px | content 660px | - -Current widest common row: -| label 220px | gap | control 320px | = about 552px before page padding -``` - -## Affected Files - -- `src/main/presenter/windowPresenter/index.ts` -- `test/main/presenter/windowPresenter.test.ts` only if the existing BrowserWindow option test can assert minimum bounds cheaply. - -## Test Strategy - -1. Add or update one window presenter test that verifies Settings BrowserWindow receives `minWidth` and `minHeight`. -2. Run `pnpm run typecheck`. -3. Run `pnpm run lint`. -4. If touching renderer layout later, add a Playwright/E2E narrow-width check then; not needed for this minimum-window fix. - diff --git a/docs/issues/settings-window-overflow/tasks.md b/docs/issues/settings-window-overflow/tasks.md deleted file mode 100644 index 1c732ecc87..0000000000 --- a/docs/issues/settings-window-overflow/tasks.md +++ /dev/null @@ -1,6 +0,0 @@ -# Settings Window Overflow - Tasks - -- [x] Add Settings `BrowserWindow` minimum size. -- [x] Add the smallest relevant window presenter assertion, if current mocks expose options cleanly. -- [x] Verify with `pnpm run typecheck`. -- [x] Verify with `pnpm run lint`. From b72ea08af15681608c7d8c5ad8e86dea23d544c3 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Tue, 30 Jun 2026 13:28:01 +0800 Subject: [PATCH 4/5] fix(sidebar): await project selection --- src/renderer/src/components/WindowSideBar.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/components/WindowSideBar.vue b/src/renderer/src/components/WindowSideBar.vue index 6d8015db8e..eaad54cc9c 100644 --- a/src/renderer/src/components/WindowSideBar.vue +++ b/src/renderer/src/components/WindowSideBar.vue @@ -1301,7 +1301,7 @@ const handleNewChat = async () => { } const handleNewChatForProject = async (projectPath: string | null) => { - projectStore.selectProject(projectPath, 'manual') + await projectStore.selectProject(projectPath, 'manual') await handleNewChat() } From 047f86509694adcb0c5e56eb5abf4cdebd214186 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Tue, 30 Jun 2026 13:39:24 +0800 Subject: [PATCH 5/5] build(provider-db): use github source --- .github/workflows/build.yml | 3 --- .github/workflows/prcheck.yml | 1 - .github/workflows/release.yml | 3 --- .github/workflows/windows-arm64-e2e.yml | 1 - .../provider-db-github-source/spec.md | 21 +++++++++++++++ scripts/fetch-provider-db.mjs | 2 +- src/main/env.d.ts | 1 - .../configPresenter/providerDbLoader.ts | 6 ----- src/renderer/src/env.d.ts | 1 - .../configPresenter/providerDbLoader.test.ts | 26 +++++++++++++++++++ 10 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 docs/architecture/provider-db-github-source/spec.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f47bfb65c..09b5f06aea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -92,7 +92,6 @@ jobs: VITE_GITHUB_CLIENT_ID: ${{ secrets.DC_GITHUB_CLIENT_ID }} VITE_GITHUB_CLIENT_SECRET: ${{ secrets.DC_GITHUB_CLIENT_SECRET }} VITE_GITHUB_REDIRECT_URI: ${{ secrets.DC_GITHUB_REDIRECT_URI }} - VITE_PROVIDER_DB_URL: ${{ secrets.CDN_PROVIDER_DB_URL }} - name: Verify packaged DuckDB VSS for Windows shell: bash @@ -169,7 +168,6 @@ jobs: VITE_GITHUB_CLIENT_ID: ${{ secrets.DC_GITHUB_CLIENT_ID }} VITE_GITHUB_CLIENT_SECRET: ${{ secrets.DC_GITHUB_CLIENT_SECRET }} VITE_GITHUB_REDIRECT_URI: ${{ secrets.DC_GITHUB_REDIRECT_URI }} - VITE_PROVIDER_DB_URL: ${{ secrets.CDN_PROVIDER_DB_URL }} - name: Verify packaged DuckDB VSS for Linux shell: bash @@ -263,7 +261,6 @@ jobs: VITE_GITHUB_CLIENT_SECRET: ${{ secrets.DC_GITHUB_CLIENT_SECRET }} VITE_GITHUB_REDIRECT_URI: ${{ secrets.DC_GITHUB_REDIRECT_URI }} NODE_OPTIONS: '--max-old-space-size=4096' - VITE_PROVIDER_DB_URL: ${{ secrets.CDN_PROVIDER_DB_URL }} - name: Verify packaged DuckDB VSS for macOS shell: bash diff --git a/.github/workflows/prcheck.yml b/.github/workflows/prcheck.yml index 64c42f5436..fe8c7cc47b 100644 --- a/.github/workflows/prcheck.yml +++ b/.github/workflows/prcheck.yml @@ -86,4 +86,3 @@ jobs: - name: Build run: pnpm run build env: - VITE_PROVIDER_DB_URL: ${{ secrets.CDN_PROVIDER_DB_URL }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc3ec91fc2..823e25d66c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -186,7 +186,6 @@ jobs: VITE_GITHUB_CLIENT_ID: ${{ secrets.DC_GITHUB_CLIENT_ID }} VITE_GITHUB_CLIENT_SECRET: ${{ secrets.DC_GITHUB_CLIENT_SECRET }} VITE_GITHUB_REDIRECT_URI: ${{ secrets.DC_GITHUB_REDIRECT_URI }} - VITE_PROVIDER_DB_URL: ${{ secrets.CDN_PROVIDER_DB_URL }} - name: Verify packaged DuckDB VSS for Windows shell: bash @@ -263,7 +262,6 @@ jobs: VITE_GITHUB_CLIENT_ID: ${{ secrets.DC_GITHUB_CLIENT_ID }} VITE_GITHUB_CLIENT_SECRET: ${{ secrets.DC_GITHUB_CLIENT_SECRET }} VITE_GITHUB_REDIRECT_URI: ${{ secrets.DC_GITHUB_REDIRECT_URI }} - VITE_PROVIDER_DB_URL: ${{ secrets.CDN_PROVIDER_DB_URL }} - name: Verify packaged DuckDB VSS for Linux shell: bash @@ -360,7 +358,6 @@ jobs: VITE_GITHUB_CLIENT_SECRET: ${{ secrets.DC_GITHUB_CLIENT_SECRET }} VITE_GITHUB_REDIRECT_URI: ${{ secrets.DC_GITHUB_REDIRECT_URI }} NODE_OPTIONS: '--max-old-space-size=4096' - VITE_PROVIDER_DB_URL: ${{ secrets.CDN_PROVIDER_DB_URL }} - name: Verify packaged DuckDB VSS for macOS shell: bash diff --git a/.github/workflows/windows-arm64-e2e.yml b/.github/workflows/windows-arm64-e2e.yml index c2ef3c7e22..c84ddbc9d2 100644 --- a/.github/workflows/windows-arm64-e2e.yml +++ b/.github/workflows/windows-arm64-e2e.yml @@ -58,7 +58,6 @@ jobs: VITE_GITHUB_CLIENT_ID: ${{ secrets.DC_GITHUB_CLIENT_ID }} VITE_GITHUB_CLIENT_SECRET: ${{ secrets.DC_GITHUB_CLIENT_SECRET }} VITE_GITHUB_REDIRECT_URI: ${{ secrets.DC_GITHUB_REDIRECT_URI }} - VITE_PROVIDER_DB_URL: ${{ secrets.CDN_PROVIDER_DB_URL }} - name: Verify bundled plugins shell: bash diff --git a/docs/architecture/provider-db-github-source/spec.md b/docs/architecture/provider-db-github-source/spec.md new file mode 100644 index 0000000000..40b6f2ec16 --- /dev/null +++ b/docs/architecture/provider-db-github-source/spec.md @@ -0,0 +1,21 @@ +# Provider DB GitHub Source - Spec + +## Problem + +Release builds can inject `VITE_PROVIDER_DB_URL` from `CDN_PROVIDER_DB_URL`, while local/test builds use the GitHub-hosted provider database. The CDN is being retired, so builds and runtime refreshes must stop depending on CDN injection. + +## Goal + +Use the GitHub provider database URL everywhere and remove provider DB URL override support from build workflows and runtime code. + +## Acceptance Criteria + +1. CI/release workflows no longer inject `CDN_PROVIDER_DB_URL` or `VITE_PROVIDER_DB_URL`. +2. Runtime provider DB refresh always uses the GitHub default URL. +3. Build-time provider DB fetch always uses the GitHub default URL. +4. Type declarations no longer expose `VITE_PROVIDER_DB_URL`. +5. The retired CDN hostname has no repository references. + +## Non-Goals + +- Do not remove unrelated `deepchatai.cn` official website, OAuth callback, or referer usage. diff --git a/scripts/fetch-provider-db.mjs b/scripts/fetch-provider-db.mjs index 41c3283fbb..b85c11716f 100644 --- a/scripts/fetch-provider-db.mjs +++ b/scripts/fetch-provider-db.mjs @@ -228,7 +228,7 @@ function sanitizeAggregateJson(json) { } async function main() { - const url = process.env.PROVIDER_DB_URL || DEFAULT_URL + const url = DEFAULT_URL const outDir = path.resolve(process.cwd(), 'resources', 'model-db') const outFile = path.join(outDir, 'providers.json') const tmpFile = outFile + '.tmp' diff --git a/src/main/env.d.ts b/src/main/env.d.ts index 69555040fd..0483a8b922 100644 --- a/src/main/env.d.ts +++ b/src/main/env.d.ts @@ -7,7 +7,6 @@ interface ImportMetaEnv { readonly VITE_LOG_IPC_CALL: string readonly VITE_AGENT_PRESENTER_DEBUG?: string readonly VITE_APP_LIFECYCLE_HOOK_DELAY?: string - readonly VITE_PROVIDER_DB_URL?: string } interface ImportMeta { diff --git a/src/main/presenter/configPresenter/providerDbLoader.ts b/src/main/presenter/configPresenter/providerDbLoader.ts index db94d7edb1..f80bd66111 100644 --- a/src/main/presenter/configPresenter/providerDbLoader.ts +++ b/src/main/presenter/configPresenter/providerDbLoader.ts @@ -178,12 +178,6 @@ export class ProviderDbLoader { } private getProviderDbUrl(): string { - const value = import.meta.env.VITE_PROVIDER_DB_URL - if (typeof value === 'string') { - const trimmed = value.trim() - if (trimmed.length > 0) return trimmed - } - return DEFAULT_PROVIDER_DB_URL } diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts index a3b3f57f79..fb3cc5eaae 100644 --- a/src/renderer/src/env.d.ts +++ b/src/renderer/src/env.d.ts @@ -16,7 +16,6 @@ interface ImportMetaEnv { readonly VITE_GITHUB_CLIENT_SECRET: string readonly VITE_GITHUB_REDIRECT_URI: string readonly VITE_LOG_IPC_CALL: string - readonly VITE_PROVIDER_DB_URL?: string } interface ImportMeta { diff --git a/test/main/presenter/configPresenter/providerDbLoader.test.ts b/test/main/presenter/configPresenter/providerDbLoader.test.ts index 12b479d1c8..5a84144892 100644 --- a/test/main/presenter/configPresenter/providerDbLoader.test.ts +++ b/test/main/presenter/configPresenter/providerDbLoader.test.ts @@ -9,6 +9,8 @@ const state = vi.hoisted(() => ({ sendToMain: vi.fn(), publishDeepchatEvent: vi.fn() })) +const DEFAULT_PROVIDER_DB_URL = + 'https://raw.githubusercontent.com/ThinkInAIXYZ/PublicProviderConf/refs/heads/dev/dist/all.json' vi.mock('fs', async () => { const actual = await vi.importActual('node:fs') @@ -107,12 +109,14 @@ describe('ProviderDbLoader', () => { state.publishDeepchatEvent.mockReset() vi.unstubAllGlobals() delete process.env.PROVIDER_DB_TTL_HOURS + delete process.env.PROVIDER_DB_URL }) afterEach(() => { vi.unstubAllGlobals() vi.restoreAllMocks() delete process.env.PROVIDER_DB_TTL_HOURS + delete process.env.PROVIDER_DB_URL fs.rmSync(tempRoot, { recursive: true, force: true }) }) @@ -270,6 +274,28 @@ describe('ProviderDbLoader', () => { }) }) + it('uses the GitHub provider DB URL even when the legacy override env var is set', async () => { + process.env.PROVIDER_DB_URL = 'https://cdn.example.invalid/provider-db.json' + writeCachedDb(createAggregate(['openai'])) + + const fetchMock = vi.fn().mockResolvedValue({ + status: 200, + ok: true, + headers: { + get: vi.fn().mockReturnValue('"etag-github"') + }, + text: vi.fn().mockResolvedValue(JSON.stringify(createAggregate(['openai', 'github']))) + }) + vi.stubGlobal('fetch', fetchMock) + + const ProviderDbLoader = await importLoader() + const loader = new ProviderDbLoader() + + await loader.refreshIfNeeded(true) + + expect(fetchMock).toHaveBeenCalledWith(DEFAULT_PROVIDER_DB_URL, expect.any(Object)) + }) + it('keeps manual refresh available while privacy mode is enabled', async () => { writeCachedDb(createAggregate(['openai']))