mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Load complete chat history when reaching the top (#2481)
* fix(chat): load older timeline history consistently Pagination depended on scroll events, so short or compacted initial pages could never request older history. Reevaluate edge visibility as layout and history change, and standardize loading indicators on the canonical app spinner. * fix(chat): keep older history loading reliably Use authoritative timeline cursor progress so merged rows cannot stall pagination. Wait for bottom anchoring before evaluating the history edge, and re-arm retries on a fresh upward gesture without automatic failure loops.
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
import { test } from "./fixtures";
|
import { test } from "./fixtures";
|
||||||
import {
|
import {
|
||||||
|
expectLoadedTimelineDoesNotScroll,
|
||||||
expectTimelinePromptNotMounted,
|
expectTimelinePromptNotMounted,
|
||||||
expectTimelinePromptVisible,
|
expectTimelinePromptVisible,
|
||||||
|
holdNextOlderTimelinePage,
|
||||||
|
makeLoadedTimelineFitViewport,
|
||||||
openAgentTimeline,
|
openAgentTimeline,
|
||||||
scrollTimelineUntilOlderHistoryIsReachable,
|
scrollTimelineUntilOlderHistoryIsReachable,
|
||||||
seedLongMockAgentTimeline,
|
seedLongMockAgentTimeline,
|
||||||
@@ -25,4 +28,21 @@ test.describe("Agent timeline pagination", () => {
|
|||||||
await agent.cleanup();
|
await agent.cleanup();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("loads older history when the initial page does not fill the viewport", async ({ page }) => {
|
||||||
|
test.setTimeout(120_000);
|
||||||
|
const agent = await seedLongMockAgentTimeline({ turns: 30 });
|
||||||
|
try {
|
||||||
|
await makeLoadedTimelineFitViewport(page);
|
||||||
|
const olderPage = await holdNextOlderTimelinePage(page, agent);
|
||||||
|
await openAgentTimeline(page, agent);
|
||||||
|
await expectTimelinePromptVisible(page, agent.newestPrompt);
|
||||||
|
await expectLoadedTimelineDoesNotScroll(page);
|
||||||
|
await olderPage.expectLoading();
|
||||||
|
olderPage.release();
|
||||||
|
await expectTimelinePromptVisible(page, agent.oldestPrompt);
|
||||||
|
} finally {
|
||||||
|
await agent.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ interface CreatedAgentTimelineGate {
|
|||||||
waitForForwardedResponse(): Promise<void>;
|
waitForForwardedResponse(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AgentTimelineResponseGate {
|
||||||
|
release(): void;
|
||||||
|
waitForDelayedResponse(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
function parseWebSocketJson(message: WebSocketMessage): unknown {
|
function parseWebSocketJson(message: WebSocketMessage): unknown {
|
||||||
const rawMessage = typeof message === "string" ? message : message.toString("utf8");
|
const rawMessage = typeof message === "string" ? message : message.toString("utf8");
|
||||||
try {
|
try {
|
||||||
@@ -118,3 +123,53 @@ export async function delayCreatedAgentInitialTailResponse(
|
|||||||
waitForForwardedResponse: () => forwardedResponse,
|
waitForForwardedResponse: () => forwardedResponse,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function delayAgentOlderTimelineResponse(
|
||||||
|
page: Page,
|
||||||
|
agentId: string,
|
||||||
|
): Promise<AgentTimelineResponseGate> {
|
||||||
|
let releaseRequested = false;
|
||||||
|
let delayedResponseSeen = false;
|
||||||
|
const delayedForwards: Array<() => void> = [];
|
||||||
|
let resolveDelayedResponse: (() => void) | null = null;
|
||||||
|
const delayedResponse = new Promise<void>((resolve) => {
|
||||||
|
resolveDelayedResponse = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.routeWebSocket(daemonWsRoutePattern(), (ws) => {
|
||||||
|
const server = ws.connectToServer();
|
||||||
|
ws.onMessage((message) => {
|
||||||
|
server.send(message);
|
||||||
|
});
|
||||||
|
server.onMessage((message) => {
|
||||||
|
const sessionMessage = getSessionMessage(message);
|
||||||
|
const payload = sessionMessage ? getPayload(sessionMessage) : null;
|
||||||
|
if (
|
||||||
|
!delayedResponseSeen &&
|
||||||
|
sessionMessage?.type === "fetch_agent_timeline_response" &&
|
||||||
|
payload?.agentId === agentId &&
|
||||||
|
payload.direction === "before"
|
||||||
|
) {
|
||||||
|
delayedResponseSeen = true;
|
||||||
|
resolveDelayedResponse?.();
|
||||||
|
if (releaseRequested) {
|
||||||
|
ws.send(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delayedForwards.push(() => ws.send(message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ws.send(message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
release() {
|
||||||
|
releaseRequested = true;
|
||||||
|
for (const forward of delayedForwards.splice(0)) {
|
||||||
|
forward();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
waitForDelayedResponse: () => delayedResponse,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { expect, type Page } from "@playwright/test";
|
import { expect, type Page } from "@playwright/test";
|
||||||
import { buildAgentRoute, seedMockAgentWorkspace, type MockAgentWorkspace } from "./mock-agent";
|
import { buildAgentRoute, seedMockAgentWorkspace, type MockAgentWorkspace } from "./mock-agent";
|
||||||
|
import {
|
||||||
|
delayAgentOlderTimelineResponse,
|
||||||
|
type AgentTimelineResponseGate,
|
||||||
|
} from "./agent-timeline-gate";
|
||||||
|
|
||||||
interface LongTimelineAgentOptions {
|
interface LongTimelineAgentOptions {
|
||||||
turns: number;
|
turns: number;
|
||||||
@@ -53,6 +57,39 @@ export async function expectTimelinePromptNotMounted(page: Page, prompt: string)
|
|||||||
await expect(page.getByText(prompt, { exact: true })).toHaveCount(0);
|
await expect(page.getByText(prompt, { exact: true })).toHaveCount(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function makeLoadedTimelineFitViewport(page: Page): Promise<void> {
|
||||||
|
await page.setViewportSize({ width: 1280, height: 8_000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expectLoadedTimelineDoesNotScroll(page: Page): Promise<void> {
|
||||||
|
const scroll = page.locator('[data-testid="agent-chat-scroll"]:visible').first();
|
||||||
|
await expect
|
||||||
|
.poll(async () =>
|
||||||
|
scroll.evaluate((element) => {
|
||||||
|
if (!(element instanceof HTMLElement)) {
|
||||||
|
throw new Error("Agent chat scroll element is not an HTMLElement");
|
||||||
|
}
|
||||||
|
return element.scrollHeight <= element.clientHeight;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function holdNextOlderTimelinePage(
|
||||||
|
page: Page,
|
||||||
|
agent: LongTimelineAgent,
|
||||||
|
): Promise<AgentTimelineResponseGate & { expectLoading(): Promise<void> }> {
|
||||||
|
const gate = await delayAgentOlderTimelineResponse(page, agent.agentId);
|
||||||
|
return {
|
||||||
|
...gate,
|
||||||
|
async expectLoading() {
|
||||||
|
await gate.waitForDelayedResponse();
|
||||||
|
await expect(page.getByTestId("load-older-history-spinner")).toBeVisible();
|
||||||
|
await expectTimelinePromptNotMounted(page, agent.oldestPrompt);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function scrollTimelineToOldestLoadedEdge(page: Page): Promise<void> {
|
export async function scrollTimelineToOldestLoadedEdge(page: Page): Promise<void> {
|
||||||
const scroll = page.locator('[data-testid="agent-chat-scroll"]:visible').first();
|
const scroll = page.locator('[data-testid="agent-chat-scroll"]:visible').first();
|
||||||
await scroll.hover();
|
await scroll.hover();
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
createHistoryStartPaginationState,
|
||||||
|
evaluateHistoryStartPagination,
|
||||||
|
rearmHistoryStartPagination,
|
||||||
|
} from "./history-start-pagination";
|
||||||
|
|
||||||
|
const visibleHistoryStart = {
|
||||||
|
distanceFromHistoryStart: 0,
|
||||||
|
hasOlderHistory: true,
|
||||||
|
isLoadingOlderHistory: false,
|
||||||
|
isReady: true,
|
||||||
|
progressKey: "epoch-1:20",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("history start pagination", () => {
|
||||||
|
it("loads once for each authoritative history cursor", () => {
|
||||||
|
const initial = createHistoryStartPaginationState();
|
||||||
|
const first = evaluateHistoryStartPagination(initial, visibleHistoryStart);
|
||||||
|
const duplicate = evaluateHistoryStartPagination(first.state, visibleHistoryStart);
|
||||||
|
const nextPage = evaluateHistoryStartPagination(first.state, {
|
||||||
|
...visibleHistoryStart,
|
||||||
|
progressKey: "epoch-1:10",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect([first.shouldLoad, duplicate.shouldLoad, nextPage.shouldLoad]).toEqual([
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows the same revision again after the user leaves the history edge", () => {
|
||||||
|
const first = evaluateHistoryStartPagination(
|
||||||
|
createHistoryStartPaginationState(),
|
||||||
|
visibleHistoryStart,
|
||||||
|
);
|
||||||
|
const away = evaluateHistoryStartPagination(first.state, {
|
||||||
|
...visibleHistoryStart,
|
||||||
|
distanceFromHistoryStart: 200,
|
||||||
|
});
|
||||||
|
const returned = evaluateHistoryStartPagination(away.state, visibleHistoryStart);
|
||||||
|
|
||||||
|
expect([first.shouldLoad, away.shouldLoad, returned.shouldLoad]).toEqual([true, false, true]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-arms the same cursor when the user makes another upward edge gesture", () => {
|
||||||
|
const first = evaluateHistoryStartPagination(
|
||||||
|
createHistoryStartPaginationState(),
|
||||||
|
visibleHistoryStart,
|
||||||
|
);
|
||||||
|
const retried = evaluateHistoryStartPagination(
|
||||||
|
rearmHistoryStartPagination(first.state),
|
||||||
|
visibleHistoryStart,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect([first.shouldLoad, retried.shouldLoad]).toEqual([true, true]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("waits while history loading is unavailable or already active", () => {
|
||||||
|
const state = createHistoryStartPaginationState();
|
||||||
|
|
||||||
|
expect([
|
||||||
|
evaluateHistoryStartPagination(state, { ...visibleHistoryStart, isReady: false }).shouldLoad,
|
||||||
|
evaluateHistoryStartPagination(state, { ...visibleHistoryStart, hasOlderHistory: false })
|
||||||
|
.shouldLoad,
|
||||||
|
evaluateHistoryStartPagination(state, { ...visibleHistoryStart, isLoadingOlderHistory: true })
|
||||||
|
.shouldLoad,
|
||||||
|
evaluateHistoryStartPagination(state, { ...visibleHistoryStart, progressKey: null })
|
||||||
|
.shouldLoad,
|
||||||
|
]).toEqual([false, false, false, false]);
|
||||||
|
});
|
||||||
|
});
|
||||||
45
packages/app/src/agent-stream/history-start-pagination.ts
Normal file
45
packages/app/src/agent-stream/history-start-pagination.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
export const HISTORY_START_THRESHOLD_PX = 96;
|
||||||
|
|
||||||
|
export interface HistoryStartPaginationState {
|
||||||
|
requestedProgressKey: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createHistoryStartPaginationState(): HistoryStartPaginationState {
|
||||||
|
return { requestedProgressKey: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rearmHistoryStartPagination(
|
||||||
|
_state: HistoryStartPaginationState,
|
||||||
|
): HistoryStartPaginationState {
|
||||||
|
return createHistoryStartPaginationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evaluateHistoryStartPagination(
|
||||||
|
state: HistoryStartPaginationState,
|
||||||
|
input: {
|
||||||
|
distanceFromHistoryStart: number;
|
||||||
|
hasOlderHistory: boolean;
|
||||||
|
isLoadingOlderHistory: boolean;
|
||||||
|
isReady: boolean;
|
||||||
|
progressKey: string | null;
|
||||||
|
},
|
||||||
|
): { state: HistoryStartPaginationState; shouldLoad: boolean } {
|
||||||
|
if (input.distanceFromHistoryStart > HISTORY_START_THRESHOLD_PX) {
|
||||||
|
return { state: createHistoryStartPaginationState(), shouldLoad: false };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!input.isReady ||
|
||||||
|
!input.hasOlderHistory ||
|
||||||
|
input.isLoadingOlderHistory ||
|
||||||
|
input.progressKey === null
|
||||||
|
) {
|
||||||
|
return { state, shouldLoad: false };
|
||||||
|
}
|
||||||
|
if (state.requestedProgressKey === input.progressKey) {
|
||||||
|
return { state, shouldLoad: false };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
state: { requestedProgressKey: input.progressKey },
|
||||||
|
shouldLoad: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
FlatList,
|
FlatList,
|
||||||
ActivityIndicator,
|
|
||||||
Keyboard,
|
Keyboard,
|
||||||
Platform,
|
Platform,
|
||||||
View,
|
View,
|
||||||
@@ -17,8 +16,12 @@ import {
|
|||||||
type ListRenderItemInfo,
|
type ListRenderItemInfo,
|
||||||
type NativeScrollEvent,
|
type NativeScrollEvent,
|
||||||
type NativeSyntheticEvent,
|
type NativeSyntheticEvent,
|
||||||
|
type ViewStyle,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
import { withUnistyles } from "react-native-unistyles";
|
||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import type { StreamItem } from "@/types/stream";
|
import type { StreamItem } from "@/types/stream";
|
||||||
|
import type { Theme } from "@/styles/theme";
|
||||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||||
import { useBottomAnchorController } from "./bottom-anchor-controller";
|
import { useBottomAnchorController } from "./bottom-anchor-controller";
|
||||||
import type { StreamRenderInput, StreamStrategy, StreamViewportHandle } from "./strategy";
|
import type { StreamRenderInput, StreamStrategy, StreamViewportHandle } from "./strategy";
|
||||||
@@ -27,12 +30,28 @@ import {
|
|||||||
isNearBottomForStreamRenderStrategy,
|
isNearBottomForStreamRenderStrategy,
|
||||||
resolveBottomAnchorTransportBehavior,
|
resolveBottomAnchorTransportBehavior,
|
||||||
} from "./strategy";
|
} from "./strategy";
|
||||||
|
import {
|
||||||
|
createHistoryStartPaginationState,
|
||||||
|
evaluateHistoryStartPagination,
|
||||||
|
rearmHistoryStartPagination,
|
||||||
|
} from "./history-start-pagination";
|
||||||
|
|
||||||
const DEFAULT_MAINTAIN_VISIBLE_CONTENT_POSITION = Object.freeze({
|
const DEFAULT_MAINTAIN_VISIBLE_CONTENT_POSITION = Object.freeze({
|
||||||
minIndexForVisible: 0,
|
minIndexForVisible: 0,
|
||||||
autoscrollToTopThreshold: 0,
|
autoscrollToTopThreshold: 0,
|
||||||
});
|
});
|
||||||
const HISTORY_START_THRESHOLD_PX = 96;
|
|
||||||
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
|
const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
});
|
||||||
|
const historyStartSlotStyle: ViewStyle = {
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
minHeight: 32,
|
||||||
|
paddingTop: 4,
|
||||||
|
paddingBottom: 8,
|
||||||
|
};
|
||||||
|
|
||||||
interface HistoryRowDisplayVariants {
|
interface HistoryRowDisplayVariants {
|
||||||
regular?: StreamItem;
|
regular?: StreamItem;
|
||||||
@@ -72,6 +91,7 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat
|
|||||||
onNearHistoryStart,
|
onNearHistoryStart,
|
||||||
isLoadingOlderHistory,
|
isLoadingOlderHistory,
|
||||||
hasOlderHistory,
|
hasOlderHistory,
|
||||||
|
olderHistoryProgressKey,
|
||||||
scrollEnabled,
|
scrollEnabled,
|
||||||
listStyle,
|
listStyle,
|
||||||
baseListContentContainerStyle,
|
baseListContentContainerStyle,
|
||||||
@@ -95,6 +115,7 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat
|
|||||||
const [isNativeViewportSettling, setIsNativeViewportSettling] = useState(false);
|
const [isNativeViewportSettling, setIsNativeViewportSettling] = useState(false);
|
||||||
const nativeViewportSettlingFrameIdRef = useRef<number | null>(null);
|
const nativeViewportSettlingFrameIdRef = useRef<number | null>(null);
|
||||||
const historyStartReadyRef = useRef(false);
|
const historyStartReadyRef = useRef(false);
|
||||||
|
const historyStartPaginationStateRef = useRef(createHistoryStartPaginationState());
|
||||||
|
|
||||||
const historyItems = useMemo(() => {
|
const historyItems = useMemo(() => {
|
||||||
if (segments.historyVirtualized.length === 0) {
|
if (segments.historyVirtualized.length === 0) {
|
||||||
@@ -123,6 +144,23 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat
|
|||||||
),
|
),
|
||||||
[displayStateHistoryRows, historyRowRevision?.contentById],
|
[displayStateHistoryRows, historyRowRevision?.contentById],
|
||||||
);
|
);
|
||||||
|
const evaluateHistoryStart = useStableEvent(() => {
|
||||||
|
const metrics = streamViewportMetricsRef.current;
|
||||||
|
const hasMeasuredViewport =
|
||||||
|
metrics.viewportMeasuredForKey === metrics.containerKey &&
|
||||||
|
metrics.contentMeasuredForKey === metrics.containerKey;
|
||||||
|
const result = evaluateHistoryStartPagination(historyStartPaginationStateRef.current, {
|
||||||
|
distanceFromHistoryStart: metrics.contentHeight - metrics.viewportHeight - metrics.offsetY,
|
||||||
|
hasOlderHistory,
|
||||||
|
isLoadingOlderHistory,
|
||||||
|
isReady: historyStartReadyRef.current && hasMeasuredViewport,
|
||||||
|
progressKey: olderHistoryProgressKey,
|
||||||
|
});
|
||||||
|
historyStartPaginationStateRef.current = result.state;
|
||||||
|
if (result.shouldLoad) {
|
||||||
|
onNearHistoryStart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const clearNativeViewportSettling = useCallback(() => {
|
const clearNativeViewportSettling = useCallback(() => {
|
||||||
if (nativeViewportSettlingFrameIdRef.current !== null) {
|
if (nativeViewportSettlingFrameIdRef.current !== null) {
|
||||||
@@ -222,14 +260,16 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat
|
|||||||
clearNativeViewportSettling();
|
clearNativeViewportSettling();
|
||||||
setIsNativeViewportSettling(false);
|
setIsNativeViewportSettling(false);
|
||||||
historyStartReadyRef.current = false;
|
historyStartReadyRef.current = false;
|
||||||
|
historyStartPaginationStateRef.current = createHistoryStartPaginationState();
|
||||||
const frame = requestAnimationFrame(() => {
|
const frame = requestAnimationFrame(() => {
|
||||||
historyStartReadyRef.current = true;
|
historyStartReadyRef.current = true;
|
||||||
|
evaluateHistoryStart();
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(frame);
|
cancelAnimationFrame(frame);
|
||||||
clearPendingUserScrollEnd();
|
clearPendingUserScrollEnd();
|
||||||
};
|
};
|
||||||
}, [agentId, clearNativeViewportSettling, clearPendingUserScrollEnd]);
|
}, [agentId, clearNativeViewportSettling, clearPendingUserScrollEnd, evaluateHistoryStart]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keyboardEvents = [
|
const keyboardEvents = [
|
||||||
@@ -308,17 +348,7 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat
|
|||||||
const nearBottom = isScrollEventNearBottom(event);
|
const nearBottom = isScrollEventNearBottom(event);
|
||||||
onNearBottomChange(nearBottom);
|
onNearBottomChange(nearBottom);
|
||||||
|
|
||||||
const distanceFromOldestEdge =
|
evaluateHistoryStart();
|
||||||
streamViewportMetricsRef.current.contentHeight -
|
|
||||||
streamViewportMetricsRef.current.viewportHeight -
|
|
||||||
contentOffset.y;
|
|
||||||
if (
|
|
||||||
historyStartReadyRef.current &&
|
|
||||||
hasOlderHistory &&
|
|
||||||
distanceFromOldestEdge <= HISTORY_START_THRESHOLD_PX
|
|
||||||
) {
|
|
||||||
onNearHistoryStart();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isUserScrollActiveRef.current &&
|
!isUserScrollActiveRef.current &&
|
||||||
@@ -336,9 +366,15 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleScrollBeginDrag = useStableEvent(() => {
|
const handleScrollBeginDrag = useStableEvent(() => {
|
||||||
|
if (!isLoadingOlderHistory) {
|
||||||
|
historyStartPaginationStateRef.current = rearmHistoryStartPagination(
|
||||||
|
historyStartPaginationStateRef.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
clearPendingUserScrollEnd();
|
clearPendingUserScrollEnd();
|
||||||
isUserScrollActiveRef.current = true;
|
isUserScrollActiveRef.current = true;
|
||||||
bottomAnchorController.beginUserScroll();
|
bottomAnchorController.beginUserScroll();
|
||||||
|
evaluateHistoryStart();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Defer drag end so momentum can take ownership, but capture the terminal
|
// Defer drag end so momentum can take ownership, but capture the terminal
|
||||||
@@ -395,6 +431,7 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat
|
|||||||
previousViewportHeight,
|
previousViewportHeight,
|
||||||
viewportHeight,
|
viewportHeight,
|
||||||
});
|
});
|
||||||
|
evaluateHistoryStart();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleContentSizeChange = useStableEvent((_width: number, height: number) => {
|
const handleContentSizeChange = useStableEvent((_width: number, height: number) => {
|
||||||
@@ -410,8 +447,13 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat
|
|||||||
previousContentHeight,
|
previousContentHeight,
|
||||||
contentHeight: nextContentHeight,
|
contentHeight: nextContentHeight,
|
||||||
});
|
});
|
||||||
|
evaluateHistoryStart();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
evaluateHistoryStart();
|
||||||
|
}, [evaluateHistoryStart, hasOlderHistory, isLoadingOlderHistory, olderHistoryProgressKey]);
|
||||||
|
|
||||||
const renderItem = useStableEvent(
|
const renderItem = useStableEvent(
|
||||||
({ item, index }: ListRenderItemInfo<StreamItem>): ReactElement | null => {
|
({ item, index }: ListRenderItemInfo<StreamItem>): ReactElement | null => {
|
||||||
const rendered = renderHistoryMountedRow(item, index, historyItems);
|
const rendered = renderHistoryMountedRow(item, index, historyItems);
|
||||||
@@ -451,15 +493,20 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const historyFooterContent = useMemo(() => {
|
const historyFooterContent = useMemo(() => {
|
||||||
if (!isLoadingOlderHistory) {
|
if (!hasOlderHistory && !isLoadingOlderHistory) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<View testID="load-older-history-spinner">
|
<View
|
||||||
<ActivityIndicator size="small" />
|
style={historyStartSlotStyle}
|
||||||
|
testID={isLoadingOlderHistory ? "load-older-history-spinner" : undefined}
|
||||||
|
>
|
||||||
|
{isLoadingOlderHistory ? (
|
||||||
|
<ThemedLoadingSpinner size="small" uniProps={foregroundMutedColorMapping} />
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}, [isLoadingOlderHistory]);
|
}, [hasOlderHistory, isLoadingOlderHistory]);
|
||||||
|
|
||||||
// RN's FlatList strictMode keeps its internal renderItem wrapper stable when
|
// RN's FlatList strictMode keeps its internal renderItem wrapper stable when
|
||||||
// data or the live header changes, preserving the row identities above.
|
// data or the live header changes, preserving the row identities above.
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ describe("createWebStreamStrategy", () => {
|
|||||||
onNearHistoryStart: vi.fn(),
|
onNearHistoryStart: vi.fn(),
|
||||||
isLoadingOlderHistory: false,
|
isLoadingOlderHistory: false,
|
||||||
hasOlderHistory: false,
|
hasOlderHistory: false,
|
||||||
|
olderHistoryProgressKey: null,
|
||||||
scrollEnabled: true,
|
scrollEnabled: true,
|
||||||
listStyle: null,
|
listStyle: null,
|
||||||
baseListContentContainerStyle: null,
|
baseListContentContainerStyle: null,
|
||||||
@@ -176,6 +177,7 @@ describe("createWebStreamStrategy", () => {
|
|||||||
onNearHistoryStart: vi.fn(),
|
onNearHistoryStart: vi.fn(),
|
||||||
isLoadingOlderHistory: false,
|
isLoadingOlderHistory: false,
|
||||||
hasOlderHistory: false,
|
hasOlderHistory: false,
|
||||||
|
olderHistoryProgressKey: null,
|
||||||
scrollEnabled: true,
|
scrollEnabled: true,
|
||||||
listStyle: null,
|
listStyle: null,
|
||||||
baseListContentContainerStyle: null,
|
baseListContentContainerStyle: null,
|
||||||
@@ -231,6 +233,7 @@ describe("createWebStreamStrategy", () => {
|
|||||||
onNearHistoryStart: vi.fn(),
|
onNearHistoryStart: vi.fn(),
|
||||||
isLoadingOlderHistory: false,
|
isLoadingOlderHistory: false,
|
||||||
hasOlderHistory: false,
|
hasOlderHistory: false,
|
||||||
|
olderHistoryProgressKey: null,
|
||||||
scrollEnabled: true,
|
scrollEnabled: true,
|
||||||
listStyle: null,
|
listStyle: null,
|
||||||
baseListContentContainerStyle: null,
|
baseListContentContainerStyle: null,
|
||||||
@@ -314,6 +317,7 @@ describe("createWebStreamStrategy", () => {
|
|||||||
onNearHistoryStart,
|
onNearHistoryStart,
|
||||||
isLoadingOlderHistory: false,
|
isLoadingOlderHistory: false,
|
||||||
hasOlderHistory: true,
|
hasOlderHistory: true,
|
||||||
|
olderHistoryProgressKey: "epoch-1:20",
|
||||||
scrollEnabled: true,
|
scrollEnabled: true,
|
||||||
listStyle: null,
|
listStyle: null,
|
||||||
baseListContentContainerStyle: null,
|
baseListContentContainerStyle: null,
|
||||||
@@ -323,10 +327,6 @@ describe("createWebStreamStrategy", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
||||||
});
|
|
||||||
|
|
||||||
const scrollContainer = container.querySelector('[data-testid="agent-chat-scroll"]');
|
const scrollContainer = container.querySelector('[data-testid="agent-chat-scroll"]');
|
||||||
if (!(scrollContainer instanceof HTMLElement)) {
|
if (!(scrollContainer instanceof HTMLElement)) {
|
||||||
throw new Error("Expected agent chat scroll container");
|
throw new Error("Expected agent chat scroll container");
|
||||||
@@ -335,11 +335,109 @@ describe("createWebStreamStrategy", () => {
|
|||||||
Object.defineProperty(scrollContainer, "scrollHeight", { configurable: true, value: 1200 });
|
Object.defineProperty(scrollContainer, "scrollHeight", { configurable: true, value: 1200 });
|
||||||
Object.defineProperty(scrollContainer, "scrollTop", { configurable: true, value: 64 });
|
Object.defineProperty(scrollContainer, "scrollTop", { configurable: true, value: 64 });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onNearHistoryStart).not.toHaveBeenCalled();
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
|
scrollContainer.dispatchEvent(new WheelEvent("wheel", { deltaY: -1 }));
|
||||||
scrollContainer?.dispatchEvent(new Event("scroll"));
|
scrollContainer?.dispatchEvent(new Event("scroll"));
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(onNearHistoryStart).toHaveBeenCalledTimes(1);
|
expect(onNearHistoryStart).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
scrollContainer.dispatchEvent(new WheelEvent("wheel", { deltaY: -1 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onNearHistoryStart).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("waits for bottom anchoring before evaluating a delayed initial tail", async () => {
|
||||||
|
HTMLElement.prototype.scrollTo = vi.fn(function (
|
||||||
|
this: HTMLElement,
|
||||||
|
options?: ScrollToOptions | number,
|
||||||
|
y?: number,
|
||||||
|
) {
|
||||||
|
const top = typeof options === "object" ? (options.top ?? 0) : (y ?? 0);
|
||||||
|
Object.defineProperty(this, "scrollTop", { configurable: true, value: top });
|
||||||
|
});
|
||||||
|
const strategy = createWebStreamStrategy({ isMobileBreakpoint: true });
|
||||||
|
const viewportRef = React.createRef<StreamViewportHandle>();
|
||||||
|
const onNearHistoryStart = vi.fn();
|
||||||
|
const renderInput = {
|
||||||
|
agentId: "agent",
|
||||||
|
boundary: {
|
||||||
|
hasVirtualizedHistory: false,
|
||||||
|
hasMountedHistory: false,
|
||||||
|
hasLiveHead: false,
|
||||||
|
},
|
||||||
|
renderers: createRenderers(vi.fn()),
|
||||||
|
listEmptyComponent: null,
|
||||||
|
viewportRef,
|
||||||
|
routeBottomAnchorRequest: null,
|
||||||
|
isAuthoritativeHistoryReady: true,
|
||||||
|
onNearBottomChange: vi.fn(),
|
||||||
|
onNearHistoryStart,
|
||||||
|
isLoadingOlderHistory: false,
|
||||||
|
hasOlderHistory: false,
|
||||||
|
olderHistoryProgressKey: null,
|
||||||
|
scrollEnabled: true,
|
||||||
|
listStyle: null,
|
||||||
|
baseListContentContainerStyle: null,
|
||||||
|
forwardListContentContainerStyle: null,
|
||||||
|
};
|
||||||
|
container = document.createElement("div");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
root = createRoot(container);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
root?.render(
|
||||||
|
strategy.render({
|
||||||
|
...renderInput,
|
||||||
|
segments: { historyVirtualized: [], historyMounted: [], liveHead: [] },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||||
|
});
|
||||||
|
|
||||||
|
const scrollContainer = container.querySelector('[data-testid="agent-chat-scroll"]');
|
||||||
|
if (!(scrollContainer instanceof HTMLElement)) {
|
||||||
|
throw new Error("Expected agent chat scroll container");
|
||||||
|
}
|
||||||
|
Object.defineProperty(scrollContainer, "clientHeight", { configurable: true, value: 400 });
|
||||||
|
Object.defineProperty(scrollContainer, "scrollHeight", { configurable: true, value: 1200 });
|
||||||
|
Object.defineProperty(scrollContainer, "scrollTop", { configurable: true, value: 0 });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
root?.render(
|
||||||
|
strategy.render({
|
||||||
|
...renderInput,
|
||||||
|
segments: {
|
||||||
|
historyVirtualized: [],
|
||||||
|
historyMounted: [userMessage(1), userMessage(2)],
|
||||||
|
liveHead: [],
|
||||||
|
},
|
||||||
|
boundary: {
|
||||||
|
hasVirtualizedHistory: false,
|
||||||
|
hasMountedHistory: true,
|
||||||
|
hasLiveHead: false,
|
||||||
|
},
|
||||||
|
hasOlderHistory: true,
|
||||||
|
olderHistoryProgressKey: "epoch-1:20",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onNearHistoryStart).not.toHaveBeenCalled();
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||||
|
});
|
||||||
|
expect(onNearHistoryStart).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps initial route entry anchored when delayed route readiness arrives before user scroll", async () => {
|
it("keeps initial route entry anchored when delayed route readiness arrives before user scroll", async () => {
|
||||||
@@ -378,6 +476,7 @@ describe("createWebStreamStrategy", () => {
|
|||||||
onNearHistoryStart: vi.fn(),
|
onNearHistoryStart: vi.fn(),
|
||||||
isLoadingOlderHistory: false,
|
isLoadingOlderHistory: false,
|
||||||
hasOlderHistory: false,
|
hasOlderHistory: false,
|
||||||
|
olderHistoryProgressKey: null,
|
||||||
scrollEnabled: true,
|
scrollEnabled: true,
|
||||||
listStyle: null,
|
listStyle: null,
|
||||||
baseListContentContainerStyle: null,
|
baseListContentContainerStyle: null,
|
||||||
@@ -485,6 +584,7 @@ describe("createWebStreamStrategy", () => {
|
|||||||
onNearHistoryStart: vi.fn(),
|
onNearHistoryStart: vi.fn(),
|
||||||
isLoadingOlderHistory: false,
|
isLoadingOlderHistory: false,
|
||||||
hasOlderHistory: false,
|
hasOlderHistory: false,
|
||||||
|
olderHistoryProgressKey: null,
|
||||||
scrollEnabled: true,
|
scrollEnabled: true,
|
||||||
listStyle: null,
|
listStyle: null,
|
||||||
baseListContentContainerStyle: null,
|
baseListContentContainerStyle: null,
|
||||||
@@ -581,6 +681,7 @@ describe("createWebStreamStrategy", () => {
|
|||||||
onNearHistoryStart: vi.fn(),
|
onNearHistoryStart: vi.fn(),
|
||||||
isLoadingOlderHistory: false,
|
isLoadingOlderHistory: false,
|
||||||
hasOlderHistory: false,
|
hasOlderHistory: false,
|
||||||
|
olderHistoryProgressKey: null,
|
||||||
scrollEnabled: true,
|
scrollEnabled: true,
|
||||||
listStyle: null,
|
listStyle: null,
|
||||||
baseListContentContainerStyle: null,
|
baseListContentContainerStyle: null,
|
||||||
@@ -679,6 +780,7 @@ describe("createWebStreamStrategy", () => {
|
|||||||
onNearHistoryStart: vi.fn(),
|
onNearHistoryStart: vi.fn(),
|
||||||
isLoadingOlderHistory: false,
|
isLoadingOlderHistory: false,
|
||||||
hasOlderHistory: false,
|
hasOlderHistory: false,
|
||||||
|
olderHistoryProgressKey: null,
|
||||||
scrollEnabled: true,
|
scrollEnabled: true,
|
||||||
listStyle: null,
|
listStyle: null,
|
||||||
baseListContentContainerStyle: null,
|
baseListContentContainerStyle: null,
|
||||||
|
|||||||
@@ -8,11 +8,19 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { ActivityIndicator } from "react-native";
|
|
||||||
import { measureElement as measureVirtualElement, useVirtualizer } from "@tanstack/react-virtual";
|
import { measureElement as measureVirtualElement, useVirtualizer } from "@tanstack/react-virtual";
|
||||||
|
import { withUnistyles } from "react-native-unistyles";
|
||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
|
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||||
|
import type { Theme } from "@/styles/theme";
|
||||||
import { estimateStreamItemHeight } from "./web-virtualization";
|
import { estimateStreamItemHeight } from "./web-virtualization";
|
||||||
import type { StreamRenderInput, StreamStrategy, StreamViewportHandle } from "./strategy";
|
import type { StreamRenderInput, StreamStrategy, StreamViewportHandle } from "./strategy";
|
||||||
import { createStreamStrategy } from "./strategy";
|
import { createStreamStrategy } from "./strategy";
|
||||||
|
import {
|
||||||
|
createHistoryStartPaginationState,
|
||||||
|
evaluateHistoryStartPagination,
|
||||||
|
rearmHistoryStartPagination,
|
||||||
|
} from "./history-start-pagination";
|
||||||
|
|
||||||
interface CreateWebStreamStrategyInput {
|
interface CreateWebStreamStrategyInput {
|
||||||
isMobileBreakpoint: boolean;
|
isMobileBreakpoint: boolean;
|
||||||
@@ -25,7 +33,11 @@ const USER_SCROLL_DELTA_EPSILON = 1;
|
|||||||
const BOTTOM_OVERSCROLL_TOLERANCE_PX = 2;
|
const BOTTOM_OVERSCROLL_TOLERANCE_PX = 2;
|
||||||
const AUTO_SCROLL_BOTTOM_THRESHOLD_PX = 64;
|
const AUTO_SCROLL_BOTTOM_THRESHOLD_PX = 64;
|
||||||
const AUTO_SCROLL_RESUME_THRESHOLD_PX = 1;
|
const AUTO_SCROLL_RESUME_THRESHOLD_PX = 1;
|
||||||
const HISTORY_START_THRESHOLD_PX = 96;
|
|
||||||
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
|
const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
});
|
||||||
|
|
||||||
const historyStartSlotStyle: CSSProperties = {
|
const historyStartSlotStyle: CSSProperties = {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -107,6 +119,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
onNearHistoryStart,
|
onNearHistoryStart,
|
||||||
isLoadingOlderHistory,
|
isLoadingOlderHistory,
|
||||||
hasOlderHistory,
|
hasOlderHistory,
|
||||||
|
olderHistoryProgressKey,
|
||||||
scrollEnabled,
|
scrollEnabled,
|
||||||
isMobileBreakpoint,
|
isMobileBreakpoint,
|
||||||
} = props;
|
} = props;
|
||||||
@@ -133,6 +146,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
const pendingAutoScrollTimeoutRef = useRef<number | null>(null);
|
const pendingAutoScrollTimeoutRef = useRef<number | null>(null);
|
||||||
const pendingVirtualRowMeasureFramesRef = useRef(new Map<Element, number>());
|
const pendingVirtualRowMeasureFramesRef = useRef(new Map<Element, number>());
|
||||||
const historyStartReadyRef = useRef(false);
|
const historyStartReadyRef = useRef(false);
|
||||||
|
const historyStartPaginationStateRef = useRef(createHistoryStartPaginationState());
|
||||||
const shouldUseVirtualizer = segments.historyVirtualized.length > 0;
|
const shouldUseVirtualizer = segments.historyVirtualized.length > 0;
|
||||||
const {
|
const {
|
||||||
renderHistoryVirtualizedRow,
|
renderHistoryVirtualizedRow,
|
||||||
@@ -173,6 +187,25 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
}, [rowVirtualizer]);
|
}, [rowVirtualizer]);
|
||||||
const virtualRows = rowVirtualizer.getVirtualItems();
|
const virtualRows = rowVirtualizer.getVirtualItems();
|
||||||
const virtualTotalSize = rowVirtualizer.getTotalSize();
|
const virtualTotalSize = rowVirtualizer.getTotalSize();
|
||||||
|
const evaluateHistoryStart = useStableEvent(() => {
|
||||||
|
const scrollContainer = scrollContainerRef.current;
|
||||||
|
if (!scrollContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bottomAnchorSettled =
|
||||||
|
!followOutputRef.current || isScrollContainerNearBottom(scrollContainer);
|
||||||
|
const result = evaluateHistoryStartPagination(historyStartPaginationStateRef.current, {
|
||||||
|
distanceFromHistoryStart: scrollContainer.scrollTop,
|
||||||
|
hasOlderHistory,
|
||||||
|
isLoadingOlderHistory,
|
||||||
|
isReady: historyStartReadyRef.current && bottomAnchorSettled,
|
||||||
|
progressKey: olderHistoryProgressKey,
|
||||||
|
});
|
||||||
|
historyStartPaginationStateRef.current = result.state;
|
||||||
|
if (result.shouldLoad) {
|
||||||
|
onNearHistoryStart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const measureVirtualizedRowElement = useCallback(
|
const measureVirtualizedRowElement = useCallback(
|
||||||
(node: HTMLDivElement | null) => {
|
(node: HTMLDivElement | null) => {
|
||||||
@@ -231,8 +264,9 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
scrollElementToBottom(scrollContainer, behavior);
|
scrollElementToBottom(scrollContainer, behavior);
|
||||||
lastKnownScrollTopRef.current = scrollContainer.scrollTop;
|
lastKnownScrollTopRef.current = scrollContainer.scrollTop;
|
||||||
syncNearBottom(scrollContainer, onNearBottomChange);
|
syncNearBottom(scrollContainer, onNearBottomChange);
|
||||||
|
evaluateHistoryStart();
|
||||||
},
|
},
|
||||||
[onNearBottomChange],
|
[evaluateHistoryStart, onNearBottomChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const scheduleStickToBottom = useCallback(() => {
|
const scheduleStickToBottom = useCallback(() => {
|
||||||
@@ -297,24 +331,20 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
|
|
||||||
lastKnownScrollTopRef.current = currentScrollTop;
|
lastKnownScrollTopRef.current = currentScrollTop;
|
||||||
updateScrollMetrics();
|
updateScrollMetrics();
|
||||||
if (
|
evaluateHistoryStart();
|
||||||
historyStartReadyRef.current &&
|
}, [cancelPendingStickToBottom, evaluateHistoryStart, updateScrollMetrics]);
|
||||||
hasOlderHistory &&
|
|
||||||
currentScrollTop <= HISTORY_START_THRESHOLD_PX
|
|
||||||
) {
|
|
||||||
onNearHistoryStart();
|
|
||||||
}
|
|
||||||
}, [cancelPendingStickToBottom, hasOlderHistory, onNearHistoryStart, updateScrollMetrics]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
historyStartPaginationStateRef.current = createHistoryStartPaginationState();
|
||||||
const frame = window.requestAnimationFrame(() => {
|
const frame = window.requestAnimationFrame(() => {
|
||||||
historyStartReadyRef.current = true;
|
historyStartReadyRef.current = true;
|
||||||
|
evaluateHistoryStart();
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
window.cancelAnimationFrame(frame);
|
window.cancelAnimationFrame(frame);
|
||||||
historyStartReadyRef.current = false;
|
historyStartReadyRef.current = false;
|
||||||
};
|
};
|
||||||
}, [props.agentId]);
|
}, [evaluateHistoryStart, props.agentId]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!isActivationReady) {
|
if (!isActivationReady) {
|
||||||
@@ -370,7 +400,12 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateScrollMetrics();
|
updateScrollMetrics();
|
||||||
|
evaluateHistoryStart();
|
||||||
}, [
|
}, [
|
||||||
|
evaluateHistoryStart,
|
||||||
|
hasOlderHistory,
|
||||||
|
isLoadingOlderHistory,
|
||||||
|
olderHistoryProgressKey,
|
||||||
segments.historyMounted.length,
|
segments.historyMounted.length,
|
||||||
segments.historyVirtualized.length,
|
segments.historyVirtualized.length,
|
||||||
segments.liveHead.length,
|
segments.liveHead.length,
|
||||||
@@ -386,8 +421,10 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateScrollMetrics();
|
updateScrollMetrics();
|
||||||
|
evaluateHistoryStart();
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
updateScrollMetrics();
|
updateScrollMetrics();
|
||||||
|
evaluateHistoryStart();
|
||||||
if (!followOutputRef.current) {
|
if (!followOutputRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -400,7 +437,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
return () => {
|
return () => {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
};
|
};
|
||||||
}, [scheduleStickToBottom, updateScrollMetrics]);
|
}, [evaluateHistoryStart, scheduleStickToBottom, updateScrollMetrics]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scrollContainer = scrollContainerRef.current;
|
const scrollContainer = scrollContainerRef.current;
|
||||||
@@ -410,8 +447,14 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
|
|
||||||
const handleWheel = (event: WheelEvent) => {
|
const handleWheel = (event: WheelEvent) => {
|
||||||
if (event.deltaY < 0) {
|
if (event.deltaY < 0) {
|
||||||
|
if (!isLoadingOlderHistory) {
|
||||||
|
historyStartPaginationStateRef.current = rearmHistoryStartPagination(
|
||||||
|
historyStartPaginationStateRef.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
pendingUserScrollUpIntentRef.current = true;
|
pendingUserScrollUpIntentRef.current = true;
|
||||||
cancelPendingStickToBottom();
|
cancelPendingStickToBottom();
|
||||||
|
evaluateHistoryStart();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handlePointerDown = () => {
|
const handlePointerDown = () => {
|
||||||
@@ -434,8 +477,14 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
}
|
}
|
||||||
const previousTouchY = lastTouchClientYRef.current;
|
const previousTouchY = lastTouchClientYRef.current;
|
||||||
if (previousTouchY !== null && touch.clientY > previousTouchY + 1) {
|
if (previousTouchY !== null && touch.clientY > previousTouchY + 1) {
|
||||||
|
if (!isLoadingOlderHistory) {
|
||||||
|
historyStartPaginationStateRef.current = rearmHistoryStartPagination(
|
||||||
|
historyStartPaginationStateRef.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
pendingUserScrollUpIntentRef.current = true;
|
pendingUserScrollUpIntentRef.current = true;
|
||||||
cancelPendingStickToBottom();
|
cancelPendingStickToBottom();
|
||||||
|
evaluateHistoryStart();
|
||||||
}
|
}
|
||||||
lastTouchClientYRef.current = touch.clientY;
|
lastTouchClientYRef.current = touch.clientY;
|
||||||
};
|
};
|
||||||
@@ -464,7 +513,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
scrollContainer.removeEventListener("touchend", handleTouchEnd);
|
scrollContainer.removeEventListener("touchend", handleTouchEnd);
|
||||||
scrollContainer.removeEventListener("touchcancel", handleTouchEnd);
|
scrollContainer.removeEventListener("touchcancel", handleTouchEnd);
|
||||||
};
|
};
|
||||||
}, [cancelPendingStickToBottom, handleDomScroll]);
|
}, [cancelPendingStickToBottom, evaluateHistoryStart, handleDomScroll, isLoadingOlderHistory]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handle: StreamViewportHandle = {
|
const handle: StreamViewportHandle = {
|
||||||
@@ -546,15 +595,20 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
return renderLiveAuxiliary();
|
return renderLiveAuxiliary();
|
||||||
}, [renderLiveAuxiliary]);
|
}, [renderLiveAuxiliary]);
|
||||||
const historyStartSlot = useMemo(() => {
|
const historyStartSlot = useMemo(() => {
|
||||||
if (!isLoadingOlderHistory) {
|
if (!hasOlderHistory && !isLoadingOlderHistory) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div style={historyStartSlotStyle} data-testid="load-older-history-spinner">
|
<div
|
||||||
<ActivityIndicator size="small" />
|
style={historyStartSlotStyle}
|
||||||
|
data-testid={isLoadingOlderHistory ? "load-older-history-spinner" : undefined}
|
||||||
|
>
|
||||||
|
{isLoadingOlderHistory ? (
|
||||||
|
<ThemedLoadingSpinner size="small" uniProps={foregroundMutedColorMapping} />
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [isLoadingOlderHistory]);
|
}, [hasOlderHistory, isLoadingOlderHistory]);
|
||||||
const shouldRenderEmpty =
|
const shouldRenderEmpty =
|
||||||
!boundary.hasMountedHistory &&
|
!boundary.hasMountedHistory &&
|
||||||
!boundary.hasVirtualizedHistory &&
|
!boundary.hasVirtualizedHistory &&
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export interface StreamRenderInput {
|
|||||||
onNearHistoryStart: () => void;
|
onNearHistoryStart: () => void;
|
||||||
isLoadingOlderHistory: boolean;
|
isLoadingOlderHistory: boolean;
|
||||||
hasOlderHistory: boolean;
|
hasOlderHistory: boolean;
|
||||||
|
olderHistoryProgressKey: string | null;
|
||||||
scrollEnabled: boolean;
|
scrollEnabled: boolean;
|
||||||
listStyle: StyleProp<ViewStyle>;
|
listStyle: StyleProp<ViewStyle>;
|
||||||
baseListContentContainerStyle: StyleProp<ViewStyle>;
|
baseListContentContainerStyle: StyleProp<ViewStyle>;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
memo,
|
memo,
|
||||||
@@ -17,7 +18,6 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Pressable,
|
Pressable,
|
||||||
Platform,
|
Platform,
|
||||||
ActivityIndicator,
|
|
||||||
type PressableStateCallbackType,
|
type PressableStateCallbackType,
|
||||||
type StyleProp,
|
type StyleProp,
|
||||||
type ViewStyle,
|
type ViewStyle,
|
||||||
@@ -247,6 +247,7 @@ export interface AgentStreamViewProps {
|
|||||||
historyPagination?: {
|
historyPagination?: {
|
||||||
hasOlder: boolean;
|
hasOlder: boolean;
|
||||||
isLoadingOlder: boolean;
|
isLoadingOlder: boolean;
|
||||||
|
progressKey: string | null;
|
||||||
onLoadOlder: () => void;
|
onLoadOlder: () => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -388,10 +389,11 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
|||||||
agentId,
|
agentId,
|
||||||
toast,
|
toast,
|
||||||
});
|
});
|
||||||
const { isLoadingOlder, hasOlder, loadOlder } = historyPagination
|
const { isLoadingOlder, hasOlder, progressKey, loadOlder } = historyPagination
|
||||||
? {
|
? {
|
||||||
isLoadingOlder: historyPagination.isLoadingOlder,
|
isLoadingOlder: historyPagination.isLoadingOlder,
|
||||||
hasOlder: historyPagination.hasOlder,
|
hasOlder: historyPagination.hasOlder,
|
||||||
|
progressKey: historyPagination.progressKey,
|
||||||
loadOlder: historyPagination.onLoadOlder,
|
loadOlder: historyPagination.onLoadOlder,
|
||||||
}
|
}
|
||||||
: agentHistoryPagination;
|
: agentHistoryPagination;
|
||||||
@@ -1029,6 +1031,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
|||||||
onNearHistoryStart: loadOlder,
|
onNearHistoryStart: loadOlder,
|
||||||
isLoadingOlderHistory: isLoadingOlder,
|
isLoadingOlderHistory: isLoadingOlder,
|
||||||
hasOlderHistory: hasOlder,
|
hasOlderHistory: hasOlder,
|
||||||
|
olderHistoryProgressKey: progressKey,
|
||||||
scrollEnabled: streamScrollEnabled,
|
scrollEnabled: streamScrollEnabled,
|
||||||
listStyle: stylesheet.list,
|
listStyle: stylesheet.list,
|
||||||
baseListContentContainerStyle: stylesheet.listContentContainer,
|
baseListContentContainerStyle: stylesheet.listContentContainer,
|
||||||
@@ -1138,6 +1141,7 @@ function historyPaginationPropsEqual(
|
|||||||
return (
|
return (
|
||||||
left?.hasOlder === right?.hasOlder &&
|
left?.hasOlder === right?.hasOlder &&
|
||||||
left?.isLoadingOlder === right?.isLoadingOlder &&
|
left?.isLoadingOlder === right?.isLoadingOlder &&
|
||||||
|
left?.progressKey === right?.progressKey &&
|
||||||
left?.onLoadOlder === right?.onLoadOlder
|
left?.onLoadOlder === right?.onLoadOlder
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1194,7 +1198,7 @@ function ToolCallSlot({
|
|||||||
return <ToolCall {...rest} onInlineDetailsExpandedChange={handleExpandedChange} />;
|
return <ToolCall {...rest} onInlineDetailsExpandedChange={handleExpandedChange} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
const ThemedCheckIcon = withUnistyles(Check);
|
const ThemedCheckIcon = withUnistyles(Check);
|
||||||
const ThemedXIcon = withUnistyles(X);
|
const ThemedXIcon = withUnistyles(X);
|
||||||
|
|
||||||
@@ -1241,7 +1245,7 @@ function PermissionActionButton({
|
|||||||
return (
|
return (
|
||||||
<Pressable testID={testID} style={pressableStyle} onPress={handlePress} disabled={isResponding}>
|
<Pressable testID={testID} style={pressableStyle} onPress={handlePress} disabled={isResponding}>
|
||||||
{isRespondingAction ? (
|
{isRespondingAction ? (
|
||||||
<ThemedActivityIndicator size="small" uniProps={colorMapping} />
|
<ThemedLoadingSpinner size="small" uniProps={colorMapping} />
|
||||||
) : (
|
) : (
|
||||||
<View style={permissionStyles.optionContent}>
|
<View style={permissionStyles.optionContent}>
|
||||||
<Icon size={14} uniProps={colorMapping} />
|
<Icon size={14} uniProps={colorMapping} />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { View, Text, Pressable, ActivityIndicator } from "react-native";
|
import { View, Text, Pressable } from "react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { X, ArrowUp, RefreshCcw, Check, Mic, Pencil } from "lucide-react-native";
|
import { X, ArrowUp, RefreshCcw, Check, Mic, Pencil } from "lucide-react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -98,7 +99,7 @@ export function DictationControls({
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
{actionsDisabled ? (
|
{actionsDisabled ? (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="small" color={theme.colors.foreground} />
|
<LoadingSpinner size="small" color={theme.colors.foreground} />
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
{!actionsDisabled && isFailed ? (
|
{!actionsDisabled && isFailed ? (
|
||||||
@@ -221,7 +222,7 @@ export function DictationOverlay({
|
|||||||
<View style={overlayStyles.actionButtonsContainer}>
|
<View style={overlayStyles.actionButtonsContainer}>
|
||||||
{actionsDisabled ? (
|
{actionsDisabled ? (
|
||||||
<View style={overlayStyles.loadingContainer}>
|
<View style={overlayStyles.loadingContainer}>
|
||||||
<ActivityIndicator size="small" color={theme.colors.accentForeground} />
|
<LoadingSpinner size="small" color={theme.colors.accentForeground} />
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
{!actionsDisabled && isFailed ? (
|
{!actionsDisabled && isFailed ? (
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ActivityIndicator, Pressable, Text, View } from "react-native";
|
import { Pressable, Text, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { Check, X, XCircle } from "lucide-react-native";
|
import { Check, X, XCircle } from "lucide-react-native";
|
||||||
@@ -69,7 +70,7 @@ export function DownloadToast() {
|
|||||||
<View style={containerStyle} pointerEvents="box-none">
|
<View style={containerStyle} pointerEvents="box-none">
|
||||||
<View style={styles.toast}>
|
<View style={styles.toast}>
|
||||||
{activeDownload.status === "downloading" ? (
|
{activeDownload.status === "downloading" ? (
|
||||||
<ActivityIndicator size="small" color={theme.colors.foreground} />
|
<LoadingSpinner size="small" color={theme.colors.foreground} />
|
||||||
) : null}
|
) : null}
|
||||||
{activeDownload.status === "complete" ? (
|
{activeDownload.status === "complete" ? (
|
||||||
<Check size={18} color={theme.colors.primary} />
|
<Check size={18} color={theme.colors.primary} />
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, type ReactElement, type RefObj
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
|
||||||
FlatList,
|
FlatList,
|
||||||
ListRenderItemInfo,
|
ListRenderItemInfo,
|
||||||
Pressable,
|
Pressable,
|
||||||
@@ -12,7 +11,7 @@ import {
|
|||||||
type StyleProp,
|
type StyleProp,
|
||||||
type ViewStyle,
|
type ViewStyle,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles";
|
||||||
import { WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
|
import { WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import { ChevronDown, Eye, EyeOff, RotateCw } from "lucide-react-native";
|
import { ChevronDown, Eye, EyeOff, RotateCw } from "lucide-react-native";
|
||||||
@@ -24,6 +23,7 @@ import {
|
|||||||
WORKSPACE_FILE_ROW_VERTICAL_PADDING,
|
WORKSPACE_FILE_ROW_VERTICAL_PADDING,
|
||||||
} from "@/components/tree-primitives";
|
} from "@/components/tree-primitives";
|
||||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
|
import type { Theme } from "@/styles/theme";
|
||||||
import type { AgentFileExplorerState, ExplorerEntry } from "@/stores/session-store";
|
import type { AgentFileExplorerState, ExplorerEntry } from "@/stores/session-store";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { FileActionsMenu } from "@/components/file-actions-menu";
|
import { FileActionsMenu } from "@/components/file-actions-menu";
|
||||||
@@ -42,6 +42,11 @@ const SORT_OPTIONS: { value: SortOption }[] = [
|
|||||||
{ value: "size" },
|
{ value: "size" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
|
const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
});
|
||||||
|
|
||||||
function formatFileSize({ size }: { size: number }): string {
|
function formatFileSize({ size }: { size: number }): string {
|
||||||
if (size < 1024) {
|
if (size < 1024) {
|
||||||
return `${size} B`;
|
return `${size} B`;
|
||||||
@@ -163,7 +168,9 @@ function TreeRowItem({
|
|||||||
if (!isDirectory) {
|
if (!isDirectory) {
|
||||||
return <MaterialFileIcon fileName={entry.name} size={16} />;
|
return <MaterialFileIcon fileName={entry.name} size={16} />;
|
||||||
}
|
}
|
||||||
if (loading) return <ActivityIndicator size="small" />;
|
if (loading) {
|
||||||
|
return <ThemedLoadingSpinner size="small" uniProps={foregroundMutedColorMapping} />;
|
||||||
|
}
|
||||||
return <TreeChevron expanded={isExpanded} />;
|
return <TreeChevron expanded={isExpanded} />;
|
||||||
})()}
|
})()}
|
||||||
</View>
|
</View>
|
||||||
@@ -549,7 +556,7 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
|
|||||||
if (showInitialLoading) {
|
if (showInitialLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centerState}>
|
<View style={styles.centerState}>
|
||||||
<ActivityIndicator size="small" />
|
<ThemedLoadingSpinner size="small" uniProps={foregroundMutedColorMapping} />
|
||||||
<Text style={styles.loadingText}>{t("workspace.fileExplorer.states.loading")}</Text>
|
<Text style={styles.loadingText}>{t("workspace.fileExplorer.states.loading")}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
Image,
|
Image,
|
||||||
Pressable,
|
Pressable,
|
||||||
ActivityIndicator,
|
|
||||||
type GestureResponderEvent,
|
type GestureResponderEvent,
|
||||||
type LayoutChangeEvent,
|
type LayoutChangeEvent,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
@@ -172,6 +172,7 @@ const ThemedTodoCheckIcon = withUnistyles(Check);
|
|||||||
const ThemedFileSymlinkIcon = withUnistyles(FileSymlink);
|
const ThemedFileSymlinkIcon = withUnistyles(FileSymlink);
|
||||||
const ThemedTriangleAlertIcon = withUnistyles(TriangleAlertIcon);
|
const ThemedTriangleAlertIcon = withUnistyles(TriangleAlertIcon);
|
||||||
const ThemedChevronRightIcon = withUnistyles(ChevronRight);
|
const ThemedChevronRightIcon = withUnistyles(ChevronRight);
|
||||||
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
|
|
||||||
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||||
const foregroundMutedColorMapping = (theme: Theme) => ({
|
const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||||
@@ -865,7 +866,9 @@ const AssistantMarkdownResolvedImage = memo(function AssistantMarkdownResolvedIm
|
|||||||
return (
|
return (
|
||||||
<View style={frameStyle}>
|
<View style={frameStyle}>
|
||||||
<View style={stateSurfaceStyle}>
|
<View style={stateSurfaceStyle}>
|
||||||
{loadState.status === "loading" ? <ActivityIndicator size="small" /> : null}
|
{loadState.status === "loading" ? (
|
||||||
|
<ThemedLoadingSpinner size="small" uniProps={foregroundMutedColorMapping} />
|
||||||
|
) : null}
|
||||||
{loadState.status === "error" ? (
|
{loadState.status === "error" ? (
|
||||||
<Text style={assistantMessageStylesheet.imageErrorText}>
|
<Text style={assistantMessageStylesheet.imageErrorText}>
|
||||||
{t("message.attachments.imageUnavailable")}
|
{t("message.attachments.imageUnavailable")}
|
||||||
@@ -1004,7 +1007,7 @@ function AssistantMarkdownImage({
|
|||||||
if (query.isLoading || dataImageQuery.isLoading) {
|
if (query.isLoading || dataImageQuery.isLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={stateFrameStyle}>
|
<View style={stateFrameStyle}>
|
||||||
<ActivityIndicator size="small" />
|
<ThemedLoadingSpinner size="small" uniProps={foregroundMutedColorMapping} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2244,7 +2247,7 @@ export const CompactionMarker = memo(function CompactionMarker({
|
|||||||
<View style={compactionStylesheet.line} />
|
<View style={compactionStylesheet.line} />
|
||||||
<View style={compactionStylesheet.label}>
|
<View style={compactionStylesheet.label}>
|
||||||
{status === "loading" ? (
|
{status === "loading" ? (
|
||||||
<ActivityIndicator size="small" color="#a1a1aa" />
|
<LoadingSpinner size="small" color="#a1a1aa" />
|
||||||
) : (
|
) : (
|
||||||
<Scissors size={12} color="#a1a1aa" />
|
<Scissors size={12} color="#a1a1aa" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,13 +3,7 @@ import { AlertTriangle, Copy, FileText, Plus, RotateCw, Trash2 } from "lucide-re
|
|||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { Pressable, type PressableStateCallbackType, Text, View } from "react-native";
|
||||||
ActivityIndicator,
|
|
||||||
Pressable,
|
|
||||||
type PressableStateCallbackType,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import {
|
import {
|
||||||
AdaptiveModalSheet,
|
AdaptiveModalSheet,
|
||||||
@@ -365,7 +359,7 @@ function DiagnosticSubSheet({
|
|||||||
body = (
|
body = (
|
||||||
<SurfaceCard key={visible ? "visible" : "hidden"}>
|
<SurfaceCard key={visible ? "visible" : "hidden"}>
|
||||||
<View style={sheetStyles.codeBlockLoading}>
|
<View style={sheetStyles.codeBlockLoading}>
|
||||||
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
<LoadingSpinner size="small" color={theme.colors.foregroundMuted} />
|
||||||
<Text style={sheetStyles.mutedText}>{t("settings.providers.diagnostic.running")}</Text>
|
<Text style={sheetStyles.mutedText}>{t("settings.providers.diagnostic.running")}</Text>
|
||||||
</View>
|
</View>
|
||||||
</SurfaceCard>
|
</SurfaceCard>
|
||||||
@@ -504,7 +498,7 @@ function ProviderModalBody(props: ProviderModalBodyProps) {
|
|||||||
if (discoveredCount === 0 && additionalCount === 0 && providerSnapshotRefreshing) {
|
if (discoveredCount === 0 && additionalCount === 0 && providerSnapshotRefreshing) {
|
||||||
return (
|
return (
|
||||||
<View style={sheetStyles.emptyState}>
|
<View style={sheetStyles.emptyState}>
|
||||||
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
<LoadingSpinner size="small" color={theme.colors.foregroundMuted} />
|
||||||
<Text style={sheetStyles.mutedText}>{t("settings.providers.models.loading")}</Text>
|
<Text style={sheetStyles.mutedText}>{t("settings.providers.models.loading")}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import { useState, useCallback, useMemo } from "react";
|
import { useState, useCallback, useMemo } from "react";
|
||||||
import {
|
import { View, Text, TextInput, Pressable, type PressableStateCallbackType } from "react-native";
|
||||||
View,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
Pressable,
|
|
||||||
ActivityIndicator,
|
|
||||||
type PressableStateCallbackType,
|
|
||||||
} from "react-native";
|
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { Check, X } from "lucide-react-native";
|
import { Check, X } from "lucide-react-native";
|
||||||
@@ -581,7 +575,7 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi
|
|||||||
testID="question-form-dismiss"
|
testID="question-form-dismiss"
|
||||||
>
|
>
|
||||||
{respondingAction === "dismiss" ? (
|
{respondingAction === "dismiss" ? (
|
||||||
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
<LoadingSpinner size="small" color={theme.colors.foregroundMuted} />
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.actionContent}>
|
<View style={styles.actionContent}>
|
||||||
<X size={14} color={theme.colors.foregroundMuted} />
|
<X size={14} color={theme.colors.foregroundMuted} />
|
||||||
@@ -599,7 +593,7 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi
|
|||||||
testID="question-form-primary-action"
|
testID="question-form-primary-action"
|
||||||
>
|
>
|
||||||
{respondingAction === "submit" ? (
|
{respondingAction === "submit" ? (
|
||||||
<ActivityIndicator size="small" color={theme.colors.accentForeground} />
|
<LoadingSpinner size="small" color={theme.colors.accentForeground} />
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.actionContent}>
|
<View style={styles.actionContent}>
|
||||||
<Check size={14} color={submitActionTextColor} />
|
<Check size={14} color={submitActionTextColor} />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ActivityIndicator, Pressable, View } from "react-native";
|
import { Pressable, View } from "react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { Mic, MicOff, Square } from "lucide-react-native";
|
import { Mic, MicOff, Square } from "lucide-react-native";
|
||||||
import { FOOTER_HEIGHT } from "@/constants/layout";
|
import { FOOTER_HEIGHT } from "@/constants/layout";
|
||||||
@@ -75,7 +76,7 @@ export function RealtimeVoiceOverlay({
|
|||||||
style={stopButtonStyle}
|
style={stopButtonStyle}
|
||||||
>
|
>
|
||||||
{isSwitching ? (
|
{isSwitching ? (
|
||||||
<ActivityIndicator size="small" color={theme.colors.palette.white} />
|
<LoadingSpinner size="small" color={theme.colors.palette.white} />
|
||||||
) : (
|
) : (
|
||||||
<Square
|
<Square
|
||||||
size={theme.iconSize.lg}
|
size={theme.iconSize.lg}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
Pressable,
|
Pressable,
|
||||||
ActivityIndicator,
|
|
||||||
ScrollView,
|
ScrollView,
|
||||||
type GestureResponderEvent,
|
type GestureResponderEvent,
|
||||||
type PressableStateCallbackType,
|
type PressableStateCallbackType,
|
||||||
@@ -143,7 +143,7 @@ const DEFAULT_STATUS_DOT_OFFSET = 0;
|
|||||||
const EMPHASIZED_STATUS_DOT_OFFSET = -1;
|
const EMPHASIZED_STATUS_DOT_OFFSET = -1;
|
||||||
const ThemedExternalLink = withUnistyles(ExternalLink);
|
const ThemedExternalLink = withUnistyles(ExternalLink);
|
||||||
const ThemedGitPullRequest = withUnistyles(GitPullRequest);
|
const ThemedGitPullRequest = withUnistyles(GitPullRequest);
|
||||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
const ThemedCircleAlert = withUnistyles(CircleAlert);
|
const ThemedCircleAlert = withUnistyles(CircleAlert);
|
||||||
const ThemedSyncedLoader = withUnistyles(SyncedLoader);
|
const ThemedSyncedLoader = withUnistyles(SyncedLoader);
|
||||||
const ThemedPlus = withUnistyles(Plus);
|
const ThemedPlus = withUnistyles(Plus);
|
||||||
@@ -746,7 +746,7 @@ function ProjectLeadingVisualStatus({
|
|||||||
if (isArchiving) {
|
if (isArchiving) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.projectLeadingVisualSlot}>
|
<View style={styles.projectLeadingVisualSlot}>
|
||||||
<ThemedActivityIndicator size={8} uniProps={foregroundMutedColorMapping} />
|
<ThemedLoadingSpinner size={8} uniProps={foregroundMutedColorMapping} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -855,7 +855,7 @@ function NewWorktreeButton({
|
|||||||
>
|
>
|
||||||
{({ hovered, pressed }) =>
|
{({ hovered, pressed }) =>
|
||||||
loading ? (
|
loading ? (
|
||||||
<ThemedActivityIndicator size={14} uniProps={foregroundMutedColorMapping} />
|
<ThemedLoadingSpinner size={14} uniProps={foregroundMutedColorMapping} />
|
||||||
) : (
|
) : (
|
||||||
<ThemedPlus
|
<ThemedPlus
|
||||||
size={15}
|
size={15}
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import { memo, useCallback, useMemo, useState, type ReactNode } from "react";
|
import { memo, useCallback, useMemo, useState, type ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { Pressable, Text, View, type GestureResponderEvent, type ViewStyle } from "react-native";
|
||||||
ActivityIndicator,
|
|
||||||
Pressable,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
type GestureResponderEvent,
|
|
||||||
type ViewStyle,
|
|
||||||
} from "react-native";
|
|
||||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||||
import {
|
import {
|
||||||
CircleAlert,
|
CircleAlert,
|
||||||
@@ -54,7 +48,7 @@ const purpleColorMapping = (theme: Theme) => ({ color: theme.colors.palette.purp
|
|||||||
|
|
||||||
const ThemedExternalLink = withUnistyles(ExternalLink);
|
const ThemedExternalLink = withUnistyles(ExternalLink);
|
||||||
const ThemedGitPullRequest = withUnistyles(GitPullRequest);
|
const ThemedGitPullRequest = withUnistyles(GitPullRequest);
|
||||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
const ThemedCircleAlert = withUnistyles(CircleAlert);
|
const ThemedCircleAlert = withUnistyles(CircleAlert);
|
||||||
const ThemedSyncedLoader = withUnistyles(SyncedLoader);
|
const ThemedSyncedLoader = withUnistyles(SyncedLoader);
|
||||||
const ThemedMonitor = withUnistyles(Monitor);
|
const ThemedMonitor = withUnistyles(Monitor);
|
||||||
@@ -207,7 +201,7 @@ function WorkspaceStatusIndicator({
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.workspaceStatusDot} testID="workspace-status-indicator-loading">
|
<View style={styles.workspaceStatusDot} testID="workspace-status-indicator-loading">
|
||||||
<ThemedActivityIndicator size={8} uniProps={foregroundMutedColorMapping} />
|
<ThemedLoadingSpinner size={8} uniProps={foregroundMutedColorMapping} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
||||||
ActivityIndicator,
|
|
||||||
Pressable,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
type PressableStateCallbackType,
|
|
||||||
} from "react-native";
|
|
||||||
import Animated, { runOnJS, useAnimatedReaction } from "react-native-reanimated";
|
import Animated, { runOnJS, useAnimatedReaction } from "react-native-reanimated";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { encodeTerminalKeyInput } from "@getpaseo/protocol/terminal-key-input";
|
import { encodeTerminalKeyInput } from "@getpaseo/protocol/terminal-key-input";
|
||||||
@@ -836,7 +831,7 @@ export function TerminalPane({
|
|||||||
|
|
||||||
{showLoadingOverlay ? (
|
{showLoadingOverlay ? (
|
||||||
<View style={styles.attachOverlay} pointerEvents="none" testID="terminal-attach-loading">
|
<View style={styles.attachOverlay} pointerEvents="none" testID="terminal-attach-loading">
|
||||||
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
<LoadingSpinner size="small" color={theme.colors.foregroundMuted} />
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import {
|
import {
|
||||||
default as React,
|
default as React,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -8,7 +9,7 @@ import {
|
|||||||
type ReactElement,
|
type ReactElement,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { ActivityIndicator, Pressable, Text, View } from "react-native";
|
import { Pressable, Text, View } from "react-native";
|
||||||
import type {
|
import type {
|
||||||
PressableProps,
|
PressableProps,
|
||||||
PressableStateCallbackType,
|
PressableStateCallbackType,
|
||||||
@@ -44,7 +45,7 @@ function ButtonIcon({ loading, leftIcon, iconSize, iconColor }: ButtonIconProps)
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ActivityIndicator size="small" color={iconColor} />
|
<LoadingSpinner size="small" color={iconColor} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import {
|
import {
|
||||||
type ComponentProps,
|
type ComponentProps,
|
||||||
createContext,
|
createContext,
|
||||||
@@ -14,7 +15,6 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Modal,
|
Modal,
|
||||||
Platform,
|
Platform,
|
||||||
@@ -630,7 +630,7 @@ function resolveLeadingContent(input: {
|
|||||||
successColor: string;
|
successColor: string;
|
||||||
}): ReactElement | null {
|
}): ReactElement | null {
|
||||||
if (input.isPending) {
|
if (input.isPending) {
|
||||||
return <ActivityIndicator size={16} color={input.pendingColor} />;
|
return <LoadingSpinner size={16} color={input.pendingColor} />;
|
||||||
}
|
}
|
||||||
if (input.isSuccess) {
|
if (input.isSuccess) {
|
||||||
return <CheckCircle size={16} color={input.successColor} />;
|
return <CheckCircle size={16} color={input.successColor} />;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -14,7 +15,6 @@ import {
|
|||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
|
||||||
Modal,
|
Modal,
|
||||||
Pressable,
|
Pressable,
|
||||||
Text,
|
Text,
|
||||||
@@ -717,7 +717,7 @@ function resolveDropdownItemLeadingContent(input: {
|
|||||||
}): ReactElement | null {
|
}): ReactElement | null {
|
||||||
const { isPending, isSuccess, leading, theme } = input;
|
const { isPending, isSuccess, leading, theme } = input;
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return <ActivityIndicator size={16} color={theme.colors.foregroundMuted} />;
|
return <LoadingSpinner size={16} color={theme.colors.foregroundMuted} />;
|
||||||
}
|
}
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
return <CheckCircle size={16} color={theme.colors.palette.green[500]} />;
|
return <CheckCircle size={16} color={theme.colors.palette.green[500]} />;
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { ActivityIndicator, type ActivityIndicatorProps } from "react-native";
|
|||||||
interface LoadingSpinnerProps {
|
interface LoadingSpinnerProps {
|
||||||
color: string;
|
color: string;
|
||||||
size?: ActivityIndicatorProps["size"];
|
size?: ActivityIndicatorProps["size"];
|
||||||
|
style?: ActivityIndicatorProps["style"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoadingSpinner({ color, size = "small" }: LoadingSpinnerProps) {
|
export function LoadingSpinner({ color, size = "small", style }: LoadingSpinnerProps) {
|
||||||
return <ActivityIndicator size={size} color={color} />;
|
return <ActivityIndicator size={size} color={color} style={style} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Pressable,
|
Pressable,
|
||||||
Text,
|
Text,
|
||||||
ActivityIndicator,
|
|
||||||
StyleSheet as RNStyleSheet,
|
StyleSheet as RNStyleSheet,
|
||||||
type PressableStateCallbackType,
|
type PressableStateCallbackType,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
@@ -884,7 +884,7 @@ function ComposerCancelButton({
|
|||||||
? t("composer.cancel.cancelingAgent")
|
? t("composer.cancel.cancelingAgent")
|
||||||
: t("composer.cancel.stopAgent");
|
: t("composer.cancel.stopAgent");
|
||||||
const icon = isCancellingAgent ? (
|
const icon = isCancellingAgent ? (
|
||||||
<ActivityIndicator size="small" color="white" />
|
<LoadingSpinner size="small" color="white" />
|
||||||
) : (
|
) : (
|
||||||
<Square size={buttonIconSize} color="white" fill="white" />
|
<Square size={buttonIconSize} color="white" fill="white" />
|
||||||
);
|
);
|
||||||
@@ -984,7 +984,7 @@ function ComposerVoiceModeButton({
|
|||||||
const renderTriggerContent = useCallback(
|
const renderTriggerContent = useCallback(
|
||||||
({ hovered }: PressableStateCallbackType & { hovered?: boolean }) => {
|
({ hovered }: PressableStateCallbackType & { hovered?: boolean }) => {
|
||||||
if (isVoiceSwitching) {
|
if (isVoiceSwitching) {
|
||||||
return <ActivityIndicator size="small" color="white" />;
|
return <LoadingSpinner size="small" color="white" />;
|
||||||
}
|
}
|
||||||
const colorMapping = hovered ? iconForegroundMapping : iconForegroundMutedMapping;
|
const colorMapping = hovered ? iconForegroundMapping : iconForegroundMutedMapping;
|
||||||
return <ThemedAudioLines size={buttonIconSize} uniProps={colorMapping} />;
|
return <ThemedAudioLines size={buttonIconSize} uniProps={colorMapping} />;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Pressable,
|
Pressable,
|
||||||
Platform,
|
Platform,
|
||||||
ActivityIndicator,
|
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
NativeSyntheticEvent,
|
NativeSyntheticEvent,
|
||||||
TextInputContentSizeChangeEventData,
|
TextInputContentSizeChangeEventData,
|
||||||
@@ -445,7 +445,7 @@ function SendButtonContent({
|
|||||||
buttonIconSize: number;
|
buttonIconSize: number;
|
||||||
}) {
|
}) {
|
||||||
if (isSubmitLoading) {
|
if (isSubmitLoading) {
|
||||||
return <ThemedActivityIndicator size="small" uniProps={iconAccentForegroundMapping} />;
|
return <ThemedLoadingSpinner size="small" uniProps={iconAccentForegroundMapping} />;
|
||||||
}
|
}
|
||||||
if (submitIcon === "return") {
|
if (submitIcon === "return") {
|
||||||
return <ThemedCornerDownLeft size={buttonIconSize} uniProps={iconAccentForegroundMapping} />;
|
return <ThemedCornerDownLeft size={buttonIconSize} uniProps={iconAccentForegroundMapping} />;
|
||||||
@@ -2000,7 +2000,7 @@ const ThemedMic = withUnistyles(Mic);
|
|||||||
const ThemedMicOff = withUnistyles(MicOff);
|
const ThemedMicOff = withUnistyles(MicOff);
|
||||||
const ThemedArrowUp = withUnistyles(ArrowUp);
|
const ThemedArrowUp = withUnistyles(ArrowUp);
|
||||||
const ThemedCornerDownLeft = withUnistyles(CornerDownLeft);
|
const ThemedCornerDownLeft = withUnistyles(CornerDownLeft);
|
||||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
const ThemedTextInput = withUnistyles(TextInput);
|
const ThemedTextInput = withUnistyles(TextInput);
|
||||||
|
|
||||||
const iconForegroundMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
const iconForegroundMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import React, { type ReactElement, useCallback, useMemo, useState } from "react";
|
import React, { type ReactElement, useCallback, useMemo, useState } from "react";
|
||||||
import { ActivityIndicator, Alert, Text, View } from "react-native";
|
import { Alert, Text, View } from "react-native";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
@@ -421,7 +422,7 @@ export function LocalDaemonSection() {
|
|||||||
>
|
>
|
||||||
{isLoading || isLoadingSettings ? (
|
{isLoading || isLoadingSettings ? (
|
||||||
<View style={[settingsStyles.card, styles.loadingCard]}>
|
<View style={[settingsStyles.card, styles.loadingCard]}>
|
||||||
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
<LoadingSpinner size="small" color={theme.colors.foregroundMuted} />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ActivityIndicator, Image, Text, TextInput, View } from "react-native";
|
import { Image, Text, TextInput, View } from "react-native";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import * as QRCode from "qrcode";
|
import * as QRCode from "qrcode";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles";
|
||||||
import { RotateCw, Copy, Check } from "lucide-react-native";
|
import { RotateCw, Copy, Check } from "lucide-react-native";
|
||||||
import { settingsStyles } from "@/styles/settings";
|
import { settingsStyles } from "@/styles/settings";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { getDesktopDaemonPairing, shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
|
import { getDesktopDaemonPairing, shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import type { Theme } from "@/styles/theme";
|
||||||
|
|
||||||
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
|
const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
});
|
||||||
|
|
||||||
type PairingViewState =
|
type PairingViewState =
|
||||||
| { tag: "loading" }
|
| { tag: "loading" }
|
||||||
@@ -184,7 +191,7 @@ function PairDeviceBody(props: PairDeviceBodyProps) {
|
|||||||
if (viewState.tag === "loading") {
|
if (viewState.tag === "loading") {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centered}>
|
<View style={styles.centered}>
|
||||||
<ActivityIndicator size="small" />
|
<ThemedLoadingSpinner size="small" uniProps={foregroundMutedColorMapping} />
|
||||||
<Text style={styles.hint}>{labels.loadingOffer}</Text>
|
<Text style={styles.hint}>{labels.loadingOffer}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -240,7 +247,7 @@ function PairDeviceQrContent(props: {
|
|||||||
if (props.qrQuery.isError) {
|
if (props.qrQuery.isError) {
|
||||||
return <Text style={styles.hint}>{props.unavailableLabel}</Text>;
|
return <Text style={styles.hint}>{props.unavailableLabel}</Text>;
|
||||||
}
|
}
|
||||||
return <ActivityIndicator size="small" />;
|
return <ThemedLoadingSpinner size="small" uniProps={foregroundMutedColorMapping} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -8,14 +9,8 @@ import React, {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import type { DaemonClient, FileReadResult } from "@getpaseo/client/internal/daemon-client";
|
import type { DaemonClient, FileReadResult } from "@getpaseo/client/internal/daemon-client";
|
||||||
import type { FileVersion } from "@getpaseo/protocol/messages";
|
import type { FileVersion } from "@getpaseo/protocol/messages";
|
||||||
import {
|
import { Image as RNImage, ScrollView as RNScrollView, Text, View } from "react-native";
|
||||||
ActivityIndicator,
|
import { StyleSheet, UnistylesRuntime, withUnistyles } from "react-native-unistyles";
|
||||||
Image as RNImage,
|
|
||||||
ScrollView as RNScrollView,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import { StyleSheet, UnistylesRuntime } from "react-native-unistyles";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MarkdownRenderer } from "@/components/markdown/renderer";
|
import { MarkdownRenderer } from "@/components/markdown/renderer";
|
||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
@@ -44,6 +39,12 @@ import { FileEditorModel, type FileEditorFile } from "./editor/model";
|
|||||||
import { FileEditorView } from "./editor/view";
|
import { FileEditorView } from "./editor/view";
|
||||||
import { confirmDialog } from "@/utils/confirm-dialog";
|
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||||
import { usePublishPanelInstanceAttributes } from "@/panels/panel-instance-attributes";
|
import { usePublishPanelInstanceAttributes } from "@/panels/panel-instance-attributes";
|
||||||
|
import type { Theme } from "@/styles/theme";
|
||||||
|
|
||||||
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
|
const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
});
|
||||||
|
|
||||||
interface CodeLineProps {
|
interface CodeLineProps {
|
||||||
tokens: HighlightToken[];
|
tokens: HighlightToken[];
|
||||||
@@ -264,7 +265,7 @@ function FilePreviewBody({
|
|||||||
if (isLoading && !preview) {
|
if (isLoading && !preview) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centerState}>
|
<View style={styles.centerState}>
|
||||||
<ActivityIndicator size="small" />
|
<ThemedLoadingSpinner size="small" uniProps={foregroundMutedColorMapping} />
|
||||||
<Text style={styles.loadingText}>{t("panels.file.loading")}</Text>
|
<Text style={styles.loadingText}>{t("panels.file.loading")}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -346,7 +347,7 @@ function FilePreviewBody({
|
|||||||
if (!imagePreviewUri) {
|
if (!imagePreviewUri) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centerState}>
|
<View style={styles.centerState}>
|
||||||
<ActivityIndicator size="small" />
|
<ThemedLoadingSpinner size="small" uniProps={foregroundMutedColorMapping} />
|
||||||
<Text style={styles.loadingText}>{t("panels.file.loading")}</Text>
|
<Text style={styles.loadingText}>{t("panels.file.loading")}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import {
|
import { View, Text, Pressable, type PressableStateCallbackType } from "react-native";
|
||||||
View,
|
|
||||||
Text,
|
|
||||||
ActivityIndicator,
|
|
||||||
Pressable,
|
|
||||||
type PressableStateCallbackType,
|
|
||||||
} from "react-native";
|
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { ChevronDown, Info, MoreVertical } from "lucide-react-native";
|
import { ChevronDown, Info, MoreVertical } from "lucide-react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -146,7 +141,7 @@ export function GitActionsSplitButton({ gitActions, hideLabels }: GitActionsSpli
|
|||||||
accessibilityLabel={gitActions.primary.label}
|
accessibilityLabel={gitActions.primary.label}
|
||||||
>
|
>
|
||||||
{gitActions.primary.status === "pending" ? (
|
{gitActions.primary.status === "pending" ? (
|
||||||
<ActivityIndicator
|
<LoadingSpinner
|
||||||
size="small"
|
size="small"
|
||||||
color={theme.colors.foreground}
|
color={theme.colors.foreground}
|
||||||
style={styles.splitButtonSpinnerOnly}
|
style={styles.splitButtonSpinnerOnly}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { DiffStat } from "@/components/diff-stat";
|
|||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
ActivityIndicator,
|
|
||||||
Pressable,
|
Pressable,
|
||||||
FlatList,
|
FlatList,
|
||||||
type LayoutChangeEvent,
|
type LayoutChangeEvent,
|
||||||
@@ -1320,7 +1319,7 @@ type PressableStyleFn = (
|
|||||||
|
|
||||||
const foregroundMutedIconColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
const foregroundMutedIconColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||||
|
|
||||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
const ThemedAlignJustify = withUnistyles(AlignJustify);
|
const ThemedAlignJustify = withUnistyles(AlignJustify);
|
||||||
const ThemedColumns2 = withUnistyles(Columns2);
|
const ThemedColumns2 = withUnistyles(Columns2);
|
||||||
const ThemedPilcrow = withUnistyles(Pilcrow);
|
const ThemedPilcrow = withUnistyles(Pilcrow);
|
||||||
@@ -1693,7 +1692,6 @@ export function DiffOptionsMenu({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ThemedRotateCw = withUnistyles(RotateCw);
|
const ThemedRotateCw = withUnistyles(RotateCw);
|
||||||
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
|
||||||
|
|
||||||
type DiffFlatItemLayoutGetter = NonNullable<FlatListProps<DiffFlatItem>["getItemLayout"]>;
|
type DiffFlatItemLayoutGetter = NonNullable<FlatListProps<DiffFlatItem>["getItemLayout"]>;
|
||||||
const EMPTY_PATH_LIST: string[] = [];
|
const EMPTY_PATH_LIST: string[] = [];
|
||||||
@@ -1779,7 +1777,7 @@ function DiffBodyContent({
|
|||||||
if (isStatusLoading) {
|
if (isStatusLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ThemedActivityIndicator size="large" uniProps={foregroundMutedIconColorMapping} />
|
<ThemedLoadingSpinner size="large" uniProps={foregroundMutedIconColorMapping} />
|
||||||
<Text style={styles.loadingText}>{checkingRepositoryLabel}</Text>
|
<Text style={styles.loadingText}>{checkingRepositoryLabel}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -1801,7 +1799,7 @@ function DiffBodyContent({
|
|||||||
if (isDiffLoading) {
|
if (isDiffLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ThemedActivityIndicator size="large" uniProps={foregroundMutedIconColorMapping} />
|
<ThemedLoadingSpinner size="large" uniProps={foregroundMutedIconColorMapping} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ export function useLoadOlderAgentHistory({
|
|||||||
useSessionStore((state) =>
|
useSessionStore((state) =>
|
||||||
state.sessions[serverId]?.agentTimelineOlderFetchInFlight.get(agentId),
|
state.sessions[serverId]?.agentTimelineOlderFetchInFlight.get(agentId),
|
||||||
) === true;
|
) === true;
|
||||||
|
const progressKey = useSessionStore((state) => {
|
||||||
|
const cursor = state.sessions[serverId]?.agentTimelineCursor.get(agentId);
|
||||||
|
return cursor ? `${cursor.epoch}:${cursor.startSeq}` : null;
|
||||||
|
});
|
||||||
const setOlderFetchInFlight = useSessionStore(
|
const setOlderFetchInFlight = useSessionStore(
|
||||||
(state) => state.setAgentTimelineOlderFetchInFlight,
|
(state) => state.setAgentTimelineOlderFetchInFlight,
|
||||||
);
|
);
|
||||||
@@ -116,6 +120,7 @@ export function useLoadOlderAgentHistory({
|
|||||||
return {
|
return {
|
||||||
isLoadingOlder,
|
isLoadingOlder,
|
||||||
hasOlder,
|
hasOlder,
|
||||||
|
progressKey,
|
||||||
loadOlder,
|
loadOlder,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
import { SquarePen } from "lucide-react-native";
|
import { SquarePen } from "lucide-react-native";
|
||||||
@@ -11,7 +12,7 @@ import React, {
|
|||||||
useSyncExternalStore,
|
useSyncExternalStore,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ActivityIndicator, StyleSheet as RNStyleSheet, Text, View } from "react-native";
|
import { StyleSheet as RNStyleSheet, Text, View } from "react-native";
|
||||||
import ReanimatedAnimated from "react-native-reanimated";
|
import ReanimatedAnimated from "react-native-reanimated";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||||
@@ -215,7 +216,7 @@ function renderChatAgentNonReadyView(args: {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container} testID="agent-loading">
|
<View style={styles.container} testID="agent-loading">
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
|
<ThemedLoadingSpinner size="large" uniProps={foregroundMutedColorMapping} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -716,7 +717,7 @@ function AgentPanelBody({
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container} testID="agent-loading">
|
<View style={styles.container} testID="agent-loading">
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
|
<ThemedLoadingSpinner size="large" uniProps={foregroundMutedColorMapping} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -1246,7 +1247,7 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
|||||||
|
|
||||||
{showHistorySyncOverlay ? (
|
{showHistorySyncOverlay ? (
|
||||||
<View style={styles.historySyncOverlay} testID="agent-history-overlay">
|
<View style={styles.historySyncOverlay} testID="agent-history-overlay">
|
||||||
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
|
<ThemedLoadingSpinner size="large" uniProps={foregroundMutedColorMapping} />
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -1255,7 +1256,7 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
|||||||
|
|
||||||
{isArchivingCurrentAgent ? (
|
{isArchivingCurrentAgent ? (
|
||||||
<View style={styles.archivingOverlay} testID="agent-archiving-overlay">
|
<View style={styles.archivingOverlay} testID="agent-archiving-overlay">
|
||||||
<ThemedActivityIndicator size="large" uniProps={foregroundColorMapping} />
|
<ThemedLoadingSpinner size="large" uniProps={foregroundColorMapping} />
|
||||||
<Text style={styles.archivingTitle}>{t("agentPanel.states.archivingTitle")}</Text>
|
<Text style={styles.archivingTitle}>{t("agentPanel.states.archivingTitle")}</Text>
|
||||||
<Text style={styles.archivingSubtitle}>{t("agentPanel.states.archivingSubtitle")}</Text>
|
<Text style={styles.archivingSubtitle}>{t("agentPanel.states.archivingSubtitle")}</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -1600,7 +1601,7 @@ function AgentSessionUnavailableState({
|
|||||||
<View style={styles.centerState}>
|
<View style={styles.centerState}>
|
||||||
{isConnecting || isPreparingSession ? (
|
{isConnecting || isPreparingSession ? (
|
||||||
<>
|
<>
|
||||||
<ActivityIndicator size="large" />
|
<ThemedLoadingSpinner size="large" uniProps={foregroundMutedColorMapping} />
|
||||||
<Text style={styles.loadingText}>
|
<Text style={styles.loadingText}>
|
||||||
{isPreparingSession
|
{isPreparingSession
|
||||||
? t("agentPanel.unavailable.preparingSession", { serverLabel })
|
? t("agentPanel.unavailable.preparingSession", { serverLabel })
|
||||||
@@ -1628,7 +1629,7 @@ function AgentSessionUnavailableState({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
|
|
||||||
const foregroundMutedColorMapping = (theme: Theme) => ({
|
const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
|
|||||||
@@ -130,6 +130,9 @@ function ProviderSubagentPanel() {
|
|||||||
target.subagentId,
|
target.subagentId,
|
||||||
timeline,
|
timeline,
|
||||||
]);
|
]);
|
||||||
|
const firstTimelineSeq = timeline?.rows.size ? Math.min(...timeline.rows.keys()) : null;
|
||||||
|
const progressKey =
|
||||||
|
timeline?.epoch && firstTimelineSeq !== null ? `${timeline.epoch}:${firstTimelineSeq}` : null;
|
||||||
|
|
||||||
const streamContext = useMemo<AgentScreenAgent>(
|
const streamContext = useMemo<AgentScreenAgent>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -147,9 +150,10 @@ function ProviderSubagentPanel() {
|
|||||||
() => ({
|
() => ({
|
||||||
hasOlder: timeline?.hasOlder === true,
|
hasOlder: timeline?.hasOlder === true,
|
||||||
isLoadingOlder,
|
isLoadingOlder,
|
||||||
|
progressKey,
|
||||||
onLoadOlder: loadOlder,
|
onLoadOlder: loadOlder,
|
||||||
}),
|
}),
|
||||||
[isLoadingOlder, loadOlder, timeline?.hasOlder],
|
[isLoadingOlder, loadOlder, progressKey, timeline?.hasOlder],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (serverInfo && !supported) {
|
if (serverInfo && !supported) {
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { CheckCircle2, ChevronRight, CircleAlert, SquareTerminal } from "lucide-react-native";
|
import { CheckCircle2, ChevronRight, CircleAlert, SquareTerminal } from "lucide-react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { Pressable, type PressableStateCallbackType, ScrollView, Text, View } from "react-native";
|
||||||
ActivityIndicator,
|
|
||||||
Pressable,
|
|
||||||
type PressableStateCallbackType,
|
|
||||||
ScrollView,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import invariant from "tiny-invariant";
|
import invariant from "tiny-invariant";
|
||||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||||
import { usePaneContext } from "@/panels/pane-context";
|
import { usePaneContext } from "@/panels/pane-context";
|
||||||
@@ -69,7 +63,7 @@ type CommandStatus = "running" | "completed" | "failed";
|
|||||||
|
|
||||||
function CommandStatusIcon({ status }: { status: CommandStatus }) {
|
function CommandStatusIcon({ status }: { status: CommandStatus }) {
|
||||||
if (status === "running") {
|
if (status === "running") {
|
||||||
return <ThemedActivityIndicator size={14} uniProps={foregroundColorMapping} />;
|
return <ThemedLoadingSpinner size={14} uniProps={foregroundColorMapping} />;
|
||||||
}
|
}
|
||||||
if (status === "completed") {
|
if (status === "completed") {
|
||||||
return <ThemedCheckCircle2 size={14} uniProps={greenColorMapping} />;
|
return <ThemedCheckCircle2 size={14} uniProps={greenColorMapping} />;
|
||||||
@@ -247,7 +241,7 @@ function SetupPanel() {
|
|||||||
|
|
||||||
{isWaiting ? (
|
{isWaiting ? (
|
||||||
<View style={styles.waitingContainer}>
|
<View style={styles.waitingContainer}>
|
||||||
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
|
<ThemedLoadingSpinner size="large" uniProps={foregroundMutedColorMapping} />
|
||||||
<Text style={styles.waitingText}>{t("workspace.setup.waiting")}</Text>
|
<Text style={styles.waitingText}>{t("workspace.setup.waiting")}</Text>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -449,7 +443,7 @@ function TopLevelSetupError({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
const ThemedCheckCircle2 = withUnistyles(CheckCircle2);
|
const ThemedCheckCircle2 = withUnistyles(CheckCircle2);
|
||||||
const ThemedCircleAlert = withUnistyles(CircleAlert);
|
const ThemedCircleAlert = withUnistyles(CircleAlert);
|
||||||
const ThemedChevronRight = withUnistyles(ChevronRight);
|
const ThemedChevronRight = withUnistyles(ChevronRight);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -8,7 +9,6 @@ import React, {
|
|||||||
type SetStateAction,
|
type SetStateAction,
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
|
||||||
Pressable,
|
Pressable,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Text,
|
Text,
|
||||||
@@ -94,7 +94,7 @@ const DROPDOWN_WIDTH = 220;
|
|||||||
const LOADING_TAB_LABEL_SKELETON_WIDTH = 80;
|
const LOADING_TAB_LABEL_SKELETON_WIDTH = 80;
|
||||||
const DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH = 36;
|
const DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH = 36;
|
||||||
|
|
||||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
const ThemedX = withUnistyles(X);
|
const ThemedX = withUnistyles(X);
|
||||||
const ThemedCopy = withUnistyles(Copy);
|
const ThemedCopy = withUnistyles(Copy);
|
||||||
const ThemedRotateCw = withUnistyles(RotateCw);
|
const ThemedRotateCw = withUnistyles(RotateCw);
|
||||||
@@ -696,7 +696,7 @@ function TabChip({
|
|||||||
const highlighted = closeHovered || pressed;
|
const highlighted = closeHovered || pressed;
|
||||||
if (isClosingTab) {
|
if (isClosingTab) {
|
||||||
return (
|
return (
|
||||||
<ThemedActivityIndicator
|
<ThemedLoadingSpinner
|
||||||
size={12}
|
size={12}
|
||||||
uniProps={highlighted ? foregroundColorMapping : mutedColorMapping}
|
uniProps={highlighted ? foregroundColorMapping : mutedColorMapping}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import { type ReactElement, useCallback, useMemo } from "react";
|
import { type ReactElement, useCallback, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
||||||
ActivityIndicator,
|
|
||||||
Pressable,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
type PressableStateCallbackType,
|
|
||||||
} from "react-native";
|
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Check, ChevronDown } from "lucide-react-native";
|
import { Check, ChevronDown } from "lucide-react-native";
|
||||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||||
@@ -47,7 +42,7 @@ interface OpenTarget {
|
|||||||
onOpen: () => Promise<void> | void;
|
onOpen: () => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
const ThemedEditorTargetIcon = withUnistyles(EditorTargetIcon);
|
const ThemedEditorTargetIcon = withUnistyles(EditorTargetIcon);
|
||||||
const ThemedChevronDown = withUnistyles(ChevronDown);
|
const ThemedChevronDown = withUnistyles(ChevronDown);
|
||||||
const ThemedCheckIcon = withUnistyles(Check);
|
const ThemedCheckIcon = withUnistyles(Check);
|
||||||
@@ -235,7 +230,7 @@ export function WorkspaceOpenInEditorButton({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{openMutation.isPending ? (
|
{openMutation.isPending ? (
|
||||||
<ThemedActivityIndicator
|
<ThemedLoadingSpinner
|
||||||
size="small"
|
size="small"
|
||||||
uniProps={foregroundColorMapping}
|
uniProps={foregroundColorMapping}
|
||||||
style={styles.splitButtonSpinnerOnly}
|
style={styles.splitButtonSpinnerOnly}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import {
|
import {
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -12,7 +13,7 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { useStoreWithEqualityFn } from "zustand/traditional";
|
import { useStoreWithEqualityFn } from "zustand/traditional";
|
||||||
import { useIsFocused } from "@react-navigation/native";
|
import { useIsFocused } from "@react-navigation/native";
|
||||||
import { ActivityIndicator, BackHandler, Keyboard, Pressable, Text, View } from "react-native";
|
import { BackHandler, Keyboard, Pressable, Text, View } from "react-native";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useRouter, type Href } from "expo-router";
|
import { useRouter, type Href } from "expo-router";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
@@ -241,7 +242,7 @@ function buildWorkspaceFileLocation(
|
|||||||
return { path: fields.path, lineStart: fields.lineStart, lineEnd: fields.lineEnd };
|
return { path: fields.path, lineStart: fields.lineStart, lineEnd: fields.lineEnd };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
const ThemedEllipsis = withUnistyles(Ellipsis);
|
const ThemedEllipsis = withUnistyles(Ellipsis);
|
||||||
const ThemedEllipsisVertical = withUnistyles(EllipsisVertical);
|
const ThemedEllipsisVertical = withUnistyles(EllipsisVertical);
|
||||||
const ThemedChevronDown = withUnistyles(ChevronDown);
|
const ThemedChevronDown = withUnistyles(ChevronDown);
|
||||||
@@ -1373,7 +1374,7 @@ function renderWorkspaceContent(input: RenderWorkspaceContentInput): React.React
|
|||||||
if (!activeTabDescriptor && !hasHydratedAgents) {
|
if (!activeTabDescriptor && !hasHydratedAgents) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.emptyState}>
|
<View style={styles.emptyState}>
|
||||||
<ThemedActivityIndicator uniProps={mutedColorMapping} />
|
<ThemedLoadingSpinner uniProps={mutedColorMapping} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user