diff --git a/apps/x/apps/main/src/mini-apps-handler.ts b/apps/x/apps/main/src/mini-apps-handler.ts index 715fe9ae..50d82b30 100644 --- a/apps/x/apps/main/src/mini-apps-handler.ts +++ b/apps/x/apps/main/src/mini-apps-handler.ts @@ -24,10 +24,20 @@ function appDir(id: string): string { */ export const MINIAPP_BRIDGE_JS = ` (function () { - var data = null, dataLoaded = false, state = null; - var dataCbs = [], stateCbs = []; + var data = null, dataLoaded = false, state = null, theme = 'dark'; + var dataCbs = [], stateCbs = [], themeCbs = []; var pending = {}, seq = 0; function post(msg) { parent.postMessage(msg, '*'); } + // Apply the host theme to so app CSS can use html.dark / html.light + // (and native controls via color-scheme). + function applyTheme(t) { + theme = t === 'light' ? 'light' : 'dark'; + var el = document.documentElement; + el.classList.remove('light', 'dark'); el.classList.add(theme); + el.setAttribute('data-theme', theme); + el.style.colorScheme = theme; + themeCbs.forEach(function (cb) { try { cb(theme); } catch (_) {} }); + } function rpc(method, params) { var id = 'r' + (++seq); return new Promise(function (resolve, reject) { @@ -49,6 +59,8 @@ export const MINIAPP_BRIDGE_JS = ` if (m.type === 'rowboat:mini-app:state') { state = m.state; stateCbs.forEach(function (cb) { try { cb(state); } catch (_) {} }); + } else if (m.type === 'rowboat:mini-app:theme') { + applyTheme(m.theme); } else if (m.type === 'rowboat:mini-app:rpc-result') { var p = pending[m.id]; if (p) { delete pending[m.id]; if (m.ok) p.resolve(m.result); else p.reject(new Error(m.error || 'request failed')); } @@ -56,6 +68,8 @@ export const MINIAPP_BRIDGE_JS = ` }); window.rowboat = { getData: function () { return data; }, + getTheme: function () { return theme; }, + onTheme: function (cb) { themeCbs.push(cb); try { cb(theme); } catch (_) {} return function () { var i = themeCbs.indexOf(cb); if (i >= 0) themeCbs.splice(i, 1); }; }, onData: function (cb) { dataCbs.push(cb); if (dataLoaded) { try { cb(data); } catch (_) {} } return function () { var i = dataCbs.indexOf(cb); if (i >= 0) dataCbs.splice(i, 1); }; }, refreshData: function () { return loadData(); }, getState: function () { return state; }, diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 9a1792b6..30341ac7 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -3604,6 +3604,13 @@ function App() { setPendingPaletteSubmit({ text, mention }) }, [isChatSidebarOpen, handleNewChatTabInSidebar]) + // Open the chat sidebar on a fresh tab and pre-fill (not send) a builder prompt. + const prefillChat = useCallback((text: string) => { + if (!isChatSidebarOpen) setIsChatSidebarOpen(true) + handleNewChatTabInSidebar() + setPresetMessage(text) + }, [isChatSidebarOpen, handleNewChatTabInSidebar]) + useEffect(() => { if (!pendingPaletteSubmit) return const fileMention: FileMention | undefined = pendingPaletteSubmit.mention @@ -5993,7 +6000,11 @@ function App() { ) : isAppsOpen ? (
- + prefillChat('Build me a mini-app that ')} + />
) : isEmailOpen ? (
diff --git a/apps/x/apps/renderer/src/components/mini-app-frame.tsx b/apps/x/apps/renderer/src/components/mini-app-frame.tsx index 975f1e0a..3e152301 100644 --- a/apps/x/apps/renderer/src/components/mini-app-frame.tsx +++ b/apps/x/apps/renderer/src/components/mini-app-frame.tsx @@ -28,6 +28,15 @@ export function MiniAppFrame({ manifest }: { manifest: miniApp.MiniAppManifest } iframeRef.current?.contentWindow?.postMessage(message, '*') } + const currentTheme = (): 'light' | 'dark' => + document.documentElement.classList.contains('dark') ? 'dark' : 'light' + + // Push host theme changes into the app so it can restyle live. + const themeObserver = new MutationObserver(() => { + postToFrame({ type: MINI_APP_MESSAGE.theme, theme: currentTheme() }) + }) + themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }) + async function handleRpc(method: string, params: Record): Promise { // fetch is a network proxy, not toolkit-scoped — handle before the scope gate. if (method === 'fetch') { @@ -74,9 +83,10 @@ export function MiniAppFrame({ manifest }: { manifest: miniApp.MiniAppManifest } } // Apps load their own data.json (served sibling) via a relative fetch; the - // host only provides per-app UI state on ready. + // host provides per-app UI state and the current theme on ready. function handleReady() { postToFrame({ type: MINI_APP_MESSAGE.state, state: stateRef.current }) + postToFrame({ type: MINI_APP_MESSAGE.theme, theme: currentTheme() }) } function handleMessage(event: MessageEvent) { @@ -112,7 +122,10 @@ export function MiniAppFrame({ manifest }: { manifest: miniApp.MiniAppManifest } } window.addEventListener('message', handleMessage) - return () => window.removeEventListener('message', handleMessage) + return () => { + window.removeEventListener('message', handleMessage) + themeObserver.disconnect() + } }, [manifest.id, scope]) return ( diff --git a/apps/x/apps/renderer/src/components/mini-apps-view.tsx b/apps/x/apps/renderer/src/components/mini-apps-view.tsx index 2f8cc9a8..571449ce 100644 --- a/apps/x/apps/renderer/src/components/mini-apps-view.tsx +++ b/apps/x/apps/renderer/src/components/mini-apps-view.tsx @@ -132,10 +132,11 @@ const CARD_CSS = ` .ma-lastrun { font-size:11.5px; color:var(--ma-lastrun); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .ma-new { - min-height:clamp(190px,24cqw,244px); border-radius:18px; border:1px dashed var(--ma-new-border); + width:100%; font:inherit; min-height:clamp(190px,24cqw,244px); border-radius:18px; border:1px dashed var(--ma-new-border); background:transparent; display:flex; flex-direction:column; align-items:center; justify-content:center; - gap:8px; color:var(--ma-new-title); cursor:default; transition: border-color .2s ease, color .2s ease; + gap:8px; color:var(--ma-new-title); cursor:pointer; transition: border-color .2s ease, color .2s ease, background .2s ease; } +.ma-new:hover { border-color:var(--ma-border-hover); background:color-mix(in srgb, var(--accent, #888) 6%, transparent); } .ma-new-title { font-size:14.5px; font-weight:600; color:var(--ma-new-title); } .ma-new-hint { font-size:12px; color:var(--ma-new-hint); text-align:center; padding:0 12px; } @@ -170,7 +171,7 @@ function Card({ app, index, onOpen }: { app: miniApp.MiniAppManifest; index: num ) } -export function MiniAppsView({ initialAppId, initialVersion }: { initialAppId?: string | null; initialVersion?: number } = {}) { +export function MiniAppsView({ initialAppId, initialVersion, onNewApp }: { initialAppId?: string | null; initialVersion?: number; onNewApp?: () => void } = {}) { const [selectedId, setSelectedId] = useState(initialAppId ?? null) const [manifests, setManifests] = useState([]) @@ -230,12 +231,12 @@ export function MiniAppsView({ initialAppId, initialVersion }: { initialAppId?: setSelectedId(m.id)} /> ))} - {/* Placeholder for copilot-generated apps (Phase 3). */} -
+ {/* Kick off the copilot builder with a pre-filled prompt. */} +
+
Describe one to the copilot
+
diff --git a/apps/x/apps/renderer/src/mini-apps/runtime.ts b/apps/x/apps/renderer/src/mini-apps/runtime.ts index 1dbdba06..c88feb62 100644 --- a/apps/x/apps/renderer/src/mini-apps/runtime.ts +++ b/apps/x/apps/renderer/src/mini-apps/runtime.ts @@ -15,10 +15,18 @@ */ const BRIDGE_SHIM = /* js */ ` (function () { - var data = null, dataLoaded = false, state = null; - var dataCbs = [], stateCbs = []; + var data = null, dataLoaded = false, state = null, theme = 'dark'; + var dataCbs = [], stateCbs = [], themeCbs = []; var pending = {}, seq = 0; function post(msg) { parent.postMessage(msg, '*'); } + function applyTheme(t) { + theme = t === 'light' ? 'light' : 'dark'; + var el = document.documentElement; + el.classList.remove('light', 'dark'); el.classList.add(theme); + el.setAttribute('data-theme', theme); + el.style.colorScheme = theme; + themeCbs.forEach(function (cb) { try { cb(theme); } catch (_) {} }); + } function rpc(method, params) { var id = 'r' + (++seq); return new Promise(function (resolve, reject) { @@ -39,6 +47,8 @@ const BRIDGE_SHIM = /* js */ ` if (m.type === 'rowboat:mini-app:state') { state = m.state; stateCbs.forEach(function (cb) { try { cb(state); } catch (_) {} }); + } else if (m.type === 'rowboat:mini-app:theme') { + applyTheme(m.theme); } else if (m.type === 'rowboat:mini-app:rpc-result') { var p = pending[m.id]; if (p) { @@ -50,6 +60,8 @@ const BRIDGE_SHIM = /* js */ ` window.rowboat = { getData: function () { return data; }, refreshData: function () { return loadData(); }, + getTheme: function () { return theme; }, + onTheme: function (cb) { themeCbs.push(cb); try { cb(theme); } catch (_) {} return function () { var i = themeCbs.indexOf(cb); if (i >= 0) themeCbs.splice(i, 1); }; }, onData: function (cb) { dataCbs.push(cb); if (dataLoaded) { try { cb(data); } catch (_) {} } diff --git a/apps/x/apps/renderer/src/mini-apps/types.ts b/apps/x/apps/renderer/src/mini-apps/types.ts index f02aecaf..49e335c9 100644 --- a/apps/x/apps/renderer/src/mini-apps/types.ts +++ b/apps/x/apps/renderer/src/mini-apps/types.ts @@ -71,6 +71,8 @@ export type MiniAppInboundMessage = | { type: 'rowboat:mini-app:data'; data: unknown } /** Current per-app state; sent on ready and after setState. */ | { type: 'rowboat:mini-app:state'; state: unknown } + /** Host theme; sent on ready and whenever the app theme changes. */ + | { type: 'rowboat:mini-app:theme'; theme: 'light' | 'dark' } /** Result of a previously requested rpc call, correlated by id. */ | { type: 'rowboat:mini-app:rpc-result'; id: string; ok: boolean; result?: unknown; error?: string } @@ -80,5 +82,6 @@ export const MINI_APP_MESSAGE = { setState: 'rowboat:mini-app:setState', data: 'rowboat:mini-app:data', state: 'rowboat:mini-app:state', + theme: 'rowboat:mini-app:theme', rpcResult: 'rowboat:mini-app:rpc-result', } as const diff --git a/apps/x/packages/core/src/application/assistant/skills/build-mini-app/skill.ts b/apps/x/packages/core/src/application/assistant/skills/build-mini-app/skill.ts index 0f5fbd59..6f9b2706 100644 --- a/apps/x/packages/core/src/application/assistant/skills/build-mini-app/skill.ts +++ b/apps/x/packages/core/src/application/assistant/skills/build-mini-app/skill.ts @@ -81,6 +81,13 @@ bridge: include the shim and code against \`window.rowboat\`: Keep it dependency-free (no remote CDNs unless truly needed; the app:// origin allows them but offline-safe is better). Render loading / empty / error states. +**Support light AND dark.** The bridge applies the host theme to \`\` — it +sets the class \`dark\` or \`light\` (and \`color-scheme\`) and updates live when the +user switches. Write CSS for BOTH: style defaults for light, then override under +\`html.dark { … }\` (or use CSS variables keyed on the theme). Never hard-code a +dark-only palette. \`rowboat.getTheme()\`/\`onTheme(cb)\` are also available if you +need JS. Do not build dark-only. + **Who writes this HTML:** - If a **coding agent is active** (user toggled the Code chip), delegate authoring via the \`code-with-agents\` skill — run it with \`cwd\` = the app's