@@ -745,11 +765,18 @@ export function RunDetailView({
@@ -817,12 +844,41 @@ export function RunDetailView({
{/* Stream viewer */}
{selectedStreamId ? (
-
+ streamError?.includes('encrypted') && !encryptionKey ? (
+
+
+
+ This stream is encrypted.
+
+
+
+ ) : (
+
+ )
) : (
([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [cursor, setCursor] = useState();
+ const [hasMore, setHasMore] = useState(false);
+ const [loadingMore, setLoadingMore] = useState(false);
+ const isFetchingRef = useRef(false);
+
+ const fetchInitial = useCallback(async () => {
+ if (isFetchingRef.current) return;
+ isFetchingRef.current = true;
+ setLoading(true);
+ setError(null);
+ setEvents([]);
+ setCursor(undefined);
+ setHasMore(false);
+
+ try {
+ const { error: fetchError, result } = await unwrapServerActionResult(
+ fetchEvents(env, runId, {
+ sortOrder,
+ limit: INITIAL_PAGE_SIZE,
+ withData: true,
+ })
+ );
+ if (fetchError) {
+ setError(fetchError);
+ } else {
+ setEvents(result.data.map(hydrateResourceIO));
+ setCursor(result.hasMore ? result.cursor : undefined);
+ setHasMore(Boolean(result.hasMore));
+ }
+ } catch (err) {
+ setError(err as Error);
+ } finally {
+ setLoading(false);
+ isFetchingRef.current = false;
+ }
+ // encryptionKey intentionally excluded — the re-hydration effect below
+ // handles decrypting in-memory events when the key arrives.
+ }, [env, runId, sortOrder]);
+
+ useEffect(() => {
+ if (enabled) fetchInitial();
+ }, [fetchInitial, enabled]);
+
+ // Re-hydrate loaded events with decryption when encryption key becomes available
+ useEffect(() => {
+ if (!encryptionKey || events.length === 0) return;
+ let cancelled = false;
+ Promise.all(events.map((ev) => hydrateResourceIOWithKey(ev, encryptionKey)))
+ .then((decrypted) => {
+ if (!cancelled) setEvents(decrypted);
+ })
+ .catch(() => {});
+ return () => {
+ cancelled = true;
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [encryptionKey]);
+
+ const loadMore = useCallback(async () => {
+ if (loadingMore || !cursor) return;
+ setLoadingMore(true);
+ try {
+ const { error: fetchError, result } = await unwrapServerActionResult(
+ fetchEvents(env, runId, {
+ cursor,
+ sortOrder,
+ limit: LOAD_MORE_PAGE_SIZE,
+ withData: true,
+ })
+ );
+ if (fetchError) {
+ setError(fetchError);
+ } else {
+ if (result.data.length > 0) {
+ const hydrated = result.data.map(hydrateResourceIO);
+ if (encryptionKey) {
+ const decrypted = await Promise.all(
+ hydrated.map((ev) => hydrateResourceIOWithKey(ev, encryptionKey))
+ );
+ setEvents((prev) => [...prev, ...decrypted]);
+ } else {
+ setEvents((prev) => [...prev, ...hydrated]);
+ }
+ }
+ setCursor(result.hasMore ? result.cursor : undefined);
+ setHasMore(Boolean(result.hasMore));
+ }
+ } catch (err) {
+ setError(err as Error);
+ } finally {
+ setLoadingMore(false);
+ }
+ }, [env, runId, sortOrder, cursor, loadingMore, encryptionKey]);
+
+ return {
+ events,
+ loading,
+ error,
+ hasMore,
+ loadingMore,
+ loadMore,
+ };
+}