Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

105 lines
2.9 KiB
TypeScript

import { useCallback, useRef, useState } from "react";
import { findMessageLocatorScrollParent } from "./AgentMessageLocatorRail";
interface TurnDisclosureScrollAnchor {
release: () => void;
}
class TurnDisclosureMotionController {
private readonly movingTurnKeys = new Set<string>();
private scrollAnchor: TurnDisclosureScrollAnchor | null = null;
setTurnMotionActive(
turnKey: string,
active: boolean,
anchorElement?: HTMLElement | null
): boolean {
if (active) {
this.movingTurnKeys.add(turnKey);
if (anchorElement) {
this.scrollAnchor?.release();
this.scrollAnchor = lockDisclosureRowPosition(anchorElement);
}
} else {
this.movingTurnKeys.delete(turnKey);
if (this.movingTurnKeys.size === 0) {
this.scrollAnchor?.release();
this.scrollAnchor = null;
}
}
return this.movingTurnKeys.size > 0;
}
}
function lockDisclosureRowPosition(
anchorElement: HTMLElement
): TurnDisclosureScrollAnchor | null {
const scrollParent = findMessageLocatorScrollParent(anchorElement);
if (!scrollParent) {
return null;
}
const anchorTop = anchorElement.getBoundingClientRect().top;
const previousOverflowAnchor =
scrollParent.style.getPropertyValue("overflow-anchor");
const previousOverflowAnchorPriority =
scrollParent.style.getPropertyPriority("overflow-anchor");
scrollParent.style.setProperty("overflow-anchor", "none");
const preserveAnchorTop = (): void => {
if (!anchorElement.isConnected) {
return;
}
const topDelta = anchorElement.getBoundingClientRect().top - anchorTop;
if (Math.abs(topDelta) < 0.5) {
return;
}
scrollParent.scrollTop += topDelta;
};
scrollParent.addEventListener("scroll", preserveAnchorTop);
return {
release: () => {
preserveAnchorTop();
scrollParent.removeEventListener("scroll", preserveAnchorTop);
if (previousOverflowAnchor) {
scrollParent.style.setProperty(
"overflow-anchor",
previousOverflowAnchor,
previousOverflowAnchorPriority
);
} else {
scrollParent.style.removeProperty("overflow-anchor");
}
}
};
}
export function useTurnDisclosureMotion(): readonly [
active: boolean,
setTurnMotionActive: (
turnKey: string,
active: boolean,
anchorElement?: HTMLElement | null
) => void
] {
const motionControllerRef = useRef<TurnDisclosureMotionController | null>(
null
);
if (!motionControllerRef.current) {
motionControllerRef.current = new TurnDisclosureMotionController();
}
const motionController = motionControllerRef.current;
const [active, setActive] = useState(false);
const setTurnMotionActive = useCallback(
(turnKey: string, moving: boolean, anchorElement?: HTMLElement | null) => {
setActive(
motionController.setTurnMotionActive(turnKey, moving, anchorElement)
);
},
[motionController]
);
return [active, setTurnMotionActive];
}