@lobehub/lobehub 2.0.10 → 2.0.11

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 (94) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/Dockerfile +44 -52
  3. package/changelog/v2.json +9 -0
  4. package/locales/ar/chat.json +4 -0
  5. package/locales/ar/models.json +65 -0
  6. package/locales/bg-BG/chat.json +4 -0
  7. package/locales/bg-BG/models.json +10 -0
  8. package/locales/de-DE/chat.json +4 -0
  9. package/locales/de-DE/models.json +41 -0
  10. package/locales/en-US/chat.json +4 -0
  11. package/locales/es-ES/chat.json +4 -0
  12. package/locales/es-ES/models.json +50 -0
  13. package/locales/fa-IR/chat.json +4 -0
  14. package/locales/fa-IR/models.json +39 -0
  15. package/locales/fr-FR/chat.json +4 -0
  16. package/locales/fr-FR/models.json +9 -0
  17. package/locales/it-IT/chat.json +4 -0
  18. package/locales/it-IT/models.json +62 -0
  19. package/locales/ja-JP/chat.json +4 -0
  20. package/locales/ja-JP/models.json +40 -0
  21. package/locales/ko-KR/chat.json +4 -0
  22. package/locales/ko-KR/models.json +31 -0
  23. package/locales/nl-NL/chat.json +4 -0
  24. package/locales/nl-NL/models.json +52 -0
  25. package/locales/pl-PL/chat.json +4 -0
  26. package/locales/pl-PL/models.json +43 -0
  27. package/locales/pt-BR/chat.json +4 -0
  28. package/locales/pt-BR/models.json +92 -0
  29. package/locales/ru-RU/chat.json +4 -0
  30. package/locales/ru-RU/models.json +34 -0
  31. package/locales/tr-TR/chat.json +4 -0
  32. package/locales/tr-TR/models.json +55 -0
  33. package/locales/vi-VN/chat.json +4 -0
  34. package/locales/vi-VN/models.json +31 -0
  35. package/locales/zh-CN/chat.json +4 -0
  36. package/locales/zh-TW/chat.json +4 -0
  37. package/package.json +1 -1
  38. package/packages/agent-runtime/src/groupOrchestration/GroupOrchestrationSupervisor.ts +18 -1
  39. package/packages/agent-runtime/src/groupOrchestration/__tests__/GroupOrchestrationSupervisor.test.ts +76 -5
  40. package/packages/agent-runtime/src/groupOrchestration/types.ts +3 -3
  41. package/packages/builtin-tool-group-management/src/client/Intervention/ExecuteTask.tsx +11 -11
  42. package/packages/builtin-tool-group-management/src/client/Intervention/ExecuteTasks.tsx +78 -79
  43. package/packages/builtin-tool-group-management/src/client/Render/ExecuteTask/index.tsx +3 -3
  44. package/packages/builtin-tool-group-management/src/client/Render/ExecuteTasks/index.tsx +61 -63
  45. package/packages/builtin-tool-group-management/src/client/Streaming/ExecuteTask/index.tsx +3 -3
  46. package/packages/builtin-tool-group-management/src/executor.test.ts +7 -9
  47. package/packages/builtin-tool-group-management/src/executor.ts +3 -3
  48. package/packages/builtin-tool-group-management/src/manifest.ts +49 -50
  49. package/packages/builtin-tool-group-management/src/systemRole.ts +153 -5
  50. package/packages/builtin-tool-group-management/src/types.ts +3 -2
  51. package/packages/builtin-tool-gtd/src/systemRole.ts +4 -4
  52. package/packages/context-engine/src/processors/TasksFlatten.ts +7 -5
  53. package/packages/context-engine/src/processors/__tests__/TasksFlatten.test.ts +164 -0
  54. package/packages/conversation-flow/src/__tests__/fixtures/inputs/agentGroup/index.ts +4 -0
  55. package/packages/conversation-flow/src/__tests__/fixtures/inputs/agentGroup/supervisor-after-multi-tasks.json +91 -0
  56. package/packages/conversation-flow/src/__tests__/fixtures/inputs/agentGroup/supervisor-content-only.json +74 -0
  57. package/packages/conversation-flow/src/__tests__/parse.test.ts +37 -0
  58. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +70 -4
  59. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +147 -0
  60. package/packages/types/src/message/ui/chat.ts +2 -0
  61. package/packages/types/src/tool/builtin.ts +5 -5
  62. package/src/features/Conversation/ChatItem/components/Title.tsx +1 -1
  63. package/src/features/Conversation/ChatList/index.tsx +0 -1
  64. package/src/features/Conversation/Messages/GroupTasks/TaskItem/ClientTaskItem.tsx +183 -0
  65. package/src/features/Conversation/Messages/GroupTasks/TaskItem/ServerTaskItem.tsx +94 -0
  66. package/src/features/Conversation/Messages/GroupTasks/TaskItem/TaskTitle.tsx +177 -0
  67. package/src/features/Conversation/Messages/GroupTasks/TaskItem/index.tsx +26 -0
  68. package/src/features/Conversation/Messages/GroupTasks/TaskItem/useClientTaskStats.ts +93 -0
  69. package/src/features/Conversation/Messages/GroupTasks/index.tsx +151 -0
  70. package/src/features/Conversation/Messages/Supervisor/index.tsx +7 -1
  71. package/src/features/Conversation/Messages/Task/ClientTaskDetail/index.tsx +72 -91
  72. package/src/features/Conversation/Messages/Task/TaskDetailPanel/StatusContent.tsx +46 -17
  73. package/src/features/Conversation/Messages/Tasks/TaskItem/ClientTaskItem.tsx +9 -24
  74. package/src/features/Conversation/Messages/Tasks/TaskItem/ServerTaskItem.tsx +18 -38
  75. package/src/features/Conversation/Messages/Tasks/shared/ErrorState.tsx +45 -2
  76. package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +16 -1
  77. package/src/features/Conversation/Messages/Tasks/shared/TaskContent.tsx +68 -0
  78. package/src/features/Conversation/Messages/Tasks/shared/TaskMessages.tsx +383 -0
  79. package/src/features/Conversation/Messages/Tasks/shared/index.ts +4 -0
  80. package/src/features/Conversation/Messages/Tasks/shared/useTaskPolling.ts +48 -0
  81. package/src/features/Conversation/Messages/index.tsx +5 -0
  82. package/src/locales/default/chat.ts +4 -0
  83. package/src/server/modules/AgentRuntime/RuntimeExecutors.ts +4 -0
  84. package/src/server/modules/AgentRuntime/__tests__/RuntimeExecutors.test.ts +106 -1
  85. package/src/server/services/aiAgent/__tests__/execAgent.threadId.test.ts +2 -2
  86. package/src/server/utils/truncateToolResult.ts +1 -4
  87. package/src/store/chat/agents/GroupOrchestration/__tests__/batch-exec-async-tasks.test.ts +15 -15
  88. package/src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.ts +22 -15
  89. package/src/store/chat/agents/__tests__/createAgentExecutors/exec-tasks.test.ts +21 -10
  90. package/src/store/chat/agents/createAgentExecutors.ts +2 -0
  91. package/src/store/chat/slices/aiAgent/actions/groupOrchestration.ts +10 -7
  92. package/src/features/Conversation/Messages/Task/ClientTaskDetail/CompletedState.tsx +0 -108
  93. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InstructionAccordion.tsx +0 -63
  94. package/src/features/Conversation/Messages/Task/ClientTaskDetail/ProcessingState.tsx +0 -123
