@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
@@ -35,13 +35,13 @@ export function Row({ row, chatId, toggleGroup, toggleTurn, toggleSubagent, open
35
35
  case 'live-thought':
36
36
  return _jsx(LiveThoughtRow, { chatId: chatId, item: row.item });
37
37
  case 'work':
38
- return isImageGeneration(row.tool) ? _jsx(GeneratedImageRow, { chatId: chatId, tool: row.tool }) : _jsx(WorkRow, { tool: row.tool });
38
+ return isImageGeneration(row.tool) ? _jsx(GeneratedImageRow, { chatId: chatId, tool: row.tool }) : _jsx(WorkRow, { chatId: chatId, tool: row.tool });
39
39
  case 'work-live':
40
40
  return isImageGeneration(row.tool) ? _jsx(GeneratedImageRow, { chatId: chatId, tool: row.tool }) : _jsx(WorkLiveRow, { chatId: chatId, tool: row.tool });
41
41
  case 'workflow':
42
42
  return _jsx(WorkflowRow, { chatId: chatId, tool: row.tool, workflow: row.workflow, onOpenAgent: openConversation });
43
43
  case 'work-group':
44
- return _jsx(WorkGroupRow, { tools: row.tools, summary: row.summary, expanded: row.expanded, onToggle: () => toggleGroup(row.id) });
44
+ return _jsx(WorkGroupRow, { tools: row.tools, summary: row.summary, look: row.look, expanded: row.expanded, onToggle: () => toggleGroup(row.id) });
45
45
  case 'subagent':
46
46
  return _jsx(SubagentBranchRow, { chatId: chatId, branch: row, onToggle: toggleSubagent, onOpenConversation: openConversation });
47
47
  case 'turn-fold':
@@ -29,7 +29,7 @@ function ChildRow({ chatId, item }) {
29
29
  if (isImageGeneration(item)) {
30
30
  return _jsx(GeneratedImageRow, { chatId: chatId, tool: item });
31
31
  }
32
- return item.state === 'running' ? _jsx(WorkLiveRow, { chatId: chatId, tool: item }) : _jsx(WorkRow, { tool: item });
32
+ return item.state === 'running' ? _jsx(WorkLiveRow, { chatId: chatId, tool: item }) : _jsx(WorkRow, { chatId: chatId, tool: item });
33
33
  }
34
34
  if (item.kind === 'assistant') {
35
35
  return _jsx(ChildText, { chatId: chatId, item: item });
@@ -1,11 +1,18 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { ChatCheckpointDiff, ChatToolItem, ChatTurnItem } from '@adecore/agent-contracts';
3
3
  import type { BaseMoved } from '../../logic/timeline.js';
4
- export declare function ToggleLine({ icon, label, detail, command, path, open, onToggle, failed, live, trailing, time, className }: {
4
+ import { type CommandLookRun } from '../../logic/command-looks.js';
5
+ import { type CommandLook } from '../../../host.js';
6
+ export declare function ToggleLine({ icon, label, brand, detail, command, look, path, open, onToggle, failed, live, trailing, time, className }: {
5
7
  icon: ReactNode;
6
- label: string;
8
+ label: ReactNode;
9
+ brand?: string;
7
10
  detail?: string;
8
11
  command?: boolean;
12
+ look?: {
13
+ run: CommandLookRun;
14
+ chatId: string;
15
+ };
9
16
  path?: boolean;
10
17
  open?: boolean;
11
18
  onToggle?(): void;
@@ -15,8 +22,9 @@ export declare function ToggleLine({ icon, label, detail, command, path, open, o
15
22
  time?: number;
16
23
  className?: string;
17
24
  }): import("react").JSX.Element;
18
- export declare function WorkRow({ tool, nested, detail, icon, label, failed }: {
25
+ export declare function WorkRow({ tool, chatId, nested, detail, icon, label, failed }: {
19
26
  tool: ChatToolItem;
27
+ chatId?: string;
20
28
  nested?: boolean;
21
29
  detail?: string;
22
30
  icon?: ReactNode;
@@ -30,9 +38,10 @@ export declare function WorkLiveRow({ chatId, tool: derived }: {
30
38
  chatId: string;
31
39
  tool: ChatToolItem;
32
40
  }): import("react").JSX.Element;
33
- export declare function WorkGroupRow({ tools, summary, expanded, onToggle }: {
41
+ export declare function WorkGroupRow({ tools, summary, look, expanded, onToggle }: {
34
42
  tools: ChatToolItem[];
35
43
  summary: string;
44
+ look?: CommandLook;
36
45
  expanded: boolean;
37
46
  onToggle(): void;
38
47
  }): import("react").JSX.Element;
@@ -1,15 +1,16 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Fragment, useEffect, useState } from 'react';
2
+ import { Fragment, useContext, useEffect, useState } from 'react';
3
3
  import clsx from 'clsx';
4
4
  import i18next from 'i18next';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { ChevronRight, FileDiff, X } from 'lucide-react';
7
+ import { matchCommandLook } from '../../logic/command-looks.js';
7
8
  import { exitCodeOf, fileChanges, hasOutput, hasToolBody, liveOutput, readImagePath, shellCommand, toolCallLines, toolStartedAt, toolSummary, toolSummaryIsPath, unifiedChanges } from '../../logic/tools.js';
8
9
  import { useOpenForFind } from '../find-reveal.js';
9
10
  import { useChatHost } from '../../../host.js';
10
11
  import { lazyNamed } from '../../../lazy.js';
11
12
  import { useChatActions } from '../../actions.js';
12
- import { useChatScope } from '../../../scope.js';
13
+ import { ChatScopeContext, useChatScope } from '../../../scope.js';
13
14
  import { useCurrentItem } from '../../../state/chats.js';
14
15
  import { useTimelineFlash } from '../../timeline-flash.js';
15
16
  import { formatClockDuration, formatElapsedShort } from '@adecore/ui/format';
@@ -31,15 +32,51 @@ function clipNotice(text) {
31
32
  function clip(text) {
32
33
  return text.slice(0, OUTPUT_LIMIT) + clipNotice(text);
33
34
  }
35
+ const BRAND = 'font-semibold';
36
+ /* `text` with `brand` in semibold where it names it, as a sentence about a look does. */
37
+ function withBrand(text, brands) {
38
+ for (const brand of brands) {
39
+ const at = brand === '' ? -1 : text.indexOf(brand);
40
+ if (at !== -1) {
41
+ return (_jsxs(_Fragment, { children: [text.slice(0, at), _jsx("span", { className: BRAND, children: brand }), text.slice(at + brand.length)] }));
42
+ }
43
+ }
44
+ return text;
45
+ }
46
+ /* The detail of a look's run, with the words its host names drawn as those names. */
47
+ function NamedLookDetail({ run, chatId, useNames }) {
48
+ const scope = useContext(ChatScopeContext);
49
+ const nameOf = useNames(scope?.id ?? '', chatId);
50
+ const names = run.words.flatMap((word) => {
51
+ const name = nameOf(word.text, word.option);
52
+ return name === null ? [] : [{ start: word.start, end: word.end, word: word.text, name }];
53
+ });
54
+ return _jsx(ShellCommand, { command: run.detail, code: run.code, names: names });
55
+ }
56
+ function LookDetail({ run, chatId }) {
57
+ const { useNames } = run.look;
58
+ if (useNames === undefined) {
59
+ return _jsx(ShellCommand, { command: run.detail, code: run.code });
60
+ }
61
+ // Keyed by the look, so a row whose command turns into another look's starts its hooks again.
62
+ return _jsx(NamedLookDetail, { run: run, chatId: chatId, useNames: useNames }, run.look.program);
63
+ }
64
+ const TAG = 'shrink-0 rounded bg-surface-hover px-1.5 text-text-faint';
65
+ /* What else the line ran, in quiet tags: the other programs and a heredoc, and how many more runs of a look. */
66
+ function LookTags({ run }) {
67
+ const { t } = useTranslation('agent-chat');
68
+ const also = run.heredoc ? [...run.others, t('work.heredoc')] : run.others;
69
+ return (_jsxs(_Fragment, { children: [also.length > 0 && _jsx("span", { className: TAG, children: t('work.alsoRan', { programs: also.join(', ') }) }), run.more > 0 && _jsx("span", { className: TAG, children: t('work.moreRuns', { count: run.more }) })] }));
70
+ }
34
71
  /*
35
72
  * The line of a call, a subagent or a group. Without `onToggle` it has nothing to open: no chevron, and
36
73
  * not a button. `time` shows at the right edge while the line is hovered or focused.
37
74
  */
38
- export function ToggleLine({ icon, label, detail, command, path, open = false, onToggle, failed, live, trailing, time, className }) {
75
+ export function ToggleLine({ icon, label, brand, detail, command, look, path, open = false, onToggle, failed, live, trailing, time, className }) {
39
76
  const classes = clsx(
40
77
  // Fix the width so long commands reach the truncation inside the negative margins.
41
78
  'group/line -mx-1 mb-0.5 flex h-7 w-[calc(100%+8px)] items-center gap-2 rounded-md px-1 text-left text-xs text-text-muted', onToggle !== undefined && 'hover:bg-surface-hover', failed && 'text-status-error', className);
42
- const content = (_jsxs(_Fragment, { children: [_jsx("span", { className: clsx(ROW_GUTTER, live && 'text-accent'), children: icon }), _jsx("span", { className: clsx('shrink-0', live && 'chat-live-text'), children: label }), detail && (_jsx("span", { "data-find-field": "summary", className: "min-w-0 truncate font-mono text-text-faint", children: command ? _jsx(ShellCommand, { command: detail }) : path ? _jsx(MiddleTruncate, { text: detail }) : detail })), _jsx("span", { className: "grow" }), trailing, time !== undefined && _jsx(RowTime, { at: time }), failed && _jsx(Icon, { icon: X, size: 12, className: "shrink-0" }), onToggle !== undefined && (_jsx(Icon, { icon: ChevronRight, size: 12, className: clsx('shrink-0 text-text-faint transition-transform', open && 'rotate-90') }))] }));
79
+ const content = (_jsxs(_Fragment, { children: [_jsx("span", { className: clsx(ROW_GUTTER, live && 'text-accent'), children: icon }), _jsxs("span", { className: clsx('shrink-0', live && 'chat-live-text'), children: [brand !== undefined && _jsx("span", { className: BRAND, children: brand }), brand !== undefined && label !== '' && ' ', label] }), look !== undefined ? (_jsxs(_Fragment, { children: [look.run.detail !== '' && (_jsx("span", { "data-find-field": "summary", className: "min-w-0 truncate font-mono text-text-faint", children: _jsx(LookDetail, { run: look.run, chatId: look.chatId }) })), _jsx(LookTags, { run: look.run })] })) : (detail && (_jsx("span", { "data-find-field": "summary", className: "min-w-0 truncate font-mono text-text-faint", children: command ? _jsx(ShellCommand, { command: detail }) : path ? _jsx(MiddleTruncate, { text: detail }) : detail }))), _jsx("span", { className: "grow" }), trailing, time !== undefined && _jsx(RowTime, { at: time }), failed && _jsx(Icon, { icon: X, size: 12, className: "shrink-0" }), onToggle !== undefined && (_jsx(Icon, { icon: ChevronRight, size: 12, className: clsx('shrink-0 text-text-faint transition-transform', open && 'rotate-90') }))] }));
43
80
  if (onToggle === undefined) {
44
81
  return _jsx("div", { className: classes, children: content });
45
82
  }
@@ -82,7 +119,7 @@ function ToolBody({ tool }) {
82
119
  * One settled tool call: a line, and what it was asked and answered behind it. An image the call
83
120
  * looked at is drawn under the line. `icon`, `label` and `failed` let a row that knows the call better name it.
84
121
  */
85
- export function WorkRow({ tool, nested, detail, icon, label, failed }) {
122
+ export function WorkRow({ tool, chatId, nested, detail, icon, label, failed }) {
86
123
  const host = useChatHost();
87
124
  const [open, setOpen] = useState(false);
88
125
  const image = tool.state === 'done' ? readImagePath(tool.name, tool.input) : null;
@@ -90,7 +127,9 @@ export function WorkRow({ tool, nested, detail, icon, label, failed }) {
90
127
  useOpenForFind(tool.id, 'output', setOpen);
91
128
  const summary = detail ?? toolSummary(tool.name, tool.input);
92
129
  const opens = hasToolBody(tool);
93
- return (_jsxs("div", { "data-find-item": tool.id, className: nested ? 'ml-6' : undefined, children: [_jsx(ToggleLine, { icon: icon ?? toolIcon(tool.name), label: label ?? tool.name, detail: summary, command: detail === undefined && summary === shellCommand(tool.name, tool.input), path: detail === undefined && toolSummaryIsPath(tool.name, tool.input), open: open && opens, onToggle: opens ? () => setOpen((o) => !o) : undefined, failed: failed ?? tool.state === 'error', time: tool.createdAt }), image !== null && ReadImage !== null && _jsx(ReadImage, { path: image }), open && opens && _jsx(ToolBody, { tool: tool })] }));
130
+ const shell = shellCommand(tool.name, tool.input);
131
+ const run = detail === undefined && icon === undefined && label === undefined && shell !== null ? matchCommandLook(shell, host.commandLooks) : null;
132
+ return (_jsxs("div", { "data-find-item": tool.id, className: nested ? 'ml-6' : undefined, children: [_jsx(ToggleLine, { icon: run === null ? (icon ?? toolIcon(tool.name)) : _jsx(run.look.Mark, { size: 12 }), label: run === null ? (label ?? tool.name) : run.match.label, brand: run?.look.brand, detail: summary, look: run === null ? undefined : { run, chatId: chatId ?? '' }, command: detail === undefined && summary === shell, path: detail === undefined && toolSummaryIsPath(tool.name, tool.input), open: open && opens, onToggle: opens ? () => setOpen((o) => !o) : undefined, failed: failed ?? tool.state === 'error', time: tool.createdAt }), image !== null && ReadImage !== null && _jsx(ReadImage, { path: image }), open && opens && _jsx(ToolBody, { tool: tool })] }));
94
133
  }
95
134
  /* "running for 12s" next to a live call. */
96
135
  export function RunningFor({ startedAt }) {
@@ -105,19 +144,24 @@ export function WorkLiveRow({ chatId, tool: derived }) {
105
144
  const tail = liveOutput(tool);
106
145
  const summary = toolSummary(tool.name, tool.input);
107
146
  const opens = hasToolBody(tool);
108
- return (_jsxs("div", { children: [_jsx(ToggleLine, { icon: toolIcon(tool.name), label: tool.name, detail: summary || tool.progress?.description || '', command: summary !== '' && summary === shellCommand(tool.name, tool.input), path: summary !== '' && toolSummaryIsPath(tool.name, tool.input), open: open && opens, onToggle: opens ? () => setOpen((o) => !o) : undefined, trailing: _jsx(RunningFor, { startedAt: toolStartedAt(tool) }), live: true }), open && opens && _jsx(ToolBody, { tool: tool }), tail !== null && !open && (_jsx("pre", { className: "mt-1.5 mb-2 ml-6 max-h-48 overflow-auto rounded-md border border-border bg-surface-raised px-3 py-2 font-mono text-code whitespace-pre-wrap text-chat-output select-text", children: _jsx(AnsiOutput, { text: tail, tailLines: 12 }) }))] }));
147
+ const shell = shellCommand(tool.name, tool.input);
148
+ const { commandLooks } = useChatHost();
149
+ const run = shell === null ? null : matchCommandLook(shell, commandLooks);
150
+ return (_jsxs("div", { children: [_jsx(ToggleLine, { icon: run === null ? toolIcon(tool.name) : _jsx(run.look.Mark, { size: 12 }), label: run === null ? tool.name : run.match.label, brand: run?.look.brand, look: run === null ? undefined : { run, chatId }, detail: summary || tool.progress?.description || '', command: summary !== '' && summary === shellCommand(tool.name, tool.input), path: summary !== '' && toolSummaryIsPath(tool.name, tool.input), open: open && opens, onToggle: opens ? () => setOpen((o) => !o) : undefined, trailing: _jsx(RunningFor, { startedAt: toolStartedAt(tool) }), live: true }), open && opens && _jsx(ToolBody, { tool: tool }), tail !== null && !open && (_jsx("pre", { className: "mt-1.5 mb-2 ml-6 max-h-48 overflow-auto rounded-md border border-border bg-surface-raised px-3 py-2 font-mono text-code whitespace-pre-wrap text-chat-output select-text", children: _jsx(AnsiOutput, { text: tail, tailLines: 12 }) }))] }));
109
151
  }
110
- /* A run of tool calls as one line ("Read 4 files"); opening it lists every call. */
111
- export function WorkGroupRow({ tools, summary, expanded, onToggle }) {
152
+ /* 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. */
153
+ export function WorkGroupRow({ tools, summary, look, expanded, onToggle }) {
112
154
  const failed = tools.some((tool) => tool.state === 'error');
113
- return (_jsx("div", { children: _jsx(ToggleLine, { icon: toolIcon(tools[0].name), label: summary, open: expanded, onToggle: onToggle, failed: failed, time: tools[0].createdAt }) }));
155
+ return (_jsx("div", { children: _jsx(ToggleLine, { icon: look === undefined ? toolIcon(tools[0].name) : _jsx(look.Mark, { size: 12 }), label: look?.brand === undefined ? summary : withBrand(summary, [look.brand]), open: expanded, onToggle: onToggle, failed: failed, time: tools[0].createdAt }) }));
114
156
  }
115
157
  /*
116
158
  * A settled turn folded behind how long it took and what it did. A line and not a button, like the
117
159
  * tool lines it hides; the work reads fainter than the time so the eye lands on the time first.
118
160
  */
119
161
  export function TurnFoldRow({ turn, label, work, expanded, onToggle }) {
120
- return (_jsxs("button", { className: clsx('group group/line -mx-1 mb-2 flex h-7 w-[calc(100%+8px)] min-w-0 items-center gap-2 rounded-md px-1 text-left text-xs text-text-muted hover:bg-surface-hover', turn.state === 'error' && 'text-status-error'), "aria-expanded": expanded, onClick: onToggle, children: [_jsx("span", { className: ROW_GUTTER, children: _jsx(Icon, { icon: ChevronRight, size: 12, className: clsx('transition-transform', expanded && 'rotate-90') }) }), _jsx("span", { className: "shrink-0 group-hover:text-text", children: label }), work.length > 0 && _jsx("span", { className: "min-w-0 truncate text-text-faint", children: work.map((part) => ` · ${part}`).join('') }), _jsx("span", { className: "grow" }), _jsx(RowTime, { at: turn.createdAt })] }));
162
+ const { commandLooks } = useChatHost();
163
+ const brands = commandLooks?.flatMap(({ brand }) => (brand === undefined ? [] : [brand])) ?? [];
164
+ return (_jsxs("button", { className: clsx('group group/line -mx-1 mb-2 flex h-7 w-[calc(100%+8px)] min-w-0 items-center gap-2 rounded-md px-1 text-left text-xs text-text-muted hover:bg-surface-hover', turn.state === 'error' && 'text-status-error'), "aria-expanded": expanded, onClick: onToggle, children: [_jsx("span", { className: ROW_GUTTER, children: _jsx(Icon, { icon: ChevronRight, size: 12, className: clsx('transition-transform', expanded && 'rotate-90') }) }), _jsx("span", { className: "shrink-0 group-hover:text-text", children: label }), work.length > 0 && (_jsx("span", { className: "min-w-0 truncate text-text-faint", children: work.map((part, index) => (_jsxs(Fragment, { children: [' · ', withBrand(part, brands)] }, index))) })), _jsx("span", { className: "grow" }), _jsx(RowTime, { at: turn.createdAt })] }));
121
165
  }
122
166
  function omittedLabel(reason) {
123
167
  return reason === 'binary' ? i18next.t('agent-chat:work.omitted.binary') : i18next.t('agent-chat:work.omitted.tooLarge');
@@ -45,5 +45,5 @@ export function WorkflowRow({ tool, workflow, chatId, onOpenAgent }) {
45
45
  });
46
46
  }
47
47
  };
48
- return (_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("div", { className: "min-w-0 grow", children: running ? _jsx(WorkLiveRow, { chatId: chatId ?? '', tool: tool }) : _jsx(WorkRow, { tool: tool, detail: workflow.name ?? undefined }) }), running && chatId && workflow.taskId && _jsx(IconButton, { icon: Square, size: "xs", label: t('activity.stop'), onClick: stop })] }), running && workflow.stalledAt !== undefined && (_jsxs("p", { role: "status", className: "ml-6 mb-1 flex items-start gap-1.5 text-xs text-text-muted", children: [_jsx(Icon, { icon: TriangleAlert, size: 12, className: "mt-0.5 shrink-0 text-status-needs-you" }), _jsx("span", { children: t('rows.workflow.stalled') })] })), phases.length > 0 && (_jsx("div", { className: "ml-6", children: phases.map((phase) => (_jsx(PhaseBlock, { phase: phase, onOpen: onOpen }, phase.key))) }))] }));
48
+ return (_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("div", { className: "min-w-0 grow", children: running ? _jsx(WorkLiveRow, { chatId: chatId ?? '', tool: tool }) : _jsx(WorkRow, { chatId: chatId, tool: tool, detail: workflow.name ?? undefined }) }), running && chatId && workflow.taskId && _jsx(IconButton, { icon: Square, size: "xs", label: t('activity.stop'), onClick: stop })] }), running && workflow.stalledAt !== undefined && (_jsxs("p", { role: "status", className: "ml-6 mb-1 flex items-start gap-1.5 text-xs text-text-muted", children: [_jsx(Icon, { icon: TriangleAlert, size: 12, className: "mt-0.5 shrink-0 text-status-needs-you" }), _jsx("span", { children: t('rows.workflow.stalled') })] })), phases.length > 0 && (_jsx("div", { className: "ml-6", children: phases.map((phase) => (_jsx(PhaseBlock, { phase: phase, onOpen: onOpen }, phase.key))) }))] }));
49
49
  }
package/dist/host.d.ts CHANGED
@@ -125,6 +125,30 @@ export interface ReplyAuthor {
125
125
  name: string;
126
126
  mark?: ReactNode;
127
127
  }
128
+ export interface CommandName {
129
+ name: string;
130
+ icon?: ReactNode;
131
+ }
132
+ export interface CommandLookMatch {
133
+ label: string;
134
+ detailFrom: number;
135
+ group?: {
136
+ key: string;
137
+ sentence(count: number): string;
138
+ };
139
+ fold?: readonly string[];
140
+ languages?: Readonly<Record<string, string>>;
141
+ }
142
+ export interface CommandLook {
143
+ program: string;
144
+ brand?: string;
145
+ Mark: ComponentType<{
146
+ size: number;
147
+ }>;
148
+ describe(args: readonly string[]): CommandLookMatch | null;
149
+ sentence?(count: number): string;
150
+ useNames?(scopeId: string, chatId: string): (word: string, option: string | null) => CommandName | null;
151
+ }
128
152
  export type UiMapSource = {
129
153
  state: 'tiles';
130
154
  styleUrl(theme: 'light' | 'dark'): string;
@@ -240,6 +264,7 @@ export interface ChatHost {
240
264
  useThreadCards(scopeId: string, chatId: string): readonly ThreadCard[];
241
265
  useReplyAuthor(scopeId: string, chatId: string): ReplyAuthor | null;
242
266
  visuals: VisualHost | null;
267
+ commandLooks?: readonly CommandLook[];
243
268
  }
244
269
  export declare function ChatHostProvider({ host, children }: {
245
270
  host: Partial<ChatHost>;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { type ChatActions, type FileRef, type ShellCodeBlockContext, type ChatPlace, type SubagentTask, type TimelineFindOptions, type TimelineFind, type ComposerDictationProps, type ComposerSlotProps, type SubagentSlotProps, type ThreadCard, type VisualHost, type ReplyAuthor, type ChatHost, type UiMapSource, ChatHostProvider, useChatHost, type AccentChoice, type ChatToast, type TextareaProps, type ResourceUrl } from './host.js';
1
+ export { type ChatActions, type FileRef, type ShellCodeBlockContext, type ChatPlace, type SubagentTask, type TimelineFindOptions, type TimelineFind, type ComposerDictationProps, type ComposerSlotProps, type SubagentSlotProps, type ThreadCard, type VisualHost, type ReplyAuthor, type ChatHost, type UiMapSource, type CommandLook, type CommandLookMatch, type CommandName, ChatHostProvider, useChatHost, type AccentChoice, type ChatToast, type TextareaProps, type ResourceUrl } from './host.js';
2
2
  export { type ChatScope, ChatScopeContext, useChatScope } from './scope.js';
3
3
  export { type ChatRequestMap, type ChatEventMap, type ChatTransportStatus, type ChatTransport, ChatTransportError, errorCode, isConnectionError } from './transport.js';
4
4
  export { portTransport, type PortTransport } from './port-transport.js';
@@ -199,6 +199,10 @@ declare const _default: {
199
199
  edits_other: string;
200
200
  workingFor: string;
201
201
  exitCode: string;
202
+ alsoRan: string;
203
+ heredoc: string;
204
+ moreRuns_one: string;
205
+ moreRuns_other: string;
202
206
  };
203
207
  timeline: {
204
208
  welcome: {
@@ -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
  }
@@ -199,6 +199,10 @@ declare const _default: {
199
199
  edits_other: string;
200
200
  workingFor: string;
201
201
  exitCode: string;
202
+ alsoRan: string;
203
+ heredoc: string;
204
+ moreRuns_one: string;
205
+ moreRuns_other: string;
202
206
  };
203
207
  timeline: {
204
208
  welcome: {
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents-react",
3
- "version": "0.25.8",
3
+ "version": "0.26.0",
4
4
  "description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -67,11 +67,11 @@
67
67
  "test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
68
68
  },
69
69
  "dependencies": {
70
- "@adecore/agent-contracts": "0.25.8",
71
- "@adecore/editor": "0.25.8",
72
- "@adecore/editor-react": "0.25.8",
73
- "@adecore/intelligent-ui": "0.25.8",
74
- "@adecore/merge": "0.25.8",
70
+ "@adecore/agent-contracts": "0.26.0",
71
+ "@adecore/editor": "0.26.0",
72
+ "@adecore/editor-react": "0.26.0",
73
+ "@adecore/intelligent-ui": "0.26.0",
74
+ "@adecore/merge": "0.26.0",
75
75
  "@codemirror/commands": "^6.11.1",
76
76
  "@codemirror/language": "^6.12.4",
77
77
  "@codemirror/state": "^6.7.6",
@@ -94,14 +94,14 @@
94
94
  "zustand": "^5.0.15"
95
95
  },
96
96
  "peerDependencies": {
97
- "@adecore/ui": "^0.25.8",
97
+ "@adecore/ui": "^0.26.0",
98
98
  "i18next": "^26.4.2",
99
99
  "react": "^19.3.0",
100
100
  "react-dom": "^19.3.0",
101
101
  "react-i18next": "^17.0.15"
102
102
  },
103
103
  "devDependencies": {
104
- "@adecore/ui": "0.25.8",
104
+ "@adecore/ui": "0.26.0",
105
105
  "@types/bun": "^1.4.2",
106
106
  "@types/react": "^19.3.0",
107
107
  "@types/react-dom": "^19.3.0",
package/src/chat/index.ts CHANGED
@@ -21,6 +21,7 @@ export {
21
21
  type ForkPoint,
22
22
  type ForkWorktreeChoice
23
23
  } from './logic/fork';
24
+ export { matchCommandLook, type CommandLookRun, type CommandLookWord } from './logic/command-looks';
24
25
  export { handbackReportOf } from './logic/handback';
25
26
  export { limitView, type LimitView } from './logic/limit';
26
27
  export type { TickKind, ScrubberTick } from './logic/scrubber';
@@ -0,0 +1,157 @@
1
+ import type { CommandLook, CommandLookMatch } from '../../host';
2
+ import { EMBEDDED_CODE_LIMIT, shellCommands, type EmbeddedCode, type ShellSimpleCommand, type ShellWord } from './embedded-code';
3
+
4
+ // A folded value keeps this many characters of what it says.
5
+ const FOLD_LENGTH = 40;
6
+ // Rows redraw often and a thread holds many commands, so each is read once per set of looks.
7
+ const CACHE_LIMIT = 300;
8
+
9
+ /* A word of a detail a host may give a name, as offsets into the detail. */
10
+ export interface CommandLookWord {
11
+ readonly start: number;
12
+ readonly end: number;
13
+ /* The word as the program gets it, quotes taken out. */
14
+ readonly text: string;
15
+ /* The option right before it, whose value it may be; null when it follows none. */
16
+ readonly option: string | null;
17
+ }
18
+
19
+ /* A shell call read as a tool of its own: the first command of the line a look knows. */
20
+ export interface CommandLookRun {
21
+ readonly look: CommandLook;
22
+ readonly match: CommandLookMatch;
23
+ /* The source from the detail word on, as typed, without redirects and with folded values shortened. */
24
+ readonly detail: string;
25
+ /* The values the match names a language for, as offsets into the detail. */
26
+ readonly code: readonly EmbeddedCode[];
27
+ /* The words of the detail that are no option. */
28
+ readonly words: readonly CommandLookWord[];
29
+ /* The programs of the other commands in the line, each once, in the order they run. */
30
+ readonly others: readonly string[];
31
+ /* How many more commands of the line a look knows. */
32
+ readonly more: number;
33
+ /* Whether the command is fed a heredoc, which the detail leaves out. */
34
+ readonly heredoc: boolean;
35
+ }
36
+
37
+ const caches = new WeakMap<readonly CommandLook[], Map<string, CommandLookRun | null>>();
38
+
39
+ /* The look of a simple command and what it says about it, or null when no look knows it. */
40
+ function lookOf(command: ShellSimpleCommand, looks: readonly CommandLook[]): { look: CommandLook; match: CommandLookMatch } | null {
41
+ const program = command.program;
42
+ if (program === null) {
43
+ return null;
44
+ }
45
+ const args = command.words.slice(program.index + 1).map(({ text }) => text);
46
+ for (const look of looks) {
47
+ if (look.program !== program.name) {
48
+ continue;
49
+ }
50
+ const match = look.describe(args);
51
+ if (match !== null) {
52
+ return { look, match };
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+
58
+ /* The source of `word`, with its value cut past the fold; the body comes back as offsets into what is returned. */
59
+ function foldedWord(source: string, word: ShellWord, fold: boolean): { text: string; body: { start: number; end: number } | null } {
60
+ const text = source.slice(word.start, word.end);
61
+ const body = word.body === null ? null : { start: word.body.start - word.start, end: word.body.end - word.start };
62
+ if (!fold) {
63
+ return { text, body };
64
+ }
65
+ if (body !== null && body.end - body.start > FOLD_LENGTH) {
66
+ const cut = body.start + FOLD_LENGTH;
67
+ return { text: `${text.slice(0, cut)}…${text.slice(body.end)}`, body: { start: body.start, end: cut + 1 } };
68
+ }
69
+ if (body === null && text.length > FOLD_LENGTH) {
70
+ return { text: `${text.slice(0, FOLD_LENGTH)}…`, body: null };
71
+ }
72
+ return { text, body };
73
+ }
74
+
75
+ /* The detail of a command from the word the match names: its words with their spacing, a redirect or heredoc between them a single space. */
76
+ function detailOf(source: string, command: ShellSimpleCommand, match: CommandLookMatch): Pick<CommandLookRun, 'detail' | 'code' | 'words'> {
77
+ const first = command.program!.index + 1 + Math.max(0, match.detailFrom);
78
+ let detail = '';
79
+ const code: EmbeddedCode[] = [];
80
+ const words: CommandLookWord[] = [];
81
+ let previous: ShellWord | null = null;
82
+ // The option the word before was, whose value this word may be.
83
+ let option: string | null = null;
84
+ for (const word of command.words.slice(first)) {
85
+ if (previous !== null) {
86
+ const gap = source.slice(previous.end, word.start);
87
+ detail += /^[ \t]*$/.test(gap) ? gap : ' ';
88
+ }
89
+ const start = detail.length;
90
+ const { text, body } = foldedWord(source, word, option !== null && match.fold?.includes(option) === true);
91
+ const language = option === null ? undefined : match.languages?.[option];
92
+ if (language !== undefined && body !== null && body.end > body.start) {
93
+ code.push({ start: start + body.start, end: start + body.end, language });
94
+ }
95
+ detail += text;
96
+ const isOption = word.text.startsWith('-') && word.text !== '-';
97
+ if (!isOption) {
98
+ words.push({ start, end: detail.length, text: word.text, option });
99
+ }
100
+ option = isOption ? word.text : null;
101
+ previous = word;
102
+ }
103
+ return { detail, code, words };
104
+ }
105
+
106
+ function read(command: string, looks: readonly CommandLook[]): CommandLookRun | null {
107
+ let found: { command: ShellSimpleCommand; look: CommandLook; match: CommandLookMatch } | null = null;
108
+ let more = 0;
109
+ const others: string[] = [];
110
+ for (const simple of shellCommands(command)) {
111
+ const known = lookOf(simple, looks);
112
+ if (known !== null && found === null) {
113
+ found = { command: simple, ...known };
114
+ } else if (known !== null) {
115
+ more++;
116
+ } else if (simple.program !== null && !others.includes(simple.program.name)) {
117
+ others.push(simple.program.name);
118
+ }
119
+ }
120
+ if (found === null) {
121
+ return null;
122
+ }
123
+ return {
124
+ look: found.look,
125
+ match: found.match,
126
+ ...detailOf(command, found.command, found.match),
127
+ others,
128
+ more,
129
+ heredoc: found.command.heredocs.length > 0
130
+ };
131
+ }
132
+
133
+ /*
134
+ * The first command of a shell line that one of `looks` knows, read as that look's tool: a program
135
+ * counts only where the shell runs it, past assignments and wrappers, so `grep tool` is no run of
136
+ * `tool`. Null for a line no look knows, and for one past the length that is drawn without looking inside.
137
+ */
138
+ export function matchCommandLook(command: string, looks: readonly CommandLook[] | undefined): CommandLookRun | null {
139
+ if (looks === undefined || looks.length === 0 || command.length > EMBEDDED_CODE_LIMIT || !looks.some(({ program }) => command.includes(program))) {
140
+ return null;
141
+ }
142
+ let cache = caches.get(looks);
143
+ if (cache === undefined) {
144
+ cache = new Map();
145
+ caches.set(looks, cache);
146
+ }
147
+ const known = cache.get(command);
148
+ if (known !== undefined) {
149
+ return known;
150
+ }
151
+ const run = read(command, looks);
152
+ cache.set(command, run);
153
+ if (cache.size > CACHE_LIMIT) {
154
+ cache.delete(cache.keys().next().value!);
155
+ }
156
+ return run;
157
+ }