diff options
| author | David Hill <[email protected]> | 2026-03-13 14:19:02 +0000 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-13 09:19:02 -0500 |
| commit | f5f07310e026215b9f6809b196c8eddbdedd77df (patch) | |
| tree | 84ffe00ed4e55b84c39bd8caafeda2141476485d /packages/app | |
| parent | c9e9dbeee1ceb5af3d1b1ce292317390286fe7a0 (diff) | |
| download | opencode-f5f07310e026215b9f6809b196c8eddbdedd77df.tar.gz opencode-f5f07310e026215b9f6809b196c8eddbdedd77df.zip | |
fix(app): sidebar spacing + session list spinner transition (#17355)
Diffstat (limited to 'packages/app')
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 10 | ||||
| -rw-r--r-- | packages/app/src/pages/layout/sidebar-items.tsx | 143 | ||||
| -rw-r--r-- | packages/app/src/pages/layout/sidebar-workspace.tsx | 4 |
3 files changed, 108 insertions, 49 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index bc04f9ecf..8ee5b3710 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1961,7 +1961,7 @@ export default function Layout(props: ParentProps) { return ( <div classList={{ - "flex flex-col min-h-0 min-w-0 box-border rounded-tl-[12px] px-2": true, + "flex flex-col min-h-0 min-w-0 box-border rounded-tl-[12px] px-3": true, "border border-b-0 border-border-weak-base": !merged(), "border-l border-t border-border-weaker-base": merged(), "bg-background-base": merged() || hover(), @@ -1976,8 +1976,8 @@ export default function Layout(props: ParentProps) { <Show when={panelProps.project}> {(p) => ( <> - <div class="shrink-0 px-2 py-1"> - <div class="group/project flex items-start justify-between gap-2 p-2 pr-1"> + <div class="shrink-0 pl-1 py-1"> + <div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0"> <div class="flex flex-col min-w-0"> <InlineEditor id={`project:${projectId()}`} @@ -2063,7 +2063,7 @@ export default function Layout(props: ParentProps) { when={workspacesEnabled()} fallback={ <> - <div class="shrink-0 py-4 px-3"> + <div class="shrink-0 py-4"> <Button size="large" icon="plus-small" @@ -2086,7 +2086,7 @@ export default function Layout(props: ParentProps) { } > <> - <div class="shrink-0 py-4 px-3"> + <div class="shrink-0 py-4"> <Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p())}> {language.t("workspace.new")} </Button> diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index 17572923e..04d898134 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -9,7 +9,8 @@ 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, For, type JSX, on, onCleanup, Show } from "solid-js" +import { createStore } from "solid-js/store" import { useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout" @@ -101,46 +102,94 @@ const SessionRow = (props: { warmPress: () => void warmFocus: () => void cancelHoverPrefetch: () => void -}): 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> +}): 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> </div> - <span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate"> - {props.session.title} - </span> - </div> - </A> -) + </A> + ) +} const SessionHoverPreview = (props: { mobile?: boolean @@ -204,8 +253,18 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { }) const isWorking = createMemo(() => { if (hasPermissions()) return false + const pending = (sessionStore.message[props.session.id] ?? []).findLast( + (message) => + message.role === "assistant" && + typeof (message as { time?: { completed?: unknown } }).time?.completed !== "number", + ) const status = sessionStore.session_status[props.session.id] - return status?.type === "busy" || status?.type === "retry" + return ( + pending !== undefined || + status?.type === "busy" || + status?.type === "retry" || + (status !== undefined && status.type !== "idle") + ) }) const tint = createMemo(() => { @@ -300,7 +359,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 transition-colors pl-2 pr-3 + class="group/session relative w-full rounded-md cursor-default pl-3 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/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index 48c63e547..73d3d2141 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -249,7 +249,7 @@ const WorkspaceSessionList = (props: { loadMore: () => Promise<void> language: ReturnType<typeof useLanguage> }): JSX.Element => ( - <nav class="flex flex-col gap-1 px-3"> + <nav class="flex flex-col gap-1"> <Show when={props.showNew()}> <NewSessionItem slug={props.slug()} @@ -382,7 +382,7 @@ export const SortableWorkspace = (props: { }} > <Collapsible variant="ghost" open={open()} class="shrink-0" onOpenChange={openWrapper}> - <div class="px-2 py-1"> + <div class="py-1"> <div class="group/workspace relative" data-component="workspace-item" |
