Initial Release: OpenQode Public Alpha v1.3
This commit is contained in:
107
bin/tui-stream-buffer.mjs
Normal file
107
bin/tui-stream-buffer.mjs
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Streaming Buffer Hook for OpenQode TUI
|
||||
*
|
||||
* Prevents "reflow per token" chaos by:
|
||||
* 1. Buffering incoming tokens
|
||||
* 2. Flushing on newlines or after 50ms interval
|
||||
* 3. Providing stable committed content for rendering
|
||||
*/
|
||||
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* useStreamBuffer - Stable streaming text buffer
|
||||
*
|
||||
* Instead of re-rendering on every token, this hook:
|
||||
* - Accumulates tokens in a pending buffer
|
||||
* - Commits to state on newlines or 50ms timeout
|
||||
* - Prevents mid-word reflows and jitter
|
||||
*
|
||||
* @returns {Object} { committed, pushToken, flushNow, reset }
|
||||
*/
|
||||
export function useStreamBuffer(flushInterval = 50) {
|
||||
const [committed, setCommitted] = useState('');
|
||||
const pendingRef = useRef('');
|
||||
const flushTimerRef = useRef(null);
|
||||
|
||||
// Push a token to the pending buffer
|
||||
const pushToken = useCallback((token) => {
|
||||
pendingRef.current += token;
|
||||
|
||||
// Flush immediately on newline
|
||||
if (token.includes('\n')) {
|
||||
if (flushTimerRef.current) {
|
||||
clearTimeout(flushTimerRef.current);
|
||||
flushTimerRef.current = null;
|
||||
}
|
||||
setCommitted(prev => prev + pendingRef.current);
|
||||
pendingRef.current = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule flush if not already pending
|
||||
if (!flushTimerRef.current) {
|
||||
flushTimerRef.current = setTimeout(() => {
|
||||
setCommitted(prev => prev + pendingRef.current);
|
||||
pendingRef.current = '';
|
||||
flushTimerRef.current = null;
|
||||
}, flushInterval);
|
||||
}
|
||||
}, [flushInterval]);
|
||||
|
||||
// Force immediate flush
|
||||
const flushNow = useCallback(() => {
|
||||
if (flushTimerRef.current) {
|
||||
clearTimeout(flushTimerRef.current);
|
||||
flushTimerRef.current = null;
|
||||
}
|
||||
if (pendingRef.current) {
|
||||
setCommitted(prev => prev + pendingRef.current);
|
||||
pendingRef.current = '';
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Reset buffer (for new messages)
|
||||
const reset = useCallback(() => {
|
||||
if (flushTimerRef.current) {
|
||||
clearTimeout(flushTimerRef.current);
|
||||
flushTimerRef.current = null;
|
||||
}
|
||||
pendingRef.current = '';
|
||||
setCommitted('');
|
||||
}, []);
|
||||
|
||||
// Get current total (committed + pending, for display during active streaming)
|
||||
const getTotal = useCallback(() => {
|
||||
return committed + pendingRef.current;
|
||||
}, [committed]);
|
||||
|
||||
return {
|
||||
committed,
|
||||
pushToken,
|
||||
flushNow,
|
||||
reset,
|
||||
getTotal,
|
||||
isPending: pendingRef.current.length > 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize debounce hook
|
||||
* Only reflows content after terminal resize settles
|
||||
*/
|
||||
export function useResizeDebounce(callback, delay = 150) {
|
||||
const timerRef = useRef(null);
|
||||
|
||||
return useCallback((cols, rows) => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
timerRef.current = setTimeout(() => {
|
||||
callback(cols, rows);
|
||||
timerRef.current = null;
|
||||
}, delay);
|
||||
}, [callback, delay]);
|
||||
}
|
||||
|
||||
export default { useStreamBuffer, useResizeDebounce };
|
||||
Reference in New Issue
Block a user