diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index e55bfb03..9a1792b6 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -2001,6 +2001,9 @@ function App() { }>>([]) const [bgTaskInitialSlug, setBgTaskInitialSlug] = useState(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(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() { ) : isAppsOpen ? (
- +
) : isEmailOpen ? (
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 d5ba3b93..b5b35726 100644 --- a/apps/x/apps/renderer/src/components/mini-apps-view.tsx +++ b/apps/x/apps/renderer/src/components/mini-apps-view.tsx @@ -153,10 +153,15 @@ function Card({ app, index, onOpen }: { app: miniApp.MiniAppManifest; index: num ) } -export function MiniAppsView() { - const [selectedId, setSelectedId] = useState(null) +export function MiniAppsView({ initialAppId, initialVersion }: { initialAppId?: string | null; initialVersion?: number } = {}) { + const [selectedId, setSelectedId] = useState(initialAppId ?? null) const [manifests, setManifests] = useState([]) + // 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(() => { diff --git a/apps/x/apps/renderer/src/lib/chat-conversation.ts b/apps/x/apps/renderer/src/lib/chat-conversation.ts index 5fb28574..59365430 100644 --- a/apps/x/apps/renderer/src/lib/chat-conversation.ts +++ b/apps/x/apps/renderer/src/lib/chat-conversation.ts @@ -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', 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 3c045cad..25347249 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 @@ -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: "" }\`. This opens the app in the middle pane +(under Mini Apps / its title) and shows an "Opened " 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) diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index 59b1d5c9..69f2c157 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -1068,9 +1068,11 @@ export const BuiltinTools: z.infer = { '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 = { 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 = {}; if (input.filters) updates.filters = input.filters;