@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.
@@ -282,8 +282,8 @@ const SERVER_MESSAGE_HANDLING = {
282
282
  command_result: "reducer-state",
283
283
  messages: "reducer-state",
284
284
  history: "intentionally-not-rendered",
285
- history_changed: "intentionally-not-rendered",
286
- context: "intentionally-not-rendered",
285
+ history_changed: "visible-notice",
286
+ context: "reducer-state",
287
287
  turn_source: "intentionally-not-rendered",
288
288
  commands: "reducer-state",
289
289
  session_status: "reducer-state",
@@ -311,8 +311,8 @@ const SERVER_MESSAGE_HANDLING = {
311
311
  project_status: "reducer-state",
312
312
  project_diff: "reducer-state",
313
313
  project_memory: "reducer-state",
314
- app_view: "intentionally-not-rendered",
315
- app_tool_result: "intentionally-not-rendered",
314
+ app_view: "reducer-state",
315
+ app_tool_result: "reducer-state",
316
316
  background_task_event: "intentionally-not-rendered",
317
317
  background_job_group_event: "intentionally-not-rendered",
318
318
  plan_event: "intentionally-not-rendered",
@@ -337,10 +337,10 @@ const SERVER_MESSAGE_HANDLING = {
337
337
  };
338
338
  //#endregion
339
339
  //#region src/hooks/use-agent-switcher-state.ts
340
- let requestCounter$6 = 0;
341
- function nextRequestId$6(prefix) {
342
- requestCounter$6 += 1;
343
- return `${prefix}_${requestCounter$6}_${Date.now()}`;
340
+ let requestCounter$7 = 0;
341
+ function nextRequestId$7(prefix) {
342
+ requestCounter$7 += 1;
343
+ return `${prefix}_${requestCounter$7}_${Date.now()}`;
344
344
  }
345
345
  /**
346
346
  * #3282 §4 part b-3: the agent switcher sheet's own correlated request/response state, the same
@@ -375,7 +375,7 @@ function useAgentSwitcherState(send) {
375
375
  return true;
376
376
  }, []);
377
377
  const requestAgentDefinitions = (0, react.useCallback)(() => {
378
- const requestId = nextRequestId$6("get_agent_definitions");
378
+ const requestId = nextRequestId$7("get_agent_definitions");
379
379
  listRequestRef.current = requestId;
380
380
  send({
381
381
  type: "get-agent-definitions",
@@ -398,7 +398,7 @@ function useAgentSwitcherState(send) {
398
398
  setAgentSwitcherOpen(false);
399
399
  }, []),
400
400
  selectAgent: (0, react.useCallback)((agentType) => {
401
- const requestId = nextRequestId$6("agent_switch");
401
+ const requestId = nextRequestId$7("agent_switch");
402
402
  switchRequestRef.current = requestId;
403
403
  setAgentSwitchMessage(null);
404
404
  send({
@@ -419,6 +419,126 @@ function useAgentSwitcherState(send) {
419
419
  };
420
420
  }
421
421
  //#endregion
422
+ //#region src/hooks/use-app-view-requests.ts
423
+ let requestCounter$6 = 0;
424
+ function nextRequestId$6(prefix) {
425
+ requestCounter$6 += 1;
426
+ return `${prefix}_${requestCounter$6}_${Date.now()}`;
427
+ }
428
+ const CONNECTION_CLOSED = "The connection to the session closed.";
429
+ /**
430
+ * The key a finished call's app view is read and called by, taken from the tool row's `appView`
431
+ * reference; `undefined` when the row gives none. Everything outside this module treats it as an
432
+ * opaque string — it is read here and put on the two wire messages below, nowhere else.
433
+ */
434
+ function appViewKey(tool) {
435
+ return tool.appView?.viewId;
436
+ }
437
+ function readAppViewMessage(requestId, viewKey) {
438
+ return {
439
+ type: "read-app-view",
440
+ requestId,
441
+ viewId: viewKey
442
+ };
443
+ }
444
+ function callAppToolMessage(requestId, viewKey, name, args) {
445
+ return {
446
+ type: "call-app-tool",
447
+ requestId,
448
+ viewId: viewKey,
449
+ name,
450
+ arguments: args
451
+ };
452
+ }
453
+ /**
454
+ * The app-view host's two requests to the session, each a promise settled by the reply that echoes
455
+ * its `requestId` (`app_view` / `app_tool_result`, or a `protocol_error` naming it). Unlike the
456
+ * panels' latest-wins state, several can be outstanding at once (one per frame on screen), so each
457
+ * keeps its own entry. A reply that lost its connection never arrives: the owner of the connection
458
+ * rejects every outstanding request when it drops, is replaced or goes away (`rejectPendingAppRequests`).
459
+ */
460
+ function useAppViewRequests(send, isConnected) {
461
+ const pendingRef = (0, react.useRef)(/* @__PURE__ */ new Map());
462
+ const [requestOwner] = (0, react.useState)(() => nextRequestId$6("app_request"));
463
+ const rejectPendingAppRequests = (0, react.useCallback)((reason = CONNECTION_CLOSED) => {
464
+ const pending = [...pendingRef.current.values()];
465
+ pendingRef.current.clear();
466
+ for (const request of pending) {
467
+ request.dispose();
468
+ request.reject(new Error(reason));
469
+ }
470
+ }, []);
471
+ const request = (0, react.useCallback)((prefix, message, pending, signal) => new Promise((resolve, reject) => {
472
+ if (!isConnected() || signal?.aborted) {
473
+ reject(/* @__PURE__ */ new Error(signal?.aborted ? "The app view closed." : CONNECTION_CLOSED));
474
+ return;
475
+ }
476
+ const requestId = nextRequestId$6(`${requestOwner}_${prefix}`);
477
+ const dispose = () => signal?.removeEventListener("abort", abort);
478
+ const abort = () => {
479
+ pendingRef.current.delete(requestId);
480
+ dispose();
481
+ reject(/* @__PURE__ */ new Error("The app view closed."));
482
+ };
483
+ pendingRef.current.set(requestId, pending(resolve, reject, dispose));
484
+ signal?.addEventListener("abort", abort, { once: true });
485
+ try {
486
+ send(message(requestId));
487
+ } catch (error) {
488
+ pendingRef.current.delete(requestId);
489
+ dispose();
490
+ reject(error instanceof Error ? error : new Error(String(error)));
491
+ }
492
+ }), [
493
+ isConnected,
494
+ requestOwner,
495
+ send
496
+ ]);
497
+ return {
498
+ readAppView: (0, react.useCallback)((viewKey, signal) => request("app_view", (requestId) => readAppViewMessage(requestId, viewKey), (resolve, reject, dispose) => ({
499
+ answer: "app_view",
500
+ resolve,
501
+ reject,
502
+ dispose
503
+ }), signal), [request]),
504
+ callAppTool: (0, react.useCallback)((viewKey, name, args, signal) => request("app_tool", (requestId) => callAppToolMessage(requestId, viewKey, name, args), (resolve, reject, dispose) => ({
505
+ answer: "app_tool_result",
506
+ resolve,
507
+ reject,
508
+ dispose
509
+ }), signal), [request]),
510
+ handleAppViewMessage: (0, react.useCallback)((msg) => {
511
+ if (msg.type === "app_view" || msg.type === "app_tool_result") {
512
+ const pending = pendingRef.current.get(msg.requestId);
513
+ if (pending === void 0 || pending.answer !== msg.type) return true;
514
+ pendingRef.current.delete(msg.requestId);
515
+ pending.dispose();
516
+ if (msg.type === "app_view" && pending.answer === "app_view") pending.resolve(msg.view);
517
+ else if (msg.type === "app_tool_result" && pending.answer === "app_tool_result") {
518
+ const { success, data, parts, error } = msg;
519
+ pending.resolve({
520
+ success,
521
+ ...data !== void 0 ? { data } : {},
522
+ ...parts !== void 0 ? { parts } : {},
523
+ ...error !== void 0 ? { error } : {}
524
+ });
525
+ }
526
+ return true;
527
+ }
528
+ if (msg.type === "protocol_error" && msg.requestId !== void 0) {
529
+ const pending = pendingRef.current.get(msg.requestId);
530
+ if (pending === void 0) return msg.requestId.startsWith(`${requestOwner}_`);
531
+ pendingRef.current.delete(msg.requestId);
532
+ pending.dispose();
533
+ pending.reject(new Error(msg.message));
534
+ return true;
535
+ }
536
+ return false;
537
+ }, [requestOwner]),
538
+ rejectPendingAppRequests
539
+ };
540
+ }
541
+ //#endregion
422
542
  //#region src/hooks/use-execution-detail.ts
423
543
  /**
424
544
  * #3288 §1: the detail sheet's own correlated request/response state — which entry is open, its
@@ -1103,7 +1223,8 @@ function toolEndFields(state) {
1103
1223
  ...state.diffLines ? { diffLines: state.diffLines } : {},
1104
1224
  ...state.diffFile ? { diffFile: state.diffFile } : {},
1105
1225
  ...state.toolResultData !== void 0 ? { toolResultData: state.toolResultData } : {},
1106
- ...state.toolResultParts ? { toolResultParts: state.toolResultParts } : {}
1226
+ ...state.toolResultParts ? { toolResultParts: state.toolResultParts } : {},
1227
+ ...state.appView ? { appView: state.appView } : {}
1107
1228
  };
1108
1229
  }
1109
1230
  /** Apply a `tool_end` to the ONE matching running tool across a turn's segments (never all of them). */
@@ -1201,6 +1322,7 @@ function buildConversationEntriesFromDisplaySegments(displaySegments) {
1201
1322
  }
1202
1323
  function useSessionClient(makeClient) {
1203
1324
  const [status, setStatus] = (0, react.useState)("disconnected");
1325
+ const statusRef = (0, react.useRef)("disconnected");
1204
1326
  const [messages, setMessages] = (0, react.useState)([]);
1205
1327
  const [activeTools, setActiveTools] = (0, react.useState)([]);
1206
1328
  const [streamingText, setStreamingText] = (0, react.useState)("");
@@ -1325,6 +1447,7 @@ function useSessionClient(makeClient) {
1325
1447
  const { handleExecutionDetailMessage, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
1326
1448
  const { handleModelListMessage, ...modelListState } = useModelListState(send);
1327
1449
  const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
1450
+ const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, (0, react.useCallback)(() => statusRef.current === "connected", []));
1328
1451
  const { handleSessionsMessage, canListSessions, markCurrent, armRestore, ...sessionDirectoryState } = useSessionDirectoryState(send);
1329
1452
  const { requestSessions, setSessionSidebarOpen } = sessionDirectoryState;
1330
1453
  const { handleSettingsMessage, openSettings, refreshSettings, ...settingsState } = useSettingsState(send);
@@ -1332,12 +1455,16 @@ function useSessionClient(makeClient) {
1332
1455
  settingsOpenRef.current = settingsState.settingsOpen;
1333
1456
  const { handleAgentDefinitionsMessage, resolveSwitchResult, openAgentSwitcher, ...agentSwitcherState } = useAgentSwitcherState(send);
1334
1457
  const { handleSchedulesMessage, requestSchedules, ...schedulesState } = useSchedulesState(send);
1458
+ const clearCompactionNotices = (0, react.useCallback)(() => {
1459
+ setSessionNotices((previous) => previous.filter((notice) => notice.kind !== "context-compacted"));
1460
+ }, []);
1335
1461
  const handleMessage = (0, react.useCallback)((msg) => {
1336
1462
  SERVER_MESSAGE_HANDLING[msg.type];
1337
1463
  if (handleUsageMessage(msg)) return;
1338
1464
  if (handleExecutionDetailMessage(msg)) return;
1339
1465
  if (handleModelListMessage(msg)) return;
1340
1466
  if (handleProjectMessage(msg)) return;
1467
+ if (handleAppViewMessage(msg)) return;
1341
1468
  if (handleSessionsMessage(msg)) return;
1342
1469
  if (handleSettingsMessage(msg)) return;
1343
1470
  if (handleAgentDefinitionsMessage(msg)) return;
@@ -1474,9 +1601,11 @@ function useSessionClient(makeClient) {
1474
1601
  requestSessions();
1475
1602
  break;
1476
1603
  case "session_switched":
1604
+ rejectPendingAppRequests("The active session changed.");
1477
1605
  settleSessionChange();
1478
1606
  sessionStatusRef.current = null;
1479
1607
  setSessionStatus(null);
1608
+ clearCompactionNotices();
1480
1609
  streamingTextRef.current = "";
1481
1610
  turnSegmentsRef.current = [];
1482
1611
  setStreamingText("");
@@ -1498,6 +1627,8 @@ function useSessionClient(makeClient) {
1498
1627
  requestSchedules();
1499
1628
  break;
1500
1629
  case "history_cleared":
1630
+ clearCompactionNotices();
1631
+ rejectPendingAppRequests("The session history was cleared.");
1501
1632
  streamingTextRef.current = "";
1502
1633
  turnSegmentsRef.current = [];
1503
1634
  setStreamingText("");
@@ -1583,6 +1714,25 @@ function useSessionClient(makeClient) {
1583
1714
  setCommandCatalog(catalog);
1584
1715
  break;
1585
1716
  }
1717
+ case "context":
1718
+ if (sessionStatusRef.current !== null) {
1719
+ const next = {
1720
+ ...sessionStatusRef.current,
1721
+ context: msg.state
1722
+ };
1723
+ sessionStatusRef.current = next;
1724
+ setSessionStatus(next);
1725
+ }
1726
+ break;
1727
+ case "history_changed": {
1728
+ const compact = msg.compact;
1729
+ if (compact?.trigger === "auto") setSessionNotices((previous) => [...previous, {
1730
+ id: nextId(),
1731
+ kind: "context-compacted",
1732
+ message: `Auto compacted context: ${Math.round(compact.beforeUsedPercentage)}% → ${Math.round(compact.afterUsedPercentage)}%`
1733
+ }]);
1734
+ break;
1735
+ }
1586
1736
  case "session_status":
1587
1737
  sessionStatusRef.current = msg.status;
1588
1738
  setSessionStatus(msg.status);
@@ -1636,8 +1786,10 @@ function useSessionClient(makeClient) {
1636
1786
  appendEntry,
1637
1787
  canListSessions,
1638
1788
  closeExecutionDetail,
1789
+ clearCompactionNotices,
1639
1790
  finishTurn,
1640
1791
  handleAgentDefinitionsMessage,
1792
+ handleAppViewMessage,
1641
1793
  handleExecutionDetailMessage,
1642
1794
  handleModelListMessage,
1643
1795
  handleProjectMessage,
@@ -1652,6 +1804,7 @@ function useSessionClient(makeClient) {
1652
1804
  requestSchedules,
1653
1805
  requestSessions,
1654
1806
  resolveSwitchResult,
1807
+ rejectPendingAppRequests,
1655
1808
  send,
1656
1809
  setSessionSidebarOpen,
1657
1810
  settleSessionChange,
@@ -1670,7 +1823,12 @@ function useSessionClient(makeClient) {
1670
1823
  }, []);
1671
1824
  (0, react.useEffect)(() => {
1672
1825
  const onStatusChange = (next) => {
1826
+ statusRef.current = next;
1673
1827
  setStatus(next);
1828
+ if (next !== "connected") {
1829
+ rejectPendingAppRequests();
1830
+ clearCompactionNotices();
1831
+ }
1674
1832
  if (next === "connected") {
1675
1833
  commandsInFlightRef.current = 0;
1676
1834
  silentCommandRequestIdsRef.current.clear();
@@ -1694,13 +1852,16 @@ function useSessionClient(makeClient) {
1694
1852
  return () => {
1695
1853
  client.disconnect();
1696
1854
  clientRef.current = null;
1855
+ rejectPendingAppRequests();
1697
1856
  };
1698
1857
  }, [
1699
1858
  makeClient,
1700
1859
  handleMessage,
1860
+ clearCompactionNotices,
1701
1861
  requestSessions,
1702
1862
  requestSchedules,
1703
- armRestore
1863
+ armRestore,
1864
+ rejectPendingAppRequests
1704
1865
  ]);
1705
1866
  return {
1706
1867
  status,
@@ -1732,6 +1893,8 @@ function useSessionClient(makeClient) {
1732
1893
  openAgentSwitcher,
1733
1894
  ...agentSwitcherState,
1734
1895
  ...schedulesState,
1896
+ readAppView,
1897
+ callAppTool,
1735
1898
  helpOpen,
1736
1899
  openHelp,
1737
1900
  closeHelp
@@ -1922,6 +2085,338 @@ function driverAttributionText(driverId) {
1922
2085
  return `from ${humanDriverLabel(driverId)}`;
1923
2086
  }
1924
2087
  //#endregion
2088
+ //#region src/app-view/call-tool-result.ts
2089
+ function isRecord(value) {
2090
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2091
+ }
2092
+ function contentFromParts(parts) {
2093
+ const content = [];
2094
+ for (const part of parts ?? []) if (part.type === "text") content.push({
2095
+ type: "text",
2096
+ text: part.text
2097
+ });
2098
+ else if (part.type === "image_inline") content.push({
2099
+ type: "image",
2100
+ data: part.data,
2101
+ mimeType: part.mimeType
2102
+ });
2103
+ else if (part.type === "audio_inline") content.push({
2104
+ type: "audio",
2105
+ data: part.data,
2106
+ mimeType: part.mimeType
2107
+ });
2108
+ return content;
2109
+ }
2110
+ function outcomeToCallToolResult(outcome) {
2111
+ let content = contentFromParts(outcome.parts);
2112
+ if (content.length === 0 && typeof outcome.data === "string") content = [{
2113
+ type: "text",
2114
+ text: outcome.data
2115
+ }];
2116
+ if (!outcome.success && outcome.error !== void 0) content = [...content, {
2117
+ type: "text",
2118
+ text: outcome.error
2119
+ }];
2120
+ return {
2121
+ content,
2122
+ ...isRecord(outcome.data) ? { structuredContent: outcome.data } : {},
2123
+ isError: !outcome.success
2124
+ };
2125
+ }
2126
+ function toCallToolResult(tool, view) {
2127
+ let content = contentFromParts(tool.toolResultParts);
2128
+ if (content.length === 0 && tool.toolResultData !== void 0) content = [{
2129
+ type: "text",
2130
+ text: tool.toolResultData
2131
+ }];
2132
+ return {
2133
+ content,
2134
+ ...isRecord(view.structuredContent) ? { structuredContent: view.structuredContent } : {},
2135
+ isError: tool.status === "error"
2136
+ };
2137
+ }
2138
+ //#endregion
2139
+ //#region src/app-view/bridge.ts
2140
+ const APP_VIEW_PROTOCOL_VERSION = "2026-01-26";
2141
+ const INVALID_PARAMS = -32602;
2142
+ const METHOD_NOT_FOUND = -32601;
2143
+ const HOST_ERROR = -32e3;
2144
+ const SANDBOX_PROXY_READY = "ui/notifications/sandbox-proxy-ready";
2145
+ const SANDBOX_RESOURCE_READY = "ui/notifications/sandbox-resource-ready";
2146
+ function isPlainObject(value) {
2147
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2148
+ const prototype = Object.getPrototypeOf(value);
2149
+ return prototype === null || Object.getPrototypeOf(prototype) === null;
2150
+ }
2151
+ function errorMessage(error) {
2152
+ return error instanceof Error ? error.message : String(error);
2153
+ }
2154
+ function createAppViewBridge(options) {
2155
+ const { view } = options;
2156
+ let disposed = false;
2157
+ let sandboxAnswered = false;
2158
+ let initialization = "uninitialized";
2159
+ let callInFlight = false;
2160
+ const respond = (id, result) => {
2161
+ options.post({
2162
+ jsonrpc: "2.0",
2163
+ id,
2164
+ result
2165
+ });
2166
+ };
2167
+ const refuse = (id, code, message) => {
2168
+ options.post({
2169
+ jsonrpc: "2.0",
2170
+ id,
2171
+ error: {
2172
+ code,
2173
+ message
2174
+ }
2175
+ });
2176
+ };
2177
+ const callTool = (id, params) => {
2178
+ if (initialization !== "ready") {
2179
+ refuse(id, HOST_ERROR, "This app view has not initialized.");
2180
+ return;
2181
+ }
2182
+ const name = isPlainObject(params) ? params["name"] : void 0;
2183
+ const runtimeName = typeof name === "string" && Object.hasOwn(view.callableTools, name) ? view.callableTools[name] : void 0;
2184
+ if (!isPlainObject(params) || runtimeName === void 0) {
2185
+ refuse(id, INVALID_PARAMS, "This tool is not available to this view.");
2186
+ return;
2187
+ }
2188
+ const rawArguments = params["arguments"] === void 0 ? {} : params["arguments"];
2189
+ if (!isPlainObject(rawArguments)) {
2190
+ refuse(id, INVALID_PARAMS, "Tool arguments must be an object.");
2191
+ return;
2192
+ }
2193
+ if (callInFlight) {
2194
+ refuse(id, HOST_ERROR, "Another tool call from this view is still running.");
2195
+ return;
2196
+ }
2197
+ callInFlight = true;
2198
+ let outcome;
2199
+ try {
2200
+ outcome = options.callAppTool(runtimeName, rawArguments);
2201
+ } catch (error) {
2202
+ outcome = Promise.reject(error);
2203
+ }
2204
+ outcome.then((settled) => {
2205
+ callInFlight = false;
2206
+ if (!disposed) respond(id, outcomeToCallToolResult(settled));
2207
+ }, (error) => {
2208
+ callInFlight = false;
2209
+ if (!disposed) refuse(id, HOST_ERROR, errorMessage(error));
2210
+ });
2211
+ };
2212
+ const handleRequest = (id, method, params) => {
2213
+ switch (method) {
2214
+ case "ui/initialize":
2215
+ if (initialization === "uninitialized") initialization = "initializing";
2216
+ respond(id, {
2217
+ protocolVersion: APP_VIEW_PROTOCOL_VERSION,
2218
+ hostCapabilities: { serverTools: {} },
2219
+ hostInfo: options.hostInfo,
2220
+ hostContext: options.hostContext
2221
+ });
2222
+ return;
2223
+ case "tools/call":
2224
+ callTool(id, params);
2225
+ return;
2226
+ case "ping":
2227
+ respond(id, {});
2228
+ return;
2229
+ case "ui/request-display-mode":
2230
+ respond(id, { mode: "inline" });
2231
+ return;
2232
+ default: refuse(id, METHOD_NOT_FOUND, `This host does not support ${method}.`);
2233
+ }
2234
+ };
2235
+ const handleNotification = (method, params) => {
2236
+ switch (method) {
2237
+ case SANDBOX_PROXY_READY:
2238
+ if (sandboxAnswered) return;
2239
+ sandboxAnswered = true;
2240
+ options.post({
2241
+ jsonrpc: "2.0",
2242
+ method: SANDBOX_RESOURCE_READY,
2243
+ params: { html: view.html }
2244
+ });
2245
+ return;
2246
+ case "ui/notifications/initialized":
2247
+ if (initialization !== "initializing") return;
2248
+ initialization = "ready";
2249
+ options.post({
2250
+ jsonrpc: "2.0",
2251
+ method: "ui/notifications/tool-input",
2252
+ params: { arguments: view.toolInput ?? {} }
2253
+ });
2254
+ options.post({
2255
+ jsonrpc: "2.0",
2256
+ method: "ui/notifications/tool-result",
2257
+ params: options.toolResult
2258
+ });
2259
+ return;
2260
+ case "ui/notifications/size-changed": {
2261
+ const height = isPlainObject(params) ? params["height"] : void 0;
2262
+ if (typeof height !== "number" || !Number.isFinite(height)) return;
2263
+ options.onHeight(Math.min(640, Math.max(48, Math.round(height))));
2264
+ return;
2265
+ }
2266
+ default: return;
2267
+ }
2268
+ };
2269
+ return {
2270
+ handleMessage(event) {
2271
+ if (disposed) return;
2272
+ const frame = options.source();
2273
+ if (frame === null || event.source !== frame) return;
2274
+ const data = event.data;
2275
+ if (!isPlainObject(data) || data["jsonrpc"] !== "2.0") return;
2276
+ const { method, id } = data;
2277
+ if (typeof method !== "string") return;
2278
+ if (id === void 0) handleNotification(method, data["params"]);
2279
+ else if (typeof id === "string" || typeof id === "number") handleRequest(id, method, data["params"]);
2280
+ },
2281
+ dispose() {
2282
+ if (disposed) return;
2283
+ disposed = true;
2284
+ if (initialization === "ready") options.post({
2285
+ jsonrpc: "2.0",
2286
+ id: "teardown",
2287
+ method: "ui/resource-teardown",
2288
+ params: { reason: "closed" }
2289
+ });
2290
+ }
2291
+ };
2292
+ }
2293
+ //#endregion
2294
+ //#region src/app-view/host-context.ts
2295
+ const AppViewHostContext = (0, react.createContext)(null);
2296
+ function readAppViewHostContext(platform) {
2297
+ return {
2298
+ theme: typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
2299
+ displayMode: "inline",
2300
+ availableDisplayModes: ["inline"],
2301
+ containerDimensions: { maxHeight: 640 },
2302
+ platform,
2303
+ locale: typeof navigator === "undefined" ? "en-US" : navigator.language,
2304
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
2305
+ };
2306
+ }
2307
+ //#endregion
2308
+ //#region src/components/AppView.tsx
2309
+ const INITIAL_HEIGHT_PX = 160;
2310
+ const APP_VIEW_UNAVAILABLE_TEXT = "This tool returned an app view, which this window cannot show.";
2311
+ function AppView({ tool }) {
2312
+ const host = (0, react.useContext)(AppViewHostContext);
2313
+ if (tool.appView === void 0) return null;
2314
+ const viewKey = appViewKey(tool);
2315
+ if (host === null || host.sandboxUrl === void 0 || viewKey === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2316
+ className: "text-[13px] text-subtle",
2317
+ children: APP_VIEW_UNAVAILABLE_TEXT
2318
+ });
2319
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppViewFrame, {
2320
+ tool,
2321
+ host,
2322
+ sandboxUrl: host.sandboxUrl,
2323
+ viewKey,
2324
+ sourceId: tool.appView.sourceId
2325
+ }, viewKey);
2326
+ }
2327
+ function AppViewFrame({ tool, host, sandboxUrl, viewKey, sourceId }) {
2328
+ const [load, setLoad] = (0, react.useState)({ status: "loading" });
2329
+ const [height, setHeight] = (0, react.useState)(INITIAL_HEIGHT_PX);
2330
+ const frameRef = (0, react.useRef)(null);
2331
+ const toolRef = (0, react.useRef)(tool);
2332
+ toolRef.current = tool;
2333
+ (0, react.useEffect)(() => {
2334
+ const controller = new AbortController();
2335
+ setLoad({ status: "loading" });
2336
+ host.readAppView(viewKey, controller.signal).then((view) => {
2337
+ if (!controller.signal.aborted) setLoad({
2338
+ status: "ready",
2339
+ view,
2340
+ host,
2341
+ sandboxUrl,
2342
+ signal: controller.signal
2343
+ });
2344
+ }, (error) => {
2345
+ if (!controller.signal.aborted) setLoad({
2346
+ status: "failed",
2347
+ reason: error instanceof Error ? error.message : String(error)
2348
+ });
2349
+ });
2350
+ return () => {
2351
+ controller.abort();
2352
+ };
2353
+ }, [
2354
+ viewKey,
2355
+ host.readAppView,
2356
+ host.callAppTool,
2357
+ sandboxUrl
2358
+ ]);
2359
+ const ready = load.status === "ready" && load.host.readAppView === host.readAppView && load.host.callAppTool === host.callAppTool && load.sandboxUrl === sandboxUrl ? load : null;
2360
+ (0, react.useLayoutEffect)(() => {
2361
+ const frame = frameRef.current;
2362
+ if (ready === null || frame === null) return void 0;
2363
+ const current = ready.host;
2364
+ const view = ready.view;
2365
+ const bridge = createAppViewBridge({
2366
+ view,
2367
+ toolResult: toCallToolResult(toolRef.current, view),
2368
+ hostInfo: current.hostInfo,
2369
+ hostContext: readAppViewHostContext(current.platform),
2370
+ post: (message) => frame.contentWindow?.postMessage(message, "*"),
2371
+ source: () => frame.contentWindow,
2372
+ callAppTool: (name, args) => current.callAppTool(viewKey, name, args, ready.signal),
2373
+ onHeight: setHeight
2374
+ });
2375
+ const onMessage = (event) => bridge.handleMessage(event);
2376
+ window.addEventListener("message", onMessage);
2377
+ return () => {
2378
+ window.removeEventListener("message", onMessage);
2379
+ bridge.dispose();
2380
+ };
2381
+ }, [ready, viewKey]);
2382
+ if (load.status === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2383
+ className: "text-[13px] text-subtle",
2384
+ children: [
2385
+ "The app from ",
2386
+ sourceId,
2387
+ " could not be shown: ",
2388
+ load.reason
2389
+ ]
2390
+ });
2391
+ if (ready === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2392
+ className: "text-[13px] text-subtle",
2393
+ children: [
2394
+ "Loading the app from ",
2395
+ sourceId,
2396
+ "…"
2397
+ ]
2398
+ });
2399
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2400
+ className: "my-1 flex flex-col gap-1",
2401
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2402
+ className: "text-[12px] text-subtle",
2403
+ children: ["App from ", sourceId]
2404
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
2405
+ ref: frameRef,
2406
+ sandbox: "allow-scripts",
2407
+ src: sandboxUrl,
2408
+ title: `App from ${sourceId}`,
2409
+ referrerPolicy: "no-referrer",
2410
+ "data-testid": "app-view-frame",
2411
+ style: {
2412
+ width: "100%",
2413
+ height
2414
+ },
2415
+ className: "block rounded-lg border border-border bg-card"
2416
+ })]
2417
+ });
2418
+ }
2419
+ //#endregion
1925
2420
  //#region src/components/DiffLines.tsx
1926
2421
  /**
1927
2422
  * #3288: renders a unified diff already built server-side (the ONE diff builder, in
@@ -2435,42 +2930,49 @@ function ToolGroup({ tools }) {
2435
2930
  if (visible.length === 0) return null;
2436
2931
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2437
2932
  className: "text-[14px]",
2438
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2439
- type: "button",
2440
- "aria-expanded": open,
2441
- onClick: () => setOpen((value) => !value),
2442
- className: "group -mx-2 flex max-w-full items-center gap-2.5 rounded-lg px-2 py-1 text-left text-muted-foreground hover:bg-hover hover:text-foreground",
2443
- children: [
2444
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Wrench, {
2445
- size: 15,
2446
- strokeWidth: 1.75,
2447
- className: "flex-shrink-0 text-subtle"
2448
- }),
2449
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2450
- className: "flex-shrink-0",
2451
- children: [
2452
- visible.length,
2453
- " tool ",
2454
- visible.length === 1 ? "call" : "calls"
2455
- ]
2456
- }),
2457
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2458
- className: "min-w-0 truncate text-subtle",
2459
- children: names
2460
- }),
2461
- failed > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2462
- className: "flex-shrink-0 rounded-md bg-destructive/12 px-1.5 text-[12.5px] text-destructive",
2463
- children: [failed, " failed"]
2464
- }),
2465
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
2466
- size: 14,
2467
- className: `flex-shrink-0 text-subtle transition-transform ${open ? "rotate-90" : ""}`
2468
- })
2469
- ]
2470
- }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2471
- className: "mt-0.5 flex flex-col pl-[25px]",
2472
- children: visible.map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCard, { tool }, tool.id))
2473
- })]
2933
+ children: [
2934
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2935
+ type: "button",
2936
+ "aria-expanded": open,
2937
+ onClick: () => setOpen((value) => !value),
2938
+ 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",
2939
+ children: [
2940
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Wrench, {
2941
+ size: 15,
2942
+ strokeWidth: 1.75,
2943
+ className: "flex-shrink-0 text-subtle"
2944
+ }),
2945
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2946
+ className: "flex-shrink-0",
2947
+ children: [
2948
+ visible.length,
2949
+ " tool ",
2950
+ visible.length === 1 ? "call" : "calls"
2951
+ ]
2952
+ }),
2953
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2954
+ className: "min-w-0 truncate text-subtle",
2955
+ children: names
2956
+ }),
2957
+ failed > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2958
+ className: "flex-shrink-0 rounded-md bg-destructive/12 px-1.5 text-[12.5px] text-destructive",
2959
+ children: [failed, " failed"]
2960
+ }),
2961
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
2962
+ size: 14,
2963
+ className: `flex-shrink-0 text-subtle transition-transform ${open ? "rotate-90" : ""}`
2964
+ })
2965
+ ]
2966
+ }),
2967
+ visible.filter((tool) => tool.appView !== void 0).map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2968
+ className: "pl-[25px]",
2969
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppView, { tool })
2970
+ }, tool.id)),
2971
+ open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2972
+ className: "mt-0.5 flex flex-col pl-[25px]",
2973
+ children: visible.map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCard, { tool }, tool.id))
2974
+ })
2975
+ ]
2474
2976
  });
2475
2977
  }
2476
2978
  /** #3288: a turn that changed files ends with this compact row — a click opens that file's diff. */
@@ -2596,7 +3098,10 @@ function ConversationView({ messages, activeTools, streamingText, isThinking, ow
2596
3098
  isThinking && !streamingText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThinkingIndicator, {}),
2597
3099
  activeTools.some((tool) => !tool.internal) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2598
3100
  className: "flex flex-col",
2599
- children: activeTools.filter((tool) => !tool.internal).map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCard, { tool }, tool.id))
3101
+ 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", {
3102
+ className: "pl-[25px]",
3103
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppView, { tool })
3104
+ })] }, tool.id))
2600
3105
  }),
