@adecore/agents-react 0.32.0 → 0.33.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.
- package/README.md +1 -1
- package/dist/chat/logic/workflow.d.ts +10 -0
- package/dist/chat/logic/workflow.js +91 -4
- package/dist/chat/ui/MermaidBlock.js +172 -136
- package/dist/chat/ui/mermaid.d.ts +8 -20
- package/dist/chat/ui/mermaid.js +40 -65
- package/dist/chat/ui/rows/WorkRows.d.ts +3 -0
- package/dist/chat/ui/rows/WorkRows.js +1 -1
- package/dist/chat/ui/rows/WorkflowRow.js +628 -168
- package/dist/locales/en/agent-chat.d.ts +8 -0
- package/dist/locales/en/agent-chat.js +9 -1
- package/dist/locales/nl/agent-chat.d.ts +8 -0
- package/dist/locales/nl/agent-chat.js +9 -1
- package/package.json +9 -9
- package/src/chat/logic/workflow.ts +86 -4
- package/src/chat/ui/MermaidBlock.tsx +55 -46
- package/src/chat/ui/mermaid.ts +52 -96
- package/src/chat/ui/rows/WorkRows.tsx +1 -1
- package/src/chat/ui/rows/WorkflowRow.tsx +144 -54
- package/src/locales/en/agent-chat.ts +9 -1
- package/src/locales/nl/agent-chat.ts +9 -1
|
@@ -1,68 +1,129 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
1
3
|
import { useTranslation } from 'react-i18next';
|
|
2
|
-
import { Bot,
|
|
3
|
-
import { workflowAgentRef, type ChatToolItem, type ChatWorkflow } from '@adecore/agent-contracts';
|
|
4
|
-
import {
|
|
4
|
+
import { Bot, ChevronRight, Square, TriangleAlert, Workflow, X } from 'lucide-react';
|
|
5
|
+
import { workflowAgentRef, type AgentKind, type ChatToolItem, type ChatWorkflow } from '@adecore/agent-contracts';
|
|
6
|
+
import { formatElapsedShort } from '@adecore/ui/format';
|
|
7
|
+
import { Icon, IconButton, useTickingText } from '@adecore/ui';
|
|
8
|
+
import { AgentIcon } from '../../../agents/AgentIcon';
|
|
9
|
+
import {
|
|
10
|
+
workflowAgentPreview,
|
|
11
|
+
workflowAgentTime,
|
|
12
|
+
workflowCounts,
|
|
13
|
+
workflowDescription,
|
|
14
|
+
workflowPhases,
|
|
15
|
+
type WorkflowAgentState,
|
|
16
|
+
type WorkflowAgentView,
|
|
17
|
+
type WorkflowCounts,
|
|
18
|
+
type WorkflowPhaseView
|
|
19
|
+
} from '../../logic/workflow';
|
|
20
|
+
import { hasToolBody } from '../../logic/tools';
|
|
5
21
|
import { useSubagentSupport } from '../../subagent-support';
|
|
6
22
|
import type { SubagentStep } from '../../subagent-view';
|
|
7
23
|
import { ROW_GUTTER } from '../icons';
|
|
8
|
-
import {
|
|
24
|
+
import { ToolBody } from './WorkRows';
|
|
9
25
|
import { useChatScope } from '../../../scope';
|
|
10
|
-
import {
|
|
26
|
+
import { useChats } from '../../../state/chats';
|
|
11
27
|
import { useChatActions } from '../../actions';
|
|
12
28
|
import { useChatHost } from '../../../host';
|
|
13
29
|
|
|
14
|
-
|
|
30
|
+
const DOT: Record<WorkflowAgentState, string> = {
|
|
31
|
+
running: 'bg-status-running',
|
|
32
|
+
waiting: 'bg-text-faint',
|
|
33
|
+
stopped: 'bg-text-faint',
|
|
34
|
+
done: 'bg-status-idle',
|
|
35
|
+
failed: 'bg-status-error'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const TIME = 'shrink-0 text-xs text-text-faint tabular-nums';
|
|
39
|
+
|
|
40
|
+
function Elapsed({ startedAt }: { startedAt: number }) {
|
|
41
|
+
const ref = useTickingText(() => formatElapsedShort(Date.now() - startedAt));
|
|
42
|
+
return <span ref={ref} className={TIME} />;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function AgentLine({ view, provider, onOpen }: { view: WorkflowAgentView; provider: AgentKind | undefined; onOpen?(step: SubagentStep): void }) {
|
|
15
46
|
const { agent, state } = view;
|
|
16
47
|
const time = workflowAgentTime(view);
|
|
17
|
-
const
|
|
48
|
+
const preview = workflowAgentPreview(view);
|
|
18
49
|
const agentId = agent.agentId;
|
|
19
50
|
// One still waiting has no transcript yet.
|
|
20
|
-
const open = onOpen !== undefined && agentId !== null ? () => onOpen({ toolUseId: workflowAgentRef(agentId), description: agent.label }) :
|
|
51
|
+
const open = onOpen !== undefined && agentId !== null ? () => onOpen({ toolUseId: workflowAgentRef(agentId), description: agent.label }) : undefined;
|
|
21
52
|
return (
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
53
|
+
<button
|
|
54
|
+
className="-mx-1 mb-0.5 flex w-[calc(100%+8px)] items-start gap-2 rounded-md px-1 py-1 text-left enabled:hover:bg-surface-hover"
|
|
55
|
+
disabled={open === undefined}
|
|
56
|
+
onClick={open}
|
|
57
|
+
>
|
|
58
|
+
<span className={clsx(ROW_GUTTER, 'relative text-text')}>
|
|
59
|
+
{provider === undefined ? <Icon icon={Bot} size={14} /> : <AgentIcon kind={provider} size={14} />}
|
|
60
|
+
<span className={clsx('absolute -right-0.75 bottom-0 size-2 rounded-full ring-2 ring-surface', DOT[state])} />
|
|
61
|
+
</span>
|
|
62
|
+
<span className="flex min-w-0 grow flex-col">
|
|
63
|
+
<span data-find-field="summary" className={clsx('truncate text-xs font-medium', state === 'failed' ? 'text-status-error' : 'text-text')}>
|
|
64
|
+
{agent.label}
|
|
65
|
+
</span>
|
|
66
|
+
{(preview.tool !== null || preview.text !== null) && (
|
|
67
|
+
<span className="truncate text-2xs text-text-muted">
|
|
68
|
+
{preview.tool !== null && <span className="font-mono text-code">{preview.tool}</span>}
|
|
69
|
+
{preview.tool !== null && preview.text !== null && ' · '}
|
|
70
|
+
{preview.text}
|
|
71
|
+
</span>
|
|
72
|
+
)}
|
|
73
|
+
</span>
|
|
74
|
+
<span className="flex h-5 shrink-0 items-center gap-2">
|
|
75
|
+
{state === 'running' && agent.startedAt !== null ? (
|
|
76
|
+
<Elapsed startedAt={agent.startedAt} />
|
|
33
77
|
) : (
|
|
34
|
-
<span className=
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
78
|
+
time !== null && <span className={TIME}>{time}</span>
|
|
79
|
+
)}
|
|
80
|
+
{state === 'failed' && <Icon icon={X} size={12} className="shrink-0 text-status-error" />}
|
|
81
|
+
<Icon icon={ChevronRight} size={12} className={clsx('shrink-0 text-text-faint', open === undefined && 'invisible')} />
|
|
82
|
+
</span>
|
|
83
|
+
</button>
|
|
38
84
|
);
|
|
39
85
|
}
|
|
40
86
|
|
|
41
|
-
function PhaseBlock({ phase, onOpen }: { phase: WorkflowPhaseView; onOpen?(step: SubagentStep): void }) {
|
|
87
|
+
function PhaseBlock({ phase, provider, onOpen }: { phase: WorkflowPhaseView; provider: AgentKind | undefined; onOpen?(step: SubagentStep): void }) {
|
|
42
88
|
const { t } = useTranslation('agent-chat');
|
|
89
|
+
const settled = phase.agents.filter((view) => view.state === 'done' || view.state === 'failed').length;
|
|
43
90
|
return (
|
|
44
|
-
<div>
|
|
91
|
+
<div className="mt-3">
|
|
45
92
|
{phase.title !== null && (
|
|
46
|
-
<div className="
|
|
47
|
-
<span className={
|
|
48
|
-
|
|
93
|
+
<div className="flex h-7 min-w-0 items-center gap-2 text-xs">
|
|
94
|
+
<span className="min-w-0 truncate font-medium text-text">{phase.title}</span>
|
|
95
|
+
<span className="shrink-0 text-text-faint tabular-nums">
|
|
96
|
+
{phase.agents.length === 0 ? t('rows.workflow.notStarted') : `${settled}/${phase.agents.length}`}
|
|
49
97
|
</span>
|
|
50
|
-
<span className="min-w-0 truncate">{phase.title}</span>
|
|
51
|
-
<span className="grow" />
|
|
52
|
-
{phase.agents.length === 0 && <span className="shrink-0 text-text-faint">{t('rows.workflow.notStarted')}</span>}
|
|
53
|
-
</div>
|
|
54
|
-
)}
|
|
55
|
-
{phase.agents.length > 0 && (
|
|
56
|
-
<div className={phase.title === null ? undefined : 'ml-6'}>
|
|
57
|
-
{phase.agents.map((view) => (
|
|
58
|
-
<AgentLine key={view.agent.index} view={view} onOpen={onOpen} />
|
|
59
|
-
))}
|
|
60
98
|
</div>
|
|
61
99
|
)}
|
|
100
|
+
{phase.agents.map((view) => (
|
|
101
|
+
<AgentLine key={view.agent.index} view={view} provider={provider} onOpen={onOpen} />
|
|
102
|
+
))}
|
|
62
103
|
</div>
|
|
63
104
|
);
|
|
64
105
|
}
|
|
65
106
|
|
|
107
|
+
function Counts({ counts }: { counts: WorkflowCounts }) {
|
|
108
|
+
const { t } = useTranslation('agent-chat');
|
|
109
|
+
const parts = [
|
|
110
|
+
{ key: 'working', count: counts.running, tone: 'text-status-running' },
|
|
111
|
+
{ key: 'finished', count: counts.done, tone: 'text-status-idle' },
|
|
112
|
+
{ key: 'failedCount', count: counts.failed, tone: 'text-status-error' }
|
|
113
|
+
].filter((part) => part.count > 0);
|
|
114
|
+
return (
|
|
115
|
+
<span className="shrink-0 truncate text-xs text-text-faint tabular-nums">
|
|
116
|
+
{t('rows.workflow.agents', { count: counts.total })}
|
|
117
|
+
{parts.map((part) => (
|
|
118
|
+
<span key={part.key}>
|
|
119
|
+
{' · '}
|
|
120
|
+
<span className={part.tone}>{t(`rows.workflow.${part.key}`, { count: part.count })}</span>
|
|
121
|
+
</span>
|
|
122
|
+
))}
|
|
123
|
+
</span>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
66
127
|
// Keep background progress visible after the launcher turn ends.
|
|
67
128
|
export function WorkflowRow({
|
|
68
129
|
tool,
|
|
@@ -77,11 +138,16 @@ export function WorkflowRow({
|
|
|
77
138
|
}) {
|
|
78
139
|
const host = useChatHost();
|
|
79
140
|
const { t } = useTranslation('agent-chat');
|
|
80
|
-
const { id } = useChatScope();
|
|
141
|
+
const { id, keyOf } = useChatScope();
|
|
142
|
+
const [open, setOpen] = useState(false);
|
|
81
143
|
// A host that does not answer `chat.subagent` has no conversation to open for anyone.
|
|
82
144
|
const refused = useSubagentSupport((s) => s.unsupported[id] === true);
|
|
145
|
+
const provider = useChats((s) => (chatId === undefined ? undefined : s.statusByKey[keyOf(chatId)]?.info?.provider));
|
|
83
146
|
const running = tool.state === 'running';
|
|
84
147
|
const phases = workflowPhases(workflow, running);
|
|
148
|
+
const counts = workflowCounts(phases);
|
|
149
|
+
const description = workflowDescription(tool.input);
|
|
150
|
+
const opens = hasToolBody(tool);
|
|
85
151
|
const onOpen = refused ? undefined : onOpenAgent;
|
|
86
152
|
const actions = useChatActions();
|
|
87
153
|
const stop = (): void => {
|
|
@@ -95,27 +161,51 @@ export function WorkflowRow({
|
|
|
95
161
|
});
|
|
96
162
|
}
|
|
97
163
|
};
|
|
164
|
+
const failed = tool.state === 'error';
|
|
165
|
+
const header = (
|
|
166
|
+
<>
|
|
167
|
+
<span className={clsx(ROW_GUTTER, running && 'text-accent')}>
|
|
168
|
+
<Icon icon={Workflow} size={12} />
|
|
169
|
+
</span>
|
|
170
|
+
<span className={clsx('shrink-0', running && 'chat-live-text')}>{t(`rows.workflow.${running ? 'running' : failed ? 'failed' : 'done'}`)}</span>
|
|
171
|
+
<span className="grow" />
|
|
172
|
+
{counts.total > 0 && <Counts counts={counts} />}
|
|
173
|
+
{opens && <Icon icon={ChevronRight} size={12} className={clsx('shrink-0 text-text-faint transition-transform', open && 'rotate-90')} />}
|
|
174
|
+
</>
|
|
175
|
+
);
|
|
176
|
+
const headerClasses = clsx(
|
|
177
|
+
'-mx-1 flex h-7 w-[calc(100%+8px)] min-w-0 items-center gap-2 rounded-md px-1 text-left text-xs',
|
|
178
|
+
failed ? 'text-status-error' : 'text-text-muted',
|
|
179
|
+
opens && 'hover:bg-surface-hover'
|
|
180
|
+
);
|
|
98
181
|
return (
|
|
99
|
-
<div>
|
|
182
|
+
<div data-find-item={tool.id} className="mb-1">
|
|
100
183
|
<div className="flex items-center gap-1">
|
|
101
184
|
<div className="min-w-0 grow">
|
|
102
|
-
{
|
|
185
|
+
{opens ? (
|
|
186
|
+
<button className={headerClasses} onClick={() => setOpen((value) => !value)}>
|
|
187
|
+
{header}
|
|
188
|
+
</button>
|
|
189
|
+
) : (
|
|
190
|
+
<div className={headerClasses}>{header}</div>
|
|
191
|
+
)}
|
|
103
192
|
</div>
|
|
104
193
|
{running && chatId && workflow.taskId && <IconButton icon={Square} size="xs" label={t('activity.stop')} onClick={stop} />}
|
|
105
194
|
</div>
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
195
|
+
{open && opens && <ToolBody tool={tool} />}
|
|
196
|
+
<div className="mt-1.5 ml-6">
|
|
197
|
+
{workflow.name !== null && <div className="truncate text-sm font-medium text-text">{workflow.name}</div>}
|
|
198
|
+
{description !== null && <p className="mt-0.5 line-clamp-2 text-xs text-text-muted">{description}</p>}
|
|
199
|
+
{running && workflow.stalledAt !== undefined && (
|
|
200
|
+
<p role="status" className="mt-1 flex items-start gap-1.5 text-xs text-text-muted">
|
|
201
|
+
<Icon icon={TriangleAlert} size={12} className="mt-0.5 shrink-0 text-status-needs-you" />
|
|
202
|
+
<span>{t('rows.workflow.stalled')}</span>
|
|
203
|
+
</p>
|
|
204
|
+
)}
|
|
205
|
+
{phases.map((phase) => (
|
|
206
|
+
<PhaseBlock key={phase.key} phase={phase} provider={provider} onOpen={onOpen} />
|
|
207
|
+
))}
|
|
208
|
+
</div>
|
|
119
209
|
</div>
|
|
120
210
|
);
|
|
121
211
|
}
|
|
@@ -176,7 +176,15 @@ export default {
|
|
|
176
176
|
notStarted: 'not started',
|
|
177
177
|
waiting: 'waiting',
|
|
178
178
|
stopped: 'stopped',
|
|
179
|
-
stalled: 'No progress for five minutes. It is still running, and you can stop it.'
|
|
179
|
+
stalled: 'No progress for five minutes. It is still running, and you can stop it.',
|
|
180
|
+
running: 'Workflow running',
|
|
181
|
+
done: 'Workflow done',
|
|
182
|
+
failed: 'Workflow failed',
|
|
183
|
+
agents_one: '{{count}} agent',
|
|
184
|
+
agents_other: '{{count}} agents',
|
|
185
|
+
working: '{{count}} working',
|
|
186
|
+
finished: '{{count}} done',
|
|
187
|
+
failedCount: '{{count}} failed'
|
|
180
188
|
}
|
|
181
189
|
},
|
|
182
190
|
work: {
|
|
@@ -176,7 +176,15 @@ export default {
|
|
|
176
176
|
notStarted: 'nog niet begonnen',
|
|
177
177
|
waiting: 'wacht',
|
|
178
178
|
stopped: 'gestopt',
|
|
179
|
-
stalled: 'Vijf minuten geen voortgang. Hij loopt nog, en je kunt hem stoppen.'
|
|
179
|
+
stalled: 'Vijf minuten geen voortgang. Hij loopt nog, en je kunt hem stoppen.',
|
|
180
|
+
running: 'Workflow loopt',
|
|
181
|
+
done: 'Workflow klaar',
|
|
182
|
+
failed: 'Workflow mislukt',
|
|
183
|
+
agents_one: '{{count}} agent',
|
|
184
|
+
agents_other: '{{count}} agents',
|
|
185
|
+
working: '{{count}} bezig',
|
|
186
|
+
finished: '{{count}} klaar',
|
|
187
|
+
failedCount: '{{count}} mislukt'
|
|
180
188
|
}
|
|
181
189
|
},
|
|
182
190
|
work: {
|