@adecore/agents-react 0.25.8 → 0.26.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.
Files changed (41) hide show
  1. package/dist/chat/index.d.ts +1 -0
  2. package/dist/chat/index.js +1 -0
  3. package/dist/chat/logic/command-looks.d.ts +19 -0
  4. package/dist/chat/logic/command-looks.js +123 -0
  5. package/dist/chat/logic/embedded-code.d.ts +25 -0
  6. package/dist/chat/logic/embedded-code.js +27 -5
  7. package/dist/chat/logic/timeline-copy.js +1 -1
  8. package/dist/chat/logic/timeline.d.ts +5 -3
  9. package/dist/chat/logic/timeline.js +64 -10
  10. package/dist/chat/ui/ShellCommand.d.ts +11 -1
  11. package/dist/chat/ui/ShellCommand.js +60 -20
  12. package/dist/chat/ui/SubagentTimeline.js +9 -1
  13. package/dist/chat/ui/Timeline.js +9 -2
  14. package/dist/chat/ui/rows/Rows.js +2 -2
  15. package/dist/chat/ui/rows/SubagentRow.js +1 -1
  16. package/dist/chat/ui/rows/WorkRows.d.ts +13 -4
  17. package/dist/chat/ui/rows/WorkRows.js +55 -11
  18. package/dist/chat/ui/rows/WorkflowRow.js +1 -1
  19. package/dist/host.d.ts +25 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/locales/en/agent-chat.d.ts +4 -0
  22. package/dist/locales/en/agent-chat.js +6 -2
  23. package/dist/locales/nl/agent-chat.d.ts +4 -0
  24. package/dist/locales/nl/agent-chat.js +6 -2
  25. package/package.json +8 -8
  26. package/src/chat/index.ts +1 -0
  27. package/src/chat/logic/command-looks.ts +157 -0
  28. package/src/chat/logic/embedded-code.ts +57 -11
  29. package/src/chat/logic/timeline-copy.ts +1 -1
  30. package/src/chat/logic/timeline.ts +70 -13
  31. package/src/chat/ui/ShellCommand.tsx +83 -23
  32. package/src/chat/ui/SubagentTimeline.tsx +11 -2
  33. package/src/chat/ui/Timeline.tsx +9 -2
  34. package/src/chat/ui/rows/Rows.tsx +2 -2
  35. package/src/chat/ui/rows/SubagentRow.tsx +1 -1
  36. package/src/chat/ui/rows/WorkRows.tsx +136 -19
  37. package/src/chat/ui/rows/WorkflowRow.tsx +1 -1
  38. package/src/host.ts +43 -0
  39. package/src/index.ts +3 -0
  40. package/src/locales/en/agent-chat.ts +6 -2
  41. package/src/locales/nl/agent-chat.ts +6 -2
@@ -1,10 +1,11 @@
1
- import { Fragment, useEffect, useState, type Dispatch, type ReactNode, type SetStateAction } from 'react';
1
+ import { Fragment, useContext, useEffect, useState, type Dispatch, type ReactNode, type SetStateAction } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import i18next from 'i18next';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { ChevronRight, FileDiff, X } from 'lucide-react';
6
6
  import type { ChatCheckpointDiff, ChatCheckpointFile, ChatFileChange, ChatToolItem, ChatTurnItem } from '@adecore/agent-contracts';
7
7
  import type { BaseMoved } from '../../logic/timeline';
