Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
892002a
feat(app): marquee scroll for overflowing sidebar titles
alexyaroshuk Feb 11, 2026
92122c9
resolve conflicts
alexyaroshuk Feb 13, 2026
3bac098
Merge branch 'dev' into feat/sidebar-title-marquee
alexyaroshuk Feb 13, 2026
4195c6a
Merge branch 'dev' into feat/sidebar-title-marquee
alexyaroshuk Feb 13, 2026
22c9959
Merge branch 'dev' into feat/sidebar-title-marquee
alexyaroshuk Feb 13, 2026
08d4d42
Merge remote-tracking branch 'upstream/dev' into feat/sidebar-title-m…
alexyaroshuk Feb 14, 2026
0134995
Merge branch 'dev' into feat/sidebar-title-marquee
alexyaroshuk Feb 14, 2026
ec51da4
Merge branch 'dev' into feat/sidebar-title-marquee
alexyaroshuk Feb 17, 2026
f02279d
Merge branch 'dev' into feat/sidebar-title-marquee
alexyaroshuk Feb 19, 2026
b7440a3
Merge remote-tracking branch 'upstream/dev' into feat/sidebar-title-m…
alexyaroshuk Feb 19, 2026
b73fe8d
Merge remote-tracking branch 'upstream/dev' into feat/sidebar-title-m…
alexyaroshuk Feb 19, 2026
ebcc4bc
Merge remote-tracking branch 'upstream/dev' into feat/sidebar-title-m…
alexyaroshuk Feb 19, 2026
22b6b6e
Merge remote-tracking branch 'upstream/dev' into feat/sidebar-title-m…
alexyaroshuk Feb 20, 2026
3df79f0
Merge remote-tracking branch 'upstream/dev' into feat/sidebar-title-m…
alexyaroshuk Feb 22, 2026
087317e
Merge remote-tracking branch 'upstream/dev' into feat/sidebar-title-m…
alexyaroshuk Feb 23, 2026
9637c75
Merge branch 'dev' into feat/sidebar-title-marquee
alexyaroshuk Feb 27, 2026
7ef17bb
Merge branch 'dev' into feat/sidebar-title-marquee
alexyaroshuk Mar 2, 2026
9624505
Merge branch 'dev' into feat/sidebar-title-marquee
alexyaroshuk Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 67 additions & 6 deletions packages/app/src/pages/layout/sidebar-items.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Message, Session, TextPart, UserMessage } from "@opencode-ai/sdk/v2/client"
import { Avatar } from "@opencode-ai/ui/avatar"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { HoverCard } from "@opencode-ai/ui/hover-card"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
Expand All @@ -9,7 +10,7 @@ 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, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
import { type Accessor, createEffect, createMemo, createSignal, 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"
Expand Down Expand Up @@ -101,13 +102,23 @@ const SessionRow = (props: {
warmPress: () => void
warmFocus: () => void
cancelHoverPrefetch: () => void
onHoverChange?: (hovered: boolean) => void
titleRef?: (el: HTMLSpanElement | undefined) => void
containerRef?: (el: HTMLDivElement | undefined) => void
marquee?: Accessor<boolean>
}): 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}
onPointerEnter={() => {
props.onHoverChange?.(true)
props.warmHover()
}}
onPointerLeave={() => {
props.onHoverChange?.(false)
props.cancelHoverPrefetch()
}}
onFocus={props.warmFocus}
onClick={() => {
props.setHoverSession(undefined)
Expand Down Expand Up @@ -135,9 +146,25 @@ const SessionRow = (props: {
</Match>
</Switch>
</div>
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
{props.session.title}
</span>
<div ref={props.containerRef} class="grow-1 min-w-0 overflow-hidden">
<span
ref={props.titleRef}
class="text-14-regular text-text-strong whitespace-nowrap"
classList={{
"inline-block marquee": props.marquee?.() ?? false,
"block overflow-hidden text-ellipsis": !(props.marquee?.() ?? false),
}}
>
{props.session.title}
</span>
</div>
<Show when={props.session.summary}>
{(summary) => (
<div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
<DiffChanges changes={summary()} />
</div>
)}
</Show>
</div>
</A>
)
Expand Down Expand Up @@ -274,6 +301,32 @@ 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())

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 = (
<SessionRow
session={props.session}
Expand All @@ -292,6 +345,14 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
warmPress={() => warm(2, "high")}
warmFocus={() => warm(2, "high")}
cancelHoverPrefetch={cancelHoverPrefetch}
onHoverChange={setItemHovered}
titleRef={(el) => {
titleRef = el
}}
containerRef={(el) => {
containerRef = el
}}
marquee={marquee}
/>
)

Expand Down
13 changes: 13 additions & 0 deletions packages/ui/src/styles/tailwind/utilities.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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;
Expand Down