@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.cjs
CHANGED
|
@@ -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: "
|
|
315
|
-
app_tool_result: "
|
|
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$
|
|
341
|
-
function nextRequestId$
|
|
342
|
-
requestCounter$
|
|
343
|
-
return `${prefix}_${requestCounter$
|
|
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$
|
|
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$
|
|
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);
|
|
@@ -1338,6 +1461,7 @@ function useSessionClient(makeClient) {
|
|
|
1338
1461
|
if (handleExecutionDetailMessage(msg)) return;
|
|
1339
1462
|
if (handleModelListMessage(msg)) return;
|
|
1340
1463
|
if (handleProjectMessage(msg)) return;
|
|
1464
|
+
if (handleAppViewMessage(msg)) return;
|
|
1341
1465
|
if (handleSessionsMessage(msg)) return;
|
|
1342
1466
|
if (handleSettingsMessage(msg)) return;
|
|
1343
1467
|
if (handleAgentDefinitionsMessage(msg)) return;
|
|
@@ -1474,6 +1598,7 @@ function useSessionClient(makeClient) {
|
|
|
1474
1598
|
requestSessions();
|
|
1475
1599
|
break;
|
|
1476
1600
|
case "session_switched":
|
|
1601
|
+
rejectPendingAppRequests("The active session changed.");
|
|
1477
1602
|
settleSessionChange();
|
|
1478
1603
|
sessionStatusRef.current = null;
|
|
1479
1604
|
setSessionStatus(null);
|
|
@@ -1498,6 +1623,7 @@ function useSessionClient(makeClient) {
|
|
|
1498
1623
|
requestSchedules();
|
|
1499
1624
|
break;
|
|
1500
1625
|
case "history_cleared":
|
|
1626
|
+
rejectPendingAppRequests("The session history was cleared.");
|
|
1501
1627
|
streamingTextRef.current = "";
|
|
1502
1628
|
turnSegmentsRef.current = [];
|
|
1503
1629
|
setStreamingText("");
|
|
@@ -1638,6 +1764,7 @@ function useSessionClient(makeClient) {
|
|
|
1638
1764
|
closeExecutionDetail,
|
|
1639
1765
|
finishTurn,
|
|
1640
1766
|
handleAgentDefinitionsMessage,
|
|
1767
|
+
handleAppViewMessage,
|
|
1641
1768
|
handleExecutionDetailMessage,
|
|
1642
1769
|
handleModelListMessage,
|
|
1643
1770
|
handleProjectMessage,
|
|
@@ -1652,6 +1779,7 @@ function useSessionClient(makeClient) {
|
|
|
1652
1779
|
requestSchedules,
|
|
1653
1780
|
requestSessions,
|
|
1654
1781
|
resolveSwitchResult,
|
|
1782
|
+
rejectPendingAppRequests,
|
|
1655
1783
|
send,
|
|
1656
1784
|
setSessionSidebarOpen,
|
|
1657
1785
|
settleSessionChange,
|
|
@@ -1670,7 +1798,9 @@ function useSessionClient(makeClient) {
|
|
|
1670
1798
|
}, []);
|
|
1671
1799
|
(0, react.useEffect)(() => {
|
|
1672
1800
|
const onStatusChange = (next) => {
|
|
1801
|
+
statusRef.current = next;
|
|
1673
1802
|
setStatus(next);
|
|
1803
|
+
if (next !== "connected") rejectPendingAppRequests();
|
|
1674
1804
|
if (next === "connected") {
|
|
1675
1805
|
commandsInFlightRef.current = 0;
|
|
1676
1806
|
silentCommandRequestIdsRef.current.clear();
|
|
@@ -1694,13 +1824,15 @@ function useSessionClient(makeClient) {
|
|
|
1694
1824
|
return () => {
|
|
1695
1825
|
client.disconnect();
|
|
1696
1826
|
clientRef.current = null;
|
|
1827
|
+
rejectPendingAppRequests();
|
|
1697
1828
|
};
|
|
1698
1829
|
}, [
|
|
1699
1830
|
makeClient,
|
|
1700
1831
|
handleMessage,
|
|
1701
1832
|
requestSessions,
|
|
1702
1833
|
requestSchedules,
|
|
1703
|
-
armRestore
|
|
1834
|
+
armRestore,
|
|
1835
|
+
rejectPendingAppRequests
|
|
1704
1836
|
]);
|
|
1705
1837
|
return {
|
|
1706
1838
|
status,
|
|
@@ -1732,6 +1864,8 @@ function useSessionClient(makeClient) {
|
|
|
1732
1864
|
openAgentSwitcher,
|
|
1733
1865
|
...agentSwitcherState,
|
|
1734
1866
|
...schedulesState,
|
|
1867
|
+
readAppView,
|
|
1868
|
+
callAppTool,
|
|
1735
1869
|
helpOpen,
|
|
1736
1870
|
openHelp,
|
|
1737
1871
|
closeHelp
|
|
@@ -1922,6 +2056,338 @@ function driverAttributionText(driverId) {
|
|
|
1922
2056
|
return `from ${humanDriverLabel(driverId)}`;
|
|
1923
2057
|
}
|
|
1924
2058
|
//#endregion
|
|
2059
|
+
//#region src/app-view/call-tool-result.ts
|
|
2060
|
+
function isRecord(value) {
|
|
2061
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2062
|
+
}
|
|
2063
|
+
function contentFromParts(parts) {
|
|
2064
|
+
const content = [];
|
|
2065
|
+
for (const part of parts ?? []) if (part.type === "text") content.push({
|
|
2066
|
+
type: "text",
|
|
2067
|
+
text: part.text
|
|
2068
|
+
});
|
|
2069
|
+
else if (part.type === "image_inline") content.push({
|
|
2070
|
+
type: "image",
|
|
2071
|
+
data: part.data,
|
|
2072
|
+
mimeType: part.mimeType
|
|
2073
|
+
});
|
|
2074
|
+
else if (part.type === "audio_inline") content.push({
|
|
2075
|
+
type: "audio",
|
|
2076
|
+
data: part.data,
|
|
2077
|
+
mimeType: part.mimeType
|
|
2078
|
+
});
|
|
2079
|
+
return content;
|
|
2080
|
+
}
|
|
2081
|
+
function outcomeToCallToolResult(outcome) {
|
|
2082
|
+
let content = contentFromParts(outcome.parts);
|
|
2083
|
+
if (content.length === 0 && typeof outcome.data === "string") content = [{
|
|
2084
|
+
type: "text",
|
|
2085
|
+
text: outcome.data
|
|
2086
|
+
}];
|
|
2087
|
+
if (!outcome.success && outcome.error !== void 0) content = [...content, {
|
|
2088
|
+
type: "text",
|
|
2089
|
+
text: outcome.error
|
|
2090
|
+
}];
|
|
2091
|
+
return {
|
|
2092
|
+
content,
|
|
2093
|
+
...isRecord(outcome.data) ? { structuredContent: outcome.data } : {},
|
|
2094
|
+
isError: !outcome.success
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
function toCallToolResult(tool, view) {
|
|
2098
|
+
let content = contentFromParts(tool.toolResultParts);
|
|
2099
|
+
if (content.length === 0 && tool.toolResultData !== void 0) content = [{
|
|
2100
|
+
type: "text",
|
|
2101
|
+
text: tool.toolResultData
|
|
2102
|
+
}];
|
|
2103
|
+
return {
|
|
2104
|
+
content,
|
|
2105
|
+
...isRecord(view.structuredContent) ? { structuredContent: view.structuredContent } : {},
|
|
2106
|
+
isError: tool.status === "error"
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
//#endregion
|
|
2110
|
+
//#region src/app-view/bridge.ts
|
|
2111
|
+
const APP_VIEW_PROTOCOL_VERSION = "2026-01-26";
|
|
2112
|
+
const INVALID_PARAMS = -32602;
|
|
2113
|
+
const METHOD_NOT_FOUND = -32601;
|
|
2114
|
+
const HOST_ERROR = -32e3;
|
|
2115
|
+
const SANDBOX_PROXY_READY = "ui/notifications/sandbox-proxy-ready";
|
|
2116
|
+
const SANDBOX_RESOURCE_READY = "ui/notifications/sandbox-resource-ready";
|
|
2117
|
+
function isPlainObject(value) {
|
|
2118
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
2119
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2120
|
+
return prototype === null || Object.getPrototypeOf(prototype) === null;
|
|
2121
|
+
}
|
|
2122
|
+
function errorMessage(error) {
|
|
2123
|
+
return error instanceof Error ? error.message : String(error);
|
|
2124
|
+
}
|
|
2125
|
+
function createAppViewBridge(options) {
|
|
2126
|
+
const { view } = options;
|
|
2127
|
+
let disposed = false;
|
|
2128
|
+
let sandboxAnswered = false;
|
|
2129
|
+
let initialization = "uninitialized";
|
|
2130
|
+
let callInFlight = false;
|
|
2131
|
+
const respond = (id, result) => {
|
|
2132
|
+
options.post({
|
|
2133
|
+
jsonrpc: "2.0",
|
|
2134
|
+
id,
|
|
2135
|
+
result
|
|
2136
|
+
});
|
|
2137
|
+
};
|
|
2138
|
+
const refuse = (id, code, message) => {
|
|
2139
|
+
options.post({
|
|
2140
|
+
jsonrpc: "2.0",
|
|
2141
|
+
id,
|
|
2142
|
+
error: {
|
|
2143
|
+
code,
|
|
2144
|
+
message
|
|
2145
|
+
}
|
|
2146
|
+
});
|
|
2147
|
+
};
|
|
2148
|
+
const callTool = (id, params) => {
|
|
2149
|
+
if (initialization !== "ready") {
|
|
2150
|
+
refuse(id, HOST_ERROR, "This app view has not initialized.");
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
const name = isPlainObject(params) ? params["name"] : void 0;
|
|
2154
|
+
const runtimeName = typeof name === "string" && Object.hasOwn(view.callableTools, name) ? view.callableTools[name] : void 0;
|
|
2155
|
+
if (!isPlainObject(params) || runtimeName === void 0) {
|
|
2156
|
+
refuse(id, INVALID_PARAMS, "This tool is not available to this view.");
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
const rawArguments = params["arguments"] === void 0 ? {} : params["arguments"];
|
|
2160
|
+
if (!isPlainObject(rawArguments)) {
|
|
2161
|
+
refuse(id, INVALID_PARAMS, "Tool arguments must be an object.");
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
if (callInFlight) {
|
|
2165
|
+
refuse(id, HOST_ERROR, "Another tool call from this view is still running.");
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
callInFlight = true;
|
|
2169
|
+
let outcome;
|
|
2170
|
+
try {
|
|
2171
|
+
outcome = options.callAppTool(runtimeName, rawArguments);
|
|
2172
|
+
} catch (error) {
|
|
2173
|
+
outcome = Promise.reject(error);
|
|
2174
|
+
}
|
|
2175
|
+
outcome.then((settled) => {
|
|
2176
|
+
callInFlight = false;
|
|
2177
|
+
if (!disposed) respond(id, outcomeToCallToolResult(settled));
|
|
2178
|
+
}, (error) => {
|
|
2179
|
+
callInFlight = false;
|
|
2180
|
+
if (!disposed) refuse(id, HOST_ERROR, errorMessage(error));
|
|
2181
|
+
});
|
|
2182
|
+
};
|
|
2183
|
+
const handleRequest = (id, method, params) => {
|
|
2184
|
+
switch (method) {
|
|
2185
|
+
case "ui/initialize":
|
|
2186
|
+
if (initialization === "uninitialized") initialization = "initializing";
|
|
2187
|
+
respond(id, {
|
|
2188
|
+
protocolVersion: APP_VIEW_PROTOCOL_VERSION,
|
|
2189
|
+
hostCapabilities: { serverTools: {} },
|
|
2190
|
+
hostInfo: options.hostInfo,
|
|
2191
|
+
hostContext: options.hostContext
|
|
2192
|
+
});
|
|
2193
|
+
return;
|
|
2194
|
+
case "tools/call":
|
|
2195
|
+
callTool(id, params);
|
|
2196
|
+
return;
|
|
2197
|
+
case "ping":
|
|
2198
|
+
respond(id, {});
|
|
2199
|
+
return;
|
|
2200
|
+
case "ui/request-display-mode":
|
|
2201
|
+
respond(id, { mode: "inline" });
|
|
2202
|
+
return;
|
|
2203
|
+
default: refuse(id, METHOD_NOT_FOUND, `This host does not support ${method}.`);
|
|
2204
|
+
}
|
|
2205
|
+
};
|
|
2206
|
+
const handleNotification = (method, params) => {
|
|
2207
|
+
switch (method) {
|
|
2208
|
+
case SANDBOX_PROXY_READY:
|
|
2209
|
+
if (sandboxAnswered) return;
|
|
2210
|
+
sandboxAnswered = true;
|
|
2211
|
+
options.post({
|
|
2212
|
+
jsonrpc: "2.0",
|
|
2213
|
+
method: SANDBOX_RESOURCE_READY,
|
|
2214
|
+
params: { html: view.html }
|
|
2215
|
+
});
|
|
2216
|
+
return;
|
|
2217
|
+
case "ui/notifications/initialized":
|
|
2218
|
+
if (initialization !== "initializing") return;
|
|
2219
|
+
initialization = "ready";
|
|
2220
|
+
options.post({
|
|
2221
|
+
jsonrpc: "2.0",
|
|
2222
|
+
method: "ui/notifications/tool-input",
|
|
2223
|
+
params: { arguments: view.toolInput ?? {} }
|
|
2224
|
+
});
|
|
2225
|
+
options.post({
|
|
2226
|
+
jsonrpc: "2.0",
|
|
2227
|
+
method: "ui/notifications/tool-result",
|
|
2228
|
+
params: options.toolResult
|
|
2229
|
+
});
|
|
2230
|
+
return;
|
|
2231
|
+
case "ui/notifications/size-changed": {
|
|
2232
|
+
const height = isPlainObject(params) ? params["height"] : void 0;
|
|
2233
|
+
if (typeof height !== "number" || !Number.isFinite(height)) return;
|
|
2234
|
+
options.onHeight(Math.min(640, Math.max(48, Math.round(height))));
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
default: return;
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
return {
|
|
2241
|
+
handleMessage(event) {
|
|
2242
|
+
if (disposed) return;
|
|
2243
|
+
const frame = options.source();
|
|
2244
|
+
if (frame === null || event.source !== frame) return;
|
|
2245
|
+
const data = event.data;
|
|
2246
|
+
if (!isPlainObject(data) || data["jsonrpc"] !== "2.0") return;
|
|
2247
|
+
const { method, id } = data;
|
|
2248
|
+
if (typeof method !== "string") return;
|
|
2249
|
+
if (id === void 0) handleNotification(method, data["params"]);
|
|
2250
|
+
else if (typeof id === "string" || typeof id === "number") handleRequest(id, method, data["params"]);
|
|
2251
|
+
},
|
|
2252
|
+
dispose() {
|
|
2253
|
+
if (disposed) return;
|
|
2254
|
+
disposed = true;
|
|
2255
|
+
if (initialization === "ready") options.post({
|
|
2256
|
+
jsonrpc: "2.0",
|
|
2257
|
+
id: "teardown",
|
|
2258
|
+
method: "ui/resource-teardown",
|
|
2259
|
+
params: { reason: "closed" }
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
//#endregion
|
|
2265
|
+
//#region src/app-view/host-context.ts
|
|
2266
|
+
const AppViewHostContext = (0, react.createContext)(null);
|
|
2267
|
+
function readAppViewHostContext(platform) {
|
|
2268
|
+
return {
|
|
2269
|
+
theme: typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
|
|
2270
|
+
displayMode: "inline",
|
|
2271
|
+
availableDisplayModes: ["inline"],
|
|
2272
|
+
containerDimensions: { maxHeight: 640 },
|
|
2273
|
+
platform,
|
|
2274
|
+
locale: typeof navigator === "undefined" ? "en-US" : navigator.language,
|
|
2275
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
//#endregion
|
|
2279
|
+
//#region src/components/AppView.tsx
|
|
2280
|
+
const INITIAL_HEIGHT_PX = 160;
|
|
2281
|
+
const APP_VIEW_UNAVAILABLE_TEXT = "This tool returned an app view, which this window cannot show.";
|
|
2282
|
+
function AppView({ tool }) {
|
|
2283
|
+
const host = (0, react.useContext)(AppViewHostContext);
|
|
2284
|
+
if (tool.appView === void 0) return null;
|
|
2285
|
+
const viewKey = appViewKey(tool);
|
|
2286
|
+
if (host === null || host.sandboxUrl === void 0 || viewKey === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2287
|
+
className: "text-[13px] text-subtle",
|
|
2288
|
+
children: APP_VIEW_UNAVAILABLE_TEXT
|
|
2289
|
+
});
|
|
2290
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppViewFrame, {
|
|
2291
|
+
tool,
|
|
2292
|
+
host,
|
|
2293
|
+
sandboxUrl: host.sandboxUrl,
|
|
2294
|
+
viewKey,
|
|
2295
|
+
sourceId: tool.appView.sourceId
|
|
2296
|
+
}, viewKey);
|
|
2297
|
+
}
|
|
2298
|
+
function AppViewFrame({ tool, host, sandboxUrl, viewKey, sourceId }) {
|
|
2299
|
+
const [load, setLoad] = (0, react.useState)({ status: "loading" });
|
|
2300
|
+
const [height, setHeight] = (0, react.useState)(INITIAL_HEIGHT_PX);
|
|
2301
|
+
const frameRef = (0, react.useRef)(null);
|
|
2302
|
+
const toolRef = (0, react.useRef)(tool);
|
|
2303
|
+
toolRef.current = tool;
|
|
2304
|
+
(0, react.useEffect)(() => {
|
|
2305
|
+
const controller = new AbortController();
|
|
2306
|
+
setLoad({ status: "loading" });
|
|
2307
|
+
host.readAppView(viewKey, controller.signal).then((view) => {
|
|
2308
|
+
if (!controller.signal.aborted) setLoad({
|
|
2309
|
+
status: "ready",
|
|
2310
|
+
view,
|
|
2311
|
+
host,
|
|
2312
|
+
sandboxUrl,
|
|
2313
|
+
signal: controller.signal
|
|
2314
|
+
});
|
|
2315
|
+
}, (error) => {
|
|
2316
|
+
if (!controller.signal.aborted) setLoad({
|
|
2317
|
+
status: "failed",
|
|
2318
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
2319
|
+
});
|
|
2320
|
+
});
|
|
2321
|
+
return () => {
|
|
2322
|
+
controller.abort();
|
|
2323
|
+
};
|
|
2324
|
+
}, [
|
|
2325
|
+
viewKey,
|
|
2326
|
+
host.readAppView,
|
|
2327
|
+
host.callAppTool,
|
|
2328
|
+
sandboxUrl
|
|
2329
|
+
]);
|
|
2330
|
+
const ready = load.status === "ready" && load.host.readAppView === host.readAppView && load.host.callAppTool === host.callAppTool && load.sandboxUrl === sandboxUrl ? load : null;
|
|
2331
|
+
(0, react.useLayoutEffect)(() => {
|
|
2332
|
+
const frame = frameRef.current;
|
|
2333
|
+
if (ready === null || frame === null) return void 0;
|
|
2334
|
+
const current = ready.host;
|
|
2335
|
+
const view = ready.view;
|
|
2336
|
+
const bridge = createAppViewBridge({
|
|
2337
|
+
view,
|
|
2338
|
+
toolResult: toCallToolResult(toolRef.current, view),
|
|
2339
|
+
hostInfo: current.hostInfo,
|
|
2340
|
+
hostContext: readAppViewHostContext(current.platform),
|
|
2341
|
+
post: (message) => frame.contentWindow?.postMessage(message, "*"),
|
|
2342
|
+
source: () => frame.contentWindow,
|
|
2343
|
+
callAppTool: (name, args) => current.callAppTool(viewKey, name, args, ready.signal),
|
|
2344
|
+
onHeight: setHeight
|
|
2345
|
+
});
|
|
2346
|
+
const onMessage = (event) => bridge.handleMessage(event);
|
|
2347
|
+
window.addEventListener("message", onMessage);
|
|
2348
|
+
return () => {
|
|
2349
|
+
window.removeEventListener("message", onMessage);
|
|
2350
|
+
bridge.dispose();
|
|
2351
|
+
};
|
|
2352
|
+
}, [ready, viewKey]);
|
|
2353
|
+
if (load.status === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2354
|
+
className: "text-[13px] text-subtle",
|
|
2355
|
+
children: [
|
|
2356
|
+
"The app from ",
|
|
2357
|
+
sourceId,
|
|
2358
|
+
" could not be shown: ",
|
|
2359
|
+
load.reason
|
|
2360
|
+
]
|
|
2361
|
+
});
|
|
2362
|
+
if (ready === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2363
|
+
className: "text-[13px] text-subtle",
|
|
2364
|
+
children: [
|
|
2365
|
+
"Loading the app from ",
|
|
2366
|
+
sourceId,
|
|
2367
|
+
"…"
|
|
2368
|
+
]
|
|
2369
|
+
});
|
|
2370
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2371
|
+
className: "my-1 flex flex-col gap-1",
|
|
2372
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2373
|
+
className: "text-[12px] text-subtle",
|
|
2374
|
+
children: ["App from ", sourceId]
|
|
2375
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
|
|
2376
|
+
ref: frameRef,
|
|
2377
|
+
sandbox: "allow-scripts",
|
|
2378
|
+
src: sandboxUrl,
|
|
2379
|
+
title: `App from ${sourceId}`,
|
|
2380
|
+
referrerPolicy: "no-referrer",
|
|
2381
|
+
"data-testid": "app-view-frame",
|
|
2382
|
+
style: {
|
|
2383
|
+
width: "100%",
|
|
2384
|
+
height
|
|
2385
|
+
},
|
|
2386
|
+
className: "block rounded-lg border border-border bg-card"
|
|
2387
|
+
})]
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
//#endregion
|
|
1925
2391
|
//#region src/components/DiffLines.tsx
|
|
1926
2392
|
/**
|
|
1927
2393
|
* #3288: renders a unified diff already built server-side (the ONE diff builder, in
|
|
@@ -2435,42 +2901,49 @@ function ToolGroup({ tools }) {
|
|
|
2435
2901
|
if (visible.length === 0) return null;
|
|
2436
2902
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2437
2903
|
className: "text-[14px]",
|
|
2438
|
-
children: [
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2904
|
+
children: [
|
|
2905
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2906
|
+
type: "button",
|
|
2907
|
+
"aria-expanded": open,
|
|
2908
|
+
onClick: () => setOpen((value) => !value),
|
|
2909
|
+
className: "group -mx-2 flex max-w-full items-center gap-2.5 rounded-lg px-2 py-1 text-left text-muted-foreground hover:bg-hover hover:text-foreground",
|
|
2910
|
+
children: [
|
|
2911
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Wrench, {
|
|
2912
|
+
size: 15,
|
|
2913
|
+
strokeWidth: 1.75,
|
|
2914
|
+
className: "flex-shrink-0 text-subtle"
|
|
2915
|
+
}),
|
|
2916
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2917
|
+
className: "flex-shrink-0",
|
|
2918
|
+
children: [
|
|
2919
|
+
visible.length,
|
|
2920
|
+
" tool ",
|
|
2921
|
+
visible.length === 1 ? "call" : "calls"
|
|
2922
|
+
]
|
|
2923
|
+
}),
|
|
2924
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2925
|
+
className: "min-w-0 truncate text-subtle",
|
|
2926
|
+
children: names
|
|
2927
|
+
}),
|
|
2928
|
+
failed > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2929
|
+
className: "flex-shrink-0 rounded-md bg-destructive/12 px-1.5 text-[12.5px] text-destructive",
|
|
2930
|
+
children: [failed, " failed"]
|
|
2931
|
+
}),
|
|
2932
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
|
|
2933
|
+
size: 14,
|
|
2934
|
+
className: `flex-shrink-0 text-subtle transition-transform ${open ? "rotate-90" : ""}`
|
|
2935
|
+
})
|
|
2936
|
+
]
|
|
2937
|
+
}),
|
|
2938
|
+
visible.filter((tool) => tool.appView !== void 0).map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2939
|
+
className: "pl-[25px]",
|
|
2940
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppView, { tool })
|
|
2941
|
+
}, tool.id)),
|
|
2942
|
+
open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2943
|
+
className: "mt-0.5 flex flex-col pl-[25px]",
|
|
2944
|
+
children: visible.map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCard, { tool }, tool.id))
|
|
2945
|
+
})
|
|
2946
|
+
]
|
|
2474
2947
|
});
|
|
2475
2948
|
}
|
|
2476
2949
|
/** #3288: a turn that changed files ends with this compact row — a click opens that file's diff. */
|
|
@@ -2596,7 +3069,10 @@ function ConversationView({ messages, activeTools, streamingText, isThinking, ow
|
|
|
2596
3069
|
isThinking && !streamingText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThinkingIndicator, {}),
|
|
2597
3070
|
activeTools.some((tool) => !tool.internal) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2598
3071
|
className: "flex flex-col",
|
|
2599
|
-
children: activeTools.filter((tool) => !tool.internal).map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCard, { tool }, tool.
|
|
3072
|
+
children: activeTools.filter((tool) => !tool.internal).map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.default.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCard, { tool }), tool.appView !== void 0 && tool.status !== "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3073
|
+
className: "pl-[25px]",
|
|
3074
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppView, { tool })
|
|
3075
|
+
})] }, tool.id))
|
|
2600
3076
|
}),
|
|
2601
3077
|
streamingText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentBlock, {
|
|
2602
3078
|
content: streamingText,
|
|
@@ -7536,6 +8012,7 @@ const NARROW_WINDOW_QUERY = "(max-width: 767.98px)";
|
|
|
7536
8012
|
*/
|
|
7537
8013
|
/** The column every part of the conversation shares, so messages, prompt and composer line up. */
|
|
7538
8014
|
const COLUMN = "mx-auto w-full max-w-[760px] px-6";
|
|
8015
|
+
const APP_VIEW_HOST_VERSION = "1.0.0";
|
|
7539
8016
|
/**
|
|
7540
8017
|
* Designed empty state shown before the first turn. Renders in the same slot as `ConversationView`
|
|
7541
8018
|
* (never together), so it carries the same `main` landmark and label (#3289 §3 review) — nothing
|
|
@@ -7602,8 +8079,25 @@ function SetupPanel({ onSetUp, disabled }) {
|
|
|
7602
8079
|
* The full desktop layout over an `IWsSessionState`. `surface` is an optional label shown next to the
|
|
7603
8080
|
* mark (e.g. "app").
|
|
7604
8081
|
*/
|
|
7605
|
-
function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor }) {
|
|
8082
|
+
function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
|
|
7606
8083
|
const [view, setView] = (0, react.useState)("chat");
|
|
8084
|
+
const { identity } = useProductIdentity();
|
|
8085
|
+
const appViewHost = (0, react.useMemo)(() => ({
|
|
8086
|
+
...appViewSandboxUrl !== void 0 ? { sandboxUrl: appViewSandboxUrl } : {},
|
|
8087
|
+
readAppView: state.readAppView,
|
|
8088
|
+
callAppTool: state.callAppTool,
|
|
8089
|
+
hostInfo: {
|
|
8090
|
+
name: identity.displayName,
|
|
8091
|
+
version: APP_VIEW_HOST_VERSION
|
|
8092
|
+
},
|
|
8093
|
+
platform: surface === "app" ? "desktop" : "web"
|
|
8094
|
+
}), [
|
|
8095
|
+
appViewSandboxUrl,
|
|
8096
|
+
state.readAppView,
|
|
8097
|
+
state.callAppTool,
|
|
8098
|
+
identity.displayName,
|
|
8099
|
+
surface
|
|
8100
|
+
]);
|
|
7607
8101
|
const composerRef = (0, react.useRef)(null);
|
|
7608
8102
|
const tasks = state.executionWorkspace?.entries ?? [];
|
|
7609
8103
|
const hasTasks = tasks.some((entry) => entry.kind !== "main_thread");
|
|
@@ -7732,12 +8226,15 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
|
|
|
7732
8226
|
args: "add"
|
|
7733
8227
|
}),
|
|
7734
8228
|
disabled: state.status !== "connected"
|
|
7735
|
-
}) : isEmpty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
8229
|
+
}) : isEmpty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppViewHostContext.Provider, {
|
|
8230
|
+
value: appViewHost,
|
|
8231
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationView, {
|
|
8232
|
+
messages: state.messages,
|
|
8233
|
+
activeTools: state.activeTools,
|
|
8234
|
+
streamingText: state.streamingText,
|
|
8235
|
+
isThinking: state.isThinking,
|
|
8236
|
+
ownDriverId: state.ownDriverId
|
|
8237
|
+
})
|
|
7741
8238
|
})
|
|
7742
8239
|
}),
|
|
7743
8240
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|