feat(mini-apps): open built app in middle pane from chat

After the copilot builds + verifies + populates an app, it opens it in the
middle pane under Mini Apps / <title>, with an 'Opened <app>' card in chat.

- app-navigation: new open-app action (reads manifest title)
- renderer: pending-nav opens the Mini Apps view with the app pre-selected;
  MiniAppsView accepts initialAppId/initialVersion
- chat: open-app app-action card labels
- build-mini-app skill: finalize step opens the app once data is populated
This commit is contained in:
Gagan 2026-07-01 02:12:05 +05:30
parent 4a4dcb1fa0
commit 6d7fd7b013
5 changed files with 46 additions and 8 deletions

View file

@ -2001,6 +2001,9 @@ function App() {
}>>([])
const [bgTaskInitialSlug, setBgTaskInitialSlug] = useState<string | null>(null)
const [bgTaskSlugVersion, setBgTaskSlugVersion] = useState(0)
// Mini App to auto-open in the Mini Apps view (set by app-navigation open-app).
const [appInitialId, setAppInitialId] = useState<string | null>(null)
const [appIdVersion, setAppIdVersion] = useState(0)
const loadBgTaskSummaries = useCallback(async () => {
try {
@ -4506,6 +4509,13 @@ function App() {
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
}
break
case 'open-app':
if (result.appId) {
setAppInitialId(result.appId as string)
setAppIdVersion((v) => v + 1)
openAppsView()
}
break
case 'update-base-view': {
// Navigate to bases if not already there
const targetPath = selectedPath && isBaseFilePath(selectedPath) ? selectedPath : BASES_DEFAULT_TAB_PATH
@ -5983,7 +5993,7 @@ function App() {
</div>
) : isAppsOpen ? (
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
<MiniAppsView />
<MiniAppsView initialAppId={appInitialId} initialVersion={appIdVersion} />
</div>
) : isEmailOpen ? (
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">

View file

@ -153,10 +153,15 @@ function Card({ app, index, onOpen }: { app: miniApp.MiniAppManifest; index: num
)
}
export function MiniAppsView() {
const [selectedId, setSelectedId] = useState<string | null>(null)
export function MiniAppsView({ initialAppId, initialVersion }: { initialAppId?: string | null; initialVersion?: number } = {}) {
const [selectedId, setSelectedId] = useState<string | null>(initialAppId ?? null)
const [manifests, setManifests] = useState<miniApp.MiniAppManifest[]>([])
// Open a specific app when asked from outside (app-navigation open-app).
useEffect(() => {
if (initialAppId) setSelectedId(initialAppId)
}, [initialAppId, initialVersion])
// List apps installed under ~/.rowboat/apps. Apps are created there by the
// copilot builder (or placed manually); none are bundled in the repo.
useEffect(() => {

View file

@ -210,6 +210,7 @@ export const getAppActionCardData = (tool: ToolCall): AppActionCardData | null =
switch (action) {
case 'open-note': return { action, label: `Opening ${(input.path as string || '').split('/').pop()?.replace(/\.md$/, '') || 'note'}...` }
case 'open-view': return { action, label: `Opening ${input.view} view...` }
case 'open-app': return { action, label: `Opening ${input.appId || 'app'}...` }
case 'update-base-view': return { action, label: 'Updating view...' }
case 'create-base': return { action, label: `Creating "${input.name}"...` }
case 'get-base-state': return null // renders as normal tool block
@ -225,6 +226,8 @@ export const getAppActionCardData = (tool: ToolCall): AppActionCardData | null =
}
case 'open-view':
return { action: 'open-view', label: `Opened ${result.view} view` }
case 'open-app':
return { action: 'open-app', label: `Opened ${result.appName || result.appId || 'app'}` }
case 'update-base-view':
return {
action: 'update-base-view',

View file

@ -95,10 +95,14 @@ manifest's \`agent\` field to the task's slug. Give the task a sensible trigger
## 5. Finalize
Call \`mini-app-install\` with the manifest + html (+ optional seed data). Then
confirm to the user, and ideally open it so they see it populate. For agent-backed
apps, trigger the agent once (\`run-background-task-agent\`) so \`data.json\` exists
immediately instead of waiting for the first scheduled run.
Call \`mini-app-install\` with the manifest + html (+ optional seed data). For
agent-backed apps, trigger the agent once (\`run-background-task-agent\`) so
\`data.json\` exists immediately instead of waiting for the first scheduled run.
Then **open it for the user**: call \`app-navigation\` with
\`{ action: "open-app", appId: "<id>" }\`. This opens the app in the middle pane
(under Mini Apps / its title) and shows an "Opened <app>" card in the chat. Only
do this once the app is installed AND its data is populated, so it renders ready.
## Manifest schema (manifest.json)

View file

@ -1068,9 +1068,11 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
'app-navigation': {
description: 'Control the app UI - navigate to notes, switch views, filter/search the knowledge base, and manage saved views.',
inputSchema: z.object({
action: z.enum(["open-note", "open-view", "update-base-view", "get-base-state", "create-base"]).describe("The navigation action to perform"),
action: z.enum(["open-note", "open-view", "open-app", "update-base-view", "get-base-state", "create-base"]).describe("The navigation action to perform"),
// open-note
path: z.string().optional().describe("Knowledge file path for open-note, e.g. knowledge/People/John.md"),
// open-app
appId: z.string().optional().describe("Mini App id (folder name under ~/.rowboat/apps) for open-app — opens it in the middle pane under Mini Apps."),
// open-view
view: z.enum(["bases", "graph"]).optional().describe("Which view to open (for open-view action)"),
// update-base-view
@ -1118,6 +1120,20 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
return { success: true, action: 'open-view', view };
}
case 'open-app': {
const appId = input.appId as string;
if (!appId) return { success: false, error: 'open-app requires appId' };
let appName = appId;
try {
const raw = await fs.readFile(path.join(WorkDir, 'apps', appId, 'manifest.json'), 'utf-8');
const m = JSON.parse(raw) as { title?: string };
if (m.title) appName = m.title;
} catch {
return { success: false, error: `Mini App not found: ${appId}` };
}
return { success: true, action: 'open-app', appId, appName };
}
case 'update-base-view': {
const updates: Record<string, unknown> = {};
if (input.filters) updates.filters = input.filters;