Skip to content

Commit f5f0731

Browse files
authored
fix(app): sidebar spacing + session list spinner transition (#17355)
1 parent c9e9dbe commit f5f0731

3 files changed

Lines changed: 108 additions & 49 deletions

File tree

packages/app/src/pages/layout.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1961,7 +1961,7 @@ export default function Layout(props: ParentProps) {
19611961
return (
19621962
<div
19631963
classList={{
1964-
"flex flex-col min-h-0 min-w-0 box-border rounded-tl-[12px] px-2": true,
1964+
"flex flex-col min-h-0 min-w-0 box-border rounded-tl-[12px] px-3": true,
19651965
"border border-b-0 border-border-weak-base": !merged(),
19661966
"border-l border-t border-border-weaker-base": merged(),
19671967
"bg-background-base": merged() || hover(),
@@ -1976,8 +1976,8 @@ export default function Layout(props: ParentProps) {
19761976
<Show when={panelProps.project}>
19771977
{(p) => (
19781978
<>
1979-
<div class="shrink-0 px-2 py-1">
1980-
<div class="group/project flex items-start justify-between gap-2 p-2 pr-1">
1979+
<div class="shrink-0 pl-1 py-1">
1980+
<div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
19811981
<div class="flex flex-col min-w-0">
19821982
<InlineEditor
19831983
id={`project:${projectId()}`}
@@ -2063,7 +2063,7 @@ export default function Layout(props: ParentProps) {
20632063
when={workspacesEnabled()}
20642064
fallback={
20652065
<>
2066-
<div class="shrink-0 py-4 px-3">
2066+
<div class="shrink-0 py-4">
20672067
<Button
20682068
size="large"
20692069
icon="plus-small"
@@ -2086,7 +2086,7 @@ export default function Layout(props: ParentProps) {
20862086
}
20872087
>
20882088
<>
2089-
<div class="shrink-0 py-4 px-3">
2089+
<div class="shrink-0 py-4">
20902090
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p())}>
20912091
{language.t("workspace.new")}
20922092
</Button>

packages/app/src/pages/layout/sidebar-items.tsx

Lines changed: 101 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
99
import { base64Encode } from "@opencode-ai/util/encode"
1010
import { getFilename } from "@opencode-ai/util/path"
1111
import { A, useNavigate, useParams } from "@solidjs/router"
12-
import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
12+
import { type Accessor, createEffect, createMemo, For, type JSX, on, onCleanup, Show } from "solid-js"
13+
import { createStore } from "solid-js/store"
1314
import { useGlobalSync } from "@/context/global-sync"
1415
import { useLanguage } from "@/context/language"
1516
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
@@ -101,46 +102,94 @@ const SessionRow = (props: {
101102
warmPress: () => void
102103
warmFocus: () => void
103104
cancelHoverPrefetch: () => void
104-
}): JSX.Element => (
105-
<A
106-
href={`/${props.slug}/session/${props.session.id}`}
107-
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"}`}
108-
onPointerDown={props.warmPress}
109-
onPointerEnter={props.warmHover}
110-
onPointerLeave={props.cancelHoverPrefetch}
111-
onFocus={props.warmFocus}
112-
onClick={() => {
113-
props.setHoverSession(undefined)
114-
if (props.sidebarOpened()) return
115-
props.clearHoverProjectSoon()
116-
}}
117-
>
118-
<div class="flex items-center gap-1 w-full">
119-
<div
120-
class="shrink-0 size-6 flex items-center justify-center"
121-
style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
122-
>
123-
<Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
124-
<Match when={props.isWorking()}>
125-
<Spinner class="size-[15px]" />
126-
</Match>
127-
<Match when={props.hasPermissions()}>
128-
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
129-
</Match>
130-
<Match when={props.hasError()}>
131-
<div class="size-1.5 rounded-full bg-text-diff-delete-base" />
132-
</Match>
133-
<Match when={props.unseenCount() > 0}>
134-
<div class="size-1.5 rounded-full bg-text-interactive-base" />
135-
</Match>
136-
</Switch>
105+
}): JSX.Element => {
106+
const [slot, setSlot] = createStore({
107+
open: false,
108+
show: false,
109+
fade: false,
110+
})
111+
112+
let f: number | undefined
113+
const clear = () => {
114+
if (f !== undefined) window.clearTimeout(f)
115+
f = undefined
116+
}
117+
118+
onCleanup(clear)
119+
createEffect(
120+
on(
121+
() => props.isWorking(),
122+
(on, prev) => {
123+
clear()
124+
if (on) {
125+
setSlot({ open: true, show: true, fade: false })
126+
return
127+
}
128+
if (prev) {
129+
setSlot({ open: false, show: true, fade: true })
130+
f = window.setTimeout(() => setSlot({ show: false, fade: false }), 260)
131+
return
132+
}
133+
setSlot({ open: false, show: false, fade: false })
134+
},
135+
{ defer: true },
136+
),
137+
)
138+
139+
return (
140+
<A
141+
href={`/${props.slug}/session/${props.session.id}`}
142+
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"}`}
143+
onPointerDown={props.warmPress}
144+
onPointerEnter={props.warmHover}
145+
onPointerLeave={props.cancelHoverPrefetch}
146+
onFocus={props.warmFocus}
147+
onClick={() => {
148+
props.setHoverSession(undefined)
149+
if (props.sidebarOpened()) return
150+
props.clearHoverProjectSoon()
151+
}}
152+
>
153+
<Show when={!props.isWorking() && (props.hasPermissions() || props.hasError() || props.unseenCount() > 0)}>
154+
<div
155+
classList={{
156+
"absolute left-0 top-1/2 -translate-y-1/2 size-1.5 rounded-full": true,
157+
"bg-surface-warning-strong": props.hasPermissions(),
158+
"bg-text-diff-delete-base": !props.hasPermissions() && props.hasError(),
159+
"bg-text-interactive-base": !props.hasPermissions() && !props.hasError() && props.unseenCount() > 0,
160+
}}
161+
aria-hidden="true"
162+
/>
163+
</Show>
164+
165+
<div class="flex items-center min-w-0 grow-1">
166+
<div
167+
class="shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
168+
style={{
169+
width: slot.open ? "16px" : "0px",
170+
"margin-right": slot.open ? "8px" : "0px",
171+
}}
172+
aria-hidden="true"
173+
>
174+
<Show when={slot.show}>
175+
<div
176+
class="transition-opacity duration-200 ease-out"
177+
classList={{
178+
"opacity-0": slot.fade,
179+
}}
180+
>
181+
<Spinner class="size-4" style={{ color: props.tint() ?? "var(--icon-interactive-base)" }} />
182+
</div>
183+
</Show>
184+
</div>
185+
186+
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
187+
{props.session.title}
188+
</span>
137189
</div>
138-
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
139-
{props.session.title}
140-
</span>
141-
</div>
142-
</A>
143-
)
190+
</A>
191+
)
192+
}
144193

145194
const SessionHoverPreview = (props: {
146195
mobile?: boolean
@@ -204,8 +253,18 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
204253
})
205254
const isWorking = createMemo(() => {
206255
if (hasPermissions()) return false
256+
const pending = (sessionStore.message[props.session.id] ?? []).findLast(
257+
(message) =>
258+
message.role === "assistant" &&
259+
typeof (message as { time?: { completed?: unknown } }).time?.completed !== "number",
260+
)
207261
const status = sessionStore.session_status[props.session.id]
208-
return status?.type === "busy" || status?.type === "retry"
262+
return (
263+
pending !== undefined ||
264+
status?.type === "busy" ||
265+
status?.type === "retry" ||
266+
(status !== undefined && status.type !== "idle")
267+
)
209268
})
210269

211270
const tint = createMemo(() => {
@@ -300,7 +359,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
300359
return (
301360
<div
302361
data-session-id={props.session.id}
303-
class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3
362+
class="group/session relative w-full rounded-md cursor-default pl-3 pr-3 transition-colors
304363
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"
305364
>
306365
<Show

packages/app/src/pages/layout/sidebar-workspace.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ const WorkspaceSessionList = (props: {
249249
loadMore: () => Promise<void>
250250
language: ReturnType<typeof useLanguage>
251251
}): JSX.Element => (
252-
<nav class="flex flex-col gap-1 px-3">
252+
<nav class="flex flex-col gap-1">
253253
<Show when={props.showNew()}>
254254
<NewSessionItem
255255
slug={props.slug()}
@@ -382,7 +382,7 @@ export const SortableWorkspace = (props: {
382382
}}
383383
>
384384
<Collapsible variant="ghost" open={open()} class="shrink-0" onOpenChange={openWrapper}>
385-
<div class="px-2 py-1">
385+
<div class="py-1">
386386
<div
387387
class="group/workspace relative"
388388
data-component="workspace-item"

0 commit comments

Comments
 (0)