@datalayer/agent-runtimes 1.3.84 → 1.3.86

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 (162) hide show
  1. package/lib/chat/ChatFloating.d.ts +5 -4
  2. package/lib/chat/ChatFloating.js +8 -6
  3. package/lib/chat/ChatLanguage.d.ts +17 -0
  4. package/lib/chat/ChatLanguage.js +29 -0
  5. package/lib/chat/assistant/AssistantStage.d.ts +4 -4
  6. package/lib/chat/assistant/AssistantStage.js +4 -2
  7. package/lib/chat/assistant/BalloonParts.d.ts +2 -1
  8. package/lib/chat/assistant/BalloonParts.js +12 -5
  9. package/lib/chat/assistant/BalloonVisual.js +3 -1
  10. package/lib/chat/assistant/ConversationBalloon.d.ts +1 -1
  11. package/lib/chat/assistant/ConversationBalloon.js +3 -2
  12. package/lib/chat/assistant/SpeechBalloon.d.ts +1 -1
  13. package/lib/chat/assistant/SpeechBalloon.js +10 -7
  14. package/lib/chat/assistant/toolLine.d.ts +8 -4
  15. package/lib/chat/assistant/toolLine.js +13 -9
  16. package/lib/chat/base/ChatBase.js +14 -11
  17. package/lib/chat/display/EmptyState.js +3 -1
  18. package/lib/chat/index.d.ts +2 -0
  19. package/lib/chat/index.js +3 -0
  20. package/lib/chat/messages/ChatMessages.js +3 -1
  21. package/lib/chat/messages/TurnFooter.js +3 -1
  22. package/lib/chat/prompt/InputPrompt.js +3 -1
  23. package/lib/chat/prompt/InputPromptLexical.js +3 -1
  24. package/lib/chat/prompt/footer/InPromptFooter.js +3 -1
  25. package/lib/chat/tools/ToolApprovalBanner.js +5 -3
  26. package/lib/chat/tools/ToolApprovalDialog.js +5 -3
  27. package/lib/chat/tools/ToolCallDisplay.js +29 -25
  28. package/lib/chat/words.d.ts +102 -0
  29. package/lib/chat/words.js +460 -0
  30. package/lib/components/a2ui/custom/index.d.ts +39 -0
  31. package/lib/components/a2ui/custom/index.js +95 -0
  32. package/lib/components/a2ui/custom/sandbox.d.ts +73 -0
  33. package/lib/components/a2ui/custom/sandbox.js +188 -0
  34. package/lib/components/a2ui/datalayer/index.d.ts +1 -1
  35. package/lib/components/a2ui/datalayer/index.js +1 -1
  36. package/lib/components/a2ui/datalayer/schema.d.ts +10 -0
  37. package/lib/components/a2ui/datalayer/schema.js +12 -1
  38. package/lib/components/a2ui/index.d.ts +1 -0
  39. package/lib/components/a2ui/index.js +1 -0
  40. package/lib/components/a2ui/visibleWhen.d.ts +12 -1
  41. package/lib/components/a2ui/visibleWhen.js +24 -3
  42. package/lib/components/inspector/AgentInspector.d.ts +25 -44
  43. package/lib/components/inspector/AgentInspector.js +27 -215
  44. package/lib/components/inspector/AgentInspectorDialog.d.ts +6 -6
  45. package/lib/components/inspector/AgentInspectorDialog.js +2 -2
  46. package/lib/components/inspector/a2aSpans.d.ts +51 -0
  47. package/lib/components/inspector/a2aSpans.js +373 -0
  48. package/lib/components/inspector/agentSpans.d.ts +120 -0
  49. package/lib/components/inspector/agentSpans.js +253 -0
  50. package/lib/components/inspector/chatSpans.d.ts +36 -0
  51. package/lib/components/inspector/chatSpans.js +50 -0
  52. package/lib/components/inspector/classify.d.ts +11 -8
  53. package/lib/components/inspector/classify.js +23 -24
  54. package/lib/components/inspector/index.d.ts +7 -7
  55. package/lib/components/inspector/index.js +7 -7
  56. package/lib/components/teams/A2ATeamGraph.d.ts +117 -22
  57. package/lib/components/teams/A2ATeamGraph.js +217 -97
  58. package/lib/components/teams/SceneTranscript.d.ts +29 -0
  59. package/lib/components/teams/SceneTranscript.js +54 -0
  60. package/lib/components/teams/SceneView.d.ts +41 -0
  61. package/lib/components/teams/SceneView.js +28 -0
  62. package/lib/components/teams/index.d.ts +4 -0
  63. package/lib/components/teams/index.js +4 -0
  64. package/lib/components/teams/sceneStage.d.ts +120 -0
  65. package/lib/components/teams/sceneStage.js +170 -0
  66. package/lib/components/teams/sceneTranscript.d.ts +74 -0
  67. package/lib/components/teams/sceneTranscript.js +294 -0
  68. package/lib/components/teams/teamConnections.d.ts +2 -2
  69. package/lib/components/teams/teamConnections.js +3 -3
  70. package/lib/components/teams/useA2ATeam.d.ts +94 -22
  71. package/lib/components/teams/useA2ATeam.js +356 -140
  72. package/lib/examples/AgentA2ATeamExample.d.ts +10 -9
  73. package/lib/examples/AgentA2ATeamExample.js +70 -60
  74. package/lib/examples/AssistantExample.js +2 -2
  75. package/lib/examples/ScenesExample.d.ts +26 -0
  76. package/lib/examples/ScenesExample.js +241 -0
  77. package/lib/examples/example-selector.js +1 -0
  78. package/lib/examples/exampleGroups.js +3 -0
  79. package/lib/examples/index.d.ts +1 -0
  80. package/lib/examples/index.js +1 -0
  81. package/lib/examples/main.js +4 -0
  82. package/lib/loop/apps/AppComposer.d.ts +2 -1
  83. package/lib/loop/apps/AppComposer.js +6 -3
  84. package/lib/loop/apps/AppRenderer.d.ts +2 -1
  85. package/lib/loop/apps/AppRenderer.js +34 -25
  86. package/lib/loop/apps/appspec.d.ts +11 -1
  87. package/lib/loop/apps/appspec.js +115 -1
  88. package/lib/loop/apps/checks.d.ts +8 -0
  89. package/lib/loop/apps/checks.js +106 -4
  90. package/lib/loop/apps/customComponents.d.ts +41 -0
  91. package/lib/loop/apps/customComponents.js +260 -0
  92. package/lib/loop/apps/hostTools.d.ts +8 -1
  93. package/lib/loop/apps/hostTools.js +9 -0
  94. package/lib/loop/apps/index.d.ts +1 -0
  95. package/lib/loop/apps/index.js +1 -0
  96. package/lib/loop/core/a2uiComponents.d.ts +10 -0
  97. package/lib/loop/core/a2uiComponents.js +15 -0
  98. package/lib/loop/core/index.d.ts +1 -0
  99. package/lib/loop/core/index.js +3 -0
  100. package/lib/loop/embed/AppEmbed.d.ts +11 -3
  101. package/lib/loop/embed/AppEmbed.js +42 -16
  102. package/lib/loop/embed/element.d.ts +5 -0
  103. package/lib/loop/embed/element.js +61 -5
  104. package/lib/loop/embed/embedConfig.d.ts +8 -0
  105. package/lib/loop/embed/embedConfig.js +18 -0
  106. package/lib/loop/embed/hostBridge.d.ts +28 -2
  107. package/lib/loop/embed/hostBridge.js +22 -2
  108. package/lib/loop/index.d.ts +4 -3
  109. package/lib/loop/index.js +4 -3
  110. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +7 -1
  111. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +7 -2
  112. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +4 -1
  113. package/lib/loop/plugins/a2ui-surface/ownComponents.d.ts +6 -0
  114. package/lib/loop/plugins/a2ui-surface/ownComponents.js +30 -0
  115. package/lib/loop/plugins/app-components/index.d.ts +26 -0
  116. package/lib/loop/plugins/app-components/index.js +56 -0
  117. package/lib/loop/plugins/app-elements/AppElements.js +4 -1
  118. package/lib/loop/plugins/app-page/AppPage.d.ts +9 -0
  119. package/lib/loop/plugins/app-page/AppPage.js +106 -8
  120. package/lib/loop/plugins/app-page/appPageModel.d.ts +17 -1
  121. package/lib/loop/plugins/app-page/appPageModel.js +120 -4
  122. package/lib/loop/plugins/app-page/index.d.ts +1 -0
  123. package/lib/loop/plugins/app-page/index.js +1 -0
  124. package/lib/loop/plugins/app-page/pageRun.d.ts +46 -0
  125. package/lib/loop/plugins/app-page/pageRun.js +111 -0
  126. package/lib/loop/plugins/assistant/index.js +8 -6
  127. package/lib/loop/plugins/chat/ChatView.js +6 -4
  128. package/lib/loop/plugins/chat/index.d.ts +1 -1
  129. package/lib/loop/plugins/chat/index.js +2 -1
  130. package/lib/models/PageTag.d.ts +1 -1
  131. package/lib/runtimes/browser/a2aPeer.d.ts +2 -1
  132. package/lib/specs/actions.js +78 -1
  133. package/lib/specs/apps.d.ts +5 -0
  134. package/lib/specs/apps.js +1384 -231
  135. package/lib/specs/appspecSchema.js +191 -1
  136. package/lib/specs/index.d.ts +1 -0
  137. package/lib/specs/index.js +1 -0
  138. package/lib/specs/mcpServers.js +8 -2
  139. package/lib/specs/scenes.d.ts +21 -0
  140. package/lib/specs/scenes.js +1191 -0
  141. package/lib/specs/teams/teams.d.ts +3 -0
  142. package/lib/specs/teams/teams.js +249 -0
  143. package/lib/types/agentspecs.d.ts +70 -0
  144. package/lib/types/chat.d.ts +6 -5
  145. package/lib/types/index.d.ts +1 -0
  146. package/lib/types/index.js +1 -0
  147. package/lib/types/scenes.d.ts +207 -0
  148. package/lib/types/scenes.js +5 -0
  149. package/lib/types/teams.d.ts +13 -2
  150. package/package.json +4 -4
  151. package/scripts/codegen/generate_agents.py +1 -0
  152. package/scripts/codegen/generate_apps.py +4 -2
  153. package/scripts/codegen/generate_scenes.py +301 -0
  154. package/scripts/codegen/generate_teams.py +15 -2
  155. package/lib/components/inspector/JsonTree.d.ts +0 -24
  156. package/lib/components/inspector/JsonTree.js +0 -89
  157. package/lib/components/inspector/a2aInspect.d.ts +0 -56
  158. package/lib/components/inspector/a2aInspect.js +0 -478
  159. package/lib/components/inspector/agentInspector.d.ts +0 -179
  160. package/lib/components/inspector/agentInspector.js +0 -267
  161. package/lib/components/inspector/chatInspect.d.ts +0 -38
  162. package/lib/components/inspector/chatInspect.js +0 -65
@@ -19,15 +19,16 @@
19
19
  * list_invoices…" while Accounting calls Odoo. Then the report appears. The
20
20
  * page's own state is `useA2ATeam`'s, which the landing's home page runs too.
21
21
  *
22
- * Under the graph, each member has its own Agent Inspector: Sales' — its
23
- * model turns, its `ask_accounting` call, the A2A messages it sends and
24
- * receives — and Accounting's — what it received over A2A, its Odoo MCP
25
- * calls, `write_notebook`, its status updates and artifacts, as the A2A
26
- * stream tells them. Both read one record (`useAgentInspector`), fed by the
27
- * team (`useA2ATeam`'s `inspector`) and by the fetch the A2A client sends
28
- * with (`inspectA2AFetch`); an A2A message shows in both, sent in one and
29
- * received in the other. Right-click a member for its menu: *Inspect the
30
- * agent…* opens its inspector over the page.
22
+ * Under the graph, each member has its own Agent Inspector, core's OTEL
23
+ * view of the page's OpenTelemetry spans: Sales' — its model turns
24
+ * (`invoke_agent`), its `ask_accounting` call (`execute_tool`), the A2A
25
+ * requests it sends and what came back, event by event — and Accounting's —
26
+ * the A2A requests sent to it (`peer.service`), its Odoo MCP calls,
27
+ * `write_notebook`, as the A2A stream tells them. Both read one tracer
28
+ * (`useAgentInspector`), fed by the team (`useA2ATeam`'s `inspector`) and by
29
+ * the fetch the A2A client sends with (`traceA2AFetch`); an A2A request
30
+ * shows in both. Right-click a member for its menu: *Inspect the agent…*
31
+ * opens its inspector over the page.
31
32
  *
32
33
  * Sales' suggestions — its Appspec's starters — are chips in its balloon
33
34
  * while it waits for a question: one clicked is asked, as if typed.
@@ -10,9 +10,9 @@ import { useExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl';
10
10
  import { useBrowserInference } from '../hooks/useBrowserInference';
11
11
  import { readJwtClaims, tokenLifetimeMs, useAnonymousSessionStore, } from '../runtimes/browser/anonymousToken';
12
12
  import { connectA2APeer } from '../runtimes/browser/a2aPeer';
13
- import { A2ATeamGraph, NOTEBOOK_AND_WORDS, teamConnectionsOf, teamToolClassifier, useA2ATeam, usePeerSandbox, } from '../components/teams';
13
+ import { A2ATeamGraph, SceneView, NOTEBOOK_AND_WORDS, teamConnectionsOf, teamToolClassifier, useA2ATeam, usePeerSandbox, } from '../components/teams';
14
14
  import { ACCOUNTING_APP_0_0_1, SALES_APP_0_0_1 } from '../specs/apps';
15
- import { AgentInspector, classifyToolCall, inspectA2AFetch, useAgentInspector, } from '../components/inspector';
15
+ import { AgentInspector, classifyToolCall, traceA2AFetch, useAgentInspector, } from '../components/inspector';
16
16
  import { SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1 } from '../specs/teams/teams';
17
17
  const TEAM = SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1;
18
18
  const SALES = SALES_APP_0_0_1;
@@ -46,8 +46,18 @@ function AgentA2ATeam() {
46
46
  }, [accountingKey]);
47
47
  const [accountingKeyEnded, setAccountingKeyEnded] = useState(() => accountingKeyClock !== undefined &&
48
48
  accountingKeyClock.expiresAt <= Date.now());
49
- // One record of what both members do, each inspector showing its own.
50
- const { sink: inspector } = useAgentInspector();
49
+ // One tracer of what both members do, each inspector showing its own.
50
+ const { tracer: inspector } = useAgentInspector();
51
+ // The members as the transcript names them: the spans' services, and the
52
+ // connections a tool call is of (Accounting → Odoo).
53
+ const sceneMembers = useMemo(() => [
54
+ { id: SALES.id, name: SALES.name },
55
+ {
56
+ id: ACCOUNTING.id,
57
+ name: ACCOUNTING.name,
58
+ connections: ACCOUNTING_CONNECTIONS,
59
+ },
60
+ ], []);
51
61
  const [peer, setPeer] = useState(null);
52
62
  const [peerError, setPeerError] = useState(null);
53
63
  const [draft, setDraft] = useState('');
@@ -61,9 +71,9 @@ function AgentA2ATeam() {
61
71
  connectA2APeer({
62
72
  url: accountingUrl,
63
73
  key: accountingKey,
64
- // The A2A traffic, recorded: Sales sends, Accounting answers.
65
- fetch: inspectA2AFetch(globalThis.fetch.bind(globalThis), {
66
- sink: inspector,
74
+ // The A2A traffic, as spans: Sales sends, Accounting answers.
75
+ fetch: traceA2AFetch(globalThis.fetch.bind(globalThis), {
76
+ tracer: inspector,
67
77
  asker: SALES.name,
68
78
  peer: ACCOUNTING.name,
69
79
  classifyTool: teamToolClassifier(ACCOUNTING_CONNECTIONS, classifyToolCall),
@@ -116,66 +126,66 @@ function AgentA2ATeam() {
116
126
  px: 2,
117
127
  pt: 2,
118
128
  mb: 3,
119
- }, children: _jsx(A2ATeamGraph, { entry: {
120
- id: SALES.id,
121
- name: SALES.name,
122
- emoji: SALES.emoji,
123
- character: SALES.interface.assistant ?? 'paperclip',
124
- where: salesMember.runsIn === 'browser'
125
- ? 'in your browser'
126
- : 'on a runtime',
127
- persona: team.entryPersona,
128
- onToggle: () => composer.current?.focus(),
129
- conversationLabel: `Ask ${SALES.name}`,
130
- onAway: team.setEntryAway,
131
- history: team.entryHistory,
132
- expandTarget: notebookArea,
133
- notebookTitle: `${ACCOUNTING.name}\u2019s notebook`,
134
- suggestions: team.ready && !team.busy ? suggestions : undefined,
135
- onSuggestion: suggestion => send(suggestion.prompt),
136
- inspector,
137
- onStop: team.busy ? team.stop : undefined,
138
- about: {
129
+ }, children: _jsx(SceneView, { members: sceneMembers, tracer: inspector, emptyText: "Ask Sales: what it says to Accounting, what Accounting reads in Odoo and answers, line by line, shows here.", children: _jsx(A2ATeamGraph, { entry: {
130
+ id: SALES.id,
139
131
  name: SALES.name,
140
- spec: `${SALES.id}:${SALES.version}`,
141
- model: SALES.model || undefined,
142
- where: 'in your browser',
143
- description: SALES.description || undefined,
144
- },
145
- }, peer: {
146
- id: ACCOUNTING.id,
147
- name: ACCOUNTING.name,
148
- // Its face as its card says it, once it is reached.
149
- emoji: peer?.face?.emoji ?? ACCOUNTING.emoji,
150
- character: ACCOUNTING.interface.assistant ?? 'wizard',
151
- where: accountingWhere,
152
- persona: team.peerPersona,
153
- onAway: team.setPeerAway,
154
- history: team.peerHistory,
155
- connections: ACCOUNTING_CONNECTIONS,
156
- inspector,
157
- sandbox: accountingSandbox,
158
- about: {
132
+ emoji: SALES.emoji,
133
+ character: SALES.interface.assistant ?? 'paperclip',
134
+ where: salesMember.runsIn === 'browser'
135
+ ? 'in your browser'
136
+ : 'on a runtime',
137
+ persona: team.entryPersona,
138
+ onToggle: () => composer.current?.focus(),
139
+ conversationLabel: `Ask ${SALES.name}`,
140
+ onAway: team.setEntryAway,
141
+ history: team.entryHistory,
142
+ expandTarget: notebookArea,
143
+ notebookTitle: `${ACCOUNTING.name}\u2019s notebook`,
144
+ suggestions: team.ready && !team.busy ? suggestions : undefined,
145
+ onSuggestion: suggestion => send(suggestion.prompt),
146
+ inspector,
147
+ onStop: team.busy ? team.stop : undefined,
148
+ about: {
149
+ name: SALES.name,
150
+ spec: `${SALES.id}:${SALES.version}`,
151
+ model: SALES.model || undefined,
152
+ where: 'in your browser',
153
+ description: SALES.description || undefined,
154
+ },
155
+ }, peer: {
156
+ id: ACCOUNTING.id,
159
157
  name: ACCOUNTING.name,
160
- spec: `${ACCOUNTING.id}:${ACCOUNTING.version}`,
161
- model: ACCOUNTING.model || undefined,
158
+ // Its face as its card says it, once it is reached.
159
+ emoji: peer?.face?.emoji ?? ACCOUNTING.emoji,
160
+ character: ACCOUNTING.interface.assistant ?? 'wizard',
162
161
  where: accountingWhere,
163
- // What its A2A card says, once it is reached.
164
- description: peer?.card.description || ACCOUNTING.description || undefined,
165
- skills: peer?.card.skills.map(skill => skill.name),
166
- protocol: peer ? 'a2a' : undefined,
167
- url: peer ? accountingUrl : undefined,
168
- },
169
- }, flow: team.flow, calls: team.calls,
170
- // Room for the balloons: suggestions, *more* and a notebook.
171
- balloonRoom: 320, connected: peer !== null, label: peer ? `A2A · ${peer.skill.name}` : 'A2A · not connected' }) }), _jsx(Box, { ref: notebookArea, sx: { mb: team.notebook ? 3 : 0 }, "data-team-notebook-placement": "under-graph" }), _jsx(Box, { sx: {
162
+ persona: team.peerPersona,
163
+ onAway: team.setPeerAway,
164
+ history: team.peerHistory,
165
+ connections: ACCOUNTING_CONNECTIONS,
166
+ inspector,
167
+ sandbox: accountingSandbox,
168
+ about: {
169
+ name: ACCOUNTING.name,
170
+ spec: `${ACCOUNTING.id}:${ACCOUNTING.version}`,
171
+ model: ACCOUNTING.model || undefined,
172
+ where: accountingWhere,
173
+ // What its A2A card says, once it is reached.
174
+ description: peer?.card.description || ACCOUNTING.description || undefined,
175
+ skills: peer?.card.skills.map(skill => skill.name),
176
+ protocol: peer ? 'a2a' : undefined,
177
+ url: peer ? accountingUrl : undefined,
178
+ },
179
+ }, flow: team.flow, calls: team.calls,
180
+ // Room for the balloons: suggestions, *more* and a notebook.
181
+ balloonRoom: 320, connected: peer !== null, label: peer ? `A2A · ${peer.skill.name}` : 'A2A · not connected' }) }) }), _jsx(Box, { ref: notebookArea, sx: { mb: team.notebook ? 3 : 0 }, "data-team-notebook-placement": "under-graph" }), _jsx(Box, { sx: {
172
182
  display: 'grid',
173
183
  gridTemplateColumns: ['1fr', '1fr', '1fr 1fr'],
174
184
  gap: 3,
175
185
  mb: 3,
176
- }, "data-team-inspectors": "", children: [SALES, ACCOUNTING].map(member => (_jsxs(Box, { sx: { minWidth: 0 }, "data-team-inspector": member.id, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: [member.emoji, " ", member.name, "\u2019s Agent Inspector"] }), _jsx(AgentInspector, { sink: inspector, agent: member.name, maxHeight: 360, exportName: `agent-inspector-${member.id}`, emptyText: member === SALES
177
- ? 'Its model turns, its call to Accounting and the A2A messages it sends and receives show here.'
178
- : 'What it receives over A2A, its Odoo MCP calls and what it sends back show here.' })] }, member.id))) }), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsxs(Box, { sx: { flex: '1 1 420px', minWidth: 0 }, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", SALES.name] }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, mt: 0 }, children: ["Or choose one of ", SALES.name, "\u2019s suggestions in its balloon."] }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: "Ask for a financial report\u2026", onChange: event => setDraft(event.target.value), onKeyDown: event => {
186
+ }, "data-team-inspectors": "", children: [SALES, ACCOUNTING].map(member => (_jsxs(Box, { sx: { minWidth: 0 }, "data-team-inspector": member.id, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: [member.emoji, " ", member.name, "\u2019s Agent Inspector"] }), _jsx(AgentInspector, { tracer: inspector, agent: member.name, compact: true, maxHeight: 360, exportName: `agent-inspector-${member.id}`, emptyText: member === SALES
187
+ ? 'Its model turns, its call to Accounting and the A2A requests it sends, with what came back, show here.'
188
+ : 'The A2A requests sent to it, its Odoo MCP calls and what it sends back show here.' })] }, member.id))) }), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsxs(Box, { sx: { flex: '1 1 420px', minWidth: 0 }, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", SALES.name] }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, mt: 0 }, children: ["Or choose one of ", SALES.name, "\u2019s suggestions in its balloon."] }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: "Ask for a financial report\u2026", onChange: event => setDraft(event.target.value), onKeyDown: event => {
179
189
  if (event.key === 'Enter' && !event.shiftKey) {
180
190
  event.preventDefault();
181
191
  send(draft);
@@ -15,7 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  * and so are the characters clippy.js publishes, fetched when picked.
16
16
  */
17
17
  import { useEffect, useRef, useState } from 'react';
18
- import { useAgentInspector } from '../components/inspector/agentInspector';
18
+ import { useAgentInspector } from '../components/inspector/agentSpans';
19
19
  import { buildReactorFromPlugins } from '@datalayer/reactor';
20
20
  import { Button, Heading, Text, ToggleSwitch } from '@primer/react';
21
21
  import { Box } from '@datalayer/primer-addons';
@@ -96,7 +96,7 @@ const AssistantExample = () => {
96
96
  // How the balloon shows the conversation (T-23): history, or current.
97
97
  const [balloon, setBalloon] = useState('history');
98
98
  // What the agent does, for the menu's *Inspect the agent…*.
99
- const { sink: inspector } = useAgentInspector();
99
+ const { tracer: inspector } = useAgentInspector();
100
100
  // Pixel, the test sprite, read once through the clippy.js reader.
101
101
  const [pixel, setPixel] = useState();
102
102
  useEffect(() => {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The scenes of the catalogue (agentspecs `scenes`, LOOP A-15), in tabs:
3
+ * each scene's face and name, its `assumes` line under them, its graph and
4
+ * its transcript behind *Graph · Transcript* (`SceneView`), its members
5
+ * standing where its stage directions put them (`positions`), the entry's
6
+ * balloon open first (`opens_first`), and the script's cues as the entry's
7
+ * suggestions. A scene is tried here before it is deployed anywhere.
8
+ *
9
+ * Played against the local servers (`npm run examples`): the entry in the
10
+ * browser asks its model through the examples' inference — a signed-in
11
+ * person's token or a visitor's trial key — and the members on a runtime
12
+ * are served over A2A by `examples/sales-accounting-a2a/serve_scenes.py`,
13
+ * Accounting on :8767 as before and each other member on a runtime of its
14
+ * own from :8768 (`sceneRuntimePorts`), or wherever `VITE_A2A_<MEMBER>_URL`
15
+ * says. A scene whose entry runs on a runtime itself (Month-end close, Crop
16
+ * monitoring) is asked over A2A from the page, as the person
17
+ * (`useA2ATeam`'s `entryPeer`). A member that is not reached is drawn all
18
+ * the same, and the box says what its application needs (`setup`) and how
19
+ * to serve it, rather than failing.
20
+ *
21
+ * Right-click a member for its menu: *Inspect the agent…* opens its Agent
22
+ * Inspector over the page, on the one tracer every member records into.
23
+ */
24
+ import type { JSX } from 'react';
25
+ export declare function ScenesExample(): JSX.Element;
26
+ export default ScenesExample;
@@ -0,0 +1,241 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { Button, Flash, Heading, Text, Textarea, UnderlineNav, } from '@primer/react';
4
+ import { Box } from '@datalayer/primer-addons';
5
+ import { AnonymousKeyExpired } from '@datalayer/core/lib/components/anonymous/AnonymousKeyExpired';
6
+ import { AnonymousKeyTimer } from '@datalayer/core/lib/components/anonymous/AnonymousKeyTimer';
7
+ import { Streamdown } from 'streamdown';
8
+ import { ThemedProvider } from './utils/themedProvider';
9
+ import { useBrowserInference } from '../hooks/useBrowserInference';
10
+ import { useAnonymousSessionStore } from '../runtimes/browser/anonymousToken';
11
+ import { connectA2APeer } from '../runtimes/browser/a2aPeer';
12
+ import { A2ATeamGraph, NOTEBOOK_AND_WORDS, PERSON, SceneView, sceneCuesOf, sceneMemberUrl, sceneRuntimePorts, sceneStageOf, teamConnectionsOf, teamToolClassifier, useA2ATeam, } from '../components/teams';
13
+ import { listSceneSpecs } from '../specs/scenes';
14
+ import { classifyToolCall, traceA2AFetch, useAgentInspector, } from '../components/inspector';
15
+ /** Where the members on a runtime are, by application id: the local ports, unless a variable says. */
16
+ const PORTS = sceneRuntimePorts();
17
+ /**
18
+ * Reach a member on a runtime: read its card at `url`, its traffic recorded
19
+ * as spans for the inspectors and the transcript, as asked by `asker`.
20
+ */
21
+ function useReached(url, asker, name, connections, tracer) {
22
+ const [reached, setReached] = useState({ peer: null, error: null });
23
+ useEffect(() => {
24
+ let current = true;
25
+ setReached({ peer: null, error: null });
26
+ connectA2APeer({
27
+ url,
28
+ fetch: traceA2AFetch(globalThis.fetch.bind(globalThis), {
29
+ tracer,
30
+ asker,
31
+ peer: name,
32
+ classifyTool: teamToolClassifier(connections, classifyToolCall),
33
+ }),
34
+ })
35
+ .then(peer => current && setReached({ peer, error: null }))
36
+ .catch(error => current &&
37
+ setReached({
38
+ peer: null,
39
+ error: error instanceof Error ? error.message : String(error),
40
+ }));
41
+ return () => {
42
+ current = false;
43
+ };
44
+ }, [url, asker, name, connections, tracer]);
45
+ return reached;
46
+ }
47
+ /** The members of a scene that run on a runtime, each with its address and connections. */
48
+ function runtimeMembersOf(stage) {
49
+ const env = import.meta.env;
50
+ return stage.members
51
+ .filter(member => member.runsIn === 'runtime')
52
+ .map(member => ({
53
+ ...member,
54
+ url: sceneMemberUrl(member.app.id, env, PORTS),
55
+ connections: teamConnectionsOf(member.app),
56
+ }));
57
+ }
58
+ /** One member on a runtime: reached, and told to the scene. */
59
+ function ReachMember({ member, asker, tracer, onReached, }) {
60
+ const reached = useReached(member.url, asker, member.app.name, member.connections, tracer);
61
+ useEffect(() => onReached(member.id, reached), [member.id, reached, onReached]);
62
+ return null;
63
+ }
64
+ /** What a member not reached needs: its application's setup, and how to serve it. */
65
+ function memberNeeds(member, error) {
66
+ return [
67
+ `${member.app.name} is not reachable at ${member.url}: ${error}`,
68
+ ...member.setup,
69
+ `Serve it with python examples/sales-accounting-a2a/serve_scenes.py (npm run examples does), or set VITE_A2A_${member.app.id
70
+ .replace(/-/g, '_')
71
+ .toUpperCase()}_URL to where it runs.`,
72
+ ];
73
+ }
74
+ function Scene({ scene }) {
75
+ const stage = useMemo(() => sceneStageOf(scene), [scene]);
76
+ const entry = stage.entry;
77
+ const entryInBrowser = entry.runsIn === 'browser';
78
+ const runtimeMembers = useMemo(() => runtimeMembersOf(stage), [stage]);
79
+ const { inference, needsSignIn, anonymous } = useBrowserInference(entryInBrowser);
80
+ // One tracer of what every member does: each inspector shows its own.
81
+ const { tracer: inspector } = useAgentInspector();
82
+ const [reached, setReached] = useState({});
83
+ const onReached = useMemo(() => (id, found) => setReached(prev => prev[id]?.peer === found.peer && prev[id]?.error === found.error
84
+ ? prev
85
+ : { ...prev, [id]: found }), []);
86
+ const reachedOf = (id) => reached[id] ?? { peer: null, error: null };
87
+ // The entry's peers over A2A, in the cast's order; the entry itself when
88
+ // it runs on a runtime.
89
+ const peerOptions = useMemo(() => stage.peers.map(peer => {
90
+ const member = runtimeMembers.find(one => one.id === peer.id);
91
+ return {
92
+ app: peer.app,
93
+ peer: reached[peer.id]?.peer ?? null,
94
+ connections: member?.connections ?? [],
95
+ ...(stage.opensFirst === peer.id
96
+ ? { greeting: peer.cast.persona.line }
97
+ : {}),
98
+ };
99
+ }), [stage, runtimeMembers, reached]);
100
+ const team = useA2ATeam({
101
+ entry: entry.app,
102
+ peers: peerOptions,
103
+ entryPeer: entryInBrowser ? undefined : reachedOf(entry.id).peer,
104
+ inference,
105
+ accept: NOTEBOOK_AND_WORDS,
106
+ inspector,
107
+ });
108
+ // The script's cues, labelled by the entry's starters (A-13): chips in
109
+ // its balloon while it waits for a question.
110
+ const suggestions = useMemo(() => sceneCuesOf(scene, entry.app.interface.starters ?? []), [scene, entry.app]);
111
+ const [draft, setDraft] = useState('');
112
+ const composer = useRef(null);
113
+ // Under the graph: where a notebook a member gives runs.
114
+ const notebookArea = useRef(null);
115
+ const send = (text) => {
116
+ setDraft('');
117
+ void team.send(text);
118
+ };
119
+ // The members as the transcript names them: the spans' services, and the
120
+ // connections a tool call is of.
121
+ const transcriptMembers = useMemo(() => stage.members.map(member => ({
122
+ id: member.app.id,
123
+ name: member.app.name,
124
+ connections: runtimeMembers.find(one => one.id === member.id)?.connections ?? [],
125
+ })), [stage, runtimeMembers]);
126
+ const whereOf = (member) => member.runsIn === 'browser'
127
+ ? 'in your browser'
128
+ : `on a runtime, ${new URL(runtimeMembers.find(one => one.id === member.id)?.url ?? 'http://runtime').host}`;
129
+ const aboutOf = (member) => {
130
+ const peer = reachedOf(member.id).peer;
131
+ const url = runtimeMembers.find(one => one.id === member.id)?.url;
132
+ return {
133
+ name: member.app.name,
134
+ spec: `${member.app.id}:${member.app.version}`,
135
+ model: member.app.model || undefined,
136
+ where: whereOf(member),
137
+ description: peer?.card.description || member.app.description || undefined,
138
+ skills: peer?.card.skills.map(skill => skill.name),
139
+ protocol: peer ? 'a2a' : undefined,
140
+ url: peer ? url : undefined,
141
+ };
142
+ };
143
+ const notReached = runtimeMembers.filter(member => reachedOf(member.id).error);
144
+ const notebookGiver = stage.peers[0] ?? entry;
145
+ return (_jsxs(Box, { "data-scene-example": scene.id, children: [_jsx(Text, { as: "p", sx: { color: 'fg.muted', mt: 0 }, "data-scene-assumes": "", children: stage.assumes }), anonymous.status === 'active' && anonymous.expiresAt && (_jsx(Box, { sx: { mb: 3 }, children: _jsx(AnonymousKeyTimer, { expiresAt: anonymous.expiresAt, grantedMs: anonymous.grantedMs, label: `${entry.app.name}'s trial key`, onExpire: () => useAnonymousSessionStore.getState().expire() }) })), anonymous.status === 'expired' && (_jsx(Box, { sx: { mb: 3 }, children: _jsx(AnonymousKeyExpired, { agentName: entry.app.name, onSignedIn: () => useAnonymousSessionStore.getState().clear() }) })), entryInBrowser && needsSignIn && (_jsxs(Flash, { variant: "warning", sx: { mb: 3 }, children: [entry.app.name, " asks its model through the Datalayer inference service: sign in, or wait for a visitor\u2019s trial key."] })), notReached.map(member => (_jsx(Flash, { variant: "danger", sx: { mb: 3 }, "data-scene-needs": member.app.id, children: memberNeeds(member, reachedOf(member.id).error ?? '').map(line => (_jsx(Text, { as: "p", sx: { m: 0 }, children: line }, line))) }, member.id))), runtimeMembers.map(member => (_jsx(ReachMember, { member: member,
146
+ // Its requests are the entry's, or the person's own when the
147
+ // page asks it itself.
148
+ asker: member.entry ? PERSON : entry.app.name, tracer: inspector, onReached: onReached }, member.id))), _jsx(Box, { sx: {
149
+ border: '1px solid',
150
+ borderColor: 'border.default',
151
+ borderRadius: 2,
152
+ bg: 'canvas.subtle',
153
+ px: 2,
154
+ pt: 2,
155
+ mb: 3,
156
+ }, children: _jsx(SceneView, { members: transcriptMembers, tracer: inspector, emptyText: `Nothing said yet: ask ${entry.app.name} with one of the suggestions.`, children: _jsx(A2ATeamGraph, { entry: {
157
+ id: entry.app.id,
158
+ name: entry.app.name,
159
+ emoji: entry.cast.persona.face || entry.app.emoji,
160
+ character: entry.app.interface.assistant ?? 'paperclip',
161
+ where: whereOf(entry),
162
+ persona: team.entryPersona,
163
+ onToggle: () => composer.current?.focus(),
164
+ conversationLabel: `Ask ${entry.app.name}`,
165
+ onAway: team.setEntryAway,
166
+ history: team.entryHistory,
167
+ connections: runtimeMembers.find(one => one.id === entry.id)
168
+ ?.connections,
169
+ expandTarget: notebookArea,
170
+ notebookTitle: `${notebookGiver.app.name}’s notebook`,
171
+ suggestions: team.ready && !team.busy ? suggestions : undefined,
172
+ onSuggestion: suggestion => send(suggestion.prompt),
173
+ inspector,
174
+ onStop: team.busy ? team.stop : undefined,
175
+ about: aboutOf(entry),
176
+ }, peers: stage.peers.map(peer => {
177
+ const kept = team.peers.find(one => one.id === peer.app.id);
178
+ return {
179
+ id: peer.app.id,
180
+ name: peer.app.name,
181
+ emoji: reachedOf(peer.id).peer?.face?.emoji ||
182
+ peer.cast.persona.face ||
183
+ peer.app.emoji,
184
+ character: peer.app.interface.assistant ?? 'wizard',
185
+ where: whereOf(peer),
186
+ persona: kept?.persona ?? team.peerPersona,
187
+ onAway: kept?.setAway,
188
+ history: kept?.history,
189
+ connections: runtimeMembers.find(one => one.id === peer.id)
190
+ ?.connections,
191
+ inspector,
192
+ about: aboutOf(peer),
193
+ };
194
+ }), flows: team.flows, calls: team.calls, links: stage.links.map(link => ({
195
+ from: stage.members.find(m => m.id === link.from)?.app.id ??
196
+ link.from,
197
+ to: stage.members.find(m => m.id === link.to)?.app.id ?? link.to,
198
+ })), positions: Object.fromEntries(stage.members
199
+ .filter(member => stage.positions[member.id])
200
+ .map(member => [member.app.id, stage.positions[member.id]])), connected: Object.fromEntries(stage.peers.map(peer => [
201
+ peer.app.id,
202
+ reachedOf(peer.id).peer !== null,
203
+ ])), labels: Object.fromEntries(stage.peers.map(peer => {
204
+ const found = reachedOf(peer.id).peer;
205
+ return [
206
+ peer.app.id,
207
+ found ? `A2A · ${found.skill.name}` : 'A2A · not connected',
208
+ ];
209
+ })),
210
+ // Room for the balloons: suggestions, *more* and a notebook.
211
+ balloonRoom: 320 }) }) }), _jsx(Box, { ref: notebookArea, sx: { mb: team.notebook ? 3 : 0 }, "data-team-notebook-placement": "under-graph" }), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsxs(Box, { sx: { flex: '1 1 420px', minWidth: 0 }, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", entry.app.name] }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, mt: 0 }, children: "Or choose one of its suggestions in its balloon: the script\u2019s cues." }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: suggestions[0]?.prompt ?? 'Ask…', onChange: event => setDraft(event.target.value), onKeyDown: event => {
212
+ if (event.key === 'Enter' && !event.shiftKey) {
213
+ event.preventDefault();
214
+ send(draft);
215
+ }
216
+ } }), _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { variant: "primary", disabled: !team.ready || team.busy || !draft.trim(), onClick: () => send(draft), children: "Ask" }), team.busy && _jsx(Button, { onClick: team.stop, children: "Stop" })] }), _jsx(Box, { sx: { mt: 3 }, "data-team-conversation": "", children: team.turns.map((turn, index) => (_jsxs(Box, { sx: {
217
+ p: 2,
218
+ mb: 2,
219
+ borderRadius: 2,
220
+ bg: turn.role === 'user' ? 'accent.subtle' : 'canvas.subtle',
221
+ }, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', display: 'block' }, children: turn.role === 'user' ? PERSON : entry.app.name }), _jsx(Streamdown, { children: turn.text })] }, index))) })] }), _jsxs(Box, { sx: { flex: '1 1 360px', minWidth: 0 }, children: [!team.notebook && stage.peers.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: "The report" }), _jsx(Box, { sx: {
222
+ p: 3,
223
+ border: '1px solid',
224
+ borderColor: 'border.default',
225
+ borderRadius: 2,
226
+ minHeight: 120,
227
+ }, "data-team-report": "", children: team.report ? (_jsx(Streamdown, { children: team.report })) : (_jsx(Text, { sx: { color: 'fg.muted' }, children: "What the last member asked returns appears here, as it returned it, or as a notebook to run when it gives one." })) })] })), scene.setup.length > 0 && (_jsxs(Box, { sx: { mt: 3 }, "data-scene-setup": "", children: [_jsx(Heading, { as: "h3", sx: { fontSize: 1, mb: 1 }, children: "What this scene needs" }), scene.setup.map(line => (_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0 }, children: line }, line)))] }))] })] })] }));
228
+ }
229
+ function Scenes() {
230
+ const scenes = useMemo(() => listSceneSpecs(), []);
231
+ const [selected, setSelected] = useState(scenes[0]?.id ?? '');
232
+ const scene = scenes.find(one => one.id === selected) ?? scenes[0];
233
+ return (_jsxs(Box, { sx: { p: 4, maxWidth: 1080, mx: 'auto' }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 4, mb: 1 }, children: "Scenes" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mt: 0 }, children: ["The catalogue\u2019s scenes, played against the local servers: each member on a runtime is served over A2A by ", _jsx("code", { children: "serve_scenes.py" }), "; the entry in your browser asks on your token or a visitor\u2019s trial key."] }), _jsx(UnderlineNav, { "aria-label": "Scenes", sx: { mb: 3 }, children: scenes.map(one => (_jsxs(UnderlineNav.Item, { "aria-current": one.id === scene?.id ? 'page' : undefined, onSelect: event => {
234
+ event.preventDefault();
235
+ setSelected(one.id);
236
+ }, "data-scene-tab": one.id, children: [one.emoji, " ", one.name] }, one.id))) }), scene && _jsx(Scene, { scene: scene }, scene.id)] }));
237
+ }
238
+ export function ScenesExample() {
239
+ return (_jsx(ThemedProvider, { children: _jsx(Scenes, {}) }));
240
+ }
241
+ export default ScenesExample;
@@ -112,6 +112,7 @@ export const EXAMPLE_ENTRIES = [
112
112
  // for one sandbox — and a second one that would not agree with the first.
113
113
  ['example', 'loop', 'workspace', 'sandbox', 'owns-sandbox-control']),
