@robota-sdk/agent-ui-web 3.0.0-beta.95 → 3.0.0-beta.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -311,8 +311,8 @@ const SERVER_MESSAGE_HANDLING = {
311
311
  project_status: "reducer-state",
312
312
  project_diff: "reducer-state",
313
313
  project_memory: "reducer-state",
314
- app_view: "intentionally-not-rendered",
315
- app_tool_result: "intentionally-not-rendered",
314
+ app_view: "reducer-state",
315
+ app_tool_result: "reducer-state",
316
316
  background_task_event: "intentionally-not-rendered",
317
317
  background_job_group_event: "intentionally-not-rendered",
318
318
  plan_event: "intentionally-not-rendered",
@@ -337,10 +337,10 @@ const SERVER_MESSAGE_HANDLING = {
337
337
  };
338
338
  //#endregion
339
339
  //#region src/hooks/use-agent-switcher-state.ts
340
- let requestCounter$6 = 0;
341
- function nextRequestId$6(prefix) {
342
- requestCounter$6 += 1;
343
- return `${prefix}_${requestCounter$6}_${Date.now()}`;
340
+ let requestCounter$7 = 0;
341
+ function nextRequestId$7(prefix) {
342
+ requestCounter$7 += 1;
343
+ return `${prefix}_${requestCounter$7}_${Date.now()}`;
344
344
  }
345
345
  /**
346
346
  * #3282 §4 part b-3: the agent switcher sheet's own correlated request/response state, the same
@@ -375,7 +375,7 @@ function useAgentSwitcherState(send) {
375
375
  return true;
376
376
  }, []);
377
377
  const requestAgentDefinitions = (0, react.useCallback)(() => {
378
- const requestId = nextRequestId$6("get_agent_definitions");
378
+ const requestId = nextRequestId$7("get_agent_definitions");
379
379
  listRequestRef.current = requestId;
380
380
  send({
381
381
  type: "get-agent-definitions",
@@ -398,7 +398,7 @@ function useAgentSwitcherState(send) {
398
398
  setAgentSwitcherOpen(false);
399
399
  }, []),
400
400
  selectAgent: (0, react.useCallback)((agentType) => {
401
- const requestId = nextRequestId$6("agent_switch");
401
+ const requestId = nextRequestId$7("agent_switch");
402
402
  switchRequestRef.current = requestId;
403
403
  setAgentSwitchMessage(null);
404
404
  send({
@@ -419,6 +419,126 @@ function useAgentSwitcherState(send) {
419
419
  };
420
420
  }
421
421
  //#endregion
422
+ //#region src/hooks/use-app-view-requests.ts
423
+ let requestCounter$6 = 0;
424
+ function nextRequestId$6(prefix) {
425
+ requestCounter$6 += 1;
426
+ return `${prefix}_${requestCounter$6}_${Date.now()}`;
427
+ }
428
+ const CONNECTION_CLOSED = "The connection to the session closed.";
429
+ /**
430
+ * The key a finished call's app view is read and called by, taken from the tool row's `appView`
431
+ * reference; `undefined` when the row gives none. Everything outside this module treats it as an
432
+ * opaque string — it is read here and put on the two wire messages below, nowhere else.
433
+ */
434
+ function appViewKey(tool) {
435
+ return tool.appView?.viewId;
436
+ }
437
+ function readAppViewMessage(requestId, viewKey) {
438
+ return {
439
+ type: "read-app-view",
440
+ requestId,
441
+ viewId: viewKey
442
+ };
443
+ }
444
+ function callAppToolMessage(requestId, viewKey, name, args) {
445
+ return {
446
+ type: "call-app-tool",
447
+ requestId,
448
+ viewId: viewKey,
449
+ name,
450
+ arguments: args
451
+ };
452
+ }
453
+ /**
454
+ * The app-view host's two requests to the session, each a promise settled by the reply that echoes
455
+ * its `requestId` (`app_view` / `app_tool_result`, or a `protocol_error` naming it). Unlike the
456
+ * panels' latest-wins state, several can be outstanding at once (one per frame on screen), so each
457
+ * keeps its own entry. A reply that lost its connection never arrives: the owner of the connection
458
+ * rejects every outstanding request when it drops, is replaced or goes away (`rejectPendingAppRequests`).
459
+ */
460
+ function useAppViewRequests(send, isConnected) {
461
+ const pendingRef = (0, react.useRef)(/* @__PURE__ */ new Map());
462
+ const [requestOwner] = (0, react.useState)(() => nextRequestId$6("app_request"));
463
+ const rejectPendingAppRequests = (0, react.useCallback)((reason = CONNECTION_CLOSED) => {
464
+ const pending = [...pendingRef.current.values()];
465
+ pendingRef.current.clear();
466
+ for (const request of pending) {
467
+ request.dispose();
468
+ request.reject(new Error(reason));
469
+ }
470
+ }, []);
471
+ const request = (0, react.useCallback)((prefix, message, pending, signal) => new Promise((resolve, reject) => {
472
+ if (!isConnected() || signal?.aborted) {
473
+ reject(/* @__PURE__ */ new Error(signal?.aborted ? "The app view closed." : CONNECTION_CLOSED));
474
+ return;
475
+ }
476
+ const requestId = nextRequestId$6(`${requestOwner}_${prefix}`);
477
+ const dispose = () => signal?.removeEventListener("abort", abort);
478
+ const abort = () => {
479
+ pendingRef.current.delete(requestId);
480
+ dispose();
481
+ reject(/* @__PURE__ */ new Error("The app view closed."));
482
+ };
483
+ pendingRef.current.set(requestId, pending(resolve, reject, dispose));
484
+ signal?.addEventListener("abort", abort, { once: true });
485
+ try {
486
+ send(message(requestId));
487
+ } catch (error) {
488
+ pendingRef.current.delete(requestId);
489
+ dispose();
490
+ reject(error instanceof Error ? error : new Error(String(error)));
491
+ }
492
+ }), [
493
+ isConnected,
494
+ requestOwner,
495
+ send
496
+ ]);
497
+ return {
498
+ readAppView: (0, react.useCallback)((viewKey, signal) => request("app_view", (requestId) => readAppViewMessage(requestId, viewKey), (resolve, reject, dispose) => ({
499
+ answer: "app_view",
500
+ resolve,
501
+ reject,
502
+ dispose
503
+ }), signal), [request]),
504
+ callAppTool: (0, react.useCallback)((viewKey, name, args, signal) => request("app_tool", (requestId) => callAppToolMessage(requestId, viewKey, name, args), (resolve, reject, dispose) => ({
505
+ answer: "app_tool_result",
506
+ resolve,
507
+ reject,
508
+ dispose
509
+ }), signal), [request]),
510
+ handleAppViewMessage: (0, react.useCallback)((msg) => {
511
+ if (msg.type === "app_view" || msg.type === "app_tool_result") {
512
+ const pending = pendingRef.current.get(msg.requestId);
513
+ if (pending === void 0 || pending.answer !== msg.type) return true;
514
+ pendingRef.current.delete(msg.requestId);
515
+ pending.dispose();
516
+ if (msg.type === "app_view" && pending.answer === "app_view") pending.resolve(msg.view);
517
+ else if (msg.type === "app_tool_result" && pending.answer === "app_tool_result") {
518
+ const { success, data, parts, error } = msg;
519
+ pending.resolve({
520
+ success,
521
+ ...data !== void 0 ? { data } : {},
522
+ ...parts !== void 0 ? { parts } : {},
523
+ ...error !== void 0 ? { error } : {}
524
+ });
525
+ }
526
+ return true;
527
+ }
528
+ if (msg.type === "protocol_error" && msg.requestId !== void 0) {
529
+ const pending = pendingRef.current.get(msg.requestId);
530
+ if (pending === void 0) return msg.requestId.startsWith(`${requestOwner}_`);
531
+ pendingRef.current.delete(msg.requestId);
532
+ pending.dispose();
533
+ pending.reject(new Error(msg.message));
534
+ return true;
535
+ }
536
+ return false;
537
+ }, [requestOwner]),
538
+ rejectPendingAppRequests
539
+ };
540
+ }
541
+ //#endregion
422
542
  //#region src/hooks/use-execution-detail.ts
423
543
  /**
424
544
  * #3288 §1: the detail sheet's own correlated request/response state — which entry is open, its
@@ -1103,7 +1223,8 @@ function toolEndFields(state) {
1103
1223
  ...state.diffLines ? { diffLines: state.diffLines } : {},
1104
1224
  ...state.diffFile ? { diffFile: state.diffFile } : {},
1105
1225
  ...state.toolResultData !== void 0 ? { toolResultData: state.toolResultData } : {},
1106
- ...state.toolResultParts ? { toolResultParts: state.toolResultParts } : {}
1226
+ ...state.toolResultParts ? { toolResultParts: state.toolResultParts } : {},
1227
+ ...state.appView ? { appView: state.appView } : {}
1107
1228
  };
1108
1229
  }
1109
1230
  /** Apply a `tool_end` to the ONE matching running tool across a turn's segments (never all of them). */
@@ -1201,6 +1322,7 @@ function buildConversationEntriesFromDisplaySegments(displaySegments) {
1201
1322
  }
1202
1323
  function useSessionClient(makeClient) {
1203
1324
  const [status, setStatus] = (0, react.useState)("disconnected");
1325
+ const statusRef = (0, react.useRef)("disconnected");
1204
1326
  const [messages, setMessages] = (0, react.useState)([]);
1205
1327
  const [activeTools, setActiveTools] = (0, react.useState)([]);
1206
1328
  const [streamingText, setStreamingText] = (0, react.useState)("");
@@ -1325,6 +1447,7 @@ function useSessionClient(makeClient) {
1325
1447
  const { handleExecutionDetailMessage, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
1326
1448
  const { handleModelListMessage, ...modelListState } = useModelListState(send);
1327
1449
  const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
1450
+ const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, (0, react.useCallback)(() => statusRef.current === "connected", []));
1328
1451
  const { handleSessionsMessage, canListSessions, markCurrent, armRestore, ...sessionDirectoryState } = useSessionDirectoryState(send);
1329
1452
  const { requestSessions, setSessionSidebarOpen } = sessionDirectoryState;
1330
1453
  const { handleSettingsMessage, openSettings, refreshSettings, ...settingsState } = useSettingsState(send);
@@ -1338,6 +1461,7 @@ function useSessionClient(makeClient) {
1338
1461
  if (handleExecutionDetailMessage(msg)) return;
1339
1462
  if (handleModelListMessage(msg)) return;
1340
1463
  if (handleProjectMessage(msg)) return;
1464
+ if (handleAppViewMessage(msg)) return;
1341
1465
  if (handleSessionsMessage(msg)) return;
1342
1466
  if (handleSettingsMessage(msg)) return;
1343
1467
  if (handleAgentDefinitionsMessage(msg)) return;
@@ -1474,6 +1598,7 @@ function useSessionClient(makeClient) {
1474
1598
  requestSessions();
1475
1599
  break;
1476
1600
  case "session_switched":
1601
+ rejectPendingAppRequests("The active session changed.");
1477
1602
  settleSessionChange();
1478
1603
  sessionStatusRef.current = null;
1479
1604
  setSessionStatus(null);
@@ -1498,6 +1623,7 @@ function useSessionClient(makeClient) {
1498
1623
  requestSchedules();
1499
1624
  break;
1500
1625
  case "history_cleared":
1626
+ rejectPendingAppRequests("The session history was cleared.");
1501
1627
  streamingTextRef.current = "";
1502
1628
  turnSegmentsRef.current = [];
1503
1629
  setStreamingText("");
@@ -1638,6 +1764,7 @@ function useSessionClient(makeClient) {
1638
1764
  closeExecutionDetail,
1639
1765
  finishTurn,
1640
1766
  handleAgentDefinitionsMessage,
1767
+ handleAppViewMessage,
1641
1768
  handleExecutionDetailMessage,
1642
1769
  handleModelListMessage,
1643
1770
  handleProjectMessage,
@@ -1652,6 +1779,7 @@ function useSessionClient(makeClient) {
1652
1779
  requestSchedules,
1653
1780
  requestSessions,
1654
1781
  resolveSwitchResult,
1782
+ rejectPendingAppRequests,
1655
1783
  send,
1656
1784
  setSessionSidebarOpen,
1657
1785
  settleSessionChange,
@@ -1670,7 +1798,9 @@ function useSessionClient(makeClient) {
1670
1798
  }, []);
1671
1799
  (0, react.useEffect)(() => {
1672
1800
  const onStatusChange = (next) => {
1801
+ statusRef.current = next;
1673
1802
  setStatus(next);
1803
+ if (next !== "connected") rejectPendingAppRequests();
1674
1804
  if (next === "connected") {
1675
1805
  commandsInFlightRef.current = 0;
1676
1806
  silentCommandRequestIdsRef.current.clear();
@@ -1694,13 +1824,15 @@ function useSessionClient(makeClient) {
1694
1824
  return () => {
1695
1825
  client.disconnect();
1696
1826
  clientRef.current = null;
1827
+ rejectPendingAppRequests();
1697
1828
  };
1698
1829
  }, [
1699
1830
  makeClient,
1700
1831
  handleMessage,
1701
1832
  requestSessions,
1702
1833
  requestSchedules,
1703
- armRestore
1834
+ armRestore,
1835
+ rejectPendingAppRequests
1704
1836
  ]);
1705
1837
  return {
1706
1838
  status,
@@ -1732,6 +1864,8 @@ function useSessionClient(makeClient) {
1732
1864
  openAgentSwitcher,
1733
1865
  ...agentSwitcherState,
1734
1866
  ...schedulesState,
1867
+ readAppView,
1868
+ callAppTool,
1735
1869
  helpOpen,
1736
1870
  openHelp,
1737
1871
  closeHelp
@@ -1922,6 +2056,338 @@ function driverAttributionText(driverId) {
1922
2056
  return `from ${humanDriverLabel(driverId)}`;
1923
2057
  }
1924
2058
  //#endregion
2059
+ //#region src/app-view/call-tool-result.ts
2060
+ function isRecord(value) {
2061
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2062
+ }
2063
+ function contentFromParts(parts) {
2064
+ const content = [];
2065
+ for (const part of parts ?? []) if (part.type === "text") content.push({
2066
+ type: "text",
2067
+ text: part.text
2068
+ });
2069
+ else if (part.type === "image_inline") content.push({
2070
+ type: "image",
2071
+ data: part.data,
2072
+ mimeType: part.mimeType
2073
+ });
2074
+ else if (part.type === "audio_inline") content.push({
2075
+ type: "audio",
2076
+ data: part.data,
2077
+ mimeType: part.mimeType
2078
+ });
2079
+ return content;
2080
+ }
2081
+ function outcomeToCallToolResult(outcome) {
2082
+ let content = contentFromParts(outcome.parts);
2083
+ if (content.length === 0 && typeof outcome.data === "string") content = [{
2084
+ type: "text",
2085
+ text: outcome.data
2086
+ }];
2087
+ if (!outcome.success && outcome.error !== void 0) content = [...content, {
2088
+ type: "text",
2089
+ text: outcome.error
2090
+ }];
2091
+ return {
2092
+ content,
2093
+ ...isRecord(outcome.data) ? { structuredContent: outcome.data } : {},
2094
+ isError: !outcome.success
2095
+ };
2096
+ }
2097
+ function toCallToolResult(tool, view) {
2098
+ let content = contentFromParts(tool.toolResultParts);
2099
+ if (content.length === 0 && tool.toolResultData !== void 0) content = [{
2100
+ type: "text",
2101
+ text: tool.toolResultData
2102
+ }];
2103
+ return {
2104
+ content,
2105
+ ...isRecord(view.structuredContent) ? { structuredContent: view.structuredContent } : {},
2106
+ isError: tool.status === "error"
2107
+ };
2108
+ }
2109
+ //#endregion
2110
+ //#region src/app-view/bridge.ts
2111
+ const APP_VIEW_PROTOCOL_VERSION = "2026-01-26";
2112
+ const INVALID_PARAMS = -32602;
2113
+ const METHOD_NOT_FOUND = -32601;
2114
+ const HOST_ERROR = -32e3;
2115
+ const SANDBOX_PROXY_READY = "ui/notifications/sandbox-proxy-ready";
2116
+ const SANDBOX_RESOURCE_READY = "ui/notifications/sandbox-resource-ready";
2117
+ function isPlainObject(value) {
2118
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2119
+ const prototype = Object.getPrototypeOf(value);
2120
+ return prototype === null || Object.getPrototypeOf(prototype) === null;
2121
+ }
2122
+ function errorMessage(error) {
2123
+ return error instanceof Error ? error.message : String(error);
2124
+ }
2125
+ function createAppViewBridge(options) {
2126
+ const { view } = options;
2127
+ let disposed = false;
2128
+ let sandboxAnswered = false;
2129
+ let initialization = "uninitialized";
2130
+ let callInFlight = false;
2131
+ const respond = (id, result) => {
2132
+ options.post({
2133
+ jsonrpc: "2.0",
2134
+ id,
2135
+ result
2136
+ });
2137
+ };
2138
+ const refuse = (id, code, message) => {
2139
+ options.post({
2140
+ jsonrpc: "2.0",
2141
+ id,
2142
+ error: {
2143
+ code,
2144
+ message
2145
+ }
2146
+ });
2147
+ };
2148
+ const callTool = (id, params) => {
2149
+ if (initialization !== "ready") {
2150
+ refuse(id, HOST_ERROR, "This app view has not initialized.");
2151
+ return;
2152
+ }
2153
+ const name = isPlainObject(params) ? params["name"] : void 0;
2154
+ const runtimeName = typeof name === "string" && Object.hasOwn(view.callableTools, name) ? view.callableTools[name] : void 0;
2155
+ if (!isPlainObject(params) || runtimeName === void 0) {
2156
+ refuse(id, INVALID_PARAMS, "This tool is not available to this view.");
2157
+ return;
2158
+ }
2159
+ const rawArguments = params["arguments"] === void 0 ? {} : params["arguments"];
2160
+ if (!isPlainObject(rawArguments)) {
2161
+ refuse(id, INVALID_PARAMS, "Tool arguments must be an object.");
2162
+ return;
2163
+ }
2164
+ if (callInFlight) {
2165
+ refuse(id, HOST_ERROR, "Another tool call from this view is still running.");
2166
+ return;
2167
+ }
2168
+ callInFlight = true;
2169
+ let outcome;
2170
+ try {
2171
+ outcome = options.callAppTool(runtimeName, rawArguments);
2172
+ } catch (error) {
2173
+ outcome = Promise.reject(error);
2174
+ }
2175
+ outcome.then((settled) => {
2176
+ callInFlight = false;
2177
+ if (!disposed) respond(id, outcomeToCallToolResult(settled));
2178
+ }, (error) => {
2179
+ callInFlight = false;
2180
+ if (!disposed) refuse(id, HOST_ERROR, errorMessage(error));
2181
+ });
2182
+ };
2183
+ const handleRequest = (id, method, params) => {
2184
+ switch (method) {
2185
+ case "ui/initialize":
2186
+ if (initialization === "uninitialized") initialization = "initializing";
2187
+ respond(id, {
2188
+ protocolVersion: APP_VIEW_PROTOCOL_VERSION,
2189
+ hostCapabilities: { serverTools: {} },
2190
+ hostInfo: options.hostInfo,
2191
+ hostContext: options.hostContext
2192
+ });
2193
+ return;
2194
+ case "tools/call":
2195
+ callTool(id, params);
2196
+ return;
2197
+ case "ping":
2198
+ respond(id, {});
2199
+ return;
2200
+ case "ui/request-display-mode":
2201
+ respond(id, { mode: "inline" });
2202
+ return;
2203
+ default: refuse(id, METHOD_NOT_FOUND, `This host does not support ${method}.`);
2204
+ }
2205
+ };
2206
+ const handleNotification = (method, params) => {
2207
+ switch (method) {
2208
+ case SANDBOX_PROXY_READY:
2209
+ if (sandboxAnswered) return;
2210
+ sandboxAnswered = true;
2211
+ options.post({
2212
+ jsonrpc: "2.0",
2213
+ method: SANDBOX_RESOURCE_READY,
2214
+ params: { html: view.html }
2215
+ });
2216
+ return;
2217
+ case "ui/notifications/initialized":
2218
+ if (initialization !== "initializing") return;
2219
+ initialization = "ready";
2220
+ options.post({
2221
+ jsonrpc: "2.0",
2222
+ method: "ui/notifications/tool-input",
2223
+ params: { arguments: view.toolInput ?? {} }
2224
+ });
2225
+ options.post({
2226
+ jsonrpc: "2.0",
2227
+ method: "ui/notifications/tool-result",
2228
+ params: options.toolResult
2229
+ });
2230
+ return;
2231
+ case "ui/notifications/size-changed": {
2232
+ const height = isPlainObject(params) ? params["height"] : void 0;
2233
+ if (typeof height !== "number" || !Number.isFinite(height)) return;
2234
+ options.onHeight(Math.min(640, Math.max(48, Math.round(height))));
2235
+ return;
2236
+ }
2237
+ default: return;
2238
+ }
2239
+ };
2240
+ return {
2241
+ handleMessage(event) {
2242
+ if (disposed) return;
2243
+ const frame = options.source();
2244
+ if (frame === null || event.source !== frame) return;
2245
+ const data = event.data;
2246
+ if (!isPlainObject(data) || data["jsonrpc"] !== "2.0") return;
2247
+ const { method, id } = data;
2248
+ if (typeof method !== "string") return;
2249
+ if (id === void 0) handleNotification(method, data["params"]);
2250
+ else if (typeof id === "string" || typeof id === "number") handleRequest(id, method, data["params"]);
2251
+ },
2252
+ dispose() {
2253
+ if (disposed) return;
2254
+ disposed = true;
2255
+ if (initialization === "ready") options.post({
2256
+ jsonrpc: "2.0",
2257
+ id: "teardown",
2258
+ method: "ui/resource-teardown",
2259
+ params: { reason: "closed" }
2260
+ });
2261
+ }
2262
+ };
2263
+ }
2264
+ //#endregion
2265
+ //#region src/app-view/host-context.ts
2266
+ const AppViewHostContext = (0, react.createContext)(null);
2267
+ function readAppViewHostContext(platform) {
2268
+ return {
2269
+ theme: typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
2270
+ displayMode: "inline",
2271
+ availableDisplayModes: ["inline"],
2272
+ containerDimensions: { maxHeight: 640 },
2273
+ platform,
2274
+ locale: typeof navigator === "undefined" ? "en-US" : navigator.language,
2275
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
2276
+ };
2277
+ }
2278
+ //#endregion
2279
+ //#region src/components/AppView.tsx
2280
+ const INITIAL_HEIGHT_PX = 160;
2281
+ const APP_VIEW_UNAVAILABLE_TEXT = "This tool returned an app view, which this window cannot show.";
2282
+ function AppView({ tool }) {
2283
+ const host = (0, react.useContext)(AppViewHostContext);
2284
+ if (tool.appView === void 0) return null;
2285
+ const viewKey = appViewKey(tool);
2286
+ if (host === null || host.sandboxUrl === void 0 || viewKey === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2287
+ className: "text-[13px] text-subtle",
2288
+ children: APP_VIEW_UNAVAILABLE_TEXT
2289
+ });
2290
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppViewFrame, {
2291
+ tool,
2292
+ host,
2293
+ sandboxUrl: host.sandboxUrl,
2294
+ viewKey,
2295
+ sourceId: tool.appView.sourceId
2296
+ }, viewKey);
2297
+ }
2298
+ function AppViewFrame({ tool, host, sandboxUrl, viewKey, sourceId }) {
2299
+ const [load, setLoad] = (0, react.useState)({ status: "loading" });
2300
+ const [height, setHeight] = (0, react.useState)(INITIAL_HEIGHT_PX);
2301
+ const frameRef = (0, react.useRef)(null);
2302
+ const toolRef = (0, react.useRef)(tool);
2303
+ toolRef.current = tool;
2304
+ (0, react.useEffect)(() => {
2305
+ const controller = new AbortController();
2306
+ setLoad({ status: "loading" });
2307
+ host.readAppView(viewKey, controller.signal).then((view) => {
2308
+ if (!controller.signal.aborted) setLoad({
2309
+ status: "ready",
2310
+ view,
2311
+ host,
2312
+ sandboxUrl,
2313
+ signal: controller.signal
2314
+ });
2315
+ }, (error) => {
2316
+ if (!controller.signal.aborted) setLoad({
2317
+ status: "failed",
2318
+ reason: error instanceof Error ? error.message : String(error)
2319
+ });
2320
+ });
2321
+ return () => {
2322
+ controller.abort();
2323
+ };
2324
+ }, [
2325
+ viewKey,
2326
+ host.readAppView,
2327
+ host.callAppTool,
2328
+ sandboxUrl
2329
+ ]);
2330
+ const ready = load.status === "ready" && load.host.readAppView === host.readAppView && load.host.callAppTool === host.callAppTool && load.sandboxUrl === sandboxUrl ? load : null;
2331
+ (0, react.useLayoutEffect)(() => {
2332
+ const frame = frameRef.current;
2333
+ if (ready === null || frame === null) return void 0;
2334
+ const current = ready.host;
2335
+ const view = ready.view;
2336
+ const bridge = createAppViewBridge({
2337
+ view,
2338
+ toolResult: toCallToolResult(toolRef.current, view),
2339
+ hostInfo: current.hostInfo,
2340
+ hostContext: readAppViewHostContext(current.platform),
2341
+ post: (message) => frame.contentWindow?.postMessage(message, "*"),
2342
+ source: () => frame.contentWindow,
2343
+ callAppTool: (name, args) => current.callAppTool(viewKey, name, args, ready.signal),
2344
+ onHeight: setHeight
2345
+ });
2346
+ const onMessage = (event) => bridge.handleMessage(event);
2347
+ window.addEventListener("message", onMessage);
2348
+ return () => {
2349
+ window.removeEventListener("message", onMessage);
2350
+ bridge.dispose();
2351
+ };
2352
+ }, [ready, viewKey]);
2353
+ if (load.status === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2354
+ className: "text-[13px] text-subtle",
2355
+ children: [
2356
+ "The app from ",
2357
+ sourceId,
2358
+ " could not be shown: ",
2359
+ load.reason
2360
+ ]
2361
+ });
2362
+ if (ready === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2363
+ className: "text-[13px] text-subtle",
2364
+ children: [
2365
+ "Loading the app from ",
2366
+ sourceId,
2367
+ "…"
2368
+ ]
2369
+ });
2370
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2371
+ className: "my-1 flex flex-col gap-1",
2372
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2373
+ className: "text-[12px] text-subtle",
2374
+ children: ["App from ", sourceId]
2375
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
2376
+ ref: frameRef,
2377
+ sandbox: "allow-scripts",
2378
+ src: sandboxUrl,
2379
+ title: `App from ${sourceId}`,
2380
+ referrerPolicy: "no-referrer",
2381
+ "data-testid": "app-view-frame",
2382
+ style: {
2383
+ width: "100%",
2384
+ height
2385
+ },
2386
+ className: "block rounded-lg border border-border bg-card"
2387
+ })]
2388
+ });
2389
+ }
2390
+ //#endregion
1925
2391
  //#region src/components/DiffLines.tsx
1926
2392
  /**
1927
2393
  * #3288: renders a unified diff already built server-side (the ONE diff builder, in
@@ -2435,42 +2901,49 @@ function ToolGroup({ tools }) {
2435
2901
  if (visible.length === 0) return null;
2436
2902
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2437
2903
  className: "text-[14px]",
2438
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2439
- type: "button",
2440
- "aria-expanded": open,
2441
- onClick: () => setOpen((value) => !value),
2442
- className: "group -mx-2 flex max-w-full items-center gap-2.5 rounded-lg px-2 py-1 text-left text-muted-foreground hover:bg-hover hover:text-foreground",
2443
- children: [
2444
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Wrench, {
2445
- size: 15,
2446
- strokeWidth: 1.75,
2447
- className: "flex-shrink-0 text-subtle"
2448
- }),
2449
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2450
- className: "flex-shrink-0",
2451
- children: [
2452
- visible.length,
2453
- " tool ",
2454
- visible.length === 1 ? "call" : "calls"
2455
- ]
2456
- }),
2457
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2458
- className: "min-w-0 truncate text-subtle",
2459
- children: names
2460
- }),
2461
- failed > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2462
- className: "flex-shrink-0 rounded-md bg-destructive/12 px-1.5 text-[12.5px] text-destructive",
2463
- children: [failed, " failed"]
2464
- }),
2465
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
2466
- size: 14,
2467
- className: `flex-shrink-0 text-subtle transition-transform ${open ? "rotate-90" : ""}`
2468
- })
2469
- ]
2470
- }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2471
- className: "mt-0.5 flex flex-col pl-[25px]",
2472
- children: visible.map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCard, { tool }, tool.id))
2473
- })]
2904
+ children: [
2905
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2906
+ type: "button",
2907
+ "aria-expanded": open,
2908
+ onClick: () => setOpen((value) => !value),
2909
+ className: "group -mx-2 flex max-w-full items-center gap-2.5 rounded-lg px-2 py-1 text-left text-muted-foreground hover:bg-hover hover:text-foreground",
2910
+ children: [
2911
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Wrench, {
2912
+ size: 15,
2913
+ strokeWidth: 1.75,
2914
+ className: "flex-shrink-0 text-subtle"
2915
+ }),
2916
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2917
+ className: "flex-shrink-0",
2918
+ children: [
2919
+ visible.length,
2920
+ " tool ",
2921
+ visible.length === 1 ? "call" : "calls"
2922
+ ]
2923
+ }),
2924
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2925
+ className: "min-w-0 truncate text-subtle",
2926
+ children: names
2927
+ }),
2928
+ failed > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2929
+ className: "flex-shrink-0 rounded-md bg-destructive/12 px-1.5 text-[12.5px] text-destructive",
2930
+ children: [failed, " failed"]
2931
+ }),
2932
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
2933
+ size: 14,
2934
+ className: `flex-shrink-0 text-subtle transition-transform ${open ? "rotate-90" : ""}`
2935
+ })
2936
+ ]
2937
+ }),
2938
+ visible.filter((tool) => tool.appView !== void 0).map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2939
+ className: "pl-[25px]",
2940
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppView, { tool })
2941
+ }, tool.id)),
2942
+ open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2943
+ className: "mt-0.5 flex flex-col pl-[25px]",
2944
+ children: visible.map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCard, { tool }, tool.id))
2945
+ })
2946
+ ]
2474
2947
  });
2475
2948
  }
2476
2949
  /** #3288: a turn that changed files ends with this compact row — a click opens that file's diff. */
@@ -2596,7 +3069,10 @@ function ConversationView({ messages, activeTools, streamingText, isThinking, ow
2596
3069
  isThinking && !streamingText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThinkingIndicator, {}),
2597
3070
  activeTools.some((tool) => !tool.internal) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2598
3071
  className: "flex flex-col",
2599
- children: activeTools.filter((tool) => !tool.internal).map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCard, { tool }, tool.id))
3072
+ children: activeTools.filter((tool) => !tool.internal).map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.default.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCard, { tool }), tool.appView !== void 0 && tool.status !== "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3073
+ className: "pl-[25px]",
3074
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppView, { tool })
3075
+ })] }, tool.id))
2600
3076
  }),