2601
3106
  streamingText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentBlock, {
2602
3107
  content: streamingText,
@@ -6464,7 +6969,7 @@ function noticeSentence(notice) {
6464
6969
  }
6465
6970
  }
6466
6971
  /**
6467
- * Session and protocol failures, as dismissible toasts over the top-right corner: they stay until
6972
+ * Session outcomes and protocol failures, as dismissible toasts over the top-right corner: they stay until
6468
6973
  * dismissed, and never push the conversation or the composer out of view. Command output is not
6469
6974
  * here — it belongs to the conversation. A provider error classified at the wire boundary shows one
6470
6975
  * plain sentence with the next step; its raw text sits behind "Details" rather than as the headline.
@@ -6476,11 +6981,12 @@ function SessionNotices({ state }) {
6476
6981
  className: "pointer-events-none absolute right-4 top-16 z-40 flex w-[min(400px,calc(100%-32px))] flex-col gap-2",
6477
6982
  children: notices.slice(-3).map((notice) => {
6478
6983
  const sentence = noticeSentence(notice);
6984
+ const compacted = notice.kind === "context-compacted";
6479
6985
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6480
- role: "alert",
6986
+ role: compacted ? "status" : "alert",
6481
6987
  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",
6482
6988
  children: [
6483
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "mt-[7px] h-2 w-2 flex-shrink-0 rounded-full bg-destructive" }),
6989
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `mt-[7px] h-2 w-2 flex-shrink-0 rounded-full ${compacted ? "bg-primary" : "bg-destructive"}` }),
6484
6990
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6485
6991
  className: "max-h-40 flex-1 overflow-y-auto whitespace-pre-wrap break-words",
6486
6992
  children: [sentence ?? notice.message, sentence && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
@@ -7536,6 +8042,7 @@ const NARROW_WINDOW_QUERY = "(max-width: 767.98px)";
7536
8042
  */
