@datalayer/agent-runtimes 1.3.29 → 1.3.32

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 (107) hide show
  1. package/lib/AgentNode.js +14 -33
  2. package/lib/chat/ChatFloating.js +40 -8
  3. package/lib/chat/assistant/AssistantStage.d.ts +26 -7
  4. package/lib/chat/assistant/AssistantStage.js +115 -8
  5. package/lib/chat/assistant/SpeechBalloon.js +7 -3
  6. package/lib/chat/assistant/characters.d.ts +32 -1
  7. package/lib/chat/assistant/characters.js +180 -15
  8. package/lib/chat/assistant/state.d.ts +28 -0
  9. package/lib/chat/assistant/state.js +31 -0
  10. package/lib/chat/base/ChatBase.js +74 -50
  11. package/lib/chat/base/modelChoice.d.ts +40 -1
  12. package/lib/chat/base/modelChoice.js +42 -15
  13. package/lib/chat/prompt/InputPrompt.d.ts +7 -1
  14. package/lib/chat/prompt/InputPrompt.js +2 -2
  15. package/lib/chat/prompt/InputPromptBase.js +1 -1
  16. package/lib/chat/prompt/menus/ModelSelector.d.ts +13 -1
  17. package/lib/chat/prompt/menus/ModelSelector.js +17 -5
  18. package/lib/chat/useViewportDrag.d.ts +9 -1
  19. package/lib/chat/useViewportDrag.js +4 -4
  20. package/lib/components/a2ui/index.d.ts +2 -0
  21. package/lib/components/a2ui/index.js +2 -0
  22. package/lib/components/a2ui/visibility.d.ts +15 -0
  23. package/lib/components/a2ui/visibility.js +24 -0
  24. package/lib/components/a2ui/visibleWhen.d.ts +17 -0
  25. package/lib/components/a2ui/visibleWhen.js +54 -0
  26. package/lib/examples/ChatAssistantExample.d.ts +4 -0
  27. package/lib/examples/ChatAssistantExample.js +26 -5
  28. package/lib/examples/utils/owlCharacterPlugin.d.ts +7 -0
  29. package/lib/examples/utils/owlCharacterPlugin.js +44 -0
  30. package/lib/loop/apps/AppFeedback.d.ts +27 -0
  31. package/lib/loop/apps/AppFeedback.js +78 -0
  32. package/lib/loop/apps/AppRenderer.d.ts +33 -1
  33. package/lib/loop/apps/AppRenderer.js +111 -29
  34. package/lib/loop/apps/agent.d.ts +15 -0
  35. package/lib/loop/apps/agent.js +42 -0
  36. package/lib/loop/apps/appspec.js +6 -3
  37. package/lib/loop/apps/checks.d.ts +10 -2
  38. package/lib/loop/apps/checks.js +21 -10
  39. package/lib/loop/apps/feedback.d.ts +51 -0
  40. package/lib/loop/apps/feedback.js +61 -0
  41. package/lib/loop/apps/index.d.ts +3 -0
  42. package/lib/loop/apps/index.js +3 -0
  43. package/lib/loop/apps/rules.d.ts +14 -0
  44. package/lib/loop/apps/rules.js +24 -0
  45. package/lib/loop/core/canvasBlocks.d.ts +16 -0
  46. package/lib/loop/core/canvasBlocks.js +13 -0
  47. package/lib/loop/core/index.d.ts +15 -0
  48. package/lib/loop/core/index.js +3 -0
  49. package/lib/loop/embed/AppEmbed.d.ts +42 -0
  50. package/lib/loop/embed/AppEmbed.js +205 -0
  51. package/lib/loop/embed/datalayer-app.d.ts +1 -0
  52. package/lib/loop/embed/datalayer-app.js +16 -0
  53. package/lib/loop/embed/element.d.ts +57 -0
  54. package/lib/loop/embed/element.js +405 -0
  55. package/lib/loop/embed/embedConfig.d.ts +120 -0
  56. package/lib/loop/embed/embedConfig.js +148 -0
  57. package/lib/loop/embed/embedTheme.d.ts +24 -0
  58. package/lib/loop/embed/embedTheme.js +83 -0
  59. package/lib/loop/index.d.ts +6 -2
  60. package/lib/loop/index.js +8 -2
  61. package/lib/loop/plugins/a2ui/A2uiView.js +2 -3
  62. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +23 -3
  63. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +36 -9
  64. package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
  65. package/lib/loop/plugins/agent-capacity/index.js +3 -1
  66. package/lib/loop/plugins/agents/switchable.d.ts +7 -0
  67. package/lib/loop/plugins/agents/switchable.js +91 -88
  68. package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
  69. package/lib/loop/plugins/app-page/AppPage.js +53 -0
  70. package/lib/loop/plugins/app-page/appPageModel.d.ts +180 -0
  71. package/lib/loop/plugins/app-page/appPageModel.js +443 -0
  72. package/lib/loop/plugins/app-page/index.d.ts +8 -0
  73. package/lib/loop/plugins/app-page/index.js +34 -0
  74. package/lib/loop/plugins/canvas-blocks/index.d.ts +63 -0
  75. package/lib/loop/plugins/canvas-blocks/index.js +118 -0
  76. package/lib/loop/plugins/chat/ChatView.js +52 -26
  77. package/lib/loop/plugins/chat/turnState.d.ts +5 -2
  78. package/lib/loop/plugins/chat/turnState.js +2 -1
  79. package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
  80. package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
  81. package/lib/loop/plugins/page-layout/index.d.ts +13 -0
  82. package/lib/loop/plugins/page-layout/index.js +10 -1
  83. package/lib/loop/presets.d.ts +12 -0
  84. package/lib/loop/presets.js +5 -3
  85. package/lib/specs/actions.js +9 -0
  86. package/lib/specs/agents/agents.js +20 -4
  87. package/lib/specs/apps.d.ts +10 -1
  88. package/lib/specs/apps.js +1779 -154
  89. package/lib/specs/appspecSchema.js +14 -8
  90. package/lib/specs/models.d.ts +1 -1
  91. package/lib/specs/models.js +8 -8
  92. package/lib/specs/tools.d.ts +1 -0
  93. package/lib/specs/tools.js +19 -0
  94. package/lib/stories/loop/LoopReference.stories.d.ts +21 -0
  95. package/lib/stories/loop/LoopReference.stories.js +31 -0
  96. package/lib/stories/loop/ReferenceScreens.d.ts +53 -0
  97. package/lib/stories/loop/ReferenceScreens.js +256 -0
  98. package/lib/stories/loop/pictures-main.d.ts +1 -0
  99. package/lib/stories/loop/pictures-main.js +64 -0
  100. package/lib/types/agentspecs.d.ts +17 -1
  101. package/lib/types/chat.d.ts +5 -0
  102. package/lib/types/config.d.ts +20 -0
  103. package/package.json +10 -6
  104. package/scripts/check-cloudflare-models.py +2 -2
  105. package/scripts/codegen/generate_agents.py +50 -2
  106. package/scripts/codegen/generate_apps.py +38 -7
  107. package/scripts/codegen/generate_models.py +4 -4
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The Canvas's palette as a contribution point (LOOP C-12): each UI plugin of
3
+ * the catalogue that renders components (`agentspecs/ui-plugins`, C-13) is a
4
+ * Reactor plugin contributing its components to `loop.canvas.block`. The
5
+ * palette lists what the enabled plugins contribute, and nothing else; a
6
+ * plugin disabled takes its blocks off it, and an application that uses one
7
+ * of them says so in its setup notes (`blockSetupNotes`).
8
+ *
9
+ * A new kind of block arrives as a plugin contributing to the point, never as
10
+ * a change to the Canvas.
11
+ *
12
+ * @module loop/plugins/canvas-blocks
13
+ */
14
+ import { type ReactorPlatform, type ReactorPlugin } from '@datalayer/reactor';
15
+ import type { AppSpec, ComponentSpec, UIPluginSpec } from '../../../types/agentspecs';
16
+ import { LoopCanvasBlock, type CanvasBlockContribution } from '../../core/canvasBlocks';
17
+ /** What is read of a reactor here: its contributions to the point, and who is enabled. */
18
+ type BlocksReader = {
19
+ getContributions: (point: typeof LoopCanvasBlock) => ReadonlyArray<{
20
+ value: CanvasBlockContribution;
21
+ }>;
22
+ };
23
+ /** The name of the Reactor plugin that contributes a UI plugin's blocks. */
24
+ export declare const canvasBlocksPluginName: (uiPlugin: string) => string;
25
+ /** A UI plugin of the catalogue as the Reactor plugin contributing its components as blocks. */
26
+ export declare function uiPluginBlocks(uiPlugin: UIPluginSpec): ReactorPlugin<Record<string, never>, unknown, unknown>;
27
+ /**
28
+ * The plugins of the catalogue's UI plugins that render components and may
29
+ * be named today (`enabled` in the catalogue).
30
+ */
31
+ export declare const CANVAS_BLOCK_PLUGINS: ReactorPlugin<Record<string, never>, unknown, unknown>[];
32
+ /**
33
+ * A reactor holding the block plugins given, started, with those named in
34
+ * `disabled` switched off. A name no plugin has is an error.
35
+ */
36
+ export declare function canvasBlocksReactor(disabled?: readonly string[], plugins?: ReactorPlugin<any, any, any>[]): ReactorPlatform;
37
+ /**
38
+ * The palette of an organization: the block plugins of the UI plugins it has
39
+ * turned off (`plugins_off` in IAM, catalogue ids such as 'a2ui') switched
40
+ * off. A UI plugin of the catalogue that contributes no block takes nothing
41
+ * off; an id the catalogue does not have is said by `unknownPluginsOff`.
42
+ */
43
+ export declare function canvasBlocksReactorOf(pluginsOff: readonly string[]): ReactorPlatform;
44
+ /** The ids an organization turns off that no UI plugin of the catalogue has. */
45
+ export declare function unknownPluginsOff(pluginsOff: readonly string[]): string[];
46
+ /**
47
+ * What an application's page uses from a UI plugin its organization has
48
+ * turned off, as its setup notes say it (`blockSetupNotes`, on the palette of
49
+ * `canvasBlocksReactorOf`).
50
+ */
51
+ export declare function pluginsOffSetupNotes(app: Pick<AppSpec, 'interface'>, pluginsOff: readonly string[]): string[];
52
+ /** The blocks the enabled plugins contribute, in contribution order: the palette. */
53
+ export declare function canvasBlocksOf(reactor: BlocksReader): ComponentSpec[];
54
+ /** The components an application's page names: those it may use, and those its surface places. */
55
+ export declare function componentsUsedBy(app: Pick<AppSpec, 'interface'>): string[];
56
+ /**
57
+ * What an application's page uses that no enabled plugin contributes, as its
58
+ * setup notes say it: one sentence per UI plugin of the catalogue whose
59
+ * blocks it uses while its plugin is off. A component the catalogue does not
60
+ * have is not a note but a problem of the instant checks.
61
+ */
62
+ export declare function blockSetupNotes(app: Pick<AppSpec, 'interface'>, reactor: BlocksReader): string[];
63
+ export {};
@@ -0,0 +1,118 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The Canvas's palette as a contribution point (LOOP C-12): each UI plugin of
7
+ * the catalogue that renders components (`agentspecs/ui-plugins`, C-13) is a
8
+ * Reactor plugin contributing its components to `loop.canvas.block`. The
9
+ * palette lists what the enabled plugins contribute, and nothing else; a
10
+ * plugin disabled takes its blocks off it, and an application that uses one
11
+ * of them says so in its setup notes (`blockSetupNotes`).
12
+ *
13
+ * A new kind of block arrives as a plugin contributing to the point, never as
14
+ * a change to the Canvas.
15
+ *
16
+ * @module loop/plugins/canvas-blocks
17
+ */
18
+ import { buildReactorFromPlugins, contribution, definePlugin, } from '@datalayer/reactor';
19
+ import { listUIPlugins } from '../../../specs/uiPlugins';
20
+ import { LoopCanvasBlock, } from '../../core/canvasBlocks';
21
+ /** The name of the Reactor plugin that contributes a UI plugin's blocks. */
22
+ export const canvasBlocksPluginName = (uiPlugin) => `@datalayer/loop-plugin-ui-${uiPlugin}`;
23
+ /** A UI plugin of the catalogue as the Reactor plugin contributing its components as blocks. */
24
+ export function uiPluginBlocks(uiPlugin) {
25
+ return definePlugin({
26
+ name: canvasBlocksPluginName(uiPlugin.id),
27
+ displayName: uiPlugin.name,
28
+ description: `The blocks of ${uiPlugin.name} on the Canvas: ${uiPlugin.components.map(component => component.name).join(', ')}.`,
29
+ octicon: 'apps',
30
+ contributes: uiPlugin.components.map(component => contribution(LoopCanvasBlock, { id: component.id, uiPlugin: uiPlugin.id, component }, { id: component.id })),
31
+ });
32
+ }
33
+ /**
34
+ * The plugins of the catalogue's UI plugins that render components and may
35
+ * be named today (`enabled` in the catalogue).
36
+ */
37
+ export const CANVAS_BLOCK_PLUGINS = listUIPlugins()
38
+ .filter(uiPlugin => uiPlugin.enabled && uiPlugin.components.length > 0)
39
+ .map(uiPluginBlocks);
40
+ /**
41
+ * A reactor holding the block plugins given, started, with those named in
42
+ * `disabled` switched off. A name no plugin has is an error.
43
+ */
44
+ export function canvasBlocksReactor(disabled = [], plugins = CANVAS_BLOCK_PLUGINS) {
45
+ const reactor = buildReactorFromPlugins(plugins);
46
+ reactor.start();
47
+ for (const name of disabled) {
48
+ if (!reactor.hasPlugin(name)) {
49
+ throw new Error(`No plugin of the Canvas is named "${name}"; they are ${plugins.map(plugin => plugin.name).join(', ') || 'none'}.`);
50
+ }
51
+ reactor.disable(name);
52
+ }
53
+ return reactor;
54
+ }
55
+ /**
56
+ * The palette of an organization: the block plugins of the UI plugins it has
57
+ * turned off (`plugins_off` in IAM, catalogue ids such as 'a2ui') switched
58
+ * off. A UI plugin of the catalogue that contributes no block takes nothing
59
+ * off; an id the catalogue does not have is said by `unknownPluginsOff`.
60
+ */
61
+ export function canvasBlocksReactorOf(pluginsOff) {
62
+ const blockPlugins = new Set(CANVAS_BLOCK_PLUGINS.map(plugin => plugin.name));
63
+ return canvasBlocksReactor(pluginsOff
64
+ .map(canvasBlocksPluginName)
65
+ .filter(name => blockPlugins.has(name)));
66
+ }
67
+ /** The ids an organization turns off that no UI plugin of the catalogue has. */
68
+ export function unknownPluginsOff(pluginsOff) {
69
+ const known = new Set(listUIPlugins().map(uiPlugin => uiPlugin.id));
70
+ return pluginsOff.filter(id => !known.has(id));
71
+ }
72
+ /**
73
+ * What an application's page uses from a UI plugin its organization has
74
+ * turned off, as its setup notes say it (`blockSetupNotes`, on the palette of
75
+ * `canvasBlocksReactorOf`).
76
+ */
77
+ export function pluginsOffSetupNotes(app, pluginsOff) {
78
+ return pluginsOff.length > 0
79
+ ? blockSetupNotes(app, canvasBlocksReactorOf(pluginsOff))
80
+ : [];
81
+ }
82
+ /** The blocks the enabled plugins contribute, in contribution order: the palette. */
83
+ export function canvasBlocksOf(reactor) {
84
+ return reactor
85
+ .getContributions(LoopCanvasBlock)
86
+ .map(entry => entry.value.component);
87
+ }
88
+ /** The components an application's page names: those it may use, and those its surface places. */
89
+ export function componentsUsedBy(app) {
90
+ return [
91
+ ...new Set([
92
+ ...(app.interface.components ?? []),
93
+ ...(app.interface.surface?.components ?? []).map(node => String(node.component)),
94
+ ]),
95
+ ];
96
+ }
97
+ /**
98
+ * What an application's page uses that no enabled plugin contributes, as its
99
+ * setup notes say it: one sentence per UI plugin of the catalogue whose
100
+ * blocks it uses while its plugin is off. A component the catalogue does not
101
+ * have is not a note but a problem of the instant checks.
102
+ */
103
+ export function blockSetupNotes(app, reactor) {
104
+ const contributed = new Set(reactor.getContributions(LoopCanvasBlock).map(entry => entry.value.id));
105
+ const off = new Map();
106
+ for (const name of componentsUsedBy(app)) {
107
+ if (contributed.has(name)) {
108
+ continue;
109
+ }
110
+ for (const uiPlugin of listUIPlugins()) {
111
+ const component = uiPlugin.components.find(each => each.id === name);
112
+ if (component) {
113
+ off.set(uiPlugin, [...(off.get(uiPlugin) ?? []), component.name]);
114
+ }
115
+ }
116
+ }
117
+ return [...off.entries()].map(([uiPlugin, names]) => `The UI plugin “${uiPlugin.name}” is not enabled, and its page uses its ${names.length === 1 ? 'block' : 'blocks'} ${names.join(', ')}: ${names.length === 1 ? 'it is' : 'they are'} off the Canvas until it is.`);
118
+ }
@@ -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, readServerDecisions, } 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';
@@ -393,7 +393,7 @@ export default function ChatView({ workspace }) {
393
393
  }
394
394
  setTransient(null);
395
395
  // A new turn: whatever the panel showed is gone, this message is it.
396
- turnFeedRef.current?.begin(message);
396
+ turnFeedRef.current?.begin(message, controlsRef.current?.thread());
397
397
  const outcome = await workspace.submit(message);
398
398
  if (!outcome.handled) {
399
399
  /*
@@ -655,9 +655,23 @@ export default function ChatView({ workspace }) {
655
655
  * icon for want of this lookup — it used the document tools and called
656
656
  * nobody.
657
657
  */
658
- const blueprintSpecId = useContributions(LoopAgentBlueprint)[0]?.value.specId;
659
- const spec = getAgentspecs(member?.specId ?? agentId) ??
660
- (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;
661
675
  /*
662
676
  * The team's name for a member first, then the spec's own.
663
677
  *
@@ -945,12 +959,12 @@ export default function ChatView({ workspace }) {
945
959
  * for something that was never theirs.
946
960
  */
947
961
  const expiredKeyIsTemporary = !signedInUser;
948
- const chatDisabled = gateBlocked || keyExpired;
962
+ const chatDisabled = gateBlocked || keyExpired || Boolean(inPageRefusal);
949
963
  const disabledReason = keyExpired
950
964
  ? expiredKeyIsTemporary
951
965
  ? 'This demo runs on a shared key, and its time is up. Sign in to keep going.'
952
966
  : 'Your key has expired. Sign in to keep going.'
953
- : gateReason;
967
+ : (inPageRefusal ?? gateReason);
954
968
  /*
955
969
  * The prompt's text, held here rather than inside `InputPrompt`.
956
970
  *
@@ -981,7 +995,7 @@ export default function ChatView({ workspace }) {
981
995
  * the member being addressed changes.
982
996
  */
983
997
  const [pickedModel, setPickedModel] = useState();
984
- const activeModel = pickedModel ?? spec?.model ?? workspace.model ?? '';
998
+ const activeModel = pickedModel ?? blueprintTurn?.model ?? spec?.model ?? workspace.model ?? '';
985
999
  /*
986
1000
  * The team, in the shape the footer asks for.
987
1001
  *
@@ -1071,7 +1085,7 @@ export default function ChatView({ workspace }) {
1071
1085
  const protocol = useMemo(() => inPage
1072
1086
  ? browserProtocolConfig({
1073
1087
  agentId: member?.specId ?? agentId,
1074
- instructions: spec?.systemPrompt,
1088
+ instructions: agentInstructions(spec, blueprintTurn?.instructions),
1075
1089
  model: activeModel || undefined,
1076
1090
  inference,
1077
1091
  // Who this member may hand work to, and what they are told. Both
@@ -1111,6 +1125,7 @@ export default function ChatView({ workspace }) {
1111
1125
  agentTools,
1112
1126
  team,
1113
1127
  spec,
1128
+ blueprintTurn,
1114
1129
  activeModel,
1115
1130
  agentServerUrl,
1116
1131
  ]);
@@ -1128,7 +1143,10 @@ export default function ChatView({ workspace }) {
1128
1143
  const configQuery = useConfig(!inPage, protocol.type === 'ag-ui' ? protocol.configEndpoint : undefined, undefined, agentId);
1129
1144
  const skillsQuery = useSkills(true);
1130
1145
  const skillActions = useSkillActions(agentId);
1131
- const [catalogModels, setCatalogModels] = useState([]);
1146
+ // `undefined` until the runtime answers: its answer is then the list.
1147
+ const [catalogModels, setCatalogModels] = useState(undefined);
1148
+ // The typed-decision models the runtime lists apart: shown, never picked.
1149
+ const [decisions, setDecisions] = useState(undefined);
1132
1150
  useEffect(() => {
1133
1151
  /*
1134
1152
  * Only where there is a server to ask.
@@ -1143,23 +1161,32 @@ export default function ChatView({ workspace }) {
1143
1161
  return undefined;
1144
1162
  }
1145
1163
  let cancelled = false;
1146
- void fetch(`${agentServerUrl}/api/v1/configure/models`)
1147
- .then(response => (response.ok ? response.json() : { models: [] }))
1164
+ // The agent's own models (its model and model_additionals), each saying
1165
+ // whether its inference serves it.
1166
+ void fetch(`${agentServerUrl}/api/v1/configure/models?agent_id=${encodeURIComponent(agentId)}`)
1167
+ .then(response => (response.ok ? response.json() : null))
1148
1168
  .then(payload => {
1149
1169
  if (!cancelled) {
1150
1170
  // In the chat's shape: the route says `available`, the menu and
1151
1171
  // the opening pick read `isAvailable`, and an unmapped row passes
1152
1172
  // for usable. See `readServerCatalogue`.
1153
- setCatalogModels(readServerCatalogue(payload));
1173
+ // A runtime that did not answer offers nothing: the agentspecs
1174
+ // catalogue is not its answer.
1175
+ setCatalogModels(payload ? readServerCatalogue(payload) : []);
1176
+ setDecisions(payload ? readServerDecisions(payload) : undefined);
1154
1177
  }
1155
1178
  })
1156
1179
  // No catalogue is not an error worth a banner: the menu simply has
1157
1180
  // nothing to offer, and the agent answers on whatever it was given.
1158
- .catch(() => undefined);
1181
+ .catch(() => {
1182
+ if (!cancelled) {
1183
+ setCatalogModels([]);
1184
+ }
1185
+ });
1159
1186
  return () => {
1160
1187
  cancelled = true;
1161
1188
  };
1162
- }, [agentServerUrl, inPage]);
1189
+ }, [agentServerUrl, agentId, inPage]);
1163
1190
  /*
1164
1191
  * What to offer when the server has no catalogue of its own.
1165
1192
  *
@@ -1168,16 +1195,14 @@ export default function ChatView({ workspace }) {
1168
1195
  * with nothing in it is a control that opens onto an empty list — so the
1169
1196
  * agentspecs catalogue stands in, filtered to the models marked available.
1170
1197
  */
1171
- const offeredModels = useMemo(() => catalogModels.length > 0
1172
- ? catalogModels
1173
- : Object.values(AI_MODEL_CATALOGUE)
1174
- // A typed-judgment model answers typed questions, not a chat.
1175
- .filter(model => model.available && isChatModel(model))
1176
- .map(model => ({
1177
- id: model.id,
1178
- name: model.name,
1179
- provider: model.provider,
1180
- })), [catalogModels]);
1198
+ const offeredModels = useMemo(() => offeredModelsFor(catalogModels, () => Object.values(AI_MODEL_CATALOGUE)
1199
+ // A typed-decision model answers typed questions, not a chat.
1200
+ .filter(model => model.available && isChatModel(model))
1201
+ .map(model => ({
1202
+ id: model.id,
1203
+ name: model.name,
1204
+ provider: model.provider,
1205
+ }))), [catalogModels]);
1181
1206
  /*
1182
1207
  * The tools the agent can call, from both sides.
1183
1208
  *
@@ -1313,6 +1338,7 @@ export default function ChatView({ workspace }) {
1313
1338
  agentUsage: contextUsage ?? undefined,
1314
1339
  showModelSelector: true,
1315
1340
  models: offeredModels,
1341
+ decisions,
1316
1342
  selectedModel: activeModel,
1317
1343
  onModelSelect: model => void selectModel(model),
1318
1344
  showToolsMenu: true,
@@ -15,8 +15,11 @@ import type { ChatTurnSnapshot, ChatTurnStatus } from '../../core';
15
15
  export type TurnFeed = {
16
16
  /** The turn, for readers. */
17
17
  turn: ReadonlySignal<ChatTurnSnapshot>;
18
- /** A new turn: the previous one is gone, this one holds the message. */
19
- begin: (user: string) => void;
18
+ /**
19
+ * A new turn: the previous one is gone, this one holds the message, and
20
+ * the conversation it went to, when the chat knows it.
21
+ */
22
+ begin: (user: string, thread?: string) => void;
20
23
  /** The reply so far. Moves the turn to `streaming` on its first text. */
21
24
  assistant: (text: string) => void;
22
25
  /** The agent stopped, one way or another. */
@@ -19,13 +19,14 @@ export function createTurnFeed() {
19
19
  const turn = signal(IDLE);
20
20
  return {
21
21
  turn,
22
- begin: user => {
22
+ begin: (user, thread) => {
23
23
  // The window's fill carries over: it is the conversation's, not the
24
24
  // turn's, and the footer under a fresh turn should not read empty
25
25
  // until the agent reports again.
26
26
  turn.value = {
27
27
  id: turn.value.id + 1,
28
28
  user,
29
+ ...(thread ? { thread } : {}),
29
30
  status: 'thinking',
30
31
  usage: turn.value.usage,
31
32
  };
@@ -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' });
@@ -91,6 +93,7 @@ function ConversationToggle() {
91
93
  export const LoopPageLayoutPlugin = definePlugin({
92
94
  name: LOOP_PAGE_LAYOUT_PLUGIN_NAME,
93
95
  config: {
96
+ arrangement: 'page',
94
97
  turnPanel: 'below',
95
98
  turnPanelFooter: 'full',
96
99
  prompt: 'docked',
@@ -98,10 +101,16 @@ export const LoopPageLayoutPlugin = definePlugin({
98
101
  pageSize: undefined,
99
102
  },
100
103
  displayName: 'Page layout',
101
- 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.',
102
105
  octicon: 'file',
103
106
  emoji: '\u{1F4C4}',
104
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
+ }
105
114
  /*
106
115
  The layout, contributed per build because the composer's stance is
107
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`.