114
114
  makeEntry('LoopAppComputerExample', () => import('./LoopAppComputerExample'), 'An application beside its computer: what its agent ran on its sandbox, its files, and Take over and Hand back (LOOP R-23).', ['example', 'loop', 'app', 'computer', 'sandbox', 'owns-sandbox-control']),
115
+ makeEntry('ScenesExample', () => import('./ScenesExample'), 'The scenes of the catalogue in tabs — Sales & Accounting, Month-end close, Crop monitoring, Disaster assessment — each its graph and transcript, its cues as suggestions, played against the local servers (LOOP A-15).', ['example', 'loop', 'scenes', 'a2a', 'agentspecs', 'team']),
115
116
  makeEntry('LoopShellExample', () => import('./LoopShellExample'), 'The Loop shell at its most naked: a blank canvas, a floating draggable prompt, and an editor selector in the corner — none, notebook or document.',
116
117
  // `owns-sandbox-control`: the shell is pinned to the browser sandbox, so
117
118
  // the page's selector would offer three targets it cannot move to.
@@ -23,7 +23,10 @@ export const EXAMPLE_GROUP_ORDER = [
23
23
  'CopilotKit',
24
24
  ];
25
25
  export const getExampleGroup = (id) => {
26
+ // The library of specs, the Loop shells, and the scenes of the catalogue
27
+ // (LOOP A-15): the applications and what is staged with them.
26
28
  if (id === 'AgentspecsExample' ||
29
+ id === 'ScenesExample' ||
27
30
  id.startsWith('Loop') ||
28
31
  id === 'DecksAgent') {
29
32
  return 'Apps';
@@ -27,6 +27,7 @@ export { default as AgentCodemodeExample } from './AgentCodemodeExample';
27
27
  export { default as AgentCodeSandboxesExample } from './AgentCodeSandboxesExample';
28
28
  export { default as AgentDecideExample } from './AgentDecideExample';
29
29
  export { default as AgentA2ATeamExample } from './AgentA2ATeamExample';
30
+ export { default as ScenesExample } from './ScenesExample';
30
31
  export { default as AgentEvalsExample } from './AgentEvalsExample';
31
32
  export { default as AgentGuardrailsExample } from './AgentGuardrailsExample';
32
33
  export { default as AgentHooksExample } from './AgentHooksExample';
@@ -33,6 +33,7 @@ export { default as AgentCodemodeExample } from './AgentCodemodeExample';
33
33
  export { default as AgentCodeSandboxesExample } from './AgentCodeSandboxesExample';
34
34
  export { default as AgentDecideExample } from './AgentDecideExample';
35
35
  export { default as AgentA2ATeamExample } from './AgentA2ATeamExample';
36
+ export { default as ScenesExample } from './ScenesExample';
36
37
  export { default as AgentEvalsExample } from './AgentEvalsExample';
37
38
  export { default as AgentGuardrailsExample } from './AgentGuardrailsExample';
38
39
  export { default as AgentHooksExample } from './AgentHooksExample';
@@ -62,6 +62,9 @@ const ANONYMOUS_EXAMPLES = new Set([
62
62
  // The Sales and Accounting team: Sales in the page on a visitor's trial
63
63
  // key, Accounting on the local server, which answers the machine itself.
64
64
  'AgentA2ATeamExample',
65
+ // The scenes of the catalogue: each entry in the page on a visitor's trial
66
+ // key, the members on a runtime on local servers that answer the machine.
67
+ 'ScenesExample',
65
68
  // Decide, on the local agent-runtimes server as above: Jev is asked
66
69
  // through the server's ai-inference, with the server's own key.
67
70
  'AgentDecideExample',
@@ -1045,6 +1048,7 @@ const ExampleAppThemed = ({ selectedExample, isChangingExample, error, ExampleCo
1045
1048
  'LoopStrategyExample',
1046
1049
  'LoopAppComputerExample',
1047
1050
  'AgentspecsExample',
1051
+ 'ScenesExample',
1048
1052
  ];
1049
1053
  const loopOrder = (id) => {
1050
1054
  const index = LOOP_ORDER.indexOf(id);
@@ -23,7 +23,8 @@
23
23
  * conversation keeps its profile: the picker says so and picks no other.
24
24
  *
25
25
  * Given the application as the person reads it (`translatedAppspec`, LOOP
26
- * P-26), its words are theirs; the composer's own, `language`'s.
26
+ * P-26), its words are theirs; the composer's own, the chat's language's
27
+ * (`useChatLanguage`).
27
28
  *
28
29
  * @module loop/apps/AppComposer
29
30
  */
@@ -28,7 +28,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
28
28
  * conversation keeps its profile: the picker says so and picks no other.
29
29
  *
30
30
  * Given the application as the person reads it (`translatedAppspec`, LOOP
31
- * P-26), its words are theirs; the composer's own, `language`'s.
31
+ * P-26), its words are theirs; the composer's own, the chat's language's
32
+ * (`useChatLanguage`).
32
33
  *
33
34
  * @module loop/apps/AppComposer
34
35
  */
@@ -40,8 +41,8 @@ import { useSignalValue } from '@datalayer/reactor/react';
40
41
  import { readFile, } from '../../components/a2ui/datalayer/FileUpload';
41
42
  import { LoopChatExtras, LoopCommand, LoopRunProps, LoopSlots, } from '../core';
42
43
  import { commandPrompt, modeChoice, modeDefaults, profileChoice, startersFor, } from './composer';
43
- import { DEFAULT_LANGUAGE } from './appspec';
44
44
  import { interfaceWords } from './language';
45
+ import { useChatLanguage } from '../../chat/ChatLanguage';
45
46
  import { uploadsAccept, uploadsInWords, uploadsRefusal } from './uploads';
46
47
  /**
47
48
  * Files chosen in the composer, held for the next message: the refusal the
@@ -89,7 +90,6 @@ export const needsAppComposer = (app) => (app.interface.commands ?? []).length >
89
90
  * Nothing for an application with none: its plugin contributes nothing.
90
91
  */
91
92
  export function defineAppComposerPlugin(app) {
92
- const words = interfaceWords(app.interface.language ?? DEFAULT_LANGUAGE);
93
93
  const profiles = app.interface.profiles ?? [];
94
94
  /** The profile picked, and whether the conversation has started with it. */
95
95
  const profile = signal(profileChoice(app)?.id);
@@ -98,6 +98,8 @@ export function defineAppComposerPlugin(app) {
98
98
  openers: startersAsOpeners(startersFor(app, profile.value)),
99
99
  }));
100
100
  function ProfilePicker() {
101
+ // In the chat's language, as the chat's own words (P-26).
102
+ const words = interfaceWords(useChatLanguage());
101
103
  const now = profileChoice(app, useSignalValue(profile));
102
104
  const kept = useSignalValue(started);
103
105
  if (!now) {
@@ -131,6 +133,7 @@ export function defineAppComposerPlugin(app) {
131
133
  /** The files held for the next message, and the last refusal (P-21). */
132
134
  const held = signal({ files: [], refused: null });
133
135
  function Attach() {
136
+ const words = interfaceWords(useChatLanguage());
134
137
  const now = useSignalValue(held);
135
138
  const input = useRef(null);
136
139
  if (!uploads) {
@@ -176,7 +176,8 @@ export type AppPreset = {
176
176
  * whose contributions are the blocks it may draw (R-01b); a thumb and a
177
177
  * comment on each answer when its record keeps feedback (V-18); its
178
178
  * commands in the composer's `/` menu and its modes beside it (P-19); the
179
- * side panel and the pages its code opens (`app-elements`, P-18); what it
179
+ * side panel and the pages its code opens (`app-elements`, P-18); the
180
+ * components its developer wrote (`app-components`, P-17); what it
180
181
  * keeps and for how long, under its prompt before the first message, when
181
182
  * `kept` says it (R-31); with
182
183
  * `sidebar`, its rules and approvals card, its activity feed and its