From cb90b5f73e962a41e8d07366e3e2a37fe089e604 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 14:23:12 +0000 Subject: [PATCH 1/2] feat(web): replace library page stubs with real implementations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the already-ported LibraryView and AppViewerContainer components into the library route pages, replacing the placeholder stubs. - LibraryPage renders LibraryView with assistantId from outlet context, matching the platform's rendering wrapper with the same container styling (rounded border, surface overlay background). - LibraryDetailPage is a deep-link page for /assistant/library/:appId that fetches the app via openApp() and renders AppViewerContainer directly, with loading/error states and back-navigation to the library listing. Routes in routes.tsx were already correctly wired from the initial scaffolding — no changes needed there. Closes LUM-1657 Co-Authored-By: ashlee@vellum.ai --- .../domains/library/library-detail-page.tsx | 102 ++++++++++++++++-- apps/web/src/domains/library/library-page.tsx | 33 +++++- 2 files changed, 124 insertions(+), 11 deletions(-) diff --git a/apps/web/src/domains/library/library-detail-page.tsx b/apps/web/src/domains/library/library-detail-page.tsx index 0708d860c2e..9572acb911c 100644 --- a/apps/web/src/domains/library/library-detail-page.tsx +++ b/apps/web/src/domains/library/library-detail-page.tsx @@ -1,13 +1,101 @@ -import { useParams } from "react-router"; +import { Loader2 } from "lucide-react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useNavigate, useParams } from "react-router"; + +import { useAssistantContext } from "@/domains/chat/assistant-context.js"; +import { openApp, shareApp } from "@/domains/chat/lib/apps.js"; +import { AppViewerContainer } from "@/domains/intelligence/components/apps/app-viewer-container.js"; +import { routes } from "@/utils/routes.js"; + +interface LoadedApp { + appId: string; + dirName?: string; + name: string; + html: string; +} export function LibraryDetailPage() { const { appId } = useParams<{ appId: string }>(); + const { assistantId } = useAssistantContext(); + const navigate = useNavigate(); + + const [app, setApp] = useState(null); + const [error, setError] = useState(null); + const [isSharing, setIsSharing] = useState(false); + const requestRef = useRef(null); + + useEffect(() => { + if (!assistantId || !appId) return; + requestRef.current = appId; + setApp(null); + setError(null); + + openApp(assistantId, appId) + .then((result) => { + if (requestRef.current !== appId) return; + setApp({ + appId: result.appId, + dirName: result.dirName, + name: result.name, + html: result.html, + }); + }) + .catch((err) => { + if (requestRef.current !== appId) return; + setError(err instanceof Error ? err.message : "Failed to open app"); + }); + }, [assistantId, appId]); + + const handleClose = useCallback(() => { + void navigate(routes.library.root); + }, [navigate]); + + const handleShare = useCallback(async () => { + if (!assistantId || !app || isSharing) return; + setIsSharing(true); + try { + await shareApp(assistantId, app.appId, app.name); + } finally { + setIsSharing(false); + } + }, [assistantId, app, isSharing]); + + if (!assistantId || !appId) return null; + + if (error) { + return ( +
+

+ {error} +

+ +
+ ); + } + + if (!app) { + return ( +
+ +
+ ); + } + return ( -
-

Library item

-

- Placeholder for library item {appId}. -

-
+ ); } diff --git a/apps/web/src/domains/library/library-page.tsx b/apps/web/src/domains/library/library-page.tsx index 0c432852dc6..2a2d7d34c53 100644 --- a/apps/web/src/domains/library/library-page.tsx +++ b/apps/web/src/domains/library/library-page.tsx @@ -1,8 +1,33 @@ +import { useCallback } from "react"; +import { useNavigate } from "react-router"; + +import { useAssistantContext } from "@/domains/chat/assistant-context.js"; +import { LibraryView } from "@/domains/intelligence/components/apps/library-view.js"; +import { routes } from "@/utils/routes.js"; + export function LibraryPage() { + const { assistantId } = useAssistantContext(); + const navigate = useNavigate(); + + const handleNewConversation = useCallback( + (initialMessage?: string) => { + void navigate( + initialMessage + ? `${routes.assistant}?message=${encodeURIComponent(initialMessage)}` + : routes.assistant, + ); + }, + [navigate], + ); + + if (!assistantId) return null; + return ( -
-

Library

-

Placeholder route. Library listing lands with the platform/web code port.

-
+
+ +
); } From 552e00527373376c650d60193a58a368ca23303d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 14:37:22 +0000 Subject: [PATCH 2/2] fix(web): add share error handling, fix dead query param, add effect cleanup - library-detail-page: add catch block + toast for shareApp failures, add toast.success on share, add useEffect unmount cleanup for requestRef - library-page: remove non-functional ?message= query param (chat route never reads it); navigate to chat without seeding initial message until cross-route state coordination is implemented Co-Authored-By: ashlee@vellum.ai --- .../src/domains/library/library-detail-page.tsx | 11 +++++++++++ apps/web/src/domains/library/library-page.tsx | 14 ++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/apps/web/src/domains/library/library-detail-page.tsx b/apps/web/src/domains/library/library-detail-page.tsx index 9572acb911c..8d8a862f4ce 100644 --- a/apps/web/src/domains/library/library-detail-page.tsx +++ b/apps/web/src/domains/library/library-detail-page.tsx @@ -2,6 +2,8 @@ import { Loader2 } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router"; +import { toast } from "@vellum/design-library"; + import { useAssistantContext } from "@/domains/chat/assistant-context.js"; import { openApp, shareApp } from "@/domains/chat/lib/apps.js"; import { AppViewerContainer } from "@/domains/intelligence/components/apps/app-viewer-container.js"; @@ -44,6 +46,10 @@ export function LibraryDetailPage() { if (requestRef.current !== appId) return; setError(err instanceof Error ? err.message : "Failed to open app"); }); + + return () => { + requestRef.current = null; + }; }, [assistantId, appId]); const handleClose = useCallback(() => { @@ -55,6 +61,11 @@ export function LibraryDetailPage() { setIsSharing(true); try { await shareApp(assistantId, app.appId, app.name); + toast.success("App exported", { description: `${app.name}.vellum` }); + } catch (err) { + toast.error("Failed to share app", { + description: err instanceof Error ? err.message : undefined, + }); } finally { setIsSharing(false); } diff --git a/apps/web/src/domains/library/library-page.tsx b/apps/web/src/domains/library/library-page.tsx index 2a2d7d34c53..f8fe58d7d70 100644 --- a/apps/web/src/domains/library/library-page.tsx +++ b/apps/web/src/domains/library/library-page.tsx @@ -10,12 +10,14 @@ export function LibraryPage() { const navigate = useNavigate(); const handleNewConversation = useCallback( - (initialMessage?: string) => { - void navigate( - initialMessage - ? `${routes.assistant}?message=${encodeURIComponent(initialMessage)}` - : routes.assistant, - ); + (_initialMessage?: string) => { + // TODO: initialMessage seeding requires cross-route state coordination + // (e.g. a Zustand store or sessionStorage handoff). The platform passes + // initialMessage directly via startNewConversation() in the same React + // tree, but here the library is a separate route. For now we just + // navigate to chat; the deploy-flow prompt handoff will come with the + // broader cross-route state work. + void navigate(routes.assistant); }, [navigate], );