diff options
| author | Adam <[email protected]> | 2026-01-15 13:32:15 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-19 10:55:57 -0600 |
| commit | 1f11a8a6ea46867e2ad199c987bf14696a1b91d8 (patch) | |
| tree | 64a1a83c5d576a9592229d67815e76359b399a03 /packages/app/src | |
| parent | d5ae8e0bef991f2b2ad9766b9ae2f1c903badab3 (diff) | |
| download | opencode-1f11a8a6ea46867e2ad199c987bf14696a1b91d8.tar.gz opencode-1f11a8a6ea46867e2ad199c987bf14696a1b91d8.zip | |
feat(app): improved session layout
Diffstat (limited to 'packages/app/src')
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 140 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 20 |
2 files changed, 92 insertions, 68 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 39ca39b67..2f71570f4 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -28,13 +28,14 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { InlineInput } from "@opencode-ai/ui/inline-input" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { HoverCard } from "@opencode-ai/ui/hover-card" +import { MessageNav } from "@opencode-ai/ui/message-nav" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Collapsible } from "@opencode-ai/ui/collapsible" import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { Spinner } from "@opencode-ai/ui/spinner" import { Dialog } from "@opencode-ai/ui/dialog" import { getFilename } from "@opencode-ai/util/path" -import { Session } from "@opencode-ai/sdk/v2/client" +import { Session, type Message, type TextPart } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" import { createStore, produce, reconcile } from "solid-js/store" import { @@ -1329,63 +1330,104 @@ export default function Layout(props: ParentProps) { return agent?.color }) + const hoverMessages = createMemo(() => + sessionStore.message[props.session.id]?.filter((message) => message.role === "user"), + ) + const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined) + const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened()) + const isActive = createMemo(() => props.session.id === params.id) + + const messageLabel = (message: Message) => { + const parts = sessionStore.part[message.id] ?? [] + const text = parts.find((part): part is TextPart => part?.type === "text" && !part.synthetic && !part.ignored) + return text?.text + } + + const item = ( + <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] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`} + onMouseEnter={() => prefetchSession(props.session, "high")} + onFocus={() => prefetchSession(props.session, "high")} + > + <div class="flex items-center gap-1 w-full"> + <div + class="shrink-0 size-6 flex items-center justify-center" + style={{ color: tint() ?? "var(--icon-interactive-base)" }} + > + <Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}> + <Match when={isWorking()}> + <Spinner class="size-[15px]" /> + </Match> + <Match when={hasPermissions()}> + <div class="size-1.5 rounded-full bg-surface-warning-strong" /> + </Match> + <Match when={hasError()}> + <div class="size-1.5 rounded-full bg-text-diff-delete-base" /> + </Match> + <Match when={notifications().length > 0}> + <div class="size-1.5 rounded-full bg-text-interactive-base" /> + </Match> + </Switch> + </div> + <Tooltip + inactive={hoverAllowed()} + placement="top-start" + value={props.session.title} + gutter={0} + openDelay={3000} + class="grow-1 min-w-0" + > + <InlineEditor + id={`session:${props.session.id}`} + value={() => props.session.title} + onSave={(next) => renameSession(props.session, next)} + class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" + displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" + stopPropagation + /> + </Tooltip> + <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> + )) + return ( <div data-session-id={props.session.id} class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3 hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active" > - <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] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`} - onMouseEnter={() => prefetchSession(props.session, "high")} - onFocus={() => prefetchSession(props.session, "high")} + <Show + when={hoverAllowed()} + fallback={item} > - <div class="flex items-center gap-1 w-full"> - <div - class="shrink-0 size-6 flex items-center justify-center" - style={{ color: tint() ?? "var(--icon-interactive-base)" }} - > - <Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}> - <Match when={isWorking()}> - <Spinner class="size-[15px]" /> - </Match> - <Match when={hasPermissions()}> - <div class="size-1.5 rounded-full bg-surface-warning-strong" /> - </Match> - <Match when={hasError()}> - <div class="size-1.5 rounded-full bg-text-diff-delete-base" /> - </Match> - <Match when={notifications().length > 0}> - <div class="size-1.5 rounded-full bg-text-interactive-base" /> - </Match> - </Switch> - </div> - <Tooltip - placement="top-start" - value={props.session.title} - gutter={0} - openDelay={3000} - class="grow-1 min-w-0" - > - <InlineEditor - id={`session:${props.session.id}`} - value={() => props.session.title} - onSave={(next) => renameSession(props.session, next)} - class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" - displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" - stopPropagation + <HoverCard openDelay={150} closeDelay={100} placement="right" gutter={12} trigger={item}> + <Show when={hoverReady()} fallback={<div class="text-12-regular text-text-weak">Loading messages…</div>}> + <MessageNav + messages={hoverMessages() ?? []} + current={undefined} + getLabel={messageLabel} + onMessageSelect={(message) => { + if (!isActive()) { + navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`) + return + } + window.location.hash = `message-${message.id}` + window.dispatchEvent(new HashChangeEvent("hashchange")) + }} + size="normal" + class="w-60" /> - </Tooltip> - <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> + </HoverCard> + </Show> <div class={`hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"}`} > diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index f063ce35b..b1c844f0c 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -18,7 +18,6 @@ import { useCodeComponent } from "@opencode-ai/ui/context/code" import { SessionTurn } from "@opencode-ai/ui/session-turn" import { createAutoScroll } from "@opencode-ai/ui/hooks" import { SessionReview } from "@opencode-ai/ui/session-review" -import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail" import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" import type { DragEvent } from "@thisbeyond/solid-dnd" @@ -1163,17 +1162,6 @@ export default function Page() { } > <div class="relative w-full h-full min-w-0"> - <Show when={isDesktop()}> - <div class="absolute inset-0 pointer-events-none z-10"> - <SessionMessageRail - messages={visibleUserMessages()} - current={activeMessage()} - onMessageSelect={scrollToMessage} - wide={!showTabs()} - class="pointer-events-auto" - /> - </div> - </Show> <div ref={setScrollRef} onScroll={(e) => { @@ -1255,13 +1243,7 @@ export default function Page() { root: "min-w-0 w-full relative", content: "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]", - container: - "px-4 md:px-6 " + - (!showTabs() - ? "md:max-w-200 md:mx-auto" - : visibleUserMessages().length > 1 - ? "md:pr-6 md:pl-18" - : ""), + container: "w-full px-4 md:px-6", }} /> </div> |
