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