Skip to content

Commit c11bf8f

Browse files
committed
Add support for mini apps
1 parent 2daeabe commit c11bf8f

21 files changed

+3031
-13
lines changed

front_end/panels/ai_chat/BUILD.gn

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,19 @@ devtools_module("ai_chat") {
146146
"tools/SearchCustomAgentsTool.ts",
147147
"tools/CallCustomAgentTool.ts",
148148
"tools/VisualIndicatorTool.ts",
149+
"tools/mini_app/ListMiniAppsTool.ts",
150+
"tools/mini_app/LaunchMiniAppTool.ts",
151+
"tools/mini_app/GetMiniAppStateTool.ts",
152+
"tools/mini_app/UpdateMiniAppStateTool.ts",
153+
"tools/mini_app/ExecuteMiniAppActionTool.ts",
154+
"tools/mini_app/CloseMiniAppTool.ts",
155+
"mini_apps/types/MiniAppTypes.ts",
156+
"mini_apps/MiniAppRegistry.ts",
157+
"mini_apps/GenericMiniAppBridge.ts",
158+
"mini_apps/MiniAppStorageManager.ts",
159+
"mini_apps/MiniAppEventBus.ts",
160+
"mini_apps/MiniAppInitialization.ts",
161+
"mini_apps/apps/agent_studio/AgentStudioMiniApp.ts",
149162
"agent_framework/ConfigurableAgentTool.ts",
150163
"agent_framework/AgentRunner.ts",
151164
"agent_framework/AgentRunnerEventBus.ts",
@@ -362,6 +375,19 @@ _ai_chat_sources = [
362375
"tools/SearchCustomAgentsTool.ts",
363376
"tools/CallCustomAgentTool.ts",
364377
"tools/VisualIndicatorTool.ts",
378+
"tools/mini_app/ListMiniAppsTool.ts",
379+
"tools/mini_app/LaunchMiniAppTool.ts",
380+
"tools/mini_app/GetMiniAppStateTool.ts",
381+
"tools/mini_app/UpdateMiniAppStateTool.ts",
382+
"tools/mini_app/ExecuteMiniAppActionTool.ts",
383+
"tools/mini_app/CloseMiniAppTool.ts",
384+
"mini_apps/types/MiniAppTypes.ts",
385+
"mini_apps/MiniAppRegistry.ts",
386+
"mini_apps/GenericMiniAppBridge.ts",
387+
"mini_apps/MiniAppStorageManager.ts",
388+
"mini_apps/MiniAppEventBus.ts",
389+
"mini_apps/MiniAppInitialization.ts",
390+
"mini_apps/apps/agent_studio/AgentStudioMiniApp.ts",
365391
"agent_framework/ConfigurableAgentTool.ts",
366392
"agent_framework/AgentRunner.ts",
367393
"agent_framework/AgentRunnerEventBus.ts",

front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { ReadabilityExtractorTool } from '../../tools/ReadabilityExtractorTool.j
1616
import { ConfigurableAgentTool, ToolRegistry } from '../ConfigurableAgentTool.js';
1717
import { ThinkingTool } from '../../tools/ThinkingTool.js';
1818
import { SaveResearchReportTool } from '../../tools/SaveResearchReportTool.js';
19+
import { SearchCustomAgentsTool } from '../../tools/SearchCustomAgentsTool.js';
20+
import { CallCustomAgentTool } from '../../tools/CallCustomAgentTool.js';
1921
import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js';
2022
import { createDirectURLNavigatorAgentConfig } from './agents/DirectURLNavigatorAgent.js';
2123
import { createResearchAgentConfig } from './agents/ResearchAgent.js';
@@ -31,13 +33,17 @@ import { createWebTaskAgentConfig } from './agents/WebTaskAgent.js';
3133
import { createEcommerceProductInfoAgentConfig } from './agents/EcommerceProductInfoAgent.js';
3234
import { createSearchAgentConfig } from './agents/SearchAgent.js';
3335
import { AgentStudioIntegration } from '../../core/AgentStudioIntegration.js';
36+
import { initializeMiniApps } from '../../mini_apps/MiniAppInitialization.js';
3437

3538
/**
3639
* Initialize all configured agents
3740
*/
3841
export async function initializeConfiguredAgents(): Promise<void> {
3942
// Ensure MCP meta-tools are available regardless of mode; selection logic decides if they are surfaced
4043
registerMCPMetaTools();
44+
45+
// Initialize mini app system (registers mini apps and mini app tools)
46+
initializeMiniApps();
4147
// Register core tools
4248
ToolRegistry.registerToolFactory('navigate_url', () => new NavigateURLTool());
4349
ToolRegistry.registerToolFactory('navigate_back', () => new NavigateBackTool());
@@ -74,7 +80,11 @@ export async function initializeConfiguredAgents(): Promise<void> {
7480

7581
// Register research report tool
7682
ToolRegistry.registerToolFactory('save_research_report', () => new SaveResearchReportTool());
77-
83+
84+
// Register custom agent tools (for calling agents created in Agent Studio)
85+
ToolRegistry.registerToolFactory('search_custom_agents', () => new SearchCustomAgentsTool());
86+
ToolRegistry.registerToolFactory('call_custom_agent', () => new CallCustomAgentTool());
87+
7888
// Create and register Direct URL Navigator Agent
7989
const directURLNavigatorAgentConfig = createDirectURLNavigatorAgentConfig();
8090
const directURLNavigatorAgent = new ConfigurableAgentTool(directURLNavigatorAgentConfig);

front_end/panels/ai_chat/core/BaseOrchestratorAgent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,8 @@ export function getAgentTools(agentType: string): Array<Tool<any, any>> {
544544
new DeleteFileTool(),
545545
new ReadFileTool(),
546546
new ListFilesTool(),
547+
new SearchCustomAgentsTool(),
548+
new CallCustomAgentTool(),
547549
];
548550
}
549551

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as SDK from '../../../core/sdk/sdk.js';
6+
import type * as Protocol from '../../../generated/protocol.js';
7+
import { createLogger } from '../core/Logger.js';
8+
import type {
9+
MiniAppBridge,
10+
MiniAppState,
11+
SPAToDevToolsAction,
12+
DevToolsToSPAAction,
13+
} from './types/MiniAppTypes.js';
14+
15+
const logger = createLogger('GenericMiniAppBridge');
16+
17+
/**
18+
* Callback type for handling SPA actions
19+
*/
20+
export type ActionHandler = (action: SPAToDevToolsAction) => void | Promise<void>;
21+
22+
/**
23+
* GenericMiniAppBridge - CDP-based bidirectional communication for mini apps
24+
*
25+
* Uses unique binding names per app type to avoid conflicts:
26+
* - Binding: __miniAppBridge_{appId}
27+
*
28+
* Communication:
29+
* - SPA → DevTools: Runtime.addBinding (instant, event-driven)
30+
* - DevTools → SPA: Runtime.evaluate calling window.miniApp.dispatch()
31+
*/
32+
export class GenericMiniAppBridge implements MiniAppBridge {
33+
private readonly appId: string;
34+
private readonly bindingName: string;
35+
36+
private target: SDK.Target.Target | null = null;
37+
private _webappId: string | null = null;
38+
private bindingHandler: ((event: { data: Protocol.Runtime.BindingCalledEvent }) => void) | null = null;
39+
private actionHandler: ActionHandler | null = null;
40+
private _installed = false;
41+
42+
constructor(appId: string) {
43+
this.appId = appId;
44+
this.bindingName = `__miniAppBridge_${appId}`;
45+
}
46+
47+
/**
48+
* Register a handler for actions from the SPA
49+
*/
50+
onAction(handler: ActionHandler): void {
51+
this.actionHandler = handler;
52+
}
53+
54+
/**
55+
* Install the bridge - sets up Runtime.addBinding for SPA→DevTools communication
56+
*/
57+
async install(webappId: string): Promise<void> {
58+
if (this._installed) {
59+
logger.warn(`Bridge for "${this.appId}" already installed`);
60+
return;
61+
}
62+
63+
this._webappId = webappId;
64+
this.target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
65+
66+
if (!this.target) {
67+
throw new Error('No primary page target available');
68+
}
69+
70+
const runtimeModel = this.target.model(SDK.RuntimeModel.RuntimeModel);
71+
if (!runtimeModel) {
72+
throw new Error('RuntimeModel not available');
73+
}
74+
75+
// Create handler for binding calls
76+
this.bindingHandler = this.handleBindingCalled.bind(this);
77+
runtimeModel.addEventListener(SDK.RuntimeModel.Events.BindingCalled, this.bindingHandler);
78+
79+
// Add the binding - this creates window.__miniAppBridge_{appId}() in the page
80+
await this.target.runtimeAgent().invoke_addBinding({
81+
name: this.bindingName,
82+
});
83+
84+
this._installed = true;
85+
logger.info(`Bridge installed for "${this.appId}"`, { webappId, bindingName: this.bindingName });
86+
}
87+
88+
/**
89+
* Uninstall the bridge - removes binding and event listeners
90+
*/
91+
async uninstall(): Promise<void> {
92+
if (!this._installed || !this.target) {
93+
return;
94+
}
95+
96+
const runtimeModel = this.target.model(SDK.RuntimeModel.RuntimeModel);
97+
98+
// Remove event listener
99+
if (runtimeModel && this.bindingHandler) {
100+
runtimeModel.removeEventListener(SDK.RuntimeModel.Events.BindingCalled, this.bindingHandler);
101+
}
102+
103+
// Remove the binding
104+
try {
105+
await this.target.runtimeAgent().invoke_removeBinding({
106+
name: this.bindingName,
107+
});
108+
} catch (error) {
109+
logger.error(`Failed to remove binding for "${this.appId}":`, error);
110+
}
111+
112+
this.bindingHandler = null;
113+
this.target = null;
114+
this._webappId = null;
115+
this._installed = false;
116+
117+
logger.info(`Bridge uninstalled for "${this.appId}"`);
118+
}
119+
120+
/**
121+
* Send an action to the SPA
122+
*/
123+
async sendToSPA(action: DevToolsToSPAAction): Promise<void> {
124+
if (!this.target || !this._webappId) {
125+
logger.error(`Bridge for "${this.appId}" not installed, cannot send to SPA`);
126+
return;
127+
}
128+
129+
try {
130+
const runtimeAgent = this.target.runtimeAgent();
131+
132+
// Call window.miniApp.dispatch() in the iframe context
133+
await runtimeAgent.invoke_evaluate({
134+
expression: `
135+
(() => {
136+
const iframe = document.getElementById(${JSON.stringify(this._webappId)});
137+
if (!iframe || !iframe.contentWindow) {
138+
console.error('[MiniAppBridge] Iframe not found: ${this._webappId}');
139+
return false;
140+
}
141+
if (typeof iframe.contentWindow.miniApp?.dispatch === 'function') {
142+
iframe.contentWindow.miniApp.dispatch(${JSON.stringify(action)});
143+
return true;
144+
}
145+
console.error('[MiniAppBridge] miniApp.dispatch not found');
146+
return false;
147+
})()
148+
`,
149+
returnByValue: true,
150+
});
151+
} catch (error) {
152+
logger.error(`Failed to send to SPA for "${this.appId}":`, error);
153+
}
154+
}
155+
156+
/**
157+
* Get the current state from the SPA
158+
*/
159+
async getState(): Promise<MiniAppState> {
160+
if (!this.target || !this._webappId) {
161+
logger.error(`Bridge for "${this.appId}" not installed, cannot get state`);
162+
return {};
163+
}
164+
165+
try {
166+
const runtimeAgent = this.target.runtimeAgent();
167+
168+
const result = await runtimeAgent.invoke_evaluate({
169+
expression: `
170+
(() => {
171+
const iframe = document.getElementById(${JSON.stringify(this._webappId)});
172+
if (!iframe || !iframe.contentWindow) {
173+
console.error('[MiniAppBridge] Iframe not found: ${this._webappId}');
174+
return null;
175+
}
176+
if (typeof iframe.contentWindow.miniApp?.getState === 'function') {
177+
return iframe.contentWindow.miniApp.getState();
178+
}
179+
console.error('[MiniAppBridge] miniApp.getState not found');
180+
return null;
181+
})()
182+
`,
183+
returnByValue: true,
184+
});
185+
186+
if (result.exceptionDetails) {
187+
logger.error(`Exception getting state for "${this.appId}":`, result.exceptionDetails.text);
188+
return {};
189+
}
190+
191+
return (result.result.value as MiniAppState) || {};
192+
} catch (error) {
193+
logger.error(`Failed to get state for "${this.appId}":`, error);
194+
return {};
195+
}
196+
}
197+
198+
/**
199+
* Handle binding calls from the SPA
200+
*/
201+
private handleBindingCalled(event: { data: Protocol.Runtime.BindingCalledEvent }): void {
202+
const { data } = event;
203+
204+
// Only handle our binding
205+
if (data.name !== this.bindingName) {
206+
return;
207+
}
208+
209+
try {
210+
const action = JSON.parse(data.payload) as SPAToDevToolsAction;
211+
logger.info(`Received action from SPA "${this.appId}":`, action.type);
212+
213+
if (this.actionHandler) {
214+
// Handle async actions
215+
const result = this.actionHandler(action);
216+
if (result instanceof Promise) {
217+
result.catch(error => {
218+
logger.error(`Error handling action for "${this.appId}":`, error);
219+
});
220+
}
221+
}
222+
} catch (error) {
223+
logger.error(`Failed to parse SPA action for "${this.appId}":`, error);
224+
}
225+
}
226+
227+
/**
228+
* Check if bridge is installed
229+
*/
230+
get installed(): boolean {
231+
return this._installed;
232+
}
233+
234+
/**
235+
* Get the webapp ID
236+
*/
237+
get webappId(): string | null {
238+
return this._webappId;
239+
}
240+
}

0 commit comments

Comments
 (0)