fix: keep generated prompts scrolled into view

Force chat to bottom on normal sends
Scroll generated commit prompts into view
Preserve saved scroll restore for passive navigation
This commit is contained in:
Bohdan Triapitsyn
2026-05-01 13:48:07 +03:00
parent 1991736ebf
commit d820e25ea1
2 changed files with 28 additions and 4 deletions
@@ -47,6 +47,7 @@ const EMPTY_PERMISSIONS: PermissionRequest[] = [];
const EMPTY_QUESTIONS: QuestionRequest[] = [];
const IDLE_SESSION_STATUS = { type: 'idle' as const };
const SESSION_RESELECTED_EVENT = 'openchamber:session-reselected';
const CHAT_FORCE_SCROLL_BOTTOM_EVENT = 'openchamber:chat-force-scroll-bottom';
const DEFAULT_RETRY_MESSAGE = 'Quota limit reached. Retrying automatically.';
const CHAT_SCROLL_STYLE = {
overflowAnchor: 'none',
@@ -581,12 +582,18 @@ export const ChatContainer: React.FC = () => {
});
const { loadEarlier, resumeToBottomInstant, restoreSavedScrollPosition } = timelineController;
const runLatestInstantResume = React.useCallback(async () => {
const runLatestInstantResume = React.useCallback(async (options?: { force?: boolean }) => {
if (!currentSessionId) {
scrollToBottom({ instant: true, force: true });
return;
}
if (options?.force) {
await resumeToBottomInstant();
clearRestoreInProgress(currentSessionId);
return;
}
// Check if this session has a saved non-bottom scroll position.
const savedMemState = sessionMemoryStateMap.get(currentSessionId);
const savedPos = savedMemState?.scrollPosition;
@@ -606,8 +613,8 @@ export const ChatContainer: React.FC = () => {
clearRestoreInProgress(currentSessionId);
}, [clearRestoreInProgress, currentSessionId, restoreSavedScrollPosition, resumeToBottomInstant, scrollToBottom, sessionIsWorking, sessionMemoryStateMap]);
const resumeToLatestInstant = React.useCallback(() => {
void runLatestInstantResume();
const resumeToLatestInstant = React.useCallback((options?: { force?: boolean }) => {
void runLatestInstantResume(options);
}, [runLatestInstantResume]);
React.useEffect(() => {
@@ -680,6 +687,12 @@ export const ChatContainer: React.FC = () => {
React.useEffect(() => {
if (typeof window === 'undefined' || !currentSessionId) return;
const handleForceScrollBottom = (event: Event) => {
const customEvent = event as CustomEvent<{ sessionId?: string }>;
if (customEvent.detail?.sessionId && customEvent.detail.sessionId !== currentSessionId) return;
resumeToLatestInstant({ force: true });
};
const handleSessionReselected = (event: Event) => {
const customEvent = event as CustomEvent<string>;
if (customEvent.detail !== currentSessionId) return;
@@ -687,11 +700,13 @@ export const ChatContainer: React.FC = () => {
void resumeToBottomInstant();
};
window.addEventListener(CHAT_FORCE_SCROLL_BOTTOM_EVENT, handleForceScrollBottom as EventListener);
window.addEventListener(SESSION_RESELECTED_EVENT, handleSessionReselected as EventListener);
return () => {
window.removeEventListener(CHAT_FORCE_SCROLL_BOTTOM_EVENT, handleForceScrollBottom as EventListener);
window.removeEventListener(SESSION_RESELECTED_EVENT, handleSessionReselected as EventListener);
};
}, [currentSessionId, isOverflowing, isPinned, isProgrammaticFollowActive, resumeToBottomInstant]);
}, [currentSessionId, isOverflowing, isPinned, isProgrammaticFollowActive, resumeToBottomInstant, resumeToLatestInstant]);
React.useLayoutEffect(() => {
const container = scrollRef.current;
+9
View File
@@ -51,6 +51,13 @@ const getRuntimeGit = () => {
return null;
};
const requestChatForceScrollBottom = (sessionId: string) => {
if (typeof window === 'undefined') return;
window.dispatchEvent(new CustomEvent('openchamber:chat-force-scroll-bottom', {
detail: { sessionId },
}));
};
export async function checkIsGitRepository(directory: string): Promise<boolean> {
const runtime = getRuntimeGit();
if (runtime) return runtime.checkIsGitRepository(directory);
@@ -373,6 +380,8 @@ const runStructuredGenerationInActiveSession = async ({
throw new Error('Generation prompts are empty');
}
requestChatForceScrollBottom(generationSession.sessionId);
const response = await opencodeClient.withDirectory(directory, async () => {
return opencodeClient.getApiClient().session.prompt({
sessionID: generationSession.sessionId,