From 390d4543c152820e9b286bfc39244de617d66fbc Mon Sep 17 00:00:00 2001 From: zhangmo8 Date: Wed, 25 Mar 2026 17:38:46 +0800 Subject: [PATCH 1/4] feat: update sidebar item ux --- src/renderer/src/components/WindowSideBar.vue | 197 +++++++- .../components/WindowSideBarSessionItem.vue | 430 ++++++++++++++++-- .../renderer/components/WindowSideBar.test.ts | 2 +- .../WindowSideBarSessionItem.test.ts | 113 +++-- 4 files changed, 655 insertions(+), 87 deletions(-) diff --git a/src/renderer/src/components/WindowSideBar.vue b/src/renderer/src/components/WindowSideBar.vue index 984cd42c6..90a7e73f1 100644 --- a/src/renderer/src/components/WindowSideBar.vue +++ b/src/renderer/src/components/WindowSideBar.vue @@ -141,13 +141,16 @@ -
+
@@ -512,7 +694,16 @@ onUnmounted(() => { -webkit-app-region: drag; } +.session-list { + overflow-anchor: none; +} + button { -webkit-app-region: no-drag; } + +:global(.sidebar-pin-flight) { + transform: translateZ(0); + backface-visibility: hidden; +} diff --git a/src/renderer/src/components/WindowSideBarSessionItem.vue b/src/renderer/src/components/WindowSideBarSessionItem.vue index ac777ee08..2363706d8 100644 --- a/src/renderer/src/components/WindowSideBarSessionItem.vue +++ b/src/renderer/src/components/WindowSideBarSessionItem.vue @@ -1,64 +1,424 @@ - - + + diff --git a/test/renderer/components/WindowSideBar.test.ts b/test/renderer/components/WindowSideBar.test.ts index 3888cf089..5f20029c9 100644 --- a/test/renderer/components/WindowSideBar.test.ts +++ b/test/renderer/components/WindowSideBar.test.ts @@ -239,7 +239,7 @@ describe('WindowSideBar agent switch', () => { const item = wrapper.findComponent({ name: 'WindowSideBarSessionItem' }) item.vm.$emit('toggle-pin', session) - await wrapper.vm.$nextTick() + await flushPromises() expect(sessionStore.toggleSessionPinned).toHaveBeenCalledWith('normal-1', true) }, 10000) diff --git a/test/renderer/components/WindowSideBarSessionItem.test.ts b/test/renderer/components/WindowSideBarSessionItem.test.ts index 024cfa6c8..06d38beb4 100644 --- a/test/renderer/components/WindowSideBarSessionItem.test.ts +++ b/test/renderer/components/WindowSideBarSessionItem.test.ts @@ -1,22 +1,29 @@ import { describe, expect, it, vi } from 'vitest' -import { defineComponent } from 'vue' import { mount } from '@vue/test-utils' -const createSession = (isPinned = false) => ({ +const createSession = (options?: { + isPinned?: boolean + status?: 'none' | 'working' | 'completed' | 'error' +}) => ({ id: 'session-1', title: 'Session Title', agentId: 'deepchat', - status: 'none' as const, + status: options?.status ?? ('none' as const), projectDir: '', providerId: 'provider-1', modelId: 'model-1', - isPinned, + isPinned: options?.isPinned ?? false, isDraft: false, createdAt: 1, updatedAt: 1 }) -const mountComponent = async (isPinned = false) => { +const mountComponent = async (options?: { + isPinned?: boolean + status?: 'none' | 'working' | 'completed' | 'error' + heroHidden?: boolean + pinFeedbackMode?: 'pinning' | 'unpinning' | null +}) => { vi.resetModules() vi.doMock('vue-i18n', () => ({ @@ -25,30 +32,19 @@ const mountComponent = async (isPinned = false) => { }) })) - const passthrough = defineComponent({ - template: '
' - }) - - const contextMenuItemStub = defineComponent({ - emits: ['select'], - template: '' - }) - const WindowSideBarSessionItem = (await import('@/components/WindowSideBarSessionItem.vue')) .default const wrapper = mount(WindowSideBarSessionItem, { props: { - session: createSession(isPinned), - active: false + session: createSession(options), + active: false, + region: options?.isPinned ? 'pinned' : 'grouped', + heroHidden: options?.heroHidden ?? false, + pinFeedbackMode: options?.pinFeedbackMode ?? null }, global: { stubs: { - ContextMenu: passthrough, - ContextMenuTrigger: passthrough, - ContextMenuContent: passthrough, - ContextMenuSeparator: passthrough, - ContextMenuItem: contextMenuItemStub, Icon: true } } @@ -61,42 +57,63 @@ describe('WindowSideBarSessionItem', () => { it('emits select when the list item is clicked', async () => { const wrapper = await mountComponent() - await wrapper.find('button').trigger('click') + await wrapper.find('.session-item').trigger('click') expect(wrapper.emitted('select')?.[0]).toEqual([expect.objectContaining({ id: 'session-1' })]) }, 10000) it('renders the correct pin action label for pinned and unpinned sessions', async () => { - const unpinnedWrapper = await mountComponent(false) - const pinnedWrapper = await mountComponent(true) + const unpinnedWrapper = await mountComponent({ isPinned: false }) + const pinnedWrapper = await mountComponent({ isPinned: true }) - expect(unpinnedWrapper.text()).toContain('thread.actions.pin') - expect(pinnedWrapper.text()).toContain('thread.actions.unpin') + const unpinnedPinButton = unpinnedWrapper.find('[aria-label="thread.actions.pin"]') + const pinnedPinButton = pinnedWrapper.find('[aria-label="thread.actions.unpin"]') + + expect(unpinnedPinButton.exists()).toBe(true) + expect(unpinnedPinButton.attributes('aria-pressed')).toBe('false') + expect(pinnedPinButton.exists()).toBe(true) + expect(pinnedPinButton.attributes('aria-pressed')).toBe('true') }, 10000) - it('emits context menu actions with the session payload', async () => { + it('emits toggle-pin and delete with the session payload', async () => { const wrapper = await mountComponent() - const menuButtons = wrapper.findAll('button') - const renameButton = menuButtons.find((button) => - button.text().includes('thread.actions.rename') - ) - const clearButton = menuButtons.find((button) => - button.text().includes('thread.actions.cleanMessages') - ) - const deleteButton = menuButtons.find((button) => - button.text().includes('thread.actions.delete') - ) - - expect(renameButton).toBeTruthy() - expect(clearButton).toBeTruthy() - expect(deleteButton).toBeTruthy() - - await renameButton!.trigger('click') - await clearButton!.trigger('click') - await deleteButton!.trigger('click') - - expect(wrapper.emitted('rename')?.[0]).toEqual([expect.objectContaining({ id: 'session-1' })]) - expect(wrapper.emitted('clear')?.[0]).toEqual([expect.objectContaining({ id: 'session-1' })]) + + await wrapper.find('[aria-label="thread.actions.pin"]').trigger('click') + await wrapper.find('[aria-label="thread.actions.delete"]').trigger('click') + + expect(wrapper.emitted('toggle-pin')?.[0]).toEqual([ + expect.objectContaining({ id: 'session-1' }) + ]) expect(wrapper.emitted('delete')?.[0]).toEqual([expect.objectContaining({ id: 'session-1' })]) }, 10000) + + it('applies the loading shimmer to the title without rendering loading text', async () => { + const wrapper = await mountComponent({ status: 'working' }) + + const title = wrapper.find('.session-title') + const sheen = wrapper.find('.session-title__sheen') + + expect(title.classes()).toContain('session-title--loading') + expect(sheen.exists()).toBe(true) + expect(sheen.attributes('aria-hidden')).toBe('true') + expect(wrapper.find('.session-status-loading').exists()).toBe(false) + expect(wrapper.text()).not.toContain('common.loading') + expect(wrapper.find('[aria-label="thread.actions.pin"]').exists()).toBe(true) + }, 10000) + + it('exposes hero transition and pin feedback state on the rendered item', async () => { + const wrapper = await mountComponent({ + isPinned: true, + heroHidden: true, + pinFeedbackMode: 'pinning' + }) + + const item = wrapper.find('.session-item') + const pinButton = wrapper.find('.pin-button') + + expect(item.attributes('data-pin-fx')).toBe('pinning') + expect(item.attributes('data-session-id')).toBe('session-1') + expect(item.attributes('data-hero-hidden')).toBe('true') + expect(pinButton.attributes('data-pin-fx')).toBe('pinning') + }, 10000) }) From e3d623c8964794576df7c0069b3399f8561b70b6 Mon Sep 17 00:00:00 2001 From: zhangmo8 Date: Thu, 26 Mar 2026 10:30:54 +0800 Subject: [PATCH 2/4] feat: enhance session management with collapsible groups and pinned sessions --- src/renderer/src/components/WindowSideBar.vue | 334 ++++++++++-------- .../components/WindowSideBarSessionItem.vue | 16 +- src/renderer/src/i18n/da-DK/chat.json | 1 + src/renderer/src/i18n/en-US/chat.json | 1 + src/renderer/src/i18n/fa-IR/chat.json | 1 + src/renderer/src/i18n/fr-FR/chat.json | 1 + src/renderer/src/i18n/he-IL/chat.json | 1 + src/renderer/src/i18n/ja-JP/chat.json | 1 + src/renderer/src/i18n/ko-KR/chat.json | 1 + src/renderer/src/i18n/pt-BR/chat.json | 1 + src/renderer/src/i18n/ru-RU/chat.json | 1 + src/renderer/src/i18n/zh-CN/chat.json | 1 + src/renderer/src/i18n/zh-HK/chat.json | 1 + src/renderer/src/i18n/zh-TW/chat.json | 1 + .../renderer/components/WindowSideBar.test.ts | 113 +++++- 15 files changed, 306 insertions(+), 169 deletions(-) diff --git a/src/renderer/src/components/WindowSideBar.vue b/src/renderer/src/components/WindowSideBar.vue index 90a7e73f1..b40661e98 100644 --- a/src/renderer/src/components/WindowSideBar.vue +++ b/src/renderer/src/components/WindowSideBar.vue @@ -140,94 +140,105 @@
+ +
+ +

{{ t('chat.sidebar.emptyTitle') }}

+

+ {{ t('chat.sidebar.emptyDescription') }} +

+
+
-
- -
- - -
- -

{{ t('chat.sidebar.emptyTitle') }}

-

- {{ t('chat.sidebar.emptyDescription') }} -

+
+ + + +
+ +
+
-