mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
fix: session turn scroll
This commit is contained in:
parent
f254cf76d9
commit
307af10c8b
2 changed files with 62 additions and 54 deletions
|
|
@ -1,5 +1,6 @@
|
|||
[data-component="session-turn"] {
|
||||
/* flex: 1; */
|
||||
--scroll-y: 0px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
|
|
@ -26,18 +27,26 @@
|
|||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
gap: 32px;
|
||||
gap: clamp(8px, calc(42px - var(--scroll-y) * 0.48), 42px);
|
||||
}
|
||||
|
||||
[data-slot="session-turn-sticky-header"] {
|
||||
[data-slot="session-turn-sticky-title"] {
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--background-stronger);
|
||||
z-index: 21;
|
||||
/* padding-bottom: clamp(0px, calc(8px - var(--scroll-y) * 0.16), 8px); */
|
||||
}
|
||||
|
||||
[data-slot="session-turn-response-trigger"] {
|
||||
position: sticky;
|
||||
top: 32px;
|
||||
background-color: var(--background-stronger);
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: calc(100% + 9px);
|
||||
margin-left: -9px;
|
||||
padding-left: 9px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
|
|
@ -49,13 +58,8 @@
|
|||
height: 32px;
|
||||
}
|
||||
|
||||
/* [data-slot="session-turn-message-content"] { */
|
||||
/* } */
|
||||
|
||||
[data-slot="session-turn-response-trigger"] {
|
||||
width: calc(100% + 9px);
|
||||
margin-left: -9px;
|
||||
padding-left: 9px;
|
||||
[data-slot="session-turn-message-content"] {
|
||||
margin-top: -24px;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-message-title"] {
|
||||
|
|
@ -292,6 +296,7 @@
|
|||
[data-slot="session-turn-collapsible"] {
|
||||
gap: 32px;
|
||||
overflow: visible;
|
||||
/* margin-top: clamp(8px, calc(24px - var(--scroll-y) * 0.32), 24px); */
|
||||
}
|
||||
|
||||
[data-slot="session-turn-collapsible-trigger-content"] {
|
||||
|
|
|
|||
|
|
@ -3,18 +3,7 @@ import { useData } from "../context"
|
|||
import { useDiffComponent } from "../context/diff"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
onCleanup,
|
||||
onMount,
|
||||
ParentProps,
|
||||
Show,
|
||||
Switch,
|
||||
} from "solid-js"
|
||||
import { createEffect, createMemo, createSignal, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
import { Typewriter } from "./typewriter"
|
||||
|
|
@ -61,12 +50,15 @@ export function SessionTurn(
|
|||
|
||||
let scrollRef: HTMLDivElement | undefined
|
||||
const [contentRef, setContentRef] = createSignal<HTMLDivElement>()
|
||||
const [stickyHeaderRef, setStickyHeaderRef] = createSignal<HTMLDivElement>()
|
||||
const [stickyTitleRef, setStickyTitleRef] = createSignal<HTMLDivElement>()
|
||||
const [stickyTriggerRef, setStickyTriggerRef] = createSignal<HTMLDivElement>()
|
||||
const [userScrolled, setUserScrolled] = createSignal(false)
|
||||
const [stickyHeaderHeight, setStickyHeaderHeight] = createSignal(0)
|
||||
const [scrollY, setScrollY] = createSignal(0)
|
||||
|
||||
function handleScroll() {
|
||||
if (!scrollRef) return
|
||||
setScrollY(scrollRef.scrollTop)
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef
|
||||
const atBottom = scrollHeight - scrollTop - clientHeight < 50
|
||||
if (!atBottom && working()) {
|
||||
|
|
@ -88,15 +80,24 @@ export function SessionTurn(
|
|||
|
||||
createResizeObserver(contentRef, () => {
|
||||
if (!scrollRef || userScrolled() || !working()) return
|
||||
scrollRef.scrollTop = scrollRef.scrollHeight
|
||||
requestAnimationFrame(() => {
|
||||
if (!scrollRef) return
|
||||
scrollRef.scrollTop = scrollRef.scrollHeight
|
||||
})
|
||||
})
|
||||
|
||||
createResizeObserver(stickyHeaderRef, ({ height }) => {
|
||||
setStickyHeaderHeight(height + 8)
|
||||
createResizeObserver(stickyTitleRef, ({ height }) => {
|
||||
const triggerHeight = stickyTriggerRef()?.offsetHeight ?? 0
|
||||
setStickyHeaderHeight(height + triggerHeight + 8)
|
||||
})
|
||||
|
||||
createResizeObserver(stickyTriggerRef, ({ height }) => {
|
||||
const titleHeight = stickyTitleRef()?.offsetHeight ?? 0
|
||||
setStickyHeaderHeight(titleHeight + height + 8)
|
||||
})
|
||||
|
||||
return (
|
||||
<div data-component="session-turn" class={props.classes?.root}>
|
||||
<div data-component="session-turn" class={props.classes?.root} style={{ "--scroll-y": `${scrollY()}px` }}>
|
||||
<div ref={scrollRef} onScroll={handleScroll} data-slot="session-turn-content" class={props.classes?.content}>
|
||||
<div ref={setContentRef} onClick={handleInteraction}>
|
||||
<Show when={message()}>
|
||||
|
|
@ -250,8 +251,8 @@ export function SessionTurn(
|
|||
class={props.classes?.container}
|
||||
style={{ "--sticky-header-height": `${stickyHeaderHeight()}px` }}
|
||||
>
|
||||
{/* Sticky Header */}
|
||||
<div ref={setStickyHeaderRef} data-slot="session-turn-sticky-header">
|
||||
{/* Title (sticky) */}
|
||||
<div ref={setStickyTitleRef} data-slot="session-turn-sticky-title">
|
||||
<div data-slot="session-turn-message-header">
|
||||
<div data-slot="session-turn-message-title">
|
||||
<Switch>
|
||||
|
|
@ -264,29 +265,31 @@ export function SessionTurn(
|
|||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<div data-slot="session-turn-message-content">
|
||||
<Message message={message()} parts={parts()} />
|
||||
</div>
|
||||
<div data-slot="session-turn-response-trigger">
|
||||
<Button
|
||||
data-slot="session-turn-collapsible-trigger-content"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => setStore("stepsExpanded", !store.stepsExpanded)}
|
||||
>
|
||||
<Show when={working()}>
|
||||
<Spinner />
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={working()}>{store.status ?? "Considering next steps..."}</Match>
|
||||
<Match when={store.stepsExpanded}>Hide steps</Match>
|
||||
<Match when={!store.stepsExpanded}>Show steps</Match>
|
||||
</Switch>
|
||||
<span>·</span>
|
||||
<span>{store.duration}</span>
|
||||
<Icon name="chevron-grabber-vertical" size="small" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* User Message (non-sticky, scrolls under sticky header) */}
|
||||
<div data-slot="session-turn-message-content">
|
||||
<Message message={message()} parts={parts()} />
|
||||
</div>
|
||||
{/* Trigger (sticky) */}
|
||||
<div ref={setStickyTriggerRef} data-slot="session-turn-response-trigger">
|
||||
<Button
|
||||
data-slot="session-turn-collapsible-trigger-content"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => setStore("stepsExpanded", !store.stepsExpanded)}
|
||||
>
|
||||
<Show when={working()}>
|
||||
<Spinner />
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={working()}>{store.status ?? "Considering next steps..."}</Match>
|
||||
<Match when={store.stepsExpanded}>Hide steps</Match>
|
||||
<Match when={!store.stepsExpanded}>Show steps</Match>
|
||||
</Switch>
|
||||
<span>·</span>
|
||||
<span>{store.duration}</span>
|
||||
<Icon name="chevron-grabber-vertical" size="small" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Response */}
|
||||
<Show when={store.stepsExpanded}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue