Browse Source

feat(app): new session layout

Adam 1 month ago
parent
commit
befd0f1636

+ 31 - 22
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>

+ 2 - 2
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",
                                     }}
                                   />

+ 2 - 2
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",
                                     }}
                                   />

+ 9 - 19
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);

+ 63 - 33
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(
@@ -389,6 +393,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) {
@@ -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>