function appsNavLink(label, href, active) { const style = { background: "transparent", border: 0, padding: "6px 10px", borderRadius: 6, color: "var(--text-muted)", fontSize: 13, textDecoration: "none", cursor: "pointer", }; return active ? : {label}; } function appStatus(app) { return app.manifest && Object.keys(app.manifest.tables || {}).length ? "published" : "draft"; } function appDescription(app) { return app.manifest?.description || "A Supabase-backed AI-powered app with its own data and tools."; } function appPalette(app) { const palettes = ["#dce8e3", "#f9ead8", "#e0e3f1", "#e8e1f0"]; const key = String(app.app_id || "").split("").reduce((sum, char) => sum + char.charCodeAt(0), 0); return palettes[key % palettes.length]; } function appDataColumns(recordPage) { const manifestColumns = Array.isArray(recordPage?.fields) ? recordPage.fields.map((field) => field.name) : []; const items = Array.isArray(recordPage?.items) ? recordPage.items : []; const systemColumns = ["id", "created_at", "updated_at"].filter((name) => items.some((item) => Object.prototype.hasOwnProperty.call(item || {}, name))); return [...systemColumns, ...manifestColumns]; } function appDataCell(value) { if (value === null || value === undefined) return { text: "—", title: "" }; if (Array.isArray(value) || typeof value === "object") { try { const json = JSON.stringify(value); return { text: json, title: json }; } catch (_) {} } const text = String(value); return { text, title: text }; } function normalizePositiveNumber(value, fallback) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed < 1) return fallback; return Math.floor(parsed); } function normalizeAppRecordPage(recordPage, fallbackPage) { const items = Array.isArray(recordPage?.items) ? recordPage.items : []; const fields = Array.isArray(recordPage?.fields) ? recordPage.fields : []; const total = Math.max(0, Number(recordPage?.total || 0)); const page_size = Math.min(100, Math.max(1, normalizePositiveNumber(recordPage?.page_size, 50))); const inferredTotalPages = total ? Math.ceil(total / page_size) : 0; const rawTotalPages = Math.max(0, Number(recordPage?.total_pages ?? inferredTotalPages) || 0); const total_pages = inferredTotalPages ? Math.max(inferredTotalPages, rawTotalPages) : rawTotalPages; const page = Math.min(total_pages || 1, normalizePositiveNumber(recordPage?.page, fallbackPage)); return { ...recordPage, table: recordPage?.table || "", items, fields, total, page, page_size, total_pages, }; } function appRecordRange(recordPage) { const total = Number(recordPage?.total || 0); if (!total) return "0 of 0 records"; const items = Array.isArray(recordPage?.items) ? recordPage.items : []; const page = normalizePositiveNumber(recordPage?.page, 1); const page_size = Math.min(100, Math.max(1, normalizePositiveNumber(recordPage?.page_size, 50))); const start = Math.min(total, ((page - 1) * page_size) + 1); const end = items.length ? Math.min(total, start + items.length - 1) : start; return `${start}–${Math.max(start, end)} of ${total} records`; } function appPaginationPages(page, totalPages) { if (totalPages <= 1) return totalPages ? [1] : []; if (totalPages <= 7) return Array.from({ length: totalPages }, (_, index) => index + 1); const pages = [1]; const start = Math.max(2, page - 1); const end = Math.min(totalPages - 1, page + 1); if (start > 2) pages.push("left-gap"); for (let current = start; current <= end; current += 1) pages.push(current); if (end < totalPages - 1) pages.push("right-gap"); pages.push(totalPages); return pages; } function readAppsUrlState() { const params = new URLSearchParams(window.location.search); return { appId: params.get("app_id"), view: params.get("view") || "settings", }; } function AppDataPanel({ app, onBack }) { const [tables, setTables] = React.useState([]); const [tablesAppId, setTablesAppId] = React.useState(null); const [selectedTable, setSelectedTable] = React.useState(""); const [page, setPage] = React.useState(1); const [searchInput, setSearchInput] = React.useState(""); const [search, setSearch] = React.useState(""); const [recordPage, setRecordPage] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(""); const requestSeqRef = React.useRef(0); React.useEffect(() => { let cancelled = false; requestSeqRef.current += 1; setLoading(true); setError(""); setTables([]); setTablesAppId(null); setSelectedTable(""); setPage(1); setSearchInput(""); setSearch(""); setRecordPage(null); api.listAppTables(app.app_id) .then((items) => { if (cancelled) return; const nextTables = Array.isArray(items) ? items : []; setTables(nextTables); setTablesAppId(app.app_id); }) .catch((err) => { if (cancelled) return; setError(err.message || "Unable to load App tables"); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [app.app_id]); React.useEffect(() => { if (!tables.length) { setSelectedTable(""); return; } if (!tables.some((table) => table.name === selectedTable)) { setSelectedTable(tables[0].name); setPage(1); } }, [tables, selectedTable]); React.useEffect(() => { const timeoutId = window.setTimeout(() => { const nextSearch = searchInput.trim(); setSearch((previous) => previous === nextSearch ? previous : nextSearch); setPage(1); }, 250); return () => window.clearTimeout(timeoutId); }, [searchInput]); React.useEffect(() => { if (!selectedTable) { setRecordPage(null); return; } if (tablesAppId !== app.app_id) { setRecordPage(null); return; } if (!tables.some((table) => table.name === selectedTable)) { setRecordPage(null); return; } const requestSeq = requestSeqRef.current + 1; requestSeqRef.current = requestSeq; setRecordPage(null); setLoading(true); setError(""); api.listAppRecordPage(app.app_id, selectedTable, { page, pageSize: 50, search }) .then((nextPage) => { if (requestSeqRef.current !== requestSeq) return; setRecordPage(normalizeAppRecordPage(nextPage || {}, page)); }) .catch((err) => { if (requestSeqRef.current !== requestSeq) return; setRecordPage(null); setError(err.message || "Unable to load App records"); }) .finally(() => { if (requestSeqRef.current === requestSeq) setLoading(false); }); }, [app.app_id, tables, tablesAppId, selectedTable, page, search]); const activeTable = tables.find((table) => table.name === selectedTable) || tables[0] || null; const columns = React.useMemo(() => appDataColumns(recordPage), [recordPage]); const pageButtons = React.useMemo(() => appPaginationPages(Number(recordPage?.page || page), Number(recordPage?.total_pages || 0)), [page, recordPage]); const hasTables = tables.length > 0; const items = Array.isArray(recordPage?.items) ? recordPage.items : []; const canGoPrevious = Number(recordPage?.page || page) > 1; const canGoNext = Number(recordPage?.page || page) < Number(recordPage?.total_pages || 0); return

{app.name} · Data

{app.app_id} · Browse published manifest tables and records.

{appStatus(app) === "published" ? "Published" : "Draft"}

{activeTable ? activeTable.name : "Records"}

{activeTable ? `${activeTable.record_count} records · Read-only App data view.` : "Select a published table to browse records."}

{!hasTables && loading &&
Loading tables…
} {!hasTables && error &&
{error}
} {hasTables && error &&
{error}
} {hasTables && !error && ( <>
{columns.map((name) => )} {loading && } {!loading && items.length === 0 && } {!loading && items.map((item, rowIndex) => {columns.map((name) => { const cell = appDataCell(item?.[name]); return ; })} )}
{name}
Loading records…
{search ? "No records match this search." : "No records yet for this table."}
{cell.text}
{recordPage ? appRecordRange(recordPage) : "0 of 0 records"}
{pageButtons.map((item) => typeof item === "number" ? : )}
)}
; } function AppSettingsPanel({ app, agents, onBack, onUpdated }) { const initialPolicy = app.manifest?.policy || {}; const [manifestText, setManifestText] = React.useState(JSON.stringify(app.manifest || { app_id: app.app_id, tables: {} }, null, 2)); const [authentication, setAuthentication] = React.useState(initialPolicy.authentication || "required"); const [allowedOrigins, setAllowedOrigins] = React.useState(initialPolicy.allowed_origins || []); const [executor, setExecutor] = React.useState(app.executor_agent_id); const [busy, setBusy] = React.useState(false); const [message, setMessage] = React.useState(""); const [error, setError] = React.useState(""); const publish = async () => { setBusy(true); setError(""); setMessage(""); try { const manifest = JSON.parse(manifestText); const normalizedOrigins = allowedOrigins.map((origin) => origin.trim()); if (normalizedOrigins.some((origin) => !origin)) throw new Error("Each allowed origin must be a non-empty URL"); const updated = await api.publishAppManifest(app.app_id, { ...manifest, app_id: app.app_id, policy: { ...manifest.policy, authentication, allowed_origins: normalizedOrigins }, }); onUpdated(updated); setMessage("Manifest published"); } catch (err) { setError(err.message || "Unable to publish manifest"); } finally { setBusy(false); } }; const replaceExecutor = async (value) => { setBusy(true); setError(""); setMessage(""); try { const updated = await api.replaceAppExecutor(app.app_id, { executor_agent_id: value, expected_version: app.version }); setExecutor(value); onUpdated(updated); setMessage("Executor updated"); } catch (err) { setError(err.message || "Unable to update executor"); } finally { setBusy(false); } }; return

{app.name}

{app.app_id} · App settings

{appStatus(app) === "published" ? "Published" : "Draft"}

App details

Owner type{app.owner_type}Owner ID{app.owner_id}Status{app.status}Version{app.version}

Executor Agent

Choose the Agent that can call and operate this App.

{!agents.length && Create an Agent first}

Runtime policy

Control whether the App requires a platform login and which browser origins may call its Runtime API.

Allowed origins

Exact browser origins, for example http://localhost:5173. Do not include a path or *.

{allowedOrigins.map((origin, index) =>
setAllowedOrigins((current) => current.map((item, itemIndex) => itemIndex === index ? event.target.value : item))} />
)}

Manifest

Define the App schema and published data contract.