2601
3077
  streamingText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentBlock, {
2602
3078
  content: streamingText,
@@ -7536,6 +8012,7 @@ const NARROW_WINDOW_QUERY = "(max-width: 767.98px)";
7536
8012
  */
7537
8013
  /** The column every part of the conversation shares, so messages, prompt and composer line up. */
7538
8014
  const COLUMN = "mx-auto w-full max-w-[760px] px-6";
8015
+ const APP_VIEW_HOST_VERSION = "1.0.0";
7539
8016
  /**
7540
8017
  * Designed empty state shown before the first turn. Renders in the same slot as `ConversationView`
7541
8018
  * (never together), so it carries the same `main` landmark and label (#3289 §3 review) — nothing
@@ -7602,8 +8079,25 @@ function SetupPanel({ onSetUp, disabled }) {
7602
8079
  * The full desktop layout over an `IWsSessionState`. `surface` is an optional label shown next to the
7603
8080
  * mark (e.g. "app").
7604
8081
  */
7605
- function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor }) {
8082
+ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
7606
8083
  const [view, setView] = (0, react.useState)("chat");
8084
+ const { identity } = useProductIdentity();
8085
+ const appViewHost = (0, react.useMemo)(() => ({
8086
+ ...appViewSandboxUrl !== void 0 ? { sandboxUrl: appViewSandboxUrl } : {},
8087
+ readAppView: state.readAppView,
8088
+ callAppTool: state.callAppTool,
8089
+ hostInfo: {
8090
+ name: identity.displayName,
8091
+ version: APP_VIEW_HOST_VERSION
8092
+ },
8093
+ platform: surface === "app" ? "desktop" : "web"
8094
+ }), [
8095
+ appViewSandboxUrl,
8096
+ state.readAppView,
8097
+ state.callAppTool,
8098
+ identity.displayName,
8099
+ surface
8100
+ ]);
7607
8101
  const composerRef = (0, react.useRef)(null);
7608
8102
  const tasks = state.executionWorkspace?.entries ?? [];
7609
8103
  const hasTasks = tasks.some((entry) => entry.kind !== "main_thread");
@@ -7732,12 +8226,15 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
7732
8226
  args: "add"
7733
8227
  }),
7734
8228
  disabled: state.status !== "connected"
7735
- }) : isEmpty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationView, {
7736
- messages: state.messages,
7737
- activeTools: state.activeTools,
7738
- streamingText: state.streamingText,
7739
- isThinking: state.isThinking,
7740
- ownDriverId: state.ownDriverId
8229
+ }) : isEmpty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppViewHostContext.Provider, {
8230
+ value: appViewHost,
8231
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationView, {
8232
+ messages: state.messages,
8233
+ activeTools: state.activeTools,
8234
+ streamingText: state.streamingText,
8235
+ isThinking: state.isThinking,
8236
+ ownDriverId: state.ownDriverId
8237
+ })
7741
8238
  })
7742
8239
  }),
7743
8240
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {