@datalayer/agent-runtimes 1.3.74 → 1.3.75
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/lib/chat/ChatFloating.d.ts +17 -1
- package/lib/chat/ChatFloating.js +52 -5
- package/lib/chat/assistant/AssistantContextMenu.d.ts +67 -0
- package/lib/chat/assistant/AssistantContextMenu.js +68 -0
- package/lib/chat/assistant/AssistantStage.d.ts +95 -1
- package/lib/chat/assistant/AssistantStage.js +227 -7
- package/lib/chat/assistant/BalloonParts.d.ts +15 -1
- package/lib/chat/assistant/BalloonParts.js +26 -9
- package/lib/chat/assistant/BalloonVisual.d.ts +10 -2
- package/lib/chat/assistant/BalloonVisual.js +3 -3
- package/lib/chat/assistant/SpeechBalloon.d.ts +47 -1
- package/lib/chat/assistant/SpeechBalloon.js +60 -8
- package/lib/chat/base/ChatBase.js +51 -1
- package/lib/chat/indicators/TypingDots.d.ts +13 -0
- package/lib/chat/indicators/TypingDots.js +21 -0
- package/lib/chat/messages/ChatMessageList.js +1 -15
- package/lib/components/inspector/AgentInspector.d.ts +59 -0
- package/lib/components/inspector/AgentInspector.js +220 -0
- package/lib/components/inspector/AgentInspectorDialog.d.ts +19 -0
- package/lib/components/inspector/AgentInspectorDialog.js +9 -0
- package/lib/components/inspector/JsonTree.d.ts +24 -0
- package/lib/components/inspector/JsonTree.js +89 -0
- package/lib/components/inspector/a2aInspect.d.ts +56 -0
- package/lib/components/inspector/a2aInspect.js +478 -0
- package/lib/components/inspector/agentInspector.d.ts +179 -0
- package/lib/components/inspector/agentInspector.js +267 -0
- package/lib/components/inspector/chatInspect.d.ts +38 -0
- package/lib/components/inspector/chatInspect.js +65 -0
- package/lib/components/inspector/classify.d.ts +19 -0
- package/lib/components/inspector/classify.js +42 -0
- package/lib/components/inspector/index.d.ts +14 -0
- package/lib/components/inspector/index.js +18 -0
- package/lib/components/teams/A2ATeamGraph.d.ts +42 -1
- package/lib/components/teams/A2ATeamGraph.js +24 -7
- package/lib/components/teams/NotebookPreviewView.js +9 -1
- package/lib/components/teams/TeamNotebook.d.ts +3 -1
- package/lib/components/teams/TeamNotebook.js +3 -3
- package/lib/components/teams/jupyterThemeVariables.d.ts +9 -0
- package/lib/components/teams/jupyterThemeVariables.js +441 -0
- package/lib/components/teams/teamConnections.d.ts +9 -1
- package/lib/components/teams/teamConnections.js +20 -0
- package/lib/components/teams/useA2ATeam.d.ts +17 -4
- package/lib/components/teams/useA2ATeam.js +87 -23
- package/lib/examples/AgentA2ATeamExample.d.ts +13 -0
- package/lib/examples/AgentA2ATeamExample.js +61 -13
- package/lib/examples/AssistantExample.js +62 -2
- package/lib/loop/apps/AppRenderer.js +1 -1
- package/lib/loop/core/index.d.ts +17 -0
- package/lib/loop/core/index.js +1 -0
- package/lib/loop/plugins/assistant/index.d.ts +1 -1
- package/lib/loop/plugins/assistant/index.js +8 -3
- package/lib/runtimes/browser/a2aPeer.d.ts +4 -1
- package/lib/types/chat.d.ts +7 -0
- package/package.json +1 -1
- package/scripts/codegen/generate_jupyter_theme_variables.mjs +57 -0
|
@@ -58,6 +58,8 @@ const EphemeralDocument = React.lazy(() => import('../document/EphemeralDocument
|
|
|
58
58
|
default: m.EphemeralDocument,
|
|
59
59
|
})));
|
|
60
60
|
import { useNotebookTools } from '../../tools/adapters/agent-runtimes/notebookHooks';
|
|
61
|
+
import { useAgentInspectorSink } from '../../components/inspector/agentInspector';
|
|
62
|
+
import { chatInspectorRecorder, } from '../../components/inspector/chatInspect';
|
|
61
63
|
// Tracks pending prompts already auto-sent for a given conversation scope.
|
|
62
64
|
// This prevents layout-driven unmount/remount cycles from re-sending prompts.
|
|
63
65
|
const sentPendingPromptKeys = new Set();
|
|
@@ -513,7 +515,7 @@ useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, onSendReady
|
|
|
513
515
|
// Extended props
|
|
514
516
|
brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, voice, voiceSpeaking = false, onStopSpeaking, showInformation = false, onInformationClick, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, onItemsChange, onDisplayItemsChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools: frontendToolsProp, enableEphemeralNotebook = false, initialEphemeralNotebookOpen = true, onEphemeralNotebookOpenChange, enableEphemeralDocument = false, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, ephemeralNotebookToolbarExtraItems, ephemeralDocumentToolbarExtraItems, ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration, ephemeralRuntimeOverride,
|
|
515
517
|
// Tool invocation hooks
|
|
516
|
-
onToolCallStart, onToolCallComplete,
|
|
518
|
+
onToolCallStart, onToolCallComplete, inspector: inspectorGiven,
|
|
517
519
|
// Identity/Authorization props
|
|
518
520
|
onAuthorizationRequired: _onAuthorizationRequired, connectedIdentities,
|
|
519
521
|
// Conversation persistence
|
|
@@ -1589,6 +1591,25 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1589
1591
|
onToolCallStartRef.current = onToolCallStart;
|
|
1590
1592
|
const onToolCallCompleteRef = useRef(onToolCallComplete);
|
|
1591
1593
|
onToolCallCompleteRef.current = onToolCallComplete;
|
|
1594
|
+
// The Agent Inspector: the turns and tool calls, recorded as they happen.
|
|
1595
|
+
const inspectorFromContext = useAgentInspectorSink();
|
|
1596
|
+
const inspectorSink = inspectorGiven ?? inspectorFromContext;
|
|
1597
|
+
const inspectorActorRef = useRef('Agent');
|
|
1598
|
+
inspectorActorRef.current =
|
|
1599
|
+
(typeof title === 'string' && title) || activeAgentId || 'Agent';
|
|
1600
|
+
const recorderRef = useRef(null);
|
|
1601
|
+
if (inspectorSink && recorderRef.current?.sink !== inspectorSink) {
|
|
1602
|
+
recorderRef.current = {
|
|
1603
|
+
sink: inspectorSink,
|
|
1604
|
+
recorder: chatInspectorRecorder(inspectorSink, () => inspectorActorRef.current, {
|
|
1605
|
+
frontendTools: () => (frontendToolsRef.current ?? []).map(tool => tool.name),
|
|
1606
|
+
}),
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
else if (!inspectorSink) {
|
|
1610
|
+
recorderRef.current = null;
|
|
1611
|
+
}
|
|
1612
|
+
const inspectAnswerRef = useRef('');
|
|
1592
1613
|
const handleRespondRef = useRef(null);
|
|
1593
1614
|
// A decision made outside the chat — a host page's own approval banner —
|
|
1594
1615
|
// is answered exactly as the tool card's buttons answer it: through the
|
|
@@ -2415,6 +2436,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2415
2436
|
const sanitizedContent = typeof rawContent === 'string'
|
|
2416
2437
|
? sanitizeAssistantContent(rawContent)
|
|
2417
2438
|
: (rawContent ?? '');
|
|
2439
|
+
if (typeof sanitizedContent === 'string' && sanitizedContent) {
|
|
2440
|
+
inspectAnswerRef.current = sanitizedContent;
|
|
2441
|
+
}
|
|
2418
2442
|
setDisplayItems(prev => {
|
|
2419
2443
|
const newItems = [...prev];
|
|
2420
2444
|
const idx = newItems.findIndex(item => !isToolCallMessage(item) && item.id === currentId);
|
|
@@ -2520,6 +2544,11 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2520
2544
|
toolCallId,
|
|
2521
2545
|
args,
|
|
2522
2546
|
});
|
|
2547
|
+
recorderRef.current?.recorder.toolStarted({
|
|
2548
|
+
id: toolCallId,
|
|
2549
|
+
name: toolName,
|
|
2550
|
+
args,
|
|
2551
|
+
});
|
|
2523
2552
|
const frontendTool = frontendToolsRef.current?.find(t => t.name === toolName);
|
|
2524
2553
|
const toolHandler = frontendTool?.handler;
|
|
2525
2554
|
// Execute when the args are complete. The protocol says so
|
|
@@ -2604,6 +2633,13 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2604
2633
|
setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
|
|
2605
2634
|
? updatedToolCall
|
|
2606
2635
|
: item));
|
|
2636
|
+
recorderRef.current?.recorder.toolEnded(toolCallId, {
|
|
2637
|
+
result: event.toolResult.result,
|
|
2638
|
+
error: event.toolResult.error ??
|
|
2639
|
+
(updatedToolCall.status === 'error'
|
|
2640
|
+
? (executionError ?? 'failed')
|
|
2641
|
+
: undefined),
|
|
2642
|
+
});
|
|
2607
2643
|
// Fire post-hook for tool results
|
|
2608
2644
|
onToolCallCompleteRef.current?.({
|
|
2609
2645
|
toolName: existingToolCall.toolName,
|
|
@@ -2657,6 +2693,11 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2657
2693
|
* The bar and its ring simply never appeared for a browser agent.
|
|
2658
2694
|
*/
|
|
2659
2695
|
{
|
|
2696
|
+
recorderRef.current?.recorder.turnEnded({
|
|
2697
|
+
text: inspectAnswerRef.current || undefined,
|
|
2698
|
+
usage: event.usage,
|
|
2699
|
+
});
|
|
2700
|
+
inspectAnswerRef.current = '';
|
|
2660
2701
|
const turnInput = event.usage?.promptTokens ?? 0;
|
|
2661
2702
|
const turnOutput = event.usage?.completionTokens ?? 0;
|
|
2662
2703
|
// The parts sum to the total when the harness did not send one:
|
|
@@ -2710,6 +2751,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2710
2751
|
break;
|
|
2711
2752
|
case 'error':
|
|
2712
2753
|
console.error('[ChatBase] Protocol error:', event.error);
|
|
2754
|
+
recorderRef.current?.recorder.turnEnded({
|
|
2755
|
+
error: event.error?.message ?? 'error',
|
|
2756
|
+
});
|
|
2713
2757
|
if (event.error?.message &&
|
|
2714
2758
|
/exceeded maximum retries/i.test(event.error.message) &&
|
|
2715
2759
|
hideMessagesAfterToolUIRef.current) {
|
|
@@ -2777,6 +2821,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2777
2821
|
result,
|
|
2778
2822
|
status: 'complete',
|
|
2779
2823
|
};
|
|
2824
|
+
recorderRef.current?.recorder.toolEnded(toolCallId, { result });
|
|
2780
2825
|
toolCallsRef.current.set(toolCallId, completedToolCall);
|
|
2781
2826
|
setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
|
|
2782
2827
|
? completedToolCall
|
|
@@ -2789,6 +2834,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2789
2834
|
status: 'error',
|
|
2790
2835
|
error: err.message,
|
|
2791
2836
|
};
|
|
2837
|
+
recorderRef.current?.recorder.toolEnded(toolCallId, {
|
|
2838
|
+
error: err.message,
|
|
2839
|
+
});
|
|
2792
2840
|
toolCallsRef.current.set(toolCallId, errorToolCall);
|
|
2793
2841
|
setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
|
|
2794
2842
|
? errorToolCall
|
|
@@ -2838,6 +2886,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2838
2886
|
if (!adapterRef.current && !onSendMessage)
|
|
2839
2887
|
return;
|
|
2840
2888
|
stoppedRef.current = false;
|
|
2889
|
+
inspectAnswerRef.current = '';
|
|
2890
|
+
recorderRef.current?.recorder.turnStarted(messageContent, selectedModel || undefined);
|
|
2841
2891
|
suppressAssistantTextForToolOnlyRef.current =
|
|
2842
2892
|
isToolCallOnlyPrompt(messageContent);
|
|
2843
2893
|
// Said, then sent: by the microphone at once, or from the composer it
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three pulsing dots: a speaker is still at work and has not written yet —
|
|
3
|
+
* the chat's waiting indicator, and the assistant's balloon's. Still, but
|
|
4
|
+
* shown, for a reader who asks for reduced motion. Hidden from a screen
|
|
5
|
+
* reader: the live region beside it says the state.
|
|
6
|
+
*
|
|
7
|
+
* @module chat/indicators/TypingDots
|
|
8
|
+
*/
|
|
9
|
+
import type { ReactElement } from 'react';
|
|
10
|
+
export declare function TypingDots({ size }: {
|
|
11
|
+
size?: number;
|
|
12
|
+
}): ReactElement;
|
|
13
|
+
export default TypingDots;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box } from '@datalayer/primer-addons';
|
|
3
|
+
export function TypingDots({ size = 8 }) {
|
|
4
|
+
return (_jsx(Box, { "aria-hidden": "true", "data-typing-dots": "", sx: { display: 'inline-flex', alignItems: 'center', gap: '4px' }, children: [0, 0.2, 0.4].map((delay, index) => (_jsx(Box, { sx: {
|
|
5
|
+
width: size,
|
|
6
|
+
height: size,
|
|
7
|
+
borderRadius: '50%',
|
|
8
|
+
bg: 'fg.muted',
|
|
9
|
+
animation: 'typingPulse 1.4s ease-in-out infinite',
|
|
10
|
+
animationDelay: `${delay}s`,
|
|
11
|
+
'@keyframes typingPulse': {
|
|
12
|
+
'0%, 60%, 100%': { transform: 'scale(0.6)', opacity: 0.4 },
|
|
13
|
+
'30%': { transform: 'scale(1)', opacity: 1 },
|
|
14
|
+
},
|
|
15
|
+
'@media (prefers-reduced-motion: reduce)': {
|
|
16
|
+
animation: 'none',
|
|
17
|
+
opacity: 0.7,
|
|
18
|
+
},
|
|
19
|
+
} }, index))) }));
|
|
20
|
+
}
|
|
21
|
+
export default TypingDots;
|
|
@@ -18,6 +18,7 @@ import { Streamdown } from 'streamdown';
|
|
|
18
18
|
import { streamdownMarkdownStyles, streamdownCodeBlockStyles, } from '../styles/streamdownStyles';
|
|
19
19
|
import { ToolCallDisplay } from '../tools/ToolCallDisplay';
|
|
20
20
|
import { marksOfToolCall, skillIdOfCall } from '../marks';
|
|
21
|
+
import { TypingDots } from '../indicators/TypingDots';
|
|
21
22
|
import { TurnFooter } from './TurnFooter';
|
|
22
23
|
import { normalizeAssistantMarkdown } from './assistantMarkdown';
|
|
23
24
|
import { parseFormSubmission } from './formSubmission';
|
|
@@ -62,21 +63,6 @@ function toneColorsOf(tone) {
|
|
|
62
63
|
fg: `${tone}.fg`,
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
|
-
/** Three pulsing dots: a speaker is still writing and has not settled yet. */
|
|
66
|
-
function TypingDots({ size = 8 }) {
|
|
67
|
-
return (_jsx(Box, { sx: { display: 'flex', alignItems: 'center', gap: '4px' }, children: [0, 0.2, 0.4].map((delay, index) => (_jsx(Box, { sx: {
|
|
68
|
-
width: size,
|
|
69
|
-
height: size,
|
|
70
|
-
borderRadius: '50%',
|
|
71
|
-
bg: 'fg.muted',
|
|
72
|
-
animation: 'typingPulse 1.4s ease-in-out infinite',
|
|
73
|
-
animationDelay: `${delay}s`,
|
|
74
|
-
'@keyframes typingPulse': {
|
|
75
|
-
'0%, 60%, 100%': { transform: 'scale(0.6)', opacity: 0.4 },
|
|
76
|
-
'30%': { transform: 'scale(1)', opacity: 1 },
|
|
77
|
-
},
|
|
78
|
-
} }, index))) }));
|
|
79
|
-
}
|
|
80
66
|
const SUBAGENT_AVATAR_CONFIG = {
|
|
81
67
|
userAvatar: _jsx(PersonIcon, { size: 14 }),
|
|
82
68
|
assistantAvatar: _jsx(DependabotIcon, { size: 14 }),
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Agent Inspector: what agents do, one row a thing, newest last — the
|
|
3
|
+
* time (to the millisecond, and since the first), who did it and toward
|
|
4
|
+
* whom, its source and kind, its name with its mark, its status and
|
|
5
|
+
* duration, and a line that says it. A row unfolds to its payload as a JSON
|
|
6
|
+
* tree (a call's arguments and its result), and to what was made of it.
|
|
7
|
+
*
|
|
8
|
+
* Filters by source, kind and actor; *Pause* holds the rows still while the
|
|
9
|
+
* record goes on, *Clear* empties it, *Export* saves the session as JSON. A
|
|
10
|
+
* large payload (a notebook) says its size and stays folded until asked.
|
|
11
|
+
*
|
|
12
|
+
* Fed by a sink (`useAgentInspector`) or given its entries.
|
|
13
|
+
*
|
|
14
|
+
* @module components/inspector/AgentInspector
|
|
15
|
+
*/
|
|
16
|
+
import type { JSX } from 'react';
|
|
17
|
+
import { type AgentInspectorEntry, type AgentInspectorExport, type AgentInspectorSink, type AgentInspectorSource } from './agentInspector';
|
|
18
|
+
export type AgentInspectorProps = {
|
|
19
|
+
/** The record to show, kept current. */
|
|
20
|
+
sink?: AgentInspectorSink | null;
|
|
21
|
+
/** Or its entries, as given. */
|
|
22
|
+
entries?: readonly AgentInspectorEntry[];
|
|
23
|
+
/** How tall the rows grow before they scroll. */
|
|
24
|
+
maxHeight?: number | string;
|
|
25
|
+
/** Payloads larger than this, in bytes, stay folded until asked. */
|
|
26
|
+
largeBytes?: number;
|
|
27
|
+
/** What is said while nothing is recorded. */
|
|
28
|
+
emptyText?: string;
|
|
29
|
+
/** The file name the session is exported as, without `.json`. */
|
|
30
|
+
exportName?: string;
|
|
31
|
+
/**
|
|
32
|
+
* One agent's own record: what it did (`actor`), and the A2A messages it
|
|
33
|
+
* sent or received — one message shows in the sender's and in the
|
|
34
|
+
* receiver's, the same entry, as sent in one and received in the other.
|
|
35
|
+
*/
|
|
36
|
+
agent?: string;
|
|
37
|
+
};
|
|
38
|
+
/** Whether an entry is one agent's: done by it, or sent to or by it. */
|
|
39
|
+
export declare function isAgentsEntry(entry: AgentInspectorEntry, agent: string): boolean;
|
|
40
|
+
/** Which way an entry went, from one agent's side: `→ Accounting`, `← Accounting`. */
|
|
41
|
+
export declare function directionFrom(entry: AgentInspectorEntry, agent: string | undefined): string;
|
|
42
|
+
/** A time of day, to the millisecond: `15:44:31.299`. */
|
|
43
|
+
export declare function clockTime(at: number): string;
|
|
44
|
+
/** Time since the first entry: `+1.204s`. */
|
|
45
|
+
export declare function sinceFirst(at: number, first: number): string;
|
|
46
|
+
/** How long a call took: `412 ms`, `3.4 s`. */
|
|
47
|
+
export declare function durationText(ms: number): string;
|
|
48
|
+
export type AgentInspectorFilter = {
|
|
49
|
+
sources: ReadonlySet<AgentInspectorSource>;
|
|
50
|
+
kinds: ReadonlySet<string>;
|
|
51
|
+
actors: ReadonlySet<string>;
|
|
52
|
+
};
|
|
53
|
+
/** The entries a filter keeps: an empty set keeps every value. */
|
|
54
|
+
export declare function filterEntries(entries: readonly AgentInspectorEntry[], filter: AgentInspectorFilter): AgentInspectorEntry[];
|
|
55
|
+
/** A session, as JSON. */
|
|
56
|
+
export declare function exportEntries(entries: readonly AgentInspectorEntry[]): AgentInspectorExport;
|
|
57
|
+
/** The Agent Inspector. */
|
|
58
|
+
export declare function AgentInspector({ sink, entries: given, maxHeight, largeBytes, emptyText, exportName, agent, }: AgentInspectorProps): JSX.Element;
|
|
59
|
+
export default AgentInspector;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { ActionList, ActionMenu, Button, Label, Text } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { DownloadIcon, PlayIcon, TrashIcon, PauseIcon, } from '@primer/octicons-react';
|
|
6
|
+
import { SpecMark } from '../../chat/marks';
|
|
7
|
+
import { AGENT_INSPECTOR_SOURCES, AGENT_INSPECTOR_SOURCE_LABELS, formatBytes, useAgentInspectorEntries, } from './agentInspector';
|
|
8
|
+
import { JsonTree } from './JsonTree';
|
|
9
|
+
/** Whether an entry is one agent's: done by it, or sent to or by it. */
|
|
10
|
+
export function isAgentsEntry(entry, agent) {
|
|
11
|
+
return (entry.actor === agent ||
|
|
12
|
+
entry.direction?.from === agent ||
|
|
13
|
+
entry.direction?.to === agent);
|
|
14
|
+
}
|
|
15
|
+
/** Which way an entry went, from one agent's side: `→ Accounting`, `← Accounting`. */
|
|
16
|
+
export function directionFrom(entry, agent) {
|
|
17
|
+
const direction = entry.direction;
|
|
18
|
+
if (!direction) {
|
|
19
|
+
return entry.actor;
|
|
20
|
+
}
|
|
21
|
+
if (agent === direction.from) {
|
|
22
|
+
return `sent → ${direction.to}`;
|
|
23
|
+
}
|
|
24
|
+
if (agent === direction.to) {
|
|
25
|
+
return `received ← ${direction.from}`;
|
|
26
|
+
}
|
|
27
|
+
return `${direction.from} → ${direction.to}`;
|
|
28
|
+
}
|
|
29
|
+
/** A kind, as its label is coloured. */
|
|
30
|
+
const KIND_VARIANTS = {
|
|
31
|
+
request: 'accent',
|
|
32
|
+
'status-update': 'attention',
|
|
33
|
+
'artifact-update': 'success',
|
|
34
|
+
task: 'secondary',
|
|
35
|
+
message: 'done',
|
|
36
|
+
'agent-card': 'secondary',
|
|
37
|
+
response: 'secondary',
|
|
38
|
+
'tool-call': 'sponsors',
|
|
39
|
+
turn: 'primary',
|
|
40
|
+
error: 'danger',
|
|
41
|
+
approval: 'severe',
|
|
42
|
+
};
|
|
43
|
+
const pad = (value, length = 2) => String(value).padStart(length, '0');
|
|
44
|
+
/** A time of day, to the millisecond: `15:44:31.299`. */
|
|
45
|
+
export function clockTime(at) {
|
|
46
|
+
const date = new Date(at);
|
|
47
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
|
|
48
|
+
}
|
|
49
|
+
/** Time since the first entry: `+1.204s`. */
|
|
50
|
+
export function sinceFirst(at, first) {
|
|
51
|
+
return `+${((at - first) / 1000).toFixed(3)}s`;
|
|
52
|
+
}
|
|
53
|
+
/** How long a call took: `412 ms`, `3.4 s`. */
|
|
54
|
+
export function durationText(ms) {
|
|
55
|
+
return ms < 1000 ? `${Math.round(ms)} ms` : `${(ms / 1000).toFixed(1)} s`;
|
|
56
|
+
}
|
|
57
|
+
/** The entries a filter keeps: an empty set keeps every value. */
|
|
58
|
+
export function filterEntries(entries, filter) {
|
|
59
|
+
return entries.filter(entry => (!filter.sources.size || filter.sources.has(entry.source)) &&
|
|
60
|
+
(!filter.kinds.size || filter.kinds.has(entry.kind)) &&
|
|
61
|
+
(!filter.actors.size || filter.actors.has(entry.actor)));
|
|
62
|
+
}
|
|
63
|
+
/** A session, as JSON. */
|
|
64
|
+
export function exportEntries(entries) {
|
|
65
|
+
return { exportedAt: new Date().toISOString(), entries: [...entries] };
|
|
66
|
+
}
|
|
67
|
+
function toggle(set, value) {
|
|
68
|
+
const next = new Set(set);
|
|
69
|
+
if (next.has(value)) {
|
|
70
|
+
next.delete(value);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
next.add(value);
|
|
74
|
+
}
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
function FilterMenu({ label, options, selected, onToggle, onAll, name, }) {
|
|
78
|
+
return (_jsxs(ActionMenu, { children: [_jsxs(ActionMenu.Button, { size: "small", "data-inspector-filter": label.toLowerCase(), children: [label, selected.size ? ` · ${selected.size}` : ''] }), _jsx(ActionMenu.Overlay, { width: "auto", children: _jsxs(ActionList, { selectionVariant: "multiple", "aria-label": label, children: [_jsx(ActionList.Item, { selected: !selected.size, onSelect: onAll, children: "All" }), _jsx(ActionList.Divider, {}), options.map(option => (_jsx(ActionList.Item, { selected: selected.has(option), onSelect: () => onToggle(option), children: name(option) }, option)))] }) })] }));
|
|
79
|
+
}
|
|
80
|
+
function Payload({ title, value, large, }) {
|
|
81
|
+
const bytes = useMemo(() => {
|
|
82
|
+
try {
|
|
83
|
+
return new TextEncoder().encode(JSON.stringify(value) ?? '').length;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
}, [value]);
|
|
89
|
+
const [shown, setShown] = useState(bytes <= large);
|
|
90
|
+
return (_jsxs(Box, { sx: { minWidth: 0 }, "data-inspector-payload": title, children: [_jsxs(Text, { sx: {
|
|
91
|
+
fontSize: 0,
|
|
92
|
+
color: 'fg.muted',
|
|
93
|
+
fontWeight: 600,
|
|
94
|
+
display: 'block',
|
|
95
|
+
}, children: [title, " \u00B7 ", formatBytes(bytes)] }), shown ? (_jsx(JsonTree, { value: value, expandDepth: bytes > large ? 1 : 2 })) : (_jsxs(Button, { size: "small", onClick: () => setShown(true), "data-inspector-large": "", children: ["Show ", formatBytes(bytes)] }))] }));
|
|
96
|
+
}
|
|
97
|
+
function Row({ entry, first, open, onToggle, large, agent, }) {
|
|
98
|
+
const ended = entry.endedAt !== undefined;
|
|
99
|
+
return (_jsxs(Box, { as: "li", "data-inspector-entry": entry.id, "data-inspector-source": entry.source, "data-inspector-kind": entry.kind, "data-inspector-status": entry.status ?? '', sx: {
|
|
100
|
+
borderBottom: '1px solid',
|
|
101
|
+
borderColor: 'border.muted',
|
|
102
|
+
listStyle: 'none',
|
|
103
|
+
}, children: [_jsxs(Box, { as: "button", type: "button", "aria-expanded": open, onClick: onToggle, sx: {
|
|
104
|
+
width: '100%',
|
|
105
|
+
display: 'grid',
|
|
106
|
+
gridTemplateColumns: ['auto 1fr', 'auto auto auto minmax(0, 1fr)'],
|
|
107
|
+
columnGap: 2,
|
|
108
|
+
rowGap: 1,
|
|
109
|
+
alignItems: 'center',
|
|
110
|
+
px: 2,
|
|
111
|
+
py: 1,
|
|
112
|
+
border: 0,
|
|
113
|
+
bg: open ? 'canvas.subtle' : 'transparent',
|
|
114
|
+
color: 'fg.default',
|
|
115
|
+
font: 'inherit',
|
|
116
|
+
fontSize: 0,
|
|
117
|
+
textAlign: 'left',
|
|
118
|
+
cursor: 'pointer',
|
|
119
|
+
'&:hover': { bg: 'canvas.subtle' },
|
|
120
|
+
}, children: [_jsxs(Box, { sx: { fontFamily: 'mono', color: 'fg.muted', whiteSpace: 'nowrap' }, children: [_jsx("span", { title: new Date(entry.at).toISOString(), children: clockTime(entry.at) }), ' ', _jsx(Text, { sx: { opacity: 0.7 }, children: sinceFirst(entry.at, first) })] }), _jsx(Text, { sx: { whiteSpace: 'nowrap', fontWeight: 600 }, "data-inspector-direction": "", children: directionFrom(entry, agent) }), _jsxs(Box, { sx: { display: 'flex', gap: 1, flexWrap: 'wrap' }, children: [_jsx(Label, { size: "small", variant: "secondary", children: AGENT_INSPECTOR_SOURCE_LABELS[entry.source] }), _jsx(Label, { size: "small", variant: KIND_VARIANTS[entry.kind] ?? 'secondary', children: entry.kind })] }), _jsxs(Box, { sx: { display: 'flex', gap: 1, alignItems: 'center', minWidth: 0 }, children: [(entry.icon || entry.emoji) && (_jsx(SpecMark, { icon: entry.icon, emoji: entry.emoji, size: 14 })), entry.name && entry.kind !== 'request' && (_jsx(Text, { sx: { fontFamily: 'mono', fontWeight: 600, whiteSpace: 'nowrap' }, children: entry.name })), entry.status && (_jsxs(Label, { size: "small", variant: entry.status === 'failed'
|
|
121
|
+
? 'danger'
|
|
122
|
+
: entry.status === 'running'
|
|
123
|
+
? 'attention'
|
|
124
|
+
: 'success', children: [entry.status, ended && entry.endedAt !== undefined
|
|
125
|
+
? ` · ${durationText(entry.endedAt - entry.at)}`
|
|
126
|
+
: ''] })), _jsx(Text, { sx: {
|
|
127
|
+
color: 'fg.muted',
|
|
128
|
+
overflow: 'hidden',
|
|
129
|
+
textOverflow: 'ellipsis',
|
|
130
|
+
whiteSpace: 'nowrap',
|
|
131
|
+
minWidth: 0,
|
|
132
|
+
}, title: entry.summary, children: entry.summary }), entry.bytes > large && (_jsx(Text, { sx: { color: 'fg.muted', whiteSpace: 'nowrap' }, children: formatBytes(entry.bytes) }))] })] }), open && (_jsxs(Box, { "data-inspector-details": "", sx: {
|
|
133
|
+
px: 3,
|
|
134
|
+
py: 2,
|
|
135
|
+
display: 'flex',
|
|
136
|
+
flexDirection: 'column',
|
|
137
|
+
gap: 2,
|
|
138
|
+
bg: 'canvas.subtle',
|
|
139
|
+
}, children: [entry.a2a && (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', fontFamily: 'mono' }, children: [
|
|
140
|
+
entry.a2a.method && `method ${entry.a2a.method}`,
|
|
141
|
+
entry.a2a.rpcId !== undefined && `id ${entry.a2a.rpcId}`,
|
|
142
|
+
entry.a2a.taskId && `task ${entry.a2a.taskId}`,
|
|
143
|
+
entry.a2a.contextId && `context ${entry.a2a.contextId}`,
|
|
144
|
+
entry.a2a.state && entry.a2a.state,
|
|
145
|
+
entry.a2a.chunks &&
|
|
146
|
+
entry.a2a.chunks > 1 &&
|
|
147
|
+
`${entry.a2a.chunks} chunks`,
|
|
148
|
+
]
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.join(' · ') })), entry.error && (_jsx(Text, { sx: { color: 'danger.fg', fontSize: 0 }, children: entry.error })), entry.payload !== undefined && (_jsx(Payload, { title: entry.kind === 'tool-call' ? 'Arguments' : 'Payload', value: entry.payload, large: large })), entry.result !== undefined && (_jsx(Payload, { title: "Result", value: entry.result, large: large })), entry.events.length > 0 && (_jsx(Payload, { title: "Events", value: entry.events, large: large })), entry.links.length > 0 && (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["Linked: ", entry.links.join(', ')] }))] }))] }));
|
|
151
|
+
}
|
|
152
|
+
/** Save a session as a JSON file. */
|
|
153
|
+
function download(name, session) {
|
|
154
|
+
const blob = new Blob([JSON.stringify(session, null, 2)], {
|
|
155
|
+
type: 'application/json',
|
|
156
|
+
});
|
|
157
|
+
const url = URL.createObjectURL(blob);
|
|
158
|
+
const link = document.createElement('a');
|
|
159
|
+
link.href = url;
|
|
160
|
+
link.download = `${name}.json`;
|
|
161
|
+
link.click();
|
|
162
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
163
|
+
}
|
|
164
|
+
/** The Agent Inspector. */
|
|
165
|
+
export function AgentInspector({ sink, entries: given, maxHeight = 420, largeBytes = 16_000, emptyText = 'Nothing recorded yet.', exportName = 'agent-inspector', agent, }) {
|
|
166
|
+
const live = useAgentInspectorEntries(sink);
|
|
167
|
+
const everything = given ?? live;
|
|
168
|
+
const all = useMemo(() => agent
|
|
169
|
+
? everything.filter(entry => isAgentsEntry(entry, agent))
|
|
170
|
+
: everything, [everything, agent]);
|
|
171
|
+
const [held, setHeld] = useState(null);
|
|
172
|
+
const [clearedAt, setClearedAt] = useState(0);
|
|
173
|
+
const [sources, setSources] = useState(new Set());
|
|
174
|
+
const [kinds, setKinds] = useState(new Set());
|
|
175
|
+
const [actors, setActors] = useState(new Set());
|
|
176
|
+
const [open, setOpen] = useState(new Set());
|
|
177
|
+
const recorded = useMemo(() => (clearedAt ? all.filter(entry => entry.at >= clearedAt) : all), [all, clearedAt]);
|
|
178
|
+
const shown = held ?? recorded;
|
|
179
|
+
const waiting = held ? recorded.length - held.length : 0;
|
|
180
|
+
const kindOptions = useMemo(() => [...new Set(recorded.map(entry => entry.kind))].sort(), [recorded]);
|
|
181
|
+
const actorOptions = useMemo(() => [...new Set(recorded.map(entry => entry.actor))].sort(), [recorded]);
|
|
182
|
+
const sourceOptions = useMemo(() => AGENT_INSPECTOR_SOURCES.filter(source => recorded.some(entry => entry.source === source)), [recorded]);
|
|
183
|
+
const rows = useMemo(() => filterEntries(shown, { sources, kinds, actors }), [shown, sources, kinds, actors]);
|
|
184
|
+
const first = shown[0]?.at ?? 0;
|
|
185
|
+
return (_jsxs(Box, { "data-agent-inspector": "", sx: {
|
|
186
|
+
border: '1px solid',
|
|
187
|
+
borderColor: 'border.default',
|
|
188
|
+
borderRadius: 2,
|
|
189
|
+
bg: 'canvas.default',
|
|
190
|
+
color: 'fg.default',
|
|
191
|
+
minWidth: 0,
|
|
192
|
+
}, children: [_jsxs(Box, { role: "toolbar", "aria-label": agent ? `${agent}'s Agent Inspector` : 'Agent Inspector', sx: {
|
|
193
|
+
display: 'flex',
|
|
194
|
+
flexWrap: 'wrap',
|
|
195
|
+
gap: 2,
|
|
196
|
+
alignItems: 'center',
|
|
197
|
+
p: 2,
|
|
198
|
+
borderBottom: '1px solid',
|
|
199
|
+
borderColor: 'border.default',
|
|
200
|
+
}, children: [_jsx(FilterMenu, { label: "Source", options: sourceOptions, selected: sources, onToggle: value => setSources(toggle(sources, value)), onAll: () => setSources(new Set()), name: value => AGENT_INSPECTOR_SOURCE_LABELS[value] }), _jsx(FilterMenu, { label: "Kind", options: kindOptions, selected: kinds, onToggle: value => setKinds(toggle(kinds, value)), onAll: () => setKinds(new Set()), name: value => value }), _jsx(FilterMenu, { label: "Actor", options: actorOptions, selected: actors, onToggle: value => setActors(toggle(actors, value)), onAll: () => setActors(new Set()), name: value => value }), _jsx(Box, { sx: { flex: 1 } }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, "data-inspector-count": "", children: [rows.length === shown.length
|
|
201
|
+
? `${rows.length} ${rows.length === 1 ? 'entry' : 'entries'}`
|
|
202
|
+
: `${rows.length} of ${shown.length}`, waiting > 0 ? ` · ${waiting} new` : ''] }), _jsx(Button, { size: "small", leadingVisual: held ? PlayIcon : PauseIcon, "aria-pressed": held !== null, onClick: () => setHeld(held ? null : recorded), "data-inspector-pause": "", children: held ? 'Resume' : 'Pause' }), _jsx(Button, { size: "small", leadingVisual: TrashIcon, onClick: () => {
|
|
203
|
+
setOpen(new Set());
|
|
204
|
+
setHeld(null);
|
|
205
|
+
// One agent's view of a shared record clears only itself.
|
|
206
|
+
if (sink && !given && !agent) {
|
|
207
|
+
sink.clear();
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
setClearedAt(Date.now());
|
|
211
|
+
}
|
|
212
|
+
}, "data-inspector-clear": "", children: "Clear" }), _jsx(Button, { size: "small", leadingVisual: DownloadIcon, disabled: !recorded.length, onClick: () => download(exportName, exportEntries(recorded)), "data-inspector-export": "", children: "Export" })] }), _jsx(Box, { as: "ol", "aria-label": "Recorded", sx: { m: 0, p: 0, maxHeight, overflowY: 'auto' }, children: rows.length === 0 ? (_jsx(Text, { as: "li", sx: {
|
|
213
|
+
display: 'block',
|
|
214
|
+
p: 3,
|
|
215
|
+
color: 'fg.muted',
|
|
216
|
+
fontSize: 1,
|
|
217
|
+
listStyle: 'none',
|
|
218
|
+
}, children: emptyText })) : (rows.map(entry => (_jsx(Row, { entry: entry, first: first, large: largeBytes, agent: agent, open: open.has(entry.id), onToggle: () => setOpen(toggle(open, entry.id)) }, entry.id)))) })] }));
|
|
219
|
+
}
|
|
220
|
+
export default AgentInspector;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Agent Inspector over the page: a large dialog, the inspector loaded
|
|
3
|
+
* only when it opens, fed by the sink the agent's chat or team feeds. Esc
|
|
4
|
+
* closes it.
|
|
5
|
+
*
|
|
6
|
+
* @module components/inspector/AgentInspectorDialog
|
|
7
|
+
*/
|
|
8
|
+
import type { JSX } from 'react';
|
|
9
|
+
import type { AgentInspectorSink } from './agentInspector';
|
|
10
|
+
export type AgentInspectorDialogProps = {
|
|
11
|
+
sink: AgentInspectorSink | null;
|
|
12
|
+
/** The dialog's title. */
|
|
13
|
+
title?: string;
|
|
14
|
+
/** One agent's own record (`AgentInspector`'s `agent`); all of it without. */
|
|
15
|
+
agent?: string;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
};
|
|
18
|
+
export declare function AgentInspectorDialog({ sink, title, agent, onClose, }: AgentInspectorDialogProps): JSX.Element;
|
|
19
|
+
export default AgentInspectorDialog;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, lazy } from 'react';
|
|
3
|
+
import { Dialog } from '@primer/react';
|
|
4
|
+
import { Text } from '@primer/react';
|
|
5
|
+
const AgentInspector = lazy(() => import('./AgentInspector'));
|
|
6
|
+
export function AgentInspectorDialog({ sink, title = 'Agent Inspector', agent, onClose, }) {
|
|
7
|
+
return (_jsx(Dialog, { title: title, onClose: onClose, width: "xlarge", height: "large", "data-agent-inspector-dialog": "", children: _jsx(Suspense, { fallback: _jsx(Text, { sx: { color: 'fg.muted' }, children: "Loading the Agent Inspector\u2026" }), children: _jsx(AgentInspector, { sink: sink, agent: agent, maxHeight: "calc(80vh - 140px)", emptyText: "Nothing recorded yet: what the agent does shows here as it does it." }) }) }));
|
|
8
|
+
}
|
|
9
|
+
export default AgentInspectorDialog;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A JSON value as a tree: objects and arrays fold and unfold, each value
|
|
3
|
+
* coloured by its type (theme colours, light and dark), long strings cut
|
|
4
|
+
* until asked for whole, and the whole value copied as JSON.
|
|
5
|
+
*
|
|
6
|
+
* @module components/inspector/JsonTree
|
|
7
|
+
*/
|
|
8
|
+
import type { JSX } from 'react';
|
|
9
|
+
export type JsonTreeProps = {
|
|
10
|
+
value: unknown;
|
|
11
|
+
/** The root's name, shown as its key. */
|
|
12
|
+
name?: string;
|
|
13
|
+
/** How many levels open at first. */
|
|
14
|
+
expandDepth?: number;
|
|
15
|
+
/** Offer *Copy JSON* above the tree. */
|
|
16
|
+
copyable?: boolean;
|
|
17
|
+
/** Longer strings are cut until clicked. */
|
|
18
|
+
stringLimit?: number;
|
|
19
|
+
};
|
|
20
|
+
/** A value's type, as the tree colours it. */
|
|
21
|
+
export declare function jsonType(value: unknown): 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object' | 'undefined';
|
|
22
|
+
/** A JSON value, as a tree. */
|
|
23
|
+
export declare function JsonTree({ value, name, expandDepth, copyable, stringLimit, }: JsonTreeProps): JSX.Element;
|
|
24
|
+
export default JsonTree;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Button, Text } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { CopyIcon, CheckIcon } from '@primer/octicons-react';
|
|
6
|
+
const MONO = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace';
|
|
7
|
+
/** A value's type, as the tree colours it. */
|
|
8
|
+
export function jsonType(value) {
|
|
9
|
+
if (value === null) {
|
|
10
|
+
return 'null';
|
|
11
|
+
}
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
return 'array';
|
|
14
|
+
}
|
|
15
|
+
const type = typeof value;
|
|
16
|
+
if (type === 'string' || type === 'number' || type === 'boolean') {
|
|
17
|
+
return type;
|
|
18
|
+
}
|
|
19
|
+
if (type === 'bigint') {
|
|
20
|
+
return 'number';
|
|
21
|
+
}
|
|
22
|
+
return type === 'undefined' ? 'undefined' : 'object';
|
|
23
|
+
}
|
|
24
|
+
const COLOURS = {
|
|
25
|
+
string: 'success.fg',
|
|
26
|
+
number: 'accent.fg',
|
|
27
|
+
boolean: 'done.fg',
|
|
28
|
+
null: 'fg.muted',
|
|
29
|
+
undefined: 'fg.muted',
|
|
30
|
+
};
|
|
31
|
+
function Leaf({ value, stringLimit, }) {
|
|
32
|
+
const type = jsonType(value);
|
|
33
|
+
const [whole, setWhole] = useState(false);
|
|
34
|
+
if (type === 'string') {
|
|
35
|
+
const text = value;
|
|
36
|
+
const cut = !whole && text.length > stringLimit;
|
|
37
|
+
return (_jsxs(Text, { "data-json-type": "string", sx: {
|
|
38
|
+
color: COLOURS.string,
|
|
39
|
+
whiteSpace: 'pre-wrap',
|
|
40
|
+
wordBreak: 'break-word',
|
|
41
|
+
}, children: [JSON.stringify(cut ? text.slice(0, stringLimit) : text), cut && (_jsxs(Box, { as: "button", type: "button", onClick: () => setWhole(true), sx: {
|
|
42
|
+
ml: 1,
|
|
43
|
+
p: 0,
|
|
44
|
+
border: 0,
|
|
45
|
+
bg: 'transparent',
|
|
46
|
+
color: 'accent.fg',
|
|
47
|
+
cursor: 'pointer',
|
|
48
|
+
font: 'inherit',
|
|
49
|
+
}, children: ["\u2026 ", text.length - stringLimit, " more"] }))] }));
|
|
50
|
+
}
|
|
51
|
+
return (_jsx(Text, { "data-json-type": type, sx: { color: COLOURS[type] ?? 'fg.default' }, children: type === 'undefined' ? 'undefined' : String(value) }));
|
|
52
|
+
}
|
|
53
|
+
function Node({ name, value, depth, expandDepth, stringLimit, }) {
|
|
54
|
+
const type = jsonType(value);
|
|
55
|
+
const branch = type === 'array' || type === 'object';
|
|
56
|
+
const [open, setOpen] = useState(depth < expandDepth);
|
|
57
|
+
const key = name !== undefined ? (_jsxs(Text, { sx: { color: 'fg.default', fontWeight: 600 }, children: [name, ": "] })) : null;
|
|
58
|
+
if (!branch) {
|
|
59
|
+
return (_jsxs(Box, { role: "treeitem", "data-json-node": "", sx: { pl: 3 }, children: [key, _jsx(Leaf, { value: value, stringLimit: stringLimit })] }));
|
|
60
|
+
}
|
|
61
|
+
const items = type === 'array'
|
|
62
|
+
? value.map((item, index) => [String(index), item])
|
|
63
|
+
: Object.entries(value);
|
|
64
|
+
const [openMark, closeMark] = type === 'array' ? ['[', ']'] : ['{', '}'];
|
|
65
|
+
return (_jsxs(Box, { role: "treeitem", "aria-expanded": open, "data-json-node": "", sx: { pl: depth ? 3 : 0 }, children: [_jsxs(Box, { as: "button", type: "button", onClick: () => setOpen(!open), "aria-label": `${open ? 'Fold' : 'Unfold'} ${name ?? 'the value'}`, "data-json-toggle": "", sx: {
|
|
66
|
+
p: 0,
|
|
67
|
+
border: 0,
|
|
68
|
+
bg: 'transparent',
|
|
69
|
+
color: 'inherit',
|
|
70
|
+
font: 'inherit',
|
|
71
|
+
cursor: 'pointer',
|
|
72
|
+
textAlign: 'left',
|
|
73
|
+
}, children: [_jsx(Text, { sx: { color: 'fg.muted', display: 'inline-block', width: '1em' }, children: open ? '▾' : '▸' }), key, _jsx(Text, { sx: { color: 'fg.muted' }, children: open
|
|
74
|
+
? openMark
|
|
75
|
+
: `${openMark} ${items.length} ${type === 'array' ? (items.length === 1 ? 'item' : 'items') : items.length === 1 ? 'key' : 'keys'} ${closeMark}` })] }), open && (_jsxs(Box, { role: "group", children: [items.map(([itemName, item]) => (_jsx(Node, { name: itemName, value: item, depth: depth + 1, expandDepth: expandDepth, stringLimit: stringLimit }, itemName))), _jsx(Text, { sx: { color: 'fg.muted', pl: 3 }, children: closeMark })] }))] }));
|
|
76
|
+
}
|
|
77
|
+
/** A JSON value, as a tree. */
|
|
78
|
+
export function JsonTree({ value, name, expandDepth = 1, copyable = true, stringLimit = 240, }) {
|
|
79
|
+
const [copied, setCopied] = useState(false);
|
|
80
|
+
const copy = () => {
|
|
81
|
+
const text = JSON.stringify(value, null, 2) ?? 'undefined';
|
|
82
|
+
void navigator.clipboard?.writeText(text).then(() => {
|
|
83
|
+
setCopied(true);
|
|
84
|
+
setTimeout(() => setCopied(false), 1500);
|
|
85
|
+
}, () => undefined);
|
|
86
|
+
};
|
|
87
|
+
return (_jsxs(Box, { "data-json-tree": "", sx: { fontFamily: MONO, fontSize: 0, lineHeight: 1.6, minWidth: 0 }, children: [copyable && (_jsx(Button, { size: "small", variant: "invisible", leadingVisual: copied ? CheckIcon : CopyIcon, onClick: copy, "data-json-copy": "", sx: { float: 'right' }, children: copied ? 'Copied' : 'Copy JSON' })), _jsx(Box, { role: "tree", "aria-label": name ?? 'JSON', children: _jsx(Node, { name: name, value: value, depth: 0, expandDepth: expandDepth, stringLimit: stringLimit }) })] }));
|
|
88
|
+
}
|
|
89
|
+
export default JsonTree;
|