@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.
@@ -1,9 +1,9 @@
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";
5
5
  import remarkGfm from "remark-gfm";
6
- import { ArrowDown, ArrowUp, Bot, Check, ChevronDown, ChevronLeft, ChevronRight, CircleCheck, CircleDashed, CircleX, Copy, ExternalLink, FileText, Gauge, GitBranch, Globe, LoaderCircle, MessageCircleQuestion, MoreHorizontal, PanelLeftClose, PanelLeftOpen, Paperclip, Pause, Pencil, Play, RefreshCw, Search, Settings, Shield, ShieldAlert, Sparkles, Square, SquarePen, SquareTerminal, Target, Trash2, Wrench, X } from "lucide-react";
6
+ import { ArrowDown, ArrowUp, Bot, Check, ChevronDown, ChevronLeft, ChevronRight, CircleCheck, CircleDashed, CircleX, Copy, ExternalLink, FileText, Gauge, GitBranch, Globe, LoaderCircle, MessageCircleQuestion, MoreHorizontal, PanelLeftClose, PanelLeftOpen, Paperclip, Pause, Pencil, Play, RefreshCw, Search, Settings, Shield, ShieldAlert, Sparkles, Square, SquareCheck, SquarePen, SquareTerminal, Target, Trash2, Wrench, X } from "lucide-react";
7
7
  import { AGENT_DRIVER_ID, OWNER_DRIVER_ID } from "@robota-sdk/agent-interface-session";
8
8
  //#region src/hooks/excluded-commands.ts
9
9
  /**
@@ -285,6 +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: "reducer-state",
289
+ app_tool_result: "reducer-state",
288
290
  background_task_event: "intentionally-not-rendered",
289
291
  background_job_group_event: "intentionally-not-rendered",
290
292
  plan_event: "intentionally-not-rendered",
@@ -309,10 +311,10 @@ const SERVER_MESSAGE_HANDLING = {
309
311
  };
310
312
  //#endregion
311
313
  //#region src/hooks/use-agent-switcher-state.ts
312
- let requestCounter$6 = 0;
313
- function nextRequestId$6(prefix) {
314
- requestCounter$6 += 1;
315
- 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()}`;
316
318
  }
317
319
  /**
318
320
  * #3282 §4 part b-3: the agent switcher sheet's own correlated request/response state, the same
@@ -347,7 +349,7 @@ function useAgentSwitcherState(send) {
347
349
  return true;
348
350
  }, []);
349
351
  const requestAgentDefinitions = useCallback(() => {
350
- const requestId = nextRequestId$6("get_agent_definitions");
352
+ const requestId = nextRequestId$7("get_agent_definitions");
351
353
  listRequestRef.current = requestId;
352
354
  send({
353
355
  type: "get-agent-definitions",
@@ -370,7 +372,7 @@ function useAgentSwitcherState(send) {
370
372
  setAgentSwitcherOpen(false);
371
373
  }, []),
372
374
  selectAgent: useCallback((agentType) => {
373
- const requestId = nextRequestId$6("agent_switch");
375
+ const requestId = nextRequestId$7("agent_switch");
374
376
  switchRequestRef.current = requestId;
375
377
  setAgentSwitchMessage(null);
376
378
  send({
@@ -391,6 +393,126 @@ function useAgentSwitcherState(send) {
391
393
  };
392
394
  }
393
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
394
516
  //#region src/hooks/use-execution-detail.ts
395
517
  /**
396
518
  * #3288 §1: the detail sheet's own correlated request/response state — which entry is open, its
@@ -1075,7 +1197,8 @@ function toolEndFields(state) {
1075
1197
  ...state.diffLines ? { diffLines: state.diffLines } : {},
1076
1198
  ...state.diffFile ? { diffFile: state.diffFile } : {},
1077
1199
  ...state.toolResultData !== void 0 ? { toolResultData: state.toolResultData } : {},
1078
- ...state.toolResultParts ? { toolResultParts: state.toolResultParts } : {}
1200
+ ...state.toolResultParts ? { toolResultParts: state.toolResultParts } : {},
1201
+ ...state.appView ? { appView: state.appView } : {}
1079
1202
  };
1080
1203
  }
1081
1204
  /** Apply a `tool_end` to the ONE matching running tool across a turn's segments (never all of them). */
@@ -1173,6 +1296,7 @@ function buildConversationEntriesFromDisplaySegments(displaySegments) {
1173
1296
  }
1174
1297
  function useSessionClient(makeClient) {
1175
1298
  const [status, setStatus] = useState("disconnected");
1299
+ const statusRef = useRef("disconnected");
1176
1300
  const [messages, setMessages] = useState([]);
1177
1301
  const [activeTools, setActiveTools] = useState([]);
1178
1302
  const [streamingText, setStreamingText] = useState("");
@@ -1297,6 +1421,7 @@ function useSessionClient(makeClient) {
1297
1421
  const { handleExecutionDetailMessage, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
1298
1422
  const { handleModelListMessage, ...modelListState } = useModelListState(send);
1299
1423
  const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
1424
+ const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, useCallback(() => statusRef.current === "connected", []));
1300
1425
  const { handleSessionsMessage, canListSessions, markCurrent, armRestore, ...sessionDirectoryState } = useSessionDirectoryState(send);
1301
1426
  const { requestSessions, setSessionSidebarOpen } = sessionDirectoryState;
1302
1427
  const { handleSettingsMessage, openSettings, refreshSettings, ...settingsState } = useSettingsState(send);
@@ -1310,6 +1435,7 @@ function useSessionClient(makeClient) {
1310
1435
  if (handleExecutionDetailMessage(msg)) return;
1311
1436
  if (handleModelListMessage(msg)) return;
1312
1437
  if (handleProjectMessage(msg)) return;
1438
+ if (handleAppViewMessage(msg)) return;
1313
1439
  if (handleSessionsMessage(msg)) return;
1314
1440
  if (handleSettingsMessage(msg)) return;
1315
1441
  if (handleAgentDefinitionsMessage(msg)) return;
@@ -1446,6 +1572,7 @@ function useSessionClient(makeClient) {
1446
1572
  requestSessions();
1447
1573
  break;
1448
1574
  case "session_switched":
1575
+ rejectPendingAppRequests("The active session changed.");
1449
1576
  settleSessionChange();
1450
1577
  sessionStatusRef.current = null;
1451
1578
  setSessionStatus(null);
@@ -1470,6 +1597,7 @@ function useSessionClient(makeClient) {
1470
1597
  requestSchedules();
1471
1598
  break;
1472
1599
  case "history_cleared":
1600
+ rejectPendingAppRequests("The session history was cleared.");
1473
1601
  streamingTextRef.current = "";
1474
1602
  turnSegmentsRef.current = [];
1475
1603
  setStreamingText("");
@@ -1610,6 +1738,7 @@ function useSessionClient(makeClient) {
1610
1738
  closeExecutionDetail,
1611
1739
  finishTurn,
1612
1740
  handleAgentDefinitionsMessage,
1741
+ handleAppViewMessage,
1613
1742
  handleExecutionDetailMessage,
1614
1743
  handleModelListMessage,
1615
1744
  handleProjectMessage,
@@ -1624,6 +1753,7 @@ function useSessionClient(makeClient) {
1624
1753
  requestSchedules,
1625
1754
  requestSessions,
1626
1755
  resolveSwitchResult,
1756
+ rejectPendingAppRequests,
1627
1757
  send,
1628
1758
  setSessionSidebarOpen,
1629
1759
  settleSessionChange,
@@ -1642,7 +1772,9 @@ function useSessionClient(makeClient) {
1642
1772
  }, []);
1643
1773
  useEffect(() => {
1644
1774
  const onStatusChange = (next) => {
1775
+ statusRef.current = next;
1645
1776
  setStatus(next);
1777
+ if (next !== "connected") rejectPendingAppRequests();
1646
1778
  if (next === "connected") {
1647
1779
  commandsInFlightRef.current = 0;
1648
1780
  silentCommandRequestIdsRef.current.clear();
@@ -1666,13 +1798,15 @@ function useSessionClient(makeClient) {
1666
1798
  return () => {
1667
1799
  client.disconnect();
1668
1800
  clientRef.current = null;
1801
+ rejectPendingAppRequests();
1669
1802
  };
1670
1803
  }, [
1671
1804
  makeClient,
1672
1805
  handleMessage,
1673
1806
  requestSessions,
1674
1807
  requestSchedules,
1675
- armRestore
1808
+ armRestore,
1809
+ rejectPendingAppRequests
1676
1810
  ]);
1677
1811
  return {
1678
1812
  status,
@@ -1704,6 +1838,8 @@ function useSessionClient(makeClient) {
1704
1838
  openAgentSwitcher,
1705
1839
  ...agentSwitcherState,
1706
1840
  ...schedulesState,
1841
+ readAppView,
1842
+ callAppTool,
1707
1843
  helpOpen,
1708
1844
  openHelp,
1709
1845
  closeHelp
@@ -1894,6 +2030,338 @@ function driverAttributionText(driverId) {
1894
2030
  return `from ${humanDriverLabel(driverId)}`;
1895
2031
  }
1896
2032
  //#endregion
2033
+ //#region src/app-view/call-tool-result.ts
2034
+ function isRecord(value) {
2035
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2036
+ }
2037
+ function contentFromParts(parts) {
2038
+ const content = [];
2039
+ for (const part of parts ?? []) if (part.type === "text") content.push({
2040
+ type: "text",
2041
+ text: part.text
2042
+ });
2043
+ else if (part.type === "image_inline") content.push({
2044
+ type: "image",
2045
+ data: part.data,
2046
+ mimeType: part.mimeType
2047
+ });
2048
+ else if (part.type === "audio_inline") content.push({
2049
+ type: "audio",
2050
+ data: part.data,
2051
+ mimeType: part.mimeType
2052
+ });
2053
+ return content;
2054
+ }
2055
+ function outcomeToCallToolResult(outcome) {
2056
+ let content = contentFromParts(outcome.parts);
2057
+ if (content.length === 0 && typeof outcome.data === "string") content = [{
2058
+ type: "text",
2059
+ text: outcome.data
2060
+ }];
2061
+ if (!outcome.success && outcome.error !== void 0) content = [...content, {
2062
+ type: "text",
2063
+ text: outcome.error
2064
+ }];
2065
+ return {
2066
+ content,
2067
+ ...isRecord(outcome.data) ? { structuredContent: outcome.data } : {},
2068
+ isError: !outcome.success
2069
+ };
2070
+ }
2071
+ function toCallToolResult(tool, view) {
2072
+ let content = contentFromParts(tool.toolResultParts);
2073
+ if (content.length === 0 && tool.toolResultData !== void 0) content = [{
2074
+ type: "text",
2075
+ text: tool.toolResultData
2076
+ }];
2077
+ return {
2078
+ content,
2079
+ ...isRecord(view.structuredContent) ? { structuredContent: view.structuredContent } : {},
2080
+ isError: tool.status === "error"
2081
+ };
2082
+ }
2083
+ //#endregion
2084
+ //#region src/app-view/bridge.ts
2085
+ const APP_VIEW_PROTOCOL_VERSION = "2026-01-26";
2086
+ const INVALID_PARAMS = -32602;
2087
+ const METHOD_NOT_FOUND = -32601;
2088
+ const HOST_ERROR = -32e3;
2089
+ const SANDBOX_PROXY_READY = "ui/notifications/sandbox-proxy-ready";
2090
+ const SANDBOX_RESOURCE_READY = "ui/notifications/sandbox-resource-ready";
2091
+ function isPlainObject(value) {
2092
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2093
+ const prototype = Object.getPrototypeOf(value);
2094
+ return prototype === null || Object.getPrototypeOf(prototype) === null;
2095
+ }
2096
+ function errorMessage(error) {
2097
+ return error instanceof Error ? error.message : String(error);
2098
+ }
2099
+ function createAppViewBridge(options) {
2100
+ const { view } = options;
2101
+ let disposed = false;
2102
+ let sandboxAnswered = false;
2103
+ let initialization = "uninitialized";
2104
+ let callInFlight = false;
2105
+ const respond = (id, result) => {
2106
+ options.post({
2107
+ jsonrpc: "2.0",
2108
+ id,
2109
+ result
2110
+ });
2111
+ };
2112
+ const refuse = (id, code, message) => {
2113
+ options.post({
2114
+ jsonrpc: "2.0",
2115
+ id,
2116
+ error: {
2117
+ code,
2118
+ message
2119
+ }
2120
+ });
2121
+ };
2122
+ const callTool = (id, params) => {
2123
+ if (initialization !== "ready") {
2124
+ refuse(id, HOST_ERROR, "This app view has not initialized.");
2125
+ return;
2126
+ }
2127
+ const name = isPlainObject(params) ? params["name"] : void 0;
2128
+ const runtimeName = typeof name === "string" && Object.hasOwn(view.callableTools, name) ? view.callableTools[name] : void 0;
2129
+ if (!isPlainObject(params) || runtimeName === void 0) {
2130
+ refuse(id, INVALID_PARAMS, "This tool is not available to this view.");
2131
+ return;
2132
+ }
2133
+ const rawArguments = params["arguments"] === void 0 ? {} : params["arguments"];
2134
+ if (!isPlainObject(rawArguments)) {
2135
+ refuse(id, INVALID_PARAMS, "Tool arguments must be an object.");
2136
+ return;
2137
+ }
2138
+ if (callInFlight) {
2139
+ refuse(id, HOST_ERROR, "Another tool call from this view is still running.");
2140
+ return;
2141
+ }
2142
+ callInFlight = true;
2143
+ let outcome;
2144
+ try {
2145
+ outcome = options.callAppTool(runtimeName, rawArguments);
2146
+ } catch (error) {
2147
+ outcome = Promise.reject(error);
2148
+ }
2149
+ outcome.then((settled) => {
2150
+ callInFlight = false;
2151
+ if (!disposed) respond(id, outcomeToCallToolResult(settled));
2152
+ }, (error) => {
2153
+ callInFlight = false;
2154
+ if (!disposed) refuse(id, HOST_ERROR, errorMessage(error));
2155
+ });
2156
+ };
2157
+ const handleRequest = (id, method, params) => {
2158
+ switch (method) {
2159
+ case "ui/initialize":
2160
+ if (initialization === "uninitialized") initialization = "initializing";
2161
+ respond(id, {
2162
+ protocolVersion: APP_VIEW_PROTOCOL_VERSION,
2163
+ hostCapabilities: { serverTools: {} },
2164
+ hostInfo: options.hostInfo,
2165
+ hostContext: options.hostContext
2166
+ });
2167
+ return;
2168
+ case "tools/call":
2169
+ callTool(id, params);
2170
+ return;
2171
+ case "ping":
2172
+ respond(id, {});
2173
+ return;
2174
+ case "ui/request-display-mode":
2175
+ respond(id, { mode: "inline" });
2176
+ return;
2177
+ default: refuse(id, METHOD_NOT_FOUND, `This host does not support ${method}.`);
2178
+ }
2179
+ };
2180
+ const handleNotification = (method, params) => {
2181
+ switch (method) {
2182
+ case SANDBOX_PROXY_READY:
2183
+ if (sandboxAnswered) return;
2184
+ sandboxAnswered = true;
2185
+ options.post({
2186
+ jsonrpc: "2.0",
2187
+ method: SANDBOX_RESOURCE_READY,
2188
+ params: { html: view.html }
2189
+ });
2190
+ return;
2191
+ case "ui/notifications/initialized":
2192
+ if (initialization !== "initializing") return;
2193
+ initialization = "ready";
2194
+ options.post({
2195
+ jsonrpc: "2.0",
2196
+ method: "ui/notifications/tool-input",
2197
+ params: { arguments: view.toolInput ?? {} }
2198
+ });
2199
+ options.post({
2200
+ jsonrpc: "2.0",
2201
+ method: "ui/notifications/tool-result",
2202
+ params: options.toolResult
2203
+ });
2204
+ return;
2205
+ case "ui/notifications/size-changed": {
2206
+ const height = isPlainObject(params) ? params["height"] : void 0;
2207
+ if (typeof height !== "number" || !Number.isFinite(height)) return;
2208
+ options.onHeight(Math.min(640, Math.max(48, Math.round(height))));
2209
+ return;
2210
+ }
2211
+ default: return;
2212
+ }
2213
+ };
2214
+ return {
2215
+ handleMessage(event) {
2216
+ if (disposed) return;
2217
+ const frame = options.source();
2218
+ if (frame === null || event.source !== frame) return;
2219
+ const data = event.data;
2220
+ if (!isPlainObject(data) || data["jsonrpc"] !== "2.0") return;
2221
+ const { method, id } = data;
2222
+ if (typeof method !== "string") return;
2223
+ if (id === void 0) handleNotification(method, data["params"]);
2224
+ else if (typeof id === "string" || typeof id === "number") handleRequest(id, method, data["params"]);
2225
+ },
2226
+ dispose() {
2227
+ if (disposed) return;
2228
+ disposed = true;
2229
+ if (initialization === "ready") options.post({
2230
+ jsonrpc: "2.0",
2231
+ id: "teardown",
2232
+ method: "ui/resource-teardown",
2233
+ params: { reason: "closed" }
2234
+ });
2235
+ }
2236
+ };
2237
+ }
2238
+ //#endregion
2239
+ //#region src/app-view/host-context.ts
2240
+ const AppViewHostContext = createContext(null);
2241
+ function readAppViewHostContext(platform) {
2242
+ return {
2243
+ theme: typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
2244
+ displayMode: "inline",
2245
+ availableDisplayModes: ["inline"],
2246
+ containerDimensions: { maxHeight: 640 },
2247
+ platform,
2248
+ locale: typeof navigator === "undefined" ? "en-US" : navigator.language,
2249
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
2250
+ };
2251
+ }
2252
+ //#endregion
2253
+ //#region src/components/AppView.tsx
2254
+ const INITIAL_HEIGHT_PX = 160;
2255
+ const APP_VIEW_UNAVAILABLE_TEXT = "This tool returned an app view, which this window cannot show.";
2256
+ function AppView({ tool }) {
2257
+ const host = useContext(AppViewHostContext);
2258
+ if (tool.appView === void 0) return null;
2259
+ const viewKey = appViewKey(tool);
2260
+ if (host === null || host.sandboxUrl === void 0 || viewKey === void 0) return /* @__PURE__ */ jsx("p", {
2261
+ className: "text-[13px] text-subtle",
2262
+ children: APP_VIEW_UNAVAILABLE_TEXT
2263
+ });
2264
+ return /* @__PURE__ */ jsx(AppViewFrame, {
2265
+ tool,
2266
+ host,
2267
+ sandboxUrl: host.sandboxUrl,
2268
+ viewKey,
2269
+ sourceId: tool.appView.sourceId
2270
+ }, viewKey);
2271
+ }
2272
+ function AppViewFrame({ tool, host, sandboxUrl, viewKey, sourceId }) {
2273
+ const [load, setLoad] = useState({ status: "loading" });
2274
+ const [height, setHeight] = useState(INITIAL_HEIGHT_PX);
2275
+ const frameRef = useRef(null);
2276
+ const toolRef = useRef(tool);
2277
+ toolRef.current = tool;
2278
+ useEffect(() => {
2279
+ const controller = new AbortController();
2280
+ setLoad({ status: "loading" });
2281
+ host.readAppView(viewKey, controller.signal).then((view) => {
2282
+ if (!controller.signal.aborted) setLoad({
2283
+ status: "ready",
2284
+ view,
2285
+ host,
2286
+ sandboxUrl,
2287
+ signal: controller.signal
2288
+ });
2289
+ }, (error) => {
2290
+ if (!controller.signal.aborted) setLoad({
2291
+ status: "failed",
2292
+ reason: error instanceof Error ? error.message : String(error)
2293
+ });
2294
+ });
2295
+ return () => {
2296
+ controller.abort();
2297
+ };
2298
+ }, [
2299
+ viewKey,
2300
+ host.readAppView,
2301
+ host.callAppTool,
2302
+ sandboxUrl
2303
+ ]);
2304
+ const ready = load.status === "ready" && load.host.readAppView === host.readAppView && load.host.callAppTool === host.callAppTool && load.sandboxUrl === sandboxUrl ? load : null;
2305
+ useLayoutEffect(() => {
2306
+ const frame = frameRef.current;
2307
+ if (ready === null || frame === null) return void 0;
2308
+ const current = ready.host;
2309
+ const view = ready.view;
2310
+ const bridge = createAppViewBridge({
2311
+ view,
2312
+ toolResult: toCallToolResult(toolRef.current, view),
2313
+ hostInfo: current.hostInfo,
2314
+ hostContext: readAppViewHostContext(current.platform),
2315
+ post: (message) => frame.contentWindow?.postMessage(message, "*"),
2316
+ source: () => frame.contentWindow,
2317
+ callAppTool: (name, args) => current.callAppTool(viewKey, name, args, ready.signal),
2318
+ onHeight: setHeight
2319
+ });
2320
+ const onMessage = (event) => bridge.handleMessage(event);
2321
+ window.addEventListener("message", onMessage);
2322
+ return () => {
2323
+ window.removeEventListener("message", onMessage);
2324
+ bridge.dispose();
2325
+ };
2326
+ }, [ready, viewKey]);
2327
+ if (load.status === "failed") return /* @__PURE__ */ jsxs("p", {
2328
+ className: "text-[13px] text-subtle",
2329
+ children: [
2330
+ "The app from ",
2331
+ sourceId,
2332
+ " could not be shown: ",
2333
+ load.reason
2334
+ ]
2335
+ });
2336
+ if (ready === null) return /* @__PURE__ */ jsxs("p", {
2337
+ className: "text-[13px] text-subtle",
2338
+ children: [
2339
+ "Loading the app from ",
2340
+ sourceId,
2341
+ "…"
2342
+ ]
2343
+ });
2344
+ return /* @__PURE__ */ jsxs("div", {
2345
+ className: "my-1 flex flex-col gap-1",
2346
+ children: [/* @__PURE__ */ jsxs("p", {
2347
+ className: "text-[12px] text-subtle",
2348
+ children: ["App from ", sourceId]
2349
+ }), /* @__PURE__ */ jsx("iframe", {
2350
+ ref: frameRef,
2351
+ sandbox: "allow-scripts",
2352
+ src: sandboxUrl,
2353
+ title: `App from ${sourceId}`,
2354
+ referrerPolicy: "no-referrer",
2355
+ "data-testid": "app-view-frame",
2356
+ style: {
2357
+ width: "100%",
2358
+ height
2359
+ },
2360
+ className: "block rounded-lg border border-border bg-card"
2361
+ })]
2362
+ });
2363
+ }
2364
+ //#endregion
1897
2365
  //#region src/components/DiffLines.tsx
1898
2366
  /**
1899
2367
  * #3288: renders a unified diff already built server-side (the ONE diff builder, in
@@ -2407,42 +2875,49 @@ function ToolGroup({ tools }) {
2407
2875
  if (visible.length === 0) return null;
2408
2876
  return /* @__PURE__ */ jsxs("div", {
2409
2877
  className: "text-[14px]",
2410
- children: [/* @__PURE__ */ jsxs("button", {
2411
- type: "button",
2412
- "aria-expanded": open,
2413
- onClick: () => setOpen((value) => !value),
2414
- 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",
2415
- children: [
2416
- /* @__PURE__ */ jsx(Wrench, {
2417
- size: 15,
2418
- strokeWidth: 1.75,
2419
- className: "flex-shrink-0 text-subtle"
2420
- }),
2421
- /* @__PURE__ */ jsxs("span", {
2422
- className: "flex-shrink-0",
2423
- children: [
2424
- visible.length,
2425
- " tool ",
2426
- visible.length === 1 ? "call" : "calls"
2427
- ]
2428
- }),
2429
- /* @__PURE__ */ jsx("span", {
2430
- className: "min-w-0 truncate text-subtle",
2431
- children: names
2432
- }),
2433
- failed > 0 && /* @__PURE__ */ jsxs("span", {
2434
- className: "flex-shrink-0 rounded-md bg-destructive/12 px-1.5 text-[12.5px] text-destructive",
2435
- children: [failed, " failed"]
2436
- }),
2437
- /* @__PURE__ */ jsx(ChevronRight, {
2438
- size: 14,
2439
- className: `flex-shrink-0 text-subtle transition-transform ${open ? "rotate-90" : ""}`
2440
- })
2441
- ]
2442
- }), open && /* @__PURE__ */ jsx("div", {
2443
- className: "mt-0.5 flex flex-col pl-[25px]",
2444
- children: visible.map((tool) => /* @__PURE__ */ jsx(ToolCard, { tool }, tool.id))
2445
- })]
2878
+ children: [
2879
+ /* @__PURE__ */ jsxs("button", {
2880
+ type: "button",
2881
+ "aria-expanded": open,
2882
+ onClick: () => setOpen((value) => !value),
2883
+ className: "group -mx-2 flex max-w-full items-center gap-2.5 rounded-lg px-2 py-1 text-left text-muted-foreground hover:bg-hover hover:text-foreground",
2884
+ children: [
2885
+ /* @__PURE__ */ jsx(Wrench, {
2886
+ size: 15,
2887
+ strokeWidth: 1.75,
2888
+ className: "flex-shrink-0 text-subtle"
2889
+ }),
2890
+ /* @__PURE__ */ jsxs("span", {
2891
+ className: "flex-shrink-0",
2892
+ children: [
2893
+ visible.length,
2894
+ " tool ",
2895
+ visible.length === 1 ? "call" : "calls"
2896
+ ]
2897
+ }),
2898
+ /* @__PURE__ */ jsx("span", {
2899
+ className: "min-w-0 truncate text-subtle",
2900
+ children: names
2901
+ }),
2902
+ failed > 0 && /* @__PURE__ */ jsxs("span", {
2903
+ className: "flex-shrink-0 rounded-md bg-destructive/12 px-1.5 text-[12.5px] text-destructive",
2904
+ children: [failed, " failed"]
2905
+ }),
2906
+ /* @__PURE__ */ jsx(ChevronRight, {
2907
+ size: 14,
2908
+ className: `flex-shrink-0 text-subtle transition-transform ${open ? "rotate-90" : ""}`
2909
+ })
2910
+ ]
2911
+ }),
2912
+ visible.filter((tool) => tool.appView !== void 0).map((tool) => /* @__PURE__ */ jsx("div", {
2913
+ className: "pl-[25px]",
2914
+ children: /* @__PURE__ */ jsx(AppView, { tool })
2915
+ }, tool.id)),
2916
+ open && /* @__PURE__ */ jsx("div", {
2917
+ className: "mt-0.5 flex flex-col pl-[25px]",
2918
+ children: visible.map((tool) => /* @__PURE__ */ jsx(ToolCard, { tool }, tool.id))
2919
+ })
2920
+ ]
2446
2921
  });
2447
2922
  }
2448
2923
  /** #3288: a turn that changed files ends with this compact row — a click opens that file's diff. */
@@ -2568,7 +3043,10 @@ function ConversationView({ messages, activeTools, streamingText, isThinking, ow
2568
3043
  isThinking && !streamingText && /* @__PURE__ */ jsx(ThinkingIndicator, {}),
2569
3044
  activeTools.some((tool) => !tool.internal) && /* @__PURE__ */ jsx("div", {
2570
3045
  className: "flex flex-col",
2571
- children: activeTools.filter((tool) => !tool.internal).map((tool) => /* @__PURE__ */ jsx(ToolCard, { tool }, tool.id))
3046
+ children: activeTools.filter((tool) => !tool.internal).map((tool) => /* @__PURE__ */ jsxs(React.Fragment, { children: [/* @__PURE__ */ jsx(ToolCard, { tool }), tool.appView !== void 0 && tool.status !== "running" && /* @__PURE__ */ jsx("div", {
3047
+ className: "pl-[25px]",
3048
+ children: /* @__PURE__ */ jsx(AppView, { tool })
3049
+ })] }, tool.id))
2572
3050
  }),
2573
3051
  streamingText && /* @__PURE__ */ jsx(AgentBlock, {
2574
3052
  content: streamingText,
@@ -3211,6 +3689,26 @@ function isEditableElement(element) {
3211
3689
  function isDestructiveAskOption(requestId, value) {
3212
3690
  return requestId === "provider-profile-action" && value === "delete";
3213
3691
  }
3692
+ /**
3693
+ * The multi-select shape of an ask, or undefined for every other ask (which answers on click). The
3694
+ * rules mirror the terminal checklist (`MultiSelectList` in agent-ui-terminal): `minSelect` (default 1)
3695
+ * gates the submit, and `maxSelect` caps the ticks — a toggle beyond the cap is ignored, never swapped
3696
+ * for the oldest tick. `default.values` pre-select; a default that names no option is dropped, and
3697
+ * more defaults than the cap keep the first ones in option order, so the answer never breaks the cap.
3698
+ */
3699
+ function readMultiSelect(request) {
3700
+ const max = request.maxSelect ?? 1;
3701
+ if (max <= 1) return void 0;
3702
+ const options = (request.options ?? []).filter((opt) => !isDestructiveAskOption(request.id, opt.value));
3703
+ const wanted = new Set(request.default?.values ?? []);
3704
+ const defaults = options.filter((opt) => wanted.has(opt.value)).slice(0, max);
3705
+ return {
3706
+ min: request.minSelect ?? 1,
3707
+ max,
3708
+ options,
3709
+ defaults: new Set(defaults.map((opt) => opt.value))
3710
+ };
3711
+ }
3214
3712
  function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverId = null, layout = "modal", armDelayMs = 450, onFocusReturn }) {
3215
3713
  const prompt = prompts[0];
3216
3714
  const promptId = prompt?.id;
@@ -3222,6 +3720,7 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
3222
3720
  const [fieldFocused, setFieldFocused] = useState(false);
3223
3721
  const fieldHandleRef = useRef(null);
3224
3722
  const pendingFocusReturnRef = useRef(false);
3723
+ const [selection, setSelection] = useState(void 0);
3225
3724
  useEffect(() => {
3226
3725
  if (promptId === void 0 || armDelayMs <= 0) return void 0;
3227
3726
  const timer = setTimeout(() => setArmedId(promptId), armDelayMs);
@@ -3286,6 +3785,28 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
3286
3785
  fieldHandleRef.current?.clear();
3287
3786
  onAnswerAsk(id, response);
3288
3787
  };
3788
+ const multi = prompt.kind === "ask" ? readMultiSelect(prompt.request) : void 0;
3789
+ const ticks = selection?.promptId === prompt.id ? selection.values : multi?.defaults;
3790
+ const selectedValues = multi && ticks ? multi.options.map((opt) => opt.value).filter((value) => ticks.has(value)) : [];
3791
+ const canSubmit = multi !== void 0 && selectedValues.length >= multi.min;
3792
+ const toggleOption = (value) => {
3793
+ if (!multi) return;
3794
+ setSelection((current) => {
3795
+ const next = new Set(current?.promptId === prompt.id ? current.values : multi.defaults);
3796
+ if (next.has(value)) next.delete(value);
3797
+ else if (next.size < multi.max) next.add(value);
3798
+ return {
3799
+ promptId: prompt.id,
3800
+ values: next
3801
+ };
3802
+ });
3803
+ };
3804
+ const submitSelection = () => {
3805
+ if (canSubmit) answerAsk(prompt.id, {
3806
+ type: "answer",
3807
+ values: selectedValues
3808
+ });
3809
+ };
3289
3810
  /** A click anywhere in the prompt that isn't on a button or the field itself focuses the field. */
3290
3811
  const onContainerClick = (event) => {
3291
3812
  if (!hasFreeText) return;
@@ -3305,6 +3826,13 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
3305
3826
  return;
3306
3827
  }
3307
3828
  if (!armed) return;
3829
+ if (multi && event.key === "Enter") {
3830
+ if (event.target === event.currentTarget) {
3831
+ event.preventDefault();
3832
+ submitSelection();
3833
+ }
3834
+ return;
3835
+ }
3308
3836
  const index = Number(event.key) - 1;
3309
3837
  if (!Number.isInteger(index) || index < 0) return;
3310
3838
  if (prompt.kind === "permission") {
@@ -3312,7 +3840,9 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
3312
3840
  return;
3313
3841
  }
3314
3842
  const option = (prompt.request.options ?? []).filter((opt) => !isDestructiveAskOption(prompt.request.id, opt.value))[index];
3315
- if (option) answerAsk(prompt.id, {
3843
+ if (!option) return;
3844
+ if (multi) toggleOption(option.value);
3845
+ else answerAsk(prompt.id, {
3316
3846
  type: "answer",
3317
3847
  values: [option.value]
3318
3848
  });
@@ -3324,7 +3854,7 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
3324
3854
  const askOptions = prompt.kind === "ask" ? prompt.request.options ?? [] : [];
3325
3855
  const destructiveAskOption = askOptions.find((opt) => isDestructiveAskOption(askRequestId, opt.value));
3326
3856
  const primaryAskOptions = destructiveAskOption ? askOptions.filter((opt) => opt !== destructiveAskOption) : askOptions;
3327
- const askArmedHint = hasFreeText && fieldFocused ? "Enter to submit · Esc to cancel" : askOptions.length > 0 ? "1–9 choose · Esc cancel" : "Esc cancels";
3857
+ 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";
3328
3858
  return /* @__PURE__ */ jsx("div", {
3329
3859
  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]",
3330
3860
  children: /* @__PURE__ */ jsx("div", {
@@ -3476,12 +4006,23 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
3476
4006
  children: [/* @__PURE__ */ jsxs("button", {
3477
4007
  type: "button",
3478
4008
  "aria-keyshortcuts": dock && index < 9 ? String(index + 1) : void 0,
3479
- className: index === 0 ? PRIMARY_BUTTON$1 : SECONDARY_BUTTON$1,
3480
- onClick: onButton(() => answerAsk(prompt.id, {
4009
+ "aria-pressed": multi ? selectedValues.includes(opt.value) : void 0,
4010
+ className: (multi ? selectedValues.includes(opt.value) : index === 0) ? PRIMARY_BUTTON$1 : SECONDARY_BUTTON$1,
4011
+ onClick: onButton(() => multi ? toggleOption(opt.value) : answerAsk(prompt.id, {
3481
4012
  type: "answer",
3482
4013
  values: [opt.value]
3483
4014
  })),
3484
- children: [/* @__PURE__ */ jsx("span", { children: opt.label }), dock && index < 9 && /* @__PURE__ */ jsx(Kbd, { children: index + 1 })]
4015
+ children: [
4016
+ multi && (selectedValues.includes(opt.value) ? /* @__PURE__ */ jsx(SquareCheck, {
4017
+ size: 15,
4018
+ strokeWidth: 2
4019
+ }) : /* @__PURE__ */ jsx(Square, {
4020
+ size: 15,
4021
+ strokeWidth: 2
4022
+ })),
4023
+ /* @__PURE__ */ jsx("span", { children: opt.label }),
4024
+ dock && index < 9 && /* @__PURE__ */ jsx(Kbd, { children: index + 1 })
4025
+ ]
3485
4026
  }), opt.description && /* @__PURE__ */ jsx("p", {
3486
4027
  className: "pl-0.5 text-[11.5px] leading-snug text-subtle",
3487
4028
  children: opt.description
@@ -3523,16 +4064,26 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
3523
4064
  }),
3524
4065
  /* @__PURE__ */ jsxs("div", {
3525
4066
  className: "flex flex-wrap items-center gap-2",
3526
- children: [/* @__PURE__ */ jsx("button", {
3527
- type: "button",
3528
- className: GHOST_BUTTON,
3529
- onClick: onButton(() => answerAsk(prompt.id, { type: "cancelled" })),
3530
- children: "Cancel"
3531
- }), dock && /* @__PURE__ */ jsx(KeyHint, {
3532
- hasFocus,
3533
- armed,
3534
- armedText: askArmedHint
3535
- })]
4067
+ children: [
4068
+ multi && /* @__PURE__ */ jsx("button", {
4069
+ type: "button",
4070
+ className: `${PRIMARY_BUTTON$1} disabled:opacity-60`,
4071
+ disabled: !canSubmit,
4072
+ onClick: onButton(submitSelection),
4073
+ children: canSubmit ? "Submit" : `Submit (min ${multi.min})`
4074
+ }),
4075
+ /* @__PURE__ */ jsx("button", {
4076
+ type: "button",
4077
+ className: GHOST_BUTTON,
4078
+ onClick: onButton(() => answerAsk(prompt.id, { type: "cancelled" })),
4079
+ children: "Cancel"
4080
+ }),
4081
+ dock && /* @__PURE__ */ jsx(KeyHint, {
4082
+ hasFocus,
4083
+ armed,
4084
+ armedText: askArmedHint
4085
+ })
4086
+ ]
3536
4087
  })
3537
4088
  ]
3538
4089
  })] })
@@ -7435,6 +7986,7 @@ const NARROW_WINDOW_QUERY = "(max-width: 767.98px)";
7435
7986
  */
7436
7987
  /** The column every part of the conversation shares, so messages, prompt and composer line up. */
7437
7988
  const COLUMN = "mx-auto w-full max-w-[760px] px-6";
7989
+ const APP_VIEW_HOST_VERSION = "1.0.0";
7438
7990
  /**
7439
7991
  * Designed empty state shown before the first turn. Renders in the same slot as `ConversationView`
7440
7992
  * (never together), so it carries the same `main` landmark and label (#3289 §3 review) — nothing
@@ -7501,8 +8053,25 @@ function SetupPanel({ onSetUp, disabled }) {
7501
8053
  * The full desktop layout over an `IWsSessionState`. `surface` is an optional label shown next to the
7502
8054
  * mark (e.g. "app").
7503
8055
  */
7504
- function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor }) {
8056
+ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
7505
8057
  const [view, setView] = useState("chat");
8058
+ const { identity } = useProductIdentity();
8059
+ const appViewHost = useMemo(() => ({
8060
+ ...appViewSandboxUrl !== void 0 ? { sandboxUrl: appViewSandboxUrl } : {},
8061
+ readAppView: state.readAppView,
8062
+ callAppTool: state.callAppTool,
8063
+ hostInfo: {
8064
+ name: identity.displayName,
8065
+ version: APP_VIEW_HOST_VERSION
8066
+ },
8067
+ platform: surface === "app" ? "desktop" : "web"
8068
+ }), [
8069
+ appViewSandboxUrl,
8070
+ state.readAppView,
8071
+ state.callAppTool,
8072
+ identity.displayName,
8073
+ surface
8074
+ ]);
7506
8075
  const composerRef = useRef(null);
7507
8076
  const tasks = state.executionWorkspace?.entries ?? [];
7508
8077
  const hasTasks = tasks.some((entry) => entry.kind !== "main_thread");
@@ -7631,12 +8200,15 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
7631
8200
  args: "add"
7632
8201
  }),
7633
8202
  disabled: state.status !== "connected"
7634
- }) : isEmpty ? /* @__PURE__ */ jsx(EmptyState, {}) : /* @__PURE__ */ jsx(ConversationView, {
7635
- messages: state.messages,
7636
- activeTools: state.activeTools,
7637
- streamingText: state.streamingText,
7638
- isThinking: state.isThinking,
7639
- ownDriverId: state.ownDriverId
8203
+ }) : isEmpty ? /* @__PURE__ */ jsx(EmptyState, {}) : /* @__PURE__ */ jsx(AppViewHostContext.Provider, {
8204
+ value: appViewHost,
8205
+ children: /* @__PURE__ */ jsx(ConversationView, {
8206
+ messages: state.messages,
8207
+ activeTools: state.activeTools,
8208
+ streamingText: state.streamingText,
8209
+ isThinking: state.isThinking,
8210
+ ownDriverId: state.ownDriverId
8211
+ })
7640
8212
  })
7641
8213
  }),
7642
8214
  /* @__PURE__ */ jsxs("div", {