@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.
@@ -1,4 +1,4 @@
1
- import React, { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
1
+ import React, { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import { decodeFrame, decodeServerMessage } from "@robota-sdk/agent-transport/client";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
4
  import ReactMarkdown from "react-markdown";
@@ -285,8 +285,8 @@ const SERVER_MESSAGE_HANDLING = {
285
285
  project_status: "reducer-state",
286
286
  project_diff: "reducer-state",
287
287
  project_memory: "reducer-state",
288
- app_view: "intentionally-not-rendered",
289
- app_tool_result: "intentionally-not-rendered",
288
+ app_view: "reducer-state",
289
+ app_tool_result: "reducer-state",
290
290
  background_task_event: "intentionally-not-rendered",
291
291
  background_job_group_event: "intentionally-not-rendered",
292
292
  plan_event: "intentionally-not-rendered",
@@ -311,10 +311,10 @@ const SERVER_MESSAGE_HANDLING = {
311
311
  };
312
312
  //#endregion
313
313
  //#region src/hooks/use-agent-switcher-state.ts
314
- let requestCounter$6 = 0;
315
- function nextRequestId$6(prefix) {
316
- requestCounter$6 += 1;
317
- return `${prefix}_${requestCounter$6}_${Date.now()}`;
314
+ let requestCounter$7 = 0;
315
+ function nextRequestId$7(prefix) {
316
+ requestCounter$7 += 1;
317
+ return `${prefix}_${requestCounter$7}_${Date.now()}`;
318
318
  }
319
319
  /**
320
320
  * #3282 §4 part b-3: the agent switcher sheet's own correlated request/response state, the same
@@ -349,7 +349,7 @@ function useAgentSwitcherState(send) {
349
349
  return true;
350
350
  }, []);
351
351
  const requestAgentDefinitions = useCallback(() => {
352
- const requestId = nextRequestId$6("get_agent_definitions");
352
+ const requestId = nextRequestId$7("get_agent_definitions");
353
353
  listRequestRef.current = requestId;
354
354
  send({
355
355
  type: "get-agent-definitions",
@@ -372,7 +372,7 @@ function useAgentSwitcherState(send) {
372
372
  setAgentSwitcherOpen(false);
373
373
  }, []),
374
374
  selectAgent: useCallback((agentType) => {
375
- const requestId = nextRequestId$6("agent_switch");
375
+ const requestId = nextRequestId$7("agent_switch");
376
376
  switchRequestRef.current = requestId;
377
377
  setAgentSwitchMessage(null);
378
378
  send({
@@ -393,6 +393,126 @@ function useAgentSwitcherState(send) {
393
393
  };
394
394
  }
395
395
  //#endregion
396
+ //#region src/hooks/use-app-view-requests.ts
397
+ let requestCounter$6 = 0;
398
+ function nextRequestId$6(prefix) {
399
+ requestCounter$6 += 1;
400
+ return `${prefix}_${requestCounter$6}_${Date.now()}`;
401
+ }
402
+ const CONNECTION_CLOSED = "The connection to the session closed.";
403
+ /**
404
+ * The key a finished call's app view is read and called by, taken from the tool row's `appView`
405
+ * reference; `undefined` when the row gives none. Everything outside this module treats it as an
406
+ * opaque string — it is read here and put on the two wire messages below, nowhere else.
407
+ */
408
+ function appViewKey(tool) {
409
+ return tool.appView?.viewId;
410
+ }
411
+ function readAppViewMessage(requestId, viewKey) {
412
+ return {
413
+ type: "read-app-view",
414
+ requestId,
415
+ viewId: viewKey
416
+ };
417
+ }
418
+ function callAppToolMessage(requestId, viewKey, name, args) {
419
+ return {
420
+ type: "call-app-tool",
421
+ requestId,
422
+ viewId: viewKey,
423
+ name,
424
+ arguments: args
425
+ };
426
+ }
427
+ /**
428
+ * The app-view host's two requests to the session, each a promise settled by the reply that echoes
429
+ * its `requestId` (`app_view` / `app_tool_result`, or a `protocol_error` naming it). Unlike the
430
+ * panels' latest-wins state, several can be outstanding at once (one per frame on screen), so each
431
+ * keeps its own entry. A reply that lost its connection never arrives: the owner of the connection
432
+ * rejects every outstanding request when it drops, is replaced or goes away (`rejectPendingAppRequests`).
433
+ */
434
+ function useAppViewRequests(send, isConnected) {
435
+ const pendingRef = useRef(/* @__PURE__ */ new Map());
436
+ const [requestOwner] = useState(() => nextRequestId$6("app_request"));
437
+ const rejectPendingAppRequests = useCallback((reason = CONNECTION_CLOSED) => {
438
+ const pending = [...pendingRef.current.values()];
439
+ pendingRef.current.clear();
440
+ for (const request of pending) {
441
+ request.dispose();
442
+ request.reject(new Error(reason));
443
+ }
444
+ }, []);
445
+ const request = useCallback((prefix, message, pending, signal) => new Promise((resolve, reject) => {
446
+ if (!isConnected() || signal?.aborted) {
447
+ reject(/* @__PURE__ */ new Error(signal?.aborted ? "The app view closed." : CONNECTION_CLOSED));
448
+ return;
449
+ }
450
+ const requestId = nextRequestId$6(`${requestOwner}_${prefix}`);
451
+ const dispose = () => signal?.removeEventListener("abort", abort);
452
+ const abort = () => {
453
+ pendingRef.current.delete(requestId);
454
+ dispose();
455
+ reject(/* @__PURE__ */ new Error("The app view closed."));
456
+ };
457
+ pendingRef.current.set(requestId, pending(resolve, reject, dispose));
458
+ signal?.addEventListener("abort", abort, { once: true });
459
+ try {
460
+ send(message(requestId));
461
+ } catch (error) {
462
+ pendingRef.current.delete(requestId);
463
+ dispose();
464
+ reject(error instanceof Error ? error : new Error(String(error)));
465
+ }
466
+ }), [
467
+ isConnected,
468
+ requestOwner,
469
+ send
470
+ ]);
471
+ return {
472
+ readAppView: useCallback((viewKey, signal) => request("app_view", (requestId) => readAppViewMessage(requestId, viewKey), (resolve, reject, dispose) => ({
473
+ answer: "app_view",
474
+ resolve,
475
+ reject,
476
+ dispose
477
+ }), signal), [request]),
478
+ callAppTool: useCallback((viewKey, name, args, signal) => request("app_tool", (requestId) => callAppToolMessage(requestId, viewKey, name, args), (resolve, reject, dispose) => ({
479
+ answer: "app_tool_result",
480
+ resolve,
481
+ reject,
482
+ dispose
483
+ }), signal), [request]),
484
+ handleAppViewMessage: useCallback((msg) => {
485
+ if (msg.type === "app_view" || msg.type === "app_tool_result") {
486
+ const pending = pendingRef.current.get(msg.requestId);
487
+ if (pending === void 0 || pending.answer !== msg.type) return true;
488
+ pendingRef.current.delete(msg.requestId);
489
+ pending.dispose();
490
+ if (msg.type === "app_view" && pending.answer === "app_view") pending.resolve(msg.view);
491
+ else if (msg.type === "app_tool_result" && pending.answer === "app_tool_result") {
492
+ const { success, data, parts, error } = msg;
493
+ pending.resolve({
494
+ success,
495
+ ...data !== void 0 ? { data } : {},
496
+ ...parts !== void 0 ? { parts } : {},
497
+ ...error !== void 0 ? { error } : {}
498
+ });
499
+ }
500
+ return true;
501
+ }
502
+ if (msg.type === "protocol_error" && msg.requestId !== void 0) {
503
+ const pending = pendingRef.current.get(msg.requestId);
504
+ if (pending === void 0) return msg.requestId.startsWith(`${requestOwner}_`);
505
+ pendingRef.current.delete(msg.requestId);
506
+ pending.dispose();
507
+ pending.reject(new Error(msg.message));
508
+ return true;
509
+ }
510
+ return false;
511
+ }, [requestOwner]),
512
+ rejectPendingAppRequests
513
+ };
514
+ }
515
+ //#endregion
396
516
  //#region src/hooks/use-execution-detail.ts
397
517
  /**
398
518
  * #3288 §1: the detail sheet's own correlated request/response state — which entry is open, its
@@ -1077,7 +1197,8 @@ function toolEndFields(state) {
1077
1197
  ...state.diffLines ? { diffLines: state.diffLines } : {},
1078
1198
  ...state.diffFile ? { diffFile: state.diffFile } : {},
1079
1199
  ...state.toolResultData !== void 0 ? { toolResultData: state.toolResultData } : {},
1080
- ...state.toolResultParts ? { toolResultParts: state.toolResultParts } : {}
1200
+ ...state.toolResultParts ? { toolResultParts: state.toolResultParts } : {},
1201
+ ...state.appView ? { appView: state.appView } : {}
1081
1202
  };
1082
1203
  }
1083
1204
  /** Apply a `tool_end` to the ONE matching running tool across a turn's segments (never all of them). */
@@ -1175,6 +1296,7 @@ function buildConversationEntriesFromDisplaySegments(displaySegments) {
1175
1296
  }
1176
1297
  function useSessionClient(makeClient) {
1177
1298
  const [status, setStatus] = useState("disconnected");
1299
+ const statusRef = useRef("disconnected");
1178
1300
  const [messages, setMessages] = useState([]);
1179
1301
  const [activeTools, setActiveTools] = useState([]);
1180
1302
  const [streamingText, setStreamingText] = useState("");
@@ -1299,6 +1421,7 @@ function useSessionClient(makeClient) {
1299
1421
  const { handleExecutionDetailMessage, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
1300
1422
  const { handleModelListMessage, ...modelListState } = useModelListState(send);
1301
1423
  const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
1424
+ const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, useCallback(() => statusRef.current === "connected", []));
1302
1425
  const { handleSessionsMessage, canListSessions, markCurrent, armRestore, ...sessionDirectoryState } = useSessionDirectoryState(send);
1303
1426
  const { requestSessions, setSessionSidebarOpen } = sessionDirectoryState;
1304
1427
  const { handleSettingsMessage, openSettings, refreshSettings, ...settingsState } = useSettingsState(send);
@@ -1312,6 +1435,7 @@ function useSessionClient(makeClient) {
1312
1435
  if (handleExecutionDetailMessage(msg)) return;
1313
1436
  if (handleModelListMessage(msg)) return;
1314
1437
  if (handleProjectMessage(msg)) return;
1438
+ if (handleAppViewMessage(msg)) return;
1315
1439
  if (handleSessionsMessage(msg)) return;
1316
1440
  if (handleSettingsMessage(msg)) return;
1317
1441
  if (handleAgentDefinitionsMessage(msg)) return;
@@ -1448,6 +1572,7 @@ function useSessionClient(makeClient) {
1448
1572
  requestSessions();
1449
1573
  break;
1450
1574
  case "session_switched":
1575
+ rejectPendingAppRequests("The active session changed.");
1451
1576
  settleSessionChange();
1452
1577
  sessionStatusRef.current = null;
1453
1578
  setSessionStatus(null);
@@ -1472,6 +1597,7 @@ function useSessionClient(makeClient) {
1472
1597
  requestSchedules();
1473
1598
  break;
1474
1599
  case "history_cleared":
1600
+ rejectPendingAppRequests("The session history was cleared.");
1475
1601
  streamingTextRef.current = "";
1476
1602
  turnSegmentsRef.current = [];
1477
1603
  setStreamingText("");
@@ -1612,6 +1738,7 @@ function useSessionClient(makeClient) {
1612
1738
  closeExecutionDetail,
1613
1739
  finishTurn,
1614
1740
  handleAgentDefinitionsMessage,
1741
+ handleAppViewMessage,
1615
1742
  handleExecutionDetailMessage,
1616
1743
  handleModelListMessage,
1617
1744
  handleProjectMessage,
@@ -1626,6 +1753,7 @@ function useSessionClient(makeClient) {
1626
1753
  requestSchedules,
1627
1754
  requestSessions,
1628
1755
  resolveSwitchResult,
1756
+ rejectPendingAppRequests,
1629
1757
  send,
1630
1758
  setSessionSidebarOpen,
1631
1759
  settleSessionChange,
@@ -1644,7 +1772,9 @@ function useSessionClient(makeClient) {
1644
1772
  }, []);
1645
1773
  useEffect(() => {
1646
1774
  const onStatusChange = (next) => {
1775
+ statusRef.current = next;
1647
1776
  setStatus(next);
1777
+ if (next !== "connected") rejectPendingAppRequests();
1648
1778
  if (next === "connected") {
1649
1779
  commandsInFlightRef.current = 0;
1650
1780
  silentCommandRequestIdsRef.current.clear();
@@ -1668,13 +1798,15 @@ function useSessionClient(makeClient) {
1668
1798
  return () => {
1669
1799
  client.disconnect();
1670
1800
  clientRef.current = null;
1801
+ rejectPendingAppRequests();
1671
1802
  };
1672
1803
  }, [
1673
1804
  makeClient,
1674
1805
  handleMessage,
1675
1806
  requestSessions,
1676
1807
  requestSchedules,
1677
- armRestore
1808
+ armRestore,
1809
+ rejectPendingAppRequests
1678
1810
  ]);
1679
1811
  return {
1680
1812
  status,
@@ -1706,6 +1838,8 @@ function useSessionClient(makeClient) {
1706
1838
  openAgentSwitcher,
1707
1839
  ...agentSwitcherState,
1708
1840
  ...schedulesState,
1841
+ readAppView,
1842
+ callAppTool,
1709
1843
  helpOpen,
1710
1844
  openHelp,
1711
1845
  closeHelp
@@ -1896,6 +2030,338 @@ function driverAttributionText(driverId) {
1896
2030
  return `from ${humanDriverLabel(driverId)}`;
1897
2031
  }
1898
2032
  //#endregion
2033
+ //#region src/app-view/call-tool-result.ts
2034
+ function isRecord(value) {
2035
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2036
+ }
2037
+ function contentFromParts(parts) {
2038
+ const content = [];
2039
+ for (const part of parts ?? []) if (part.type === "text") content.push({
2040
+ type: "text",
2041
+ text: part.text
2042
+ });
2043
+ else if (part.type === "image_inline") content.push({
2044
+ type: "image",
2045
+ data: part.data,
2046
+ mimeType: part.mimeType
2047
+ });
2048
+ else if (part.type === "audio_inline") content.push({
2049
+ type: "audio",
2050
+ data: part.data,
2051
+ mimeType: part.mimeType
2052
+ });
2053
+ return content;
2054
+ }
2055
+ function outcomeToCallToolResult(outcome) {
2056
+ let content = contentFromParts(outcome.parts);
2057
+ if (content.length === 0 && typeof outcome.data === "string") content = [{
2058
+ type: "text",
2059
+ text: outcome.data
2060
+ }];
2061
+ if (!outcome.success && outcome.error !== void 0) content = [...content, {
2062
+ type: "text",
2063
+ text: outcome.error
2064
+ }];
2065
+ return {
2066
+ content,
2067
+ ...isRecord(outcome.data) ? { structuredContent: outcome.data } : {},
2068
+ isError: !outcome.success
2069
+ };
2070
+ }
2071
+ function toCallToolResult(tool, view) {
2072
+ let content = contentFromParts(tool.toolResultParts);
2073
+ if (content.length === 0 && tool.toolResultData !== void 0) content = [{
2074
+ type: "text",
2075
+ text: tool.toolResultData
2076
+ }];
2077
+ return {
2078
+ content,
2079
+ ...isRecord(view.structuredContent) ? { structuredContent: view.structuredContent } : {},
2080
+ isError: tool.status === "error"
2081
+ };
2082
+ }
2083
+ //#endregion
2084
+ //#region src/app-view/bridge.ts
2085
+ const APP_VIEW_PROTOCOL_VERSION = "2026-01-26";
2086
+ const INVALID_PARAMS = -32602;
2087
+ const METHOD_NOT_FOUND = -32601;
2088
+ const HOST_ERROR = -32e3;
2089
+ const SANDBOX_PROXY_READY = "ui/notifications/sandbox-proxy-ready";
2090
+ const SANDBOX_RESOURCE_READY = "ui/notifications/sandbox-resource-ready";
2091
+ function isPlainObject(value) {
2092
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2093
+ const prototype = Object.getPrototypeOf(value);
2094
+ return prototype === null || Object.getPrototypeOf(prototype) === null;
2095
+ }
2096
+ function errorMessage(error) {
2097
+ return error instanceof Error ? error.message : String(error);
2098
+ }
2099
+ function createAppViewBridge(options) {
2100
+ const { view } = options;
2101
+ let disposed = false;
2102
+ let sandboxAnswered = false;
2103
+ let initialization = "uninitialized";
2104
+ let callInFlight = false;
2105
+ const respond = (id, result) => {
2106
+ options.post({
2107
+ jsonrpc: "2.0",
2108
+ id,
2109
+ result
2110
+ });
2111
+ };
2112
+ const refuse = (id, code, message) => {
2113
+ options.post({
2114
+ jsonrpc: "2.0",
2115
+ id,
2116
+ error: {
2117
+ code,
2118
+ message
2119
+ }
2120
+ });
2121
+ };
2122
+ const callTool = (id, params) => {
2123
+ if (initialization !== "ready") {
2124
+ refuse(id, HOST_ERROR, "This app view has not initialized.");
2125
+ return;
2126
+ }
2127
+ const name = isPlainObject(params) ? params["name"] : void 0;
2128
+ const runtimeName = typeof name === "string" && Object.hasOwn(view.callableTools, name) ? view.callableTools[name] : void 0;
2129
+ if (!isPlainObject(params) || runtimeName === void 0) {
2130
+ refuse(id, INVALID_PARAMS, "This tool is not available to this view.");
2131
+ return;
2132
+ }
2133
+ const rawArguments = params["arguments"] === void 0 ? {} : params["arguments"];
2134
+ if (!isPlainObject(rawArguments)) {
2135
+ refuse(id, INVALID_PARAMS, "Tool arguments must be an object.");
2136
+ return;
2137
+ }
2138
+ if (callInFlight) {
2139
+ refuse(id, HOST_ERROR, "Another tool call from this view is still running.");
2140
+ return;
2141
+ }
2142
+ callInFlight = true;
2143
+ let outcome;
2144
+ try {
2145
+ outcome = options.callAppTool(runtimeName, rawArguments);
2146
+ } catch (error) {
2147
+ outcome = Promise.reject(error);
2148
+ }
2149
+ outcome.then((settled) => {
2150
+ callInFlight = false;
2151
+ if (!disposed) respond(id, outcomeToCallToolResult(settled));
2152
+ }, (error) => {
2153
+ callInFlight = false;
2154
+ if (!disposed) refuse(id, HOST_ERROR, errorMessage(error));
2155
+ });
2156
+ };
2157
+ const handleRequest = (id, method, params) => {
2158
+ switch (method) {
2159
+ case "ui/initialize":
2160
+ if (initialization === "uninitialized") initialization = "initializing";
2161
+ respond(id, {
2162
+ protocolVersion: APP_VIEW_PROTOCOL_VERSION,
2163
+ hostCapabilities: { serverTools: {} },
2164
+ hostInfo: options.hostInfo,
2165
+ hostContext: options.hostContext
2166
+ });
2167
+ return;
2168
+ case "tools/call":
2169
+ callTool(id, params);
2170
+ return;
2171
+ case "ping":
2172
+ respond(id, {});
2173
+ return;
2174
+ case "ui/request-display-mode":
2175
+ respond(id, { mode: "inline" });
2176
+ return;
2177
+ default: refuse(id, METHOD_NOT_FOUND, `This host does not support ${method}.`);
2178
+ }
2179
+ };
2180
+ const handleNotification = (method, params) => {
2181
+ switch (method) {
2182
+ case SANDBOX_PROXY_READY:
2183
+ if (sandboxAnswered) return;
2184
+ sandboxAnswered = true;
2185
+ options.post({
2186
+ jsonrpc: "2.0",
2187
+ method: SANDBOX_RESOURCE_READY,
2188
+ params: { html: view.html }
2189
+ });
2190
+ return;
2191
+ case "ui/notifications/initialized":
2192
+ if (initialization !== "initializing") return;
2193
+ initialization = "ready";
2194
+ options.post({
2195
+ jsonrpc: "2.0",
2196
+ method: "ui/notifications/tool-input",
2197
+ params: { arguments: view.toolInput ?? {} }
2198
+ });
2199
+ options.post({
2200
+ jsonrpc: "2.0",
2201
+ method: "ui/notifications/tool-result",
2202
+ params: options.toolResult
2203
+ });
2204
+ return;
2205
+ case "ui/notifications/size-changed": {
2206
+ const height = isPlainObject(params) ? params["height"] : void 0;
2207
+ if (typeof height !== "number" || !Number.isFinite(height)) return;
2208
+ options.onHeight(Math.min(640, Math.max(48, Math.round(height))));
2209
+ return;
2210
+ }
2211
+ default: return;
2212
+ }
2213
+ };
2214
+ return {
2215
+ handleMessage(event) {
2216
+ if (disposed) return;
2217
+ const frame = options.source();
2218
+ if (frame === null || event.source !== frame) return;
2219
+ const data = event.data;
2220
+ if (!isPlainObject(data) || data["jsonrpc"] !== "2.0") return;
2221
+ const { method, id } = data;
2222
+ if (typeof method !== "string") return;
2223
+ if (id === void 0) handleNotification(method, data["params"]);
2224
+ else if (typeof id === "string" || typeof id === "number") handleRequest(id, method, data["params"]);
2225
+ },
2226
+ dispose() {
2227
+ if (disposed) return;
2228
+ disposed = true;
2229
+ if (initialization === "ready") options.post({
2230
+ jsonrpc: "2.0",
2231
+ id: "teardown",
2232
+ method: "ui/resource-teardown",
2233
+ params: { reason: "closed" }
2234
+ });
2235
+ }
2236
+ };
2237
+ }
2238
+ //#endregion
2239
+ //#region src/app-view/host-context.ts
2240
+ const AppViewHostContext = createContext(null);
2241
+ function readAppViewHostContext(platform) {
2242
+ return {
2243
+ theme: typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
2244
+ displayMode: "inline",
2245
+ availableDisplayModes: ["inline"],
2246
+ containerDimensions: { maxHeight: 640 },
2247
+ platform,
2248
+ locale: typeof navigator === "undefined" ? "en-US" : navigator.language,
2249
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
2250
+ };
2251
+ }
2252
+ //#endregion
2253
+ //#region src/components/AppView.tsx
2254
+ const INITIAL_HEIGHT_PX = 160;
2255
+ const APP_VIEW_UNAVAILABLE_TEXT = "This tool returned an app view, which this window cannot show.";
2256
+ function AppView({ tool }) {
2257
+ const host = useContext(AppViewHostContext);
2258
+ if (tool.appView === void 0) return null;
2259
+ const viewKey = appViewKey(tool);
2260
+ if (host === null || host.sandboxUrl === void 0 || viewKey === void 0) return /* @__PURE__ */ jsx("p", {
2261
+ className: "text-[13px] text-subtle",
2262
+ children: APP_VIEW_UNAVAILABLE_TEXT
2263
+ });
2264
+ return /* @__PURE__ */ jsx(AppViewFrame, {
2265
+ tool,
2266
+ host,
2267
+ sandboxUrl: host.sandboxUrl,
2268
+ viewKey,
2269
+ sourceId: tool.appView.sourceId
2270
+ }, viewKey);
2271
+ }
2272
+ function AppViewFrame({ tool, host, sandboxUrl, viewKey, sourceId }) {
2273
+ const [load, setLoad] = useState({ status: "loading" });
2274
+ const [height, setHeight] = useState(INITIAL_HEIGHT_PX);
2275
+ const frameRef = useRef(null);
2276
+ const toolRef = useRef(tool);
2277
+ toolRef.current = tool;
2278
+ useEffect(() => {
2279
+ const controller = new AbortController();
2280
+ setLoad({ status: "loading" });
2281
+ host.readAppView(viewKey, controller.signal).then((view) => {
2282
+ if (!controller.signal.aborted) setLoad({
2283
+ status: "ready",
2284
+ view,
2285
+ host,
2286
+ sandboxUrl,
2287
+ signal: controller.signal
2288
+ });
2289
+ }, (error) => {
2290
+ if (!controller.signal.aborted) setLoad({
2291
+ status: "failed",
2292
+ reason: error instanceof Error ? error.message : String(error)
2293
+ });
2294
+ });
2295
+ return () => {
2296
+ controller.abort();
2297
+ };
2298
+ }, [
2299
+ viewKey,
2300
+ host.readAppView,
2301
+ host.callAppTool,
2302
+ sandboxUrl
2303
+ ]);
2304
+ const ready = load.status === "ready" && load.host.readAppView === host.readAppView && load.host.callAppTool === host.callAppTool && load.sandboxUrl === sandboxUrl ? load : null;
2305
+ useLayoutEffect(() => {
2306
+ const frame = frameRef.current;
2307
+ if (ready === null || frame === null) return void 0;
2308
+ const current = ready.host;
2309
+ const view = ready.view;
2310
+ const bridge = createAppViewBridge({
2311
+ view,
2312
+ toolResult: toCallToolResult(toolRef.current, view),
2313
+ hostInfo: current.hostInfo,
2314
+ hostContext: readAppViewHostContext(current.platform),
2315
+ post: (message) => frame.contentWindow?.postMessage(message, "*"),
2316
+ source: () => frame.contentWindow,
2317
+ callAppTool: (name, args) => current.callAppTool(viewKey, name, args, ready.signal),
2318
+ onHeight: setHeight
2319
+ });
2320
+ const onMessage = (event) => bridge.handleMessage(event);
2321
+ window.addEventListener("message", onMessage);
2322
+ return () => {
2323
+ window.removeEventListener("message", onMessage);
2324
+ bridge.dispose();
2325
+ };
2326
+ }, [ready, viewKey]);
2327
+ if (load.status === "failed") return /* @__PURE__ */ jsxs("p", {
2328
+ className: "text-[13px] text-subtle",
2329
+ children: [
2330
+ "The app from ",
2331
+ sourceId,
2332
+ " could not be shown: ",
2333
+ load.reason
2334
+ ]
2335
+ });
2336
+ if (ready === null) return /* @__PURE__ */ jsxs("p", {
2337
+ className: "text-[13px] text-subtle",
2338
+ children: [
2339
+ "Loading the app from ",
2340
+ sourceId,
2341
+ "…"
2342
+ ]
2343
+ });
2344
+ return /* @__PURE__ */ jsxs("div", {
2345
+ className: "my-1 flex flex-col gap-1",
2346
+ children: [/* @__PURE__ */ jsxs("p", {
2347
+ className: "text-[12px] text-subtle",
2348
+ children: ["App from ", sourceId]
2349
+ }), /* @__PURE__ */ jsx("iframe", {
2350
+ ref: frameRef,
2351
+ sandbox: "allow-scripts",
2352
+ src: sandboxUrl,
2353
+ title: `App from ${sourceId}`,
2354
+ referrerPolicy: "no-referrer",
2355
+ "data-testid": "app-view-frame",
2356
+ style: {
2357
+ width: "100%",
2358
+ height
2359
+ },
2360
+ className: "block rounded-lg border border-border bg-card"
2361
+ })]
2362
+ });
2363
+ }
2364
+ //#endregion
1899
2365
  //#region src/components/DiffLines.tsx
1900
2366
  /**
1901
2367
  * #3288: renders a unified diff already built server-side (the ONE diff builder, in
@@ -2409,42 +2875,49 @@ function ToolGroup({ tools }) {
2409
2875
  if (visible.length === 0) return null;
2410
2876
  return /* @__PURE__ */ jsxs("div", {
2411
2877
  className: "text-[14px]",
2412
- children: [/* @__PURE__ */ jsxs("button", {
2413
- type: "button",
2414
- "aria-expanded": open,
2415
- onClick: () => setOpen((value) => !value),
2416
- 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",
2417
- children: [
2418
- /* @__PURE__ */ jsx(Wrench, {
2419
- size: 15,
2420
- strokeWidth: 1.75,
2421
- className: "flex-shrink-0 text-subtle"
2422
- }),
2423
- /* @__PURE__ */ jsxs("span", {
2424
- className: "flex-shrink-0",
2425
- children: [
2426
- visible.length,
2427
- " tool ",
2428
- visible.length === 1 ? "call" : "calls"
2429
- ]
2430
- }),
2431
- /* @__PURE__ */ jsx("span", {
2432
- className: "min-w-0 truncate text-subtle",
2433
- children: names
2434
- }),
2435
- failed > 0 && /* @__PURE__ */ jsxs("span", {
2436
- className: "flex-shrink-0 rounded-md bg-destructive/12 px-1.5 text-[12.5px] text-destructive",
2437
- children: [failed, " failed"]
2438
- }),
2439
- /* @__PURE__ */ jsx(ChevronRight, {
2440
- size: 14,
2441
- className: `flex-shrink-0 text-subtle transition-transform ${open ? "rotate-90" : ""}`
2442
- })
2443
- ]
2444
- }), open && /* @__PURE__ */ jsx("div", {
2445
- className: "mt-0.5 flex flex-col pl-[25px]",
2446
- children: visible.map((tool) => /* @__PURE__ */ jsx(ToolCard, { tool }, tool.id))
2447
- })]
2878
+ children: [
2879
+ /* @__PURE__ */ jsxs("button", {
2880
+ type: "button",
2881
+ "aria-expanded": open,
2882
+ onClick: () => setOpen((value) => !value),
2883
+ 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",
2884
+ children: [
2885
+ /* @__PURE__ */ jsx(Wrench, {
2886
+ size: 15,
2887
+ strokeWidth: 1.75,
2888
+ className: "flex-shrink-0 text-subtle"
2889
+ }),
2890
+ /* @__PURE__ */ jsxs("span", {
2891
+ className: "flex-shrink-0",
2892
+ children: [
2893
+ visible.length,
2894
+ " tool ",
2895
+ visible.length === 1 ? "call" : "calls"
2896
+ ]
2897
+ }),
2898
+ /* @__PURE__ */ jsx("span", {
2899
+ className: "min-w-0 truncate text-subtle",
2900
+ children: names
2901
+ }),
2902
+ failed > 0 && /* @__PURE__ */ jsxs("span", {
2903
+ className: "flex-shrink-0 rounded-md bg-destructive/12 px-1.5 text-[12.5px] text-destructive",
2904
+ children: [failed, " failed"]
2905
+ }),
2906
+ /* @__PURE__ */ jsx(ChevronRight, {
2907
+ size: 14,
2908
+ className: `flex-shrink-0 text-subtle transition-transform ${open ? "rotate-90" : ""}`
2909
+ })
2910
+ ]
2911
+ }),
2912
+ visible.filter((tool) => tool.appView !== void 0).map((tool) => /* @__PURE__ */ jsx("div", {
2913
+ className: "pl-[25px]",
2914
+ children: /* @__PURE__ */ jsx(AppView, { tool })
2915
+ }, tool.id)),
2916
+ open && /* @__PURE__ */ jsx("div", {
2917
+ className: "mt-0.5 flex flex-col pl-[25px]",
2918
+ children: visible.map((tool) => /* @__PURE__ */ jsx(ToolCard, { tool }, tool.id))
2919
+ })
2920
+ ]
2448
2921
  });
2449
2922
  }
2450
2923
  /** #3288: a turn that changed files ends with this compact row — a click opens that file's diff. */
@@ -2570,7 +3043,10 @@ function ConversationView({ messages, activeTools, streamingText, isThinking, ow
2570
3043
  isThinking && !streamingText && /* @__PURE__ */ jsx(ThinkingIndicator, {}),
2571
3044
  activeTools.some((tool) => !tool.internal) && /* @__PURE__ */ jsx("div", {
2572
3045
  className: "flex flex-col",
2573
- children: activeTools.filter((tool) => !tool.internal).map((tool) => /* @__PURE__ */ jsx(ToolCard, { tool }, tool.id))
3046
+ children: activeTools.filter((tool) => !tool.internal).map((tool) => /* @__PURE__ */ jsxs(React.Fragment, { children: [/* @__PURE__ */ jsx(ToolCard, { tool }), tool.appView !== void 0 && tool.status !== "running" && /* @__PURE__ */ jsx("div", {
3047
+ className: "pl-[25px]",
3048
+ children: /* @__PURE__ */ jsx(AppView, { tool })
3049
+ })] }, tool.id))
2574
3050
  }),
2575
3051
  streamingText && /* @__PURE__ */ jsx(AgentBlock, {
2576
3052
  content: streamingText,
@@ -7510,6 +7986,7 @@ const NARROW_WINDOW_QUERY = "(max-width: 767.98px)";
7510
7986
  */
7511
7987
  /** The column every part of the conversation shares, so messages, prompt and composer line up. */
7512
7988
  const COLUMN = "mx-auto w-full max-w-[760px] px-6";
7989
+ const APP_VIEW_HOST_VERSION = "1.0.0";
7513
7990
  /**
7514
7991
  * Designed empty state shown before the first turn. Renders in the same slot as `ConversationView`
7515
7992
  * (never together), so it carries the same `main` landmark and label (#3289 §3 review) — nothing
@@ -7576,8 +8053,25 @@ function SetupPanel({ onSetUp, disabled }) {
7576
8053
  * The full desktop layout over an `IWsSessionState`. `surface` is an optional label shown next to the
7577
8054
  * mark (e.g. "app").
7578
8055
  */
7579
- function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor }) {
8056
+ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
7580
8057
  const [view, setView] = useState("chat");
8058
+ const { identity } = useProductIdentity();
8059
+ const appViewHost = useMemo(() => ({
8060
+ ...appViewSandboxUrl !== void 0 ? { sandboxUrl: appViewSandboxUrl } : {},
8061
+ readAppView: state.readAppView,
8062
+ callAppTool: state.callAppTool,
8063
+ hostInfo: {
8064
+ name: identity.displayName,
8065
+ version: APP_VIEW_HOST_VERSION
8066
+ },
8067
+ platform: surface === "app" ? "desktop" : "web"
8068
+ }), [
8069
+ appViewSandboxUrl,
8070
+ state.readAppView,
8071
+ state.callAppTool,
8072
+ identity.displayName,
8073
+ surface
8074
+ ]);
7581
8075
  const composerRef = useRef(null);
7582
8076
  const tasks = state.executionWorkspace?.entries ?? [];
7583
8077
  const hasTasks = tasks.some((entry) => entry.kind !== "main_thread");
@@ -7706,12 +8200,15 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
7706
8200
  args: "add"
7707
8201
  }),
7708
8202
  disabled: state.status !== "connected"
7709
- }) : isEmpty ? /* @__PURE__ */ jsx(EmptyState, {}) : /* @__PURE__ */ jsx(ConversationView, {
7710
- messages: state.messages,
7711
- activeTools: state.activeTools,
7712
- streamingText: state.streamingText,
7713
- isThinking: state.isThinking,
7714
- ownDriverId: state.ownDriverId
8203
+ }) : isEmpty ? /* @__PURE__ */ jsx(EmptyState, {}) : /* @__PURE__ */ jsx(AppViewHostContext.Provider, {
8204
+ value: appViewHost,
8205
+ children: /* @__PURE__ */ jsx(ConversationView, {
8206
+ messages: state.messages,
8207
+ activeTools: state.activeTools,
8208
+ streamingText: state.streamingText,
8209
+ isThinking: state.isThinking,
8210
+ ownDriverId: state.ownDriverId
8211
+ })
7715
8212
  })
7716
8213
  }),
7717
8214
  /* @__PURE__ */ jsxs("div", {