@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
@@ -0,0 +1,183 @@
1
+ 'use client';
2
+
3
+ import { AccordionItem, Block, Text } from '@lobehub/ui';
4
+ import { memo, useMemo, useState } from 'react';
5
+
6
+ import { useAgentGroupStore } from '@/store/agentGroup';
7
+ import { agentGroupSelectors } from '@/store/agentGroup/selectors';
8
+ import { useChatStore } from '@/store/chat';
9
+ import { displayMessageSelectors } from '@/store/chat/selectors';
10
+ import { messageMapKey } from '@/store/chat/utils/messageMapKey';
11
+ import { ThreadStatus } from '@/types/index';
12
+ import type { UIChatMessage } from '@/types/index';
13
+
14
+ import {
15
+ ErrorState,
16
+ InitializingState,
17
+ TaskMessages,
18
+ isProcessingStatus,
19
+ } from '../../Tasks/shared';
20
+ import TaskTitle, { type TaskMetrics } from './TaskTitle';
21
+
22
+ interface ClientTaskItemProps {
23
+ item: UIChatMessage;
24
+ }
25
+
26
+ const ClientTaskItem = memo<ClientTaskItemProps>(({ item }) => {
27
+ const { id, agentId: itemAgentId, groupId: itemGroupId, metadata, taskDetail } = item;
28
+ const [expanded, setExpanded] = useState(false);
29
+
30
+ const title = taskDetail?.title || metadata?.taskTitle;
31
+ const instruction = metadata?.instruction;
32
+ const status = taskDetail?.status;
33
+ const threadId = taskDetail?.threadId;
34
+
35
+ const isProcessing = isProcessingStatus(status);
36
+ const isCompleted = status === ThreadStatus.Completed;
37
+ const isError = status === ThreadStatus.Failed || status === ThreadStatus.Cancel;
38
+ const isInitializing = !taskDetail || !status;
39
+
40
+ // Fetch thread messages for client mode
41
+ // Use item's agentId (from task message) to query with the correct SubAgent ID that created the thread
42
+ // Fall back to activeAgentId if task message doesn't have agentId (shouldn't happen normally)
43
+ const [activeAgentId, activeTopicId, useFetchMessages] = useChatStore((s) => [
44
+ s.activeAgentId,
45
+ s.activeTopicId,
46
+ s.useFetchMessages,
47
+ ]);
48
+
49
+ // Use task message's agentId (skip 'supervisor' as it's not a valid agent ID for queries)
50
+ // Fall back to activeAgentId if not available
51
+ const agentId = itemAgentId && itemAgentId !== 'supervisor' ? itemAgentId : activeAgentId;
52
+
53
+ // Get agent info from store for displaying
54
+ const activeGroupId = useAgentGroupStore(agentGroupSelectors.activeGroupId);
55
+ const agent = useAgentGroupStore((s) =>
56
+ activeGroupId && itemAgentId
57
+ ? agentGroupSelectors.getAgentByIdFromGroup(activeGroupId, itemAgentId)(s)
58
+ : null,
59
+ );
60
+
61
+ const threadContext = useMemo(
62
+ () => ({
63
+ agentId,
64
+ groupId: itemGroupId,
65
+ scope: 'thread' as const,
66
+ threadId,
67
+ topicId: activeTopicId,
68
+ }),
69
+ [agentId, itemGroupId, activeTopicId, threadId],
70
+ );
71
+
72
+ const threadMessageKey = useMemo(
73
+ () => (threadId ? messageMapKey(threadContext) : null),
74
+ [threadId, threadContext],
75
+ );
76
+
77
+ // Fetch thread messages (skip when executing - messages come from real-time updates)
78
+ useFetchMessages(threadContext, isProcessing);
79
+
80
+ // Get thread messages from store using selector
81
+ const threadMessages = useChatStore((s) =>
82
+ threadMessageKey
83
+ ? displayMessageSelectors.getDisplayMessagesByKey(threadMessageKey)(s)
84
+ : undefined,
85
+ );
86
+
87
+ // Find the assistantGroup message which contains the children blocks
88
+ const assistantGroupMessage = threadMessages?.find((item) => item.role === 'assistantGroup');
89
+ const blocks = assistantGroupMessage?.children;
90
+ const childrenCount = blocks?.length ?? 0;
91
+
92
+ // Get model/provider from assistantGroup message
93
+ const model = assistantGroupMessage?.model;
94
+ const provider = assistantGroupMessage?.provider;
95
+
96
+ // Build metrics for TaskTitle based on blocks data
97
+ const metrics: TaskMetrics | undefined = useMemo(() => {
98
+ if (isProcessing && blocks) {
99
+ const toolCalls = blocks.reduce((sum, block) => sum + (block.tools?.length || 0), 0);
100
+ return {
101
+ isLoading: false,
102
+ startTime: assistantGroupMessage?.createdAt,
103
+ steps: blocks.length,
104
+ toolCalls,
105
+ };
106
+ }
107
+ if (isCompleted || isError) {
108
+ return {
109
+ duration: taskDetail?.duration,
110
+ steps: taskDetail?.totalSteps,
111
+ toolCalls: taskDetail?.totalToolCalls,
112
+ };
113
+ }
114
+ return undefined;
115
+ }, [
116
+ isProcessing,
117
+ isCompleted,
118
+ isError,
119
+ blocks,
120
+ assistantGroupMessage?.createdAt,
121
+ taskDetail?.duration,
122
+ taskDetail?.totalSteps,
123
+ taskDetail?.totalToolCalls,
124
+ ]);
125
+
126
+ // Check if we have blocks to show (for Processing and Completed states)
127
+ const hasBlocks = blocks && childrenCount > 0;
128
+
129
+ return (
130
+ <AccordionItem
131
+ expand={expanded}
132
+ itemKey={id}
133
+ onExpandChange={setExpanded}
134
+ paddingBlock={4}
135
+ paddingInline={4}
136
+ title={
137
+ <TaskTitle
138
+ agent={
139
+ agent
140
+ ? { avatar: agent.avatar || undefined, backgroundColor: agent.backgroundColor }
141
+ : undefined
142
+ }
143
+ metrics={metrics}
144
+ status={status}
145
+ title={title}
146
+ />
147
+ }
148
+ >
149
+ <Block gap={16} padding={12} style={{ marginBlock: 8 }} variant={'outlined'}>
150
+ {instruction && (
151
+ <Block padding={12}>
152
+ <Text fontSize={13} type={'secondary'}>
153
+ {instruction}
154
+ </Text>
155
+ </Block>
156
+ )}
157
+
158
+ {/* Initializing State - no taskDetail yet or no blocks */}
159
+ {(isInitializing || (isProcessing && !hasBlocks)) && <InitializingState />}
160
+
161
+ {/* Processing or Completed State - show blocks via TaskMessages */}
162
+ {!isInitializing && (isProcessing || isCompleted) && hasBlocks && threadMessages && (
163
+ <TaskMessages
164
+ duration={taskDetail?.duration}
165
+ isProcessing={isProcessing}
166
+ messages={threadMessages}
167
+ model={model ?? undefined}
168
+ provider={provider ?? undefined}
169
+ startTime={assistantGroupMessage?.createdAt}
170
+ totalCost={taskDetail?.totalCost}
171
+ />
172
+ )}
173
+
174
+ {/* Error State */}
175
+ {!isInitializing && isError && taskDetail && <ErrorState taskDetail={taskDetail} />}
176
+ </Block>
177
+ </AccordionItem>
178
+ );
179
+ }, Object.is);
180
+
181
+ ClientTaskItem.displayName = 'ClientTaskItem';
182
+
183
+ export default ClientTaskItem;
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+
3
+ import { ThreadStatus } from '@lobechat/types';
4
+ import type { UIChatMessage } from '@lobechat/types';
5
+ import { AccordionItem, Block } from '@lobehub/ui';
6
+ import isEqual from 'fast-deep-equal';
7
+ import { memo, useMemo, useState } from 'react';
8
+
9
+ import { useAgentGroupStore } from '@/store/agentGroup';
10
+ import { agentGroupSelectors } from '@/store/agentGroup/selectors';
11
+
12
+ import { TaskContent } from '../../Tasks/shared';
13
+ import TaskTitle, { type TaskMetrics } from './TaskTitle';
14
+
15
+ interface ServerTaskItemProps {
16
+ item: UIChatMessage;
17
+ }
18
+
19
+ const ServerTaskItem = memo<ServerTaskItemProps>(({ item }) => {
20
+ const { id, agentId, metadata, taskDetail, tasks } = item;
21
+ const [expanded, setExpanded] = useState(false);
22
+
23
+ const title = taskDetail?.title || metadata?.taskTitle;
24
+ const status = taskDetail?.status;
25
+ const threadId = taskDetail?.threadId;
26
+
27
+ const isCompleted = status === ThreadStatus.Completed;
28
+ const isError = status === ThreadStatus.Failed || status === ThreadStatus.Cancel;
29
+
30
+ // Get agent info from store
31
+ const activeGroupId = useAgentGroupStore(agentGroupSelectors.activeGroupId);
32
+ const agent = useAgentGroupStore((s) =>
33
+ activeGroupId && agentId
34
+ ? agentGroupSelectors.getAgentByIdFromGroup(activeGroupId, agentId)(s)
35
+ : null,
36
+ );
37
+
38
+ // Build metrics for TaskTitle (only for completed/error states)
39
+ const metrics: TaskMetrics | undefined = useMemo(() => {
40
+ if (isCompleted || isError) {
41
+ return {
42
+ duration: taskDetail?.duration,
43
+ steps: taskDetail?.totalSteps,
44
+ toolCalls: taskDetail?.totalToolCalls,
45
+ };
46
+ }
47
+ return undefined;
48
+ }, [
49
+ isCompleted,
50
+ isError,
51
+ taskDetail?.duration,
52
+ taskDetail?.totalSteps,
53
+ taskDetail?.totalToolCalls,
54
+ ]);
55
+
56
+ return (
57
+ <AccordionItem
58
+ expand={expanded}
59
+ itemKey={id}
60
+ onExpandChange={setExpanded}
61
+ paddingBlock={4}
62
+ paddingInline={4}
63
+ title={
64
+ <TaskTitle
65
+ agent={
66
+ agent
67
+ ? { avatar: agent.avatar || undefined, backgroundColor: agent.backgroundColor }
68
+ : undefined
69
+ }
70
+ metrics={metrics}
71
+ status={status}
72
+ title={title}
73
+ />
74
+ }
75
+ >
76
+ <Block gap={16} padding={12} style={{ marginBlock: 8 }} variant={'outlined'}>
77
+ {expanded && (
78
+ <TaskContent
79
+ id={id}
80
+ isError={isError}
81
+ messages={tasks}
82
+ status={status}
83
+ taskDetail={taskDetail}
84
+ threadId={threadId}
85
+ />
86
+ )}
87
+ </Block>
88
+ </AccordionItem>
89
+ );
90
+ }, isEqual);
91
+
92
+ ServerTaskItem.displayName = 'ServerTaskItem';
93
+
94
+ export default ServerTaskItem;
@@ -0,0 +1,177 @@
1
+ 'use client';
2
+
3
+ import { ThreadStatus } from '@lobechat/types';
4
+ import { Avatar, Block, Flexbox, Icon, Text } from '@lobehub/ui';
5
+ import { cssVar } from 'antd-style';
6
+ import { Footprints, ListChecksIcon, Wrench, XIcon } from 'lucide-react';
7
+ import { memo, useEffect, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+
10
+ import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
11
+ import { DEFAULT_AVATAR } from '@/const/meta';
12
+
13
+ import { formatDuration, formatElapsedTime, isProcessingStatus } from '../../Tasks/shared';
14
+
15
+ export interface TaskMetrics {
16
+ /** Task duration in milliseconds (for completed tasks) */
17
+ duration?: number;
18
+ /** Whether metrics are still loading */
19
+ isLoading?: boolean;
20
+ /** Start time timestamp for elapsed time calculation */
21
+ startTime?: number;
22
+ /** Number of execution steps/blocks */
23
+ steps?: number;
24
+ /** Total tool calls count */
25
+ toolCalls?: number;
26
+ }
27
+
28
+ interface TaskTitleProps {
29
+ /** Agent info for avatar display */
30
+ agent?: {
31
+ avatar?: string;
32
+ backgroundColor?: string | null;
33
+ };
34
+ /** Metrics to display (steps, tool calls, elapsed time) */
35
+ metrics?: TaskMetrics;
36
+ status?: ThreadStatus;
37
+ title?: string;
38
+ }
39
+
40
+ const TaskStatusIndicator = memo<{ status?: ThreadStatus }>(({ status }) => {
41
+ const isCompleted = status === ThreadStatus.Completed;
42
+ const isError = status === ThreadStatus.Failed || status === ThreadStatus.Cancel;
43
+ const isProcessing = status ? isProcessingStatus(status) : false;
44
+ const isInitializing = !status;
45
+
46
+ let icon;
47
+
48
+ if (isCompleted) {
49
+ icon = <Icon color={cssVar.colorSuccess} icon={ListChecksIcon} />;
50
+ } else if (isError) {
51
+ icon = <Icon color={cssVar.colorError} icon={XIcon} />;
52
+ } else if (isProcessing || isInitializing) {
53
+ icon = <NeuralNetworkLoading size={16} />;
54
+ } else {
55
+ return null;
56
+ }
57
+
58
+ return (
59
+ <Block
60
+ align={'center'}
61
+ flex={'none'}
62
+ gap={4}
63
+ height={24}
64
+ horizontal
65
+ justify={'center'}
66
+ style={{
67
+ fontSize: 12,
68
+ }}
69
+ variant={'outlined'}
70
+ width={24}
71
+ >
72
+ {icon}
73
+ </Block>
74
+ );
75
+ });
76
+
77
+ TaskStatusIndicator.displayName = 'TaskStatusIndicator';
78
+
79
+ interface MetricsDisplayProps {
80
+ metrics: TaskMetrics;
81
+ status?: ThreadStatus;
82
+ }
83
+
84
+ const MetricsDisplay = memo<MetricsDisplayProps>(({ metrics, status }) => {
85
+ const { t } = useTranslation('chat');
86
+ const { steps, toolCalls, startTime, duration, isLoading } = metrics;
87
+ const [elapsedTime, setElapsedTime] = useState(0);
88
+
89
+ const isProcessing = status ? isProcessingStatus(status) : false;
90
+
91
+ // Calculate initial elapsed time
92
+ useEffect(() => {
93
+ if (startTime && isProcessing) {
94
+ setElapsedTime(Math.max(0, Date.now() - startTime));
95
+ }
96
+ }, [startTime, isProcessing]);
97
+
98
+ // Timer for updating elapsed time every second (only when processing)
99
+ useEffect(() => {
100
+ if (!startTime || !isProcessing) return;
101
+
102
+ const timer = setInterval(() => {
103
+ setElapsedTime(Math.max(0, Date.now() - startTime));
104
+ }, 1000);
105
+
106
+ return () => clearInterval(timer);
107
+ }, [startTime, isProcessing]);
108
+
109
+ // Don't show metrics if loading or no data
110
+ if (isLoading) return null;
111
+
112
+ const hasSteps = steps !== undefined && steps > 0;
113
+ const hasToolCalls = toolCalls !== undefined && toolCalls > 0;
114
+ const hasTime = isProcessing ? startTime !== undefined : duration !== undefined;
115
+
116
+ // Don't render if no metrics to show
117
+ if (!hasSteps && !hasToolCalls && !hasTime) return null;
118
+
119
+ return (
120
+ <Flexbox align="center" gap={8} horizontal>
121
+ {/* Steps */}
122
+ {hasSteps && (
123
+ <Flexbox align="center" gap={2} horizontal>
124
+ <Icon color={cssVar.colorTextTertiary} icon={Footprints} size={12} />
125
+ <Text fontSize={12} type="secondary">
126
+ {steps}
127
+ </Text>
128
+ </Flexbox>
129
+ )}
130
+ {/* Tool calls */}
131
+ {hasToolCalls && (
132
+ <Flexbox align="center" gap={2} horizontal>
133
+ <Icon color={cssVar.colorTextTertiary} icon={Wrench} size={12} />
134
+ <Text fontSize={12} type="secondary">
135
+ {toolCalls}
136
+ </Text>
137
+ </Flexbox>
138
+ )}
139
+ {/* Time */}
140
+ {hasTime && (
141
+ <Text fontSize={12} type="secondary">
142
+ {isProcessing
143
+ ? formatElapsedTime(elapsedTime)
144
+ : duration
145
+ ? t('task.metrics.duration', { duration: formatDuration(duration) })
146
+ : null}
147
+ </Text>
148
+ )}
149
+ </Flexbox>
150
+ );
151
+ });
152
+
153
+ MetricsDisplay.displayName = 'MetricsDisplay';
154
+
155
+ const TaskTitle = memo<TaskTitleProps>(({ title, status, metrics, agent }) => {
156
+ return (
157
+ <Flexbox align="center" gap={6} horizontal>
158
+ <TaskStatusIndicator status={status} />
159
+ {agent && (
160
+ <Avatar
161
+ avatar={agent.avatar || DEFAULT_AVATAR}
162
+ background={agent.backgroundColor || undefined}
163
+ shape={'circle'}
164
+ size={20}
165
+ />
166
+ )}
167
+ <Text ellipsis fontSize={14}>
168
+ {title}
169
+ </Text>
170
+ {metrics && <MetricsDisplay metrics={metrics} status={status} />}
171
+ </Flexbox>
172
+ );
173
+ });
174
+
175
+ TaskTitle.displayName = 'TaskTitle';
176
+
177
+ export default TaskTitle;
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import type { UIChatMessage } from '@lobechat/types';
4
+ import isEqual from 'fast-deep-equal';
5
+ import { memo } from 'react';
6
+
7
+ import ClientTaskItem from './ClientTaskItem';
8
+ import ServerTaskItem from './ServerTaskItem';
9
+
10
+ interface TaskItemProps {
11
+ item: UIChatMessage;
12
+ }
13
+
14
+ const TaskItem = memo<TaskItemProps>(({ item }) => {
15
+ const isClientMode = item.taskDetail?.clientMode;
16
+
17
+ if (isClientMode) {
18
+ return <ClientTaskItem item={item} />;
19
+ }
20
+
21
+ return <ServerTaskItem item={item} />;
22
+ }, isEqual);
23
+
24
+ TaskItem.displayName = 'TaskItem';
25
+
26
+ export default TaskItem;
@@ -0,0 +1,93 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+
5
+ import { useChatStore } from '@/store/chat';
6
+ import { displayMessageSelectors } from '@/store/chat/selectors';
7
+ import { messageMapKey } from '@/store/chat/utils/messageMapKey';
8
+
9
+ export interface ClientTaskStats {
10
+ isLoading: boolean;
11
+ startTime?: number;
12
+ steps: number;
13
+ toolCalls: number;
14
+ }
15
+
16
+ interface UseClientTaskStatsOptions {
17
+ /** Agent ID from the task message (use task's agentId, not activeAgentId) */
18
+ agentId?: string;
19
+ enabled?: boolean;
20
+ /** Group ID from the task message (use task's groupId, not activeGroupId) */
21
+ groupId?: string;
22
+ threadId?: string;
23
+ }
24
+
25
+ /**
26
+ * Hook to fetch thread messages and compute task statistics for client mode tasks.
27
+ * Used in TaskItem to display progress metrics (steps, tool calls, elapsed time).
28
+ */
29
+ export const useClientTaskStats = ({
30
+ agentId: propAgentId,
31
+ groupId,
32
+ threadId,
33
+ enabled = true,
34
+ }: UseClientTaskStatsOptions): ClientTaskStats => {
35
+ // Use task message's agentId to query with the correct SubAgent ID that created the thread
36
+ // Fall back to activeAgentId if not provided
37
+ const [activeAgentId, activeTopicId, useFetchMessages] = useChatStore((s) => [
38
+ s.activeAgentId,
39
+ s.activeTopicId,
40
+ s.useFetchMessages,
41
+ ]);
42
+
43
+ const agentId = propAgentId || activeAgentId;
44
+
45
+ const threadContext = useMemo(
46
+ () => ({
47
+ agentId,
48
+ groupId,
49
+ scope: 'thread' as const,
50
+ threadId,
51
+ topicId: activeTopicId,
52
+ }),
53
+ [agentId, groupId, activeTopicId, threadId],
54
+ );
55
+
56
+ const threadMessageKey = useMemo(
57
+ () => (threadId ? messageMapKey(threadContext) : null),
58
+ [threadId, threadContext],
59
+ );
60
+
61
+ // Fetch thread messages (skip when disabled or no threadId)
62
+ useFetchMessages(threadContext, !enabled || !threadId);
63
+
64
+ // Get thread messages from store using selector
65
+ const threadMessages = useChatStore((s) =>
66
+ threadMessageKey
67
+ ? displayMessageSelectors.getDisplayMessagesByKey(threadMessageKey)(s)
68
+ : undefined,
69
+ );
70
+
71
+ // Compute stats from thread messages
72
+ return useMemo(() => {
73
+ if (!threadMessages || !enabled) {
74
+ return { isLoading: true, steps: 0, toolCalls: 0 };
75
+ }
76
+
77
+ // Find the assistantGroup message which contains the children blocks
78
+ const assistantGroupMessage = threadMessages.find((item) => item.role === 'assistantGroup');
79
+ const blocks = assistantGroupMessage?.children ?? [];
80
+
81
+ // Calculate stats
82
+ const steps = blocks.length;
83
+ const toolCalls = blocks.reduce((sum, block) => sum + (block.tools?.length || 0), 0);
84
+ const startTime = assistantGroupMessage?.createdAt;
85
+
86
+ return {
87
+ isLoading: false,
88
+ startTime,
89
+ steps,
90
+ toolCalls,
91
+ };
92
+ }, [threadMessages, enabled]);
93
+ };