From 44507c86d2b2298651b689da4a0a871f481f8d5e Mon Sep 17 00:00:00 2001 From: J Chris Anderson Date: Tue, 2 Dec 2025 08:37:12 -0800 Subject: [PATCH 1/2] feat: Add app metadata endpoint and fix instance naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create GET /api/apps/:slug endpoint for fetching app metadata - Returns public metadata only (title, remixOf, hasScreenshot, hasIcon) - Update vibe-viewer to fetch real app title instead of "Fresh Data" - Make sessionId optional in SessionSidebarProps - Remove hardcoded sessionId="" from BrutalistLayout Benefits: - Instances get actual app names instead of "Fresh Data" - Uses public slug endpoint (doesn't leak chatId) - SessionSidebar more flexible for non-chat pages - Fallback to slug if metadata fetch fails 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- hosting/pkg/src/endpoints/appGet.ts | 69 +++++++++++++++++++ hosting/pkg/src/index.ts | 3 + prompts/pkg/chat.ts | 2 +- .../pkg/app/components/BrutalistLayout.tsx | 6 +- vibes.diy/pkg/app/routes/vibe-viewer.tsx | 31 +++++++-- 5 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 hosting/pkg/src/endpoints/appGet.ts diff --git a/hosting/pkg/src/endpoints/appGet.ts b/hosting/pkg/src/endpoints/appGet.ts new file mode 100644 index 000000000..ae36ffb52 --- /dev/null +++ b/hosting/pkg/src/endpoints/appGet.ts @@ -0,0 +1,69 @@ +import { OpenAPIRoute, contentJson } from "chanfana"; +import { Context } from "hono"; +import { z } from "zod"; + +// Public metadata response - no sensitive data +const AppMetadataSchema = z.object({ + slug: z.string(), + title: z.string().optional(), + remixOf: z.string().nullable().optional(), + hasScreenshot: z.boolean(), + hasIcon: z.boolean(), +}); + +export class AppGet extends OpenAPIRoute { + schema = { + tags: ["Apps"], + summary: "Get app metadata by slug", + request: { + params: z.object({ + slug: z.string(), + }), + }, + responses: { + "200": { + description: "Returns app metadata", + ...contentJson( + z.object({ + success: z.boolean(), + app: AppMetadataSchema, + }), + ), + }, + "404": { + description: "App not found", + ...contentJson( + z.object({ + error: z.string(), + }), + ), + }, + }, + }; + + async handle(c: Context<{ Bindings: Env }>) { + const { slug } = c.req.param(); + const kv = c.env.KV; + + // Fetch app from KV by slug + const appData = await kv.get(slug); + + if (!appData) { + return c.json({ error: "App not found" }, 404); + } + + const app = JSON.parse(appData); + + // Return only public metadata - no code, userId, or chatId + return c.json({ + success: true, + app: { + slug: app.slug, + title: app.title || slug, + remixOf: app.remixOf || null, + hasScreenshot: app.hasScreenshot || false, + hasIcon: app.hasIcon || false, + }, + }); + } +} diff --git a/hosting/pkg/src/index.ts b/hosting/pkg/src/index.ts index 562bafc0e..34826d90b 100644 --- a/hosting/pkg/src/index.ts +++ b/hosting/pkg/src/index.ts @@ -1,5 +1,6 @@ import { fromHono } from "chanfana"; import { AppCreate } from "./endpoints/appCreate.js"; +import { AppGet } from "./endpoints/appGet.js"; import { ClaudeChat, ImageEdit, @@ -86,6 +87,7 @@ openapi.use("/api/*", async (c, next) => { // Register OpenAPI endpoints openapi.post("/api/apps", AppCreate); +openapi.get("/api/apps/:slug", AppGet); // Register OpenAI image endpoints openapi.post("/api/openai-image/generate", ImageGenerate); @@ -107,6 +109,7 @@ export default { // Test exports - expose internal modules for testing export { AppCreate } from "./endpoints/appCreate.js"; +export { AppGet } from "./endpoints/appGet.js"; // Re-export from hosting-base export { // the only import of these is tests, they should take direct from hosting-base, and then remove these lines diff --git a/prompts/pkg/chat.ts b/prompts/pkg/chat.ts index dde1497d7..3ffb35997 100644 --- a/prompts/pkg/chat.ts +++ b/prompts/pkg/chat.ts @@ -215,5 +215,5 @@ export interface ChatInterfaceProps extends ChatState { export interface SessionSidebarProps { isVisible: boolean; onClose: () => void; - sessionId: string; + sessionId?: string; } diff --git a/vibes.diy/pkg/app/components/BrutalistLayout.tsx b/vibes.diy/pkg/app/components/BrutalistLayout.tsx index 48da6f4f8..4e22c2d57 100644 --- a/vibes.diy/pkg/app/components/BrutalistLayout.tsx +++ b/vibes.diy/pkg/app/components/BrutalistLayout.tsx @@ -32,11 +32,7 @@ export default function BrutalistLayout({ return (
{/* SessionSidebar */} - + {/* Hamburger menu button - fixed top left */}
diff --git a/vibes.diy/pkg/app/routes/vibe-viewer.tsx b/vibes.diy/pkg/app/routes/vibe-viewer.tsx index cb0152cc4..7d3514e61 100644 --- a/vibes.diy/pkg/app/routes/vibe-viewer.tsx +++ b/vibes.diy/pkg/app/routes/vibe-viewer.tsx @@ -55,11 +55,34 @@ function VibeInstanceViewerContent() { const fullId = `${titleId}-${installId}`; const instanceExists = instances.some((inst) => inst._id === fullId); - // Create instance if it doesn't exist (lazy creation for Fresh Data) - // Pass the installId explicitly to ensure correct _id is created + // Create instance if it doesn't exist - fetch real title from hosting API if (!instanceExists) { - // Let error throw - no catch handler - createInstance("Fresh Data", {}, installId); + const fetchAndCreateInstance = async () => { + try { + // Fetch app metadata from hosting API + const apiBaseUrl = + import.meta.env.VITE_API_BASE_URL || + "https://vibes-hosting-v2-preview.jchris.workers.dev"; + const response = await fetch(`${apiBaseUrl}/api/apps/${titleId}`); + + let title = titleId; // Fallback to slug if fetch fails + if (response.ok) { + const data = (await response.json()) as { + app?: { title?: string }; + }; + title = data.app?.title || titleId; + } + + // Create instance with real title + await createInstance(title, {}, installId); + } catch (error) { + // If fetch fails, use slug as title + console.warn("Failed to fetch app metadata, using slug:", error); + await createInstance(titleId, {}, installId); + } + }; + + fetchAndCreateInstance(); } }, [titleId, installId, instances, createInstance, isCreating]); From b6eea34ffe49a7b8bad7fa009d4a5aa4da261911 Mon Sep 17 00:00:00 2001 From: CharlieHelps Date: Wed, 3 Dec 2025 13:40:50 +0000 Subject: [PATCH 2/2] fix: guard async instance creation in vibe viewer --- vibes.diy/pkg/app/routes/vibe-viewer.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/vibes.diy/pkg/app/routes/vibe-viewer.tsx b/vibes.diy/pkg/app/routes/vibe-viewer.tsx index 7d3514e61..1819d60dd 100644 --- a/vibes.diy/pkg/app/routes/vibe-viewer.tsx +++ b/vibes.diy/pkg/app/routes/vibe-viewer.tsx @@ -57,6 +57,8 @@ function VibeInstanceViewerContent() { // Create instance if it doesn't exist - fetch real title from hosting API if (!instanceExists) { + let cancelled = false; + const fetchAndCreateInstance = async () => { try { // Fetch app metadata from hosting API @@ -73,16 +75,24 @@ function VibeInstanceViewerContent() { title = data.app?.title || titleId; } - // Create instance with real title - await createInstance(title, {}, installId); + if (!cancelled) { + // Create instance with real title + await createInstance(title, {}, installId); + } } catch (error) { // If fetch fails, use slug as title console.warn("Failed to fetch app metadata, using slug:", error); - await createInstance(titleId, {}, installId); + if (!cancelled) { + await createInstance(titleId, {}, installId); + } } }; - fetchAndCreateInstance(); + void fetchAndCreateInstance(); + + return () => { + cancelled = true; + }; } }, [titleId, installId, instances, createInstance, isCreating]);