@adecore/agents-react 0.32.0 → 0.33.1

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.
@@ -1,68 +1,129 @@
1
+ import { useState } from 'react';
2
+ import clsx from 'clsx';
1
3
  import { useTranslation } from 'react-i18next';
2
- import { Bot, ListTree, TriangleAlert, Square } from 'lucide-react';
3
- import { workflowAgentRef, type ChatToolItem, type ChatWorkflow } from '@adecore/agent-contracts';
4
- import { workflowAgentTime, workflowPhases, type WorkflowAgentView, type WorkflowPhaseView } from '../../logic/workflow';
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 { RunningFor, ToggleLine, WorkLiveRow, WorkRow } from './WorkRows';
24
+ import { ToolBody } from './WorkRows';
9
25
  import { useChatScope } from '../../../scope';
10
- import { Icon, IconButton } from '@adecore/ui';
26
+ import { useChats } from '../../../state/chats';
11
27
  import { useChatActions } from '../../actions';
12
28
  import { useChatHost } from '../../../host';
13
29
 
14
- function AgentLine({ view, onOpen }: { view: WorkflowAgentView; onOpen?(step: SubagentStep): void }) {
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 running = state === 'running';
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 }) : () => undefined;
51
+ const open = onOpen !== undefined && agentId !== null ? () => onOpen({ toolUseId: workflowAgentRef(agentId), description: agent.label }) : undefined;
21
52
  return (
22
- <ToggleLine
23
- icon={<Icon icon={Bot} size={12} />}
24
- label={agent.label}
25
- detail={running ? (agent.lastTool ?? undefined) : undefined}
26
- open={false}
27
- onToggle={open}
28
- failed={state === 'failed'}
29
- live={running}
30
- trailing={
31
- running && agent.startedAt !== null ? (
32
- <RunningFor startedAt={agent.startedAt} />
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="shrink-0 text-xs text-text-faint tabular-nums">{time}</span>
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="mb-0.5 flex h-7 min-w-0 items-center gap-2 text-xs text-text-muted">
47
- <span className={ROW_GUTTER}>
48
- <Icon icon={ListTree} size={12} />
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
- {running ? <WorkLiveRow chatId={chatId ?? ''} tool={tool} /> : <WorkRow chatId={chatId} tool={tool} detail={workflow.name ?? undefined} />}
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
- {running && workflow.stalledAt !== undefined && (
107
- <p role="status" className="ml-6 mb-1 flex items-start gap-1.5 text-xs text-text-muted">
108
- <Icon icon={TriangleAlert} size={12} className="mt-0.5 shrink-0 text-status-needs-you" />
109
- <span>{t('rows.workflow.stalled')}</span>
110
- </p>
111
- )}
112
- {phases.length > 0 && (
113
- <div className="ml-6">
114
- {phases.map((phase) => (
115
- <PhaseBlock key={phase.key} phase={phase} onOpen={onOpen} />
116
- ))}
117
- </div>
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: {