diff options
| -rw-r--r-- | packages/app/src/components/session/session-header.tsx | 8 | ||||
| -rw-r--r-- | packages/app/src/pages/layout/sidebar-items.tsx | 147 | ||||
| -rw-r--r-- | packages/app/src/pages/session/message-timeline.tsx | 4 | ||||
| -rw-r--r-- | packages/app/src/utils/agent.ts | 12 |
4 files changed, 65 insertions, 106 deletions
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index ae9d2800e..4c9e30e43 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/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> diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index 04d898134..5ce526103 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/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 diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index b4a740c60..74f2e8c2c 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/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> diff --git a/packages/app/src/utils/agent.ts b/packages/app/src/utils/agent.ts index 7c2c81e74..390932a13 100644 --- a/packages/app/src/utils/agent.ts +++ b/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) + } +} |
