Просмотр исходного кода

fix(app): new layout improvements

Adam 1 месяц назад
Родитель
Сommit
7811e01c8e

+ 87 - 48
packages/app/src/pages/session.tsx

@@ -18,6 +18,7 @@ import { useCodeComponent } from "@opencode-ai/ui/context/code"
 import { SessionTurn } from "@opencode-ai/ui/session-turn"
 import { SessionTurn } from "@opencode-ai/ui/session-turn"
 import { createAutoScroll } from "@opencode-ai/ui/hooks"
 import { createAutoScroll } from "@opencode-ai/ui/hooks"
 import { SessionReview } from "@opencode-ai/ui/session-review"
 import { SessionReview } from "@opencode-ai/ui/session-review"
+import { Mark } from "@opencode-ai/ui/logo"
 
 
 import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
 import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
 import type { DragEvent } from "@thisbeyond/solid-dnd"
 import type { DragEvent } from "@thisbeyond/solid-dnd"
@@ -787,17 +788,14 @@ export default function Page() {
       .filter((tab) => tab !== "context"),
       .filter((tab) => tab !== "context"),
   )
   )
 
 
-  const reviewTab = createMemo(() => hasReview() || tabs().active() === "review")
-  const mobileReview = createMemo(() => !isDesktop() && hasReview() && store.mobileTab === "review")
+  const mobileReview = createMemo(() => !isDesktop() && view().reviewPanel.opened() && store.mobileTab === "review")
 
 
-  const showTabs = createMemo(
-    () => view().reviewPanel.opened() && (hasReview() || tabs().all().length > 0 || contextOpen()),
-  )
+  const showTabs = createMemo(() => view().reviewPanel.opened())
 
 
   const activeTab = createMemo(() => {
   const activeTab = createMemo(() => {
     const active = tabs().active()
     const active = tabs().active()
     if (active) return active
     if (active) return active
-    if (reviewTab()) return "review"
+    if (hasReview()) return "review"
 
 
     const first = openedTabs()[0]
     const first = openedTabs()[0]
     if (first) return first
     if (first) return first
@@ -1095,8 +1093,8 @@ export default function Page() {
     <div class="relative bg-background-base size-full overflow-hidden flex flex-col">
     <div class="relative bg-background-base size-full overflow-hidden flex flex-col">
       <SessionHeader />
       <SessionHeader />
       <div class="flex-1 min-h-0 flex flex-col md:flex-row">
       <div class="flex-1 min-h-0 flex flex-col md:flex-row">
-        {/* Mobile tab bar - only shown on mobile when there are diffs */}
-        <Show when={!isDesktop() && hasReview()}>
+        {/* Mobile tab bar - only shown on mobile when user opened review */}
+        <Show when={!isDesktop() && view().reviewPanel.opened()}>
           <Tabs class="h-auto">
           <Tabs class="h-auto">
             <Tabs.List>
             <Tabs.List>
               <Tabs.Trigger
               <Tabs.Trigger
@@ -1113,7 +1111,10 @@ export default function Page() {
                 classes={{ button: "w-full" }}
                 classes={{ button: "w-full" }}
                 onClick={() => setStore("mobileTab", "review")}
                 onClick={() => setStore("mobileTab", "review")}
               >
               >
-                {reviewCount()} Files Changed
+                <Switch>
+                  <Match when={hasReview()}>{reviewCount()} Files Changed</Match>
+                  <Match when={true}>Review</Match>
+                </Switch>
               </Tabs.Trigger>
               </Tabs.Trigger>
             </Tabs.List>
             </Tabs.List>
           </Tabs>
           </Tabs>
@@ -1138,26 +1139,36 @@ export default function Page() {
                     when={!mobileReview()}
                     when={!mobileReview()}
                     fallback={
                     fallback={
                       <div class="relative h-full overflow-hidden">
                       <div class="relative h-full overflow-hidden">
-                        <Show
-                          when={diffsReady()}
-                          fallback={<div class="px-4 py-4 text-text-weak">Loading changes...</div>}
-                        >
-                          <SessionReviewTab
-                            diffs={diffs}
-                            view={view}
-                            diffStyle="unified"
-                            onViewFile={(path) => {
-                              const value = file.tab(path)
-                              tabs().open(value)
-                              file.load(path)
-                            }}
-                            classes={{
-                              root: "pb-[calc(var(--prompt-height,8rem)+24px)]",
-                              header: "px-4",
-                              container: "px-4",
-                            }}
-                          />
-                        </Show>
+                        <Switch>
+                          <Match when={hasReview()}>
+                            <Show
+                              when={diffsReady()}
+                              fallback={<div class="px-4 py-4 text-text-weak">Loading changes...</div>}
+                            >
+                              <SessionReviewTab
+                                diffs={diffs}
+                                view={view}
+                                diffStyle="unified"
+                                onViewFile={(path) => {
+                                  const value = file.tab(path)
+                                  tabs().open(value)
+                                  file.load(path)
+                                }}
+                                classes={{
+                                  root: "pb-[calc(var(--prompt-height,8rem)+32px)]",
+                                  header: "px-4",
+                                  container: "px-4",
+                                }}
+                              />
+                            </Show>
+                          </Match>
+                          <Match when={true}>
+                            <div class="px-4 pt-18 pb-6 flex flex-col items-center justify-center text-center gap-3">
+                              <Mark class="w-6 opacity-40" />
+                              <div class="text-13-regular text-text-weak max-w-56">No changes in this session yet.</div>
+                            </div>
+                          </Match>
+                        </Switch>
                       </div>
                       </div>
                     }
                     }
                   >
                   >
@@ -1170,11 +1181,29 @@ export default function Page() {
                         }}
                         }}
                         onClick={autoScroll.handleInteraction}
                         onClick={autoScroll.handleInteraction}
                         class="relative min-w-0 w-full h-full overflow-y-auto no-scrollbar"
                         class="relative min-w-0 w-full h-full overflow-y-auto no-scrollbar"
+                        style={{ "--session-title-height": info()?.title ? "40px" : "0px" }}
                       >
                       >
+                        <Show when={info()?.title}>
+                          <div
+                            classList={{
+                              "sticky top-0 z-30 bg-background-stronger": true,
+                              "w-full": true,
+                              "px-4 md:px-6": true,
+                              "md:max-w-200 md:mx-auto": !showTabs(),
+                            }}
+                          >
+                            <div class="h-10 flex items-center">
+                              <h1 class="text-16-medium text-text-strong truncate">{info()?.title}</h1>
+                            </div>
+                          </div>
+                        </Show>
+
                         <div
                         <div
                           ref={autoScroll.contentRef}
                           ref={autoScroll.contentRef}
                           class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
                           class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
                           classList={{
                           classList={{
+                            "w-full": true,
+                            "md:max-w-200 md:mx-auto": !showTabs(),
                             "mt-0.5": !showTabs(),
                             "mt-0.5": !showTabs(),
                             "mt-0": showTabs(),
                             "mt-0": showTabs(),
                           }}
                           }}
@@ -1225,6 +1254,7 @@ export default function Page() {
                                   data-message-id={message.id}
                                   data-message-id={message.id}
                                   classList={{
                                   classList={{
                                     "min-w-0 w-full max-w-full": true,
                                     "min-w-0 w-full max-w-full": true,
+                                    "md:max-w-200": !showTabs(),
                                     "last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-64px)]":
                                     "last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-64px)]":
                                       platform.platform !== "desktop",
                                       platform.platform !== "desktop",
                                     "last:min-h-[calc(100vh-7rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-6rem-var(--prompt-height,10rem)-64px)]":
                                     "last:min-h-[calc(100vh-7rem-var(--prompt-height,8rem)-64px)] md:last:min-h-[calc(100vh-6rem-var(--prompt-height,10rem)-64px)]":
@@ -1233,7 +1263,6 @@ export default function Page() {
                                 >
                                 >
                                   <SessionTurn
                                   <SessionTurn
                                     sessionID={params.id!}
                                     sessionID={params.id!}
-                                    sessionTitle={info()?.title}
                                     messageID={message.id}
                                     messageID={message.id}
                                     lastUserMessageID={lastUserMessage()?.id}
                                     lastUserMessageID={lastUserMessage()?.id}
                                     stepsExpanded={store.expanded[message.id] ?? false}
                                     stepsExpanded={store.expanded[message.id] ?? false}
@@ -1333,7 +1362,7 @@ export default function Page() {
               <Tabs value={activeTab()} onChange={openTab}>
               <Tabs value={activeTab()} onChange={openTab}>
                 <div class="sticky top-0 shrink-0 flex">
                 <div class="sticky top-0 shrink-0 flex">
                   <Tabs.List>
                   <Tabs.List>
-                    <Show when={reviewTab()}>
+                    <Show when={true}>
                       <Tabs.Trigger value="review">
                       <Tabs.Trigger value="review">
                         <div class="flex items-center gap-3">
                         <div class="flex items-center gap-3">
                           <Show when={diffs()}>
                           <Show when={diffs()}>
@@ -1386,26 +1415,36 @@ export default function Page() {
                     </div>
                     </div>
                   </Tabs.List>
                   </Tabs.List>
                 </div>
                 </div>
-                <Show when={reviewTab()}>
+                <Show when={true}>
                   <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
                   <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
                     <Show when={activeTab() === "review"}>
                     <Show when={activeTab() === "review"}>
                       <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
                       <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
-                        <Show
-                          when={diffsReady()}
-                          fallback={<div class="px-6 py-4 text-text-weak">Loading changes...</div>}
-                        >
-                          <SessionReviewTab
-                            diffs={diffs}
-                            view={view}
-                            diffStyle={layout.review.diffStyle()}
-                            onDiffStyleChange={layout.review.setDiffStyle}
-                            onViewFile={(path) => {
-                              const value = file.tab(path)
-                              tabs().open(value)
-                              file.load(path)
-                            }}
-                          />
-                        </Show>
+                        <Switch>
+                          <Match when={hasReview()}>
+                            <Show
+                              when={diffsReady()}
+                              fallback={<div class="px-6 py-4 text-text-weak">Loading changes...</div>}
+                            >
+                              <SessionReviewTab
+                                diffs={diffs}
+                                view={view}
+                                diffStyle={layout.review.diffStyle()}
+                                onDiffStyleChange={layout.review.setDiffStyle}
+                                onViewFile={(path) => {
+                                  const value = file.tab(path)
+                                  tabs().open(value)
+                                  file.load(path)
+                                }}
+                              />
+                            </Show>
+                          </Match>
+                          <Match when={true}>
+                            <div class="px-6 pt-18 pb-6 flex flex-col items-center justify-center text-center gap-3">
+                              <Mark class="w-6 opacity-40" />
+                              <div class="text-13-regular text-text-weak max-w-56">No changes in this session yet.</div>
+                            </div>
+                          </Match>
+                        </Switch>
                       </div>
                       </div>
                     </Show>
                     </Show>
                   </Tabs.Content>
                   </Tabs.Content>

+ 33 - 2
packages/ui/src/components/message-part.tsx

@@ -46,6 +46,7 @@ import { checksum } from "@opencode-ai/util/encode"
 import { Tooltip } from "./tooltip"
 import { Tooltip } from "./tooltip"
 import { IconButton } from "./icon-button"
 import { IconButton } from "./icon-button"
 import { createAutoScroll } from "../hooks"
 import { createAutoScroll } from "../hooks"
+import { createResizeObserver } from "@solid-primitives/resize-observer"
 
 
 interface Diagnostic {
 interface Diagnostic {
   range: {
   range: {
@@ -297,6 +298,23 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part
 export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
 export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
   const dialog = useDialog()
   const dialog = useDialog()
   const [copied, setCopied] = createSignal(false)
   const [copied, setCopied] = createSignal(false)
+  const [expanded, setExpanded] = createSignal(false)
+  const [canExpand, setCanExpand] = createSignal(false)
+  let textRef: HTMLDivElement | undefined
+
+  const updateCanExpand = () => {
+    const el = textRef
+    if (!el) return
+    if (expanded()) return
+    setCanExpand(el.scrollHeight > el.clientHeight + 2)
+  }
+
+  createResizeObserver(
+    () => textRef,
+    () => {
+      updateCanExpand()
+    },
+  )
 
 
   const textPart = createMemo(
   const textPart = createMemo(
     () => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
     () => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
@@ -304,6 +322,11 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
 
 
   const text = createMemo(() => textPart()?.text || "")
   const text = createMemo(() => textPart()?.text || "")
 
 
+  createEffect(() => {
+    text()
+    updateCanExpand()
+  })
+
   const files = createMemo(() => (props.parts?.filter((p) => p.type === "file") as FilePart[]) ?? [])
   const files = createMemo(() => (props.parts?.filter((p) => p.type === "file") as FilePart[]) ?? [])
 
 
   const attachments = createMemo(() =>
   const attachments = createMemo(() =>
@@ -335,7 +358,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
   }
   }
 
 
   return (
   return (
-    <div data-component="user-message">
+    <div data-component="user-message" data-expanded={expanded()} data-can-expand={canExpand()}>
       <Show when={attachments().length > 0}>
       <Show when={attachments().length > 0}>
         <div data-slot="user-message-attachments">
         <div data-slot="user-message-attachments">
           <For each={attachments()}>
           <For each={attachments()}>
@@ -365,8 +388,16 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
         </div>
         </div>
       </Show>
       </Show>
       <Show when={text()}>
       <Show when={text()}>
-        <div data-slot="user-message-text">
+        <div data-slot="user-message-text" ref={(el) => (textRef = el)}>
           <HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
           <HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
+          <button
+            data-slot="user-message-expand"
+            type="button"
+            aria-label={expanded() ? "Collapse message" : "Expand message"}
+            onClick={() => setExpanded((v) => !v)}
+          >
+            <Icon name="chevron-down" size="small" />
+          </button>
           <div data-slot="user-message-copy-wrapper">
           <div data-slot="user-message-copy-wrapper">
             <Tooltip value={copied() ? "Copied!" : "Copy"} placement="top" gutter={8}>
             <Tooltip value={copied() ? "Copied!" : "Copy"} placement="top" gutter={8}>
               <IconButton icon={copied() ? "check" : "copy"} variant="secondary" onClick={handleCopy} />
               <IconButton icon={copied() ? "check" : "copy"} variant="secondary" onClick={handleCopy} />

+ 78 - 17
packages/ui/src/components/session-turn.css

@@ -44,23 +44,33 @@
     }
     }
   }
   }
 
 
-  [data-slot="session-turn-sticky-title"] {
-    width: 100%;
+  [data-slot="session-turn-sticky"] {
+    width: calc(100% + 9px);
     position: sticky;
     position: sticky;
-    top: 0;
+    top: var(--session-title-height, 0px);
+    z-index: 20;
     background-color: var(--background-stronger);
     background-color: var(--background-stronger);
-    z-index: 21;
+    margin-left: -9px;
+    padding-left: 9px;
+    padding-bottom: 12px;
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+
+    &::before {
+      content: "";
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      left: 0;
+      right: 0;
+      background-color: var(--background-stronger);
+      z-index: -1;
+    }
   }
   }
 
 
   [data-slot="session-turn-response-trigger"] {
   [data-slot="session-turn-response-trigger"] {
-    position: sticky;
-    top: calc(var(--sticky-header-height, 0px));
-    background-color: var(--background-stronger);
-    z-index: 20;
-    width: calc(100% + 9px);
-    margin-left: -9px;
-    padding-left: 9px;
-    padding-bottom: 8px;
+    width: fit-content;
   }
   }
 
 
   [data-slot="session-turn-message-header"] {
   [data-slot="session-turn-message-header"] {
@@ -75,6 +85,61 @@
     max-width: 100%;
     max-width: 100%;
   }
   }
 
 
+  [data-component="user-message"] [data-slot="user-message-text"] {
+    max-height: var(--user-message-collapsed-height, 64px);
+  }
+
+  [data-component="user-message"][data-expanded="true"] [data-slot="user-message-text"] {
+    max-height: none;
+  }
+
+  [data-component="user-message"][data-can-expand="true"] [data-slot="user-message-text"] {
+    padding-right: 36px;
+    padding-bottom: 28px;
+  }
+
+  [data-component="user-message"] [data-slot="user-message-text"] [data-slot="user-message-expand"] {
+    display: none;
+    position: absolute;
+    bottom: 6px;
+    right: 6px;
+    padding: 0;
+  }
+
+  [data-component="user-message"][data-can-expand="true"]
+    [data-slot="user-message-text"]
+    [data-slot="user-message-expand"],
+  [data-component="user-message"][data-expanded="true"]
+    [data-slot="user-message-text"]
+    [data-slot="user-message-expand"] {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    height: 22px;
+    width: 22px;
+    border: none;
+    border-radius: 6px;
+    background: transparent;
+    cursor: pointer;
+    color: var(--text-weak);
+
+    [data-slot="icon-svg"] {
+      transition: transform 0.15s ease;
+    }
+  }
+
+  [data-component="user-message"][data-expanded="true"]
+    [data-slot="user-message-text"]
+    [data-slot="user-message-expand"]
+    [data-slot="icon-svg"] {
+    transform: rotate(180deg);
+  }
+
+  [data-component="user-message"] [data-slot="user-message-text"] [data-slot="user-message-expand"]:hover {
+    background: var(--surface-raised-base);
+    color: var(--text-base);
+  }
+
   [data-slot="session-turn-user-badges"] {
   [data-slot="session-turn-user-badges"] {
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
@@ -266,11 +331,7 @@
   }
   }
 
 
   [data-component="sticky-accordion-header"] {
   [data-component="sticky-accordion-header"] {
-    top: var(--sticky-header-height, 40px);
-
-    &[data-expanded]::before {
-      top: calc(-1 * var(--sticky-header-height, 40px));
-    }
+    position: static;
   }
   }
 
 
   [data-slot="session-turn-accordion-trigger-content"] {
   [data-slot="session-turn-accordion-trigger-content"] {

+ 41 - 120
packages/ui/src/components/session-turn.tsx

@@ -13,17 +13,13 @@ import { getDirectory, getFilename } from "@opencode-ai/util/path"
 
 
 import { Binary } from "@opencode-ai/util/binary"
 import { Binary } from "@opencode-ai/util/binary"
 import { createEffect, createMemo, createSignal, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js"
 import { createEffect, createMemo, createSignal, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js"
-import { createResizeObserver } from "@solid-primitives/resize-observer"
 import { DiffChanges } from "./diff-changes"
 import { DiffChanges } from "./diff-changes"
-import { Typewriter } from "./typewriter"
 import { Message, Part } from "./message-part"
 import { Message, Part } from "./message-part"
 import { Markdown } from "./markdown"
 import { Markdown } from "./markdown"
 import { Accordion } from "./accordion"
 import { Accordion } from "./accordion"
 import { StickyAccordionHeader } from "./sticky-accordion-header"
 import { StickyAccordionHeader } from "./sticky-accordion-header"
 import { FileIcon } from "./file-icon"
 import { FileIcon } from "./file-icon"
 import { Icon } from "./icon"
 import { Icon } from "./icon"
-import { ProviderIcon } from "./provider-icon"
-import type { IconName } from "./provider-icons/types"
 import { IconButton } from "./icon-button"
 import { IconButton } from "./icon-button"
 import { Tooltip } from "./tooltip"
 import { Tooltip } from "./tooltip"
 import { Card } from "./card"
 import { Card } from "./card"
@@ -331,8 +327,6 @@ export function SessionTurn(
 
 
   const response = createMemo(() => lastTextPart()?.text)
   const response = createMemo(() => lastTextPart()?.text)
   const responsePartId = createMemo(() => lastTextPart()?.id)
   const responsePartId = createMemo(() => lastTextPart()?.id)
-  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 hasDiffs = createMemo(() => (data.store.session_diff?.[props.sessionID]?.length ?? 0) > 0)
   const hideResponsePart = createMemo(() => !working() && !!responsePartId())
   const hideResponsePart = createMemo(() => !working() && !!responsePartId())
 
 
@@ -371,15 +365,11 @@ export function SessionTurn(
   const diffBatch = 20
   const diffBatch = 20
 
 
   const [store, setStore] = createStore({
   const [store, setStore] = createStore({
-    stickyTitleRef: undefined as HTMLDivElement | undefined,
-    stickyTriggerRef: undefined as HTMLDivElement | undefined,
-    stickyHeaderHeight: 0,
     retrySeconds: 0,
     retrySeconds: 0,
     diffsOpen: [] as string[],
     diffsOpen: [] as string[],
     diffLimit: diffInit,
     diffLimit: diffInit,
     status: rawStatus(),
     status: rawStatus(),
     duration: duration(),
     duration: duration(),
-    titleShown: false,
   })
   })
 
 
   createEffect(
   createEffect(
@@ -393,18 +383,6 @@ 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(() => {
   createEffect(() => {
     const r = retry()
     const r = retry()
     if (!r) {
     if (!r) {
@@ -420,22 +398,6 @@ export function SessionTurn(
     onCleanup(() => clearInterval(timer))
     onCleanup(() => clearInterval(timer))
   })
   })
 
 
-  createResizeObserver(
-    () => store.stickyTitleRef,
-    ({ height }) => {
-      const triggerHeight = store.stickyTriggerRef?.offsetHeight ?? 0
-      setStore("stickyHeaderHeight", height + triggerHeight + 8)
-    },
-  )
-
-  createResizeObserver(
-    () => store.stickyTriggerRef,
-    ({ height }) => {
-      const titleHeight = store.stickyTitleRef?.offsetHeight ?? 0
-      setStore("stickyHeaderHeight", titleHeight + height + 8)
-    },
-  )
-
   createEffect(() => {
   createEffect(() => {
     const timer = setInterval(() => {
     const timer = setInterval(() => {
       setStore("duration", duration())
       setStore("duration", duration())
@@ -491,99 +453,58 @@ export function SessionTurn(
                 data-message={msg().id}
                 data-message={msg().id}
                 data-slot="session-turn-message-container"
                 data-slot="session-turn-message-container"
                 class={props.classes?.container}
                 class={props.classes?.container}
-                style={{ "--sticky-header-height": `${store.stickyHeaderHeight}px` }}
               >
               >
                 <Switch>
                 <Switch>
                   <Match when={isShellMode()}>
                   <Match when={isShellMode()}>
                     <Part part={shellModePart()!} message={msg()} defaultOpen />
                     <Part part={shellModePart()!} message={msg()} defaultOpen />
                   </Match>
                   </Match>
                   <Match when={true}>
                   <Match when={true}>
-                    <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">
+                    <div data-slot="session-turn-sticky">
+                      {/* User Message */}
+                      <div data-slot="session-turn-message-content">
+                        <Message message={msg()} parts={parts()} />
+                      </div>
+
+                      {/* Trigger (sticky) */}
+                      <Show when={working() || hasSteps()}>
+                        <div data-slot="session-turn-response-trigger">
+                          <Button
+                            data-expandable={assistantMessages().length > 0}
+                            data-slot="session-turn-collapsible-trigger-content"
+                            variant="ghost"
+                            size="small"
+                            onClick={props.onStepsExpandedToggle ?? (() => {})}
+                          >
+                            <Show when={working()}>
+                              <Spinner />
+                            </Show>
                             <Switch>
                             <Switch>
-                              <Match when={working()}>
-                                <Typewriter as="h1" text={sessionTitle()} data-slot="session-turn-typewriter" />
-                              </Match>
-                              <Match when={true}>
-                                <h1>{sessionTitle()}</h1>
+                              <Match when={retry()}>
+                                <span data-slot="session-turn-retry-message">
+                                  {(() => {
+                                    const r = retry()
+                                    if (!r) return ""
+                                    return r.message.length > 60 ? r.message.slice(0, 60) + "..." : r.message
+                                  })()}
+                                </span>
+                                <span data-slot="session-turn-retry-seconds">
+                                  · retrying {store.retrySeconds > 0 ? `in ${store.retrySeconds}s ` : ""}
+                                </span>
+                                <span data-slot="session-turn-retry-attempt">(#{retry()?.attempt})</span>
                               </Match>
                               </Match>
+                              <Match when={working()}>{store.status ?? "Considering next steps"}</Match>
+                              <Match when={props.stepsExpanded}>Hide steps</Match>
+                              <Match when={!props.stepsExpanded}>Show steps</Match>
                             </Switch>
                             </Switch>
-                          </div>
+                            <span>·</span>
+                            <span>{store.duration}</span>
+                            <Show when={assistantMessages().length > 0}>
+                              <Icon name="chevron-grabber-vertical" size="small" />
+                            </Show>
+                          </Button>
                         </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()} />
+                      </Show>
                     </div>
                     </div>
-
-                    {/* Trigger (sticky) */}
-                    <Show when={working() || hasSteps()}>
-                      <div ref={(el) => setStore("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
-                        <Button
-                          data-expandable={assistantMessages().length > 0}
-                          data-slot="session-turn-collapsible-trigger-content"
-                          variant="ghost"
-                          size="small"
-                          onClick={props.onStepsExpandedToggle ?? (() => {})}
-                        >
-                          <Show when={working()}>
-                            <Spinner />
-                          </Show>
-                          <Switch>
-                            <Match when={retry()}>
-                              <span data-slot="session-turn-retry-message">
-                                {(() => {
-                                  const r = retry()
-                                  if (!r) return ""
-                                  return r.message.length > 60 ? r.message.slice(0, 60) + "..." : r.message
-                                })()}
-                              </span>
-                              <span data-slot="session-turn-retry-seconds">
-                                · retrying {store.retrySeconds > 0 ? `in ${store.retrySeconds}s ` : ""}
-                              </span>
-                              <span data-slot="session-turn-retry-attempt">(#{retry()?.attempt})</span>
-                            </Match>
-                            <Match when={working()}>{store.status ?? "Considering next steps"}</Match>
-                            <Match when={props.stepsExpanded}>Hide steps</Match>
-                            <Match when={!props.stepsExpanded}>Show steps</Match>
-                          </Switch>
-                          <span>·</span>
-                          <span>{store.duration}</span>
-                          <Show when={assistantMessages().length > 0}>
-                            <Icon name="chevron-grabber-vertical" size="small" />
-                          </Show>
-                        </Button>
-                      </div>
-                    </Show>
                     {/* Response */}
                     {/* Response */}
                     <Show when={props.stepsExpanded && assistantMessages().length > 0}>
                     <Show when={props.stepsExpanded && assistantMessages().length > 0}>
                       <div data-slot="session-turn-collapsible-content-inner">
                       <div data-slot="session-turn-collapsible-content-inner">