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

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";
@@ -256,8 +256,8 @@ const SERVER_MESSAGE_HANDLING = {
256
256
  command_result: "reducer-state",
257
257
  messages: "reducer-state",
258
258
  history: "intentionally-not-rendered",
259
- history_changed: "intentionally-not-rendered",
260
- context: "intentionally-not-rendered",
259
+ history_changed: "visible-notice",
260
+ context: "reducer-state",
261
261
  turn_source: "intentionally-not-rendered",
262
262
  commands: "reducer-state",
263
263
  session_status: "reducer-state",
@@ -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);
@@ -1306,12 +1429,16 @@ function useSessionClient(makeClient) {
1306
1429
  settingsOpenRef.current = settingsState.settingsOpen;
1307
1430
  const { handleAgentDefinitionsMessage, resolveSwitchResult, openAgentSwitcher, ...agentSwitcherState } = useAgentSwitcherState(send);
1308
1431
  const { handleSchedulesMessage, requestSchedules, ...schedulesState } = useSchedulesState(send);
1432
+ const clearCompactionNotices = useCallback(() => {
1433
+ setSessionNotices((previous) => previous.filter((notice) => notice.kind !== "context-compacted"));
1434
+ }, []);
1309
1435
  const handleMessage = useCallback((msg) => {
1310
1436
  SERVER_MESSAGE_HANDLING[msg.type];
1311
1437
  if (handleUsageMessage(msg)) return;
1312
1438
  if (handleExecutionDetailMessage(msg)) return;
1313
1439
  if (handleModelListMessage(msg)) return;
1314
1440
  if (handleProjectMessage(msg)) return;
1441
+ if (handleAppViewMessage(msg)) return;
1315
1442
  if (handleSessionsMessage(msg)) return;
1316
1443
  if (handleSettingsMessage(msg)) return;
1317
1444
  if (handleAgentDefinitionsMessage(msg)) return;
@@ -1448,9 +1575,11 @@ function useSessionClient(makeClient) {
1448
1575
  requestSessions();
1449
1576
  break;
1450
1577
  case "session_switched":
1578
+ rejectPendingAppRequests("The active session changed.");
1451
1579
  settleSessionChange();
1452
1580
  sessionStatusRef.current = null;
1453
1581
  setSessionStatus(null);
1582
+ clearCompactionNotices();
1454
1583
  streamingTextRef.current = "";
1455
1584
  turnSegmentsRef.current = [];
1456
1585
  setStreamingText("");
@@ -1472,6 +1601,8 @@ function useSessionClient(makeClient) {
1472
1601
  requestSchedules();
1473
1602
  break;
1474
1603
  case "history_cleared":
1604
+ clearCompactionNotices();
1605
+ rejectPendingAppRequests("The session history was cleared.");
1475
1606
  streamingTextRef.current = "";
1476
1607
  turnSegmentsRef.current = [];
1477
1608
  setStreamingText("");
@@ -1557,6 +1688,25 @@ function useSessionClient(makeClient) {
1557
1688
  setCommandCatalog(catalog);
1558
1689
  break;
1559
1690
  }
1691
+ case "context":
1692
+ if (sessionStatusRef.current !== null) {
1693
+ const next = {
1694
+ ...sessionStatusRef.current,
1695
+ context: msg.state
1696
+ };
1697
+ sessionStatusRef.current = next;
1698
+ setSessionStatus(next);
1699
+ }
1700
+ break;
1701
+ case "history_changed": {
1702
+ const compact = msg.compact;
1703
+ if (compact?.trigger === "auto") setSessionNotices((previous) => [...previous, {
1704
+ id: nextId(),
1705
+ kind: "context-compacted",
1706
+ message: `Auto compacted context: ${Math.round(compact.beforeUsedPercentage)}% → ${Math.round(compact.afterUsedPercentage)}%`
1707
+ }]);
1708
+ break;
1709
+ }
1560
1710
  case "session_status":
1561
1711
  sessionStatusRef.current = msg.status;
1562
1712
  setSessionStatus(msg.status);
@@ -1610,8 +1760,10 @@ function useSessionClient(makeClient) {
1610
1760
  appendEntry,
1611
1761
  canListSessions,
1612
1762
  closeExecutionDetail,
1763
+ clearCompactionNotices,
1613
1764
  finishTurn,
1614
1765
  handleAgentDefinitionsMessage,
1766
+ handleAppViewMessage,
1615
1767
  handleExecutionDetailMessage,
1616
1768
  handleModelListMessage,
1617
1769
  handleProjectMessage,
@@ -1626,6 +1778,7 @@ function useSessionClient(makeClient) {
1626
1778
  requestSchedules,
1627
1779
  requestSessions,
1628
1780
  resolveSwitchResult,
1781
+ rejectPendingAppRequests,
1629
1782
  send,
1630
1783
  setSessionSidebarOpen,
1631
1784
  settleSessionChange,
@@ -1644,7 +1797,12 @@ function useSessionClient(makeClient) {
1644
1797
  }, []);
1645
1798
  useEffect(() => {
1646
1799
  const onStatusChange = (next) => {
1800
+ statusRef.current = next;
1647
1801
  setStatus(next);
1802
+ if (next !== "connected") {
1803
+ rejectPendingAppRequests();
1804
+ clearCompactionNotices();
1805
+ }
1648
1806
  if (next === "connected") {
1649
1807
  commandsInFlightRef.current = 0;
1650
1808
  silentCommandRequestIdsRef.current.clear();
@@ -1668,13 +1826,16 @@ function useSessionClient(makeClient) {
1668
1826
  return () => {
1669
1827
  client.disconnect();
1670
1828
  clientRef.current = null;
1829
+ rejectPendingAppRequests();
1671
1830
  };
1672
1831
  }, [
1673
1832
  makeClient,
1674
1833
  handleMessage,
1834
+ clearCompactionNotices,
1675
1835
  requestSessions,
1676
1836
  requestSchedules,
1677
- armRestore
1837
+ armRestore,
1838
+ rejectPendingAppRequests
1678
1839
  ]);
1679
1840
  return {
1680
1841
  status,
@@ -1706,6 +1867,8 @@ function useSessionClient(makeClient) {
1706
1867
  openAgentSwitcher,
1707
1868
  ...agentSwitcherState,
1708
1869
  ...schedulesState,
1870
+ readAppView,
1871
+ callAppTool,
1709
1872
  helpOpen,
1710
1873
  openHelp,
1711
1874
  closeHelp
@@ -1896,6 +2059,338 @@ function driverAttributionText(driverId) {
1896
2059
  return `from ${humanDriverLabel(driverId)}`;
1897
2060
  }
1898
2061
  //#endregion
2062
+ //#region src/app-view/call-tool-result.ts
2063
+ function isRecord(value) {
2064
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2065
+ }
2066
+ function contentFromParts(parts) {
2067
+ const content = [];
2068
+ for (const part of parts ?? []) if (part.type === "text") content.push({
2069
+ type: "text",
2070
+ text: part.text
2071
+ });
2072
+ else if (part.type === "image_inline") content.push({
2073
+ type: "image",
2074
+ data: part.data,
2075
+ mimeType: part.mimeType
2076
+ });
2077
+ else if (part.type === "audio_inline") content.push({
2078
+ type: "audio",
2079
+ data: part.data,
2080
+ mimeType: part.mimeType
2081
+ });
2082
+ return content;
2083
+ }
2084
+ function outcomeToCallToolResult(outcome) {
2085
+ let content = contentFromParts(outcome.parts);
2086
+ if (content.length === 0 && typeof outcome.data === "string") content = [{
2087
+ type: "text",
2088
+ text: outcome.data
2089
+ }];
2090
+ if (!outcome.success && outcome.error !== void 0) content = [...content, {
2091
+ type: "text",
2092
+ text: outcome.error
2093
+ }];
2094
+ return {
2095
+ content,
2096
+ ...isRecord(outcome.data) ? { structuredContent: outcome.data } : {},
2097
+ isError: !outcome.success
2098
+ };
2099
+ }
2100
+ function toCallToolResult(tool, view) {
2101
+ let content = contentFromParts(tool.toolResultParts);
2102
+ if (content.length === 0 && tool.toolResultData !== void 0) content = [{
2103
+ type: "text",
2104
+ text: tool.toolResultData
2105
+ }];
2106
+ return {
2107
+ content,
2108
+ ...isRecord(view.structuredContent) ? { structuredContent: view.structuredContent } : {},
2109
+ isError: tool.status === "error"
2110
+ };
2111
+ }
2112
+ //#endregion
2113
+ //#region src/app-view/bridge.ts
2114
+ const APP_VIEW_PROTOCOL_VERSION = "2026-01-26";
2115
+ const INVALID_PARAMS = -32602;
2116
+ const METHOD_NOT_FOUND = -32601;
2117
+ const HOST_ERROR = -32e3;
2118
+ const SANDBOX_PROXY_READY = "ui/notifications/sandbox-proxy-ready";
2119
+ const SANDBOX_RESOURCE_READY = "ui/notifications/sandbox-resource-ready";
2120
+ function isPlainObject(value) {
2121
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2122
+ const prototype = Object.getPrototypeOf(value);
2123
+ return prototype === null || Object.getPrototypeOf(prototype) === null;
2124
+ }
2125
+ function errorMessage(error) {
2126
+ return error instanceof Error ? error.message : String(error);
2127
+ }
2128
+ function createAppViewBridge(options) {
2129
+ const { view } = options;
2130
+ let disposed = false;
2131
+ let sandboxAnswered = false;
2132
+ let initialization = "uninitialized";
2133
+ let callInFlight = false;
2134
+ const respond = (id, result) => {
2135
+ options.post({
2136
+ jsonrpc: "2.0",
2137
+ id,
2138
+ result
2139
+ });
2140
+ };
2141
+ const refuse = (id, code, message) => {
2142
+ options.post({
2143
+ jsonrpc: "2.0",
2144
+ id,
2145
+ error: {
2146
+ code,
2147
+ message
2148
+ }
2149
+ });
2150
+ };
2151
+ const callTool = (id, params) => {
2152
+ if (initialization !== "ready") {
2153
+ refuse(id, HOST_ERROR, "This app view has not initialized.");
2154
+ return;
2155
+ }
2156
+ const name = isPlainObject(params) ? params["name"] : void 0;
2157
+ const runtimeName = typeof name === "string" && Object.hasOwn(view.callableTools, name) ? view.callableTools[name] : void 0;
2158
+ if (!isPlainObject(params) || runtimeName === void 0) {
2159
+ refuse(id, INVALID_PARAMS, "This tool is not available to this view.");
2160
+ return;
2161
+ }
2162
+ const rawArguments = params["arguments"] === void 0 ? {} : params["arguments"];
2163
+ if (!isPlainObject(rawArguments)) {
2164
+ refuse(id, INVALID_PARAMS, "Tool arguments must be an object.");
2165
+ return;
2166
+ }
2167
+ if (callInFlight) {
2168
+ refuse(id, HOST_ERROR, "Another tool call from this view is still running.");
2169
+ return;
2170
+ }
2171
+ callInFlight = true;
2172
+ let outcome;
2173
+ try {
2174
+ outcome = options.callAppTool(runtimeName, rawArguments);
2175
+ } catch (error) {
2176
+ outcome = Promise.reject(error);
2177
+ }
2178
+ outcome.then((settled) => {
2179
+ callInFlight = false;
2180
+ if (!disposed) respond(id, outcomeToCallToolResult(settled));
2181
+ }, (error) => {
2182
+ callInFlight = false;
2183
+ if (!disposed) refuse(id, HOST_ERROR, errorMessage(error));
2184
+ });
2185
+ };
2186
+ const handleRequest = (id, method, params) => {
2187
+ switch (method) {
2188
+ case "ui/initialize":
2189
+ if (initialization === "uninitialized") initialization = "initializing";
2190
+ respond(id, {
2191
+ protocolVersion: APP_VIEW_PROTOCOL_VERSION,
2192
+ hostCapabilities: { serverTools: {} },
2193
+ hostInfo: options.hostInfo,
2194
+ hostContext: options.hostContext
2195
+ });
2196
+ return;
2197
+ case "tools/call":
2198
+ callTool(id, params);
2199
+ return;
2200
+ case "ping":
2201
+ respond(id, {});
2202
+ return;
2203
+ case "ui/request-display-mode":
2204
+ respond(id, { mode: "inline" });
2205
+ return;
2206
+ default: refuse(id, METHOD_NOT_FOUND, `This host does not support ${method}.`);
2207
+ }
2208
+ };
2209
+ const handleNotification = (method, params) => {
2210
+ switch (method) {
2211
+ case SANDBOX_PROXY_READY:
2212
+ if (sandboxAnswered) return;
2213
+ sandboxAnswered = true;
2214
+ options.post({
2215
+ jsonrpc: "2.0",
2216
+ method: SANDBOX_RESOURCE_READY,
2217
+ params: { html: view.html }
2218
+ });
2219
+ return;
2220
+ case "ui/notifications/initialized":
2221
+ if (initialization !== "initializing") return;
2222
+ initialization = "ready";
2223
+ options.post({
2224
+ jsonrpc: "2.0",
2225
+ method: "ui/notifications/tool-input",
2226
+ params: { arguments: view.toolInput ?? {} }
2227
+ });
2228
+ options.post({
2229
+ jsonrpc: "2.0",
2230
+ method: "ui/notifications/tool-result",
2231
+ params: options.toolResult
2232
+ });
2233
+ return;
2234
+ case "ui/notifications/size-changed": {
2235
+ const height = isPlainObject(params) ? params["height"] : void 0;
2236
+ if (typeof height !== "number" || !Number.isFinite(height)) return;
2237
+ options.onHeight(Math.min(640, Math.max(48, Math.round(height))));
2238
+ return;
2239
+ }
2240
+ default: return;
2241
+ }
2242
+ };
2243
+ return {
2244
+ handleMessage(event) {
2245
+ if (disposed) return;
2246
+ const frame = options.source();
2247
+ if (frame === null || event.source !== frame) return;
2248
+ const data = event.data;
2249
+ if (!isPlainObject(data) || data["jsonrpc"] !== "2.0") return;
2250
+ const { method, id } = data;
2251
+ if (typeof method !== "string") return;
2252
+ if (id === void 0) handleNotification(method, data["params"]);
2253
+ else if (typeof id === "string" || typeof id === "number") handleRequest(id, method, data["params"]);
2254
+ },
2255
+ dispose() {
2256
+ if (disposed) return;
2257
+ disposed = true;
2258
+ if (initialization === "ready") options.post({
2259
+ jsonrpc: "2.0",
2260
+ id: "teardown",
2261
+ method: "ui/resource-teardown",
2262
+ params: { reason: "closed" }
2263
+ });
2264
+ }
2265
+ };
2266
+ }
2267
+ //#endregion
2268
+ //#region src/app-view/host-context.ts
2269
+ const AppViewHostContext = createContext(null);
2270
+ function readAppViewHostContext(platform) {
2271
+ return {
2272
+ theme: typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
2273
+ displayMode: "inline",
2274
+ availableDisplayModes: ["inline"],
2275
+ containerDimensions: { maxHeight: 640 },
2276
+ platform,
2277
+ locale: typeof navigator === "undefined" ? "en-US" : navigator.language,
2278
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
2279
+ };
2280
+ }
2281
+ //#endregion
2282
+ //#region src/components/AppView.tsx
2283
+ const INITIAL_HEIGHT_PX = 160;
2284
+ const APP_VIEW_UNAVAILABLE_TEXT = "This tool returned an app view, which this window cannot show.";
2285
+ function AppView({ tool }) {
2286
+ const host = useContext(AppViewHostContext);
2287
+ if (tool.appView === void 0) return null;
2288
+ const viewKey = appViewKey(tool);
2289
+ if (host === null || host.sandboxUrl === void 0 || viewKey === void 0) return /* @__PURE__ */ jsx("p", {
2290
+ className: "text-[13px] text-subtle",
2291
+ children: APP_VIEW_UNAVAILABLE_TEXT
2292
+ });
2293
+ return /* @__PURE__ */ jsx(AppViewFrame, {
2294
+ tool,
2295
+ host,
2296
+ sandboxUrl: host.sandboxUrl,
2297
+ viewKey,
2298
+ sourceId: tool.appView.sourceId
2299
+ }, viewKey);
2300
+ }
2301
+ function AppViewFrame({ tool, host, sandboxUrl, viewKey, sourceId }) {
2302
+ const [load, setLoad] = useState({ status: "loading" });
2303
+ const [height, setHeight] = useState(INITIAL_HEIGHT_PX);
2304
+ const frameRef = useRef(null);
2305
+ const toolRef = useRef(tool);
2306
+ toolRef.current = tool;
2307
+ useEffect(() => {
2308
+ const controller = new AbortController();
2309
+ setLoad({ status: "loading" });
2310
+ host.readAppView(viewKey, controller.signal).then((view) => {
2311
+ if (!controller.signal.aborted) setLoad({
2312
+ status: "ready",
2313
+ view,
2314
+ host,
2315
+ sandboxUrl,
2316
+ signal: controller.signal
2317
+ });
2318
+ }, (error) => {
2319
+ if (!controller.signal.aborted) setLoad({
2320
+ status: "failed",
2321
+ reason: error instanceof Error ? error.message : String(error)
2322
+ });
2323
+ });
2324
+ return () => {
2325
+ controller.abort();
2326
+ };
2327
+ }, [
2328
+ viewKey,
2329
+ host.readAppView,
2330
+ host.callAppTool,
2331
+ sandboxUrl
2332
+ ]);
2333
+ const ready = load.status === "ready" && load.host.readAppView === host.readAppView && load.host.callAppTool === host.callAppTool && load.sandboxUrl === sandboxUrl ? load : null;
2334
+ useLayoutEffect(() => {
2335
+ const frame = frameRef.current;
2336
+ if (ready === null || frame === null) return void 0;
2337
+ const current = ready.host;
2338
+ const view = ready.view;
2339
+ const bridge = createAppViewBridge({
2340
+ view,
2341
+ toolResult: toCallToolResult(toolRef.current, view),
2342
+ hostInfo: current.hostInfo,
2343
+ hostContext: readAppViewHostContext(current.platform),
2344
+ post: (message) => frame.contentWindow?.postMessage(message, "*"),
2345
+ source: () => frame.contentWindow,
2346
+ callAppTool: (name, args) => current.callAppTool(viewKey, name, args, ready.signal),
2347
+ onHeight: setHeight
2348
+ });
2349
+ const onMessage = (event) => bridge.handleMessage(event);
2350
+ window.addEventListener("message", onMessage);
2351
+ return () => {
2352
+ window.removeEventListener("message", onMessage);
2353
+ bridge.dispose();
2354
+ };
2355
+ }, [ready, viewKey]);
2356
+ if (load.status === "failed") return /* @__PURE__ */ jsxs("p", {
2357
+ className: "text-[13px] text-subtle",
2358
+ children: [
2359
+ "The app from ",
2360
+ sourceId,
2361
+ " could not be shown: ",
2362
+ load.reason
2363
+ ]
2364
+ });
2365
+ if (ready === null) return /* @__PURE__ */ jsxs("p", {
2366
+ className: "text-[13px] text-subtle",
2367
+ children: [
2368
+ "Loading the app from ",
2369
+ sourceId,
2370
+ "…"
2371
+ ]
2372
+ });
2373
+ return /* @__PURE__ */ jsxs("div", {
2374
+ className: "my-1 flex flex-col gap-1",
2375
+ children: [/* @__PURE__ */ jsxs("p", {
2376
+ className: "text-[12px] text-subtle",
2377
+ children: ["App from ", sourceId]
2378
+ }), /* @__PURE__ */ jsx("iframe", {
2379
+ ref: frameRef,
2380
+ sandbox: "allow-scripts",
2381
+ src: sandboxUrl,
2382
+ title: `App from ${sourceId}`,
2383
+ referrerPolicy: "no-referrer",
2384
+ "data-testid": "app-view-frame",
2385
+ style: {
2386
+ width: "100%",
2387
+ height
2388
+ },
2389
+ className: "block rounded-lg border border-border bg-card"
2390
+ })]
2391
+ });
2392
+ }
2393
+ //#endregion
1899
2394
  //#region src/components/DiffLines.tsx
1900
2395
  /**
1901
2396
  * #3288: renders a unified diff already built server-side (the ONE diff builder, in
@@ -2409,42 +2904,49 @@ function ToolGroup({ tools }) {
2409
2904
  if (visible.length === 0) return null;
2410
2905
  return /* @__PURE__ */ jsxs("div", {
2411
2906
  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
- })]
2907
+ children: [
2908
+ /* @__PURE__ */ jsxs("button", {
2909
+ type: "button",
2910
+ "aria-expanded": open,
2911
+ onClick: () => setOpen((value) => !value),
2912
+ 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",
2913
+ children: [
2914
+ /* @__PURE__ */ jsx(Wrench, {
2915
+ size: 15,
2916
+ strokeWidth: 1.75,
2917
+ className: "flex-shrink-0 text-subtle"
2918
+ }),
2919
+ /* @__PURE__ */ jsxs("span", {
2920
+ className: "flex-shrink-0",
2921
+ children: [
2922
+ visible.length,
2923
+ " tool ",
2924
+ visible.length === 1 ? "call" : "calls"
2925
+ ]
2926
+ }),
2927
+ /* @__PURE__ */ jsx("span", {
2928
+ className: "min-w-0 truncate text-subtle",
2929
+ children: names
2930
+ }),
2931
+ failed > 0 && /* @__PURE__ */ jsxs("span", {
2932
+ className: "flex-shrink-0 rounded-md bg-destructive/12 px-1.5 text-[12.5px] text-destructive",
2933
+ children: [failed, " failed"]
2934
+ }),
2935
+ /* @__PURE__ */ jsx(ChevronRight, {
2936
+ size: 14,
2937
+ className: `flex-shrink-0 text-subtle transition-transform ${open ? "rotate-90" : ""}`
2938
+ })
2939
+ ]
2940
+ }),
2941
+ visible.filter((tool) => tool.appView !== void 0).map((tool) => /* @__PURE__ */ jsx("div", {
2942
+ className: "pl-[25px]",
2943
+ children: /* @__PURE__ */ jsx(AppView, { tool })
2944
+ }, tool.id)),
2945
+ open && /* @__PURE__ */ jsx("div", {
2946
+ className: "mt-0.5 flex flex-col pl-[25px]",
2947
+ children: visible.map((tool) => /* @__PURE__ */ jsx(ToolCard, { tool }, tool.id))
2948
+ })
2949
+ ]
2448
2950
  });
2449
2951
  }
2450
2952
  /** #3288: a turn that changed files ends with this compact row — a click opens that file's diff. */
@@ -2570,7 +3072,10 @@ function ConversationView({ messages, activeTools, streamingText, isThinking, ow
2570
3072
  isThinking && !streamingText && /* @__PURE__ */ jsx(ThinkingIndicator, {}),
2571
3073
  activeTools.some((tool) => !tool.internal) && /* @__PURE__ */ jsx("div", {
2572
3074
  className: "flex flex-col",
2573
- children: activeTools.filter((tool) => !tool.internal).map((tool) => /* @__PURE__ */ jsx(ToolCard, { tool }, tool.id))
3075
+ 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", {
3076
+ className: "pl-[25px]",
3077
+ children: /* @__PURE__ */ jsx(AppView, { tool })
3078
+ })] }, tool.id))
2574
3079
  }),
2575
3080
  streamingText && /* @__PURE__ */ jsx(AgentBlock, {
2576
3081
  content: streamingText,
@@ -6438,7 +6943,7 @@ function noticeSentence(notice) {
6438
6943
  }
6439
6944
  }
6440
6945
  /**
6441
- * Session and protocol failures, as dismissible toasts over the top-right corner: they stay until
6946
+ * Session outcomes and protocol failures, as dismissible toasts over the top-right corner: they stay until
6442
6947
  * dismissed, and never push the conversation or the composer out of view. Command output is not
6443
6948
  * here — it belongs to the conversation. A provider error classified at the wire boundary shows one
6444
6949
  * plain sentence with the next step; its raw text sits behind "Details" rather than as the headline.
@@ -6450,11 +6955,12 @@ function SessionNotices({ state }) {
6450
6955
  className: "pointer-events-none absolute right-4 top-16 z-40 flex w-[min(400px,calc(100%-32px))] flex-col gap-2",
6451
6956
  children: notices.slice(-3).map((notice) => {
6452
6957
  const sentence = noticeSentence(notice);
6958
+ const compacted = notice.kind === "context-compacted";
6453
6959
  return /* @__PURE__ */ jsxs("div", {
6454
- role: "alert",
6960
+ role: compacted ? "status" : "alert",
6455
6961
  className: "gui-rise pointer-events-auto flex items-start gap-3 rounded-xl bg-popover px-4 py-3 text-[14px] leading-snug text-popover-foreground shadow-xl shadow-black/30",
6456
6962
  children: [
6457
- /* @__PURE__ */ jsx("span", { className: "mt-[7px] h-2 w-2 flex-shrink-0 rounded-full bg-destructive" }),
6963
+ /* @__PURE__ */ jsx("span", { className: `mt-[7px] h-2 w-2 flex-shrink-0 rounded-full ${compacted ? "bg-primary" : "bg-destructive"}` }),
6458
6964
  /* @__PURE__ */ jsxs("span", {
6459
6965
  className: "max-h-40 flex-1 overflow-y-auto whitespace-pre-wrap break-words",
6460
6966
  children: [sentence ?? notice.message, sentence && /* @__PURE__ */ jsxs("details", {
@@ -7510,6 +8016,7 @@ const NARROW_WINDOW_QUERY = "(max-width: 767.98px)";
7510
8016
  */
7511
8017
  /** The column every part of the conversation shares, so messages, prompt and composer line up. */
7512
8018
  const COLUMN = "mx-auto w-full max-w-[760px] px-6";
8019
+ const APP_VIEW_HOST_VERSION = "1.0.0";
7513
8020
  /**
7514
8021
  * Designed empty state shown before the first turn. Renders in the same slot as `ConversationView`
7515
8022
  * (never together), so it carries the same `main` landmark and label (#3289 §3 review) — nothing
@@ -7576,8 +8083,25 @@ function SetupPanel({ onSetUp, disabled }) {
7576
8083
  * The full desktop layout over an `IWsSessionState`. `surface` is an optional label shown next to the
7577
8084
  * mark (e.g. "app").
7578
8085
  */
7579
- function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor }) {
8086
+ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
7580
8087
  const [view, setView] = useState("chat");
8088
+ const { identity } = useProductIdentity();
8089
+ const appViewHost = useMemo(() => ({
8090
+ ...appViewSandboxUrl !== void 0 ? { sandboxUrl: appViewSandboxUrl } : {},
8091
+ readAppView: state.readAppView,
8092
+ callAppTool: state.callAppTool,
8093
+ hostInfo: {
8094
+ name: identity.displayName,
8095
+ version: APP_VIEW_HOST_VERSION
8096
+ },
8097
+ platform: surface === "app" ? "desktop" : "web"
8098
+ }), [
8099
+ appViewSandboxUrl,
8100
+ state.readAppView,
8101
+ state.callAppTool,
8102
+ identity.displayName,
8103
+ surface
8104
+ ]);
7581
8105
  const composerRef = useRef(null);
7582
8106
  const tasks = state.executionWorkspace?.entries ?? [];
7583
8107
  const hasTasks = tasks.some((entry) => entry.kind !== "main_thread");
@@ -7706,12 +8230,15 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
7706
8230
  args: "add"
7707
8231
  }),
7708
8232
  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
8233
+ }) : isEmpty ? /* @__PURE__ */ jsx(EmptyState, {}) : /* @__PURE__ */ jsx(AppViewHostContext.Provider, {
8234
+ value: appViewHost,
8235
+ children: /* @__PURE__ */ jsx(ConversationView, {
8236
+ messages: state.messages,
8237
+ activeTools: state.activeTools,
8238
+ streamingText: state.streamingText,
8239
+ isThinking: state.isThinking,
8240
+ ownDriverId: state.ownDriverId
8241
+ })
7715
8242
  })
7716
8243
  }),
7717
8244
  /* @__PURE__ */ jsxs("div", {