diff --git a/apps/x/apps/renderer/src/components/apps/app-detail.tsx b/apps/x/apps/renderer/src/components/apps/app-detail.tsx
index 1068149e..39d294f0 100644
--- a/apps/x/apps/renderer/src/components/apps/app-detail.tsx
+++ b/apps/x/apps/renderer/src/components/apps/app-detail.tsx
@@ -180,8 +180,20 @@ export function AppDetail({ folder, onClose }: { folder: string; onClose: () =>
{app.install.updatedAt && }
>
+ ) : app.publish ? (
+ <>
+
Local app — published to the Rowboat catalog.
+ Local app — created on this machine{app.publish ? `, published as ${app.publish.repo}` : ''}.
+ Local app — created on this machine.
)}
{app.kind === 'installed' && app.install?.repo && (
@@ -268,11 +280,22 @@ export function AppDetail({ folder, onClose }: { folder: string; onClose: () =>
)
}
-function InfoRow({ k, v, mono }: { k: string; v: string; mono?: boolean }) {
+function InfoRow({ k, v, mono, link }: { k: string; v: string; mono?: boolean; link?: string }) {
return (
{k}
-
{v}
+ {link ? (
+
+ {v}
+
+ ) : (
+
{v}
+ )}
)
}
diff --git a/apps/x/apps/renderer/src/components/apps/app-frame.tsx b/apps/x/apps/renderer/src/components/apps/app-frame.tsx
index c406b903..ae7dcc5c 100644
--- a/apps/x/apps/renderer/src/components/apps/app-frame.tsx
+++ b/apps/x/apps/renderer/src/components/apps/app-frame.tsx
@@ -1,8 +1,9 @@
import { useEffect, useState } from 'react'
-import { ArrowLeft, ExternalLink, Info, RotateCw } from 'lucide-react'
+import { ArrowLeft, BadgeCheck, ExternalLink, Info, RotateCw, UploadCloud } from 'lucide-react'
import type { rowboatApp } from '@x/shared'
import { appOpened } from '@/lib/analytics'
import { AppDetail } from '@/components/apps/app-detail'
+import { PublishDialog } from '@/components/apps/publish-dialog'
// Full-height iframe on the app's own origin (spec §6.6). No sandbox attr —
// per-app browser origins are the isolation boundary. Toolbar: back, reload,
@@ -11,6 +12,7 @@ import { AppDetail } from '@/components/apps/app-detail'
export function AppFrame({ app, onBack }: { app: rowboatApp.AppSummary; onBack: () => void }) {
const [reloadNonce, setReloadNonce] = useState(0)
const [showDetail, setShowDetail] = useState(false)
+ const [showPublish, setShowPublish] = useState(false)
// Load watchdog: if the iframe hasn't fired `load` within the deadline,
// surface a visible retry state instead of a silent blank pane.
const [loadState, setLoadState] = useState<'loading' | 'ok' | 'stuck'>('loading')
@@ -62,6 +64,27 @@ export function AppFrame({ app, onBack }: { app: rowboatApp.AppSummary; onBack:
>
+ {app.kind === 'local' && (
+ app.publish ? (
+
+ ) : (
+
+ )
+ )}
+ {showPublish && (
+