diff --git a/frontend/src/app/getting-started.tsx b/frontend/src/app/getting-started.tsx index f7e5b6dde5..ed01299276 100644 --- a/frontend/src/app/getting-started.tsx +++ b/frontend/src/app/getting-started.tsx @@ -1103,8 +1103,7 @@ function useOtherAgentInstructionsCode(provider?: Provider) { } function CopyAgentInstructionsButton({ provider }: { provider?: Provider }) { - // FIXME: after we bring back rivet compute - if (/*provider === "rivet"*/ false) { + if (provider === "rivet") { return ; } return ; @@ -1277,7 +1276,7 @@ function BackendSetupRivet() { return (
- {/* */} +
+ + + +
Actors diff --git a/frontend/src/app/provider-dropdown.tsx b/frontend/src/app/provider-dropdown.tsx index a6d23f65cf..b667b460d8 100644 --- a/frontend/src/app/provider-dropdown.tsx +++ b/frontend/src/app/provider-dropdown.tsx @@ -101,6 +101,7 @@ export function ProviderDropdown({ children }: { children: React.ReactNode }) { {children} + {externalClouds} diff --git a/frontend/src/components/actors/actors-actor-details.tsx b/frontend/src/components/actors/actors-actor-details.tsx index c93cf606a5..5df22eeede 100644 --- a/frontend/src/components/actors/actors-actor-details.tsx +++ b/frontend/src/components/actors/actors-actor-details.tsx @@ -45,6 +45,7 @@ import { GuardConnectableInspector, useInspectorGuard, } from "./guard-connectable-inspector"; +import { features } from "@/lib/features"; import type { ActorId } from "./queries"; import { ActorWorkerContextProvider } from "./worker/actor-worker-context"; import { ActorWorkflowTab } from "./workflow/actor-workflow-tab"; @@ -114,16 +115,16 @@ const TAB_PRIORITY = [ type TabId = (typeof TAB_PRIORITY)[number]; -// FIXME: once we have back rivet cloud function useManagedPool() { - // if (__APP_TYPE__ !== "cloud") return false; - // const provider = useCloudNamespaceDataProvider(); - // const { data: hasManagedPool } = useSuspenseQuery( - // provider.currentNamespaceHasManagedPoolQueryOptions(), - // ); + if (!features.platform) return false; + // biome-ignore lint/correctness/useHookAtTopLevel: guarded by build constant + const provider = useCloudNamespaceDataProvider(); + // biome-ignore lint/correctness/useHookAtTopLevel: guarded by build constant + const { data: hasManagedPool } = useSuspenseQuery( + provider.currentNamespaceHasManagedPoolQueryOptions(), + ); - // return hasManagedPool; - return false; + return hasManagedPool; } function useActorTabVisibility(actorId: ActorId) { diff --git a/frontend/src/components/deployment-logs.tsx b/frontend/src/components/deployment-logs.tsx index 74b91a4600..7f32fa150c 100644 --- a/frontend/src/components/deployment-logs.tsx +++ b/frontend/src/components/deployment-logs.tsx @@ -1,5 +1,5 @@ import type { RivetSse } from "@rivet-gg/cloud"; -import { faTriangleExclamation, Icon } from "@rivet-gg/icons"; +import { faArrowDown, faTriangleExclamation, Icon } from "@rivet-gg/icons"; import type { Virtualizer } from "@tanstack/react-virtual"; import { useCallback, useEffect, useRef, useState } from "react"; import { ErrorDetails } from "@/components/actors"; @@ -144,12 +144,30 @@ export function DeploymentLogs({ // Track the log count before a load-more so we can restore scroll position. const prevLogCountRef = useRef(0); + // Freeze displayed logs when not following so appended entries don't shift scroll. + // Always update when following, and also when history is prepended (logs grew + // from the front, detectable because the previously-first entry moved). + const frozenLogsRef = useRef(logs); + const frozenFirstIdRef = useRef(undefined); + if (follow) { + frozenLogsRef.current = logs; + frozenFirstIdRef.current = logs[0]?.data.insertId; + } else if ( + logs.length > 0 && + logs[0]?.data.insertId !== frozenFirstIdRef.current + ) { + // First entry changed — history was prepended. Accept the update. + frozenLogsRef.current = logs; + frozenFirstIdRef.current = logs[0]?.data.insertId; + } + const displayedLogs = follow ? logs : frozenLogsRef.current; + // When hasMore, index 0 is the sentinel row; real logs start at index 1. const sentinelOffset = hasMore ? 1 : 0; - const totalCount = logs.length + sentinelOffset; + const totalCount = displayedLogs.length + sentinelOffset; useEffect(() => { - if (follow && !isLoading && virtualizerRef.current && logs.length > 0) { + if (follow && !isLoading && virtualizerRef.current && displayedLogs.length > 0) { // https://github.com/TanStack/virtual/issues/537 const rafId = requestAnimationFrame(() => { virtualizerRef.current?.scrollToIndex(totalCount - 1, { @@ -158,7 +176,7 @@ export function DeploymentLogs({ }); return () => cancelAnimationFrame(rafId); } - }, [totalCount, logs.length, follow, isLoading]); + }, [totalCount, displayedLogs.length, follow, isLoading]); // After prepending older history, scroll to restore the previously-first row. const wasLoadingMoreRef = useRef(false); @@ -166,9 +184,9 @@ export function DeploymentLogs({ if ( wasLoadingMoreRef.current && !isLoadingMore && - logs.length > prevLogCountRef.current + displayedLogs.length > prevLogCountRef.current ) { - const addedCount = logs.length - prevLogCountRef.current; + const addedCount = displayedLogs.length - prevLogCountRef.current; const rafId = requestAnimationFrame(() => { // +1 to skip sentinel row at index 0. virtualizerRef.current?.scrollToIndex( @@ -181,7 +199,7 @@ export function DeploymentLogs({ return () => cancelAnimationFrame(rafId); } wasLoadingMoreRef.current = isLoadingMore; - }, [isLoadingMore, logs.length, sentinelOffset]); + }, [isLoadingMore, displayedLogs.length, sentinelOffset]); useEffect(() => { if (logsRef) { @@ -204,12 +222,12 @@ export function DeploymentLogs({ hasMore && !isLoadingMore ) { - prevLogCountRef.current = logs.length; + prevLogCountRef.current = displayedLogs.length; loadMoreHistory(); } } }, - [totalCount, logs.length, hasMore, isLoadingMore, loadMoreHistory], + [totalCount, displayedLogs.length, hasMore, isLoadingMore, loadMoreHistory], ); if (isLoading) { @@ -265,25 +283,42 @@ export function DeploymentLogs({ Stream error: {streamError}
) : null} - - virtualizerRef={virtualizerRef} - viewportRef={viewportRef} - onChange={handleScrollChange} - count={totalCount} - estimateSize={() => 24} - className="w-full flex-1 min-h-0" - scrollerProps={{ - className: "w-full", - }} - viewportProps={{}} - getRowData={(index) => { - if (hasMore && index === 0) { - return { isSentinel: true, isLoadingMore }; - } - return { entry: logs[index - sentinelOffset] }; - }} - row={LogRow} - /> +
+ + virtualizerRef={virtualizerRef} + viewportRef={viewportRef} + onChange={handleScrollChange} + count={totalCount} + estimateSize={() => 24} + className="w-full h-full" + scrollerProps={{ + className: "w-full", + }} + viewportProps={{}} + getRowData={(index) => { + if (hasMore && index === 0) { + return { isSentinel: true, isLoadingMore }; + } + return { entry: displayedLogs[index - sentinelOffset] }; + }} + row={LogRow} + /> + {!follow ? ( +
+ +
+ ) : null} +
); } diff --git a/frontend/src/components/use-deployment-logs-stream.ts b/frontend/src/components/use-deployment-logs-stream.ts index 02375be53e..1e06779d46 100644 --- a/frontend/src/components/use-deployment-logs-stream.ts +++ b/frontend/src/components/use-deployment-logs-stream.ts @@ -1,5 +1,5 @@ -import type { RivetSse } from "@rivet-gg/cloud"; -import { startTransition, useEffect, useRef, useState } from "react"; +import type { Rivet, RivetSse } from "@rivet-gg/cloud"; +import { startTransition, useCallback, useEffect, useRef, useState } from "react"; import { cloudEnv } from "@/lib/env"; const MAX_RETRIES = 8; @@ -103,6 +103,42 @@ async function sleep(ms: number, signal: AbortSignal) { } +const HISTORY_PAGE_SIZE = 100; +const INITIAL_HISTORY_SIZE = 50; + +async function fetchLogsHistory( + baseUrl: string, + project: string, + namespace: string, + pool: string, + params: { before?: string; limit?: number; region?: string; contains?: string }, +): Promise { + const qs = new URLSearchParams(); + if (params.before) qs.set("before", params.before); + if (params.limit) qs.set("limit", String(params.limit)); + if (params.region) qs.set("region", params.region); + if (params.contains) qs.set("contains", params.contains); + const query = qs.toString(); + const url = `${baseUrl}/projects/${encodeURIComponent(project)}/namespaces/${encodeURIComponent(namespace)}/managed-pools/${encodeURIComponent(pool)}/logs/history${query ? `?${query}` : ""}`; + + const response = await fetch(url, { + method: "GET", + headers: { Accept: "application/json" }, + credentials: "include", + }); + + if (!response.ok) { + const body = await response.text(); + throw new Error(`fetchLogsHistory failed with status ${response.status}: ${body}`); + } + + return response.json(); +} + +function historyToLogEvent(item: Rivet.LogHistoryResponseItem): RivetSse.LogStreamEvent.Log { + return { event: "log", data: item }; +} + async function streamWithRetry( project: string, namespace: string, @@ -173,8 +209,12 @@ export function useDeploymentLogsStream({ const [logs, setLogs] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [hasMore, setHasMore] = useState(true); const pendingRef = useRef([]); const pausedRef = useRef(paused); + const logsRef = useRef(logs); + logsRef.current = logs; useEffect(() => { pausedRef.current = paused; @@ -200,35 +240,64 @@ export function useDeploymentLogsStream({ } } - streamWithRetry( - project, - namespace, - pool, - filter, - region, - controller.signal, - () => setIsLoading(false), - onEntry, - ) - .then((result) => { - setIsLoading(false); - if (result === "exhausted") { - setError( - "Failed to connect to log stream after multiple attempts.", - ); - } else if (typeof result === "object") { - setError(result.error); - } - }) - .catch((err) => { - if ((err as Error).name !== "AbortError") { - console.error("Log stream fatal error:", err); - setIsLoading(false); - setError( - "An unexpected error occurred while streaming logs.", - ); + async function start() { + // Seed the view with recent historical logs so it isn't empty on load. + try { + const initial = await fetchLogsHistory( + cloudEnv().VITE_APP_CLOUD_API_URL, + project, + namespace, + pool, + { + limit: INITIAL_HISTORY_SIZE, + region: region || undefined, + contains: filter || undefined, + }, + ); + if (controller.signal.aborted) return; + if (initial.length > 0) { + const converted = initial.map(historyToLogEvent); + startTransition(() => { + setLogs(converted); + }); } - }); + } catch { + // Non-fatal. The stream will still start. + } + + if (controller.signal.aborted) return; + setIsLoading(false); + + const result = await streamWithRetry( + project, + namespace, + pool, + filter, + region, + controller.signal, + () => setIsLoading(false), + onEntry, + ); + + setIsLoading(false); + if (result === "exhausted") { + setError( + "Failed to connect to log stream after multiple attempts.", + ); + } else if (typeof result === "object") { + setError(result.error); + } + } + + start().catch((err) => { + if ((err as Error).name !== "AbortError") { + console.error("Log stream fatal error:", err); + setIsLoading(false); + setError( + "An unexpected error occurred while streaming logs.", + ); + } + }); return () => controller.abort(); }, [project, namespace, pool, filter, region]); @@ -243,13 +312,57 @@ export function useDeploymentLogsStream({ } }, [paused]); + // Reset hasMore when filters change. + useEffect(() => { + setHasMore(true); + }, [project, namespace, pool, filter, region]); + + const loadMoreHistory = useCallback(async () => { + if (isLoadingMore || !hasMore) return; + setIsLoadingMore(true); + try { + const currentLogs = logsRef.current; + const before = currentLogs.length > 0 + ? currentLogs[0].data.timestamp + : new Date().toISOString(); + + const items = await fetchLogsHistory( + cloudEnv().VITE_APP_CLOUD_API_URL, + project, + namespace, + pool, + { + before, + limit: HISTORY_PAGE_SIZE, + region: region || undefined, + contains: filter || undefined, + }, + ); + + if (items.length < HISTORY_PAGE_SIZE) { + setHasMore(false); + } + + if (items.length > 0) { + const converted = items.map(historyToLogEvent); + startTransition(() => { + setLogs((prev) => [...converted, ...prev]); + }); + } + } catch (err) { + console.error("Failed to load historical logs:", err); + } finally { + setIsLoadingMore(false); + } + }, [isLoadingMore, hasMore, project, namespace, pool, filter, region]); + return { logs, isLoading, error, streamError: null, - isLoadingMore: false, - hasMore: false, - loadMoreHistory: () => { }, + isLoadingMore, + hasMore, + loadMoreHistory, }; } diff --git a/package.json b/package.json index a11337ea39..c1883a112f 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "overrides": { "react": "19.1.0", "react-dom": "19.1.0", - "@rivet-gg/cloud": "https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@413534c", + "@rivet-gg/cloud": "https://pkg.pr.new/rivet-dev/engine-ee/@rivet-gg/cloud@299", "rivetkit": "workspace:*", "@rivetkit/engine-api-full": "workspace:*", "@codemirror/state": "6.5.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23472ae6e5..95854f5536 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,7 +17,7 @@ overrides: '@types/react-dom': ^19 react: 19.1.0 react-dom: 19.1.0 - '@rivet-gg/cloud': https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@413534c + '@rivet-gg/cloud': https://pkg.pr.new/rivet-dev/engine-ee/@rivet-gg/cloud@299 '@codemirror/state': 6.5.2 '@codemirror/view': 6.38.2 '@codemirror/autocomplete': 6.18.7 @@ -418,8 +418,8 @@ importers: specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@rivet-gg/cloud': - specifier: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@413534c - version: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@413534c + specifier: https://pkg.pr.new/rivet-dev/engine-ee/@rivet-gg/cloud@299 + version: https://pkg.pr.new/rivet-dev/engine-ee/@rivet-gg/cloud@299 '@rivetkit/engine-api-full': specifier: workspace:* version: link:../../engine/sdks/typescript/api-full @@ -3414,8 +3414,8 @@ importers: specifier: ^1.2.3 version: 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@rivet-gg/cloud': - specifier: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@413534c - version: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@413534c + specifier: https://pkg.pr.new/rivet-dev/engine-ee/@rivet-gg/cloud@299 + version: https://pkg.pr.new/rivet-dev/engine-ee/@rivet-gg/cloud@299 '@rivet-gg/icons': specifier: workspace:* version: link:packages/icons @@ -4576,8 +4576,8 @@ importers: specifier: 25.5.3 version: 25.5.3 '@rivet-gg/cloud': - specifier: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@413534c - version: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@413534c + specifier: https://pkg.pr.new/rivet-dev/engine-ee/@rivet-gg/cloud@299 + version: https://pkg.pr.new/rivet-dev/engine-ee/@rivet-gg/cloud@299 '@rivet-gg/components': specifier: workspace:* version: link:../frontend/packages/components @@ -8912,8 +8912,8 @@ packages: '@rivet-gg/api@25.5.3': resolution: {integrity: sha512-pj8xYQ+I/aQDbThmicPxvR+TWAzGoLSE53mbJi4QZHF8VH2oMvU7CMWqy7OTFH30DIRyVzsnHHRJZKGwtmQL3g==} - '@rivet-gg/cloud@https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@413534c': - resolution: {tarball: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@413534c} + '@rivet-gg/cloud@https://pkg.pr.new/rivet-dev/engine-ee/@rivet-gg/cloud@299': + resolution: {tarball: https://pkg.pr.new/rivet-dev/engine-ee/@rivet-gg/cloud@299} version: 0.0.0 '@rivetkit/bare-ts@0.6.2': @@ -21887,7 +21887,7 @@ snapshots: react-simple-code-editor: 0.14.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) serve-handler: 6.1.6 tailwind-merge: 2.6.0 - tailwindcss-animate: 1.0.7(tailwindcss@4.2.2) + tailwindcss-animate: 1.0.7(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)) zod: 3.25.76 transitivePeerDependencies: - '@cfworker/json-schema' @@ -23416,7 +23416,7 @@ snapshots: transitivePeerDependencies: - encoding - '@rivet-gg/cloud@https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@413534c': + '@rivet-gg/cloud@https://pkg.pr.new/rivet-dev/engine-ee/@rivet-gg/cloud@299': dependencies: cross-fetch: 4.1.0 form-data: 4.0.5 diff --git a/rivetkit-typescript/packages/rivetkit/src/client/utils.ts b/rivetkit-typescript/packages/rivetkit/src/client/utils.ts index 39a6d2f01a..e2c3cd0e2b 100644 --- a/rivetkit-typescript/packages/rivetkit/src/client/utils.ts +++ b/rivetkit-typescript/packages/rivetkit/src/client/utils.ts @@ -3,7 +3,6 @@ import type { VersionedDataHandler } from "vbare"; import type { z } from "zod/v4"; import type { Encoding } from "@/common/encoding"; import { assertUnreachable } from "@/common/utils"; -import type { HttpResponseError } from "@/common/client-protocol"; import { HTTP_RESPONSE_ERROR_VERSIONED } from "@/common/client-protocol-versioned"; import { type HttpResponseError as HttpResponseErrorJson,