summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/app/src/components/session/session-header.tsx8
-rw-r--r--packages/app/src/pages/layout/sidebar-items.tsx147
-rw-r--r--packages/app/src/pages/session/message-timeline.tsx4
-rw-r--r--packages/app/src/utils/agent.ts12
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)
+ }
+}