diff options
Diffstat (limited to 'packages/ui/src/components')
| -rw-r--r-- | packages/ui/src/components/message-part.tsx | 11 | ||||
| -rw-r--r-- | packages/ui/src/components/session-message-rail.css | 2 | ||||
| -rw-r--r-- | packages/ui/src/components/session-turn.tsx | 125 |
3 files changed, 25 insertions, 113 deletions
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 4f0645bf8..973a06230 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -23,6 +23,7 @@ import { DiffChanges } from "./diff-changes" import { Markdown } from "./markdown" import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path" import { checksum } from "@opencode-ai/util/encode" +import { createAutoScroll } from "../hooks" interface Diagnostic { range: { @@ -330,6 +331,7 @@ export interface ToolProps { metadata: Record<string, any> tool: string output?: string + status?: string hideDetails?: boolean defaultOpen?: boolean } @@ -398,6 +400,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { tool={part.tool} metadata={metadata} output={part.state.status === "completed" ? part.state.output : undefined} + status={part.state.status} hideDetails={props.hideDetails} defaultOpen={props.defaultOpen} /> @@ -561,6 +564,10 @@ ToolRegistry.register({ const summary = () => (props.metadata.summary ?? []) as { id: string; tool: string; state: { status: string; title?: string } }[] + const autoScroll = createAutoScroll({ + working: () => true, + }) + return ( <BasicTool icon="task" @@ -571,8 +578,8 @@ ToolRegistry.register({ subtitle: props.input.description, }} > - <div data-component="tool-output" data-scrollable> - <div data-component="task-tools"> + <div ref={autoScroll.scrollRef} onScroll={autoScroll.handleScroll} data-component="tool-output" data-scrollable> + <div ref={autoScroll.contentRef} data-component="task-tools"> <For each={summary()}> {(item) => { const info = getToolInfo(item.tool) diff --git a/packages/ui/src/components/session-message-rail.css b/packages/ui/src/components/session-message-rail.css index dc2352c22..5729e4920 100644 --- a/packages/ui/src/components/session-message-rail.css +++ b/packages/ui/src/components/session-message-rail.css @@ -17,7 +17,7 @@ display: none; } -@container (min-width: 72rem) { +@container (min-width: 88rem) { [data-slot="session-message-rail-compact"] { display: none; } diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 71e6689f6..e49e70864 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -3,7 +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 { batch, createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js" +import { createEffect, createMemo, 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" @@ -19,6 +19,7 @@ import { Button } from "./button" import { Spinner } from "./spinner" import { createStore } from "solid-js/store" import { DateTime, DurationUnit, Interval } from "luxon" +import { createAutoScroll } from "../hooks" function computeStatusFromPart(part: PartType | undefined): string | undefined { if (!part) return undefined @@ -233,17 +234,14 @@ export function SessionTurn( }) } - let scrollRef: HTMLDivElement | undefined + const autoScroll = createAutoScroll({ + working, + onUserInteracted: props.onUserInteracted, + }) + const [store, setStore] = createStore({ - contentRef: undefined as HTMLDivElement | undefined, stickyTitleRef: undefined as HTMLDivElement | undefined, stickyTriggerRef: undefined as HTMLDivElement | undefined, - lastScrollTop: 0, - lastScrollHeight: 0, - lastContainerWidth: 0, - autoScrolled: false, - userScrolled: false, - reflowing: false, stickyHeaderHeight: 0, retrySeconds: 0, status: rawStatus(), @@ -265,104 +263,6 @@ export function SessionTurn( onCleanup(() => clearInterval(timer)) }) - function handleScroll() { - if (!scrollRef || store.autoScrolled) return - - const scrollTop = scrollRef.scrollTop - const scrollHeight = scrollRef.scrollHeight - - if (store.reflowing) { - batch(() => { - setStore("lastScrollTop", scrollTop) - setStore("lastScrollHeight", scrollHeight) - }) - return - } - - const scrollHeightChanged = Math.abs(scrollHeight - store.lastScrollHeight) > 10 - const scrollTopDelta = scrollTop - store.lastScrollTop - - if (scrollHeightChanged && scrollTopDelta < 0) { - const heightRatio = store.lastScrollHeight > 0 ? scrollHeight / store.lastScrollHeight : 1 - const expectedScrollTop = store.lastScrollTop * heightRatio - if (Math.abs(scrollTop - expectedScrollTop) < 100) { - batch(() => { - setStore("lastScrollTop", scrollTop) - setStore("lastScrollHeight", scrollHeight) - }) - return - } - } - - const reset = scrollTop <= 0 && store.lastScrollTop > 0 && working() && !store.userScrolled - if (reset) { - batch(() => { - setStore("lastScrollTop", scrollTop) - setStore("lastScrollHeight", scrollHeight) - }) - requestAnimationFrame(scrollToBottom) - return - } - - const scrolledUp = scrollTop < store.lastScrollTop - 50 && !scrollHeightChanged - if (scrolledUp && working()) { - setStore("userScrolled", true) - props.onUserInteracted?.() - } - - batch(() => { - setStore("lastScrollTop", scrollTop) - setStore("lastScrollHeight", scrollHeight) - }) - } - - function handleInteraction() { - if (working()) { - setStore("userScrolled", true) - props.onUserInteracted?.() - } - } - - function scrollToBottom() { - if (!scrollRef || store.userScrolled || !working()) return - setStore("autoScrolled", true) - requestAnimationFrame(() => { - scrollRef?.scrollTo({ top: scrollRef.scrollHeight, behavior: "smooth" }) - requestAnimationFrame(() => { - batch(() => { - setStore("lastScrollTop", scrollRef?.scrollTop ?? 0) - setStore("lastScrollHeight", scrollRef?.scrollHeight ?? 0) - setStore("autoScrolled", false) - }) - }) - }) - } - - createResizeObserver( - () => store.contentRef, - ({ width }) => { - const widthChanged = Math.abs(width - store.lastContainerWidth) > 5 - if (widthChanged && store.lastContainerWidth > 0) { - setStore("reflowing", true) - requestAnimationFrame(() => { - requestAnimationFrame(() => { - setStore("reflowing", false) - if (working() && !store.userScrolled) { - scrollToBottom() - } - }) - }) - } else if (!store.reflowing) { - scrollToBottom() - } - setStore("lastContainerWidth", width) - }, - ) - - createEffect(() => { - if (!working()) setStore("userScrolled", false) - }) - createResizeObserver( () => store.stickyTitleRef, ({ height }) => { @@ -412,12 +312,17 @@ export function SessionTurn( return ( <div data-component="session-turn" class={props.classes?.root}> - <div ref={scrollRef} onScroll={handleScroll} data-slot="session-turn-content" class={props.classes?.content}> - <div onClick={handleInteraction}> + <div + ref={autoScroll.scrollRef} + onScroll={autoScroll.handleScroll} + data-slot="session-turn-content" + class={props.classes?.content} + > + <div onClick={autoScroll.handleInteraction}> <Show when={message()}> {(msg) => ( <div - ref={(el) => setStore("contentRef", el)} + ref={autoScroll.contentRef} data-message={msg().id} data-slot="session-turn-message-container" class={props.classes?.container} |
