@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.
- package/CHANGELOG.md +100 -0
- package/dist/browser/index.d.ts +24 -3
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +641 -69
- package/dist/browser/index.js.map +1 -1
- package/dist/node/index.cjs +639 -67
- package/dist/node/index.d.cts +24 -3
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +24 -3
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +641 -69
- package/dist/node/index.js.map +1 -1
- package/docs/SPEC.md +9 -3
- 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/PermissionPrompt.tsx +108 -7
- package/src/components/SessionSurface.tsx +39 -12
- package/src/hooks/server-message-handling.ts +3 -0
- 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,6 +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: "reducer-state",
|
|
315
|
+
app_tool_result: "reducer-state",
|
|
314
316
|
background_task_event: "intentionally-not-rendered",
|
|
315
317
|
background_job_group_event: "intentionally-not-rendered",
|
|
316
318
|
plan_event: "intentionally-not-rendered",
|
|
@@ -335,10 +337,10 @@ const SERVER_MESSAGE_HANDLING = {
|
|
|
335
337
|
};
|
|
336
338
|
//#endregion
|
|
337
339
|
//#region src/hooks/use-agent-switcher-state.ts
|
|
338
|
-
let requestCounter$
|
|
339
|
-
function nextRequestId$
|
|
340
|
-
requestCounter$
|
|
341
|
-
return `${prefix}_${requestCounter$
|
|
340
|
+
let requestCounter$7 = 0;
|
|
341
|
+
function nextRequestId$7(prefix) {
|
|
342
|
+
requestCounter$7 += 1;
|
|
343
|
+
return `${prefix}_${requestCounter$7}_${Date.now()}`;
|
|
342
344
|
}
|
|
343
345
|
/**
|
|
344
346
|
* #3282 §4 part b-3: the agent switcher sheet's own correlated request/response state, the same
|
|
@@ -373,7 +375,7 @@ function useAgentSwitcherState(send) {
|
|
|
373
375
|
return true;
|
|
374
376
|
}, []);
|
|
375
377
|
const requestAgentDefinitions = (0, react.useCallback)(() => {
|
|
376
|
-
const requestId = nextRequestId$
|
|
378
|
+
const requestId = nextRequestId$7("get_agent_definitions");
|
|
377
379
|
listRequestRef.current = requestId;
|
|
378
380
|
send({
|
|
379
381
|
type: "get-agent-definitions",
|
|
@@ -396,7 +398,7 @@ function useAgentSwitcherState(send) {
|
|
|
396
398
|
setAgentSwitcherOpen(false);
|
|
397
399
|
}, []),
|
|
398
400
|
selectAgent: (0, react.useCallback)((agentType) => {
|
|
399
|
-
const requestId = nextRequestId$
|
|
401
|
+
const requestId = nextRequestId$7("agent_switch");
|
|
400
402
|
switchRequestRef.current = requestId;
|
|
401
403
|
setAgentSwitchMessage(null);
|
|
402
404
|
send({
|
|
@@ -417,6 +419,126 @@ function useAgentSwitcherState(send) {
|
|
|
417
419
|
};
|
|
418
420
|
}
|
|
419
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
|
|
420
542
|
//#region src/hooks/use-execution-detail.ts
|
|
421
543
|
/**
|
|
422
544
|
* #3288 §1: the detail sheet's own correlated request/response state — which entry is open, its
|
|
@@ -1101,7 +1223,8 @@ function toolEndFields(state) {
|
|
|
1101
1223
|
...state.diffLines ? { diffLines: state.diffLines } : {},
|
|
1102
1224
|
...state.diffFile ? { diffFile: state.diffFile } : {},
|
|
1103
1225
|
...state.toolResultData !== void 0 ? { toolResultData: state.toolResultData } : {},
|
|
1104
|
-
...state.toolResultParts ? { toolResultParts: state.toolResultParts } : {}
|
|
1226
|
+
...state.toolResultParts ? { toolResultParts: state.toolResultParts } : {},
|
|
1227
|
+
...state.appView ? { appView: state.appView } : {}
|
|
1105
1228
|
};
|
|
1106
1229
|
}
|
|
1107
1230
|
/** Apply a `tool_end` to the ONE matching running tool across a turn's segments (never all of them). */
|
|
@@ -1199,6 +1322,7 @@ function buildConversationEntriesFromDisplaySegments(displaySegments) {
|
|
|
1199
1322
|
}
|
|
1200
1323
|
function useSessionClient(makeClient) {
|
|
1201
1324
|
const [status, setStatus] = (0, react.useState)("disconnected");
|
|
1325
|
+
const statusRef = (0, react.useRef)("disconnected");
|
|
1202
1326
|
const [messages, setMessages] = (0, react.useState)([]);
|
|
1203
1327
|
const [activeTools, setActiveTools] = (0, react.useState)([]);
|
|
1204
1328
|
const [streamingText, setStreamingText] = (0, react.useState)("");
|
|
@@ -1323,6 +1447,7 @@ function useSessionClient(makeClient) {
|
|
|
1323
1447
|
const { handleExecutionDetailMessage, closeExecutionDetail, ...executionDetailState } = useExecutionDetailState(send);
|
|
1324
1448
|
const { handleModelListMessage, ...modelListState } = useModelListState(send);
|
|
1325
1449
|
const { handleProjectMessage, ...projectPanelState } = useProjectPanelState(send);
|
|
1450
|
+
const { handleAppViewMessage, rejectPendingAppRequests, readAppView, callAppTool } = useAppViewRequests(send, (0, react.useCallback)(() => statusRef.current === "connected", []));
|
|
1326
1451
|
const { handleSessionsMessage, canListSessions, markCurrent, armRestore, ...sessionDirectoryState } = useSessionDirectoryState(send);
|
|
1327
1452
|
const { requestSessions, setSessionSidebarOpen } = sessionDirectoryState;
|
|
1328
1453
|
const { handleSettingsMessage, openSettings, refreshSettings, ...settingsState } = useSettingsState(send);
|
|
@@ -1336,6 +1461,7 @@ function useSessionClient(makeClient) {
|
|
|
1336
1461
|
if (handleExecutionDetailMessage(msg)) return;
|
|
1337
1462
|
if (handleModelListMessage(msg)) return;
|
|
1338
1463
|
if (handleProjectMessage(msg)) return;
|
|
1464
|
+
if (handleAppViewMessage(msg)) return;
|
|
1339
1465
|
if (handleSessionsMessage(msg)) return;
|
|
1340
1466
|
if (handleSettingsMessage(msg)) return;
|
|
1341
1467
|
if (handleAgentDefinitionsMessage(msg)) return;
|
|
@@ -1472,6 +1598,7 @@ function useSessionClient(makeClient) {
|
|
|
1472
1598
|
requestSessions();
|
|
1473
1599
|
break;
|
|
1474
1600
|
case "session_switched":
|
|
1601
|
+
rejectPendingAppRequests("The active session changed.");
|
|
1475
1602
|
settleSessionChange();
|
|
1476
1603
|
sessionStatusRef.current = null;
|
|
1477
1604
|
setSessionStatus(null);
|
|
@@ -1496,6 +1623,7 @@ function useSessionClient(makeClient) {
|
|
|
1496
1623
|
requestSchedules();
|
|
1497
1624
|
break;
|
|
1498
1625
|
case "history_cleared":
|
|
1626
|
+
rejectPendingAppRequests("The session history was cleared.");
|
|
1499
1627
|
streamingTextRef.current = "";
|
|
1500
1628
|
turnSegmentsRef.current = [];
|
|
1501
1629
|
setStreamingText("");
|
|
@@ -1636,6 +1764,7 @@ function useSessionClient(makeClient) {
|
|
|
1636
1764
|
closeExecutionDetail,
|
|
1637
1765
|
finishTurn,
|
|
1638
1766
|
handleAgentDefinitionsMessage,
|
|
1767
|
+
handleAppViewMessage,
|
|
1639
1768
|
handleExecutionDetailMessage,
|
|
1640
1769
|
handleModelListMessage,
|
|
1641
1770
|
handleProjectMessage,
|
|
@@ -1650,6 +1779,7 @@ function useSessionClient(makeClient) {
|
|
|
1650
1779
|
requestSchedules,
|
|
1651
1780
|
requestSessions,
|
|
1652
1781
|
resolveSwitchResult,
|
|
1782
|
+
rejectPendingAppRequests,
|
|
1653
1783
|
send,
|
|
1654
1784
|
setSessionSidebarOpen,
|
|
1655
1785
|
settleSessionChange,
|
|
@@ -1668,7 +1798,9 @@ function useSessionClient(makeClient) {
|
|
|
1668
1798
|
}, []);
|
|
1669
1799
|
(0, react.useEffect)(() => {
|
|
1670
1800
|
const onStatusChange = (next) => {
|
|
1801
|
+
statusRef.current = next;
|
|
1671
1802
|
setStatus(next);
|
|
1803
|
+
if (next !== "connected") rejectPendingAppRequests();
|
|
1672
1804
|
if (next === "connected") {
|
|
1673
1805
|
commandsInFlightRef.current = 0;
|
|
1674
1806
|
silentCommandRequestIdsRef.current.clear();
|
|
@@ -1692,13 +1824,15 @@ function useSessionClient(makeClient) {
|
|
|
1692
1824
|
return () => {
|
|
1693
1825
|
client.disconnect();
|
|
1694
1826
|
clientRef.current = null;
|
|
1827
|
+
rejectPendingAppRequests();
|
|
1695
1828
|
};
|
|
1696
1829
|
}, [
|
|
1697
1830
|
makeClient,
|
|
1698
1831
|
handleMessage,
|
|
1699
1832
|
requestSessions,
|
|
1700
1833
|
requestSchedules,
|
|
1701
|
-
armRestore
|
|
1834
|
+
armRestore,
|
|
1835
|
+
rejectPendingAppRequests
|
|
1702
1836
|
]);
|
|
1703
1837
|
return {
|
|
1704
1838
|
status,
|
|
@@ -1730,6 +1864,8 @@ function useSessionClient(makeClient) {
|
|
|
1730
1864
|
openAgentSwitcher,
|
|
1731
1865
|
...agentSwitcherState,
|
|
1732
1866
|
...schedulesState,
|
|
1867
|
+
readAppView,
|
|
1868
|
+
callAppTool,
|
|
1733
1869
|
helpOpen,
|
|
1734
1870
|
openHelp,
|
|
1735
1871
|
closeHelp
|
|
@@ -1920,6 +2056,338 @@ function driverAttributionText(driverId) {
|
|
|
1920
2056
|
return `from ${humanDriverLabel(driverId)}`;
|
|
1921
2057
|
}
|
|
1922
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
|
|
1923
2391
|
//#region src/components/DiffLines.tsx
|
|
1924
2392
|
/**
|
|
1925
2393
|
* #3288: renders a unified diff already built server-side (the ONE diff builder, in
|
|
@@ -2433,42 +2901,49 @@ function ToolGroup({ tools }) {
|
|
|
2433
2901
|
if (visible.length === 0) return null;
|
|
2434
2902
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2435
2903
|
className: "text-[14px]",
|
|
2436
|
-
children: [
|
|
2437
|
-
|
|
2438
|
-
|
|
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
|
-
|
|
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
|
+
]
|
|
2472
2947
|
});
|
|
2473
2948
|
}
|
|
2474
2949
|
/** #3288: a turn that changed files ends with this compact row — a click opens that file's diff. */
|
|
@@ -2594,7 +3069,10 @@ function ConversationView({ messages, activeTools, streamingText, isThinking, ow
|
|
|
2594
3069
|
isThinking && !streamingText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThinkingIndicator, {}),
|
|
2595
3070
|
activeTools.some((tool) => !tool.internal) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2596
3071
|
className: "flex flex-col",
|
|
2597
|
-
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))
|
|
2598
3076
|
}),
|
|
2599
3077
|
streamingText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AgentBlock, {
|
|
2600
3078
|
content: streamingText,
|
|
@@ -3237,6 +3715,26 @@ function isEditableElement(element) {
|
|
|
3237
3715
|
function isDestructiveAskOption(requestId, value) {
|
|
3238
3716
|
return requestId === "provider-profile-action" && value === "delete";
|
|
3239
3717
|
}
|
|
3718
|
+
/**
|
|
3719
|
+
* The multi-select shape of an ask, or undefined for every other ask (which answers on click). The
|
|
3720
|
+
* rules mirror the terminal checklist (`MultiSelectList` in agent-ui-terminal): `minSelect` (default 1)
|
|
3721
|
+
* gates the submit, and `maxSelect` caps the ticks — a toggle beyond the cap is ignored, never swapped
|
|
3722
|
+
* for the oldest tick. `default.values` pre-select; a default that names no option is dropped, and
|
|
3723
|
+
* more defaults than the cap keep the first ones in option order, so the answer never breaks the cap.
|
|
3724
|
+
*/
|
|
3725
|
+
function readMultiSelect(request) {
|
|
3726
|
+
const max = request.maxSelect ?? 1;
|
|
3727
|
+
if (max <= 1) return void 0;
|
|
3728
|
+
const options = (request.options ?? []).filter((opt) => !isDestructiveAskOption(request.id, opt.value));
|
|
3729
|
+
const wanted = new Set(request.default?.values ?? []);
|
|
3730
|
+
const defaults = options.filter((opt) => wanted.has(opt.value)).slice(0, max);
|
|
3731
|
+
return {
|
|
3732
|
+
min: request.minSelect ?? 1,
|
|
3733
|
+
max,
|
|
3734
|
+
options,
|
|
3735
|
+
defaults: new Set(defaults.map((opt) => opt.value))
|
|
3736
|
+
};
|
|
3737
|
+
}
|
|
3240
3738
|
function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverId = null, layout = "modal", armDelayMs = 450, onFocusReturn }) {
|
|
3241
3739
|
const prompt = prompts[0];
|
|
3242
3740
|
const promptId = prompt?.id;
|
|
@@ -3248,6 +3746,7 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
|
|
|
3248
3746
|
const [fieldFocused, setFieldFocused] = (0, react.useState)(false);
|
|
3249
3747
|
const fieldHandleRef = (0, react.useRef)(null);
|
|
3250
3748
|
const pendingFocusReturnRef = (0, react.useRef)(false);
|
|
3749
|
+
const [selection, setSelection] = (0, react.useState)(void 0);
|
|
3251
3750
|
(0, react.useEffect)(() => {
|
|
3252
3751
|
if (promptId === void 0 || armDelayMs <= 0) return void 0;
|
|
3253
3752
|
const timer = setTimeout(() => setArmedId(promptId), armDelayMs);
|
|
@@ -3312,6 +3811,28 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
|
|
|
3312
3811
|
fieldHandleRef.current?.clear();
|
|
3313
3812
|
onAnswerAsk(id, response);
|
|
3314
3813
|
};
|
|
3814
|
+
const multi = prompt.kind === "ask" ? readMultiSelect(prompt.request) : void 0;
|
|
3815
|
+
const ticks = selection?.promptId === prompt.id ? selection.values : multi?.defaults;
|
|
3816
|
+
const selectedValues = multi && ticks ? multi.options.map((opt) => opt.value).filter((value) => ticks.has(value)) : [];
|
|
3817
|
+
const canSubmit = multi !== void 0 && selectedValues.length >= multi.min;
|
|
3818
|
+
const toggleOption = (value) => {
|
|
3819
|
+
if (!multi) return;
|
|
3820
|
+
setSelection((current) => {
|
|
3821
|
+
const next = new Set(current?.promptId === prompt.id ? current.values : multi.defaults);
|
|
3822
|
+
if (next.has(value)) next.delete(value);
|
|
3823
|
+
else if (next.size < multi.max) next.add(value);
|
|
3824
|
+
return {
|
|
3825
|
+
promptId: prompt.id,
|
|
3826
|
+
values: next
|
|
3827
|
+
};
|
|
3828
|
+
});
|
|
3829
|
+
};
|
|
3830
|
+
const submitSelection = () => {
|
|
3831
|
+
if (canSubmit) answerAsk(prompt.id, {
|
|
3832
|
+
type: "answer",
|
|
3833
|
+
values: selectedValues
|
|
3834
|
+
});
|
|
3835
|
+
};
|
|
3315
3836
|
/** A click anywhere in the prompt that isn't on a button or the field itself focuses the field. */
|
|
3316
3837
|
const onContainerClick = (event) => {
|
|
3317
3838
|
if (!hasFreeText) return;
|
|
@@ -3331,6 +3852,13 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
|
|
|
3331
3852
|
return;
|
|
3332
3853
|
}
|
|
3333
3854
|
if (!armed) return;
|
|
3855
|
+
if (multi && event.key === "Enter") {
|
|
3856
|
+
if (event.target === event.currentTarget) {
|
|
3857
|
+
event.preventDefault();
|
|
3858
|
+
submitSelection();
|
|
3859
|
+
}
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3334
3862
|
const index = Number(event.key) - 1;
|
|
3335
3863
|
if (!Number.isInteger(index) || index < 0) return;
|
|
3336
3864
|
if (prompt.kind === "permission") {
|
|
@@ -3338,7 +3866,9 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
|
|
|
3338
3866
|
return;
|
|
3339
3867
|
}
|
|
3340
3868
|
const option = (prompt.request.options ?? []).filter((opt) => !isDestructiveAskOption(prompt.request.id, opt.value))[index];
|
|
3341
|
-
if (option)
|
|
3869
|
+
if (!option) return;
|
|
3870
|
+
if (multi) toggleOption(option.value);
|
|
3871
|
+
else answerAsk(prompt.id, {
|
|
3342
3872
|
type: "answer",
|
|
3343
3873
|
values: [option.value]
|
|
3344
3874
|
});
|
|
@@ -3350,7 +3880,7 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
|
|
|
3350
3880
|
const askOptions = prompt.kind === "ask" ? prompt.request.options ?? [] : [];
|
|
3351
3881
|
const destructiveAskOption = askOptions.find((opt) => isDestructiveAskOption(askRequestId, opt.value));
|
|
3352
3882
|
const primaryAskOptions = destructiveAskOption ? askOptions.filter((opt) => opt !== destructiveAskOption) : askOptions;
|
|
3353
|
-
const askArmedHint = hasFreeText && fieldFocused ? "Enter to submit · Esc to cancel" : askOptions.length > 0 ? "1–9 choose · Esc cancel" : "Esc cancels";
|
|
3883
|
+
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";
|
|
3354
3884
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3355
3885
|
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]",
|
|
3356
3886
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -3502,12 +4032,23 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
|
|
|
3502
4032
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3503
4033
|
type: "button",
|
|
3504
4034
|
"aria-keyshortcuts": dock && index < 9 ? String(index + 1) : void 0,
|
|
3505
|
-
|
|
3506
|
-
|
|
4035
|
+
"aria-pressed": multi ? selectedValues.includes(opt.value) : void 0,
|
|
4036
|
+
className: (multi ? selectedValues.includes(opt.value) : index === 0) ? PRIMARY_BUTTON$1 : SECONDARY_BUTTON$1,
|
|
4037
|
+
onClick: onButton(() => multi ? toggleOption(opt.value) : answerAsk(prompt.id, {
|
|
3507
4038
|
type: "answer",
|
|
3508
4039
|
values: [opt.value]
|
|
3509
4040
|
})),
|
|
3510
|
-
children: [
|
|
4041
|
+
children: [
|
|
4042
|
+
multi && (selectedValues.includes(opt.value) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.SquareCheck, {
|
|
4043
|
+
size: 15,
|
|
4044
|
+
strokeWidth: 2
|
|
4045
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Square, {
|
|
4046
|
+
size: 15,
|
|
4047
|
+
strokeWidth: 2
|
|
4048
|
+
})),
|
|
4049
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: opt.label }),
|
|
4050
|
+
dock && index < 9 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Kbd, { children: index + 1 })
|
|
4051
|
+
]
|
|
3511
4052
|
}), opt.description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3512
4053
|
className: "pl-0.5 text-[11.5px] leading-snug text-subtle",
|
|
3513
4054
|
children: opt.description
|
|
@@ -3549,16 +4090,26 @@ function PermissionPrompt({ prompts, onAnswerPermission, onAnswerAsk, ownDriverI
|
|
|
3549
4090
|
}),
|
|
3550
4091
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3551
4092
|
className: "flex flex-wrap items-center gap-2",
|
|
3552
|
-
children: [
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
4093
|
+
children: [
|
|
4094
|
+
multi && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4095
|
+
type: "button",
|
|
4096
|
+
className: `${PRIMARY_BUTTON$1} disabled:opacity-60`,
|
|
4097
|
+
disabled: !canSubmit,
|
|
4098
|
+
onClick: onButton(submitSelection),
|
|
4099
|
+
children: canSubmit ? "Submit" : `Submit (min ${multi.min})`
|
|
4100
|
+
}),
|
|
4101
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4102
|
+
type: "button",
|
|
4103
|
+
className: GHOST_BUTTON,
|
|
4104
|
+
onClick: onButton(() => answerAsk(prompt.id, { type: "cancelled" })),
|
|
4105
|
+
children: "Cancel"
|
|
4106
|
+
}),
|
|
4107
|
+
dock && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyHint, {
|
|
4108
|
+
hasFocus,
|
|
4109
|
+
armed,
|
|
4110
|
+
armedText: askArmedHint
|
|
4111
|
+
})
|
|
4112
|
+
]
|
|
3562
4113
|
})
|
|
3563
4114
|
]
|
|
3564
4115
|
})] })
|
|
@@ -7461,6 +8012,7 @@ const NARROW_WINDOW_QUERY = "(max-width: 767.98px)";
|
|
|
7461
8012
|
*/
|
|
7462
8013
|
/** The column every part of the conversation shares, so messages, prompt and composer line up. */
|
|
7463
8014
|
const COLUMN = "mx-auto w-full max-w-[760px] px-6";
|
|
8015
|
+
const APP_VIEW_HOST_VERSION = "1.0.0";
|
|
7464
8016
|
/**
|
|
7465
8017
|
* Designed empty state shown before the first turn. Renders in the same slot as `ConversationView`
|
|
7466
8018
|
* (never together), so it carries the same `main` landmark and label (#3289 §3 review) — nothing
|
|
@@ -7527,8 +8079,25 @@ function SetupPanel({ onSetUp, disabled }) {
|
|
|
7527
8079
|
* The full desktop layout over an `IWsSessionState`. `surface` is an optional label shown next to the
|
|
7528
8080
|
* mark (e.g. "app").
|
|
7529
8081
|
*/
|
|
7530
|
-
function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor }) {
|
|
8082
|
+
function SessionSurface({ state, surface, personalUsageEnabled = false, onReconnect, pickFiles, getPathForFile, onOpenMemoryInEditor, appViewSandboxUrl }) {
|
|
7531
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
|
+
]);
|
|
7532
8101
|
const composerRef = (0, react.useRef)(null);
|
|
7533
8102
|
const tasks = state.executionWorkspace?.entries ?? [];
|
|
7534
8103
|
const hasTasks = tasks.some((entry) => entry.kind !== "main_thread");
|
|
@@ -7657,12 +8226,15 @@ function SessionSurface({ state, surface, personalUsageEnabled = false, onReconn
|
|
|
7657
8226
|
args: "add"
|
|
7658
8227
|
}),
|
|
7659
8228
|
disabled: state.status !== "connected"
|
|
7660
|
-
}) : isEmpty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EmptyState, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
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
|
+
})
|
|
7666
8238
|
})
|
|
7667
8239
|
}),
|
|
7668
8240
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|