Fix response viewer stream scrolling

This commit is contained in:
Gregory Schier
2025-10-24 14:39:25 -07:00
parent bbf85c953d
commit cf9882b5b9
5 changed files with 46 additions and 37 deletions

View File

@@ -1,3 +1,4 @@
import { activeRequestAtom } from '../hooks/useActiveRequest';
import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import { useHotKey, useSubscribeHotKeys } from '../hooks/useHotKey';

View File

@@ -151,7 +151,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
}, [allEnvironmentVariables, autocompleteVariables]);
// Track a local key for updates. If the default value is changed when the input is not in focus,
// regenerate this to force the field to update.
const [focusedUpdateKey, regenerateFocusedUpdateKey] = useRandomKey();
const [focusedUpdateKey, regenerateFocusedUpdateKey] = useRandomKey('initial');
const forceUpdateKey = `${forceUpdateKeyFromAbove}::${focusedUpdateKey}`;
if (settings && wrapLines === undefined) {
@@ -352,17 +352,6 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
[],
);
// Force input to update when receiving change and not in focus
useLayoutEffect(() => {
const currDoc = cm.current?.view.state.doc.toString() || '';
const nextDoc = defaultValue || '';
const notFocused = !cm.current?.view.hasFocus;
const hasChanged = currDoc !== nextDoc;
if (notFocused && hasChanged) {
regenerateFocusedUpdateKey();
}
}, [defaultValue, regenerateFocusedUpdateKey]);
const [, { focusParamValue }] = useRequestEditor();
const onClickPathParameter = useCallback(
async (name: string) => {
@@ -487,33 +476,24 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
// For read-only mode, update content when `defaultValue` changes
useEffect(
function updateReadOnlyEditor() {
if (!readOnly || cm.current?.view == null || defaultValue == null) return;
// Replace codemirror contents
const currentDoc = cm.current.view.state.doc.toString();
if (defaultValue.startsWith(currentDoc)) {
// If we're just appending, append only the changes. This preserves
// things like scroll position.
cm.current.view.dispatch({
changes: cm.current.view.state.changes({
from: currentDoc.length,
insert: defaultValue.slice(currentDoc.length),
}),
});
} else {
// If we're replacing everything, reset the entire content
cm.current.view.dispatch({
changes: cm.current.view.state.changes({
from: 0,
to: currentDoc.length,
insert: defaultValue,
}),
});
if (readOnly && cm.current?.view != null) {
updateContents(cm.current.view, defaultValue || '');
}
},
[defaultValue, readOnly],
);
// Force input to update when receiving change and not in focus
useLayoutEffect(
function updateNonFocusedEditor() {
const notFocused = !cm.current?.view.hasFocus;
if (notFocused && cm.current != null) {
updateContents(cm.current.view, defaultValue || '');
}
},
[defaultValue, readOnly, regenerateFocusedUpdateKey],
);
// Add bg classes to actions, so they appear over the text
const decoratedActions = useMemo(() => {
const results = [];
@@ -720,3 +700,30 @@ function getCachedEditorState(doc: string, stateKey: string | null) {
function computeFullStateKey(stateKey: string): string {
return `editor.${stateKey}`;
}
function updateContents(view: EditorView, text: string) {
// Replace codemirror contents
const currentDoc = view.state.doc.toString();
if (currentDoc === text) {
return;
} else if (text.startsWith(currentDoc)) {
// If we're just appending, append only the changes. This preserves
// things like scroll position.
view.dispatch({
changes: view.state.changes({
from: currentDoc.length,
insert: text.slice(currentDoc.length),
}),
});
} else {
// If we're replacing everything, reset the entire content
view.dispatch({
changes: view.state.changes({
from: 0,
to: currentDoc.length,
insert: text,
}),
});
}
}

View File

@@ -132,7 +132,7 @@ export function TextViewer({ language, text, response, requestId, pretty, classN
language={language}
actions={actions}
extraExtensions={extraExtensions}
stateKey={null}
stateKey={'response.body.' + response.id}
/>
);
}

View File

@@ -1,8 +1,8 @@
import { useCallback, useState } from 'react';
import { generateId } from '../lib/generateId';
export function useRandomKey() {
const [value, setValue] = useState<string>(generateId());
export function useRandomKey(initialValue?: string) {
const [value, setValue] = useState<string>(initialValue ?? generateId());
const regenerate = useCallback(() => setValue(generateId()), []);
return [value, regenerate] as const;
}

View File

@@ -10,6 +10,7 @@ export function useResponseBodyText({
filter: string | null;
}) {
return useQuery({
placeholderData: (prev) => prev, // Keep previous data on refetch
queryKey: [
'response_body_text',
response.id,