8
+ import { matchCommandLook, type CommandLookRun } from '../../logic/command-looks';
8
9
  import {
9
10
  exitCodeOf,
10
11
  fileChanges,
@@ -21,10 +22,10 @@ import {
21
22
  type FileChange
22
23
  } from '../../logic/tools';
23
24
  import { useOpenForFind } from '../find-reveal';
24
- import { useChatHost } from '../../../host';
25
+ import { useChatHost, type CommandLook } from '../../../host';
25
26
  import { lazyNamed } from '../../../lazy';
26
27
  import { useChatActions } from '../../actions';
27
- import { useChatScope } from '../../../scope';
28
+ import { ChatScopeContext, useChatScope } from '../../../scope';
28
29
  import { useCurrentItem } from '../../../state/chats';
29
30
  import { useTimelineFlash } from '../../timeline-flash';
30
31
  import { formatClockDuration, formatElapsedShort } from '@adecore/ui/format';
@@ -32,7 +33,7 @@ import { Icon, LazyBoundary, MiddleTruncate, Spinner, useTickingText } from '@ad
32
33
  import { ROW_GUTTER, toolIcon } from '../icons';
33
34
  import { AnsiOutput } from '../AnsiOutput';
34
35
  import { RowTime } from '../RowTime';
35
- import { ShellCommand } from '../ShellCommand';
36
+ import { ShellCommand, type NamedWord } from '../ShellCommand';
36
37
  import { DiffStack, ReviewStackView } from '../diff/DiffStack';
37
38
  import { reviewEntries, type ReviewEntry } from '../diff/review-plan';
38
39
  import { useReviewStack } from '../diff/use-review-stack';
@@ -51,6 +52,59 @@ function clip(text: string): string {
51
52
  return text.slice(0, OUTPUT_LIMIT) + clipNotice(text);
52
53
  }
53
54
 
55
+ const BRAND = 'font-semibold';
56
+
57
+ /* `text` with `brand` in semibold where it names it, as a sentence about a look does. */
58
+ function withBrand(text: string, brands: readonly string[]): ReactNode {
59
+ for (const brand of brands) {
60
+ const at = brand === '' ? -1 : text.indexOf(brand);
61
+ if (at !== -1) {
62
+ return (
63
+ <>
64
+ {text.slice(0, at)}
65
+ <span className={BRAND}>{brand}</span>
66
+ {text.slice(at + brand.length)}
67
+ </>
68
+ );
69
+ }
70
+ }
71
+ return text;
72
+ }
73
+
74
+ /* The detail of a look's run, with the words its host names drawn as those names. */
75
+ function NamedLookDetail({ run, chatId, useNames }: { run: CommandLookRun; chatId: string; useNames: NonNullable<CommandLook['useNames']> }) {
76
+ const scope = useContext(ChatScopeContext);
77
+ const nameOf = useNames(scope?.id ?? '', chatId);
78
+ const names = run.words.flatMap((word): NamedWord[] => {
79
+ const name = nameOf(word.text, word.option);
80
+ return name === null ? [] : [{ start: word.start, end: word.end, word: word.text, name }];
81
+ });
82
+ return <ShellCommand command={run.detail} code={run.code} names={names} />;
83
+ }
84
+
85
+ function LookDetail({ run, chatId }: { run: CommandLookRun; chatId: string }) {
86
+ const { useNames } = run.look;
87
+ if (useNames === undefined) {
88
+ return <ShellCommand command={run.detail} code={run.code} />;
89
+ }
90
+ // Keyed by the look, so a row whose command turns into another look's starts its hooks again.
91
+ return <NamedLookDetail key={run.look.program} run={run} chatId={chatId} useNames={useNames} />;
92
+ }
93
+
94
+ const TAG = 'shrink-0 rounded bg-surface-hover px-1.5 text-text-faint';
95
+
96
+ /* What else the line ran, in quiet tags: the other programs and a heredoc, and how many more runs of a look. */
97
+ function LookTags({ run }: { run: CommandLookRun }) {
98
+ const { t } = useTranslation('agent-chat');
99
+ const also = run.heredoc ? [...run.others, t('work.heredoc')] : run.others;
100
+ return (
101
+ <>
102
+ {also.length > 0 && <span className={TAG}>{t('work.alsoRan', { programs: also.join(', ') })}</span>}
103
+ {run.more > 0 && <span className={TAG}>{t('work.moreRuns', { count: run.more })}</span>}
104
+ </>
105
+ );
106
+ }
107
+
54
108
  /*
55
109
  * The line of a call, a subagent or a group. Without `onToggle` it has nothing to open: no chevron, and
56
110
  * not a button. `time` shows at the right edge while the line is hovered or focused.
@@ -58,8 +112,10 @@ function clip(text: string): string {
58
112
  export function ToggleLine({
59
113
  icon,
60
114
  label,
115
+ brand,
61
116
  detail,
62
117
  command,
118
+ look,
63
119
  path,
64
120
  open = false,
65
121
  onToggle,
@@ -70,10 +126,14 @@ export function ToggleLine({
70
126
  className
71
127
  }: {
72
128
  icon: ReactNode;
73
- label: string;
129
+ label: ReactNode;
130
+ /* Drawn in semibold before the label. */
131
+ brand?: string;
74
132
  detail?: string;
75
133
  /* Draws `detail` as a shell command, in the colors of the code theme. */
76
134
  command?: boolean;
135
+ /* A shell call read as a look's tool, whose detail and tags take the place of `detail`. */
136
+ look?: { run: CommandLookRun; chatId: string };
77
137
  /* `detail` is the path of a file, cut in the middle so its name stays in sight. */
78
138
  path?: boolean;
79
139
  open?: boolean;
@@ -94,11 +154,26 @@ export function ToggleLine({
94
154
  const content = (
95
155
  <>
96
156
  <span className={clsx(ROW_GUTTER, live && 'text-accent')}>{icon}</span>
97
- <span className={clsx('shrink-0', live && 'chat-live-text')}>{label}</span>
98
- {detail && (
99
- <span data-find-field="summary" className="min-w-0 truncate font-mono text-text-faint">
100
- {command ? <ShellCommand command={detail} /> : path ? <MiddleTruncate text={detail} /> : detail}
101
- </span>
157
+ <span className={clsx('shrink-0', live && 'chat-live-text')}>
158
+ {brand !== undefined && <span className={BRAND}>{brand}</span>}
159
+ {brand !== undefined && label !== '' && ' '}
160
+ {label}
161
+ </span>
162
+ {look !== undefined ? (
163
+ <>
164
+ {look.run.detail !== '' && (
165
+ <span data-find-field="summary" className="min-w-0 truncate font-mono text-text-faint">
166
+ <LookDetail run={look.run} chatId={look.chatId} />
167
+ </span>
168
+ )}
169
+ <LookTags run={look.run} />
170
+ </>
171
+ ) : (
172
+ detail && (
173
+ <span data-find-field="summary" className="min-w-0 truncate font-mono text-text-faint">
174
+ {command ? <ShellCommand command={detail} /> : path ? <MiddleTruncate text={detail} /> : detail}
175
+ </span>
176
+ )
102
177
  )}
103
178
  <span className="grow" />
104
179
  {trailing}
@@ -237,6 +312,7 @@ function ToolBody({ tool }: { tool: ChatToolItem }) {
237
312
  */
238
313
  export function WorkRow({
239
314
  tool,
315
+ chatId,
240
316
  nested,
241
317
  detail,
242
318
  icon,
@@ -244,6 +320,8 @@ export function WorkRow({
244
320
  failed
245
321
  }: {
246
322
  tool: ChatToolItem;
323
+ /* The chat the call is in, which names the words of a look's run. */
324
+ chatId?: string;
247
325
  nested?: boolean;
248
326
  detail?: string;
249
327
  icon?: ReactNode;
@@ -257,13 +335,17 @@ export function WorkRow({
257
335
  useOpenForFind(tool.id, 'output', setOpen);
258
336
  const summary = detail ?? toolSummary(tool.name, tool.input);
259
337
  const opens = hasToolBody(tool);
338
+ const shell = shellCommand(tool.name, tool.input);
339
+ const run = detail === undefined && icon === undefined && label === undefined && shell !== null ? matchCommandLook(shell, host.commandLooks) : null;
260
340
  return (
261
341
  <div data-find-item={tool.id} className={nested ? 'ml-6' : undefined}>
262
342
  <ToggleLine
263
- icon={icon ?? toolIcon(tool.name)}
264
- label={label ?? tool.name}
343
+ icon={run === null ? (icon ?? toolIcon(tool.name)) : <run.look.Mark size={12} />}
344
+ label={run === null ? (label ?? tool.name) : run.match.label}
345
+ brand={run?.look.brand}
265
346
  detail={summary}
266
- command={detail === undefined && summary === shellCommand(tool.name, tool.input)}
347
+ look={run === null ? undefined : { run, chatId: chatId ?? '' }}
348
+ command={detail === undefined && summary === shell}
267
349
  path={detail === undefined && toolSummaryIsPath(tool.name, tool.input)}
268
350
  open={open && opens}
269
351
  onToggle={opens ? () => setOpen((o) => !o) : undefined}
@@ -290,11 +372,16 @@ export function WorkLiveRow({ chatId, tool: derived }: { chatId: string; tool: C
290
372
  const tail = liveOutput(tool);
291
373
  const summary = toolSummary(tool.name, tool.input);
292
374
  const opens = hasToolBody(tool);
375
+ const shell = shellCommand(tool.name, tool.input);
376
+ const { commandLooks } = useChatHost();
377
+ const run = shell === null ? null : matchCommandLook(shell, commandLooks);
293
378
  return (
294
379
  <div>
295
380
  <ToggleLine
296
- icon={toolIcon(tool.name)}
297
- label={tool.name}
381
+ icon={run === null ? toolIcon(tool.name) : <run.look.Mark size={12} />}
382
+ label={run === null ? tool.name : run.match.label}
383
+ brand={run?.look.brand}
384
+ look={run === null ? undefined : { run, chatId }}
298
385
  detail={summary || tool.progress?.description || ''}
299
386
  command={summary !== '' && summary === shellCommand(tool.name, tool.input)}
300
387
  path={summary !== '' && toolSummaryIsPath(tool.name, tool.input)}
@@ -313,12 +400,31 @@ export function WorkLiveRow({ chatId, tool: derived }: { chatId: string; tool: C
313
400
  );
314
401
  }
315
402
 
316
- /* A run of tool calls as one line ("Read 4 files"); opening it lists every call. */
317
- export function WorkGroupRow({ tools, summary, expanded, onToggle }: { tools: ChatToolItem[]; summary: string; expanded: boolean; onToggle(): void }) {
403
+ /* A run of tool calls as one line ("Read 4 files"); opening it lists every call. A run of one look wears its mark and its brand. */
404
+ export function WorkGroupRow({
405
+ tools,
406
+ summary,
407
+ look,
408
+ expanded,
409
+ onToggle
410
+ }: {
411
+ tools: ChatToolItem[];
412
+ summary: string;
413
+ look?: CommandLook;
414
+ expanded: boolean;
415
+ onToggle(): void;
416
+ }) {
318
417
  const failed = tools.some((tool) => tool.state === 'error');
319
418
  return (
320
419
  <div>
321
- <ToggleLine icon={toolIcon(tools[0]!.name)} label={summary} open={expanded} onToggle={onToggle} failed={failed} time={tools[0]!.createdAt} />
420
+ <ToggleLine
421
+ icon={look === undefined ? toolIcon(tools[0]!.name) : <look.Mark size={12} />}
422
+ label={look?.brand === undefined ? summary : withBrand(summary, [look.brand])}
423
+ open={expanded}
424
+ onToggle={onToggle}
425
+ failed={failed}
426
+ time={tools[0]!.createdAt}
427
+ />
322
428
  </div>
323
429
  );
324
430
  }
@@ -340,6 +446,8 @@ export function TurnFoldRow({
340
446
  expanded: boolean;
341
447
  onToggle(): void;
342
448
  }) {
449
+ const { commandLooks } = useChatHost();
450
+ const brands = commandLooks?.flatMap(({ brand }) => (brand === undefined ? [] : [brand])) ?? [];
343
451
  return (
344
452
  <button
345
453
  className={clsx(
@@ -353,7 +461,16 @@ export function TurnFoldRow({
353
461
  <Icon icon={ChevronRight} size={12} className={clsx('transition-transform', expanded && 'rotate-90')} />
354
462
  </span>
355
463
  <span className="shrink-0 group-hover:text-text">{label}</span>
356
- {work.length > 0 && <span className="min-w-0 truncate text-text-faint">{work.map((part) => ` · ${part}`).join('')}</span>}
464
+ {work.length > 0 && (
465
+ <span className="min-w-0 truncate text-text-faint">
466
+ {work.map((part, index) => (
467
+ <Fragment key={index}>
468
+ {' · '}
469
+ {withBrand(part, brands)}
470
+ </Fragment>
471
+ ))}
472
+ </span>
473
+ )}
357
474
  <span className="grow" />
358
475
  <RowTime at={turn.createdAt} />
359
476
  </button>
@@ -99,7 +99,7 @@ export function WorkflowRow({
99
99
  <div>
100
100
  <div className="flex items-center gap-1">
101
101
  <div className="min-w-0 grow">
102
- {running ? <WorkLiveRow chatId={chatId ?? ''} tool={tool} /> : <WorkRow tool={tool} detail={workflow.name ?? undefined} />}
102
+ {running ? <WorkLiveRow chatId={chatId ?? ''} tool={tool} /> : <WorkRow chatId={chatId} tool={tool} detail={workflow.name ?? undefined} />}
103
103
  </div>
104
104
  {running && chatId && workflow.taskId && <IconButton icon={Square} size="xs" label={t('activity.stop')} onClick={stop} />}
105
105
  </div>
package/src/host.ts CHANGED
@@ -195,6 +195,47 @@ export interface ReplyAuthor {
195
195
  mark?: ReactNode;
196
196
  }
197
197
 
198
+ /* A name a host gives a word of a command, such as the title of what an id stands for. */
199
+ export interface CommandName {
200
+ name: string;
201
+ /* Drawn before the name, at the size of the detail. */
202
+ icon?: ReactNode;
203
+ }
204
+
205
+ /* What one run of a program is about, as its look reads it from the words. */
206
+ export interface CommandLookMatch {
207
+ /* Drawn where the tool name is, after the brand; empty draws the brand alone. */
208
+ label: string;
209
+ /* The word the detail starts at, counted from the first word after the program; the detail is the source from there, as it was typed. */
210
+ detailFrom: number;
211
+ /* Calls with the same key read as one sentence in a group or a folded turn. */
212
+ group?: { key: string; sentence(count: number): string };
213
+ /* Options whose value the line shortens past 40 characters, such as a prompt. */
214
+ fold?: readonly string[];
215
+ /* Options whose value is code, by Shiki language id, such as `{ '--sql': 'sql' }`. */
216
+ languages?: Readonly<Record<string, string>>;
217
+ }
218
+
219
+ /* A program the app recognizes inside a shell call and draws as a tool of its own. */
220
+ export interface CommandLook {
221
+ /* The program as it is run, matched on its last path segment. */
222
+ program: string;
223
+ /* Drawn in semibold before every label of this look, such as a product name. */
224
+ brand?: string;
225
+ /* The mark in the gutter, drawn in `currentColor` at the size of a tool icon. */
226
+ Mark: ComponentType<{ size: number }>;
227
+ /* What one run is about, from its words with the quotes taken out; null leaves it a shell call. */
228
+ describe(args: readonly string[]): CommandLookMatch | null;
229
+ /* Calls of this look under different group keys, as one sentence; left out, such a run reads as shell calls. */
230
+ sentence?(count: number): string;
231
+ /*
232
+ * Names for words in the detail, such as ids, as a hook the row calls once. A word it names is drawn as
233
+ * that name, with the word itself in a tooltip; null leaves the word as it was typed. `option` is the
234
+ * option the word is the value of, or null for a word that follows none.
235
+ */
236
+ useNames?(scopeId: string, chatId: string): (word: string, option: string | null) => CommandName | null;
237
+ }
238
+
198
239
  /*
199
240
  * Where the tiles of a Map come from. The host owns the service and whatever admits a request to it, so
200
241
  * a chat never knows an address, a key or a vendor: it asks for the style of the theme and lets the host
@@ -363,6 +404,8 @@ export interface ChatHost {
363
404
  useReplyAuthor(scopeId: string, chatId: string): ReplyAuthor | null;
364
405
  /* The pages agents publish, drawn in the thread above their reply; null draws none. */
365
406
  visuals: VisualHost | null;
407
+ /* Programs drawn as tools of their own; left out, every shell call reads as itself. */
408
+ commandLooks?: readonly CommandLook[];
366
409
  }
367
410
 
368
411
  const NO_PROMPTS: readonly HostPrompt[] = [];
package/src/index.ts CHANGED
@@ -15,6 +15,9 @@ export {
15
15
  type ReplyAuthor,
16
16
  type ChatHost,
17
17
  type UiMapSource,
18
+ type CommandLook,
19
+ type CommandLookMatch,
20
+ type CommandName,
18
21
  ChatHostProvider,
19
22
  useChatHost,
20
23
  type AccentChoice,
@@ -198,7 +198,11 @@ export default {
198
198
  edits_one: '{{count}} edit',
199
199
  edits_other: '{{count}} edits',
200
200
  workingFor: 'Working for',
201
- exitCode: 'Exit code {{code}}'
201
+ exitCode: 'Exit code {{code}}',
202
+ alsoRan: '+ {{programs}}',
203
+ heredoc: 'heredoc',
204
+ moreRuns_one: '+{{count}}',
205
+ moreRuns_other: '+{{count}}'
202
206
  },
203
207
  timeline: {
204
208
  welcome: {
@@ -223,7 +227,7 @@ export default {
223
227
  menu: {
224
228
  copyMessage: 'Copy message',
225
229
  copyCode: 'Copy code',
226
- copyMarkdown: 'Copy as markdown',
230
+ copyMarkdown: 'Copy as Markdown',
227
231
  forkFromHere: 'Fork from here',
228
232
  openInPreview: 'Open in preview'
229
233
  }
@@ -198,7 +198,11 @@ export default {
198
198
  edits_one: '{{count}} bewerking',
199
199
  edits_other: '{{count}} bewerkingen',
200
200
  workingFor: 'Werkt al',
201
- exitCode: 'Afsluitcode {{code}}'
201
+ exitCode: 'Afsluitcode {{code}}',
202
+ alsoRan: '+ {{programs}}',
203
+ heredoc: 'heredoc',
204
+ moreRuns_one: '+{{count}}',
205
+ moreRuns_other: '+{{count}}'
202
206
  },
203
207
  timeline: {
204
208
  welcome: {
@@ -223,7 +227,7 @@ export default {
223
227
  menu: {
224
228
  copyMessage: 'Bericht kopiëren',
225
229
  copyCode: 'Code kopiëren',
226
- copyMarkdown: 'Kopiëren als markdown',
230
+ copyMarkdown: 'Kopiëren als Markdown',
227
231
  forkFromHere: 'Vanaf hier forken',
228
232
  openInPreview: 'Openen in preview'
229
233
  }