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