@datalayer/agent-runtimes 1.5.90 → 1.5.91

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.
Files changed (74) hide show
  1. package/README.md +29 -297
  2. package/dist-embed/datalayer-app-main.js +2 -2
  3. package/lib/components/teams/A2ATeamGraph.d.ts +40 -155
  4. package/lib/components/teams/A2ATeamGraph.js +122 -400
  5. package/lib/components/teams/SceneTranscript.d.ts +5 -1
  6. package/lib/components/teams/SceneTranscript.js +2 -2
  7. package/lib/components/teams/SceneView.d.ts +17 -5
  8. package/lib/components/teams/SceneView.js +7 -3
  9. package/lib/components/teams/backdrops/catalogue.d.ts +35 -0
  10. package/lib/components/teams/backdrops/catalogue.js +34 -0
  11. package/lib/components/teams/backdrops/edges.d.ts +20 -0
  12. package/lib/components/teams/backdrops/edges.js +29 -0
  13. package/lib/components/teams/backdrops/faces.d.ts +24 -0
  14. package/lib/components/teams/backdrops/faces.js +39 -0
  15. package/lib/components/teams/backdrops/halo.d.ts +14 -0
  16. package/lib/components/teams/backdrops/halo.js +5 -0
  17. package/lib/components/teams/backdrops/hero.d.ts +14 -0
  18. package/lib/components/teams/backdrops/hero.js +178 -0
  19. package/lib/components/teams/backdrops/index.d.ts +22 -0
  20. package/lib/components/teams/backdrops/index.js +38 -0
  21. package/lib/components/teams/backdrops/nodes.d.ts +123 -0
  22. package/lib/components/teams/backdrops/nodes.js +265 -0
  23. package/lib/components/teams/backdrops/plain.d.ts +14 -0
  24. package/lib/components/teams/backdrops/plain.js +116 -0
  25. package/lib/components/teams/backdrops/studio.d.ts +17 -0
  26. package/lib/components/teams/backdrops/studio.js +122 -0
  27. package/lib/components/teams/backdrops/theme.d.ts +14 -0
  28. package/lib/components/teams/backdrops/theme.js +19 -0
  29. package/lib/components/teams/backdrops/types.d.ts +214 -0
  30. package/lib/components/teams/backdrops/types.js +5 -0
  31. package/lib/components/teams/index.d.ts +2 -0
  32. package/lib/components/teams/index.js +2 -0
  33. package/lib/components/teams/sceneTranscriptRules.d.ts +32 -1
  34. package/lib/components/teams/sceneTranscriptRules.js +104 -2
  35. package/lib/components/teams/transcript/RecordingLabel.d.ts +17 -0
  36. package/lib/components/teams/transcript/RecordingLabel.js +7 -0
  37. package/lib/components/teams/transcript/SequenceDiagram.d.ts +39 -0
  38. package/lib/components/teams/transcript/SequenceDiagram.js +190 -0
  39. package/lib/components/teams/transcript/TranscriptDetail.d.ts +27 -0
  40. package/lib/components/teams/transcript/TranscriptDetail.js +48 -0
  41. package/lib/components/teams/transcript/TranscriptTable.d.ts +23 -0
  42. package/lib/components/teams/transcript/TranscriptTable.js +112 -0
  43. package/lib/components/teams/transcript/TranscriptViews.d.ts +66 -0
  44. package/lib/components/teams/transcript/TranscriptViews.js +135 -0
  45. package/lib/components/teams/transcript/TranscriptViewsTypes.d.ts +13 -0
  46. package/lib/components/teams/transcript/TranscriptViewsTypes.js +5 -0
  47. package/lib/components/teams/transcript/index.d.ts +23 -0
  48. package/lib/components/teams/transcript/index.js +27 -0
  49. package/lib/components/teams/transcript/recordingPlayback.d.ts +52 -0
  50. package/lib/components/teams/transcript/recordingPlayback.js +146 -0
  51. package/lib/components/teams/transcript/selection.d.ts +59 -0
  52. package/lib/components/teams/transcript/selection.js +79 -0
  53. package/lib/components/teams/transcript/sequenceDiagramRules.d.ts +35 -0
  54. package/lib/components/teams/transcript/sequenceDiagramRules.js +158 -0
  55. package/lib/components/teams/transcript/transcriptDetailRules.d.ts +80 -0
  56. package/lib/components/teams/transcript/transcriptDetailRules.js +208 -0
  57. package/lib/components/teams/transcript/transcriptExport.d.ts +31 -0
  58. package/lib/components/teams/transcript/transcriptExport.js +82 -0
  59. package/lib/components/teams/transcript/transcriptRows.d.ts +76 -0
  60. package/lib/components/teams/transcript/transcriptRows.js +187 -0
  61. package/lib/components/teams/transcript/useSceneRecording.d.ts +45 -0
  62. package/lib/components/teams/transcript/useSceneRecording.js +96 -0
  63. package/lib/examples/ScenesExample.js +16 -3
  64. package/lib/specs/actions.js +2 -0
  65. package/lib/specs/apps.d.ts +1 -0
  66. package/lib/specs/apps.js +219 -0
  67. package/lib/specs/sceneRecordings.d.ts +14 -0
  68. package/lib/specs/sceneRecordings.js +12 -0
  69. package/lib/specs/sceneSchema.js +11 -0
  70. package/lib/specs/scenes.d.ts +1 -0
  71. package/lib/specs/scenes.js +159 -0
  72. package/lib/types/scenes.d.ts +54 -0
  73. package/package.json +2 -1
  74. package/scripts/codegen/generate_scenes.py +80 -0
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The transcript as a sequence diagram (SCENES S2-02), drawn by mermaid
3
+ * from {@link sequenceDiagramPlan}'s source. mermaid is loaded when the
4
+ * view opens (`import('mermaid')`), never with the graph; the drawing is
5
+ * themed from Primer's tokens, light and dark; a click on an arrow or a
6
+ * note selects its line, and the selected one is drawn heavier.
7
+ *
8
+ * @module components/teams/transcript/SequenceDiagram
9
+ */
10
+ import type { JSX } from 'react';
11
+ import { type SequenceCastMember } from './sequenceDiagramRules';
12
+ import type { TranscriptRowsLines } from './TranscriptViewsTypes';
13
+ /** What of mermaid the diagram uses: so that a test gives a stand-in. */
14
+ export type MermaidLike = {
15
+ initialize: (config: Record<string, unknown>) => void;
16
+ render: (id: string, source: string) => Promise<{
17
+ svg: string;
18
+ bindFunctions?: (element: Element) => void;
19
+ }>;
20
+ };
21
+ /** mermaid, loaded once the view opens: its own chunk. */
22
+ export declare const loadMermaid: () => Promise<MermaidLike>;
23
+ export type SequenceDiagramProps = TranscriptRowsLines & {
24
+ /** The cast, in the order the participants stand: the members, then the systems. */
25
+ cast?: readonly SequenceCastMember[];
26
+ /** The selected line's key: its arrow is drawn heavier. */
27
+ selected?: string | null;
28
+ /** An arrow or a note was clicked: its line's key. */
29
+ onSelect?: (key: string) => void;
30
+ /** What is said while there is no line. */
31
+ emptyText?: string;
32
+ /** How tall it grows before it scrolls, in pixels. */
33
+ maxHeight?: number;
34
+ /** How mermaid is loaded: `import('mermaid')` unless a test says. */
35
+ load?: () => Promise<MermaidLike>;
36
+ };
37
+ /** The transcript as a sequence diagram, drawn by mermaid when the view opens. */
38
+ export declare function SequenceDiagram({ lines, cast, selected, onSelect, emptyText, maxHeight, load, }: SequenceDiagramProps): JSX.Element;
39
+ export default SequenceDiagram;
@@ -0,0 +1,190 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useMemo, useRef, useState } from 'react';
3
+ import { Spinner, Text, useTheme } from '@primer/react';
4
+ import { Box } from '@datalayer/primer-addons';
5
+ import { sequenceDiagramPlan, } from './sequenceDiagramRules.js';
6
+ /** mermaid, loaded once the view opens: its own chunk. */
7
+ export const loadMermaid = async () => {
8
+ const loaded = await import('mermaid');
9
+ return loaded.default;
10
+ };
11
+ /** Primer's tokens the drawing is themed from, each with what it is for. */
12
+ const TOKENS = {
13
+ background: ['--bgColor-default', 'canvas.default', '#ffffff'],
14
+ surface: ['--bgColor-muted', 'canvas.subtle', '#f6f8fa'],
15
+ text: ['--fgColor-default', 'fg.default', '#1f2328'],
16
+ muted: ['--fgColor-muted', 'fg.muted', '#59636e'],
17
+ border: ['--borderColor-default', 'border.default', '#d0d7de'],
18
+ accent: ['--bgColor-accent-muted', 'accent.subtle', '#ddf4ff'],
19
+ accentBorder: ['--borderColor-accent-emphasis', 'accent.emphasis', '#0969da'],
20
+ attention: ['--bgColor-attention-muted', 'attention.subtle', '#fff8c5'],
21
+ };
22
+ /** A token's value: the CSS variable on the element, else the theme's colour, else the default. */
23
+ function paletteOf(element, colors) {
24
+ const styles = getComputedStyle(element);
25
+ const fromTheme = (path) => {
26
+ let value = colors;
27
+ for (const step of path.split('.')) {
28
+ value =
29
+ value && typeof value === 'object'
30
+ ? value[step]
31
+ : undefined;
32
+ }
33
+ return typeof value === 'string' ? value : undefined;
34
+ };
35
+ return Object.fromEntries(Object.entries(TOKENS).map(([name, [variable, path, fallback]]) => [
36
+ name,
37
+ styles.getPropertyValue(variable).trim() || fromTheme(path) || fallback,
38
+ ]));
39
+ }
40
+ let renderSequence = 0;
41
+ /** Everything mermaid draws a message with, in document order: the arrow's text and its line. */
42
+ const MESSAGE_PARTS = 'text.messageText, [class^="messageLine"]';
43
+ const NOTE_PARTS = 'text.noteText, rect.note';
44
+ /** The transcript as a sequence diagram, drawn by mermaid when the view opens. */
45
+ export function SequenceDiagram({ lines, cast = [], selected, onSelect, emptyText = 'What the scene says and does draws here, arrow by arrow.', maxHeight, load = loadMermaid, }) {
46
+ const target = useRef(null);
47
+ const reactId = useId();
48
+ const { theme, resolvedColorScheme } = useTheme();
49
+ const [state, setState] = useState('loading');
50
+ const [error, setError] = useState();
51
+ const plan = useMemo(() => sequenceDiagramPlan(lines, cast), [lines, cast]);
52
+ const fontFamily = theme
53
+ ?.fonts?.normal;
54
+ const colors = theme
55
+ ?.colors;
56
+ useEffect(() => {
57
+ const element = target.current;
58
+ if (!element || lines.length === 0) {
59
+ return undefined;
60
+ }
61
+ let active = true;
62
+ setState('loading');
63
+ setError(undefined);
64
+ const draw = async () => {
65
+ const mermaid = await load();
66
+ if (!active) {
67
+ return;
68
+ }
69
+ const palette = paletteOf(element, colors);
70
+ mermaid.initialize({
71
+ startOnLoad: false,
72
+ securityLevel: 'strict',
73
+ theme: 'base',
74
+ fontFamily: fontFamily || 'system-ui, sans-serif',
75
+ themeVariables: {
76
+ darkMode: resolvedColorScheme?.includes('dark') ?? false,
77
+ background: palette.background,
78
+ primaryColor: palette.surface,
79
+ primaryTextColor: palette.text,
80
+ primaryBorderColor: palette.border,
81
+ secondaryColor: palette.accent,
82
+ tertiaryColor: palette.background,
83
+ lineColor: palette.muted,
84
+ textColor: palette.text,
85
+ actorBkg: palette.surface,
86
+ actorBorder: palette.border,
87
+ actorTextColor: palette.text,
88
+ actorLineColor: palette.border,
89
+ signalColor: palette.muted,
90
+ signalTextColor: palette.text,
91
+ labelBoxBkgColor: palette.surface,
92
+ labelBoxBorderColor: palette.border,
93
+ labelTextColor: palette.text,
94
+ loopTextColor: palette.text,
95
+ noteBkgColor: palette.attention,
96
+ noteBorderColor: palette.border,
97
+ noteTextColor: palette.text,
98
+ activationBkgColor: palette.accent,
99
+ activationBorderColor: palette.accentBorder,
100
+ sequenceNumberColor: palette.text,
101
+ },
102
+ sequence: { useMaxWidth: true, mirrorActors: false },
103
+ });
104
+ // StrictMode runs the effect twice before the first drawing settles,
105
+ // and mermaid keeps temporary DOM under this id: each attempt its own.
106
+ renderSequence += 1;
107
+ const id = `dla-sequence-${reactId.replace(/[^a-zA-Z0-9_-]/g, '')}-${renderSequence}`;
108
+ const result = await mermaid.render(id, plan.source);
109
+ if (!active || !target.current) {
110
+ return;
111
+ }
112
+ target.current.innerHTML = result.svg;
113
+ result.bindFunctions?.(target.current);
114
+ setState('drawn');
115
+ };
116
+ void draw().catch((reason) => {
117
+ if (active) {
118
+ setState('error');
119
+ setError(reason instanceof Error ? reason.message : String(reason));
120
+ }
121
+ });
122
+ return () => {
123
+ active = false;
124
+ element.replaceChildren();
125
+ };
126
+ }, [
127
+ plan,
128
+ colors,
129
+ fontFamily,
130
+ resolvedColorScheme,
131
+ lines.length,
132
+ load,
133
+ reactId,
134
+ ]);
135
+ // The selected line's arrow or note, marked on the drawing as it stands.
136
+ useEffect(() => {
137
+ const element = target.current;
138
+ if (!element || state !== 'drawn') {
139
+ return;
140
+ }
141
+ for (const part of element.querySelectorAll('[data-transcript-selected]')) {
142
+ part.removeAttribute('data-transcript-selected');
143
+ }
144
+ if (!selected) {
145
+ return;
146
+ }
147
+ const arrow = plan.arrows.indexOf(selected);
148
+ const note = plan.notes.indexOf(selected);
149
+ const texts = element.querySelectorAll(arrow >= 0 ? 'text.messageText' : 'text.noteText');
150
+ const strokes = element.querySelectorAll(arrow >= 0 ? '[class^="messageLine"]' : 'rect.note');
151
+ const index = arrow >= 0 ? arrow : note;
152
+ if (index < 0) {
153
+ return;
154
+ }
155
+ texts[index]?.setAttribute('data-transcript-selected', 'text');
156
+ strokes[index]?.setAttribute('data-transcript-selected', 'stroke');
157
+ }, [selected, plan, state]);
158
+ const onClick = (event) => {
159
+ if (!onSelect || !target.current) {
160
+ return;
161
+ }
162
+ const hit = event.target?.closest(`${MESSAGE_PARTS}, ${NOTE_PARTS}`);
163
+ if (!hit) {
164
+ return;
165
+ }
166
+ const isNote = hit.matches(NOTE_PARTS);
167
+ const parts = [
168
+ ...target.current.querySelectorAll(isNote ? NOTE_PARTS : MESSAGE_PARTS),
169
+ ];
170
+ // Each message is a text and a line, each note a rect and a text: two parts each, in order.
171
+ const index = Math.floor(parts.indexOf(hit) / 2);
172
+ const key = (isNote ? plan.notes : plan.arrows)[index];
173
+ if (key) {
174
+ onSelect(key);
175
+ }
176
+ };
177
+ if (lines.length === 0) {
178
+ return (_jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, m: 0 }, "data-sequence-diagram": "empty", children: emptyText }));
179
+ }
180
+ return (_jsxs(Box, { "data-sequence-diagram": state, "data-sequence-arrows": plan.arrows.length, children: [state === 'loading' && (_jsxs(Box, { display: "flex", alignItems: "center", gap: 2, py: 2, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: "Drawing the sequence\u2026" })] })), state === 'error' && (_jsxs(Text, { as: "p", sx: { color: 'danger.fg', fontSize: 1, m: 0 }, "data-sequence-error": "", children: ["The sequence could not be drawn: ", error] })), _jsx(Box, { ref: target, role: "img", "aria-label": "Sequence diagram of the transcript", onClick: onClick, sx: {
181
+ ...(maxHeight ? { maxHeight, overflow: 'auto' } : {}),
182
+ '& svg': { maxWidth: '100%', height: 'auto' },
183
+ '& text.messageText, & text.noteText, & [class^="messageLine"], & rect.note': {
184
+ cursor: onSelect ? 'pointer' : undefined,
185
+ },
186
+ '& [data-transcript-selected="text"]': { fontWeight: 700 },
187
+ '& [data-transcript-selected="stroke"]': { strokeWidth: 3 },
188
+ } })] }));
189
+ }
190
+ export default SequenceDiagram;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The drill-down (SCENES S3-01): the selected line's span — the words sent
3
+ * and answered, a tool's name, arguments and result when the span recorded
4
+ * them, when it started and ended, its status and an error's message, the
5
+ * span and trace ids, and for a coding harness's step what the Shield
6
+ * decided and by which rule — and *Open in the Inspector*. For a line read from a
7
+ * recording, it says there is no span to open.
8
+ *
9
+ * @module components/teams/transcript/TranscriptDetail
10
+ */
11
+ import type { JSX } from 'react';
12
+ import type { OtelSpan } from '@datalayer/core/lib/otel/types';
13
+ import { type SceneTranscriptLine } from '../sceneTranscriptRules';
14
+ import { type TranscriptSpanIds } from './transcriptDetailRules';
15
+ export type TranscriptDetailProps = {
16
+ line: SceneTranscriptLine;
17
+ /** Its span; none for a line read from a recording. */
18
+ span?: OtelSpan;
19
+ /** The Inspector's page for a span, when the site has one: account-scoped, no trailing slash. */
20
+ inspectorHref?: (ids: TranscriptSpanIds) => string | undefined;
21
+ /** Opens the Inspector over the page on the span. */
22
+ onOpenInspector?: (ids: TranscriptSpanIds) => void;
23
+ onClose?: () => void;
24
+ };
25
+ /** The detail of a line, from its span. */
26
+ export declare function TranscriptDetail({ line, span, inspectorHref, onOpenInspector, onClose, }: TranscriptDetailProps): JSX.Element;
27
+ export default TranscriptDetail;
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Button, Heading, Label, Link, Text } from '@primer/react';
3
+ import { XIcon } from '@primer/octicons-react';
4
+ import { Box } from '@datalayer/primer-addons';
5
+ import { ChatMarkdown } from '../../../chat/messages/ChatMarkdown.js';
6
+ import { lineHeading, timeText, } from '../sceneTranscriptRules.js';
7
+ import { durationText } from './transcriptRows.js';
8
+ import { NO_SPAN_TEXT, transcriptDetailOf, } from './transcriptDetailRules.js';
9
+ /** One labelled part of the detail. */
10
+ function Part({ label, children, mono, }) {
11
+ return (_jsxs(Box, { "data-transcript-detail-part": label.toLowerCase(), children: [_jsx(Text, { as: "div", sx: { color: 'fg.muted', fontSize: 0, fontWeight: 600, mb: 1 }, children: label }), _jsx(Box, { sx: {
12
+ fontSize: 1,
13
+ overflowWrap: 'anywhere',
14
+ ...(mono
15
+ ? { fontFamily: 'mono', fontSize: 0, whiteSpace: 'pre-wrap', m: 0 }
16
+ : {}),
17
+ }, as: mono ? 'pre' : 'div', children: children })] }));
18
+ }
19
+ /** The detail of a line, from its span. */
20
+ export function TranscriptDetail({ line, span, inspectorHref, onOpenInspector, onClose, }) {
21
+ const detail = transcriptDetailOf(line, span);
22
+ const href = detail.ids && inspectorHref ? inspectorHref(detail.ids) : undefined;
23
+ const status = detail.status;
24
+ return (_jsxs(Box, { "data-transcript-detail": line.key, "data-transcript-detail-status": status, display: "flex", flexDirection: "column", gap: 3, p: 3, borderRadius: 2, border: "1px solid", borderColor: "border.default", bg: "canvas.default", minWidth: 0, children: [_jsxs(Box, { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 2, children: [_jsxs(Box, { minWidth: 0, children: [_jsx(Heading, { as: "h3", sx: { fontSize: 1, m: 0, overflowWrap: 'anywhere' }, children: lineHeading(line) }), _jsxs(Box, { display: "flex", gap: 1, mt: 1, flexWrap: "wrap", children: [_jsx(Label, { size: "small", children: line.kind }), _jsx(Label, { size: "small", variant: status === 'failed'
25
+ ? 'danger'
26
+ : status === 'running'
27
+ ? 'accent'
28
+ : 'success', children: status })] })] }), onClose && (_jsx(Button, { size: "small", variant: "invisible", "aria-label": "Close the detail", onClick: onClose, leadingVisual: XIcon, "data-transcript-detail-close": "" }))] }), !span && (_jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, m: 0 }, "data-transcript-detail-recorded": "", children: NO_SPAN_TEXT })), detail.shield && (_jsx(Part, { label: "The Shield", children: _jsxs(Box, { display: "flex", flexDirection: "column", gap: 1, "data-transcript-detail-shield": detail.shield.decision, children: [_jsxs(Box, { display: "flex", gap: 1, flexWrap: "wrap", alignItems: "center", children: [_jsx(Label, { size: "small", variant: detail.shield.decision === 'deny'
29
+ ? 'danger'
30
+ : detail.shield.decision === 'ask'
31
+ ? 'attention'
32
+ : 'success', children: detail.shield.said || 'no decision' }), detail.shield.rule && (_jsxs(Text, { "data-transcript-detail-rule": "", children: [`by the rule \u2018${detail.shield.rule}\u2019`, detail.shield.scope ? ` (${detail.shield.scope})` : ''] }))] }), (detail.shield.decidedBy || detail.shield.harness) && (_jsx(Text, { sx: { color: 'fg.muted', fontSize: 0 }, children: [
33
+ detail.shield.decidedBy
34
+ ? `decided by the ${detail.shield.decidedBy}`
35
+ : '',
36
+ detail.shield.harness ? `for ${detail.shield.harness}` : '',
37
+ ]
38
+ .filter(Boolean)
39
+ .join(', ') })), (detail.shield.path || detail.shield.command) && (_jsx(Text, { sx: { fontFamily: 'mono', fontSize: 0 }, children: detail.shield.command ?? detail.shield.path }))] }) })), detail.tool ? (_jsxs(_Fragment, { children: [_jsx(Part, { label: "Tool", mono: true, children: detail.tool.name }), detail.tool.arguments && (_jsx(Part, { label: "Arguments", mono: true, children: detail.tool.arguments })), detail.tool.result && (_jsx(Part, { label: "Result", mono: true, children: detail.tool.result }))] })) : (_jsxs(_Fragment, { children: [(detail.sent ?? (line.kind === 'asked' ? line.text : undefined)) && (_jsx(Part, { label: "Sent", children: _jsx(ChatMarkdown, { text: detail.sent ?? line.text, density: "compact" }) })), (detail.answered ??
40
+ (line.kind === 'said' ? line.text : undefined)) && (_jsx(Part, { label: "Answered", children: _jsx(ChatMarkdown, { text: detail.answered ?? line.text, density: "compact" }) }))] })), detail.error && (_jsx(Part, { label: "Error", children: _jsx(Text, { sx: { color: 'danger.fg' }, "data-transcript-detail-error": "", children: detail.error }) })), _jsxs(Part, { label: "When", mono: true, children: [`started ${timeText(detail.startedAt ?? line.at)}`, detail.endedAt !== undefined
41
+ ? `\nended ${timeText(detail.endedAt)}`
42
+ : status === 'running'
43
+ ? '\nstill running'
44
+ : '', detail.durationMs !== undefined
45
+ ? `\ntook ${durationText(detail.durationMs)}`
46
+ : ''] }), detail.ids && (_jsxs(Part, { label: "Ids", mono: true, children: [`span ${detail.ids.spanId}\ntrace ${detail.ids.traceId}`, detail.ids.parentSpanId ? `\nparent ${detail.ids.parentSpanId}` : ''] })), detail.ids && (onOpenInspector || href) && (_jsxs(Box, { display: "flex", gap: 2, flexWrap: "wrap", children: [onOpenInspector && (_jsx(Button, { size: "small", onClick: () => onOpenInspector(detail.ids), "data-transcript-detail-inspector": "", children: "Open in the Inspector" })), href && (_jsx(Link, { href: href, target: "_blank", rel: "noopener", sx: { fontSize: 1 }, "data-transcript-detail-trace": "", children: "The trace" }))] }))] }));
47
+ }
48
+ export default TranscriptDetail;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The transcript as a table (SCENES S2-01), Primer's `DataTable`: time,
3
+ * from → to, over A2A or MCP, kind, text, how long, state. Sortable by time
4
+ * (an open call last among its ties), a failed row marked, a click on a row
5
+ * selects its line.
6
+ *
7
+ * @module components/teams/transcript/TranscriptTable
8
+ */
9
+ import type { JSX } from 'react';
10
+ import type { TranscriptRowsLines } from './TranscriptViewsTypes';
11
+ export type TranscriptTableProps = TranscriptRowsLines & {
12
+ /** The selected line's key. */
13
+ selected?: string | null;
14
+ /** A row was clicked: its line's key. */
15
+ onSelect?: (key: string) => void;
16
+ /** What is said while there is no line. */
17
+ emptyText?: string;
18
+ /** How tall it grows before it scrolls, in pixels. */
19
+ maxHeight?: number;
20
+ };
21
+ /** The transcript as a table, with a click on a row selecting its line. */
22
+ export declare function TranscriptTable({ lines, selected, onSelect, emptyText, maxHeight, }: TranscriptTableProps): JSX.Element;
23
+ export default TranscriptTable;
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useMemo } from 'react';
3
+ import { Label, Text } from '@primer/react';
4
+ import { DataTable, Table } from '@primer/react/experimental';
5
+ import { Box } from '@datalayer/primer-addons';
6
+ import { timeText } from '../sceneTranscriptRules.js';
7
+ import { compareByTime, durationText, OVER_WORDS, STATE_WORDS, transcriptRows, } from './transcriptRows.js';
8
+ /** The state, as a label: failed in red, running in the accent, done quiet. */
9
+ function StateLabel({ row }) {
10
+ const variant = row.state === 'failed'
11
+ ? 'danger'
12
+ : row.state === 'open'
13
+ ? 'accent'
14
+ : row.exchangeFailed
15
+ ? 'attention'
16
+ : 'secondary';
17
+ return (_jsx(Label, { variant: variant, size: "small", "data-transcript-state": row.state, children: row.state === 'done' && row.exchangeFailed
18
+ ? 'exchange failed'
19
+ : STATE_WORDS[row.state] }));
20
+ }
21
+ const COLUMNS = [
22
+ {
23
+ id: 'at',
24
+ header: 'Time',
25
+ field: 'at',
26
+ sortBy: compareByTime,
27
+ width: 'auto',
28
+ renderCell: row => (_jsx(Text, { as: "time", sx: { fontFamily: 'mono', fontSize: 0, whiteSpace: 'nowrap' }, dateTime: new Date(row.at).toISOString(), children: timeText(row.at) })),
29
+ },
30
+ {
31
+ id: 'who',
32
+ header: 'From → To',
33
+ field: 'from',
34
+ width: 'auto',
35
+ renderCell: row => (_jsx(Text, { sx: { fontWeight: 600, whiteSpace: 'nowrap' }, children: row.to ? `${row.from} → ${row.to}` : row.from })),
36
+ },
37
+ {
38
+ id: 'over',
39
+ header: 'Over',
40
+ field: 'over',
41
+ width: 'auto',
42
+ renderCell: row => (_jsx(Text, { sx: { color: 'fg.muted', fontSize: 0 }, children: row.over ? OVER_WORDS[row.over] : '—' })),
43
+ },
44
+ {
45
+ id: 'kind',
46
+ header: 'Kind',
47
+ field: 'kind',
48
+ width: 'auto',
49
+ renderCell: row => _jsx(Text, { sx: { fontSize: 0 }, children: row.kind }),
50
+ },
51
+ {
52
+ id: 'text',
53
+ header: 'Text',
54
+ field: 'text',
55
+ width: 'grow',
56
+ renderCell: row => (_jsxs(Box, { as: "span", "data-transcript-row": row.id, sx: {
57
+ display: 'block',
58
+ fontFamily: row.kind === 'called' ? 'mono' : undefined,
59
+ color: row.state === 'failed' ? 'danger.fg' : undefined,
60
+ overflowWrap: 'anywhere',
61
+ whiteSpace: 'normal',
62
+ }, children: [row.text, row.state === 'open' ? '…' : ''] })),
63
+ },
64
+ {
65
+ id: 'duration',
66
+ header: 'Duration',
67
+ field: 'durationMs',
68
+ align: 'end',
69
+ width: 'auto',
70
+ renderCell: row => (_jsx(Text, { sx: { fontFamily: 'mono', fontSize: 0, whiteSpace: 'nowrap' }, children: durationText(row.durationMs) })),
71
+ },
72
+ {
73
+ id: 'state',
74
+ header: 'State',
75
+ field: 'state',
76
+ width: 'auto',
77
+ renderCell: row => _jsx(StateLabel, { row: row }),
78
+ },
79
+ ];
80
+ /** The transcript as a table, with a click on a row selecting its line. */
81
+ export function TranscriptTable({ lines, selected, onSelect, emptyText = 'What the scene says and does shows here, row by row.', maxHeight, }) {
82
+ const title = useId();
83
+ const rows = useMemo(() => transcriptRows(lines), [lines]);
84
+ // A click anywhere on a row: the row's line, found from the text cell's mark.
85
+ const onClick = (event) => {
86
+ if (!onSelect) {
87
+ return;
88
+ }
89
+ const target = event.target;
90
+ const tr = target?.closest('tr');
91
+ const marked = tr?.querySelector('[data-transcript-row]');
92
+ const key = marked?.dataset.transcriptRow;
93
+ if (key) {
94
+ onSelect(key);
95
+ }
96
+ };
97
+ if (rows.length === 0) {
98
+ return (_jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, m: 0 }, "data-transcript-table": "", children: emptyText }));
99
+ }
100
+ return (_jsx(Box, { "data-transcript-table": "", "data-transcript-rows": rows.length, onClick: onClick, sx: {
101
+ ...(maxHeight ? { maxHeight, overflow: 'auto' } : {}),
102
+ '& tr': { cursor: onSelect ? 'pointer' : undefined },
103
+ ...(selected
104
+ ? {
105
+ [`& tr:has([data-transcript-row="${CSS.escape(selected)}"])`]: {
106
+ backgroundColor: 'accent.subtle',
107
+ },
108
+ }
109
+ : {}),
110
+ }, children: _jsxs(Table.Container, { children: [_jsx(Table.Title, { as: "h3", id: title, children: "Transcript" }), _jsx(DataTable, { "aria-labelledby": title, data: rows, columns: COLUMNS, cellPadding: "condensed", initialSortColumn: "at", initialSortDirection: "ASC" })] }) }));
111
+ }
112
+ export default TranscriptTable;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The three views of a transcript under one switch (SCENES S2-03): Lines
3
+ * (`SceneTranscript`), Table (`TranscriptTable`) and Sequence
4
+ * (`SequenceDiagram`), with the filter they share (members, kinds, *failed
5
+ * only*, failed first), the selection they share (a line, a row, an arrow:
6
+ * the same {@link TranscriptSelection}, handed to the page for the stage),
7
+ * the drill-down beside the view on a wide stage and under it on a narrow
8
+ * one (`TranscriptDetail`, S3-01), *Copy* of the view (the lines as text,
9
+ * the table as Markdown, the diagram as its source) and *Export JSON*
10
+ * (S3-03).
11
+ *
12
+ * @module components/teams/transcript/TranscriptViews
13
+ */
14
+ import type { JSX } from 'react';
15
+ import type { OtelLiveTracer } from '@datalayer/core/lib/otel/live';
16
+ import type { OtelSpan } from '@datalayer/core/lib/otel/types';
17
+ import { type SceneTranscriptLine, type SceneTranscriptMember } from '../sceneTranscriptRules';
18
+ import { type MermaidLike } from './SequenceDiagram';
19
+ import { type SequenceCastMember } from './sequenceDiagramRules';
20
+ import { type TranscriptSpanIds } from './transcriptDetailRules';
21
+ import { type TranscriptSelection } from './selection';
22
+ import type { TranscriptViewChoice } from './TranscriptViewsTypes';
23
+ export type TranscriptViewsProps = {
24
+ /** The lines, in order. */
25
+ lines: readonly SceneTranscriptLine[];
26
+ /** The members, in the order the diagram's participants stand, with their connections. */
27
+ members?: readonly SceneTranscriptMember[];
28
+ /** The spans the lines were read from: the drill-down reads a line's. */
29
+ spans?: readonly OtelSpan[];
30
+ /** Or the tracer they are recorded into. */
31
+ tracer?: OtelLiveTracer | null;
32
+ /** The lines come from a recording: the drill-down says there is no span. Said by itself when neither spans nor a tracer is given. */
33
+ recorded?: boolean;
34
+ /** The scene, by id: the export's file name. */
35
+ scene?: string;
36
+ /** The selected line's key, held by the page; with `onSelect`. */
37
+ selected?: string | null;
38
+ /** The selection changed: a line, or none. */
39
+ onSelect?: (selection: TranscriptSelection | null) => void;
40
+ /** Which view shows first: the lines unless said. */
41
+ defaultView?: TranscriptViewChoice;
42
+ /** The view, held by the page; with `onViewChange`. */
43
+ view?: TranscriptViewChoice;
44
+ onViewChange?: (view: TranscriptViewChoice) => void;
45
+ /** What is said while there is no line yet. */
46
+ emptyText?: string;
47
+ /** How tall a view grows before it scrolls, in pixels. */
48
+ maxHeight?: number;
49
+ /** The Inspector's page for a span, when the site has one. */
50
+ inspectorHref?: (ids: TranscriptSpanIds) => string | undefined;
51
+ /** Opens the Inspector over the page on the span. */
52
+ onOpenInspector?: (ids: TranscriptSpanIds) => void;
53
+ /** The clipboard: `navigator.clipboard.writeText` unless a test gives one. */
54
+ writeText?: (text: string) => Promise<void>;
55
+ /** How a file is handed to the browser: a download unless a test says. */
56
+ download?: (name: string, text: string) => void;
57
+ /** How mermaid is loaded (`SequenceDiagram`). */
58
+ loadMermaid?: () => Promise<MermaidLike>;
59
+ };
60
+ /** The cast as the diagram stands it: the members, then every system they reach. */
61
+ export declare function sequenceCastOf(members: readonly SceneTranscriptMember[]): SequenceCastMember[];
62
+ /** What the view copies: the lines as text, the table as Markdown, the diagram as its source. */
63
+ export declare function copyTextOf(view: TranscriptViewChoice, lines: readonly SceneTranscriptLine[], cast: readonly SequenceCastMember[]): string;
64
+ /** The three views, the filter, the selection, the drill-down, Copy and Export. */
65
+ export declare function TranscriptViews({ lines, members, spans, tracer, recorded, scene, selected, onSelect, defaultView, view, onViewChange, emptyText, maxHeight, inspectorHref, onOpenInspector, writeText, download, loadMermaid, }: TranscriptViewsProps): JSX.Element;
66
+ export default TranscriptViews;
@@ -0,0 +1,135 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useMemo, useState } from 'react';
3
+ import { ActionList, ActionMenu, Button, Checkbox, FormControl, SegmentedControl, } from '@primer/react';
4
+ import { Box } from '@datalayer/primer-addons';
5
+ import { useOtelLiveSpans } from '@datalayer/core/lib/otel/live/index.js';
6
+ import { SceneTranscript } from '../SceneTranscript.js';
7
+ import { transcriptText, } from '../sceneTranscriptRules.js';
8
+ import { SequenceDiagram } from './SequenceDiagram.js';
9
+ import { sequenceDiagramOf, } from './sequenceDiagramRules.js';
10
+ import { TranscriptDetail } from './TranscriptDetail.js';
11
+ import { spanOfLine } from './transcriptDetailRules.js';
12
+ import { downloadText, transcriptFileName, transcriptJson, } from './transcriptExport.js';
13
+ import { filterLines, namesOf, transcriptMarkdown, transcriptRows, } from './transcriptRows.js';
14
+ import { TranscriptTable } from './TranscriptTable.js';
15
+ import { selectionOfLine } from './selection.js';
16
+ const KINDS = ['asked', 'called', 'said'];
17
+ const KIND_WORDS = {
18
+ asked: 'Asked',
19
+ called: 'Called',
20
+ said: 'Said',
21
+ };
22
+ const VIEW_WORDS = {
23
+ lines: 'Lines',
24
+ table: 'Table',
25
+ sequence: 'Sequence',
26
+ };
27
+ const COPY_WORDS = {
28
+ lines: 'Copy as text',
29
+ table: 'Copy as Markdown',
30
+ sequence: 'Copy as mermaid',
31
+ };
32
+ /** How long *Copied* stays, in milliseconds. */
33
+ const COPIED_MS = 1500;
34
+ const NO_MEMBERS = [];
35
+ /** The cast as the diagram stands it: the members, then every system they reach. */
36
+ export function sequenceCastOf(members) {
37
+ const cast = members.map(member => ({
38
+ name: member.name,
39
+ }));
40
+ for (const member of members) {
41
+ for (const connection of member.connections ?? []) {
42
+ if (!cast.some(one => one.name === connection.label)) {
43
+ cast.push({ name: connection.label, system: true });
44
+ }
45
+ }
46
+ }
47
+ return cast;
48
+ }
49
+ /** What the view copies: the lines as text, the table as Markdown, the diagram as its source. */
50
+ export function copyTextOf(view, lines, cast) {
51
+ switch (view) {
52
+ case 'table':
53
+ return transcriptMarkdown(transcriptRows(lines));
54
+ case 'sequence':
55
+ return sequenceDiagramOf(lines, cast);
56
+ default:
57
+ return transcriptText(lines);
58
+ }
59
+ }
60
+ /** The three views, the filter, the selection, the drill-down, Copy and Export. */
61
+ export function TranscriptViews({ lines, members = NO_MEMBERS, spans, tracer, recorded, scene, selected, onSelect, defaultView = 'lines', view, onViewChange, emptyText, maxHeight = 360, inspectorHref, onOpenInspector, writeText, download = downloadText, loadMermaid, }) {
62
+ const [ownView, setOwnView] = useState(defaultView);
63
+ const shown = view ?? ownView;
64
+ const choose = (next) => {
65
+ setOwnView(next);
66
+ onViewChange?.(next);
67
+ };
68
+ const [ownSelected, setOwnSelected] = useState(null);
69
+ const selectedKey = selected !== undefined ? selected : ownSelected;
70
+ const select = useCallback((key) => {
71
+ setOwnSelected(key);
72
+ const line = key ? lines.find(one => one.key === key) : undefined;
73
+ onSelect?.(line ? selectionOfLine(line) : null);
74
+ }, [lines, onSelect]);
75
+ const [filter, setFilter] = useState({});
76
+ const [copied, setCopied] = useState(false);
77
+ const liveSpans = useOtelLiveSpans(spans ? null : tracer);
78
+ const allSpans = spans ?? liveSpans;
79
+ const isRecording = recorded ?? (!spans && !tracer);
80
+ const names = useMemo(() => namesOf(lines), [lines]);
81
+ const kept = useMemo(() => filterLines(lines, filter), [lines, filter]);
82
+ const cast = useMemo(() => sequenceCastOf(members), [members]);
83
+ const selectedLine = selectedKey
84
+ ? lines.find(line => line.key === selectedKey)
85
+ : undefined;
86
+ const selectedSpan = selectedLine && !isRecording
87
+ ? spanOfLine(selectedLine, allSpans)
88
+ : undefined;
89
+ const toggle = (list, value) => list?.includes(value)
90
+ ? list.filter(one => one !== value)
91
+ : [...(list ?? []), value];
92
+ const onCopy = async () => {
93
+ const write = writeText ??
94
+ (text => globalThis.navigator?.clipboard?.writeText(text) ?? Promise.resolve());
95
+ try {
96
+ await write(copyTextOf(shown, kept, cast));
97
+ setCopied(true);
98
+ setTimeout(() => setCopied(false), COPIED_MS);
99
+ }
100
+ catch {
101
+ // The clipboard refused: the view is still on screen.
102
+ }
103
+ };
104
+ const onExport = () => download(transcriptFileName(scene), transcriptJson(kept, { scene }));
105
+ const narrowed = Boolean(filter.members?.length || filter.kinds?.length || filter.failedOnly);
106
+ return (_jsxs(Box, { "data-transcript-views": shown, "data-transcript-kept": kept.length, "data-transcript-recorded": isRecording ? '' : undefined, children: [_jsxs(Box, { display: "flex", alignItems: "center", gap: 2, flexWrap: "wrap", mb: 2, children: [_jsx(SegmentedControl, { "aria-label": "Transcript view", size: "small", children: Object.keys(VIEW_WORDS).map(choice => (_jsx(SegmentedControl.Button, { selected: shown === choice, "aria-pressed": shown === choice, onClick: () => choose(choice), "data-transcript-view-choice": choice, children: VIEW_WORDS[choice] }, choice))) }), _jsxs(ActionMenu, { children: [_jsx(ActionMenu.Button, { size: "small", "data-transcript-filter": "members", children: filter.members?.length
107
+ ? `Members (${filter.members.length})`
108
+ : 'Members' }), _jsx(ActionMenu.Overlay, { width: "small", children: _jsxs(ActionList, { selectionVariant: "multiple", children: [names.map(name => (_jsx(ActionList.Item, { selected: Boolean(filter.members?.includes(name)), onSelect: () => setFilter(prior => ({
109
+ ...prior,
110
+ members: toggle(prior.members, name),
111
+ })), "data-transcript-member": name, children: name }, name))), names.length === 0 && (_jsx(ActionList.Item, { disabled: true, children: "No one yet" }))] }) })] }), _jsxs(ActionMenu, { children: [_jsx(ActionMenu.Button, { size: "small", "data-transcript-filter": "kinds", children: filter.kinds?.length ? `Kinds (${filter.kinds.length})` : 'Kinds' }), _jsx(ActionMenu.Overlay, { width: "small", children: _jsx(ActionList, { selectionVariant: "multiple", children: KINDS.map(kind => (_jsx(ActionList.Item, { selected: Boolean(filter.kinds?.includes(kind)), onSelect: () => setFilter(prior => ({
112
+ ...prior,
113
+ kinds: toggle(prior.kinds, kind),
114
+ })), "data-transcript-kind": kind, children: KIND_WORDS[kind] }, kind))) }) })] }), _jsxs(FormControl, { sx: { alignItems: 'center' }, children: [_jsx(Checkbox, { checked: Boolean(filter.failedOnly), onChange: event => setFilter(prior => ({
115
+ ...prior,
116
+ failedOnly: event.target.checked,
117
+ })), "data-transcript-failed-only": "" }), _jsx(FormControl.Label, { sx: { fontSize: 0, fontWeight: 'normal' }, children: "Failed only" })] }), _jsxs(FormControl, { sx: { alignItems: 'center' }, children: [_jsx(Checkbox, { checked: Boolean(filter.failedFirst), onChange: event => setFilter(prior => ({
118
+ ...prior,
119
+ failedFirst: event.target.checked,
120
+ })), "data-transcript-failed-first": "" }), _jsx(FormControl.Label, { sx: { fontSize: 0, fontWeight: 'normal' }, children: "Failed first" })] }), _jsx(Box, { flex: 1 }), kept.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Button, { size: "small", onClick: () => void onCopy(), "data-transcript-copy": shown, children: copied ? 'Copied' : COPY_WORDS[shown] }), _jsx(Button, { size: "small", onClick: onExport, "data-transcript-export": "", children: "Export JSON" })] }))] }), _jsxs(Box, { sx: {
121
+ display: 'grid',
122
+ gap: 3,
123
+ alignItems: 'start',
124
+ gridTemplateColumns: selectedLine
125
+ ? ['1fr', '1fr', 'minmax(0, 1fr) minmax(280px, 340px)']
126
+ : '1fr',
127
+ }, children: [_jsxs(Box, { minWidth: 0, children: [shown === 'lines' && (_jsx(SceneTranscript, { lines: kept, emptyText: narrowed && lines.length
128
+ ? 'No line matches the filter.'
129
+ : emptyText, maxHeight: maxHeight, copy: false, selected: selectedKey, onSelect: select })), shown === 'table' && (_jsx(TranscriptTable, { lines: kept, emptyText: narrowed && lines.length
130
+ ? 'No row matches the filter.'
131
+ : emptyText, maxHeight: maxHeight, selected: selectedKey, onSelect: select })), shown === 'sequence' && (_jsx(SequenceDiagram, { lines: kept, cast: cast, emptyText: narrowed && lines.length
132
+ ? 'No arrow matches the filter.'
133
+ : emptyText, maxHeight: maxHeight, selected: selectedKey, onSelect: select, ...(loadMermaid ? { load: loadMermaid } : {}) }))] }), selectedLine && (_jsx(TranscriptDetail, { line: selectedLine, span: selectedSpan, inspectorHref: inspectorHref, onOpenInspector: onOpenInspector, onClose: () => select(null) }))] })] }));
134
+ }
135
+ export default TranscriptViews;