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

This commit is contained in:
David Hill 2026-03-13 15:08:23 +00:00 committed by GitHub
parent c7a52b6a2d
commit 536abea2e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 65 additions and 106 deletions

View File

@ -16,9 +16,11 @@ import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { useServer } from "@/context/server" import { useServer } from "@/context/server"
import { useSync } from "@/context/sync"
import { useTerminal } from "@/context/terminal" import { useTerminal } from "@/context/terminal"
import { focusTerminalById } from "@/pages/session/helpers" import { focusTerminalById } from "@/pages/session/helpers"
import { useSessionLayout } from "@/pages/session/session-layout" import { useSessionLayout } from "@/pages/session/session-layout"
import { messageAgentColor } from "@/utils/agent"
import { decode64 } from "@/utils/base64" import { decode64 } from "@/utils/base64"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import { StatusPopover } from "../status-popover" import { StatusPopover } from "../status-popover"
@ -132,6 +134,7 @@ export function SessionHeader() {
const server = useServer() const server = useServer()
const platform = usePlatform() const platform = usePlatform()
const language = useLanguage() const language = useLanguage()
const sync = useSync()
const terminal = useTerminal() const terminal = useTerminal()
const { params, view } = useSessionLayout() const { params, view } = useSessionLayout()
@ -218,6 +221,9 @@ export function SessionHeader() {
({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const), ({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const),
) )
const opening = createMemo(() => openRequest.app !== undefined) 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) => { const selectApp = (app: OpenApp) => {
if (!options().some((item) => item.id === app)) return 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"> <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} />}> <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> </Show>
</div> </div>
<span class="text-12-regular text-text-strong">{language.t("common.open")}</span> <span class="text-12-regular text-text-strong">{language.t("common.open")}</span>

View File

@ -9,14 +9,13 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
import { base64Encode } from "@opencode-ai/util/encode" import { base64Encode } from "@opencode-ai/util/encode"
import { getFilename } from "@opencode-ai/util/path" import { getFilename } from "@opencode-ai/util/path"
import { A, useNavigate, useParams } from "@solidjs/router" import { A, useNavigate, useParams } from "@solidjs/router"
import { type Accessor, createEffect, createMemo, For, type JSX, on, onCleanup, Show } from "solid-js" import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
import { createStore } from "solid-js/store"
import { useGlobalSync } from "@/context/global-sync" import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout" import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
import { useNotification } from "@/context/notification" import { useNotification } from "@/context/notification"
import { usePermission } from "@/context/permission" import { usePermission } from "@/context/permission"
import { agentColor } from "@/utils/agent" import { messageAgentColor } from "@/utils/agent"
import { sessionPermissionRequest } from "../session/composer/session-request-tree" import { sessionPermissionRequest } from "../session/composer/session-request-tree"
import { hasProjectPermissions } from "./helpers" import { hasProjectPermissions } from "./helpers"
@ -102,94 +101,46 @@ const SessionRow = (props: {
warmPress: () => void warmPress: () => void
warmFocus: () => void warmFocus: () => void
cancelHoverPrefetch: () => void cancelHoverPrefetch: () => void
}): JSX.Element => { }): JSX.Element => (
const [slot, setSlot] = createStore({ <A
open: false, href={`/${props.slug}/session/${props.session.id}`}
show: false, 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"}`}
fade: false, onPointerDown={props.warmPress}
}) onPointerEnter={props.warmHover}
onPointerLeave={props.cancelHoverPrefetch}
let f: number | undefined onFocus={props.warmFocus}
const clear = () => { onClick={() => {
if (f !== undefined) window.clearTimeout(f) props.setHoverSession(undefined)
f = undefined if (props.sidebarOpened()) return
} props.clearHoverProjectSoon()
}}
onCleanup(clear) >
createEffect( <div class="flex items-center gap-1 w-full">
on( <div
() => props.isWorking(), class="shrink-0 size-6 flex items-center justify-center"
(on, prev) => { style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
clear() >
if (on) { <Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
setSlot({ open: true, show: true, fade: false }) <Match when={props.isWorking()}>
return <Spinner class="size-[15px]" />
} </Match>
if (prev) { <Match when={props.hasPermissions()}>
setSlot({ open: false, show: true, fade: true }) <div class="size-1.5 rounded-full bg-surface-warning-strong" />
f = window.setTimeout(() => setSlot({ show: false, fade: false }), 260) </Match>
return <Match when={props.hasError()}>
} <div class="size-1.5 rounded-full bg-text-diff-delete-base" />
setSlot({ open: false, show: false, fade: false }) </Match>
}, <Match when={props.unseenCount() > 0}>
{ defer: true }, <div class="size-1.5 rounded-full bg-text-interactive-base" />
), </Match>
) </Switch>
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>
</div> </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: { const SessionHoverPreview = (props: {
mobile?: boolean mobile?: boolean
@ -268,19 +219,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
}) })
const tint = createMemo(() => { const tint = createMemo(() => {
const messages = sessionStore.message[props.session.id] return messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent)
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)
}) })
const hoverMessages = createMemo(() => const hoverMessages = createMemo(() =>
@ -359,7 +298,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
return ( return (
<div <div
data-session-id={props.session.id} 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" 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 <Show

View File

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

View File

@ -9,3 +9,15 @@ export function agentColor(name: string, custom?: string) {
if (custom) return custom if (custom) return custom
return defaults[name] ?? defaults[name.toLowerCase()] 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)
}
}