Преглед изворни кода

fix(app): restore sidebar dash and sync session spinner colors (#17384)

David Hill пре 1 месец
родитељ
комит
536abea2e2

+ 7 - 1
packages/app/src/components/session/session-header.tsx

@@ -16,9 +16,11 @@ import { useLanguage } from "@/context/language"
 import { useLayout } from "@/context/layout"
 import { usePlatform } from "@/context/platform"
 import { useServer } from "@/context/server"
+import { useSync } from "@/context/sync"
 import { useTerminal } from "@/context/terminal"
 import { focusTerminalById } from "@/pages/session/helpers"
 import { useSessionLayout } from "@/pages/session/session-layout"
+import { messageAgentColor } from "@/utils/agent"
 import { decode64 } from "@/utils/base64"
 import { Persist, persisted } from "@/utils/persist"
 import { StatusPopover } from "../status-popover"
@@ -132,6 +134,7 @@ export function SessionHeader() {
   const server = useServer()
   const platform = usePlatform()
   const language = useLanguage()
+  const sync = useSync()
   const terminal = useTerminal()
   const { params, view } = useSessionLayout()
 
@@ -218,6 +221,9 @@ export function SessionHeader() {
       ({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const),
   )
   const opening = createMemo(() => openRequest.app !== undefined)
+  const tint = createMemo(() =>
+    messageAgentColor(params.id ? sync.data.message[params.id] : undefined, sync.data.agent),
+  )
 
   const selectApp = (app: OpenApp) => {
     if (!options().some((item) => item.id === app)) return
@@ -330,7 +336,7 @@ export function SessionHeader() {
                         >
                           <div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
                             <Show when={opening()} fallback={<AppIcon id={current().icon} />}>
-                              <Spinner class="size-3.5 text-icon-base" />
+                              <Spinner class="size-3.5" style={{ color: tint() ?? "var(--icon-base)" }} />
                             </Show>
                           </div>
                           <span class="text-12-regular text-text-strong">{language.t("common.open")}</span>

+ 43 - 104
packages/app/src/pages/layout/sidebar-items.tsx

@@ -9,14 +9,13 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
 import { base64Encode } from "@opencode-ai/util/encode"
 import { getFilename } from "@opencode-ai/util/path"
 import { A, useNavigate, useParams } from "@solidjs/router"
-import { type Accessor, createEffect, createMemo, For, type JSX, on, onCleanup, Show } from "solid-js"
-import { createStore } from "solid-js/store"
+import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
 import { useGlobalSync } from "@/context/global-sync"
 import { useLanguage } from "@/context/language"
 import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
 import { useNotification } from "@/context/notification"
 import { usePermission } from "@/context/permission"
-import { agentColor } from "@/utils/agent"
+import { messageAgentColor } from "@/utils/agent"
 import { sessionPermissionRequest } from "../session/composer/session-request-tree"
 import { hasProjectPermissions } from "./helpers"
 
@@ -102,94 +101,46 @@ const SessionRow = (props: {
   warmPress: () => void
   warmFocus: () => void
   cancelHoverPrefetch: () => void
-}): JSX.Element => {
-  const [slot, setSlot] = createStore({
-    open: false,
-    show: false,
-    fade: false,
-  })
-
-  let f: number | undefined
-  const clear = () => {
-    if (f !== undefined) window.clearTimeout(f)
-    f = undefined
-  }
-
-  onCleanup(clear)
-  createEffect(
-    on(
-      () => props.isWorking(),
-      (on, prev) => {
-        clear()
-        if (on) {
-          setSlot({ open: true, show: true, fade: false })
-          return
-        }
-        if (prev) {
-          setSlot({ open: false, show: true, fade: true })
-          f = window.setTimeout(() => setSlot({ show: false, fade: false }), 260)
-          return
-        }
-        setSlot({ open: false, show: false, fade: false })
-      },
-      { defer: true },
-    ),
-  )
-
-  return (
-    <A
-      href={`/${props.slug}/session/${props.session.id}`}
-      class={`relative flex items-center min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
-      onPointerDown={props.warmPress}
-      onPointerEnter={props.warmHover}
-      onPointerLeave={props.cancelHoverPrefetch}
-      onFocus={props.warmFocus}
-      onClick={() => {
-        props.setHoverSession(undefined)
-        if (props.sidebarOpened()) return
-        props.clearHoverProjectSoon()
-      }}
-    >
-      <Show when={!props.isWorking() && (props.hasPermissions() || props.hasError() || props.unseenCount() > 0)}>
-        <div
-          classList={{
-            "absolute left-0 top-1/2 -translate-y-1/2 size-1.5 rounded-full": true,
-            "bg-surface-warning-strong": props.hasPermissions(),
-            "bg-text-diff-delete-base": !props.hasPermissions() && props.hasError(),
-            "bg-text-interactive-base": !props.hasPermissions() && !props.hasError() && props.unseenCount() > 0,
-          }}
-          aria-hidden="true"
-        />
-      </Show>
-
-      <div class="flex items-center min-w-0 grow-1">
-        <div
-          class="shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
-          style={{
-            width: slot.open ? "16px" : "0px",
-            "margin-right": slot.open ? "8px" : "0px",
-          }}
-          aria-hidden="true"
-        >
-          <Show when={slot.show}>
-            <div
-              class="transition-opacity duration-200 ease-out"
-              classList={{
-                "opacity-0": slot.fade,
-              }}
-            >
-              <Spinner class="size-4" style={{ color: props.tint() ?? "var(--icon-interactive-base)" }} />
-            </div>
-          </Show>
-        </div>
-
-        <span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
-          {props.session.title}
-        </span>
+}): JSX.Element => (
+  <A
+    href={`/${props.slug}/session/${props.session.id}`}
+    class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
+    onPointerDown={props.warmPress}
+    onPointerEnter={props.warmHover}
+    onPointerLeave={props.cancelHoverPrefetch}
+    onFocus={props.warmFocus}
+    onClick={() => {
+      props.setHoverSession(undefined)
+      if (props.sidebarOpened()) return
+      props.clearHoverProjectSoon()
+    }}
+  >
+    <div class="flex items-center gap-1 w-full">
+      <div
+        class="shrink-0 size-6 flex items-center justify-center"
+        style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
+      >
+        <Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
+          <Match when={props.isWorking()}>
+            <Spinner class="size-[15px]" />
+          </Match>
+          <Match when={props.hasPermissions()}>
+            <div class="size-1.5 rounded-full bg-surface-warning-strong" />
+          </Match>
+          <Match when={props.hasError()}>
+            <div class="size-1.5 rounded-full bg-text-diff-delete-base" />
+          </Match>
+          <Match when={props.unseenCount() > 0}>
+            <div class="size-1.5 rounded-full bg-text-interactive-base" />
+          </Match>
+        </Switch>
       </div>
-    </A>
-  )
-}
+      <span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
+        {props.session.title}
+      </span>
+    </div>
+  </A>
+)
 
 const SessionHoverPreview = (props: {
   mobile?: boolean
@@ -268,19 +219,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
   })
 
   const tint = createMemo(() => {
-    const messages = sessionStore.message[props.session.id]
-    if (!messages) return undefined
-    let user: Message | undefined
-    for (let i = messages.length - 1; i >= 0; i--) {
-      const message = messages[i]
-      if (message.role !== "user") continue
-      user = message
-      break
-    }
-    if (!user?.agent) return undefined
-
-    const agent = sessionStore.agent.find((a) => a.name === user.agent)
-    return agentColor(user.agent, agent?.color)
+    return messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent)
   })
 
   const hoverMessages = createMemo(() =>
@@ -359,7 +298,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
   return (
     <div
       data-session-id={props.session.id}
-      class="group/session relative w-full rounded-md cursor-default pl-3 pr-3 transition-colors
+      class="group/session relative w-full rounded-md cursor-default pl-2 pr-3 transition-colors
              hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
     >
       <Show

+ 3 - 1
packages/app/src/pages/session/message-timeline.tsx

@@ -27,6 +27,7 @@ import { usePlatform } from "@/context/platform"
 import { useSettings } from "@/context/settings"
 import { useSDK } from "@/context/sdk"
 import { useSync } from "@/context/sync"
+import { messageAgentColor } from "@/utils/agent"
 import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
 
 type MessageComment = {
@@ -246,6 +247,7 @@ export function MessageTimeline(props: {
     return sync.data.session_status[id] ?? idle
   })
   const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
+  const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
 
   const [slot, setSlot] = createStore({
     open: false,
@@ -689,7 +691,7 @@ export function MessageTimeline(props: {
                               "opacity-0": slot.fade,
                             }}
                           >
-                            <Spinner class="size-4" style={{ color: "var(--icon-interactive-base)" }} />
+                            <Spinner class="size-4" style={{ color: tint() ?? "var(--icon-interactive-base)" }} />
                           </div>
                         </Show>
                       </div>

+ 12 - 0
packages/app/src/utils/agent.ts

@@ -9,3 +9,15 @@ export function agentColor(name: string, custom?: string) {
   if (custom) return custom
   return defaults[name] ?? defaults[name.toLowerCase()]
 }
+
+export function messageAgentColor(
+  list: readonly { role: string; agent?: string }[] | undefined,
+  agents: readonly { name: string; color?: string }[],
+) {
+  if (!list) return undefined
+  for (let i = list.length - 1; i >= 0; i--) {
+    const item = list[i]
+    if (item.role !== "user" || !item.agent) continue
+    return agentColor(item.agent, agents.find((agent) => agent.name === item.agent)?.color)
+  }
+}