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:
@@ -47,6 +47,7 @@ const EMPTY_PERMISSIONS: PermissionRequest[] = [];
|
|||||||
const EMPTY_QUESTIONS: QuestionRequest[] = [];
|
const EMPTY_QUESTIONS: QuestionRequest[] = [];
|
||||||
const IDLE_SESSION_STATUS = { type: 'idle' as const };
|
const IDLE_SESSION_STATUS = { type: 'idle' as const };
|
||||||
const SESSION_RESELECTED_EVENT = 'openchamber:session-reselected';
|
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 DEFAULT_RETRY_MESSAGE = 'Quota limit reached. Retrying automatically.';
|
||||||
const CHAT_SCROLL_STYLE = {
|
const CHAT_SCROLL_STYLE = {
|
||||||
overflowAnchor: 'none',
|
overflowAnchor: 'none',
|
||||||
@@ -581,12 +582,18 @@ export const ChatContainer: React.FC = () => {
|
|||||||
});
|
});
|
||||||
const { loadEarlier, resumeToBottomInstant, restoreSavedScrollPosition } = timelineController;
|
const { loadEarlier, resumeToBottomInstant, restoreSavedScrollPosition } = timelineController;
|
||||||
|
|
||||||
const runLatestInstantResume = React.useCallback(async () => {
|
const runLatestInstantResume = React.useCallback(async (options?: { force?: boolean }) => {
|
||||||
if (!currentSessionId) {
|
if (!currentSessionId) {
|
||||||
scrollToBottom({ instant: true, force: true });
|
scrollToBottom({ instant: true, force: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.force) {
|
||||||
|
await resumeToBottomInstant();
|
||||||
|
clearRestoreInProgress(currentSessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this session has a saved non-bottom scroll position.
|
// Check if this session has a saved non-bottom scroll position.
|
||||||
const savedMemState = sessionMemoryStateMap.get(currentSessionId);
|
const savedMemState = sessionMemoryStateMap.get(currentSessionId);
|
||||||
const savedPos = savedMemState?.scrollPosition;
|
const savedPos = savedMemState?.scrollPosition;
|
||||||
@@ -606,8 +613,8 @@ export const ChatContainer: React.FC = () => {
|
|||||||
clearRestoreInProgress(currentSessionId);
|
clearRestoreInProgress(currentSessionId);
|
||||||
}, [clearRestoreInProgress, currentSessionId, restoreSavedScrollPosition, resumeToBottomInstant, scrollToBottom, sessionIsWorking, sessionMemoryStateMap]);
|
}, [clearRestoreInProgress, currentSessionId, restoreSavedScrollPosition, resumeToBottomInstant, scrollToBottom, sessionIsWorking, sessionMemoryStateMap]);
|
||||||
|
|
||||||
const resumeToLatestInstant = React.useCallback(() => {
|
const resumeToLatestInstant = React.useCallback((options?: { force?: boolean }) => {
|
||||||
void runLatestInstantResume();
|
void runLatestInstantResume(options);
|
||||||
}, [runLatestInstantResume]);
|
}, [runLatestInstantResume]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -680,6 +687,12 @@ export const ChatContainer: React.FC = () => {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window === 'undefined' || !currentSessionId) return;
|
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 handleSessionReselected = (event: Event) => {
|
||||||
const customEvent = event as CustomEvent<string>;
|
const customEvent = event as CustomEvent<string>;
|
||||||
if (customEvent.detail !== currentSessionId) return;
|
if (customEvent.detail !== currentSessionId) return;
|
||||||
@@ -687,11 +700,13 @@ export const ChatContainer: React.FC = () => {
|
|||||||
void resumeToBottomInstant();
|
void resumeToBottomInstant();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.addEventListener(CHAT_FORCE_SCROLL_BOTTOM_EVENT, handleForceScrollBottom as EventListener);
|
||||||
window.addEventListener(SESSION_RESELECTED_EVENT, handleSessionReselected as EventListener);
|
window.addEventListener(SESSION_RESELECTED_EVENT, handleSessionReselected as EventListener);
|
||||||
return () => {
|
return () => {
|
||||||
|
window.removeEventListener(CHAT_FORCE_SCROLL_BOTTOM_EVENT, handleForceScrollBottom as EventListener);
|
||||||
window.removeEventListener(SESSION_RESELECTED_EVENT, handleSessionReselected as EventListener);
|
window.removeEventListener(SESSION_RESELECTED_EVENT, handleSessionReselected as EventListener);
|
||||||
};
|
};
|
||||||
}, [currentSessionId, isOverflowing, isPinned, isProgrammaticFollowActive, resumeToBottomInstant]);
|
}, [currentSessionId, isOverflowing, isPinned, isProgrammaticFollowActive, resumeToBottomInstant, resumeToLatestInstant]);
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
const container = scrollRef.current;
|
const container = scrollRef.current;
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ const getRuntimeGit = () => {
|
|||||||
return null;
|
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> {
|
export async function checkIsGitRepository(directory: string): Promise<boolean> {
|
||||||
const runtime = getRuntimeGit();
|
const runtime = getRuntimeGit();
|
||||||
if (runtime) return runtime.checkIsGitRepository(directory);
|
if (runtime) return runtime.checkIsGitRepository(directory);
|
||||||
@@ -373,6 +380,8 @@ const runStructuredGenerationInActiveSession = async ({
|
|||||||
throw new Error('Generation prompts are empty');
|
throw new Error('Generation prompts are empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestChatForceScrollBottom(generationSession.sessionId);
|
||||||
|
|
||||||
const response = await opencodeClient.withDirectory(directory, async () => {
|
const response = await opencodeClient.withDirectory(directory, async () => {
|
||||||
return opencodeClient.getApiClient().session.prompt({
|
return opencodeClient.getApiClient().session.prompt({
|
||||||
sessionID: generationSession.sessionId,
|
sessionID: generationSession.sessionId,
|
||||||
|
|||||||
Reference in New Issue
Block a user