@@ -2,12 +2,14 @@
2
2
 
3
3
  import { Flexbox, Text } from '@lobehub/ui';
4
4
  import { createStaticStyles, keyframes } from 'antd-style';
5
- import { memo } from 'react';
5
+ import { memo, useEffect, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
9
9
  import { shinyTextStyles } from '@/styles';
10
10
 
11
+ import { formatElapsedTime } from './utils';
12
+
11
13
  const shimmer = keyframes`
12
14
  0% {
13
15
  transform: translateX(-100%);
@@ -48,6 +50,18 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
48
50
 
49
51
  const InitializingState = memo(() => {
50
52
  const { t } = useTranslation('chat');
53
+ const [elapsedTime, setElapsedTime] = useState(0);
54
+
55
+ // Timer for updating elapsed time every second
56
+ useEffect(() => {
57
+ const startTime = Date.now();
58
+
59
+ const timer = setInterval(() => {
60
+ setElapsedTime(Date.now() - startTime);
61
+ }, 1000);
62
+
63
+ return () => clearInterval(timer);
64
+ }, []);
51
65
 
52
66
  return (
53
67
  <Flexbox className={styles.container} gap={12}>
@@ -56,6 +70,7 @@ const InitializingState = memo(() => {
56
70
  <Text className={shinyTextStyles.shinyText} weight={500}>
57
71
  {t('task.status.initializing')}
58
72
  </Text>
73
+ <Text type="secondary">({formatElapsedTime(elapsedTime)})</Text>
59
74
  </Flexbox>
60
75
  </Flexbox>
61
76
  );
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+
3
+ import { ThreadStatus } from '@lobechat/types';
4
+ import type { TaskDetail, UIChatMessage } from '@lobechat/types';
5
+ import { Flexbox, Text } from '@lobehub/ui';
6
+ import { memo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+
9
+ import BubblesLoading from '@/components/BubblesLoading';
10
+
11
+ import ErrorState from './ErrorState';
12
+ import InitializingState from './InitializingState';
13
+ import TaskMessages from './TaskMessages';
14
+ import { useTaskPolling } from './useTaskPolling';
15
+
16
+ export interface TaskContentProps {
17
+ id: string;
18
+ isError: boolean;
19
+ messages: UIChatMessage[] | undefined;
20
+ status: ThreadStatus | undefined;
21
+ taskDetail: TaskDetail | undefined;
22
+ threadId: string | undefined;
23
+ }
24
+
25
+ const TaskContent = memo<TaskContentProps>(
26
+ ({ id, threadId, status, messages, taskDetail, isError }) => {
27
+ const { t } = useTranslation('chat');
28
+ const { isProcessing } = useTaskPolling({
29
+ messageId: id,
30
+ status,
31
+ threadId,
32
+ });
33
+
34
+ // No messages yet
35
+ if (!messages || messages.length === 0) {
36
+ // Still processing: show full initializing state
37
+ if (isProcessing) {
38
+ return <InitializingState />;
39
+ }
40
+
41
+ // Already completed but loading messages: show simple loading
42
+ return (
43
+ <Flexbox align="center" gap={4} horizontal>
44
+ <BubblesLoading />
45
+ <Text type="secondary">{t('task.status.fetchingDetails')}</Text>
46
+ </Flexbox>
47
+ );
48
+ }
49
+
50
+ return (
51
+ <>
52
+ <TaskMessages
53
+ duration={taskDetail?.duration}
54
+ isProcessing={isProcessing}
55
+ messages={messages}
56
+ startTime={taskDetail?.startedAt ? new Date(taskDetail.startedAt).getTime() : undefined}
57
+ totalCost={taskDetail?.totalCost}
58
+ />
59
+ {/* Error states: Failed, Cancel */}
60
+ {isError && taskDetail && <ErrorState taskDetail={taskDetail} />}
61
+ </>
62
+ );
63
+ },
64
+ );
65
+
66
+ TaskContent.displayName = 'TaskContent';
67
+
68
+ export default TaskContent;
@@ -0,0 +1,383 @@
1
+ 'use client';
2
+
3
+ import { type AssistantContentBlock, type UIChatMessage } from '@lobechat/types';
4
+ import {
5
+ Accordion,
6
+ AccordionItem,
7
+ Block,
8
+ Flexbox,
9
+ Icon,
10
+ Markdown,
11
+ ScrollShadow,
12
+ Text,
13
+ } from '@lobehub/ui';
14
+ import { createStaticStyles, cssVar } from 'antd-style';
15
+ import { ScrollText, Workflow } from 'lucide-react';
16
+ import { type RefObject, memo, useEffect, useMemo, useState } from 'react';
17
+ import { useTranslation } from 'react-i18next';
18
+
19
+ import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
20
+ import { useAutoScroll } from '@/hooks/useAutoScroll';
21
+
22
+ import ContentBlock from '../../AssistantGroup/components/ContentBlock';
23
+ import Usage from '../../components/Extras/Usage';
24
+ import AnimatedNumber from '../../components/Extras/Usage/UsageDetail/AnimatedNumber';
25
+ import { accumulateUsage, formatDuration, formatElapsedTime } from './utils';
26
+
27
+ const styles = createStaticStyles(({ css }) => ({
28
+ contentScroll: css`
29
+ max-height: min(50vh, 300px);
30
+ `,
31
+ instructionContent: css`
32
+ overflow: auto;
33
+ max-height: 300px;
34
+ `,
35
+ }));
36
+
37
+ /**
38
+ * InstructionAccordion - Shows the task instruction in a collapsible accordion
39
+ */
40
+ const InstructionAccordion = memo<{ childrenCount: number; instruction: string }>(
41
+ ({ instruction, childrenCount }) => {
42
+ const { t } = useTranslation('chat');
43
+
44
+ // Auto-collapse instruction when children count exceeds threshold
45
+ const [expandedKeys, setExpandedKeys] = useState<string[]>(['instruction']);
46
+
47
+ useEffect(() => {
48
+ if (childrenCount > 1) {
49
+ setExpandedKeys([]);
50
+ }
51
+ }, [childrenCount > 1]);
52
+
53
+ return (
54
+ <Accordion
55
+ expandedKeys={expandedKeys}
56
+ gap={8}
57
+ onExpandedChange={(keys) => setExpandedKeys(keys as string[])}
58
+ >
59
+ <AccordionItem
60
+ itemKey="instruction"
61
+ paddingBlock={4}
62
+ paddingInline={4}
63
+ title={
64
+ <Flexbox align="center" gap={8} horizontal>
65
+ <Block
66
+ align="center"
67
+ flex="none"
68
+ gap={4}
69
+ height={24}
70
+ horizontal
71
+ justify="center"
72
+ style={{ fontSize: 12 }}
73
+ variant="outlined"
74
+ width={24}
75
+ >
76
+ <Icon color={cssVar.colorTextSecondary} icon={ScrollText} />
77
+ </Block>
78
+ <Text as="span" type="secondary">
79
+ {t('task.instruction')}
80
+ </Text>
81
+ </Flexbox>
82
+ }
83
+ >
84
+ <Block
85
+ className={styles.instructionContent}
86
+ padding={12}
87
+ style={{ marginBlock: 8 }}
88
+ variant={'outlined'}
89
+ >
90
+ <Markdown variant={'chat'}>{instruction}</Markdown>
91
+ </Block>
92
+ </AccordionItem>
93
+ </Accordion>
94
+ );
95
+ },
96
+ );
97
+
98
+ InstructionAccordion.displayName = 'InstructionAccordion';
99
+
100
+ interface TaskMessagesProps {
101
+ /**
102
+ * Task duration in ms (for completed state)
103
+ */
104
+ duration?: number;
105
+ /**
106
+ * Whether the task is currently processing
107
+ */
108
+ isProcessing?: boolean;
109
+ /**
110
+ * Messages from task execution (parsed by conversation-flow)
111
+ * Will extract assistantGroup.children as blocks for rendering
112
+ */
113
+ messages: UIChatMessage[];
114
+ /**
115
+ * Model name for usage display
116
+ */
117
+ model?: string;
118
+ /**
119
+ * Provider name for usage display
120
+ */
121
+ provider?: string;
122
+ /**
123
+ * Task start time for elapsed time calculation
124
+ */
125
+ startTime?: number;
126
+ /**
127
+ * Total cost (for completed state)
128
+ */
129
+ totalCost?: number;
130
+ }
131
+
132
+ /**
133
+ * Processing state - shows all blocks with loading indicator
134
+ */
135
+ const ProcessingView = memo<{
136
+ accumulatedUsage: { cost?: number; totalTokens?: number };
137
+ assistantId: string;
138
+ blocks: AssistantContentBlock[];
139
+ model?: string;
140
+ provider?: string;
141
+ startTime?: number;
142
+ totalToolCalls: number;
143
+ }>(({ blocks, assistantId, startTime, model, provider, totalToolCalls, accumulatedUsage }) => {
144
+ const { t } = useTranslation('chat');
145
+ const [elapsedTime, setElapsedTime] = useState(0);
146
+ const { ref, handleScroll } = useAutoScroll<HTMLDivElement>({
147
+ deps: [blocks],
148
+ enabled: true,
149
+ });
150
+
151
+ // Calculate initial elapsed time
152
+ useEffect(() => {
153
+ if (startTime) {
154
+ setElapsedTime(Math.max(0, Date.now() - startTime));
155
+ }
156
+ }, [startTime]);
157
+
158
+ // Timer for updating elapsed time every second
159
+ useEffect(() => {
160
+ if (!startTime) return;
161
+
162
+ const timer = setInterval(() => {
163
+ setElapsedTime(Math.max(0, Date.now() - startTime));
164
+ }, 1000);
165
+
166
+ return () => clearInterval(timer);
167
+ }, [startTime]);
168
+
169
+ return (
170
+ <Flexbox gap={8}>
171
+ <Flexbox align="center" gap={8} horizontal paddingInline={4}>
172
+ <Block
173
+ align="center"
174
+ flex="none"
175
+ gap={4}
176
+ height={24}
177
+ horizontal
178
+ justify="center"
179
+ style={{ fontSize: 12 }}
180
+ variant="outlined"
181
+ width={24}
182
+ >
183
+ <NeuralNetworkLoading size={16} />
184
+ </Block>
185
+ <Flexbox align="center" gap={4} horizontal>
186
+ <Text as="span" type="secondary" weight={500}>
187
+ <AnimatedNumber
188
+ duration={500}
189
+ formatter={(v) => Math.round(v).toString()}
190
+ value={totalToolCalls}
191
+ />
192
+ </Text>
193
+ <Text as="span" type="secondary">
194
+ {t('task.metrics.toolCallsShort')}
195
+ </Text>
196
+ {startTime && (
197
+ <Text as="span" type="secondary">
198
+ ({formatElapsedTime(elapsedTime)})
199
+ </Text>
200
+ )}
201
+ </Flexbox>
202
+ </Flexbox>
203
+ <ScrollShadow
204
+ className={styles.contentScroll}
205
+ offset={12}
206
+ onScroll={handleScroll}
207
+ ref={ref as RefObject<HTMLDivElement>}
208
+ size={8}
209
+ >
210
+ <Flexbox gap={8}>
211
+ {blocks.map((block) => (
212
+ <ContentBlock {...block} assistantId={assistantId} disableEditing key={block.id} />
213
+ ))}
214
+ </Flexbox>
215
+ </ScrollShadow>
216
+
217
+ {/* Usage display */}
218
+ {model && provider && <Usage model={model} provider={provider} usage={accumulatedUsage} />}
219
+ </Flexbox>
220
+ );
221
+ });
222
+
223
+ ProcessingView.displayName = 'ProcessingView';
224
+
225
+ /**
226
+ * Completed state - shows intermediate steps in accordion, final result visible
227
+ */
228
+ const CompletedView = memo<{
229
+ assistantId: string;
230
+ blocks: AssistantContentBlock[];
231
+ duration?: number;
232
+ model?: string;
233
+ provider?: string;
234
+ totalCost?: number;
235
+ totalTokens?: number;
236
+ totalToolCalls: number;
237
+ }>(({ blocks, assistantId, duration, totalToolCalls, model, provider, totalTokens, totalCost }) => {
238
+ const { t } = useTranslation('chat');
239
+
240
+ // Split blocks: intermediate steps (all but last) and final result (last)
241
+ const { intermediateBlocks, finalBlock } = useMemo(() => {
242
+ if (blocks.length === 0) return { finalBlock: null, intermediateBlocks: [] };
243
+ if (blocks.length === 1) return { finalBlock: blocks[0], intermediateBlocks: [] };
244
+
245
+ return {
246
+ finalBlock: blocks.at(-1)!,
247
+ intermediateBlocks: blocks.slice(0, -1),
248
+ };
249
+ }, [blocks]);
250
+
251
+ if (!finalBlock) return null;
252
+
253
+ const title = (
254
+ <Flexbox align="center" gap={8} horizontal>
255
+ <Block
256
+ align="center"
257
+ flex="none"
258
+ gap={4}
259
+ height={24}
260
+ horizontal
261
+ justify="center"
262
+ style={{ fontSize: 12 }}
263
+ variant="outlined"
264
+ width={24}
265
+ >
266
+ <Icon color={cssVar.colorTextSecondary} icon={Workflow} />
267
+ </Block>
268
+ <Flexbox align="center" gap={4} horizontal>
269
+ <Text as="span" type="secondary" weight={500}>
270
+ {totalToolCalls}
271
+ </Text>
272
+ <Text as="span" type="secondary">
273
+ {t('task.metrics.toolCallsShort')}
274
+ </Text>
275
+ {/* Duration display */}
276
+ {duration && (
277
+ <Text as="span" type="secondary">
278
+ {t('task.metrics.duration', { duration: formatDuration(duration) })}
279
+ </Text>
280
+ )}
281
+ </Flexbox>
282
+ </Flexbox>
283
+ );
284
+
285
+ return (
286
+ <Flexbox gap={8}>
287
+ {/* Intermediate steps - collapsed by default */}
288
+ {intermediateBlocks.length > 0 && (
289
+ <Accordion defaultExpandedKeys={[]} gap={8}>
290
+ <AccordionItem itemKey="intermediate" paddingBlock={4} paddingInline={4} title={title}>
291
+ <Flexbox gap={8} paddingInline={4} style={{ marginTop: 8 }}>
292
+ {intermediateBlocks.map((block) => (
293
+ <ContentBlock {...block} assistantId={assistantId} disableEditing key={block.id} />
294
+ ))}
295
+ </Flexbox>
296
+ </AccordionItem>
297
+ </Accordion>
298
+ )}
299
+
300
+ {/* Final result - always visible */}
301
+ <ContentBlock {...finalBlock} assistantId={assistantId} disableEditing />
302
+
303
+ {/* Usage display */}
304
+ {model && provider && (
305
+ <Usage model={model} provider={provider} usage={{ cost: totalCost, totalTokens }} />
306
+ )}
307
+ </Flexbox>
308
+ );
309
+ });
310
+
311
+ CompletedView.displayName = 'CompletedView';
312
+
313
+ /**
314
+ * TaskMessages - Renders task execution messages (blocks) for both processing and completed states
315
+ *
316
+ * Extracts assistantGroup.children (blocks) from messages and renders them:
317
+ * - Processing: Shows all blocks with loading indicator and real-time updates
318
+ * - Completed: Shows intermediate steps in accordion, final result always visible
319
+ */
320
+ const TaskMessages = memo<TaskMessagesProps>(
321
+ ({ messages, isProcessing = false, startTime, duration, model, provider, totalCost }) => {
322
+ // Extract blocks and instruction from messages
323
+ const { blocks, assistantId, instruction } = useMemo(() => {
324
+ if (!messages || messages.length === 0)
325
+ return { assistantId: '', blocks: [], instruction: undefined };
326
+
327
+ const assistantGroupMessage = messages.find((item) => item.role === 'assistantGroup');
328
+ const userMessage = messages.find((item) => item.role === 'user');
329
+
330
+ return {
331
+ assistantId: assistantGroupMessage?.id ?? '',
332
+ blocks: assistantGroupMessage?.children ?? [],
333
+ instruction: userMessage?.content,
334
+ };
335
+ }, [messages]);
336
+
337
+ // Calculate total tool calls
338
+ const totalToolCalls = useMemo(
339
+ () => blocks.reduce((sum, block) => sum + (block.tools?.length || 0), 0),
340
+ [blocks],
341
+ );
342
+
343
+ // Accumulate usage from all blocks
344
+ const accumulatedUsage = useMemo(() => accumulateUsage(blocks), [blocks]);
345
+
346
+ return (
347
+ <Flexbox gap={4}>
348
+ {/* Instruction accordion */}
349
+ {instruction && (
350
+ <InstructionAccordion childrenCount={blocks.length} instruction={instruction} />
351
+ )}
352
+
353
+ {/* Processing or Completed view */}
354
+ {isProcessing ? (
355
+ <ProcessingView
356
+ accumulatedUsage={accumulatedUsage}
357
+ assistantId={assistantId}
358
+ blocks={blocks}
359
+ model={model}
360
+ provider={provider}
361
+ startTime={startTime}
362
+ totalToolCalls={totalToolCalls}
363
+ />
364
+ ) : (
365
+ <CompletedView
366
+ assistantId={assistantId}
367
+ blocks={blocks}
368
+ duration={duration}
369
+ model={model}
370
+ provider={provider}
371
+ totalCost={totalCost}
372
+ totalTokens={accumulatedUsage.totalTokens}
373
+ totalToolCalls={totalToolCalls}
374
+ />
375
+ )}
376
+ </Flexbox>
377
+ );
378
+ },
379
+ );
380
+
381
+ TaskMessages.displayName = 'TaskMessages';
382
+
383
+ export default TaskMessages;
@@ -5,4 +5,8 @@ export { default as ErrorState } from './ErrorState';
5
5
  export { default as InitializingState } from './InitializingState';
6
6
  export type { ProcessingStateVariant } from './ProcessingState';
7
7
  export { default as ProcessingState } from './ProcessingState';
8
+ export type { TaskContentProps } from './TaskContent';
9
+ export { default as TaskContent } from './TaskContent';
10
+ export { default as TaskMessages } from './TaskMessages';
11
+ export * from './useTaskPolling';
8
12
  export * from './utils';
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+
3
+ import { ThreadStatus } from '@lobechat/types';
4
+ import { useEffect, useState } from 'react';
5
+
6
+ import { useChatStore } from '@/store/chat';
7
+
8
+ import { isProcessingStatus } from './utils';
9
+
10
+ interface UseTaskPollingParams {
11
+ messageId: string;
12
+ status: ThreadStatus | undefined;
13
+ threadId: string | undefined;
14
+ }
15
+
16
+ export const useTaskPolling = ({ messageId, threadId, status }: UseTaskPollingParams) => {
17
+ const isProcessing = isProcessingStatus(status);
18
+ const [hasFetched, setHasFetched] = useState(false);
19
+
20
+ const [useEnablePollingTaskStatus, operations] = useChatStore((s) => [
21
+ s.useEnablePollingTaskStatus,
22
+ s.operations,
23
+ ]);
24
+
25
+ // Check if exec_async_task is already polling for this message
26
+ const hasActiveOperationPolling = Object.values(operations).some(
27
+ (op) =>
28
+ op.status === 'running' &&
29
+ op.type === 'execAgentRuntime' &&
30
+ op.context?.messageId === messageId,
31
+ );
32
+
33
+ // Enable polling when:
34
+ // 1. Has threadId
35
+ // 2. Not already being polled by an active operation
36
+ // 3. Either hasn't fetched yet (initial fetch) or is still processing (continuous polling)
37
+ const shouldPoll = !!threadId && !hasActiveOperationPolling && (!hasFetched || isProcessing);
38
+ const { data } = useEnablePollingTaskStatus(threadId, messageId, shouldPoll);
39
+
40
+ // Mark as fetched when we get data
41
+ useEffect(() => {
42
+ if (data?.taskDetail && !hasFetched) {
43
+ setHasFetched(true);
44
+ }
45
+ }, [data?.taskDetail, hasFetched]);
46
+
47
+ return { isProcessing };
48
+ };
@@ -15,6 +15,7 @@ import AgentCouncilMessage from './AgentCouncil';
15
15
  import AssistantMessage from './Assistant';
16
16
  import AssistantGroupMessage from './AssistantGroup';
17
17
  import CompressedGroupMessage from './CompressedGroup';
18
+ import GroupTasksMessage from './GroupTasks';
18
19
  import SupervisorMessage from './Supervisor';
19
20
  import TaskMessage from './Task';
20
21
  import TasksMessage from './Tasks';
@@ -159,6 +160,10 @@ const MessageItem = memo<MessageItemProps>(
159
160
  return <TasksMessage id={id} index={index} />;
160
161
  }
161
162
 
163
+ case 'groupTasks': {
164
+ return <GroupTasksMessage id={id} index={index} />;
165
+ }
166
+
162
167
  case 'agentCouncil': {
163
168
  return <AgentCouncilMessage id={id} index={index} />;
164
169
  }
@@ -369,6 +369,9 @@ export default {
369
369
  'task.activity.toolCalling': 'Calling {{toolName}}...',
370
370
  'task.activity.toolResult': '{{toolName}} result received',
371
371
  'task.batchTasks': '{{count}} Batch Subtasks',
372
+ 'task.groupTasks': '{{count}} Parallel Tasks',
373
+ 'task.groupTasksTitle': '{{agents}} and {{count}} agents tasks',
374
+ 'task.groupTasksTitleSimple': '{{agents}} {{count}} tasks',
372
375
  'task.instruction': 'Task Instruction',
373
376
  'task.intermediateSteps': '{{count}} intermediate steps',
374
377
  'task.metrics.duration': '(took {{duration}})',
@@ -376,6 +379,7 @@ export default {
376
379
  'task.metrics.toolCallsShort': 'skill uses',
377
380
  'task.status.cancelled': 'Task Cancelled',
378
381
  'task.status.failed': 'Task Failed',
382
+ 'task.status.fetchingDetails': 'Fetching details...',
379
383
  'task.status.initializing': 'Initializing task...',
380
384
  'task.subtask': 'Subtask',
381
385
  'thread.divider': 'Subtopic',
@@ -753,7 +753,11 @@ export const createRuntimeExecutors = (
753
753
  const newState = structuredClone(state);
754
754
 
755
755
  // Query latest messages from database
756
+ // Must pass agentId to ensure correct query scope, otherwise when topicId is undefined,
757
+ // the query will use isNull(topicId) condition which won't find messages with actual topicId
756
758
  const latestMessages = await ctx.messageModel.query({
759
+ agentId: state.metadata?.agentId,
760
+ threadId: state.metadata?.threadId,
757
761
  topicId: state.metadata?.topicId,
758
762
  });
759
763