diff options
| author | Adam <[email protected]> | 2026-01-15 15:05:40 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-19 10:55:57 -0600 |
| commit | befd0f16362678dcd99cd9118cbcb044997c9511 (patch) | |
| tree | 46affcaa30de5927f5036c608c3eb2eea8804deb | |
| parent | 1f11a8a6ea46867e2ad199c987bf14696a1b91d8 (diff) | |
| download | opencode-befd0f16362678dcd99cd9118cbcb044997c9511.tar.gz opencode-befd0f16362678dcd99cd9118cbcb044997c9511.zip | |
feat(app): new session layout
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 53 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 4 | ||||
| -rw-r--r-- | packages/enterprise/src/routes/share/[shareID].tsx | 4 | ||||
| -rw-r--r-- | packages/ui/src/components/session-turn.css | 28 | ||||
| -rw-r--r-- | packages/ui/src/components/session-turn.tsx | 96 |
5 files changed, 107 insertions, 78 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 2f71570f4..5312ff0a2 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1296,7 +1296,13 @@ export default function Layout(props: ParentProps) { ) } - const SessionItem = (props: { session: Session; slug: string; mobile?: boolean; dense?: boolean }): JSX.Element => { + const SessionItem = (props: { + session: Session + slug: string + mobile?: boolean + dense?: boolean + popover?: boolean + }): JSX.Element => { const notification = useNotification() const notifications = createMemo(() => notification.session.unseen(props.session.id)) const hasError = createMemo(() => notifications().some((n) => n.type === "error")) @@ -1335,6 +1341,7 @@ export default function Layout(props: ParentProps) { ) const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined) const hoverAllowed = createMemo(() => !props.mobile && layout.sidebar.opened()) + const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed()) const isActive = createMemo(() => props.session.id === params.id) const messageLabel = (message: Message) => { @@ -1370,23 +1377,14 @@ export default function Layout(props: ParentProps) { </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> + <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 + /> <Show when={props.session.summary}> {(summary) => ( <div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden"> @@ -1396,7 +1394,7 @@ export default function Layout(props: ParentProps) { </Show> </div> </A> - )) + ) return ( <div @@ -1405,8 +1403,12 @@ export default function Layout(props: ParentProps) { hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active" > <Show - when={hoverAllowed()} - fallback={item} + when={hoverEnabled()} + fallback={ + <Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}> + {item} + </Tooltip> + } > <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>}> @@ -1730,6 +1732,7 @@ export default function Layout(props: ParentProps) { slug={base64Encode(props.project.worktree)} dense mobile={props.mobile} + popover={false} /> )} </For> @@ -1746,7 +1749,13 @@ export default function Layout(props: ParentProps) { </div> <For each={sessions(directory)}> {(session) => ( - <SessionItem session={session} slug={base64Encode(directory)} dense mobile={props.mobile} /> + <SessionItem + session={session} + slug={base64Encode(directory)} + dense + mobile={props.mobile} + popover={false} + /> )} </For> </div> diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index b1c844f0c..5f282ac85 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1233,6 +1233,7 @@ export default function Page() { > <SessionTurn sessionID={params.id!} + sessionTitle={info()?.title} messageID={message.id} lastUserMessageID={lastUserMessage()?.id} stepsExpanded={store.expanded[message.id] ?? false} @@ -1241,8 +1242,7 @@ export default function Page() { } classes={{ root: "min-w-0 w-full relative", - content: - "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]", + content: "flex flex-col justify-between !overflow-visible", container: "w-full px-4 md:px-6", }} /> diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index d657ddc12..483db4d93 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -295,13 +295,13 @@ export default function () { {(message) => ( <SessionTurn sessionID={data().sessionID} + sessionTitle={info().title} messageID={message.id} stepsExpanded={store.expandedSteps[message.id] ?? false} onStepsExpandedToggle={() => setStore("expandedSteps", message.id, (v) => !v)} classes={{ root: "min-w-0 w-full relative", - content: - "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]", + content: "flex flex-col justify-between !overflow-visible", container: "px-4", }} /> diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 1e3cc0b29..f7ab97179 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -29,23 +29,6 @@ gap: 28px; overflow-anchor: none; - [data-slot="session-turn-user-badges"] { - position: absolute; - right: 0; - display: flex; - gap: 6px; - padding-left: 16px; - background: linear-gradient(to right, transparent, var(--background-stronger) 12px); - opacity: 0; - transition: opacity 0.15s ease; - pointer-events: none; - } - - &:hover [data-slot="session-turn-user-badges"] { - opacity: 1; - pointer-events: auto; - } - [data-slot="session-turn-badge"] { display: inline-flex; align-items: center; @@ -71,7 +54,7 @@ [data-slot="session-turn-response-trigger"] { position: sticky; - top: 32px; + top: calc(var(--sticky-header-height, 0px)); background-color: var(--background-stronger); z-index: 20; width: calc(100% + 9px); @@ -88,10 +71,17 @@ } [data-slot="session-turn-message-content"] { - margin-top: -18px; + margin-top: 0; max-width: 100%; } + [data-slot="session-turn-user-badges"] { + display: flex; + align-items: center; + gap: 6px; + padding-left: 16px; + } + [data-slot="session-turn-message-title"] { width: 100%; font-size: var(--font-size-large); diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index ae1321bac..8b807af82 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -119,6 +119,7 @@ function AssistantMessageItem(props: { export function SessionTurn( props: ParentProps<{ sessionID: string + sessionTitle?: string messageID: string lastUserMessageID?: string stepsExpanded?: boolean @@ -330,7 +331,9 @@ export function SessionTurn( const response = createMemo(() => lastTextPart()?.text) const responsePartId = createMemo(() => lastTextPart()?.id) - const hasDiffs = createMemo(() => message()?.summary?.diffs?.length) + const sessionInfo = createMemo(() => data.store.session.find((item) => item.id === props.sessionID)) + const sessionTitle = createMemo(() => props.sessionTitle ?? sessionInfo()?.title) + const hasDiffs = createMemo(() => (data.store.session_diff?.[props.sessionID]?.length ?? 0) > 0) const hideResponsePart = createMemo(() => !working() && !!responsePartId()) const [responseCopied, setResponseCopied] = createSignal(false) @@ -376,6 +379,7 @@ export function SessionTurn( diffLimit: diffInit, status: rawStatus(), duration: duration(), + titleShown: false, }) createEffect( @@ -390,6 +394,18 @@ export function SessionTurn( ) createEffect(() => { + if (!sessionTitle()) { + setStore("titleShown", false) + return + } + if (store.titleShown) return + const first = allMessages().find((item) => item?.role === "user") + if (!first) return + if (first.id !== props.messageID) return + setStore("titleShown", true) + }) + + createEffect(() => { const r = retry() if (!r) { setStore("retrySeconds", 0) @@ -482,40 +498,53 @@ export function SessionTurn( <Part part={shellModePart()!} message={msg()} defaultOpen /> </Match> <Match when={true}> - {/* Title (sticky) */} - <div ref={(el) => setStore("stickyTitleRef", el)} data-slot="session-turn-sticky-title"> - <div data-slot="session-turn-message-header"> - <div data-slot="session-turn-message-title"> - <Switch> - <Match when={working()}> - <Typewriter as="h1" text={msg().summary?.title} data-slot="session-turn-typewriter" /> - </Match> - <Match when={true}> - <h1>{msg().summary?.title}</h1> - </Match> - </Switch> - </div> - <div data-slot="session-turn-user-badges"> - <Show when={(msg() as UserMessage).agent}> - <span data-slot="session-turn-badge">{(msg() as UserMessage).agent}</span> - </Show> - <Show when={(msg() as UserMessage).model?.modelID}> - <span data-slot="session-turn-badge" class="inline-flex items-center gap-1"> - <ProviderIcon - id={(msg() as UserMessage).model!.providerID as IconName} - class="size-3.5 shrink-0" - /> - {(msg() as UserMessage).model?.modelID} - </span> - </Show> - <span data-slot="session-turn-badge">{(msg() as UserMessage).variant || "default"}</span> + <Show when={sessionTitle() && store.titleShown}> + <div ref={(el) => setStore("stickyTitleRef", el)} data-slot="session-turn-sticky-title"> + <div data-slot="session-turn-message-header"> + <div data-slot="session-turn-message-title"> + <Switch> + <Match when={working()}> + <Typewriter as="h1" text={sessionTitle()} data-slot="session-turn-typewriter" /> + </Match> + <Match when={true}> + <h1>{sessionTitle()}</h1> + </Match> + </Switch> + </div> </div> </div> - </div> + </Show> + + <Show + when={ + (msg() as UserMessage).agent || + (msg() as UserMessage).model?.modelID || + (msg() as UserMessage).variant + } + > + <div data-slot="session-turn-user-badges"> + <Show when={(msg() as UserMessage).agent}> + <span data-slot="session-turn-badge">{(msg() as UserMessage).agent}</span> + </Show> + <Show when={(msg() as UserMessage).model?.modelID}> + <span data-slot="session-turn-badge" class="inline-flex items-center gap-1"> + <ProviderIcon + id={(msg() as UserMessage).model!.providerID as IconName} + class="size-3.5 shrink-0" + /> + {(msg() as UserMessage).model?.modelID} + </span> + </Show> + <Show when={(msg() as UserMessage).variant}> + <span data-slot="session-turn-badge">{(msg() as UserMessage).variant}</span> + </Show> + </div> + </Show> {/* User Message */} <div data-slot="session-turn-message-content"> <Message message={msg()} parts={parts()} /> </div> + {/* Trigger (sticky) */} <Show when={working() || hasSteps()}> <div ref={(el) => setStore("stickyTriggerRef", el)} data-slot="session-turn-response-trigger"> @@ -612,7 +641,7 @@ export function SessionTurn( setStore("diffsOpen", value) }} > - <For each={(msg().summary?.diffs ?? []).slice(0, store.diffLimit)}> + <For each={(data.store.session_diff?.[props.sessionID] ?? []).slice(0, store.diffLimit)}> {(diff) => ( <Accordion.Item value={diff.file}> <StickyAccordionHeader> @@ -658,13 +687,13 @@ export function SessionTurn( )} </For> </Accordion> - <Show when={(msg().summary?.diffs?.length ?? 0) > store.diffLimit}> + <Show when={(data.store.session_diff?.[props.sessionID]?.length ?? 0) > store.diffLimit}> <Button data-slot="session-turn-accordion-more" variant="ghost" size="small" onClick={() => { - const total = msg().summary?.diffs?.length ?? 0 + const total = data.store.session_diff?.[props.sessionID]?.length ?? 0 setStore("diffLimit", (limit) => { const next = limit + diffBatch if (next > total) return total @@ -672,7 +701,8 @@ export function SessionTurn( }) }} > - Show more changes ({(msg().summary?.diffs?.length ?? 0) - store.diffLimit}) + Show more changes ( + {(data.store.session_diff?.[props.sessionID]?.length ?? 0) - store.diffLimit}) </Button> </Show> </div> |
