@datalayer/agent-runtimes 1.3.28 → 1.3.30

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 (99) hide show
  1. package/lib/AgentNode.js +14 -33
  2. package/lib/chat/ChatFloating.d.ts +12 -2
  3. package/lib/chat/ChatFloating.js +196 -25
  4. package/lib/chat/assistant/AssistantStage.d.ts +70 -0
  5. package/lib/chat/assistant/AssistantStage.js +192 -0
  6. package/lib/chat/assistant/SpeechBalloon.d.ts +26 -0
  7. package/lib/chat/assistant/SpeechBalloon.js +46 -0
  8. package/lib/chat/assistant/SpriteCharacter.d.ts +26 -0
  9. package/lib/chat/assistant/SpriteCharacter.js +73 -0
  10. package/lib/chat/assistant/characters.d.ts +30 -0
  11. package/lib/chat/assistant/characters.js +41 -0
  12. package/lib/chat/assistant/formats/acs.d.ts +88 -0
  13. package/lib/chat/assistant/formats/acs.js +493 -0
  14. package/lib/chat/assistant/formats/agentCompression.d.ts +5 -0
  15. package/lib/chat/assistant/formats/agentCompression.js +92 -0
  16. package/lib/chat/assistant/formats/blobs.d.ts +6 -0
  17. package/lib/chat/assistant/formats/blobs.js +52 -0
  18. package/lib/chat/assistant/formats/clippy.d.ts +21 -0
  19. package/lib/chat/assistant/formats/clippy.js +167 -0
  20. package/lib/chat/assistant/formats/index.d.ts +13 -0
  21. package/lib/chat/assistant/formats/index.js +15 -0
  22. package/lib/chat/assistant/formats/objectLiteral.d.ts +23 -0
  23. package/lib/chat/assistant/formats/objectLiteral.js +233 -0
  24. package/lib/chat/assistant/formats/stateAnimations.d.ts +16 -0
  25. package/lib/chat/assistant/formats/stateAnimations.js +115 -0
  26. package/lib/chat/assistant/formats/types.d.ts +56 -0
  27. package/lib/chat/assistant/formats/types.js +11 -0
  28. package/lib/chat/assistant/state.d.ts +62 -0
  29. package/lib/chat/assistant/state.js +116 -0
  30. package/lib/chat/base/ChatBase.js +41 -32
  31. package/lib/chat/base/modelChoice.d.ts +13 -0
  32. package/lib/chat/base/modelChoice.js +13 -0
  33. package/lib/chat/header/ChatViewModeToggle.js +6 -3
  34. package/lib/{loop/plugins/chat → chat/presence}/Presence.js +1 -1
  35. package/lib/{loop/plugins/chat → chat/presence}/presenceStatus.d.ts +7 -1
  36. package/lib/{loop/plugins/chat → chat/presence}/presenceStatus.js +18 -0
  37. package/lib/chat/viewModes.d.ts +6 -0
  38. package/lib/chat/viewModes.js +10 -1
  39. package/lib/examples/ChatAssistantExample.d.ts +8 -0
  40. package/lib/examples/ChatAssistantExample.js +59 -0
  41. package/lib/examples/example-selector.js +1 -0
  42. package/lib/examples/main.js +3 -0
  43. package/lib/loop/apps/AppRenderer.d.ts +34 -2
  44. package/lib/loop/apps/AppRenderer.js +96 -29
  45. package/lib/loop/apps/agent.d.ts +15 -0
  46. package/lib/loop/apps/agent.js +42 -0
  47. package/lib/loop/apps/appspec.d.ts +5 -1
  48. package/lib/loop/apps/appspec.js +21 -1
  49. package/lib/loop/apps/checks.js +3 -1
  50. package/lib/loop/apps/index.d.ts +1 -0
  51. package/lib/loop/apps/index.js +1 -0
  52. package/lib/loop/core/index.d.ts +23 -0
  53. package/lib/loop/core/index.js +1 -0
  54. package/lib/loop/embed/AppEmbed.d.ts +42 -0
  55. package/lib/loop/embed/AppEmbed.js +205 -0
  56. package/lib/loop/embed/datalayer-app.d.ts +1 -0
  57. package/lib/loop/embed/datalayer-app.js +16 -0
  58. package/lib/loop/embed/element.d.ts +57 -0
  59. package/lib/loop/embed/element.js +405 -0
  60. package/lib/loop/embed/embedConfig.d.ts +120 -0
  61. package/lib/loop/embed/embedConfig.js +148 -0
  62. package/lib/loop/embed/embedTheme.d.ts +24 -0
  63. package/lib/loop/embed/embedTheme.js +83 -0
  64. package/lib/loop/index.d.ts +6 -2
  65. package/lib/loop/index.js +8 -2
  66. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +21 -3
  67. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +34 -6
  68. package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
  69. package/lib/loop/plugins/agent-capacity/index.js +3 -1
  70. package/lib/loop/plugins/agents/switchable.d.ts +7 -0
  71. package/lib/loop/plugins/agents/switchable.js +91 -88
  72. package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
  73. package/lib/loop/plugins/app-page/AppPage.js +53 -0
  74. package/lib/loop/plugins/app-page/appPage.d.ts +180 -0
  75. package/lib/loop/plugins/app-page/appPage.js +443 -0
  76. package/lib/loop/plugins/app-page/index.d.ts +8 -0
  77. package/lib/loop/plugins/app-page/index.js +34 -0
  78. package/lib/loop/plugins/assistant-characters/index.d.ts +30 -0
  79. package/lib/loop/plugins/assistant-characters/index.js +41 -0
  80. package/lib/loop/plugins/chat/ChatView.js +50 -39
  81. package/lib/loop/plugins/chat/index.d.ts +1 -1
  82. package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
  83. package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
  84. package/lib/loop/plugins/page-layout/index.d.ts +13 -0
  85. package/lib/loop/plugins/page-layout/index.js +15 -2
  86. package/lib/loop/presets.d.ts +12 -0
  87. package/lib/loop/presets.js +5 -3
  88. package/lib/specs/actions.js +14 -0
  89. package/lib/specs/agents/agents.js +4 -0
  90. package/lib/specs/apps.d.ts +13 -1
  91. package/lib/specs/apps.js +2715 -373
  92. package/lib/specs/appspecSchema.js +21 -3
  93. package/lib/types/agentspecs.d.ts +21 -1
  94. package/lib/types/chat.d.ts +1 -1
  95. package/lib/types/config.d.ts +12 -0
  96. package/package.json +2 -1
  97. package/scripts/codegen/generate_agents.py +50 -2
  98. package/scripts/codegen/generate_apps.py +36 -6
  99. /package/lib/{loop/plugins/chat → chat/presence}/Presence.d.ts +0 -0
@@ -8,13 +8,13 @@ import { computed, signal } from '@datalayer/reactor';
8
8
  const NO_STANCE = signal(undefined);
9
9
  import { useContributions, useSignalValue, useGate, useReactorPlatform, ReactorLazy, ReactorSlot, useSlotComponents, } from '@datalayer/reactor/react';
10
10
  import { ChatBase } from '../../../chat/base/ChatBase';
11
- import { readServerCatalogue } from '../../../chat/base/modelChoice';
11
+ import { offeredModels as offeredModelsFor, readServerCatalogue, } from '../../../chat/base/modelChoice';
12
12
  import { SUGGESTION_CHIP_WIDTH } from '../../../chat/display/EmptyState';
13
13
  import { AnonymousKeyExpired } from '@datalayer/core/lib/components/anonymous/AnonymousKeyExpired';
14
14
  import { useAnonymousSessionStore } from '../../../runtimes/browser/anonymousToken';
15
15
  import { browserProtocolConfig } from '../../../runtimes/browser';
16
16
  import { useBrowserInference } from '../../../hooks/useBrowserInference';
17
- import { getAgentspecs } from '../../../specs/agents';
17
+ import { agentInstructions, inPageAgentRefusal, resolveAgentspec, } from '../../apps/agent';
18
18
  import { targetRunsAgentInPage, } from '../agents/switchable';
19
19
  import { agentIcon } from '../agents/agentIcons';
20
20
  import { specSubagents } from '../agents/specSubagents';
@@ -43,8 +43,8 @@ import { turnWritersOf } from './turnState';
43
43
  import { isInactiveSurfaceContribution, orderToolContributions, toolsForChatView, } from './chatViewTools';
44
44
  import { useAgentCommandTools } from '../../../tools/adapters/commands';
45
45
  import { NONE_EDITOR, seedEditorChoice, setEditorOptions, } from '../shell/editorChoice';
46
- import { PresenceFace, PresenceLine } from './Presence';
47
- import { presenceState } from './presenceStatus';
46
+ import { PresenceFace, PresenceLine } from '../../../chat/presence/Presence';
47
+ import { presenceState, presenceToolOf, } from '../../../chat/presence/presenceStatus';
48
48
  /** No editor: the conversation on its own. */
49
49
  const NO_SURFACE = '';
50
50
  /**
@@ -212,18 +212,7 @@ export default function ChatView({ workspace }) {
212
212
  */
213
213
  const handleDisplayItemsChange = useCallback((items) => {
214
214
  // The newest tool call, for an application's line of status (T-08).
215
- let newest;
216
- for (let index = items.length - 1; index >= 0; index -= 1) {
217
- const item = items[index];
218
- if (typeof item.toolName === 'string' && item.toolCallId) {
219
- newest = item;
220
- break;
221
- }
222
- }
223
- const open = !!newest && newest.status !== 'complete' && newest.status !== 'error';
224
- const pendingApproval = open &&
225
- newest?.result
226
- ?.pending_approval === true;
215
+ const { open, pendingApproval } = presenceToolOf(items);
227
216
  setPresenceTool(previous => previous.open === open && previous.pendingApproval === pendingApproval
228
217
  ? previous
229
218
  : { open, pendingApproval });
@@ -666,9 +655,23 @@ export default function ChatView({ workspace }) {
666
655
  * icon for want of this lookup — it used the document tools and called
667
656
  * nobody.
668
657
  */
669
- const blueprintSpecId = useContributions(LoopAgentBlueprint)[0]?.value.specId;
670
- const spec = getAgentspecs(member?.specId ?? agentId) ??
671
- (blueprintSpecId ? getAgentspecs(blueprintSpecId) : undefined);
658
+ const blueprint = useContributions(LoopAgentBlueprint)[0]?.value;
659
+ const blueprintSpecId = blueprint?.specId;
660
+ /*
661
+ * An agent of the catalogue or a Cog's, as a runtime resolves it: an
662
+ * application on a Cog looked up among the agentspecs alone was turned in
663
+ * the page with no instructions at all (LOOP H-01).
664
+ */
665
+ const spec = resolveAgentspec(member?.specId ?? agentId) ??
666
+ (blueprintSpecId ? resolveAgentspec(blueprintSpecId) : undefined);
667
+ /*
668
+ * The blueprint's words and model — an application's — on top of its
669
+ * spec's, for the agent it contributes; never for a team's member.
670
+ */
671
+ const blueprintTurn = member ? undefined : blueprint;
672
+ // Turned in the page, an agent that resolves to nothing is refused rather
673
+ // than run bare.
674
+ const inPageRefusal = inPage && !member ? inPageAgentRefusal(blueprintSpecId, spec) : undefined;
672
675
  /*
673
676
  * The team's name for a member first, then the spec's own.
674
677
  *
@@ -956,12 +959,12 @@ export default function ChatView({ workspace }) {
956
959
  * for something that was never theirs.
957
960
  */
958
961
  const expiredKeyIsTemporary = !signedInUser;
959
- const chatDisabled = gateBlocked || keyExpired;
962
+ const chatDisabled = gateBlocked || keyExpired || Boolean(inPageRefusal);
960
963
  const disabledReason = keyExpired
961
964
  ? expiredKeyIsTemporary
962
965
  ? 'This demo runs on a shared key, and its time is up. Sign in to keep going.'
963
966
  : 'Your key has expired. Sign in to keep going.'
964
- : gateReason;
967
+ : (inPageRefusal ?? gateReason);
965
968
  /*
966
969
  * The prompt's text, held here rather than inside `InputPrompt`.
967
970
  *
@@ -992,7 +995,7 @@ export default function ChatView({ workspace }) {
992
995
  * the member being addressed changes.
993
996
  */
994
997
  const [pickedModel, setPickedModel] = useState();
995
- const activeModel = pickedModel ?? spec?.model ?? workspace.model ?? '';
998
+ const activeModel = pickedModel ?? blueprintTurn?.model ?? spec?.model ?? workspace.model ?? '';
996
999
  /*
997
1000
  * The team, in the shape the footer asks for.
998
1001
  *
@@ -1082,7 +1085,7 @@ export default function ChatView({ workspace }) {
1082
1085
  const protocol = useMemo(() => inPage
1083
1086
  ? browserProtocolConfig({
1084
1087
  agentId: member?.specId ?? agentId,
1085
- instructions: spec?.systemPrompt,
1088
+ instructions: agentInstructions(spec, blueprintTurn?.instructions),
1086
1089
  model: activeModel || undefined,
1087
1090
  inference,
1088
1091
  // Who this member may hand work to, and what they are told. Both
@@ -1122,6 +1125,7 @@ export default function ChatView({ workspace }) {
1122
1125
  agentTools,
1123
1126
  team,
1124
1127
  spec,
1128
+ blueprintTurn,
1125
1129
  activeModel,
1126
1130
  agentServerUrl,
1127
1131
  ]);
@@ -1139,7 +1143,8 @@ export default function ChatView({ workspace }) {
1139
1143
  const configQuery = useConfig(!inPage, protocol.type === 'ag-ui' ? protocol.configEndpoint : undefined, undefined, agentId);
1140
1144
  const skillsQuery = useSkills(true);
1141
1145
  const skillActions = useSkillActions(agentId);
1142
- const [catalogModels, setCatalogModels] = useState([]);
1146
+ // `undefined` until the runtime answers: its answer is then the list.
1147
+ const [catalogModels, setCatalogModels] = useState(undefined);
1143
1148
  useEffect(() => {
1144
1149
  /*
1145
1150
  * Only where there is a server to ask.
@@ -1154,23 +1159,31 @@ export default function ChatView({ workspace }) {
1154
1159
  return undefined;
1155
1160
  }
1156
1161
  let cancelled = false;
1157
- void fetch(`${agentServerUrl}/api/v1/configure/models`)
1158
- .then(response => (response.ok ? response.json() : { models: [] }))
1162
+ // The agent's own models (its model and model_additionals), each saying
1163
+ // whether its inference serves it.
1164
+ void fetch(`${agentServerUrl}/api/v1/configure/models?agent_id=${encodeURIComponent(agentId)}`)
1165
+ .then(response => (response.ok ? response.json() : null))
1159
1166
  .then(payload => {
1160
1167
  if (!cancelled) {
1161
1168
  // In the chat's shape: the route says `available`, the menu and
1162
1169
  // the opening pick read `isAvailable`, and an unmapped row passes
1163
1170
  // for usable. See `readServerCatalogue`.
1164
- setCatalogModels(readServerCatalogue(payload));
1171
+ // A runtime that did not answer offers nothing: the agentspecs
1172
+ // catalogue is not its answer.
1173
+ setCatalogModels(payload ? readServerCatalogue(payload) : []);
1165
1174
  }
1166
1175
  })
1167
1176
  // No catalogue is not an error worth a banner: the menu simply has
1168
1177
  // nothing to offer, and the agent answers on whatever it was given.
1169
- .catch(() => undefined);
1178
+ .catch(() => {
1179
+ if (!cancelled) {
1180
+ setCatalogModels([]);
1181
+ }
1182
+ });
1170
1183
  return () => {
1171
1184
  cancelled = true;
1172
1185
  };
1173
- }, [agentServerUrl, inPage]);
1186
+ }, [agentServerUrl, agentId, inPage]);
1174
1187
  /*
1175
1188
  * What to offer when the server has no catalogue of its own.
1176
1189
  *
@@ -1179,16 +1192,14 @@ export default function ChatView({ workspace }) {
1179
1192
  * with nothing in it is a control that opens onto an empty list — so the
1180
1193
  * agentspecs catalogue stands in, filtered to the models marked available.
1181
1194
  */
1182
- const offeredModels = useMemo(() => catalogModels.length > 0
1183
- ? catalogModels
1184
- : Object.values(AI_MODEL_CATALOGUE)
1185
- // A typed-judgment model answers typed questions, not a chat.
1186
- .filter(model => model.available && isChatModel(model))
1187
- .map(model => ({
1188
- id: model.id,
1189
- name: model.name,
1190
- provider: model.provider,
1191
- })), [catalogModels]);
1195
+ const offeredModels = useMemo(() => offeredModelsFor(catalogModels, () => Object.values(AI_MODEL_CATALOGUE)
1196
+ // A typed-judgment model answers typed questions, not a chat.
1197
+ .filter(model => model.available && isChatModel(model))
1198
+ .map(model => ({
1199
+ id: model.id,
1200
+ name: model.name,
1201
+ provider: model.provider,
1202
+ }))), [catalogModels]);
1192
1203
  /*
1193
1204
  * The tools the agent can call, from both sides.
1194
1205
  *
@@ -1,4 +1,4 @@
1
- import type { PresenceState } from './presenceStatus';
1
+ import type { PresenceState } from '../../../chat/presence/presenceStatus';
2
2
  /** What a host may set on the chat. */
3
3
  /** Who is answering, as the person meets them. */
4
4
  export interface ChatPresence {
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The split: the conversation and the work side by side, one hairline
3
+ * between them that a person drags (LOOP T-07, an Appspec's `layout: split`).
4
+ *
5
+ * The page layout's other arrangement. Where `page` puts the work on a sheet
6
+ * and the conversation in a panel that opens when wanted, `split` keeps both
7
+ * on screen: the conversation on the left — its transcript with the composer
8
+ * under it — and the work on the right, the way the reference screens show a
9
+ * mail, a document or the agent's computer beside the chat.
10
+ *
11
+ * The hairline is a separator a person can move with the pointer or the
12
+ * keyboard (arrows by a step, Home and End to the bounds); the share it
13
+ * leaves the conversation is kept while the workspace is mounted.
14
+ *
15
+ * @module loop/plugins/page-layout/SplitLayout
16
+ */
17
+ import type { JSX } from 'react';
18
+ import type { ChatLayoutParts } from '../../core';
19
+ /** The conversation's share of the width when the split opens. */
20
+ export declare const SPLIT_DEFAULT_SHARE = 0.4;
21
+ /** The narrowest and widest the conversation may be made. */
22
+ export declare const SPLIT_MIN_SHARE = 0.2;
23
+ export declare const SPLIT_MAX_SHARE = 0.8;
24
+ /** How far an arrow key moves the hairline. */
25
+ export declare const SPLIT_KEY_STEP = 0.05;
26
+ /** A share, held within the bounds. */
27
+ export declare function clampSplitShare(share: number): number;
28
+ /** Where a key moves the hairline, or `null` for a key it ignores. */
29
+ export declare function splitShareForKey(share: number, key: string): number | null;
30
+ export declare function SplitLayout({ editors, hasEditor, transcript, prompt, chips, picker, transient, }: ChatLayoutParts): JSX.Element;
31
+ export default SplitLayout;
@@ -0,0 +1,129 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useRef, useState } from 'react';
3
+ import { Box } from '@datalayer/primer-addons';
4
+ /** The conversation's share of the width when the split opens. */
5
+ export const SPLIT_DEFAULT_SHARE = 0.4;
6
+ /** The narrowest and widest the conversation may be made. */
7
+ export const SPLIT_MIN_SHARE = 0.2;
8
+ export const SPLIT_MAX_SHARE = 0.8;
9
+ /** How far an arrow key moves the hairline. */
10
+ export const SPLIT_KEY_STEP = 0.05;
11
+ /** A share, held within the bounds. */
12
+ export function clampSplitShare(share) {
13
+ if (!Number.isFinite(share)) {
14
+ return SPLIT_DEFAULT_SHARE;
15
+ }
16
+ return Math.min(SPLIT_MAX_SHARE, Math.max(SPLIT_MIN_SHARE, share));
17
+ }
18
+ /** Where a key moves the hairline, or `null` for a key it ignores. */
19
+ export function splitShareForKey(share, key) {
20
+ switch (key) {
21
+ case 'ArrowLeft':
22
+ return clampSplitShare(share - SPLIT_KEY_STEP);
23
+ case 'ArrowRight':
24
+ return clampSplitShare(share + SPLIT_KEY_STEP);
25
+ case 'Home':
26
+ return SPLIT_MIN_SHARE;
27
+ case 'End':
28
+ return SPLIT_MAX_SHARE;
29
+ default:
30
+ return null;
31
+ }
32
+ }
33
+ export function SplitLayout({ editors, hasEditor, transcript, prompt, chips, picker, transient, }) {
34
+ const [share, setShare] = useState(SPLIT_DEFAULT_SHARE);
35
+ const row = useRef(null);
36
+ const dragging = useRef(false);
37
+ const shareAt = useCallback((clientX) => {
38
+ const box = row.current?.getBoundingClientRect();
39
+ if (!box || box.width <= 0) {
40
+ return null;
41
+ }
42
+ return clampSplitShare((clientX - box.left) / box.width);
43
+ }, []);
44
+ const onPointerDown = (event) => {
45
+ dragging.current = true;
46
+ event.currentTarget.setPointerCapture?.(event.pointerId);
47
+ event.preventDefault();
48
+ };
49
+ const onPointerMove = (event) => {
50
+ if (!dragging.current) {
51
+ return;
52
+ }
53
+ const next = shareAt(event.clientX);
54
+ if (next !== null) {
55
+ setShare(next);
56
+ }
57
+ };
58
+ const onPointerUp = (event) => {
59
+ dragging.current = false;
60
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
61
+ };
62
+ const onKeyDown = (event) => {
63
+ const next = splitShareForKey(share, event.key);
64
+ if (next !== null) {
65
+ event.preventDefault();
66
+ setShare(next);
67
+ }
68
+ };
69
+ const percent = Math.round(share * 100);
70
+ return (_jsxs(Box, { "data-testid": "loop-split-layout", sx: {
71
+ flex: '1 1 auto',
72
+ minHeight: 0,
73
+ display: 'flex',
74
+ flexDirection: 'column',
75
+ }, children: [picker, _jsxs(Box, { ref: row, sx: {
76
+ flex: '1 1 auto',
77
+ minHeight: 0,
78
+ display: 'flex',
79
+ // The hidden editors position themselves against this row.
80
+ position: 'relative',
81
+ overflow: 'hidden',
82
+ }, children: [_jsxs(Box, { "data-testid": "loop-split-conversation", sx: {
83
+ flex: hasEditor ? `0 0 ${percent}%` : '1 1 auto',
84
+ minWidth: 0,
85
+ minHeight: 0,
86
+ display: 'flex',
87
+ flexDirection: 'column',
88
+ // The transcript draws a rule on its left when an editor is on
89
+ // screen, for the split it sits on the right of. Here it is on
90
+ // the left, and the hairline is the separator: the rule is
91
+ // pushed out of the row, which clips it.
92
+ ml: hasEditor ? '-1px' : 0,
93
+ }, children: [transcript, chips, prompt] }), hasEditor ? (_jsx(Box, { role: "separator", "aria-orientation": "vertical", "aria-label": "Resize the conversation and the page", "aria-valuemin": Math.round(SPLIT_MIN_SHARE * 100), "aria-valuemax": Math.round(SPLIT_MAX_SHARE * 100), "aria-valuenow": percent, tabIndex: 0, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerUp, onKeyDown: onKeyDown, sx: {
94
+ // A hairline to the eye, a few pixels to the pointer.
95
+ flex: '0 0 7px',
96
+ mx: '-3px',
97
+ zIndex: 1,
98
+ cursor: 'col-resize',
99
+ touchAction: 'none',
100
+ display: 'flex',
101
+ justifyContent: 'center',
102
+ outline: 'none',
103
+ '&::before': {
104
+ content: '""',
105
+ width: '1px',
106
+ height: '100%',
107
+ bg: 'border.default',
108
+ },
109
+ '&:hover::before, &:focus-visible::before': {
110
+ width: '3px',
111
+ bg: 'accent.emphasis',
112
+ },
113
+ } })) : null, _jsx(Box, { "data-testid": "loop-split-page", sx: hasEditor
114
+ ? {
115
+ flex: '1 1 0',
116
+ minWidth: 0,
117
+ minHeight: 0,
118
+ display: 'flex',
119
+ }
120
+ : // Mounted but out of sight: the editors keep their place.
121
+ {
122
+ position: 'absolute',
123
+ inset: 0,
124
+ visibility: 'hidden',
125
+ pointerEvents: 'none',
126
+ zIndex: -1,
127
+ }, children: editors })] }), transient] }));
128
+ }
129
+ export default SplitLayout;
@@ -3,10 +3,23 @@ import type { ChatViewMode } from '../../../types/chat';
3
3
  import { type TurnPanelFooter } from './TurnPanel';
4
4
  export { pageLayoutPanelOpen, pageLayoutSheet, openPagePanel as openConversationPanel, } from '@datalayer/primer-addons/lib/reactor';
5
5
  export type { TurnPanelFooter } from './TurnPanel';
6
+ export { SplitLayout, SPLIT_DEFAULT_SHARE, SPLIT_KEY_STEP, SPLIT_MAX_SHARE, SPLIT_MIN_SHARE, clampSplitShare, splitShareForKey, } from './SplitLayout';
6
7
  export type { PageSize, PageSizeFormat, } from '@datalayer/primer-addons/lib/reactor';
7
8
  export declare const LOOP_PAGE_LAYOUT_PLUGIN_NAME = "@datalayer/loop-plugin-page-layout";
8
9
  /** What a host may set on the page layout. */
9
10
  export type LoopPageLayoutConfig = {
11
+ /**
12
+ * How the parts are arranged.
13
+ *
14
+ * `page` (the default): the work on a centred sheet, the composer over
15
+ * it, the conversation in a panel that opens when wanted. `split`: the
16
+ * conversation and the work side by side, the conversation on the left
17
+ * with the composer under it, one hairline between them that a person
18
+ * drags (`SplitLayout`) — an Appspec's `layout: split`. The split keeps
19
+ * the conversation on screen, so it has no turn panel, no conversation
20
+ * toggle and no display modes; the other settings are the page's.
21
+ */
22
+ arrangement?: 'page' | 'split';
10
23
  /**
11
24
  * Where the turn panel hangs on the composer.
12
25
  *
@@ -6,12 +6,14 @@ import { PageLayout, PagePanelToggle, openPagePanel, } from '@datalayer/primer-a
6
6
  import { ChatViewModeToggle } from '../../../chat/header/ChatViewModeToggle';
7
7
  import { LoopChatLayout, LoopChatTurn, LoopPromptPanel, LoopSlots, } from '../../core';
8
8
  import { TurnPanel } from './TurnPanel';
9
+ import { SplitLayout } from './SplitLayout';
9
10
  /*
10
11
  * The layout's state is primer-addons', re-exported under the names the
11
12
  * Loop always had: a host that opened the conversation panel by calling
12
13
  * `openConversationPanel()` keeps doing so.
13
14
  */
14
15
  export { pageLayoutPanelOpen, pageLayoutSheet, openPagePanel as openConversationPanel, } from '@datalayer/primer-addons/lib/reactor';
16
+ export { SplitLayout, SPLIT_DEFAULT_SHARE, SPLIT_KEY_STEP, SPLIT_MAX_SHARE, SPLIT_MIN_SHARE, clampSplitShare, splitShareForKey, } from './SplitLayout';
15
17
  export const LOOP_PAGE_LAYOUT_PLUGIN_NAME = '@datalayer/loop-plugin-page-layout';
16
18
  /* A signal to read when no chat contributed a turn: the hook needs one. */
17
19
  const NO_TURN = signal({ id: 0, status: 'idle' });
@@ -35,6 +37,8 @@ const promptStance = signal('floating-top');
35
37
  * page layout to a page, the assumption that signal already makes.
36
38
  */
37
39
  let floatingStance = 'floating-top';
40
+ /** The modes the page layout does not place. */
41
+ const PAGE_LAYOUT_DISABLED_MODES = ['assistant'];
38
42
  export function pageLayoutForChatMode(mode) {
39
43
  switch (mode) {
40
44
  case 'sidebar':
@@ -60,7 +64,9 @@ export function setPageLayoutChatMode(mode) {
60
64
  /** The toggle in the floating composer's footer. */
61
65
  function ChatModeAction() {
62
66
  const mode = useSignalValue(pageLayoutChatMode);
63
- return _jsx(ChatViewModeToggle, { value: mode, onChange: setPageLayoutChatMode });
67
+ // The page layout places the chat itself: the floating assistant, which
68
+ // ChatFloating draws, is offered greyed out with its reason (T-21).
69
+ return (_jsx(ChatViewModeToggle, { value: mode, onChange: setPageLayoutChatMode, disabledModes: PAGE_LAYOUT_DISABLED_MODES }));
64
70
  }
65
71
  /**
66
72
  * The chat view's parts on the page layout.
@@ -87,6 +93,7 @@ function ConversationToggle() {
87
93
  export const LoopPageLayoutPlugin = definePlugin({
88
94
  name: LOOP_PAGE_LAYOUT_PLUGIN_NAME,
89
95
  config: {
96
+ arrangement: 'page',
90
97
  turnPanel: 'below',
91
98
  turnPanelFooter: 'full',
92
99
  prompt: 'docked',
@@ -94,10 +101,16 @@ export const LoopPageLayoutPlugin = definePlugin({
94
101
  pageSize: undefined,
95
102
  },
96
103
  displayName: 'Page layout',
97
- description: 'The editor — or, in the chat view, the conversation — on a centred sheet, the prompt docked above it at the same width or floating over it as a draggable card, the current turn under the prompt, the conversation in a side panel.',
104
+ description: 'The editor — or, in the chat view, the conversation — on a centred sheet, the prompt docked above it at the same width or floating over it as a draggable card, the current turn under the prompt, the conversation in a side panel; or, split, the conversation and the work side by side with a hairline to drag.',
98
105
  octicon: 'file',
99
106
  emoji: '\u{1F4C4}',
100
107
  build: ({ config, ...ctx }) => {
108
+ if (config.arrangement === 'split') {
109
+ // The conversation is on screen: the composer docked under it, and
110
+ // nothing that stands in for a transcript out of sight.
111
+ ctx.contribute(LoopChatLayout, { id: 'split-layout', prompt: 'docked', Component: SplitLayout }, { id: 'split-layout' });
112
+ return { components: [] };
113
+ }
101
114
  /*
102
115
  The layout, contributed per build because the composer's stance is
103
116
  configuration: `docked-top` means the layout owns the composer's width
@@ -34,6 +34,11 @@ export type LoopPresetOptions = {
34
34
  * A surface id rather than a fixed union, because the surfaces are
35
35
  * contributed: an editor plugin the preset has never heard of is still a
36
36
  * valid answer, and `'none'` is how a host asks for the chat alone.
37
+ *
38
+ * The notebook when unsaid, and none when `editors` is off. Said with
39
+ * `editors` off, it is honoured: the surface of a plugin the host mounts —
40
+ * an application's page — opens beside the chat without the notebook and
41
+ * the document.
37
42
  */
38
43
  defaultEditor?: ChatPluginConfig['defaultSurface'];
39
44
  /** Whether the chat offers its surface switcher. */
@@ -82,6 +87,13 @@ export type LoopPresetOptions = {
82
87
  * `LoopChatLayout` point.
83
88
  */
84
89
  pageLayout?: boolean;
90
+ /**
91
+ * How the page layout arranges the parts: `page` (the default), the work
92
+ * on a sheet with the conversation in a panel; or `split`, the
93
+ * conversation and the work side by side with a hairline to drag. Only
94
+ * read with `pageLayout`.
95
+ */
96
+ pageLayoutArrangement?: 'page' | 'split';
85
97
  /**
86
98
  * Where the page layout hangs the current turn on the composer: `below`
87
99
  * (the default), `above`, or `none`. Only read with `pageLayout`.
@@ -52,12 +52,13 @@ import { LoopPageLayoutPlugin } from './plugins/page-layout';
52
52
  * be installed or signed into, and the switches a demonstration wants are off.
53
53
  */
54
54
  export function loopPlugins(options = {}) {
55
- const { serverUrl, target = 'browser', defaultEditor = 'notebook', showViewSelector = true, hideChatHeader = false, chatHeaderButtons = false, promptPlacement, autoFocusPrompt = true, suggestionLabels = true, fullScreenTopOffset = 0, showAgentVariants = false, agentSummary = true, teamId, teamPicker = true, pageLayout = false, pageLayoutTurnPanel = 'below', pageLayoutPrompt = 'docked', pageLayoutPromptAnchor = 'top', pageLayoutSize, pageLayoutTurnPanelFooter = 'full', localAgent, localAgentSpec, datalayerAgentSpecId, datalayerCreatePayload, targetFixed, themeVariant, presence, showTokenUsage, floatingPrompt = false, editorSelector = false, editors = true, graph = false, commandPalette = false, pluginsPanel = false, windowFrame = false, firstPromptHook, } = options;
55
+ const { serverUrl, target = 'browser', defaultEditor: askedEditor, showViewSelector = true, hideChatHeader = false, chatHeaderButtons = false, promptPlacement, autoFocusPrompt = true, suggestionLabels = true, fullScreenTopOffset = 0, showAgentVariants = false, agentSummary = true, teamId, teamPicker = true, pageLayout = false, pageLayoutArrangement = 'page', pageLayoutTurnPanel = 'below', pageLayoutPrompt = 'docked', pageLayoutPromptAnchor = 'top', pageLayoutSize, pageLayoutTurnPanelFooter = 'full', localAgent, localAgentSpec, datalayerAgentSpecId, datalayerCreatePayload, targetFixed, themeVariant, presence, showTokenUsage, floatingPrompt = false, editorSelector = false, editors = true, graph = false, commandPalette = false, pluginsPanel = false, windowFrame = false, firstPromptHook, } = options;
56
+ const defaultEditor = askedEditor ?? (editors ? 'notebook' : 'none');
56
57
  return [
57
58
  // The chat owns the editor beside it, so which one opens is its
58
59
  // configuration rather than the workspace's.
59
60
  configurePlugin(ChatPlugin, {
60
- defaultSurface: editors ? defaultEditor : 'none',
61
+ defaultSurface: defaultEditor,
61
62
  // The chat's strip stands down when the header selector offers the
62
63
  // same choice; see `editorSelector` — and when there is no editor
63
64
  // to choose.
@@ -130,13 +131,14 @@ export function loopPlugins(options = {}) {
130
131
  // happens to be on is a workspace with a trap in it. The selector itself
131
132
  // stays behind the old switch.
132
133
  configurePlugin(ShellPlugin, {
133
- defaultEditor: editors ? defaultEditor : 'none',
134
+ defaultEditor,
134
135
  showSelector: editors && editorSelector,
135
136
  }),
136
137
  ...(floatingPrompt ? [PromptPlugin] : []),
137
138
  ...(pageLayout
138
139
  ? [
139
140
  configurePlugin(LoopPageLayoutPlugin, {
141
+ arrangement: pageLayoutArrangement,
140
142
  turnPanel: pageLayoutTurnPanel,
141
143
  turnPanelFooter: pageLayoutTurnPanelFooter,
142
144
  prompt: pageLayoutPrompt,
@@ -439,6 +439,8 @@ export const SERVER_ACTIONS = {
439
439
  * to reproduce.
440
440
  */
441
441
  export const APP_BEHAVIOURS = {
442
+ 'customer-interview': {},
443
+ 'data-quality': {},
442
444
  'inbox-triage': {
443
445
  'google-workspace.append_table_rows': 'leave_to_me',
444
446
  'google-workspace.batch_modify_gmail_message_labels': 'do_it',
@@ -561,8 +563,13 @@ export const APP_BEHAVIOURS = {
561
563
  'google-workspace.update_drive_file': 'leave_to_me',
562
564
  'google-workspace.update_paragraph_style': 'leave_to_me',
563
565
  },
566
+ 'model-choice': {},
567
+ 'pipeline-report': {},
564
568
  'quote-calculator': {},
569
+ 'report-from-a-file': {},
565
570
  'ship-or-fix': {},
571
+ 'supplier-comparison': {},
572
+ 'support-desk': {},
566
573
  'web-research': {
567
574
  'tavily.tavily_crawl': 'do_it',
568
575
  'tavily.tavily_extract': 'do_it',
@@ -576,6 +583,8 @@ export const APP_BEHAVIOURS = {
576
583
  * application and tool, each condition and the behaviour when it holds.
577
584
  */
578
585
  export const APP_ESCALATIONS = {
586
+ 'customer-interview': {},
587
+ 'data-quality': {},
579
588
  'inbox-triage': {
580
589
  'google-workspace.batch_modify_gmail_message_labels': [
581
590
  {
@@ -610,7 +619,12 @@ export const APP_ESCALATIONS = {
610
619
  },
611
620
  ],
612
621
  },
622
+ 'model-choice': {},
623
+ 'pipeline-report': {},
613
624
  'quote-calculator': {},
625
+ 'report-from-a-file': {},
614
626
  'ship-or-fix': {},
627
+ 'supplier-comparison': {},
628
+ 'support-desk': {},
615
629
  'web-research': {},
616
630
  };
@@ -3380,6 +3380,7 @@ export const EXAMPLE_ONE_TRIGGER_AGENTSPEC_0_0_1 = {
3380
3380
  domain: undefined,
3381
3381
  enabled: true,
3382
3382
  model: 'bedrock:us.anthropic.claude-sonnet-4-6',
3383
+ modelAdditionals: ['alibaba:qwen-max'],
3383
3384
  mcpServers: [],
3384
3385
  skills: [
3385
3386
  SKILL_MAP['github:0.0.1']
@@ -4389,6 +4390,7 @@ export const EXAMPLE_SIMPLE_AGENTSPEC_0_0_1 = {
4389
4390
  domain: undefined,
4390
4391
  enabled: true,
4391
4392
  model: 'bedrock:us.anthropic.claude-sonnet-4-6',
4393
+ modelAdditionals: ['alibaba:qwen-max'],
4392
4394
  mcpServers: [],
4393
4395
  skills: [
4394
4396
  SKILL_MAP['events:0.0.1']
@@ -5021,6 +5023,7 @@ export const JUPYTER_NOTEBOOK_COMPACTOR_AGENTSPEC_0_0_1 = {
5021
5023
  domain: undefined,
5022
5024
  enabled: true,
5023
5025
  model: 'bedrock:us.anthropic.claude-sonnet-4-6',
5026
+ modelAdditionals: ['alibaba:qwen-max'],
5024
5027
  mcpServers: [],
5025
5028
  skills: [].filter(Boolean),
5026
5029
  tools: [TOOL_MAP['runtime-echo:0.0.1']],
@@ -7663,6 +7666,7 @@ export const WORKER_CRAWLER_AGENTSPEC_0_0_1 = {
7663
7666
  domain: 'market-analyst',
7664
7667
  enabled: true,
7665
7668
  model: 'bedrock:us.anthropic.claude-sonnet-4-6',
7669
+ modelAdditionals: ['alibaba:qwen-max'],
7666
7670
  mcpServers: [MCP_SERVER_MAP['tavily:0.0.1']],
7667
7671
  skills: [
7668
7672
  SKILL_MAP['github:0.0.1']
@@ -7,10 +7,17 @@
7
7
  * This file is AUTO-GENERATED from YAML specifications.
8
8
  * DO NOT EDIT MANUALLY - run 'make specs' to regenerate.
9
9
  */
10
- import type { AppKind, AppSpec } from '../types/agentspecs';
10
+ import type { AppBuilt, AppKind, AppSpec } from '../types/agentspecs';
11
+ export declare const CUSTOMER_INTERVIEW_APP_0_0_1: AppSpec;
12
+ export declare const DATA_QUALITY_APP_0_0_1: AppSpec;
11
13
  export declare const INBOX_TRIAGE_APP_0_0_1: AppSpec;
14
+ export declare const MODEL_CHOICE_APP_0_0_1: AppSpec;
15
+ export declare const PIPELINE_REPORT_APP_0_0_1: AppSpec;
12
16
  export declare const QUOTE_CALCULATOR_APP_0_0_1: AppSpec;
17
+ export declare const REPORT_FROM_A_FILE_APP_0_0_1: AppSpec;
13
18
  export declare const SHIP_OR_FIX_APP_0_0_1: AppSpec;
19
+ export declare const SUPPLIER_COMPARISON_APP_0_0_1: AppSpec;
20
+ export declare const SUPPORT_DESK_APP_0_0_1: AppSpec;
14
21
  export declare const WEB_RESEARCH_APP_0_0_1: AppSpec;
15
22
  export declare const APP_CATALOGUE: Record<string, AppSpec>;
16
23
  /** An application, by `id` or `id:version`, or undefined. */
@@ -21,5 +28,10 @@ export declare function getApp(ref: string): AppSpec | undefined;
21
28
  * Appspec have to give back.
22
29
  */
23
30
  export declare const APP_SOURCES: Record<string, Record<string, unknown>>;
31
+ /**
32
+ * How each application was built: `python` (its `app.py`), `canvas` (its
33
+ * page composed on the Canvas) or `written` (its spec written out).
34
+ */
35
+ export declare const APP_BUILT: Record<string, AppBuilt>;
24
36
  /** Every application of the catalogue, or those of a kind. */
25
37
  export declare function listApps(kind?: AppKind): AppSpec[];