diff --git a/pkg/miniapp/frontend/build.mjs b/pkg/miniapp/frontend/build.mjs index 943851551..3093decda 100644 --- a/pkg/miniapp/frontend/build.mjs +++ b/pkg/miniapp/frontend/build.mjs @@ -10,12 +10,15 @@ await rm(outDir, { recursive: true, force: true }); await mkdir(outDir, { recursive: true }); const result = await Bun.build({ - entrypoints: [path.join(srcDir, 'app.js')], + entrypoints: [path.join(srcDir, 'index.tsx')], outdir: outDir, target: 'browser', format: 'iife', sourcemap: 'none', packages: 'bundle', + naming: { + entry: 'app.[ext]', + }, }); if (!result.success) { diff --git a/pkg/miniapp/frontend/bun.lock b/pkg/miniapp/frontend/bun.lock index b8130a079..cc3c31024 100644 --- a/pkg/miniapp/frontend/bun.lock +++ b/pkg/miniapp/frontend/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "highlight.js": "^11.11.1", "marked": "^17.0.4", + "preact": "^10.25.0", }, "devDependencies": { "happy-dom": "^16.8.1", @@ -173,6 +174,8 @@ "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "3.3.11", "picocolors": "1.1.1", "source-map-js": "1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "preact": ["preact@10.29.0", "", {}, "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg=="], + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "2.3.3" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], diff --git a/pkg/miniapp/frontend/package.json b/pkg/miniapp/frontend/package.json index 4a6497882..e56d3af10 100644 --- a/pkg/miniapp/frontend/package.json +++ b/pkg/miniapp/frontend/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "highlight.js": "^11.11.1", - "marked": "^17.0.4" + "marked": "^17.0.4", + "preact": "^10.25.0" } } diff --git a/pkg/miniapp/frontend/src/app.tsx b/pkg/miniapp/frontend/src/app.tsx new file mode 100644 index 000000000..0837a7e21 --- /dev/null +++ b/pkg/miniapp/frontend/src/app.tsx @@ -0,0 +1,60 @@ +import { useEffect, useState, useCallback, useRef } from 'preact/hooks'; +import { useSSE } from './hooks/use-sse'; +import { PlanTab } from './components/plan/plan-tab'; +import { WorkTab } from './components/work/work-tab'; +import { ToolsTab } from './components/tools/tools-tab'; +import { DevTab } from './components/dev/dev-tab'; + +const TABS = [ + { id: 'plan', label: 'Plan' }, + { id: 'work', label: 'Work' }, + { id: 'tools', label: 'Tools' }, + { id: 'dev', label: 'Dev' }, +] as const; + +type TabId = (typeof TABS)[number]['id']; + +export function App() { + const [activeTab, setActiveTab] = useState('plan'); + const indicatorRef = useRef(null); + const sse = useSSE(); + + const switchTab = useCallback((id: TabId, index: number) => { + setActiveTab(id); + if (indicatorRef.current) { + indicatorRef.current.style.transform = `translateX(${index * 100}%)`; + } + }, []); + + return ( + <> +
+
+
+ {TABS.map((tab, i) => ( + + ))} +
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+ + ); +} diff --git a/pkg/miniapp/frontend/src/components/dev/dev-tab.tsx b/pkg/miniapp/frontend/src/components/dev/dev-tab.tsx new file mode 100644 index 000000000..45da307f5 --- /dev/null +++ b/pkg/miniapp/frontend/src/components/dev/dev-tab.tsx @@ -0,0 +1,121 @@ +import { useEffect, useState, useCallback } from 'preact/hooks'; +import type { SSEHook } from '../../hooks/use-sse'; +import { apiFetch, apiPost } from '../../hooks/use-api'; +import { isFresh } from '../../utils'; + +interface DevTabProps { + active: boolean; + sse: SSEHook; +} + +export function DevTab({ active, sse }: DevTabProps) { + const [data, setData] = useState(null); + + const loadDev = useCallback(async () => { + try { + const d = await apiFetch('/miniapp/api/dev'); + setData(d); + } catch {} + }, []); + + useEffect(() => { + if (sse.dev) setData(sse.dev); + }, [sse.dev]); + + useEffect(() => { + if (active && !isFresh(sse.lastUpdate, 'dev')) loadDev(); + }, [active]); + + const targets = data?.targets || []; + const activeId = data?.active_id || ''; + const isActive = !!data?.active; + + const handleToggle = async (id: string) => { + const action = id === activeId ? 'deactivate' : 'activate'; + const body = + action === 'activate' + ? { action: 'activate', id } + : { action: 'deactivate' }; + try { + const d = await apiPost('/miniapp/api/dev', body); + if (!d.error) setData(d); + } catch {} + }; + + const handleDelete = async (id: string, name: string) => { + if (!confirm('Remove "' + name + '"?')) return; + try { + const d = await apiPost('/miniapp/api/dev', { + action: 'unregister', + id, + }); + if (!d.error) setData(d); + } catch {} + }; + + const iframeSrc = isActive ? location.origin + '/miniapp/dev/' : ''; + const targetDisplay = data?.target + ? data.target.replace(/^https?:\/\//, '') + : ''; + + return ( + <> +
+ + Dev Preview + {targetDisplay} +
+ + {targets.length === 0 ? ( +
+ No targets registered. +
+ Ask the agent to start a dev server. +
+ ) : ( + targets.map((t: any) => { + const isTargetActive = t.id === activeId; + const displayUrl = t.target.replace(/^https?:\/\//, ''); + return ( +
handleToggle(t.id)} + > + + {t.name} + {displayUrl} + { + e.stopPropagation(); + handleDelete(t.id, t.name); + }} + > + {'\u00D7'} + +
+ ); + }) + )} + + {isActive && ( +
+
+ -
-
-
- -
-
-
-
-
👑
-
CNDR
-
-
-
-
👩‍💼
-
SEC
-
-
-
-
🕊️
-
HB
-
-
-
-
- -
-
-
-
🔍
-
SCOUT
-
-
-
-
📊
-
ANLY
-
-
-
-
💻
-
CODE
-
-
-
-
🔧
-
WRKR
-
-
-
-
🎯
-
CORD
-
-
-
-
-
- - Connecting… -
-
- - +
- -