7537
8043
  /** The column every part of the conversation shares, so messages, prompt and composer line up. */
7538
8044
  const COLUMN = "mx-auto w-full max-w-[760px] px-6";
8045
+ const APP_VIEW_HOST_VERSION = "1.0.0";
7539
8046
  /**
7540
8047
  * Designed empty state shown before the first turn. Renders in the same slot as `ConversationView`
7541
8048
  * (never together), so it carries the same `main` landmark and label (#3289 §3 review) — nothing
@@ -7602,8 +8109,25 @@ function SetupPanel({ onSetUp, disabled }) {
7602
8109
  * The full desktop layout over an `IWsSessionState`. `surface` is an optional label shown next to the
7603
8110
  * mark (e.g. "app").
7604
8111
  */
7605
- function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor }) {
8112
+ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
7606
8113
  const [view, setView] = (0, react.useState)("chat");
8114
+ const { identity } = useProductIdentity();
8115
+ const appViewHost = (0, react.useMemo)(() => ({
8116
+ ...appViewSandboxUrl !== void 0 ? { sandboxUrl: appViewSandboxUrl } : {},
8117
+ readAppView: state.readAppView,
8118
+ callAppTool: state.callAppTool,
8119
+ hostInfo: {
8120
+ name: identity.displayName,
8121
+ version: APP_VIEW_HOST_VERSION
8122
+ },
8123
+ platform: surface === "app" ? "desktop" : "web"
8124
+ }), [
8125
+ appViewSandboxUrl,
8126
+ state.readAppView,
8127
+ state.callAppTool,
8128
+ identity.displayName,
8129
+ surface
8130
+ ]);
7607
8131
  const composerRef = (0, react.useRef)(null);
7608
8132
  const tasks = state.executionWorkspace?.entries ?? [];
7609
8133
  const hasTasks = tasks.some((entry) => entry.kind !== "main_thread");
@@ -7732,12 +8256,15 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
7732
8256
  args: "add"
7733
8257
  }),
7734
8258
  disabled: state.status !== "connected"
7735
- }) : isEmpty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationView, {
7736
- messages: state.messages,
7737
- activeTools: state.activeTools,
7738
- streamingText: state.streamingText,
7739
- isThinking: state.isThinking,
7740
- ownDriverId: state.ownDriverId
8259
+ }) : isEmpty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppViewHostContext.Provider, {
8260
+ value: appViewHost,
8261
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationView, {
8262
+ messages: state.messages,
8263
+ activeTools: state.activeTools,
8264
+ streamingText: state.streamingText,
8265
+ isThinking: state.isThinking,
8266
+ ownDriverId: state.ownDriverId
8267
+ })
7741
8268
  })
7742
8269
  }),
7743
8270
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {