feat(webui): polish agent output and app discovery

This commit is contained in:
Xubin Ren
2026-07-22 22:42:31 +08:00
parent b189a37648
commit aa8387fb4d
87 changed files with 6225 additions and 2379 deletions
+38 -17
View File
@@ -49,6 +49,7 @@ export function PromptRail({
scrollRef,
}: PromptRailProps) {
const railRef = useRef<HTMLDivElement>(null);
const measuredPromptsRef = useRef<MeasuredPrompt[]>([]);
const promptAnchors = useMemo(() => userPromptAnchors(messages), [messages]);
const [markers, setMarkers] = useState<PromptMarker[]>([]);
const [activePromptId, setActivePromptId] = useState<string | null>(null);
@@ -59,6 +60,7 @@ export function PromptRail({
const nextRailHeight = railRef.current?.clientHeight ?? 0;
if (!scrollEl || promptAnchors.length < MIN_PROMPTS_FOR_RAIL) {
measuredPromptsRef.current = [];
setMarkers([]);
setActivePromptId(null);
return;
@@ -66,17 +68,26 @@ export function PromptRail({
const scrollRange = scrollEl.scrollHeight - scrollEl.clientHeight;
if (scrollRange < RAIL_MIN_SCROLL_RANGE_PX) {
measuredPromptsRef.current = [];
setMarkers([]);
setActivePromptId(null);
return;
}
const measured = measurePrompts(scrollEl, promptAnchors, scrollRange);
measuredPromptsRef.current = measured;
const grouped = groupPromptMarkers(measured, nextRailHeight);
setMarkers(distributeMarkerPositions(grouped, nextRailHeight));
setActivePromptId(activePromptForScroll(measured, scrollEl.scrollTop));
}, [promptAnchors, scrollRef]);
const updateActivePrompt = useCallback(() => {
const scrollEl = scrollRef.current;
if (!scrollEl) return;
const next = activePromptForScroll(measuredPromptsRef.current, scrollEl.scrollTop);
setActivePromptId((current) => current === next ? current : next);
}, [scrollRef]);
useEffect(() => {
let frame = 0;
let remainingFrames = MEASURE_RETRY_FRAMES;
@@ -95,20 +106,26 @@ export function PromptRail({
const scrollEl = scrollRef.current;
if (!scrollEl) return undefined;
let frame = 0;
const schedule = () => {
window.cancelAnimationFrame(frame);
frame = window.requestAnimationFrame(updateMarkers);
let scrollFrame = 0;
let resizeFrame = 0;
const scheduleActivePrompt = () => {
window.cancelAnimationFrame(scrollFrame);
scrollFrame = window.requestAnimationFrame(updateActivePrompt);
};
const scheduleMeasurement = () => {
window.cancelAnimationFrame(resizeFrame);
resizeFrame = window.requestAnimationFrame(updateMarkers);
};
scrollEl.addEventListener("scroll", schedule, { passive: true });
window.addEventListener("resize", schedule);
scrollEl.addEventListener("scroll", scheduleActivePrompt, { passive: true });
window.addEventListener("resize", scheduleMeasurement);
return () => {
window.cancelAnimationFrame(frame);
scrollEl.removeEventListener("scroll", schedule);
window.removeEventListener("resize", schedule);
window.cancelAnimationFrame(scrollFrame);
window.cancelAnimationFrame(resizeFrame);
scrollEl.removeEventListener("scroll", scheduleActivePrompt);
window.removeEventListener("resize", scheduleMeasurement);
};
}, [scrollRef, updateMarkers]);
}, [scrollRef, updateActivePrompt, updateMarkers]);
useEffect(() => {
const scrollEl = scrollRef.current;
@@ -309,16 +326,20 @@ function activePromptForScroll(
scrollTop: number,
): string | null {
if (measured.length === 0) return null;
let active = measured[0];
const cursor = scrollTop + 96;
for (const prompt of measured) {
if (prompt.top <= cursor) {
active = prompt;
continue;
let lower = 0;
let upper = measured.length - 1;
let activeIndex = 0;
while (lower <= upper) {
const middle = Math.floor((lower + upper) / 2);
if (measured[middle].top <= cursor) {
activeIndex = middle;
lower = middle + 1;
} else {
upper = middle - 1;
}
break;
}
return active.id;
return measured[activeIndex].id;
}
function groupedPromptLabel(count: number, latestLabel: string): string {