Просмотр исходного кода

feat(desktop): refine session tab preview popover (#34792)

usrnk1 2 месяцев назад
Родитель
Сommit
917eca4f55

+ 0 - 6
packages/app/src/components/titlebar-tab-nav.tsx

@@ -71,11 +71,6 @@ export function TabNavItem(props: {
     const home = serverCtx()?.sync.data.path.home
     return home ? session.directory.replace(home, "~") : session.directory
   })
-  const branch = createMemo(() => {
-    const session = props.session()
-    if (!session) return
-    return serverCtx()?.sync.child(session.directory, { bootstrap: false })[0].vcs?.branch
-  })
   // Only label the server when multiple servers are connected.
   const serverLabel = createMemo(() => {
     if (global.servers.list().length <= 1) return
@@ -322,7 +317,6 @@ export function TabNavItem(props: {
         projectName: projectName(),
         title: props.session()?.title,
         path: previewPath(),
-        branch: branch(),
         serverName: serverLabel(),
       }}
     />

+ 18 - 24
packages/app/src/components/titlebar-tab-popover.css

@@ -10,7 +10,7 @@
   width: 256px;
   display: flex;
   flex-direction: column;
-  gap: 8px;
+  gap: 6px;
   padding: 12px;
 
   background: var(--v2-background-bg-base);
@@ -23,30 +23,37 @@
   pointer-events: none;
 
   transform-origin: var(--kb-hovercard-content-transform-origin);
-  /* Entrance: fade in while scaling up from 0.96 to 1.0. */
-  animation: sessionTabPopoverIn 150ms cubic-bezier(0.16, 1, 0.3, 1);
+  /* Entrance: instant (trying no animation — may revert to 120ms ease-out). */
+  animation: none;
 
   &:focus-visible,
   &:focus-within {
     outline: none;
   }
 
-  /* Exit: subtle fade + scale down, using the same ease-out curve as entry. */
+  /* Exit: instant (trying no animation — may revert to 80ms ease-out). */
   &[data-closed] {
-    animation: sessionTabPopoverOut 100ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
+    animation: none;
+  }
+
+  /* Warm streak (rapid tab hopping): appear/disappear instantly so the enter
+     and exit animations don't repeat on every tab. */
+  &[data-instant],
+  &[data-instant][data-closed] {
+    animation: none;
   }
 
   [data-slot="header"] {
     display: flex;
     flex-direction: column;
-    gap: 8px;
+    gap: 6px;
     width: 100%;
   }
 
   [data-slot="project"] {
     font-weight: 440;
     font-size: 13px;
-    line-height: 1;
+    line-height: 1.5;
     letter-spacing: -0.04px;
     color: var(--v2-text-text-muted);
   }
@@ -54,7 +61,7 @@
   [data-slot="title"] {
     font-weight: 530;
     font-size: 13px;
-    line-height: 1.2;
+    line-height: 1.5;
     letter-spacing: -0.04px;
     color: var(--v2-text-text-base);
     overflow-wrap: anywhere;
@@ -62,31 +69,18 @@
 
   [data-slot="row"] {
     display: flex;
-    align-items: center;
-    gap: 6px;
     width: 100%;
     min-width: 0;
   }
 
-  [data-slot="icon"] {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    flex-shrink: 0;
-    width: 16px;
-    height: 16px;
-    color: var(--v2-icon-icon-muted);
-  }
-
   [data-slot="detail"] {
     flex: 1 0 0;
     min-width: 0;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
+    /* Wrap the full path onto the next line instead of clipping the end. */
+    overflow-wrap: anywhere;
     font-weight: 440;
     font-size: 13px;
-    line-height: 1;
+    line-height: 1.5;
     letter-spacing: -0.04px;
     color: var(--v2-text-text-muted);
   }

+ 11 - 18
packages/app/src/components/titlebar-tab-popover.tsx

@@ -1,6 +1,5 @@
 import { HoverCard as Kobalte } from "@kobalte/core/hover-card"
-import { Show, type JSXElement } from "solid-js"
-import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
+import { createSignal, Show, type JSXElement } from "solid-js"
 import "./titlebar-tab-popover.css"
 
 // Initial hover delay before the preview appears, per design.
@@ -10,14 +9,13 @@ const CLOSE_DELAY = 0
 // After a preview closes, hovering a neighbouring tab within this window skips
 // the open delay — mirrors the tooltip's skipDelayDuration so moving across
 // tabs doesn't re-wait the full delay each time.
-const SKIP_WINDOW = 300
+const SKIP_WINDOW = 500
 let lastClosedAt = 0
 
 export interface TabPreviewData {
   projectName?: string
   title?: string
   path?: string
-  branch?: string
   serverName?: string
 }
 
@@ -28,12 +26,18 @@ export function TabPreviewPopover(props: {
   data: TabPreviewData
 }) {
   let triggerEl: HTMLDivElement | undefined
+  // When opened during a rapid tab-hopping streak, this preview appears and
+  // disappears instantly (no repeated enter/exit animation) — only the first,
+  // "cold" preview animates. Mirrors how browsers reuse one tab tooltip.
+  const [instant, setInstant] = createSignal(false)
 
+  const warm = () => Date.now() - lastClosedAt < SKIP_WINDOW
   // Kobalte reads openDelay lazily when the pointer enters the trigger, so this
   // resolves the skip window per-hover.
-  const resolveOpenDelay = () => (Date.now() - lastClosedAt < SKIP_WINDOW ? 0 : OPEN_DELAY)
+  const resolveOpenDelay = () => (warm() ? 0 : OPEN_DELAY)
   const handleOpenChange = (open: boolean) => {
-    if (!open) lastClosedAt = Date.now()
+    if (open) setInstant(warm())
+    else lastClosedAt = Date.now()
     props.onOpenChange(open)
   }
 
@@ -61,6 +65,7 @@ export function TabPreviewPopover(props: {
             if (theme) el.setAttribute("data-theme", theme)
           }}
           data-component="session-tab-popover"
+          data-instant={instant() || undefined}
         >
           <div data-slot="header">
             <Show when={props.data.projectName}>
@@ -73,22 +78,10 @@ export function TabPreviewPopover(props: {
 
           <Show when={props.data.path}>
             <div data-slot="row">
-              <span data-slot="icon">
-                <IconV2 name="folder" />
-              </span>
               <span data-slot="detail">{props.data.path}</span>
             </div>
           </Show>
 
-          <Show when={props.data.branch}>
-            <div data-slot="row">
-              <span data-slot="icon">
-                <IconV2 name="branch" />
-              </span>
-              <span data-slot="detail">{props.data.branch}</span>
-            </div>
-          </Show>
-
           <Show when={props.data.serverName}>
             <div data-slot="server">{props.data.serverName}</div>
           </Show>