@adecore/agents-react 0.25.8 → 0.25.9
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/dist/chat/index.d.ts +1 -0
- package/dist/chat/index.js +1 -0
- package/dist/chat/logic/command-looks.d.ts +19 -0
- package/dist/chat/logic/command-looks.js +123 -0
- package/dist/chat/logic/embedded-code.d.ts +25 -0
- package/dist/chat/logic/embedded-code.js +27 -5
- package/dist/chat/logic/timeline.d.ts +5 -3
- package/dist/chat/logic/timeline.js +64 -10
- package/dist/chat/ui/ShellCommand.d.ts +11 -1
- package/dist/chat/ui/ShellCommand.js +60 -20
- package/dist/chat/ui/SubagentTimeline.js +9 -1
- package/dist/chat/ui/Timeline.js +9 -2
- package/dist/chat/ui/rows/Rows.js +2 -2
- package/dist/chat/ui/rows/SubagentRow.js +1 -1
- package/dist/chat/ui/rows/WorkRows.d.ts +13 -4
- package/dist/chat/ui/rows/WorkRows.js +55 -11
- package/dist/chat/ui/rows/WorkflowRow.js +1 -1
- package/dist/host.d.ts +25 -0
- package/dist/index.d.ts +1 -1
- package/dist/locales/en/agent-chat.d.ts +4 -0
- package/dist/locales/en/agent-chat.js +5 -1
- package/dist/locales/nl/agent-chat.d.ts +4 -0
- package/dist/locales/nl/agent-chat.js +5 -1
- package/package.json +8 -8
- package/src/chat/index.ts +1 -0
- package/src/chat/logic/command-looks.ts +157 -0
- package/src/chat/logic/embedded-code.ts +57 -11
- package/src/chat/logic/timeline.ts +70 -13
- package/src/chat/ui/ShellCommand.tsx +83 -23
- package/src/chat/ui/SubagentTimeline.tsx +11 -2
- package/src/chat/ui/Timeline.tsx +9 -2
- package/src/chat/ui/rows/Rows.tsx +2 -2
- package/src/chat/ui/rows/SubagentRow.tsx +1 -1
- package/src/chat/ui/rows/WorkRows.tsx +136 -19
- package/src/chat/ui/rows/WorkflowRow.tsx +1 -1
- package/src/host.ts +43 -0
- package/src/index.ts +3 -0
- package/src/locales/en/agent-chat.ts +5 -1
- package/src/locales/nl/agent-chat.ts +5 -1
|
@@ -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
|
-
|
|
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:
|
|
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 }),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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';
|
|
@@ -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: {
|
|
@@ -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: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents-react",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.9",
|
|
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.
|
|
71
|
-
"@adecore/editor": "0.25.
|
|
72
|
-
"@adecore/editor-react": "0.25.
|
|
73
|
-
"@adecore/intelligent-ui": "0.25.
|
|
74
|
-
"@adecore/merge": "0.25.
|
|
70
|
+
"@adecore/agent-contracts": "0.25.9",
|
|
71
|
+
"@adecore/editor": "0.25.9",
|
|
72
|
+
"@adecore/editor-react": "0.25.9",
|
|
73
|
+
"@adecore/intelligent-ui": "0.25.9",
|
|
74
|
+
"@adecore/merge": "0.25.9",
|
|
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.
|
|
97
|
+
"@adecore/ui": "^0.25.9",
|
|
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.
|
|
104
|
+
"@adecore/ui": "0.25.9",
|
|
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
|
+
}
|
|
@@ -12,22 +12,46 @@ export const SHELL_LANGUAGE = 'shellscript';
|
|
|
12
12
|
export const EMBEDDED_CODE_LIMIT = 20_000;
|
|
13
13
|
const MAX_DEPTH = 3;
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/* A stretch of a shell command, as offsets into it. */
|
|
16
|
+
export interface ShellSpan {
|
|
16
17
|
readonly start: number;
|
|
17
18
|
readonly end: number;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
interface
|
|
21
|
+
export interface ShellWord extends ShellSpan {
|
|
21
22
|
/* The word as the shell hands it to the program, quotes and escapes taken out. */
|
|
22
23
|
readonly text: string;
|
|
23
24
|
/* The source of what the program gets, for a word that is bare or one quoted string; null for a mix. */
|
|
24
|
-
readonly body:
|
|
25
|
+
readonly body: ShellSpan | null;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/* A redirect such as `2>&1` or `> out.txt`, its descriptor included. */
|
|
29
|
+
export interface ShellRedirect extends ShellSpan {
|
|
30
|
+
readonly op: string;
|
|
31
|
+
readonly target: ShellWord;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* The `<<'EOF'` of a heredoc, and the lines it feeds the command. */
|
|
35
|
+
export interface ShellHeredoc extends ShellSpan {
|
|
36
|
+
readonly body: ShellSpan;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* One simple command of a line, as a shell would split it: the words it runs and what it reads and writes. */
|
|
40
|
+
export interface ShellSimpleCommand {
|
|
41
|
+
readonly words: readonly ShellWord[];
|
|
42
|
+
/* The word it runs, past variable assignments, wrappers and runners; null for a command of assignments only. */
|
|
43
|
+
readonly program: { readonly index: number; readonly name: string } | null;
|
|
44
|
+
readonly redirects: readonly ShellRedirect[];
|
|
45
|
+
readonly heredocs: readonly ShellHeredoc[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type Span = ShellSpan;
|
|
49
|
+
type Word = ShellWord;
|
|
50
|
+
|
|
27
51
|
interface SimpleCommand {
|
|
28
52
|
readonly words: Word[];
|
|
29
|
-
readonly redirects:
|
|
30
|
-
readonly heredocs:
|
|
53
|
+
readonly redirects: ShellRedirect[];
|
|
54
|
+
readonly heredocs: ShellHeredoc[];
|
|
31
55
|
}
|
|
32
56
|
|
|
33
57
|
interface Interpreter {
|
|
@@ -126,12 +150,15 @@ function programOf(word: string): string {
|
|
|
126
150
|
function parseCommands(source: string, from: number, to: number): SimpleCommand[] {
|
|
127
151
|
const commands: SimpleCommand[] = [];
|
|
128
152
|
let current: SimpleCommand = { words: [], redirects: [], heredocs: [] };
|
|
129
|
-
const pending: { command: SimpleCommand; delimiter: string; strip: boolean }[] = [];
|
|
153
|
+
const pending: { command: SimpleCommand; delimiter: string; strip: boolean; operator: Span }[] = [];
|
|
154
|
+
// The start of the `2` of a `2>&1` just read, which the redirect after it begins at.
|
|
155
|
+
let descriptorStart: number | null = null;
|
|
130
156
|
const finish = (): void => {
|
|
131
157
|
if (current.words.length > 0 || current.redirects.length > 0) {
|
|
132
158
|
commands.push(current);
|
|
133
159
|
}
|
|
134
160
|
current = { words: [], redirects: [], heredocs: [] };
|
|
161
|
+
descriptorStart = null;
|
|
135
162
|
};
|
|
136
163
|
const skipBlanks = (index: number): number => {
|
|
137
164
|
while (index < to && BLANKS.includes(source[index]!)) {
|
|
@@ -215,7 +242,7 @@ function parseCommands(source: string, from: number, to: number): SimpleCommand[
|
|
|
215
242
|
index++;
|
|
216
243
|
for (const heredoc of pending) {
|
|
217
244
|
const { body, next } = readHeredoc(index, heredoc.delimiter, heredoc.strip);
|
|
218
|
-
heredoc.command.heredocs.push(body);
|
|
245
|
+
heredoc.command.heredocs.push({ ...heredoc.operator, body });
|
|
219
246
|
index = next;
|
|
220
247
|
}
|
|
221
248
|
pending.length = 0;
|
|
@@ -230,11 +257,12 @@ function parseCommands(source: string, from: number, to: number): SimpleCommand[
|
|
|
230
257
|
} else if (source.startsWith('<<<', index)) {
|
|
231
258
|
index = readWord(skipBlanks(index + 3)).end;
|
|
232
259
|
} else if (source.startsWith('<<', index)) {
|
|
260
|
+
const start = index;
|
|
233
261
|
index += 2;
|
|
234
262
|
const strip = source[index] === '-';
|
|
235
263
|
const word = readWord(skipBlanks(strip ? index + 1 : index));
|
|
236
264
|
if (word.text !== '') {
|
|
237
|
-
pending.push({ command: current, delimiter: word.text, strip });
|
|
265
|
+
pending.push({ command: current, delimiter: word.text, strip, operator: { start, end: word.end } });
|
|
238
266
|
}
|
|
239
267
|
index = word.end;
|
|
240
268
|
} else if (char === '<' || char === '>' || (char === '&' && source[index + 1] === '>')) {
|
|
@@ -244,7 +272,8 @@ function parseCommands(source: string, from: number, to: number): SimpleCommand[
|
|
|
244
272
|
}
|
|
245
273
|
const op = source.slice(index, end);
|
|
246
274
|
const target = readWord(skipBlanks(end));
|
|
247
|
-
current.redirects.push({ op, target });
|
|
275
|
+
current.redirects.push({ start: descriptorStart ?? index, end: Math.max(target.end, end), op, target });
|
|
276
|
+
descriptorStart = null;
|
|
248
277
|
index = Math.max(target.end, end);
|
|
249
278
|
} else if (SEPARATORS.includes(char)) {
|
|
250
279
|
index++;
|
|
@@ -253,7 +282,9 @@ function parseCommands(source: string, from: number, to: number): SimpleCommand[
|
|
|
253
282
|
const word = readWord(index);
|
|
254
283
|
// The `2` of `2>&1` is a file descriptor, not an argument.
|
|
255
284
|
const descriptor = /^\d+$/.test(word.text) && (source[word.end] === '>' || source[word.end] === '<');
|
|
256
|
-
if (
|
|
285
|
+
if (descriptor) {
|
|
286
|
+
descriptorStart = word.start;
|
|
287
|
+
} else {
|
|
257
288
|
current.words.push(word);
|
|
258
289
|
}
|
|
259
290
|
index = Math.max(word.end, index + 1);
|
|
@@ -350,7 +381,7 @@ function codeOf(command: SimpleCommand): EmbeddedCode[] {
|
|
|
350
381
|
}
|
|
351
382
|
const heredocLanguage = interpreter !== undefined && (!readsFile || interpreter.client === true) ? interpreter.language : writtenLanguage(command, program);
|
|
352
383
|
if (heredocLanguage !== null) {
|
|
353
|
-
for (const body of command.heredocs) {
|
|
384
|
+
for (const { body } of command.heredocs) {
|
|
354
385
|
found.push({ ...body, language: heredocLanguage });
|
|
355
386
|
}
|
|
356
387
|
}
|
|
@@ -392,6 +423,21 @@ function collect(source: string, from: number, to: number, depth: number, into:
|
|
|
392
423
|
}
|
|
393
424
|
}
|
|
394
425
|
|
|
426
|
+
/*
|
|
427
|
+
* The simple commands of a shell line, in the order they come: a pipeline, a list and a line each split
|
|
428
|
+
* apart, a heredoc with the command it feeds. Only the line itself: the script of a `bash -lc '…'` stays a word.
|
|
429
|
+
*/
|
|
430
|
+
export function shellCommands(source: string): ShellSimpleCommand[] {
|
|
431
|
+
return parseCommands(source, 0, source.length).map((command) => {
|
|
432
|
+
const index = programIndex(command.words);
|
|
433
|
+
const word = command.words[index];
|
|
434
|
+
return {
|
|
435
|
+
...command,
|
|
436
|
+
program: word === undefined ? null : { index, name: word.text.slice(word.text.lastIndexOf('/') + 1) }
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
395
441
|
/*
|
|
396
442
|
* The code inside a shell command that is in another language, or a shell script of its own: the
|
|
397
443
|
* script of `bash -lc '…'`, `python3 -c '…'`, `node -e '…'`, `psql -c '…'`, and a heredoc fed to an
|