From 892002a19af1d35a7e72944727339cdcbec95912 Mon Sep 17 00:00:00 2001 From: Alex Yaroshuk Date: Thu, 12 Feb 2026 04:06:07 +0800 Subject: [PATCH 1/2] feat(app): marquee scroll for overflowing sidebar titles --- .../app/src/pages/layout/sidebar-items.tsx | 55 ++++++++++++++++--- packages/ui/src/styles/tailwind/utilities.css | 13 +++++ 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index 678bfa0d86da..5a2134d40c2d 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -14,7 +14,7 @@ import { Spinner } from "@opencode-ai/ui/spinner" import { Tooltip } from "@opencode-ai/ui/tooltip" import { getFilename } from "@opencode-ai/util/path" import { type Message, type Session, type TextPart } from "@opencode-ai/sdk/v2/client" -import { For, Match, Show, Switch, createMemo, onCleanup, type Accessor, type JSX } from "solid-js" +import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, type Accessor, type JSX } from "solid-js" import { agentColor } from "@/utils/agent" const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" @@ -142,14 +142,44 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { return text?.text } + const [itemHovered, setItemHovered] = createSignal(false) + const [overflows, setOverflows] = createSignal(false) + const marquee = createMemo(() => itemHovered() && overflows()) + + let titleRef: HTMLSpanElement | undefined + let containerRef: HTMLDivElement | undefined + createEffect(() => { + if (!titleRef || !containerRef || !itemHovered()) return + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (!titleRef || !containerRef) return + const titleWidth = titleRef.scrollWidth + const containerWidth = containerRef.offsetWidth + if (titleWidth <= containerWidth) { + setOverflows(false) + return + } + const overflowWidth = titleWidth - containerWidth + 8 + const duration = overflowWidth / 40 + titleRef.style.setProperty("--overflow-width", `${overflowWidth}px`) + titleRef.style.setProperty("--marquee-duration", `${duration}s`) + setOverflows(true) + }) + }) + }) + const item = ( { + setItemHovered(true) + scheduleHoverPrefetch() + }} + onPointerLeave={() => { + setItemHovered(false) + cancelHoverPrefetch() + }} onFocus={() => props.prefetchSession(props.session, "high")} onClick={() => { props.setHoverSession(undefined) @@ -177,9 +207,18 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { - - {props.session.title} - +
+ + {props.session.title} + +
{(summary) => (
diff --git a/packages/ui/src/styles/tailwind/utilities.css b/packages/ui/src/styles/tailwind/utilities.css index be305b4cbce3..9417cd97fa64 100644 --- a/packages/ui/src/styles/tailwind/utilities.css +++ b/packages/ui/src/styles/tailwind/utilities.css @@ -49,6 +49,19 @@ text-align: left; } +@utility marquee { + animation: marquee var(--marquee-duration, 2s) linear infinite alternate; +} + +@keyframes marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(calc(-1 * var(--overflow-width, 0px))); + } +} + @utility fade-up-text { animation: fadeUp 0.4s ease-out forwards; opacity: 0; From 92122c9d654adbef58dcc48d379f4e4179dccb02 Mon Sep 17 00:00:00 2001 From: Alex Yaroshuk Date: Fri, 13 Feb 2026 19:29:56 +0800 Subject: [PATCH 2/2] resolve conflicts --- .../app/src/pages/layout/sidebar-items.tsx | 188 ++++++++++++++---- 1 file changed, 147 insertions(+), 41 deletions(-) diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index 5a2134d40c2d..408156204473 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -13,8 +13,19 @@ import { MessageNav } from "@opencode-ai/ui/message-nav" import { Spinner } from "@opencode-ai/ui/spinner" import { Tooltip } from "@opencode-ai/ui/tooltip" import { getFilename } from "@opencode-ai/util/path" -import { type Message, type Session, type TextPart } from "@opencode-ai/sdk/v2/client" -import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, type Accessor, type JSX } from "solid-js" +import { type Message, type Session, type TextPart, type UserMessage } from "@opencode-ai/sdk/v2/client" +import { + For, + Match, + Show, + Switch, + createEffect, + createMemo, + createSignal, + onCleanup, + type Accessor, + type JSX, +} from "solid-js" import { agentColor } from "@/utils/agent" const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" @@ -70,6 +81,116 @@ export type SessionItemProps = { archiveSession: (session: Session) => Promise } +const SessionRow = (props: { + session: Session + slug: string + mobile?: boolean + dense?: boolean + tint: Accessor + isWorking: Accessor + hasPermissions: Accessor + hasError: Accessor + unseenCount: Accessor + setHoverSession: (id: string | undefined) => void + clearHoverProjectSoon: () => void + sidebarOpened: Accessor + prefetchSession: (session: Session, priority?: "high" | "low") => void + scheduleHoverPrefetch: () => void + cancelHoverPrefetch: () => void +}): JSX.Element => ( + props.prefetchSession(props.session, "high")} + onClick={() => { + props.setHoverSession(undefined) + if (props.sidebarOpened()) return + props.clearHoverProjectSoon() + }} + > +
+
+ }> + + + + +
+ + +
+ + 0}> +
+ + +
+ + {props.session.title} + + + {(summary) => ( +
+ +
+ )} +
+
+
+) + +const SessionHoverPreview = (props: { + mobile?: boolean + nav: Accessor + hoverSession: Accessor + session: Session + sidebarHovering: Accessor + hoverReady: Accessor + hoverMessages: Accessor + language: ReturnType + isActive: Accessor + slug: string + setHoverSession: (id: string | undefined) => void + messageLabel: (message: Message) => string | undefined + onMessageSelect: (message: Message) => void + trigger: JSX.Element +}): JSX.Element => ( + props.setHoverSession(open ? props.session.id : undefined)} + > + {props.language.t("session.messages.loading")}
} + > +
+ +
+ + +) + export const SessionItem = (props: SessionItemProps): JSX.Element => { const params = useParams() const navigate = useNavigate() @@ -113,7 +234,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { }) const hoverMessages = createMemo(() => - sessionStore.message[props.session.id]?.filter((message) => message.role === "user"), + sessionStore.message[props.session.id]?.filter((message): message is UserMessage => message.role === "user"), ) const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined) const hoverAllowed = createMemo(() => !props.mobile && props.sidebarExpanded()) @@ -141,7 +262,6 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { const text = parts.find((part): part is TextPart => part?.type === "text" && !part.synthetic && !part.ignored) return text?.text } - const [itemHovered, setItemHovered] = createSignal(false) const [overflows, setOverflows] = createSignal(false) const marquee = createMemo(() => itemHovered() && overflows()) @@ -244,44 +364,30 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { } > - { + if (!isActive()) { + layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id) + navigate(`${props.slug}/session/${props.session.id}`) + return + } + window.history.replaceState(null, "", `#message-${message.id}`) + window.dispatchEvent(new HashChangeEvent("hashchange")) + }} trigger={item} - mount={!props.mobile ? props.nav() : undefined} - open={props.hoverSession() === props.session.id} - onOpenChange={(open) => props.setHoverSession(open ? props.session.id : undefined)} - > - {language.t("session.messages.loading")}
} - > -
- { - if (!isActive()) { - layout.pendingMessage.set( - `${base64Encode(props.session.directory)}/${props.session.id}`, - message.id, - ) - navigate(`${props.slug}/session/${props.session.id}`) - return - } - window.history.replaceState(null, "", `#message-${message.id}`) - window.dispatchEvent(new HashChangeEvent("hashchange")) - }} - size="normal" - class="w-60" - /> -
- - + />