Centralize tool call scroll helpers

This commit is contained in:
Shantur Rathore
2025-12-06 22:22:44 +00:00
parent 0b9cce6f86
commit 04f6e362b9
3 changed files with 42 additions and 13 deletions

View File

@@ -9,7 +9,14 @@ import type { DiffViewMode } from "../stores/preferences"
import { sendPermissionResponse } from "../stores/instances" import { sendPermissionResponse } from "../stores/instances"
import type { TextPart, RenderCache } from "../types/message" import type { TextPart, RenderCache } from "../types/message"
import { resolveToolRenderer } from "./tool-call/renderers" import { resolveToolRenderer } from "./tool-call/renderers"
import type { DiffPayload, DiffRenderOptions, MarkdownRenderOptions, ToolCallPart, ToolRendererContext } from "./tool-call/types" import type {
DiffPayload,
DiffRenderOptions,
MarkdownRenderOptions,
ToolCallPart,
ToolRendererContext,
ToolScrollHelpers,
} from "./tool-call/types"
import { getRelativePath, getToolIcon, getToolName, isToolStateCompleted, isToolStateError, isToolStateRunning } from "./tool-call/utils" import { getRelativePath, getToolIcon, getToolName, isToolStateCompleted, isToolStateError, isToolStateRunning } from "./tool-call/utils"
import { getLogger } from "../lib/logger" import { getLogger } from "../lib/logger"
@@ -419,7 +426,20 @@ export default function ToolCall(props: ToolCallProps) {
persistScrollSnapshot(event.currentTarget) persistScrollSnapshot(event.currentTarget)
} }
const scrollHelpers: ToolScrollHelpers = {
registerContainer: (element, options) => {
if (options?.disableTracking) return
initializeScrollContainer(element)
},
handleScroll: handleScrollEvent,
renderSentinel: (options) => {
if (options?.disableTracking) return null
return <div ref={setBottomSentinel} aria-hidden="true" class="tool-call-scroll-sentinel" style={{ height: "1px" }} />
},
}
createEffect(() => { createEffect(() => {
const container = scrollContainer() const container = scrollContainer()
if (!container) return if (!container) return
@@ -565,11 +585,8 @@ export default function ToolCall(props: ToolCallProps) {
return ( return (
<div <div
class="message-text tool-call-markdown tool-call-markdown-large tool-call-diff-shell" class="message-text tool-call-markdown tool-call-markdown-large tool-call-diff-shell"
ref={(element) => { ref={(element) => scrollHelpers.registerContainer(element, { disableTracking: options?.disableScrollTracking })}
if (options?.disableScrollTracking) return onScroll={options?.disableScrollTracking ? undefined : scrollHelpers.handleScroll}
initializeScrollContainer(element)
}}
onScroll={options?.disableScrollTracking ? undefined : handleScrollEvent}
> >
<div class="tool-call-diff-toolbar" role="group" aria-label="Diff view mode"> <div class="tool-call-diff-toolbar" role="group" aria-label="Diff view mode">
<span class="tool-call-diff-toolbar-label">{toolbarLabel}</span> <span class="tool-call-diff-toolbar-label">{toolbarLabel}</span>
@@ -601,9 +618,7 @@ export default function ToolCall(props: ToolCallProps) {
cacheEntryParams={cacheHandle.params()} cacheEntryParams={cacheHandle.params()}
onRendered={handleDiffRendered} onRendered={handleDiffRendered}
/> />
<Show when={!options?.disableScrollTracking}> {scrollHelpers.renderSentinel({ disableTracking: options?.disableScrollTracking })}
<div ref={setBottomSentinel} aria-hidden="true" class="tool-call-scroll-sentinel" style={{ height: "1px" }} />
</Show>
</div> </div>
) )
} }
@@ -630,14 +645,14 @@ export default function ToolCall(props: ToolCallProps) {
} }
return ( return (
<div class={messageClass} ref={(element) => initializeScrollContainer(element)} onScroll={handleScrollEvent}> <div class={messageClass} ref={(element) => scrollHelpers.registerContainer(element)} onScroll={scrollHelpers.handleScroll}>
<Markdown <Markdown
part={markdownPart} part={markdownPart}
isDark={isDark()} isDark={isDark()}
disableHighlight={disableHighlight} disableHighlight={disableHighlight}
onRendered={handleMarkdownRendered} onRendered={handleMarkdownRendered}
/> />
<div ref={setBottomSentinel} aria-hidden="true" class="tool-call-scroll-sentinel" style={{ height: "1px" }} /> {scrollHelpers.renderSentinel()}
</div> </div>
) )
} }
@@ -654,6 +669,7 @@ export default function ToolCall(props: ToolCallProps) {
partVersion: partVersionAccessor, partVersion: partVersionAccessor,
renderMarkdown: renderMarkdownContent, renderMarkdown: renderMarkdownContent,
renderDiff: renderDiffContent, renderDiff: renderDiffContent,
scrollHelpers,
} }
let previousPartVersion: number | undefined let previousPartVersion: number | undefined

View File

@@ -40,7 +40,7 @@ export const taskRenderer: ToolRenderer = {
} }
return base return base
}, },
renderBody({ toolState, toolCall, messageVersion, partVersion }) { renderBody({ toolState, toolCall, messageVersion, partVersion, scrollHelpers }) {
const items = createMemo(() => { const items = createMemo(() => {
// Track the reactive change points so we only recompute when the part/message changes // Track the reactive change points so we only recompute when the part/message changes
messageVersion?.() messageVersion?.()
@@ -63,7 +63,11 @@ export const taskRenderer: ToolRenderer = {
if (items().length === 0) return null if (items().length === 0) return null
return ( return (
<div class="message-text tool-call-markdown tool-call-task-container"> <div
class="message-text tool-call-markdown tool-call-task-container"
ref={(element) => scrollHelpers?.registerContainer(element)}
onScroll={scrollHelpers ? (event) => scrollHelpers.handleScroll(event as Event & { currentTarget: HTMLDivElement }) : undefined}
>
<div class="tool-call-task-summary"> <div class="tool-call-task-summary">
<For each={items()}> <For each={items()}>
{(item) => { {(item) => {
@@ -78,7 +82,9 @@ export const taskRenderer: ToolRenderer = {
}} }}
</For> </For>
</div> </div>
{scrollHelpers?.renderSentinel?.()}
</div> </div>
) )
}, },
} }

View File

@@ -21,6 +21,12 @@ export interface DiffRenderOptions {
label?: string label?: string
} }
export interface ToolScrollHelpers {
registerContainer(element: HTMLDivElement | null, options?: { disableTracking?: boolean }): void
handleScroll(event: Event & { currentTarget: HTMLDivElement }): void
renderSentinel(options?: { disableTracking?: boolean }): JSXElement | null
}
export interface ToolRendererContext { export interface ToolRendererContext {
toolCall: Accessor<ToolCallPart> toolCall: Accessor<ToolCallPart>
toolState: Accessor<ToolState | undefined> toolState: Accessor<ToolState | undefined>
@@ -29,6 +35,7 @@ export interface ToolRendererContext {
partVersion?: Accessor<number | undefined> partVersion?: Accessor<number | undefined>
renderMarkdown(options: MarkdownRenderOptions): JSXElement | null renderMarkdown(options: MarkdownRenderOptions): JSXElement | null
renderDiff(payload: DiffPayload, options?: DiffRenderOptions): JSXElement | null renderDiff(payload: DiffPayload, options?: DiffRenderOptions): JSXElement | null
scrollHelpers?: ToolScrollHelpers
} }
export interface ToolRenderer { export interface ToolRenderer {