feat(apps): copilot answers from installed apps and surfaces them

Generic app-copilot bridge (no app-specific wiring anywhere):

- app-read-data builtin: read (or list) an installed app's data/ files —
  the agent-maintained JSON the app renders. Same path confinement as
  app-set-data; 50k char cap with truncation flag; parsed JSON returned
  when possible. Attached via the app-navigation skill.
- read-view view=apps: installed apps with name/description/dataFiles/
  agentSlugs, navigating the user's screen to the Apps view (same
  show-while-telling contract as email/bg-tasks). Renderer routes the
  new view value.
- copilot instructions embed the installed-apps list (name +
  description per app) with routing guidance: prefer an app's fresh
  data over external calls, answer from it, open-app to surface it.
  The apps:list handler fingerprints the app set and invalidates the
  instructions cache when it changes (installs, deletes, copilot-
  created folders all flow through that poll).
- app-navigation skill: apps section + a worked example framing the
  pattern generically (match apps by their own descriptions, never a
  hardcoded topic map).

Verified against real data: app-read-data returns pr-dashboard's live
10-PR data.json, escape paths rejected, key-order golden test updated
and passing.
This commit is contained in:
Gagan 2026-07-10 14:23:03 +05:30
parent 214cae7190
commit 7d38dc84a2
7 changed files with 172 additions and 9 deletions

View file

@ -79,6 +79,9 @@ import * as appsPublisher from '@x/core/dist/apps/publisher.js';
// D18 install previews awaiting confirmation, keyed by app name.
const appInstallPreviews = new Map<string, Awaited<ReturnType<typeof appsInstaller.previewInstall>>>();
// Last-seen app-set fingerprint; a change invalidates the copilot
// instructions cache (they embed the installed-apps list).
let lastAppsFingerprint: string | null = null;
import { consumePendingDeepLink } from './deeplink.js';
import { qualifyAndDisconnectComposioGoogle } from '@x/core/dist/migrations/composio-google-migration.js';
import { IAgentScheduleRepo } from '@x/core/dist/agent-schedule/repo.js';
@ -1618,6 +1621,15 @@ export function setupIpcHandlers() {
for (const app of apps) {
if (app.agentSlugs.length) await appsAgents.syncAppAgents(app);
}
// The copilot instructions embed the installed-apps list. This handler
// is the one place that sees every change to the app set (installs,
// deletes, copilot-created folders — the renderer polls it), so refresh
// the instructions cache when the set actually changes.
const fingerprint = JSON.stringify(apps.map((a) => [a.folder, a.manifest?.name, a.manifest?.description, a.hasDist]));
if (fingerprint !== lastAppsFingerprint) {
lastAppsFingerprint = fingerprint;
invalidateCopilotInstructionsCache();
}
return {
serverRunning: status.running,
...(status.error ? { serverError: status.error } : {}),

View file

@ -4773,6 +4773,7 @@ function App() {
case 'knowledge': void navigateToView({ type: 'knowledge-view' }); break
case 'workspace': void navigateToView({ type: 'workspace' }); break
case 'code': void navigateToView({ type: 'code' }); break
case 'apps': openAppsView(); break
}
}