@robota-sdk/agent-ui-web 3.0.0-beta.94 → 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,6 +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: "reducer-state",
315
+ app_tool_result: "reducer-state",
314
316
  background_task_event: "intentionally-not-rendered",
315
317
  background_job_group_event: "intentionally-not-rendered",
316
318
  plan_event: "intentionally-not-rendered",
@@ -335,10 +337,10 @@ const SERVER_MESSAGE_HANDLING = {
335
337
  };
336
338
  //#endregion
337
339
  //#region src/hooks/use-agent-switcher-state.ts
338
- let requestCounter$6 = 0;
339
- function nextRequestId$6(prefix) {
340
- requestCounter$6 += 1;
341
- 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()}`;
342
344
  }
343
345
  /**
344
346
  * #3282 §4 part b-3: the agent switcher sheet's own correlated request/response state, the same
@@ -373,7 +375,7 @@ function useAgentSwitcherState(send) {
373
375
  return true;
374
376
  }, []);
375
377
  const requestAgentDefinitions = (0, react.useCallback)(() => {
376
- const requestId = nextRequestId$6("get_agent_definitions");
378
+ const requestId = nextRequestId$7("get_agent_definitions");
377
379
  listRequestRef.current = requestId;
378
380
  send({
379
381
  type: "get-agent-definitions",
@@ -396,7 +398,7 @@ function useAgentSwitcherState(send) {
396
398
  setAgentSwitcherOpen(false);
397
399
  }, []),
398
400
  selectAgent: (0, react.useCallback)((agentType) => {
399
- const requestId = nextRequestId$6("agent_switch");
401
+ const requestId = nextRequestId$7("agent_switch");
400
402
  switchRequestRef.current = requestId;
401
403
  setAgentSwitchMessage(null);
402
404
  send({
@@ -417,6 +419,126 @@ function useAgentSwitcherState(send) {
417
419
  };
418
420
  }
419
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
420
542
  //#region src/hooks/use-execution-detail.ts
421
543
  /**
422
544
  * #3288 §1: the detail sheet's own correlated request/response state — which entry is open, its
@@ -1101,7 +1223,8 @@ function toolEndFields(state) {
1101
1223
  ...state.diffLines ? { diffLines: state.diffLines } : {},
1102
1224
  ...state.diffFile ? { diffFile: state.diffFile } : {},
1103
1225
  ...state.toolResultData !== void 0 ? { toolResultData: state.toolResultData } : {},
1104
- ...state.toolResultParts ? { toolResultParts: state.toolResultParts } : {}
1226
+ ...state.toolResultParts ? { toolResultParts: state.toolResultParts } : {},
1227
+ ...state.appView ? { appView: state.appView } : {}
1105
1228
  };
1106
1229
  }
1107
1230
  /** Apply a `tool_end` to the ONE matching running tool across a turn's segments (never all of them). */
@@ -1199,6 +1322,7 @@ function buildConversationEntriesFromDisplaySegments(displaySegments) {
1199
1322
  }
1200
1323
  function useSessionClient(makeClient) {
1201
1324
  const [status, setStatus] = (0, react.useState)("disconnected");
1325
+ const statusRef = (0, react.useRef)("disconnected");
1202
1326
  const [messages, setMessages] = (0, react.useState)([]);
1203
1327
  const [activeTools, setActiveTools] = (0, react.useState)([]);
1204
1328
  const [streamingText, setStreamingText] = (0, react.useState)("");
@@ -1323,6 +1447,7 @@ function useSessionClient(makeClient) {
1323
1447
  const { handleExecutionDetailMessage, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
1324
1448
  const { handleModelListMessage, ...modelListState } = useModelListState(send);
1325
1449
  const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
1450
+ const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, (0, react.useCallback)(() => statusRef.current === "connected", []));
1326
1451
  const { handleSessionsMessage, canListSessions, markCurrent, armRestore, ...sessionDirectoryState } = useSessionDirectoryState(send);
1327
1452
  const { requestSessions, setSessionSidebarOpen } = sessionDirectoryState;
1328
1453
  const { handleSettingsMessage, openSettings, refreshSettings, ...settingsState } = useSettingsState(send);
@@ -1336,6 +1461,7 @@ function useSessionClient(makeClient) {
1336
1461
  if (handleExecutionDetailMessage(msg)) return;
1337
1462
  if (handleModelListMessage(msg)) return;
1338
1463
  if (handleProjectMessage(msg)) return;
1464
+ if (handleAppViewMessage(msg)) return;
1339
1465
  if (handleSessionsMessage(msg)) return;
1340
1466
  if (handleSettingsMessage(msg)) return;
1341
1467
  if (handleAgentDefinitionsMessage(msg)) return;
@@ -1472,6 +1598,7 @@ function useSessionClient(makeClient) {
1472
1598
  requestSessions();
1473
1599
  break;
1474
1600
  case "session_switched":
1601
+ rejectPendingAppRequests("The active session changed.");
1475
1602
  settleSessionChange();
1476
1603
  sessionStatusRef.current = null;
1477
1604
  setSessionStatus(null);
@@ -1496,6 +1623,7 @@ function useSessionClient(makeClient) {
1496
1623
  requestSchedules();
1497
1624
  break;
1498
1625
  case "history_cleared":
1626
+ rejectPendingAppRequests("The session history was cleared.");
1499
1627
  streamingTextRef.current = "";
1500
1628
  turnSegmentsRef.current = [];
1501
1629
  setStreamingText("");
@@ -1636,6 +1764,7 @@ function useSessionClient(makeClient) {
1636
1764
  closeExecutionDetail,
1637
1765
  finishTurn,
1638
1766
  handleAgentDefinitionsMessage,
1767
+ handleAppViewMessage,
1639
1768
  handleExecutionDetailMessage,
1640
1769
  handleModelListMessage,
1641
1770
  handleProjectMessage,
@@ -1650,6 +1779,7 @@ function useSessionClient(makeClient) {
1650
1779
  requestSchedules,
1651
1780
  requestSessions,
1652
1781
  resolveSwitchResult,
1782
+ rejectPendingAppRequests,
1653
1783
  send,
1654
1784
  setSessionSidebarOpen,
1655
1785
  settleSessionChange,
@@ -1668,7 +1798,9 @@ function useSessionClient(makeClient) {
1668
1798
  }, []);
1669
1799
  (0, react.useEffect)(() => {
1670
1800
  const onStatusChange = (next) => {
1801
+ statusRef.current = next;
1671
1802
  setStatus(next);
1803
+ if (next !== "connected") rejectPendingAppRequests();
1672
1804
  if (next === "connected") {
1673
1805
  commandsInFlightRef.current = 0;
1674
1806
  silentCommandRequestIdsRef.current.clear();
@@ -1692,13 +1824,15 @@ function useSessionClient(makeClient) {
1692
1824
  return () => {
1693
1825
  client.disconnect();
1694
1826
  clientRef.current = null;
1827
+ rejectPendingAppRequests();
1695
1828
  };
1696
1829
  }, [
1697
1830
  makeClient,
1698
1831
  handleMessage,
1699
1832
  requestSessions,
1700
1833
  requestSchedules,
1701
- armRestore
1834
+ armRestore,
1835
+ rejectPendingAppRequests
1702
1836
  ]);
1703
1837
  return {
1704
1838
  status,
@@ -1730,6 +1864,8 @@ function useSessionClient(makeClient) {
1730
1864
  openAgentSwitcher,
1731
1865
  ...agentSwitcherState,
1732
1866
  ...schedulesState,
1867
+ readAppView,
1868
+ callAppTool,
1733
1869
  helpOpen,
1734
1870
  openHelp,
1735
1871
  closeHelp
@@ -1920,6 +2056,338 @@ function driverAttributionText(driverId) {
1920
2056
  return `from ${humanDriverLabel(driverId)}`;
1921
2057
  }
1922
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
1923
2391
  //#region src/components/DiffLines.tsx
1924
2392
  /**
1925
2393
  * #3288: renders a unified diff already built server-side (the ONE diff builder, in
@@ -2433,42 +2901,49 @@ function ToolGroup({ tools }) {
2433
2901
  if (visible.length === 0) return null;
2434
2902
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2435
2903
  className: "text-[14px]",
2436
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2437
- type: "button",
2438
- "aria-expanded": open,
2439
- onClick: () => setOpen((value) => !value),
2440
- 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",
2441
- children: [
2442
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Wrench, {
2443
- size: 15,
2444
- strokeWidth: 1.75,
2445
- className: "flex-shrink-0 text-subtle"
2446
- }),
2447
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2448
- className: "flex-shrink-0",
2449
- children: [
2450
- visible.length,
2451
- " tool ",
2452
- visible.length === 1 ? "call" : "calls"
2453
- ]
2454
- }),
2455
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2456
- className: "min-w-0 truncate text-subtle",
2457
- children: names
2458
- }),
2459
- failed > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2460
- className: "flex-shrink-0 rounded-md bg-destructive/12 px-1.5 text-[12.5px] text-destructive",
2461
- children: [failed, " failed"]
2462
- }),
2463
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
2464
- size: 14,
2465
- className: `flex-shrink-0 text-subtle transition-transform ${open ? "rotate-90" : ""}`
2466
- })
2467
- ]
2468
- }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2469
- className: "mt-0.5 flex flex-col pl-[25px]",
2470
- children: visible.map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCard, { tool }, tool.id))
2471
- })]
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
+ ]
2472
2947
  });
2473
2948
  }
2474
2949
  /** #3288: a turn that changed files ends with this compact row — a click opens that file's diff. */
@@ -2594,7 +3069,10 @@ function ConversationView({ messages, activeTools, streamingText, isThinking, ow
2594
3069
  isThinking && !streamingText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThinkingIndicator, {}),
2595
3070
  activeTools.some((tool) => !tool.internal) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2596
3071
  className: "flex flex-col",
2597
- 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))
2598
3076
  }),
2599
3077
  streamingText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentBlock, {
2600
3078
  content: streamingText,
@@ -3237,6 +3715,26 @@ function isEditableElement(element) {
3237
3715
  function isDestructiveAskOption(requestId, value) {
3238
3716
  return requestId === "provider-profile-action" && value === "delete";
3239
3717
  }
3718
+ /**
3719
+ * The multi-select shape of an ask, or undefined for every other ask (which answers on click). The
3720
+ * rules mirror the terminal checklist (`MultiSelectList` in agent-ui-terminal): `minSelect` (default 1)
3721
+ * gates the submit, and `maxSelect` caps the ticks — a toggle beyond the cap is ignored, never swapped
3722
+ * for the oldest tick. `default.values` pre-select; a default that names no option is dropped, and
3723
+ * more defaults than the cap keep the first ones in option order, so the answer never breaks the cap.
3724
+ */
3725
+ function readMultiSelect(request) {
3726
+ const max = request.maxSelect ?? 1;
3727
+ if (max <= 1) return void 0;
3728
+ const options = (request.options ?? []).filter((opt) => !isDestructiveAskOption(request.id, opt.value));
3729
+ const wanted = new Set(request.default?.values ?? []);
3730
+ const defaults = options.filter((opt) => wanted.has(opt.value)).slice(0, max);
3731
+ return {
3732
+ min: request.minSelect ?? 1,
3733
+ max,
3734
+ options,
3735
+ defaults: new Set(defaults.map((opt) => opt.value))
3736
+ };
3737
+ }
3240
3738
  function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverId = null, layout = "modal", armDelayMs = 450, onFocusReturn }) {
3241
3739
  const prompt = prompts[0];
3242
3740
  const promptId = prompt?.id;
@@ -3248,6 +3746,7 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
3248
3746
  const [fieldFocused, setFieldFocused] = (0, react.useState)(false);
3249
3747
  const fieldHandleRef = (0, react.useRef)(null);
3250
3748
  const pendingFocusReturnRef = (0, react.useRef)(false);
3749
+ const [selection, setSelection] = (0, react.useState)(void 0);
3251
3750
  (0, react.useEffect)(() => {
3252
3751
  if (promptId === void 0 || armDelayMs <= 0) return void 0;
3253
3752
  const timer = setTimeout(() => setArmedId(promptId), armDelayMs);
@@ -3312,6 +3811,28 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
3312
3811
  fieldHandleRef.current?.clear();
3313
3812
  onAnswerAsk(id, response);
3314
3813
  };
3814
+ const multi = prompt.kind === "ask" ? readMultiSelect(prompt.request) : void 0;
3815
+ const ticks = selection?.promptId === prompt.id ? selection.values : multi?.defaults;
3816
+ const selectedValues = multi && ticks ? multi.options.map((opt) => opt.value).filter((value) => ticks.has(value)) : [];
3817
+ const canSubmit = multi !== void 0 && selectedValues.length >= multi.min;
3818
+ const toggleOption = (value) => {
3819
+ if (!multi) return;
3820
+ setSelection((current) => {
3821
+ const next = new Set(current?.promptId === prompt.id ? current.values : multi.defaults);
3822
+ if (next.has(value)) next.delete(value);
3823
+ else if (next.size < multi.max) next.add(value);
3824
+ return {
3825
+ promptId: prompt.id,
3826
+ values: next
3827
+ };
3828
+ });
3829
+ };
3830
+ const submitSelection = () => {
3831
+ if (canSubmit) answerAsk(prompt.id, {
3832
+ type: "answer",
3833
+ values: selectedValues
3834
+ });
3835
+ };
3315
3836
  /** A click anywhere in the prompt that isn't on a button or the field itself focuses the field. */
3316
3837
  const onContainerClick = (event) => {
3317
3838
  if (!hasFreeText) return;
@@ -3331,6 +3852,13 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
3331
3852
  return;
3332
3853
  }
3333
3854
  if (!armed) return;
3855
+ if (multi && event.key === "Enter") {
3856
+ if (event.target === event.currentTarget) {
3857
+ event.preventDefault();
3858
+ submitSelection();
3859
+ }
3860
+ return;
3861
+ }
3334
3862
  const index = Number(event.key) - 1;
3335
3863
  if (!Number.isInteger(index) || index < 0) return;
3336
3864
  if (prompt.kind === "permission") {
@@ -3338,7 +3866,9 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
3338
3866
  return;
3339
3867
  }
3340
3868
  const option = (prompt.request.options ?? []).filter((opt) => !isDestructiveAskOption(prompt.request.id, opt.value))[index];
3341
- if (option) answerAsk(prompt.id, {
3869
+ if (!option) return;
3870
+ if (multi) toggleOption(option.value);
3871
+ else answerAsk(prompt.id, {
3342
3872
  type: "answer",
3343
3873
  values: [option.value]
3344
3874
  });
@@ -3350,7 +3880,7 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
3350
3880
  const askOptions = prompt.kind === "ask" ? prompt.request.options ?? [] : [];
3351
3881
  const destructiveAskOption = askOptions.find((opt) => isDestructiveAskOption(askRequestId, opt.value));
3352
3882
  const primaryAskOptions = destructiveAskOption ? askOptions.filter((opt) => opt !== destructiveAskOption) : askOptions;
3353
- const askArmedHint = hasFreeText && fieldFocused ? "Enter to submit · Esc to cancel" : askOptions.length > 0 ? "1–9 choose · Esc cancel" : "Esc cancels";
3883
+ const askArmedHint = hasFreeText && fieldFocused ? "Enter to submit · Esc to cancel" : multi && askOptions.length > 0 ? "1–9 toggle · Enter submit · Esc cancel" : askOptions.length > 0 ? "1–9 choose · Esc cancel" : "Esc cancels";
3354
3884
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3355
3885
  className: dock ? "agent-ui gui-rise flex-shrink-0" : "agent-ui fixed inset-0 z-50 flex items-center justify-center bg-black/55 p-4 backdrop-blur-[2px]",
3356
3886
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -3502,12 +4032,23 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
3502
4032
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3503
4033
  type: "button",
3504
4034
  "aria-keyshortcuts": dock && index < 9 ? String(index + 1) : void 0,
3505
- className: index === 0 ? PRIMARY_BUTTON$1 : SECONDARY_BUTTON$1,
3506
- onClick: onButton(() => answerAsk(prompt.id, {
4035
+ "aria-pressed": multi ? selectedValues.includes(opt.value) : void 0,
4036
+ className: (multi ? selectedValues.includes(opt.value) : index === 0) ? PRIMARY_BUTTON$1 : SECONDARY_BUTTON$1,
4037
+ onClick: onButton(() => multi ? toggleOption(opt.value) : answerAsk(prompt.id, {
3507
4038
  type: "answer",
3508
4039
  values: [opt.value]
3509
4040
  })),
3510
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: opt.label }), dock && index < 9 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Kbd, { children: index + 1 })]
4041
+ children: [
4042
+ multi && (selectedValues.includes(opt.value) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.SquareCheck, {
4043
+ size: 15,
4044
+ strokeWidth: 2
4045
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Square, {
4046
+ size: 15,
4047
+ strokeWidth: 2
4048
+ })),
4049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: opt.label }),
4050
+ dock && index < 9 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Kbd, { children: index + 1 })
4051
+ ]
3511
4052
  }), opt.description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3512
4053
  className: "pl-0.5 text-[11.5px] leading-snug text-subtle",
3513
4054
  children: opt.description
@@ -3549,16 +4090,26 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
3549
4090
  }),
3550
4091
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3551
4092
  className: "flex flex-wrap items-center gap-2",
3552
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3553
- type: "button",
3554
- className: GHOST_BUTTON,
3555
- onClick: onButton(() => answerAsk(prompt.id, { type: "cancelled" })),
3556
- children: "Cancel"
3557
- }), dock && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyHint, {
3558
- hasFocus,
3559
- armed,
3560
- armedText: askArmedHint
3561
- })]
4093
+ children: [
4094
+ multi && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4095
+ type: "button",
4096
+ className: `${PRIMARY_BUTTON$1} disabled:opacity-60`,
4097
+ disabled: !canSubmit,
4098
+ onClick: onButton(submitSelection),
4099
+ children: canSubmit ? "Submit" : `Submit (min ${multi.min})`
4100
+ }),
4101
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4102
+ type: "button",
4103
+ className: GHOST_BUTTON,
4104
+ onClick: onButton(() => answerAsk(prompt.id, { type: "cancelled" })),
4105
+ children: "Cancel"
4106
+ }),
4107
+ dock && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyHint, {
4108
+ hasFocus,
4109
+ armed,
4110
+ armedText: askArmedHint
4111
+ })
4112
+ ]
3562
4113
  })
3563
4114
  ]
3564
4115
  })] })
@@ -7461,6 +8012,7 @@ const NARROW_WINDOW_QUERY = "(max-width: 767.98px)";
7461
8012
  */
7462
8013
  /** The column every part of the conversation shares, so messages, prompt and composer line up. */
7463
8014
  const COLUMN = "mx-auto w-full max-w-[760px] px-6";
8015
+ const APP_VIEW_HOST_VERSION = "1.0.0";
7464
8016
  /**
7465
8017
  * Designed empty state shown before the first turn. Renders in the same slot as `ConversationView`
7466
8018
  * (never together), so it carries the same `main` landmark and label (#3289 §3 review) — nothing
@@ -7527,8 +8079,25 @@ function SetupPanel({ onSetUp, disabled }) {
7527
8079
  * The full desktop layout over an `IWsSessionState`. `surface` is an optional label shown next to the
7528
8080
  * mark (e.g. "app").
7529
8081
  */
7530
- function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor }) {
8082
+ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
7531
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
+ ]);
7532
8101
  const composerRef = (0, react.useRef)(null);
7533
8102
  const tasks = state.executionWorkspace?.entries ?? [];
7534
8103
  const hasTasks = tasks.some((entry) => entry.kind !== "main_thread");
@@ -7657,12 +8226,15 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
7657
8226
  args: "add"
7658
8227
  }),
7659
8228
  disabled: state.status !== "connected"
7660
- }) : isEmpty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationView, {
7661
- messages: state.messages,
7662
- activeTools: state.activeTools,
7663
- streamingText: state.streamingText,
7664
- isThinking: state.isThinking,
7665
- 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
+ })
7666
8238
  })
7667
8239
  }),
7668
8240
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {