@lowdefy/blocks-antd-x 6.0.0 → 7.0.0

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.
@@ -0,0 +1,32 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import React from 'react';
16
+ import { FloatButton } from 'antd';
17
+ // Shared by the loading fallback and the drawer, so the button painted before the
18
+ // implementation loads is the same one that replaces it.
19
+ function DrawerLauncher({ blockId, config, Icon, onClick }) {
20
+ return /*#__PURE__*/ React.createElement(FloatButton, {
21
+ icon: /*#__PURE__*/ React.createElement(Icon, {
22
+ blockId: `${blockId}_launcher_icon`,
23
+ properties: {
24
+ name: 'message',
25
+ title: ''
26
+ }
27
+ }),
28
+ onClick: onClick,
29
+ tooltip: config?.title ?? 'Chat'
30
+ });
31
+ }
32
+ export default DrawerLauncher;
@@ -13,14 +13,15 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import React, { useState } from 'react';
16
- import { Drawer, FloatButton } from 'antd';
17
- import { MessageOutlined } from '@ant-design/icons';
18
- function DrawerWrapper({ config, children }) {
19
- const [open, setOpen] = useState(false);
20
- return /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(FloatButton, {
21
- icon: /*#__PURE__*/ React.createElement(MessageOutlined, null),
22
- onClick: ()=>setOpen(true),
23
- tooltip: config?.title ?? 'Chat'
16
+ import { Drawer } from 'antd';
17
+ import DrawerLauncher from './DrawerLauncher.js';
18
+ function DrawerWrapper({ blockId, config, defaultOpen, children, Icon }) {
19
+ const [open, setOpen] = useState(defaultOpen);
20
+ return /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(DrawerLauncher, {
21
+ blockId: blockId,
22
+ config: config,
23
+ Icon: Icon,
24
+ onClick: ()=>setOpen(true)
24
25
  }), /*#__PURE__*/ React.createElement(Drawer, {
25
26
  title: config?.title ?? 'Chat',
26
27
  placement: config?.placement ?? 'right',
@@ -13,10 +13,10 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { DefaultChatTransport } from 'ai';
16
- function createLowdefyChatTransport({ pageId, agentId, conversationId, urlQuery, sharedStateRef }) {
16
+ function createLowdefyChatTransport({ basePath = '', pageId, agentId, conversationId, urlQuery, pathParams, sharedStateRef, sharedStateReadOnlyRef }) {
17
17
  // The server route takes the last path segment as the agentId — module-scoped
18
18
  // agent ids contain '/', so encode the agentId into a single segment.
19
- const base = `/api/agent/${pageId}/${encodeURIComponent(agentId)}`;
19
+ const base = `${basePath}/api/agent/${pageId}/${encodeURIComponent(agentId)}`;
20
20
  const api = conversationId ? `${base}?conversationId=${encodeURIComponent(conversationId)}` : base;
21
21
  return new DefaultChatTransport({
22
22
  api,
@@ -27,8 +27,14 @@ function createLowdefyChatTransport({ pageId, agentId, conversationId, urlQuery,
27
27
  ...urlQuery ? {
28
28
  urlQuery
29
29
  } : {},
30
+ ...pathParams ? {
31
+ pathParams
32
+ } : {},
30
33
  ...sharedState ? {
31
34
  sharedState
35
+ } : {},
36
+ ...sharedStateReadOnlyRef?.current ? {
37
+ sharedStateReadOnly: true
32
38
  } : {}
33
39
  };
34
40
  }
@@ -12,31 +12,14 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import React, { useMemo } from 'react';
15
+ */ import React, { Suspense, lazy, useEffect, useMemo, useState } from 'react';
16
16
  import Markdown from '@ant-design/x-markdown';
17
- import { Actions, CodeHighlighter, FileCard, Mermaid, Sources, ThoughtChain, Think } from '@ant-design/x';
18
- import { DeleteOutlined, ReloadOutlined, RobotOutlined } from '@ant-design/icons';
17
+ import { Actions, FileCard, Sources, ThoughtChain, Think } from '@ant-design/x';
18
+ import { getSafeHref } from '@lowdefy/block-utils';
19
19
  import { getFileCardType, getFileCardIcon, getFileName } from './fileCardUtils.js';
20
20
  import formatToolResult from './formatToolResult.js';
21
21
  import { getToolInfo, partCategory } from './messageParts.js';
22
22
  import ToolApproval from './ToolApproval.js';
23
- // Module-level singleton for the LaTeX marked extension.
24
- // Loaded once on first use — the Latex plugin from @ant-design/x-markdown
25
- // includes KaTeX CSS import and handles $...$ inline and $$...$$ display math.
26
- let latexConfig = null;
27
- function getLatexConfig() {
28
- if (latexConfig) return latexConfig;
29
- try {
30
- // eslint-disable-next-line global-require
31
- const Latex = require('@ant-design/x-markdown/plugins/Latex').default;
32
- latexConfig = {
33
- extensions: Latex()
34
- };
35
- } catch {
36
- latexConfig = {};
37
- }
38
- return latexConfig;
39
- }
40
23
  // Renders a code block as plain text without syntax highlighting or mermaid rendering.
41
24
  function PlainCodeBlock({ children, block, lang }) {
42
25
  if (!block) {
@@ -46,31 +29,97 @@ function PlainCodeBlock({ children, block, lang }) {
46
29
  className: lang ? `language-${lang}` : undefined
47
30
  }, children));
48
31
  }
32
+ // Mermaid and the Prism highlighter are most of AgentChat's weight, and a plain reply
33
+ // uses neither. Deep paths keep them out of the chunk that imports the @ant-design/x
34
+ // barrel; they load the first time a message renders a fence that needs them. A chunk
35
+ // that fails to load (a deploy replaced it) degrades to plain code, not a broken chat.
36
+ function lazyCodeRenderer(load, name) {
37
+ return /*#__PURE__*/ lazy(()=>load().catch((error)=>{
38
+ console.warn(`AgentChat could not load ${name}; showing plain code.`, error);
39
+ return {
40
+ default: function PlainCode({ children, lang }) {
41
+ return /*#__PURE__*/ React.createElement(PlainCodeBlock, {
42
+ block: true,
43
+ lang: lang
44
+ }, children);
45
+ }
46
+ };
47
+ }));
48
+ }
49
+ const Mermaid = lazyCodeRenderer(()=>import('@ant-design/x/es/mermaid/index.js'), 'Mermaid');
50
+ const CodeHighlighter = lazyCodeRenderer(()=>import('@ant-design/x/es/code-highlighter/index.js'), 'CodeHighlighter');
49
51
  // Renders code blocks with syntax highlighting via CodeHighlighter and
50
52
  // mermaid diagrams via the Mermaid component from @ant-design/x.
53
+ // Each lazy renderer sits in its own Suspense, so a first load shows the plain code in
54
+ // place rather than suspending the whole chat.
51
55
  function RichCodeBlock({ renderMermaid, codeHighlighter }) {
52
56
  return function CodeBlock({ children, block, lang }) {
53
57
  if (!block) {
54
58
  return /*#__PURE__*/ React.createElement("code", null, children);
55
59
  }
60
+ const plain = /*#__PURE__*/ React.createElement(PlainCodeBlock, {
61
+ block: true,
62
+ lang: lang
63
+ }, children);
56
64
  if (renderMermaid && lang === 'mermaid') {
57
65
  return /*#__PURE__*/ React.createElement("div", {
58
66
  style: {
59
67
  width: '100%'
60
68
  }
61
- }, /*#__PURE__*/ React.createElement(Mermaid, null, children));
69
+ }, /*#__PURE__*/ React.createElement(Suspense, {
70
+ fallback: plain
71
+ }, /*#__PURE__*/ React.createElement(Mermaid, null, children)));
62
72
  }
63
73
  if (codeHighlighter) {
64
- return /*#__PURE__*/ React.createElement(CodeHighlighter, {
74
+ return /*#__PURE__*/ React.createElement(Suspense, {
75
+ fallback: plain
76
+ }, /*#__PURE__*/ React.createElement(CodeHighlighter, {
65
77
  lang: lang,
66
78
  prismLightMode: false
67
- }, children);
79
+ }, children));
68
80
  }
69
- return /*#__PURE__*/ React.createElement("pre", null, /*#__PURE__*/ React.createElement("code", {
70
- className: lang ? `language-${lang}` : undefined
71
- }, children));
81
+ return plain;
72
82
  };
73
83
  }
84
+ // The LaTeX marked extension (KaTeX and its CSS) loads only for chats that set
85
+ // messageDisplay.renderLatex. Markdown renders without it until it arrives, then re-renders
86
+ // with it. One config object is shared so every bubble passes Markdown the same reference.
87
+ let latexConfig = null;
88
+ let latexConfigPromise = null;
89
+ function loadLatexConfig() {
90
+ if (!latexConfigPromise) {
91
+ latexConfigPromise = import('@ant-design/x-markdown/plugins/Latex/index.js').then(({ default: Latex })=>{
92
+ latexConfig = {
93
+ extensions: Latex()
94
+ };
95
+ return latexConfig;
96
+ }, (error)=>{
97
+ // Cleared so a later chat mount tries again.
98
+ latexConfigPromise = null;
99
+ console.warn('AgentChat could not load LaTeX rendering; showing the source.', error);
100
+ return null;
101
+ });
102
+ }
103
+ return latexConfigPromise;
104
+ }
105
+ function useLatexConfig(renderLatex) {
106
+ const [loadedConfig, setLoadedConfig] = useState(latexConfig);
107
+ useEffect(()=>{
108
+ if (!renderLatex || loadedConfig) return undefined;
109
+ let mounted = true;
110
+ loadLatexConfig().then((config)=>{
111
+ if (mounted && config) setLoadedConfig(config);
112
+ });
113
+ return ()=>{
114
+ mounted = false;
115
+ };
116
+ }, [
117
+ renderLatex,
118
+ loadedConfig
119
+ ]);
120
+ if (!renderLatex) return undefined;
121
+ return loadedConfig ?? undefined;
122
+ }
74
123
  function summarizeToolOutput(output, translate) {
75
124
  if (output === null || output === undefined) return translate('agent.toolResult.completedNoData');
76
125
  if (Array.isArray(output)) {
@@ -115,7 +164,8 @@ function linkText(children) {
115
164
  // new tab for the same reason. Both still report the click, so an app that handles
116
165
  // onLinkClick can show the target in place instead.
117
166
  function MarkdownLink({ Link, onLinkClick }) {
118
- return function MarkdownAnchor({ href, children, ...props }) {
167
+ return function MarkdownAnchor({ href: rawHref, children, ...props }) {
168
+ const href = getSafeHref(rawHref);
119
169
  if (!href) return /*#__PURE__*/ React.createElement("span", props, children);
120
170
  const text = linkText(children);
121
171
  const handleClick = (domEvent)=>onLinkClick?.({
@@ -124,7 +174,7 @@ function MarkdownLink({ Link, onLinkClick }) {
124
174
  domEvent
125
175
  });
126
176
  const external = /^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith('//');
127
- // A fragment cannot be expressed as a pageId link, so it keeps the anchor.
177
+ // A fragment moves within the document, which the router cannot do, so it keeps the anchor.
128
178
  const inApp = !external && href.startsWith('/') && !href.includes('#');
129
179
  if (!inApp || !Link) {
130
180
  return /*#__PURE__*/ React.createElement("a", {
@@ -135,14 +185,9 @@ function MarkdownLink({ Link, onLinkClick }) {
135
185
  onClick: handleClick
136
186
  }, children);
137
187
  }
138
- // `pageId`, not `href`: Link renders a plain anchor for href and url — its new-origin form —
139
- // and only routes client-side for a pageId. Passing the path as href therefore went through
140
- // Link and still reloaded the page, dropping the conversation this exists to preserve.
141
- const [pathname, search = ''] = href.slice(1).split('?');
142
188
  return /*#__PURE__*/ React.createElement(Link, {
143
189
  ...props,
144
- pageId: pathname,
145
- urlQuery: Object.fromEntries(new URLSearchParams(search)),
190
+ url: href,
146
191
  onClick: handleClick
147
192
  }, children);
148
193
  };
@@ -157,7 +202,7 @@ function normalizeActions(actions) {
157
202
  }
158
203
  return actions ?? {};
159
204
  }
160
- function BubbleActions({ actions, textContent, messageId, onFeedback, feedbackValue, onRegenerate, onDelete, translate }) {
205
+ function BubbleActions({ Icon, actions, textContent, messageId, onFeedback, feedbackValue, onRegenerate, onDelete, translate }) {
161
206
  const normalized = normalizeActions(actions);
162
207
  const items = [];
163
208
  if (normalized.copy) {
@@ -185,7 +230,13 @@ function BubbleActions({ actions, textContent, messageId, onFeedback, feedbackVa
185
230
  if (normalized.regenerate) {
186
231
  items.push({
187
232
  key: 'regenerate',
188
- icon: /*#__PURE__*/ React.createElement(ReloadOutlined, null),
233
+ icon: /*#__PURE__*/ React.createElement(Icon, {
234
+ blockId: `${messageId}_regenerate_icon`,
235
+ properties: {
236
+ name: 'refresh',
237
+ title: ''
238
+ }
239
+ }),
189
240
  label: translate('agent.message.regenerate'),
190
241
  onItemClick: ()=>onRegenerate?.({
191
242
  messageId
@@ -195,7 +246,13 @@ function BubbleActions({ actions, textContent, messageId, onFeedback, feedbackVa
195
246
  if (normalized.delete) {
196
247
  items.push({
197
248
  key: 'delete',
198
- icon: /*#__PURE__*/ React.createElement(DeleteOutlined, null),
249
+ icon: /*#__PURE__*/ React.createElement(Icon, {
250
+ blockId: `${messageId}_delete_icon`,
251
+ properties: {
252
+ name: 'delete',
253
+ title: ''
254
+ }
255
+ }),
199
256
  label: translate('agent.message.delete'),
200
257
  danger: true,
201
258
  onItemClick: ()=>onDelete?.({
@@ -223,7 +280,7 @@ function SourcesDisplay({ sourceParts, config, translate }) {
223
280
  expandIconPosition: config?.sourcesDisplay?.expandIconPosition ?? 'end'
224
281
  });
225
282
  }
226
- function MessageBubble({ content, isStreaming, parts, config, addToolApprovalResponse, actions, messageId, onFeedback, feedbackValue, onLinkClick, Link, onRegenerate, onDelete, translate }) {
283
+ function MessageBubble({ Icon, content, isStreaming, parts, config, addToolApprovalResponse, actions, messageId, onFeedback, feedbackValue, onLinkClick, Link, onRegenerate, onDelete, translate }) {
227
284
  const showThoughtChain = config?.showThoughtChain !== false;
228
285
  const showReasoning = config?.showReasoning !== false;
229
286
  const reasoningDisplay = config?.reasoningDisplay ?? 'interleaved';
@@ -231,7 +288,7 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
231
288
  const renderMermaid = config?.renderMermaid !== false;
232
289
  const codeHighlighter = config?.codeHighlighter !== false;
233
290
  const renderLatex = config?.renderLatex ?? false;
234
- const markdownConfig = renderLatex ? getLatexConfig() : undefined;
291
+ const markdownConfig = useLatexConfig(renderLatex);
235
292
  const markdownComponents = useMemo(()=>{
236
293
  const code = !renderMermaid && !codeHighlighter ? PlainCodeBlock : RichCodeBlock({
237
294
  renderMermaid,
@@ -266,6 +323,7 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
266
323
  config: config,
267
324
  translate: translate
268
325
  }), showActions && /*#__PURE__*/ React.createElement(BubbleActions, {
326
+ Icon: Icon,
269
327
  actions: normalizedActions,
270
328
  textContent: content,
271
329
  messageId: messageId,
@@ -464,7 +522,13 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
464
522
  collapsible: true
465
523
  } : {},
466
524
  ...isSubAgent ? {
467
- icon: /*#__PURE__*/ React.createElement(RobotOutlined, null)
525
+ icon: /*#__PURE__*/ React.createElement(Icon, {
526
+ blockId: `${tool.toolCallId}_icon`,
527
+ properties: {
528
+ name: 'bot',
529
+ title: ''
530
+ }
531
+ })
468
532
  } : {},
469
533
  ...status === 'loading' ? {
470
534
  blink: true
@@ -539,6 +603,7 @@ function MessageBubble({ content, isStreaming, parts, config, addToolApprovalRes
539
603
  config: config,
540
604
  translate: translate
541
605
  }), showActions && /*#__PURE__*/ React.createElement(BubbleActions, {
606
+ Icon: Icon,
542
607
  actions: normalizedActions,
543
608
  textContent: allTextContent,
544
609
  messageId: messageId,
@@ -15,7 +15,6 @@
15
15
  */ import React from 'react';
16
16
  import { Bubble, FileCard } from '@ant-design/x';
17
17
  import { Avatar } from 'antd';
18
- import { RobotOutlined, UserOutlined } from '@ant-design/icons';
19
18
  import { getFileCardType, getFileCardIcon, getFileName } from './fileCardUtils.js';
20
19
  import { hasVisibleContent } from './messageParts.js';
21
20
  import MessageBubble from './MessageBubble.js';
@@ -56,7 +55,7 @@ function ThinkingBubbleContent({ bubbleId, config }) {
56
55
  label: label
57
56
  });
58
57
  }
59
- const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messages, isStreaming, config, welcome, onWelcomePromptFill, addToolApprovalResponse, onFeedback, onLinkClick, Link, feedbackValues, onRegenerate, onDelete, onEditMessage, translate }, ref) {
58
+ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ Icon, messages, isStreaming, config, welcome, onWelcomePromptFill, addToolApprovalResponse, onFeedback, onLinkClick, Link, feedbackValues, onRegenerate, onDelete, onEditMessage, translate }, ref) {
60
59
  // Build a lookup map for message parts.
61
60
  // Bubble.List's contentRender callback only receives (content, info) where info.key is the
62
61
  // item key — it does not receive the full message object with its parts array. This map
@@ -154,7 +153,15 @@ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messag
154
153
  placement: 'end',
155
154
  variant: config?.roles?.user?.variant ?? 'filled',
156
155
  shape: config?.roles?.user?.shape ?? 'round',
157
- avatar: roleAvatar(config?.roles?.user, /*#__PURE__*/ React.createElement(UserOutlined, null)),
156
+ styles: config?.roles?.user?.styles,
157
+ classNames: config?.roles?.user?.classNames,
158
+ avatar: roleAvatar(config?.roles?.user, /*#__PURE__*/ React.createElement(Icon, {
159
+ blockId: "agent_chat_user_avatar",
160
+ properties: {
161
+ name: 'user',
162
+ title: ''
163
+ }
164
+ })),
158
165
  header: roleHeader(config?.roles?.user, 'agent.message.userHeader', translate),
159
166
  editable: config?.editableMessages !== false ? {
160
167
  onEditConfirm: (key, newContent)=>{
@@ -200,7 +207,15 @@ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messag
200
207
  style: {
201
208
  maxWidth: '100%'
202
209
  },
203
- avatar: roleAvatar(config?.roles?.assistant, /*#__PURE__*/ React.createElement(RobotOutlined, null)),
210
+ styles: config?.roles?.assistant?.styles,
211
+ classNames: config?.roles?.assistant?.classNames,
212
+ avatar: roleAvatar(config?.roles?.assistant, /*#__PURE__*/ React.createElement(Icon, {
213
+ blockId: "agent_chat_assistant_avatar",
214
+ properties: {
215
+ name: 'bot',
216
+ title: ''
217
+ }
218
+ })),
204
219
  header: roleHeader(config?.roles?.assistant, 'agent.message.assistantHeader', translate),
205
220
  typing: config?.roles?.assistant?.typing ? config.roles.assistant.typing : {
206
221
  effect: 'fade-in'
@@ -208,6 +223,7 @@ const MessageList = /*#__PURE__*/ React.forwardRef(function MessageList({ messag
208
223
  contentRender: (content, info)=>{
209
224
  const parts = partsMap.get(info.key);
210
225
  return /*#__PURE__*/ React.createElement(MessageBubble, {
226
+ Icon: Icon,
211
227
  content: content,
212
228
  isStreaming: isStreaming,
213
229
  parts: parts,
@@ -13,6 +13,7 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import React, { useState } from 'react';
16
+ import { getSafeHref } from '@lowdefy/block-utils';
16
17
  const URL_REGEX = /^https?:\/\/\S+$/;
17
18
  const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})/;
18
19
  function isUrl(value) {
@@ -52,7 +53,7 @@ function formatValue(value, depth, translate) {
52
53
  }
53
54
  if (isUrl(value)) {
54
55
  return /*#__PURE__*/ React.createElement("a", {
55
- href: value,
56
+ href: getSafeHref(value),
56
57
  target: "_blank",
57
58
  rel: "noopener noreferrer",
58
59
  style: {
@@ -14,7 +14,22 @@
14
14
  limitations under the License.
15
15
  */ export default {
16
16
  category: 'display',
17
- icons: [],
17
+ actions: [
18
+ 'Request',
19
+ 'SetState'
20
+ ],
21
+ operators: [
22
+ '_event'
23
+ ],
24
+ icons: [
25
+ 'refresh',
26
+ 'delete',
27
+ 'bot',
28
+ 'message',
29
+ 'user',
30
+ 'attach',
31
+ 'close'
32
+ ],
18
33
  valueType: null,
19
34
  events: {
20
35
  onMessageComplete: 'Trigger when a message finishes streaming.',
@@ -39,6 +54,7 @@
39
54
  regenerate: 'Regenerate the last assistant message. Accepts optional args.messageId to regenerate a specific message.',
40
55
  setMessages: 'Replace the message list. Accepts args.messages array.',
41
56
  sendMessage: 'Send a message programmatically. Accepts args.text string.',
57
+ setInput: 'Fill the composer without sending. Accepts args.text string; anything else empties it.',
42
58
  clearMessages: 'Clear all messages from the chat.',
43
59
  deleteMessage: 'Delete a specific message. Accepts args.messageId string.',
44
60
  stop: 'Stop the current streaming response.',
@@ -0,0 +1,60 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { type } from '@lowdefy/helpers';
16
+ // A reply cut off mid-stream leaves its tool call with no result, and UIMessage validation
17
+ // rejects the whole history for one such part: every later send fails, and a history stored
18
+ // that way fails on every load. Approval states stay; a pending approval is answerable on resume.
19
+ // A client-side tool the block is still running is in the same state, so its call id is in
20
+ // liveToolCallIds and the part is kept: its output is on the way.
21
+ const UNANSWERED = new Set([
22
+ 'input-streaming',
23
+ 'input-available'
24
+ ]);
25
+ function isDeadToolPart({ part, liveToolCallIds }) {
26
+ const partType = part?.type;
27
+ const isTool = partType === 'dynamic-tool' || type.isString(partType) && partType.startsWith('tool-');
28
+ return isTool && UNANSWERED.has(part.state) && !liveToolCallIds.has(part.toolCallId);
29
+ }
30
+ function replayableMessages({ messages, liveToolCallIds = new Set() }) {
31
+ const list = messages ?? [];
32
+ let changed = false;
33
+ const result = [];
34
+ for (const message of list){
35
+ if (!type.isArray(message?.parts)) {
36
+ result.push(message);
37
+ continue;
38
+ }
39
+ const parts = message.parts.filter((part)=>!isDeadToolPart({
40
+ part,
41
+ liveToolCallIds
42
+ }));
43
+ // Only a step marker left is the empty shell onError already drops.
44
+ if (!parts.some((part)=>part?.type !== 'step-start')) {
45
+ changed = true;
46
+ continue;
47
+ }
48
+ if (parts.length === message.parts.length) {
49
+ result.push(message);
50
+ continue;
51
+ }
52
+ changed = true;
53
+ result.push({
54
+ ...message,
55
+ parts
56
+ });
57
+ }
58
+ return changed ? result : list;
59
+ }
60
+ export default replayableMessages;
@@ -137,6 +137,14 @@
137
137
  "shape": {
138
138
  "type": "string",
139
139
  "enum": ["default", "round", "corner"]
140
+ },
141
+ "styles": {
142
+ "type": "object",
143
+ "description": "Inline styles per bubble part: root, content, body, header, footer, avatar, extra. The way to recolour a bubble: the filled and outlined colours come from the theme's colorPrimary and are not reachable from a stylesheet."
144
+ },
145
+ "classNames": {
146
+ "type": "object",
147
+ "description": "Class names per bubble part, same keys as styles."
140
148
  }
141
149
  }
142
150
  },
@@ -152,6 +160,14 @@
152
160
  "shape": {
153
161
  "type": "string",
154
162
  "enum": ["default", "round", "corner"]
163
+ },
164
+ "styles": {
165
+ "type": "object",
166
+ "description": "Inline styles per bubble part: root, content, body, header, footer, avatar, extra."
167
+ },
168
+ "classNames": {
169
+ "type": "object",
170
+ "description": "Class names per bubble part, same keys as styles."
155
171
  }
156
172
  }
157
173
  }
@@ -175,9 +191,14 @@
175
191
  "description": "URL query parameters to send with each agent request. Available server-side in onFinish hook payload and via _payload operator in agent properties."
176
192
  },
177
193
  "sharedState": {
178
- "description": "When set to a non-empty object, the object is sent to the agent each turn as read context and the agent can write to any top-level key via the update-page-state tool. Typically set to `{ _state: true }` to expose the whole page state, or a curated shape like `{ legal: _state.legal_name }`.",
194
+ "description": "When set to a non-empty object, the object is sent to the agent each turn as read context and the agent can write to any top-level key via the update-page-state tool (unless `sharedStateReadOnly` is true). Typically set to `{ _state: true }` to expose the whole page state, or a curated shape like `{ legal: _state.legal_name }`.",
179
195
  "type": "object"
180
196
  },
197
+ "sharedStateReadOnly": {
198
+ "type": "boolean",
199
+ "default": false,
200
+ "description": "When true, `sharedState` is sent to the agent as read-only context: the update-page-state tool is not offered, so the agent cannot write page state."
201
+ },
181
202
  "sender": {
182
203
  "type": "object",
183
204
  "properties": {
@@ -188,6 +209,14 @@
188
209
  "default": "enter"
189
210
  },
190
211
  "allowSpeech": { "type": "boolean", "default": false },
212
+ "styles": {
213
+ "type": "object",
214
+ "description": "Inline styles per composer part: root, prefix, input, suffix, footer, switch, content. The way to recolour the composer border, which the theme sets with a precedence no page stylesheet beats."
215
+ },
216
+ "classNames": {
217
+ "type": "object",
218
+ "description": "Class names per composer part, same keys as styles."
219
+ },
191
220
  "attachments": {
192
221
  "type": "object",
193
222
  "description": "File attachments on the composer. When enabled, files can be picked with the paperclip, pasted from the clipboard (e.g. a screenshot) or dragged onto the composer; accept and maxSize apply to all three.",
@@ -203,7 +232,7 @@
203
232
  },
204
233
  "uploadPolicyRequestId": {
205
234
  "type": "string",
206
- "description": "Id of an upload-policy request (e.g. AwsS3PresignedPostPolicy, GcsSignedPostPolicy, AzureBlobUploadSas). When set, files are uploaded to storage and their URLs are sent to the LLM instead of base64 data."
235
+ "description": "Id of an upload-policy request (e.g. AwsS3PresignedPostPolicy, GcsSignedPostPolicy, AzureBlobUploadSas). When set, files are uploaded to storage and their URLs are sent to the LLM instead of base64 data. Each uploaded file part keeps its storage key as providerMetadata.lowdefy.key, which a tool endpoint reads with _agent: files."
207
236
  },
208
237
  "downloadPolicyRequestId": {
209
238
  "type": "string",
@@ -243,6 +272,11 @@
243
272
  "title": { "type": "string" }
244
273
  }
245
274
  },
275
+ "loading": {
276
+ "type": "boolean",
277
+ "default": false,
278
+ "description": "The app is still loading this conversation's transcript. While true, a skeleton replaces the message area, suggestions are hidden and the composer is disabled, so nothing can be sent into a conversation whose messages have not synced yet."
279
+ },
246
280
  "suggestions": {
247
281
  "type": "array",
248
282
  "items": {