From 8541556d7d0f453cf20fe6b19b4ca45f4c9a37e7 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 5 Aug 2026 16:08:50 -0700 Subject: [PATCH] fix(knowledge): scroll the whole chunk editor area instead of the textarea --- .../components/chunk-editor/chunk-editor.tsx | 69 +++++++++++++------ 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-editor/chunk-editor.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-editor/chunk-editor.tsx index 74fd800c91f..fd6e1f23667 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-editor/chunk-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-editor/chunk-editor.tsx @@ -23,6 +23,18 @@ const TOKEN_BG_COLORS = [ 'rgba(217, 70, 239, 0.55)', ] as const +/** + * Collapsing to `auto` is what lets the box shrink, but it also drops the scroll range to zero + * across the `scrollHeight` read, clamping the scroller to the top — so the offset is captured + * and restored in the same frame. + */ +function syncTextareaHeight(textarea: HTMLTextAreaElement, scroller: HTMLElement) { + const { scrollTop } = scroller + textarea.style.height = 'auto' + textarea.style.height = `${textarea.scrollHeight}px` + scroller.scrollTop = scrollTop +} + interface ChunkEditorProps { mode?: 'edit' | 'create' chunk?: ChunkData @@ -49,7 +61,7 @@ export function ChunkEditor({ onCreated, }: ChunkEditorProps) { const textareaRef = useRef(null) - const tokenizedScrollRef = useRef(null) + const scrollRef = useRef(null) const preservedScrollTopRef = useRef(0) const { mutateAsync: updateChunk } = useUpdateChunk() const { mutateAsync: createChunk } = useCreateChunk() @@ -182,22 +194,39 @@ export function ChunkEditor({ [saveRef] ) - const hasToggledTokenizerRef = useRef(false) + const handleTokenizerChange = useCallback((value: boolean) => { + preservedScrollTopRef.current = scrollRef.current?.scrollTop ?? 0 + setTokenizerOn(value) + }, []) - const handleTokenizerChange = useCallback( - (value: boolean) => { - const source = tokenizerOn ? tokenizedScrollRef.current : textareaRef.current - preservedScrollTopRef.current = source?.scrollTop ?? 0 - hasToggledTokenizerRef.current = true - setTokenizerOn(value) - }, - [tokenizerOn] - ) + /** + * The textarea's height is synced to its content so the surrounding container owns the only + * scrollbar, as in the rich markdown editor. + */ + useLayoutEffect(() => { + const textarea = textareaRef.current + const scroller = scrollRef.current + if (!textarea || !scroller) return + syncTextareaHeight(textarea, scroller) + }, [editedContent, tokenizerOn]) + + /** + * The box is `overflow-hidden`, so a width change that re-wraps lines without touching the + * content would clip the tail with no scrollbar to reach it. The scroller is observed rather + * than the textarea because it supplies the width without being the element the callback resizes. + */ + useLayoutEffect(() => { + const textarea = textareaRef.current + const scroller = scrollRef.current + if (!textarea || !scroller) return + const observer = new ResizeObserver(() => syncTextareaHeight(textarea, scroller)) + observer.observe(scroller) + return () => observer.disconnect() + }, [tokenizerOn]) + /** Must run after the measure above, which establishes the scroll range this offset needs. */ useLayoutEffect(() => { - if (!hasToggledTokenizerRef.current) return - const target = tokenizerOn ? tokenizedScrollRef.current : textareaRef.current - if (target) target.scrollTop = preservedScrollTopRef.current + if (scrollRef.current) scrollRef.current.scrollTop = preservedScrollTopRef.current }, [tokenizerOn]) const tokenStrings = useMemo(() => { @@ -214,9 +243,10 @@ export function ChunkEditor({ return (
{ if (e.target === e.currentTarget) textareaRef.current?.focus() }} @@ -226,10 +256,7 @@ export function ChunkEditor({ }} > {tokenizerOn ? ( -
+
{tokenStrings.map((token, index) => ( )}
-
+