fix: improve Windows UX and stabilize chat/session behavior across runtimes (#693)
* fix: preserve unsent prompt when adding editor context in VS Code * fix: append Add to chat selections as markdown blocks with stable spacing Convert selected assistant content to markdown before appending Wrap each Add to chat selection in an `md` fenced block Preserve multiline composer formatting across repeated appends * fix: normalize persisted Windows paths to prevent identity mismatches * fix: hide Windows subprocess console popups across server tasks Hide OpenCode startup and shell command child windows in the web server Apply windowsHide to cloudflared and skills-catalog git subprocesses Cover remaining git service exec paths that could surface console windows * fix: restore chat auto re-pin when reaching bottom Re-pin now triggers when scrolling back into the bottom zone, not only via the button. Upward user scroll intent still unpins immediately and is not overridden by re-pin. Unified bottom/re-pin threshold logic to reduce sensitivity mismatches. * fix: restore chat scroll release on mobile during streaming Restores pinned-scroll release on touch scroll up so mobile users can leave auto-follow while streaming. Improves re-pin behavior near bottom to avoid sticky or inconsistent pin states. Includes related chat UI and dependency updates in the same change set. * fix: hide daemon startup probe consoles on Windows * fix: prevent pinned scroll tug-of-war during streaming * fix: prefer git.exe to avoid Windows diff popup flashes * fix: prefer git.exe discovery in Windows git flows * fix: avoid where probes in Windows git resolution * fix: avoid update-check subprocess flashes on Windows * fix: normalize read file path labels * feat: add OpenChamber defaults and improve theme ports Add new OpenChamber light and dark themes Regenerate imported themes with stronger surface mapping Set OpenChamber themes as the default top options * fix: stabilize chat pin and unpin behavior during streaming Restores reliable unpin on upward wheel and touch gestures while auto-follow is active. Prevents immediate re-pin while the user is actively scrolling upward near the bottom. Keeps smooth follow-to-bottom behavior while reducing scroll tug-of-war. * fix: suppress Windows command popups in VSCode runtime processes Hide spawned git and server process windows in VS Code runtime Extend hidden-window handling to server port cleanup and reveal commands Keep behavior unchanged on non-Windows platforms
This commit is contained in:
committed by
GitHub
parent
a07c068b66
commit
3123de5f43
@@ -72,8 +72,6 @@ const PROGRAMMATIC_SCROLL_SUPPRESS_MS = 200;
|
||||
const DIRECT_SCROLL_INTENT_WINDOW_MS = 250;
|
||||
// Threshold for re-pinning: 10% of container height (matches bottom spacer)
|
||||
const PIN_THRESHOLD_RATIO = 0.10;
|
||||
const REPIN_BLOCK_AFTER_RELEASE_MS = 4000;
|
||||
const STRICT_REPIN_DISTANCE_PX = 160;
|
||||
const SORTED_PIN_THRESHOLD_PX = 24;
|
||||
|
||||
export const useChatScrollManager = ({
|
||||
@@ -113,7 +111,6 @@ export const useChatScrollManager = ({
|
||||
const suppressUserScrollUntilRef = React.useRef<number>(0);
|
||||
const lastDirectScrollIntentAtRef = React.useRef<number>(0);
|
||||
const isPinnedRef = React.useRef(true);
|
||||
const repinBlockedUntilRef = React.useRef<number>(0);
|
||||
const lastScrollTopRef = React.useRef<number>(0);
|
||||
const touchLastYRef = React.useRef<number | null>(null);
|
||||
|
||||
@@ -127,10 +124,6 @@ export const useChatScrollManager = ({
|
||||
return container.scrollHeight - container.scrollTop - container.clientHeight;
|
||||
}, []);
|
||||
|
||||
const isStrictlyAtBottom = React.useCallback((distanceFromBottom: number) => {
|
||||
return distanceFromBottom <= STRICT_REPIN_DISTANCE_PX;
|
||||
}, []);
|
||||
|
||||
const updatePinnedState = React.useCallback((newPinned: boolean) => {
|
||||
if (isPinnedRef.current !== newPinned) {
|
||||
isPinnedRef.current = newPinned;
|
||||
@@ -148,9 +141,6 @@ export const useChatScrollManager = ({
|
||||
}, [markProgrammaticScroll, scrollEngine]);
|
||||
|
||||
const scrollPinnedToBottom = React.useCallback(() => {
|
||||
if (Date.now() < repinBlockedUntilRef.current) {
|
||||
return;
|
||||
}
|
||||
if (streamingMessageId) {
|
||||
scrollToBottomInternal({ followBottom: true });
|
||||
return;
|
||||
@@ -192,7 +182,6 @@ export const useChatScrollManager = ({
|
||||
if (!container) return;
|
||||
|
||||
// Re-pin when explicitly scrolling to bottom
|
||||
repinBlockedUntilRef.current = 0;
|
||||
updatePinnedState(true);
|
||||
|
||||
scrollToBottomInternal(options);
|
||||
@@ -201,7 +190,6 @@ export const useChatScrollManager = ({
|
||||
|
||||
const releasePinnedScroll = React.useCallback(() => {
|
||||
scrollEngine.cancelFollow();
|
||||
repinBlockedUntilRef.current = Date.now() + REPIN_BLOCK_AFTER_RELEASE_MS;
|
||||
updatePinnedState(false);
|
||||
updateScrollButtonVisibility();
|
||||
}, [scrollEngine, updatePinnedState, updateScrollButtonVisibility]);
|
||||
@@ -221,26 +209,19 @@ export const useChatScrollManager = ({
|
||||
|
||||
// Handle pin/unpin logic
|
||||
const currentScrollTop = container.scrollTop;
|
||||
const distanceFromBottom = getDistanceFromBottom();
|
||||
|
||||
const scrollingUp = currentScrollTop < lastScrollTopRef.current;
|
||||
|
||||
// Unpin whenever we move away from bottom.
|
||||
// Also handle programmatic jumps to older content (timeline navigation)
|
||||
// so we don't snap back to bottom on the next content update.
|
||||
if (isPinnedRef.current) {
|
||||
const nearBottom = isNearBottom(distanceFromBottom, getPinThreshold());
|
||||
const scrollingUpByUserIntent = Boolean(!isProgrammatic && event?.isTrusted && hasDirectIntent && scrollingUp);
|
||||
const programmaticJumpAwayFromBottom = Boolean(!event?.isTrusted && scrollingUp && !nearBottom);
|
||||
|
||||
if (scrollingUpByUserIntent || programmaticJumpAwayFromBottom) {
|
||||
// Unpin requires strict user intent check
|
||||
if (event?.isTrusted && !isProgrammatic && hasDirectIntent) {
|
||||
if (scrollingUp && isPinnedRef.current) {
|
||||
updatePinnedState(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-pin only when returning to bottom, not while still scrolling up.
|
||||
if (!isPinnedRef.current && now >= repinBlockedUntilRef.current) {
|
||||
if (event?.isTrusted && !scrollingUp && isStrictlyAtBottom(distanceFromBottom)) {
|
||||
// Re-pin at bottom should always work (even momentum scroll)
|
||||
if (!isPinnedRef.current) {
|
||||
const distanceFromBottom = getDistanceFromBottom();
|
||||
if (!scrollingUp && distanceFromBottom <= getPinThreshold()) {
|
||||
updatePinnedState(true);
|
||||
}
|
||||
}
|
||||
@@ -255,7 +236,6 @@ export const useChatScrollManager = ({
|
||||
currentSessionId,
|
||||
getDistanceFromBottom,
|
||||
getPinThreshold,
|
||||
isStrictlyAtBottom,
|
||||
scrollEngine,
|
||||
sessionMessages.length,
|
||||
updatePinnedState,
|
||||
@@ -275,7 +255,6 @@ export const useChatScrollManager = ({
|
||||
rootHeight: container.clientHeight,
|
||||
});
|
||||
|
||||
// Scrolling up while pinned → unpin and kill follow loop immediately
|
||||
if (isPinnedRef.current && shouldPauseAutoScrollOnWheel({
|
||||
root: container,
|
||||
target: event.target,
|
||||
@@ -283,9 +262,7 @@ export const useChatScrollManager = ({
|
||||
})) {
|
||||
scrollEngine.cancelFollow();
|
||||
updatePinnedState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
}, [scrollEngine, updatePinnedState]);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -383,7 +360,6 @@ export const useChatScrollManager = ({
|
||||
// Maintain pin-to-bottom when content changes
|
||||
React.useEffect(() => {
|
||||
if (!isPinnedRef.current) return;
|
||||
if (Date.now() < repinBlockedUntilRef.current) return;
|
||||
if (isSyncing) return;
|
||||
|
||||
const container = scrollRef.current;
|
||||
@@ -405,7 +381,7 @@ export const useChatScrollManager = ({
|
||||
updateScrollButtonVisibility();
|
||||
|
||||
// Maintain pin when content grows - fast smooth follow
|
||||
if (isPinnedRef.current && Date.now() >= repinBlockedUntilRef.current) {
|
||||
if (isPinnedRef.current) {
|
||||
const distanceFromBottom = getDistanceFromBottom();
|
||||
if (distanceFromBottom > getAutoFollowThreshold()) {
|
||||
scrollPinnedToBottom();
|
||||
@@ -417,7 +393,7 @@ export const useChatScrollManager = ({
|
||||
|
||||
// Also observe children for content changes
|
||||
const childObserver = new MutationObserver(() => {
|
||||
if (isPinnedRef.current && Date.now() >= repinBlockedUntilRef.current) {
|
||||
if (isPinnedRef.current) {
|
||||
const distanceFromBottom = getDistanceFromBottom();
|
||||
if (distanceFromBottom > getAutoFollowThreshold()) {
|
||||
scrollPinnedToBottom();
|
||||
@@ -454,7 +430,7 @@ export const useChatScrollManager = ({
|
||||
updateScrollButtonVisibility();
|
||||
|
||||
// Maintain pin when content changes - fast smooth follow
|
||||
if (isPinnedRef.current && Date.now() >= repinBlockedUntilRef.current) {
|
||||
if (isPinnedRef.current) {
|
||||
const distanceFromBottom = getDistanceFromBottom();
|
||||
if (distanceFromBottom > getAutoFollowThreshold()) {
|
||||
scrollPinnedToBottom();
|
||||
@@ -471,7 +447,7 @@ export const useChatScrollManager = ({
|
||||
const handlers: AnimationHandlers = {
|
||||
onChunk: () => {
|
||||
updateScrollButtonVisibility();
|
||||
if (isPinnedRef.current && Date.now() >= repinBlockedUntilRef.current) {
|
||||
if (isPinnedRef.current) {
|
||||
const distanceFromBottom = getDistanceFromBottom();
|
||||
if (distanceFromBottom > getAutoFollowThreshold()) {
|
||||
scrollPinnedToBottom();
|
||||
@@ -485,7 +461,7 @@ export const useChatScrollManager = ({
|
||||
onAnimationStart: () => {},
|
||||
onAnimatedHeightChange: () => {
|
||||
updateScrollButtonVisibility();
|
||||
if (isPinnedRef.current && Date.now() >= repinBlockedUntilRef.current) {
|
||||
if (isPinnedRef.current) {
|
||||
const distanceFromBottom = getDistanceFromBottom();
|
||||
if (distanceFromBottom > getAutoFollowThreshold()) {
|
||||
scrollPinnedToBottom();
|
||||
|
||||
Reference in New Issue
Block a user