Skip to content

Commit 07375ac

Browse files
authored
refactor: enhance transition effects and performance across message components (#1542)
- Updated transition classes in MessageActionButtons.vue for smoother animations. - Refactored MessageBlockContent.vue to optimize artifact snapshot handling with computed properties. - Improved transition effects in MessageBlockToolCall.vue for better user experience. - Added a mention icon map in MessageContent.vue to streamline icon retrieval. - Enhanced MessageItemUser.vue with a new line counting function for better text handling. - Optimized MessageToolbar.vue for consistent transition effects on button interactions. - Refactored BrowserPanel.vue to simplify state management for synced bounds. - Improved ChatSidePanel.vue with better resizing and visibility handling. - Updated ChatPage.vue to enhance chat search highlight scheduling. - Cleaned up ChatTabView.vue by removing legacy collapsed new chat button functionality. - Enhanced tests in ChatTabView.test.ts and WindowSideBar.test.ts for improved coverage and accuracy.
1 parent c6ac348 commit 07375ac

18 files changed

Lines changed: 569 additions & 291 deletions

src/renderer/src/App.vue

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import SpotlightOverlay from '@/components/spotlight/SpotlightOverlay.vue'
3030
import { useSpotlightStore } from '@/stores/ui/spotlight'
3131
import { useSidepanelStore } from '@/stores/ui/sidepanel'
3232
import { useSidebarStore } from '@/stores/ui/sidebar'
33+
import { useProviderStore } from '@/stores/providerStore'
34+
import { useModelStore } from '@/stores/modelStore'
3335
import { useAppIpcRuntime } from '@/composables/useAppIpcRuntime'
3436
import type { DatabaseRepairSuggestedPayload } from '@shared/presenter'
3537
import { createWindowClient } from '@api/WindowClient'
@@ -57,14 +59,16 @@ const { isWinMacOS } = useDeviceVersion()
5759
const themeStore = useThemeStore()
5860
const langStore = useLanguageStore()
5961
const modelCheckStore = useModelCheckStore()
62+
const providerStore = useProviderStore()
63+
const modelStore = useModelStore()
6064
const { t } = useI18n()
6165
const toasterTheme = computed(() =>
6266
themeStore.themeMode === 'system' ? (themeStore.isDark ? 'dark' : 'light') : themeStore.themeMode
6367
)
6468
// Error notification queue and currently displayed error
6569
const errorQueue = ref<Array<{ id: string; title: string; message: string; type: string }>>([])
6670
const currentErrorId = ref<string | null>(null)
67-
const errorDisplayTimer = ref<number | null>(null)
71+
let errorDisplayTimer: number | null = null
6872
6973
const { setup: setupMcpDeeplink, cleanup: cleanupMcpDeeplink } = useMcpInstallDeeplinkHandler()
7074
@@ -90,7 +94,6 @@ watch(
9094
([themeMode, isDark, fontSizeClass]) => {
9195
const nextThemeName = resolveThemeName(themeMode, isDark)
9296
syncAppearanceClasses(nextThemeName, fontSizeClass)
93-
console.log('newTheme', nextThemeName)
9497
},
9598
{ immediate: true }
9699
)
@@ -133,12 +136,11 @@ const displayError = (error: { id: string; title: string; message: string; type:
133136
})
134137
135138
// Set timer to automatically close current error after 3 seconds
136-
if (errorDisplayTimer.value) {
137-
clearTimeout(errorDisplayTimer.value)
139+
if (errorDisplayTimer) {
140+
clearTimeout(errorDisplayTimer)
138141
}
139142
140-
errorDisplayTimer.value = window.setTimeout(() => {
141-
console.log('errorDisplayTimer.value', errorDisplayTimer.value)
143+
errorDisplayTimer = window.setTimeout(() => {
142144
// Handle logic after error is closed
143145
dismiss()
144146
handleErrorClosed()
@@ -158,9 +160,9 @@ const handleErrorClosed = () => {
158160
}
159161
} else {
160162
// Queue is empty, clear timer
161-
if (errorDisplayTimer.value) {
162-
clearTimeout(errorDisplayTimer.value)
163-
errorDisplayTimer.value = null
163+
if (errorDisplayTimer) {
164+
clearTimeout(errorDisplayTimer)
165+
errorDisplayTimer = null
164166
}
165167
}
166168
}
@@ -392,9 +394,10 @@ onMounted(() => {
392394
// Ensure icons are loaded (load asynchronously, can happen in parallel with store init)
393395
void ensureIconsLoaded()
394396
395-
// Start all critical data loads in parallel, don't wait for them
396-
// This way session data starts loading much earlier instead of waiting for initAppStores() to complete
397+
// Start shell-critical data directly from the main window so it does not depend on settings.
397398
void initAppStores()
399+
void providerStore.ensureInitialized()
400+
void modelStore.initialize()
398401
void sessionStore.fetchSessions()
399402
setupMcpDeeplink()
400403
setupAppIpcRuntime()
@@ -434,9 +437,9 @@ onMounted(() => {
434437
435438
// Clear timers and event listeners before component unmounts
436439
onBeforeUnmount(() => {
437-
if (errorDisplayTimer.value) {
438-
clearTimeout(errorDisplayTimer.value)
439-
errorDisplayTimer.value = null
440+
if (errorDisplayTimer) {
441+
clearTimeout(errorDisplayTimer)
442+
errorDisplayTimer = null
440443
}
441444
442445
window.removeEventListener('keydown', handleEscKey)

src/renderer/src/assets/style.css

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,18 @@
110110
--radius-md: calc(var(--radius) - 2px);
111111
--radius-sm: calc(var(--radius) - 4px);
112112

113-
--animation-accordion-down: accordion-down 0.2s ease-out;
114-
--animation-accordion-up: accordion-up 0.2s ease-out;
115-
--animation-collapsible-down: collapsible-down 0.2s ease-in-out;
116-
--animation-collapsible-up: collapsible-up 0.2s ease-in-out;
117-
--animation-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
113+
--animation-accordion-down: accordion-down var(--dc-motion-fast) var(--dc-ease-out-soft);
114+
--animation-accordion-up: accordion-up var(--dc-motion-fast) var(--dc-ease-out-soft);
115+
--animation-collapsible-down: collapsible-down var(--dc-motion-default)
116+
var(--dc-ease-out-express);
117+
--animation-collapsible-up: collapsible-up var(--dc-motion-default) var(--dc-ease-out-express);
118+
--animation-pulse: pulse 2.4s var(--dc-ease-out-soft) infinite;
119+
120+
--dc-motion-fast: 140ms;
121+
--dc-motion-default: 220ms;
122+
--dc-motion-slow: 320ms;
123+
--dc-ease-out-express: cubic-bezier(0.16, 1, 0.3, 1);
124+
--dc-ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1);
118125

119126
--display-weight: 700;
120127
--text-weight: 400;
@@ -833,13 +840,36 @@
833840
}
834841

835842
body {
836-
/* background-color: var(--background); */
837843
color: var(--foreground);
838844
font-weight: var(--text-weight);
845+
transition:
846+
color var(--dc-motion-default) var(--dc-ease-out-soft),
847+
background-color var(--dc-motion-default) var(--dc-ease-out-soft);
839848
}
840849

841850
html {
842851
font-family: var(--dc-font-family);
852+
text-rendering: optimizeLegibility;
853+
-webkit-font-smoothing: antialiased;
854+
scroll-behavior: smooth;
855+
transition:
856+
color var(--dc-motion-default) var(--dc-ease-out-soft),
857+
background-color var(--dc-motion-default) var(--dc-ease-out-soft);
858+
}
859+
860+
@media (prefers-reduced-motion: reduce) {
861+
html {
862+
scroll-behavior: auto;
863+
}
864+
865+
*,
866+
*::before,
867+
*::after {
868+
scroll-behavior: auto !important;
869+
transition-duration: 1ms !important;
870+
animation-duration: 1ms !important;
871+
animation-iteration-count: 1 !important;
872+
}
843873
}
844874

845875
code,

src/renderer/src/components/WindowSideBar.vue

Lines changed: 74 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
<TooltipProvider :delay-duration="200">
33
<div
44
data-testid="window-sidebar"
5-
class="flex flex-row h-full shrink-0 window-drag-region transition-all duration-200"
5+
class="window-sidebar-shell flex flex-row h-full shrink-0 overflow-hidden window-drag-region transition-[width] duration-[var(--dc-motion-default)] ease-[var(--dc-ease-out-express)]"
66
:class="collapsed ? 'w-12' : 'w-[288px]'"
77
>
88
<!-- Left Column: Agent Icons (48px) -->
9-
<div class="flex flex-col items-center shrink-0 pt-2 pb-2 gap-1 w-12">
9+
<div class="window-no-drag-region flex flex-col items-center shrink-0 pt-2 pb-2 gap-1 w-12">
1010
<!-- All agents button -->
1111
<Tooltip>
1212
<TooltipTrigger as-child>
@@ -129,9 +129,13 @@
129129

130130
<!-- Right Column: Session List (240px) -->
131131
<div
132-
v-show="!collapsed"
133132
data-testid="window-sidebar-session-column"
134-
class="window-no-drag-region flex flex-col w-0 flex-1 min-w-0"
133+
class="window-sidebar-session-column window-no-drag-region flex flex-col w-0 flex-1 min-w-0 transition-[opacity,transform] duration-[var(--dc-motion-default)] ease-[var(--dc-ease-out-express)]"
134+
:class="
135+
collapsed ? 'pointer-events-none translate-x-1.5 opacity-0' : 'translate-x-0 opacity-100'
136+
"
137+
:aria-hidden="collapsed ? 'true' : undefined"
138+
:inert="collapsed ? true : undefined"
135139
>
136140
<!-- Header -->
137141
<div class="flex items-center justify-between px-3 h-10 shrink-0">
@@ -246,7 +250,7 @@
246250
<div
247251
ref="sessionListRef"
248252
class="session-list flex-1 overflow-y-auto px-1.5"
249-
@scroll="handleSessionListScroll"
253+
@scroll.passive="handleSessionListScroll"
250254
>
251255
<div v-if="pinnedSessions.length > 0" class="pt-2">
252256
<button
@@ -267,24 +271,22 @@
267271
</span>
268272
</button>
269273

270-
<Transition name="sidebar-group-collapse">
271-
<div v-if="!isPinnedSectionCollapsed" class="space-y-0.5">
272-
<WindowSideBarSessionItem
273-
v-for="session in pinnedSessions"
274-
:key="`pinned-${session.id}`"
275-
:session="session"
276-
:active="sessionStore.activeSessionId === session.id"
277-
region="pinned"
278-
:hero-hidden="pinFlightSessionId === session.id"
279-
:force-pin-docked="pinDockedSessionId === session.id"
280-
:pin-feedback-mode="pinFeedbackSessionId === session.id ? pinFeedbackMode : null"
281-
:search-query="sessionSearchQuery"
282-
@select="handleSessionClick"
283-
@toggle-pin="handleTogglePin"
284-
@delete="openDeleteDialog"
285-
/>
286-
</div>
287-
</Transition>
274+
<div v-show="!isPinnedSectionCollapsed" class="space-y-0.5">
275+
<WindowSideBarSessionItem
276+
v-for="session in pinnedSessions"
277+
:key="`pinned-${session.id}`"
278+
:session="session"
279+
:active="sessionStore.activeSessionId === session.id"
280+
region="pinned"
281+
:hero-hidden="pinFlightSessionId === session.id"
282+
:force-pin-docked="pinDockedSessionId === session.id"
283+
:pin-feedback-mode="pinFeedbackSessionId === session.id ? pinFeedbackMode : null"
284+
:search-query="sessionSearchQuery"
285+
@select="handleSessionClick"
286+
@toggle-pin="handleTogglePin"
287+
@delete="openDeleteDialog"
288+
/>
289+
</div>
288290
</div>
289291

290292
<template v-for="group in filteredGroups" :key="getGroupIdentifier(group)">
@@ -305,24 +307,22 @@
305307
{{ getGroupLabel(group) }}
306308
</span>
307309
</button>
308-
<Transition name="sidebar-group-collapse">
309-
<div v-if="!isGroupCollapsed(group)" class="space-y-0.5">
310-
<WindowSideBarSessionItem
311-
v-for="session in group.sessions"
312-
:key="session.id"
313-
:session="session"
314-
:active="sessionStore.activeSessionId === session.id"
315-
region="grouped"
316-
:hero-hidden="pinFlightSessionId === session.id"
317-
:force-pin-docked="pinDockedSessionId === session.id"
318-
:pin-feedback-mode="pinFeedbackSessionId === session.id ? pinFeedbackMode : null"
319-
:search-query="sessionSearchQuery"
320-
@select="handleSessionClick"
321-
@toggle-pin="handleTogglePin"
322-
@delete="openDeleteDialog"
323-
/>
324-
</div>
325-
</Transition>
310+
<div v-show="!isGroupCollapsed(group)" class="space-y-0.5">
311+
<WindowSideBarSessionItem
312+
v-for="session in group.sessions"
313+
:key="session.id"
314+
:session="session"
315+
:active="sessionStore.activeSessionId === session.id"
316+
region="grouped"
317+
:hero-hidden="pinFlightSessionId === session.id"
318+
:force-pin-docked="pinDockedSessionId === session.id"
319+
:pin-feedback-mode="pinFeedbackSessionId === session.id ? pinFeedbackMode : null"
320+
:search-query="sessionSearchQuery"
321+
@select="handleSessionClick"
322+
@toggle-pin="handleTogglePin"
323+
@delete="openDeleteDialog"
324+
/>
325+
</div>
326326
</template>
327327

328328
<div
@@ -471,6 +471,7 @@ let agentSwitchSeq = 0
471471
let agentSwitchQueue: Promise<void> = Promise.resolve()
472472
let remoteControlStatusTimer: ReturnType<typeof setInterval> | null = null
473473
let pinFeedbackTimer: number | null = null
474+
let sessionListScrollFrame: number | null = null
474475
const sidebarSelectedAgentId = computed(() => {
475476
const activeSessionAgentId = sessionStore.activeSession?.agentId?.trim()
476477
if (sessionStore.hasActiveSession && activeSessionAgentId) {
@@ -821,7 +822,7 @@ const restoreSessionListScroll = (scrollTop: number | null) => {
821822
sessionListRef.value.scrollTop = scrollTop
822823
}
823824
824-
const handleSessionListScroll = () => {
825+
const performSessionListScrollCheck = () => {
825826
const listElement = sessionListRef.value
826827
if (!listElement || sessionStore.loadingMore || !sessionStore.hasMore) {
827828
return
@@ -835,6 +836,17 @@ const handleSessionListScroll = () => {
835836
}
836837
}
837838
839+
const handleSessionListScroll = () => {
840+
if (sessionListScrollFrame !== null) {
841+
return
842+
}
843+
844+
sessionListScrollFrame = window.requestAnimationFrame(() => {
845+
sessionListScrollFrame = null
846+
performSessionListScrollCheck()
847+
})
848+
}
849+
838850
const getSessionItemElement = (sessionId: string, region: 'pinned' | 'grouped') =>
839851
document.querySelector<HTMLElement>(
840852
`.session-item[data-session-id="${sessionId}"][data-session-region="${region}"]`
@@ -1001,6 +1013,11 @@ onUnmounted(() => {
10011013
remoteControlStatusTimer = null
10021014
}
10031015
1016+
if (sessionListScrollFrame !== null) {
1017+
window.cancelAnimationFrame(sessionListScrollFrame)
1018+
sessionListScrollFrame = null
1019+
}
1020+
10041021
pinFlightSessionId.value = null
10051022
pinDockedSessionId.value = null
10061023
clearPinFeedback()
@@ -1016,6 +1033,16 @@ onUnmounted(() => {
10161033
-webkit-app-region: no-drag;
10171034
}
10181035
1036+
.window-sidebar-shell {
1037+
contain: layout style paint;
1038+
}
1039+
1040+
.window-sidebar-session-column {
1041+
backface-visibility: hidden;
1042+
transform: translateZ(0);
1043+
will-change: transform, opacity;
1044+
}
1045+
10191046
.session-list {
10201047
overflow-anchor: none;
10211048
}
@@ -1046,26 +1073,10 @@ input {
10461073
margin-left: var(--pin-text-shift) !important;
10471074
}
10481075
1049-
.sidebar-group-collapse-enter-active,
1050-
.sidebar-group-collapse-leave-active {
1051-
overflow: hidden;
1052-
transition:
1053-
max-height 180ms ease,
1054-
opacity 160ms ease,
1055-
transform 180ms ease;
1056-
}
1057-
1058-
.sidebar-group-collapse-enter-from,
1059-
.sidebar-group-collapse-leave-to {
1060-
max-height: 0;
1061-
opacity: 0;
1062-
transform: translateY(-4px);
1063-
}
1064-
1065-
.sidebar-group-collapse-enter-to,
1066-
.sidebar-group-collapse-leave-from {
1067-
max-height: 720px;
1068-
opacity: 1;
1069-
transform: translateY(0);
1076+
@media (prefers-reduced-motion: reduce) {
1077+
.window-sidebar-shell,
1078+
.window-sidebar-session-column {
1079+
transition: none;
1080+
}
10701081
}
10711082
</style>

0 commit comments

Comments
 (0)