@adecore/agents-react 0.20.1 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/logic/generated-image.d.ts +27 -0
- package/dist/chat/logic/generated-image.js +32 -0
- package/dist/chat/logic/timeline-target.d.ts +13 -1
- package/dist/chat/logic/timeline-target.js +26 -0
- package/dist/chat/logic/timeline.js +16 -10
- package/dist/chat/subagent-conversation.d.ts +2 -1
- package/dist/chat/subagent-conversation.js +7 -1
- package/dist/chat/timeline-flash.d.ts +3 -1
- package/dist/chat/timeline-flash.js +2 -2
- package/dist/chat/ui/ImageView.d.ts +8 -2
- package/dist/chat/ui/ImageView.js +12 -6
- package/dist/chat/ui/SubagentTimeline.js +6 -2
- package/dist/chat/ui/Timeline.js +67 -7
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.d.ts +23 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +103 -0
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.d.ts +10 -0
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +11 -0
- package/dist/chat/ui/intelligent-ui/UiReply.d.ts +15 -0
- package/dist/chat/ui/intelligent-ui/UiReply.js +329 -0
- package/dist/chat/ui/intelligent-ui/block-local.d.ts +11 -0
- package/dist/chat/ui/intelligent-ui/block-local.js +42 -0
- package/dist/chat/ui/intelligent-ui/chart-data.d.ts +16 -0
- package/dist/chat/ui/intelligent-ui/chart-data.js +46 -0
- package/dist/chat/ui/intelligent-ui/node-text.d.ts +15 -0
- package/dist/chat/ui/intelligent-ui/node-text.js +106 -0
- package/dist/chat/ui/intelligent-ui/registry.d.ts +2 -0
- package/dist/chat/ui/intelligent-ui/registry.js +51 -0
- package/dist/chat/ui/intelligent-ui/render-context.d.ts +53 -0
- package/dist/chat/ui/intelligent-ui/render-context.js +3 -0
- package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +3 -0
- package/dist/chat/ui/intelligent-ui/renderers/chart.js +123 -0
- package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +4 -0
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +45 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +14 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.js +59 -0
- package/dist/chat/ui/intelligent-ui/renderers/data.d.ts +7 -0
- package/dist/chat/ui/intelligent-ui/renderers/data.js +80 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +8 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +72 -0
- package/dist/chat/ui/intelligent-ui/renderers/links.d.ts +10 -0
- package/dist/chat/ui/intelligent-ui/renderers/links.js +87 -0
- package/dist/chat/ui/intelligent-ui/renderers/structure.d.ts +6 -0
- package/dist/chat/ui/intelligent-ui/renderers/structure.js +32 -0
- package/dist/chat/ui/intelligent-ui/renderers/text.d.ts +8 -0
- package/dist/chat/ui/intelligent-ui/renderers/text.js +61 -0
- package/dist/chat/ui/intelligent-ui/reply-parts.d.ts +9 -0
- package/dist/chat/ui/intelligent-ui/reply-parts.js +18 -0
- package/dist/chat/ui/intelligent-ui/table-data.d.ts +31 -0
- package/dist/chat/ui/intelligent-ui/table-data.js +79 -0
- package/dist/chat/ui/intelligent-ui/tones.d.ts +7 -0
- package/dist/chat/ui/intelligent-ui/tones.js +31 -0
- package/dist/chat/ui/intelligent-ui/use-later.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/use-later.js +16 -0
- package/dist/chat/ui/intelligent-ui/use-ui-links.d.ts +9 -0
- package/dist/chat/ui/intelligent-ui/use-ui-links.js +126 -0
- package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +21 -0
- package/dist/chat/ui/intelligent-ui/use-ui-queries.js +272 -0
- package/dist/chat/ui/reply-context.d.ts +8 -0
- package/dist/chat/ui/reply-context.js +1 -0
- package/dist/chat/ui/rows/GeneratedImageRow.d.ts +5 -0
- package/dist/chat/ui/rows/GeneratedImageRow.js +121 -0
- package/dist/chat/ui/rows/MessageRows.js +21 -4
- package/dist/chat/ui/rows/Rows.js +4 -2
- package/dist/chat/ui/rows/SubagentRow.js +5 -0
- package/dist/chat/ui/rows/WorkRows.d.ts +4 -1
- package/dist/chat/ui/rows/WorkRows.js +4 -3
- package/dist/host.d.ts +11 -1
- package/dist/locales/en/agent-chat.json +101 -0
- package/dist/locales/nl/agent-chat.json +101 -0
- package/dist/state/chats.js +13 -0
- package/dist/theme.css +19 -0
- package/package.json +128 -7
- package/src/chat/logic/generated-image.ts +48 -0
- package/src/chat/logic/timeline-target.ts +41 -1
- package/src/chat/logic/timeline.ts +19 -10
- package/src/chat/subagent-conversation.ts +13 -2
- package/src/chat/timeline-flash.ts +5 -3
- package/src/chat/ui/ImageView.tsx +51 -8
- package/src/chat/ui/SubagentTimeline.tsx +24 -1
- package/src/chat/ui/Timeline.tsx +97 -21
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +219 -0
- package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +21 -0
- package/src/chat/ui/intelligent-ui/UiReply.tsx +541 -0
- package/src/chat/ui/intelligent-ui/block-local.ts +51 -0
- package/src/chat/ui/intelligent-ui/chart-data.ts +67 -0
- package/src/chat/ui/intelligent-ui/node-text.ts +119 -0
- package/src/chat/ui/intelligent-ui/registry.ts +53 -0
- package/src/chat/ui/intelligent-ui/render-context.ts +84 -0
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +319 -0
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +102 -0
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +140 -0
- package/src/chat/ui/intelligent-ui/renderers/data.tsx +181 -0
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +144 -0
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +158 -0
- package/src/chat/ui/intelligent-ui/renderers/structure.tsx +84 -0
- package/src/chat/ui/intelligent-ui/renderers/text.tsx +124 -0
- package/src/chat/ui/intelligent-ui/reply-parts.ts +22 -0
- package/src/chat/ui/intelligent-ui/table-data.ts +104 -0
- package/src/chat/ui/intelligent-ui/tones.ts +37 -0
- package/src/chat/ui/intelligent-ui/use-later.ts +17 -0
- package/src/chat/ui/intelligent-ui/use-ui-links.ts +143 -0
- package/src/chat/ui/intelligent-ui/use-ui-queries.ts +308 -0
- package/src/chat/ui/reply-context.ts +7 -0
- package/src/chat/ui/rows/GeneratedImageRow.tsx +244 -0
- package/src/chat/ui/rows/MessageRows.tsx +60 -4
- package/src/chat/ui/rows/Rows.tsx +4 -2
- package/src/chat/ui/rows/SubagentRow.tsx +5 -0
- package/src/chat/ui/rows/WorkRows.tsx +20 -5
- package/src/host.ts +27 -1
- package/src/locales/en/agent-chat.json +101 -0
- package/src/locales/nl/agent-chat.json +101 -0
- package/src/state/chats.ts +13 -0
- package/src/theme.css +19 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import type { TFunction } from 'i18next';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { Trans, useTranslation } from 'react-i18next';
|
|
5
|
+
import { Check, Copy, Ellipsis, ExternalLink, FolderDown, ImageIcon, Maximize2 } from 'lucide-react';
|
|
6
|
+
import type { ChatAttachment, ChatToolItem } from '@adecore/agent-contracts';
|
|
7
|
+
import { Button, ButtonGroup, copyText, ErrorBoundary, FileIcon, Icon, IconButton, Menu, Surface } from '@adecore/ui';
|
|
8
|
+
import { chatHost } from '../../../host';
|
|
9
|
+
import { useChatScope } from '../../../scope';
|
|
10
|
+
import { useCurrentItem } from '../../../state/chats';
|
|
11
|
+
import { formatBytes } from '../../attachments';
|
|
12
|
+
import { attachmentAspect, generatedImageView, type GeneratedImageFailure } from '../../logic/generated-image';
|
|
13
|
+
import { toolStartedAt } from '../../logic/tools';
|
|
14
|
+
import { CHIP_IN_MESSAGE, MENTION_TONE } from '../chips';
|
|
15
|
+
import { openFileLink, useFileLinkCwd } from '../file-links';
|
|
16
|
+
import { ImageThumb } from '../ImageView';
|
|
17
|
+
import { ROW_GUTTER } from '../icons';
|
|
18
|
+
import { RunningFor, WorkRow } from './WorkRows';
|
|
19
|
+
|
|
20
|
+
const SAVED_FLASH_MS = 2000;
|
|
21
|
+
|
|
22
|
+
// Where each image went, so a row the thread unmounts while scrolling still says so when it comes back.
|
|
23
|
+
const savedPaths = new Map<string, string>();
|
|
24
|
+
|
|
25
|
+
/* True for `ms` after `start()`, for a check mark that says an action landed. */
|
|
26
|
+
function useFlash(ms: number): [boolean, () => void] {
|
|
27
|
+
const [on, setOn] = useState(false);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!on) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const timer = setTimeout(() => setOn(false), ms);
|
|
33
|
+
return () => clearTimeout(timer);
|
|
34
|
+
}, [on, ms]);
|
|
35
|
+
return [on, () => setOn(true)];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function failureText(failure: GeneratedImageFailure, t: TFunction<'agent-chat'>): string | undefined {
|
|
39
|
+
switch (failure.kind) {
|
|
40
|
+
case 'provider':
|
|
41
|
+
return failure.message ?? undefined;
|
|
42
|
+
case 'empty':
|
|
43
|
+
return t('generatedImage.empty');
|
|
44
|
+
case 'not-image':
|
|
45
|
+
return t('generatedImage.notImage');
|
|
46
|
+
case 'too-large':
|
|
47
|
+
return t('generatedImage.tooLarge', { size: formatBytes(failure.limit) });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* The line over the image, in the measures of a tool line but nothing to fold. */
|
|
52
|
+
function HeadLine({ label, detail, live, startedAt }: { label: string; detail?: string; live?: boolean; startedAt?: number }) {
|
|
53
|
+
return (
|
|
54
|
+
<div className="-mx-1 mb-0.5 flex h-7 items-center gap-2 px-1 text-xs text-text-muted">
|
|
55
|
+
<span className={clsx(ROW_GUTTER, live && 'text-accent')}>
|
|
56
|
+
<Icon icon={ImageIcon} size={12} />
|
|
57
|
+
</span>
|
|
58
|
+
<span className={clsx('shrink-0', live && 'chat-live-text')}>{label}</span>
|
|
59
|
+
{detail && <span className="min-w-0 truncate text-text-faint">{detail}</span>}
|
|
60
|
+
<span className="grow" />
|
|
61
|
+
{startedAt !== undefined && <RunningFor startedAt={startedAt} />}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* The file a saved image became, which opens where the app opens a file link. */
|
|
67
|
+
function SavedFile({ path }: { path: string }) {
|
|
68
|
+
const cwd = useFileLinkCwd();
|
|
69
|
+
const { id } = useChatScope();
|
|
70
|
+
const chip = (
|
|
71
|
+
<>
|
|
72
|
+
<FileIcon path={path} size={14} />
|
|
73
|
+
<span className="truncate">{path}</span>
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
if (chatHost().fileLinks === null) {
|
|
77
|
+
return <span className={clsx(CHIP_IN_MESSAGE, MENTION_TONE)}>{chip}</span>;
|
|
78
|
+
}
|
|
79
|
+
return (
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
className={clsx(CHIP_IN_MESSAGE, MENTION_TONE, 'hover:brightness-95')}
|
|
83
|
+
data-file-path={path}
|
|
84
|
+
onClick={() => openFileLink(cwd, { path, directory: false }, id)}
|
|
85
|
+
>
|
|
86
|
+
{chip}
|
|
87
|
+
</button>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function ReadyImage({ chatId, attachment, prompt, transparent }: { chatId: string; attachment: ChatAttachment; prompt: string | null; transparent: boolean }) {
|
|
92
|
+
const { t } = useTranslation('agent-chat');
|
|
93
|
+
const { id: scopeId } = useChatScope();
|
|
94
|
+
const { attachments } = chatHost();
|
|
95
|
+
const source = attachments.useUrl(scopeId, chatId, attachment.id);
|
|
96
|
+
const savedKey = `${scopeId}\n${chatId}\n${attachment.id}`;
|
|
97
|
+
const [lightbox, setLightbox] = useState(false);
|
|
98
|
+
const [saving, setSaving] = useState(false);
|
|
99
|
+
const [savedPath, setSavedPath] = useState(() => savedPaths.get(savedKey) ?? null);
|
|
100
|
+
const [savedFlash, flashSaved] = useFlash(SAVED_FLASH_MS);
|
|
101
|
+
const missing = source.failure !== null;
|
|
102
|
+
const aspect = attachmentAspect(attachment);
|
|
103
|
+
const { open, saveToProject } = attachments;
|
|
104
|
+
|
|
105
|
+
const detail = [
|
|
106
|
+
attachment.width !== undefined && attachment.height !== undefined
|
|
107
|
+
? t('generatedImage.dimensions', { width: attachment.width, height: attachment.height })
|
|
108
|
+
: null,
|
|
109
|
+
formatBytes(attachment.size),
|
|
110
|
+
transparent ? t('generatedImage.transparent') : null
|
|
111
|
+
]
|
|
112
|
+
.filter((part) => part !== null)
|
|
113
|
+
.join(' · ');
|
|
114
|
+
|
|
115
|
+
const openFile = open === undefined ? undefined : () => open(scopeId, chatId, attachment.id, attachment.name);
|
|
116
|
+
|
|
117
|
+
const save =
|
|
118
|
+
saveToProject === undefined
|
|
119
|
+
? undefined
|
|
120
|
+
: async (): Promise<void> => {
|
|
121
|
+
setSaving(true);
|
|
122
|
+
try {
|
|
123
|
+
const path = await saveToProject(scopeId, chatId, attachment.id, attachment.name);
|
|
124
|
+
if (path !== null) {
|
|
125
|
+
savedPaths.set(savedKey, path);
|
|
126
|
+
setSavedPath(path);
|
|
127
|
+
flashSaved();
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// The host's dialog shows why; the row stays as it was.
|
|
131
|
+
} finally {
|
|
132
|
+
setSaving(false);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const lightboxActions = (
|
|
137
|
+
<>
|
|
138
|
+
{openFile !== undefined && <IconButton icon={ExternalLink} size="sm" label={t('generatedImage.open')} onClick={openFile} />}
|
|
139
|
+
{save !== undefined && <IconButton icon={FolderDown} size="sm" label={t('generatedImage.save')} disabled={saving} onClick={() => void save()} />}
|
|
140
|
+
</>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div>
|
|
145
|
+
<HeadLine label={t('generatedImage.label')} detail={detail} />
|
|
146
|
+
<div className="flex max-w-90 flex-col gap-1.5">
|
|
147
|
+
<div
|
|
148
|
+
className={clsx(
|
|
149
|
+
'group/image relative',
|
|
150
|
+
aspect === undefined && 'w-fit',
|
|
151
|
+
transparent && source.url !== null && 'chat-checkerboard rounded-lg'
|
|
152
|
+
)}
|
|
153
|
+
>
|
|
154
|
+
<ImageThumb
|
|
155
|
+
source={source}
|
|
156
|
+
alt={attachment.name}
|
|
157
|
+
aspect={aspect}
|
|
158
|
+
className={aspect === undefined ? 'max-h-90 max-w-full object-contain' : undefined}
|
|
159
|
+
actions={lightboxActions}
|
|
160
|
+
open={lightbox}
|
|
161
|
+
onOpenChange={setLightbox}
|
|
162
|
+
/>
|
|
163
|
+
{source.url !== null && (
|
|
164
|
+
<Surface
|
|
165
|
+
role="toolbar"
|
|
166
|
+
aria-label={t('generatedImage.actions')}
|
|
167
|
+
className="absolute top-2 right-2 rounded-lg p-0.5 opacity-0 transition-opacity group-focus-within/image:opacity-100 group-hover/image:opacity-100 has-[[data-popup-open]]:opacity-100 pointer-coarse:opacity-100"
|
|
168
|
+
>
|
|
169
|
+
<ButtonGroup>
|
|
170
|
+
<IconButton icon={Maximize2} size="sm" label={t('generatedImage.openLarge')} onClick={() => setLightbox(true)} />
|
|
171
|
+
{prompt !== null && (
|
|
172
|
+
<Menu.Root>
|
|
173
|
+
<IconButton icon={Ellipsis} size="sm" label={t('generatedImage.more')} render={<Menu.Trigger />} />
|
|
174
|
+
<Menu.Popup align="end">
|
|
175
|
+
<Menu.Item onClick={() => copyText(prompt)}>
|
|
176
|
+
<Icon icon={Copy} size={14} /> {t('generatedImage.copyPrompt')}
|
|
177
|
+
</Menu.Item>
|
|
178
|
+
</Menu.Popup>
|
|
179
|
+
</Menu.Root>
|
|
180
|
+
)}
|
|
181
|
+
</ButtonGroup>
|
|
182
|
+
</Surface>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
{(openFile !== undefined || save !== undefined) && (
|
|
186
|
+
<div className="flex items-center gap-1.5">
|
|
187
|
+
{openFile !== undefined && (
|
|
188
|
+
<Button size="xs" variant="secondary" disabled={missing} onClick={openFile}>
|
|
189
|
+
{t('generatedImage.open')}
|
|
190
|
+
</Button>
|
|
191
|
+
)}
|
|
192
|
+
{save !== undefined && (
|
|
193
|
+
<Button size="xs" variant="secondary" disabled={missing || saving || savedFlash} onClick={() => void save()}>
|
|
194
|
+
{savedFlash ? (
|
|
195
|
+
<>
|
|
196
|
+
<Icon icon={Check} size={12} /> {t('generatedImage.saved')}
|
|
197
|
+
</>
|
|
198
|
+
) : (
|
|
199
|
+
t('generatedImage.save')
|
|
200
|
+
)}
|
|
201
|
+
</Button>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
{savedPath !== null && !savedFlash && (
|
|
206
|
+
<div className="flex min-w-0 items-center gap-1.5 text-xs text-text-muted">
|
|
207
|
+
<Trans t={t} i18nKey="generatedImage.savedTo" values={{ path: savedPath }} components={{ file: <SavedFile path={savedPath} /> }} />
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/*
|
|
216
|
+
* The row of an image generation. Not a fold like other tool calls: the image is the result a person
|
|
217
|
+
* came for, so it stays open, opens large in the lightbox and offers what the host lets a person do
|
|
218
|
+
* with it. A failure is an ordinary failed tool line, with the reason as its detail.
|
|
219
|
+
*/
|
|
220
|
+
export function GeneratedImageRow({ chatId, tool: derived }: { chatId: string; tool: ChatToolItem }) {
|
|
221
|
+
const { t } = useTranslation('agent-chat');
|
|
222
|
+
const tool = useCurrentItem(chatId, derived);
|
|
223
|
+
const view = generatedImageView(tool);
|
|
224
|
+
switch (view.state) {
|
|
225
|
+
case 'generating':
|
|
226
|
+
return <HeadLine label={t('generatedImage.generating')} live startedAt={toolStartedAt(tool)} />;
|
|
227
|
+
case 'failed':
|
|
228
|
+
return (
|
|
229
|
+
<WorkRow
|
|
230
|
+
tool={tool}
|
|
231
|
+
icon={<Icon icon={ImageIcon} size={12} />}
|
|
232
|
+
label={t('generatedImage.failed')}
|
|
233
|
+
detail={failureText(view.failure, t) ?? ''}
|
|
234
|
+
failed
|
|
235
|
+
/>
|
|
236
|
+
);
|
|
237
|
+
case 'ready':
|
|
238
|
+
return (
|
|
239
|
+
<ErrorBoundary label={t('generatedImage.renderFailed')} resetKeys={[view.attachment]} compact className="relative rounded-lg">
|
|
240
|
+
<ReadyImage chatId={chatId} attachment={view.attachment} prompt={view.prompt} transparent={view.transparent} />
|
|
241
|
+
</ErrorBoundary>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { useContext, useMemo, useState, type ReactNode } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { Bot, Brain, Check, ChevronDown, CircleAlert, File, Info, MessageCircleQuestionMark, TriangleAlert, X } from 'lucide-react';
|
|
5
|
-
import type {
|
|
4
|
+
import { Bot, Brain, Check, ChevronDown, CircleAlert, File, Info, MessageCircleQuestionMark, Reply, TriangleAlert, X } from 'lucide-react';
|
|
5
|
+
import type {
|
|
6
|
+
ChatApprovalItem,
|
|
7
|
+
ChatAttachment,
|
|
8
|
+
ChatAssistantItem,
|
|
9
|
+
ChatQuestionItem,
|
|
10
|
+
ChatThinkingItem,
|
|
11
|
+
ChatUiChoiceOrigin,
|
|
12
|
+
ChatUserItem
|
|
13
|
+
} from '@adecore/agent-contracts';
|
|
6
14
|
import { fileBadge, formatBytes, isImageAttachment } from '../../attachments';
|
|
7
15
|
import { useChatRow, useCurrentItem } from '../../../state/chats';
|
|
8
16
|
import { chatHost } from '../../../host';
|
|
@@ -15,12 +23,13 @@ import { FadingWords } from '../FadingWords';
|
|
|
15
23
|
import { settledBlocksText } from '../markdown-blocks';
|
|
16
24
|
import { WHOLE_FADE_CLASS } from '../rehype-fade';
|
|
17
25
|
import { useRevealedText } from '../reveal';
|
|
18
|
-
import { formatAgo, formatElapsedShort, formatTokens } from '@adecore/ui/format';
|
|
26
|
+
import { formatAgo, formatElapsedShort, formatMoment, formatTokens } from '@adecore/ui/format';
|
|
19
27
|
import { Icon, Tooltip, useTickingText } from '@adecore/ui';
|
|
20
28
|
import { toolSummary } from '../../logic/tools';
|
|
21
29
|
import { ROW_GUTTER } from '../icons';
|
|
22
30
|
import { useOpenForFind } from '../find-reveal';
|
|
23
|
-
import { ReplyContext } from '../reply-context';
|
|
31
|
+
import { ReplyContext, UiReplyNavigationContext } from '../reply-context';
|
|
32
|
+
import { UiReply } from '../intelligent-ui/UiReply';
|
|
24
33
|
|
|
25
34
|
// A long prompt folds so the answer stays in view; the reader can open it.
|
|
26
35
|
const USER_FOLD_LINES = 8;
|
|
@@ -99,6 +108,7 @@ export function UserRow({ chatId, item }: { chatId: string; item: ChatUserItem }
|
|
|
99
108
|
))}
|
|
100
109
|
</div>
|
|
101
110
|
)}
|
|
111
|
+
{item.uiChoice !== undefined && <ChoiceLine chatId={chatId} choice={item.uiChoice} />}
|
|
102
112
|
{item.text !== '' && (
|
|
103
113
|
<div className="relative max-w-[80%] rounded-2xl bg-surface-active px-3.5 py-2.5 text-sm wrap-anywhere text-text select-text">
|
|
104
114
|
<div data-find-field="text" className={clsx(long && !open && FOLD)}>
|
|
@@ -116,6 +126,34 @@ export function UserRow({ chatId, item }: { chatId: string; item: ChatUserItem }
|
|
|
116
126
|
);
|
|
117
127
|
}
|
|
118
128
|
|
|
129
|
+
/*
|
|
130
|
+
* The choice a message answers. The label stays out of the message, which holds exactly what the agent
|
|
131
|
+
* read, and out of a copied selection; a screen reader hears it between the heading and the message.
|
|
132
|
+
*/
|
|
133
|
+
function ChoiceLine({ chatId, choice }: { chatId: string; choice: ChatUiChoiceOrigin }) {
|
|
134
|
+
const { t } = useTranslation('agent-chat');
|
|
135
|
+
const inherited = useContext(UiReplyNavigationContext);
|
|
136
|
+
const navigation = inherited?.chatId === chatId ? inherited : null;
|
|
137
|
+
const Tag = navigation ? 'button' : 'div';
|
|
138
|
+
return (
|
|
139
|
+
<Tag
|
|
140
|
+
type={navigation ? 'button' : undefined}
|
|
141
|
+
onClick={navigation ? () => navigation.reveal(choice) : undefined}
|
|
142
|
+
className={clsx(
|
|
143
|
+
'mb-1 flex max-w-[80%] items-start gap-1.5 px-1 text-left text-xs text-text-muted select-none',
|
|
144
|
+
navigation && 'rounded-md hover:text-text focus-visible:outline-2 focus-visible:outline-accent'
|
|
145
|
+
)}
|
|
146
|
+
>
|
|
147
|
+
<Icon icon={Reply} size={12} className="mt-1 shrink-0" />
|
|
148
|
+
<span className="min-w-0 wrap-anywhere">
|
|
149
|
+
{choice.older
|
|
150
|
+
? t('blocks.choseOlder', { label: choice.label, time: formatMoment(choice.sourceAt) })
|
|
151
|
+
: t('blocks.chose', { label: choice.label })}
|
|
152
|
+
</span>
|
|
153
|
+
</Tag>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
119
157
|
/*
|
|
120
158
|
* Who wrote a message, for a screen reader only: one heading per message is what lets VoiceOver's
|
|
121
159
|
* rotor walk a thread from message to message. `select-none`, so a selection copied across two
|
|
@@ -199,6 +237,24 @@ export function AssistantRow({ chatId, item: derived }: { chatId: string; item:
|
|
|
199
237
|
const live = mode === 'words' && item.streaming;
|
|
200
238
|
const reveal = useRevealedText(item.text, live);
|
|
201
239
|
|
|
240
|
+
if (item.ui?.length && !item.parentToolUseId && !(mode === 'whole' && item.streaming)) {
|
|
241
|
+
return (
|
|
242
|
+
<div data-find-item={item.id} className="-mx-1 px-1 pb-2">
|
|
243
|
+
<ReplyHeading chatId={chatId} />
|
|
244
|
+
<div data-find-field="text" data-quote-answer className="contents">
|
|
245
|
+
<UiReply
|
|
246
|
+
text={item.text}
|
|
247
|
+
blocks={item.ui}
|
|
248
|
+
answers={item.uiAnswers}
|
|
249
|
+
queries={item.uiQueries}
|
|
250
|
+
reply={reply}
|
|
251
|
+
context={{ scopeId: scope.id, chatId, itemId: item.id, phase: item.streaming ? 'streaming' : 'final' }}
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
202
258
|
if (mode === 'blocks') {
|
|
203
259
|
const settled = item.streaming ? settledBlocksText(item.text) : item.text;
|
|
204
260
|
return (
|
|
@@ -4,7 +4,9 @@ import type { ThreadCard } from '../../../host';
|
|
|
4
4
|
import type { TimelineRow } from '../../logic/timeline';
|
|
5
5
|
import type { SubagentStep } from '../../subagent-view';
|
|
6
6
|
import { AgentTurnRow, ApprovalHistoryRow, AssistantRow, CompactionRow, NoteRow, QuestionHistoryRow, ReportRow, ThinkingRow, UserRow } from './MessageRows';
|
|
7
|
+
import { isImageGeneration } from '../../logic/generated-image';
|
|
7
8
|
import { ForksRow } from './ForksRow';
|
|
9
|
+
import { GeneratedImageRow } from './GeneratedImageRow';
|
|
8
10
|
import { SubagentBranchRow } from './SubagentRow';
|
|
9
11
|
import { VisualRow } from './VisualRow';
|
|
10
12
|
import { WorkflowRow } from './WorkflowRow';
|
|
@@ -51,9 +53,9 @@ export function Row({ row, chatId, toggleGroup, toggleTurn, toggleSubagent, open
|
|
|
51
53
|
case 'thinking':
|
|
52
54
|
return <ThinkingRow chatId={chatId} item={row.item} />;
|
|
53
55
|
case 'work':
|
|
54
|
-
return <WorkRow tool={row.tool} />;
|
|
56
|
+
return isImageGeneration(row.tool) ? <GeneratedImageRow chatId={chatId} tool={row.tool} /> : <WorkRow tool={row.tool} />;
|
|
55
57
|
case 'work-live':
|
|
56
|
-
return <WorkLiveRow chatId={chatId} tool={row.tool} />;
|
|
58
|
+
return isImageGeneration(row.tool) ? <GeneratedImageRow chatId={chatId} tool={row.tool} /> : <WorkLiveRow chatId={chatId} tool={row.tool} />;
|
|
57
59
|
case 'workflow':
|
|
58
60
|
return <WorkflowRow chatId={chatId} tool={row.tool} workflow={row.workflow} onOpenAgent={openConversation} />;
|
|
59
61
|
case 'work-group':
|
|
@@ -9,6 +9,8 @@ import { Icon, useNow } from '@adecore/ui';
|
|
|
9
9
|
import { useOpenForFind } from '../find-reveal';
|
|
10
10
|
import { Markdown } from '../Markdown';
|
|
11
11
|
import { TaskRow } from './TaskRow';
|
|
12
|
+
import { isImageGeneration } from '../../logic/generated-image';
|
|
13
|
+
import { GeneratedImageRow } from './GeneratedImageRow';
|
|
12
14
|
import { RunningFor, ToggleLine, WorkLiveRow, WorkRow } from './WorkRows';
|
|
13
15
|
import { entryTimeOf, statusWordOf, taskIdOf } from '../../subagent-list';
|
|
14
16
|
import { useSubagentSupport } from '../../subagent-support';
|
|
@@ -27,6 +29,9 @@ const CHILDREN_MAX_PX = 320;
|
|
|
27
29
|
*/
|
|
28
30
|
function ChildRow({ chatId, item }: { chatId: string; item: ChatItem }) {
|
|
29
31
|
if (item.kind === 'tool') {
|
|
32
|
+
if (isImageGeneration(item)) {
|
|
33
|
+
return <GeneratedImageRow chatId={chatId} tool={item} />;
|
|
34
|
+
}
|
|
30
35
|
return item.state === 'running' ? <WorkLiveRow chatId={chatId} tool={item} /> : <WorkRow tool={item} />;
|
|
31
36
|
}
|
|
32
37
|
if (item.kind === 'assistant') {
|
|
@@ -114,8 +114,23 @@ function ToolBody({ tool }: { tool: ChatToolItem }) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/* One settled tool call: a line, and its input and output behind it. An image the call looked at
|
|
117
|
-
is drawn under the line, because a picture says more about that read than its path does.
|
|
118
|
-
|
|
117
|
+
is drawn under the line, because a picture says more about that read than its path does.
|
|
118
|
+
`icon`, `label` and `failed` let a row that knows the call better name it in its own words. */
|
|
119
|
+
export function WorkRow({
|
|
120
|
+
tool,
|
|
121
|
+
nested,
|
|
122
|
+
detail,
|
|
123
|
+
icon,
|
|
124
|
+
label,
|
|
125
|
+
failed
|
|
126
|
+
}: {
|
|
127
|
+
tool: ChatToolItem;
|
|
128
|
+
nested?: boolean;
|
|
129
|
+
detail?: string;
|
|
130
|
+
icon?: React.ReactNode;
|
|
131
|
+
label?: string;
|
|
132
|
+
failed?: boolean;
|
|
133
|
+
}) {
|
|
119
134
|
const [open, setOpen] = useState(false);
|
|
120
135
|
const image = tool.state === 'done' ? readImagePath(tool.name, tool.input) : null;
|
|
121
136
|
const { ReadImage } = chatHost();
|
|
@@ -123,12 +138,12 @@ export function WorkRow({ tool, nested, detail }: { tool: ChatToolItem; nested?:
|
|
|
123
138
|
return (
|
|
124
139
|
<div data-find-item={tool.id} className={nested ? 'ml-6' : undefined}>
|
|
125
140
|
<ToggleLine
|
|
126
|
-
icon={toolIcon(tool.name)}
|
|
127
|
-
label={tool.name}
|
|
141
|
+
icon={icon ?? toolIcon(tool.name)}
|
|
142
|
+
label={label ?? tool.name}
|
|
128
143
|
detail={detail ?? toolSummary(tool.name, tool.input)}
|
|
129
144
|
open={open}
|
|
130
145
|
onToggle={() => setOpen((o) => !o)}
|
|
131
|
-
failed={tool.state === 'error'}
|
|
146
|
+
failed={failed ?? tool.state === 'error'}
|
|
132
147
|
/>
|
|
133
148
|
{image !== null && ReadImage !== null && <ReadImage path={image} />}
|
|
134
149
|
{open && <ToolBody tool={tool} />}
|
package/src/host.ts
CHANGED
|
@@ -2,7 +2,18 @@ import type { ComponentProps, ComponentType, ReactNode, RefObject } from 'react'
|
|
|
2
2
|
import type { Extension } from '@codemirror/state';
|
|
3
3
|
import type { EditorView } from '@codemirror/view';
|
|
4
4
|
import type { ThemeRegistration } from 'shiki';
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
AgentKind,
|
|
7
|
+
ChatCheckpointDiff,
|
|
8
|
+
ChatConfigurePayload,
|
|
9
|
+
ChatItem,
|
|
10
|
+
ChatSubagentItem,
|
|
11
|
+
ChatUiChoicePayload,
|
|
12
|
+
ChatUiQueryPayload,
|
|
13
|
+
ChatUiQueryReading,
|
|
14
|
+
ChatUiLinkPayload,
|
|
15
|
+
ChatUiLinkReading
|
|
16
|
+
} from '@adecore/agent-contracts';
|
|
6
17
|
import { isApplePlatform } from '@adecore/ui';
|
|
7
18
|
import type { TimelineRow } from './chat/logic/timeline';
|
|
8
19
|
import type { FindReveal } from './chat/ui/find-reveal';
|
|
@@ -189,6 +200,14 @@ export interface ReplyAuthor {
|
|
|
189
200
|
*/
|
|
190
201
|
export interface ChatHost {
|
|
191
202
|
storage?: ChatStorageOptions;
|
|
203
|
+
intelligentUi?: {
|
|
204
|
+
subscribe?(scopeId: string, chatId: string, changed: () => void): () => void;
|
|
205
|
+
link?(scopeId: string, payload: ChatUiLinkPayload): Promise<ChatUiLinkReading>;
|
|
206
|
+
openLink?(scopeId: string, reading: ChatUiLinkReading): void;
|
|
207
|
+
openUrl?(scopeId: string, url: string): void;
|
|
208
|
+
query?(scopeId: string, payload: ChatUiQueryPayload): Promise<ChatUiQueryReading>;
|
|
209
|
+
sendChoice(scopeId: string, payload: ChatUiChoicePayload): Promise<'sent' | 'queued'>;
|
|
210
|
+
};
|
|
192
211
|
/* The colors an account may wear in the order a picker offers them, the few it shows first, and their names. */
|
|
193
212
|
accents: {
|
|
194
213
|
all: readonly AccentChoice[];
|
|
@@ -209,6 +228,13 @@ export interface ChatHost {
|
|
|
209
228
|
attachments: {
|
|
210
229
|
useUrl(scopeId: string, chatId: string, attachmentId: string): ResourceUrl;
|
|
211
230
|
read(scopeId: string, chatId: string, attachmentId: string): Promise<Blob>;
|
|
231
|
+
/* Opens the file outside the chat, such as in the system's own app or as a download named `suggestedName`; left out, a generated image offers no Open. */
|
|
232
|
+
open?(scopeId: string, chatId: string, attachmentId: string, suggestedName: string): void;
|
|
233
|
+
/*
|
|
234
|
+
* Saves a copy into the folder the chat works for, through the host's own dialog, which also shows a failure.
|
|
235
|
+
* Resolves with the path as a file link opens it, or null when the person cancels; left out, a generated image offers no Save.
|
|
236
|
+
*/
|
|
237
|
+
saveToProject?(scopeId: string, chatId: string, attachmentId: string, suggestedName: string): Promise<string | null>;
|
|
212
238
|
};
|
|
213
239
|
/* The image a tool read, drawn under its row; null draws none. */
|
|
214
240
|
ReadImage: ComponentType<{ path: string }> | null;
|
|
@@ -435,6 +435,25 @@
|
|
|
435
435
|
"zoomOut": "Zoom out",
|
|
436
436
|
"reset": "Reset"
|
|
437
437
|
},
|
|
438
|
+
"generatedImage": {
|
|
439
|
+
"label": "Generated image",
|
|
440
|
+
"generating": "Generating image",
|
|
441
|
+
"failed": "Image generation failed",
|
|
442
|
+
"empty": "The image came back empty",
|
|
443
|
+
"notImage": "The result is not an image",
|
|
444
|
+
"tooLarge": "The image is larger than {{size}}",
|
|
445
|
+
"dimensions": "{{width}} × {{height}}",
|
|
446
|
+
"transparent": "transparent",
|
|
447
|
+
"actions": "Image actions",
|
|
448
|
+
"more": "More actions",
|
|
449
|
+
"copyPrompt": "Copy prompt",
|
|
450
|
+
"open": "Open",
|
|
451
|
+
"openLarge": "Open large",
|
|
452
|
+
"save": "Save to project…",
|
|
453
|
+
"saved": "Saved",
|
|
454
|
+
"savedTo": "Saved to <file>{{path}}</file>",
|
|
455
|
+
"renderFailed": "This image failed to render"
|
|
456
|
+
},
|
|
438
457
|
"account": {
|
|
439
458
|
"pill": "{{provider}} account {{account}}"
|
|
440
459
|
},
|
|
@@ -460,5 +479,87 @@
|
|
|
460
479
|
"code": {
|
|
461
480
|
"actions": "Code actions",
|
|
462
481
|
"copyFailed": "Could not copy the code. Try again."
|
|
482
|
+
},
|
|
483
|
+
"blocks": {
|
|
484
|
+
"label": "Interactive block",
|
|
485
|
+
"progress": {
|
|
486
|
+
"label": "Progress",
|
|
487
|
+
"of": "{{value}} of {{max}}"
|
|
488
|
+
},
|
|
489
|
+
"step": {
|
|
490
|
+
"done": "Done",
|
|
491
|
+
"running": "Running",
|
|
492
|
+
"pending": "Waiting",
|
|
493
|
+
"failed": "Failed",
|
|
494
|
+
"skipped": "Skipped"
|
|
495
|
+
},
|
|
496
|
+
"stat": {
|
|
497
|
+
"change": "{{change}} from {{previous}}",
|
|
498
|
+
"from": "from {{previous}}"
|
|
499
|
+
},
|
|
500
|
+
"table": {
|
|
501
|
+
"label": "Table of {{columns}}",
|
|
502
|
+
"showAll": "Show all {{total}} rows"
|
|
503
|
+
},
|
|
504
|
+
"chart": {
|
|
505
|
+
"values": "Values",
|
|
506
|
+
"summary": "{{count}} values, highest {{label}} at {{value}}",
|
|
507
|
+
"summarySeries": "{{count}} categories, highest {{series}} for {{label}} at {{value}}"
|
|
508
|
+
},
|
|
509
|
+
"image": {
|
|
510
|
+
"label": "Image",
|
|
511
|
+
"unavailable": "The image is not available"
|
|
512
|
+
},
|
|
513
|
+
"sources": "Sources",
|
|
514
|
+
"citation": "Source {{number}}: {{title}}",
|
|
515
|
+
"outsideProject": "Outside the project, shown as text",
|
|
516
|
+
"options": "Options",
|
|
517
|
+
"choices": "Choices",
|
|
518
|
+
"checklist": "Checklist",
|
|
519
|
+
"choice": {
|
|
520
|
+
"waiting": "Available when the reply is done",
|
|
521
|
+
"reading": "Available once the live data is read",
|
|
522
|
+
"unavailable": "Not available",
|
|
523
|
+
"sent": "Sent",
|
|
524
|
+
"queued": "Queued",
|
|
525
|
+
"failed": "Could not send. Try again.",
|
|
526
|
+
"ready": "Choices are ready"
|
|
527
|
+
},
|
|
528
|
+
"answered": "Answered with “{{label}}”",
|
|
529
|
+
"chose": "Chose “{{label}}”",
|
|
530
|
+
"choseOlder": "Chose “{{label}}” on the reply of {{time}}",
|
|
531
|
+
"fixes_one": "{{count}} fix",
|
|
532
|
+
"fixes_other": "{{count}} fixes",
|
|
533
|
+
"shownAsText": "Shown as text",
|
|
534
|
+
"unknownComponent": "This version cannot draw {{component}}",
|
|
535
|
+
"renderFailed": "Could not draw this part",
|
|
536
|
+
"unreadable": "The block could not be read",
|
|
537
|
+
"live": {
|
|
538
|
+
"label": "Live",
|
|
539
|
+
"read": "read {{time}}",
|
|
540
|
+
"sources": "Reads {{sources}}",
|
|
541
|
+
"failed": "Could not read {{source}}",
|
|
542
|
+
"refused": "Not allowed to read {{source}}",
|
|
543
|
+
"notRead": "Not read",
|
|
544
|
+
"reasons": {
|
|
545
|
+
"access-unavailable": "The access of the chat that wrote this is no longer available.",
|
|
546
|
+
"block-stale": "This block changed since it was drawn.",
|
|
547
|
+
"busy": "Too many live blocks are reading right now.",
|
|
548
|
+
"link-unchecked": "This link has to be checked again.",
|
|
549
|
+
"link-unsupported": "This is not a link the machine can open.",
|
|
550
|
+
"links-unsupported": "The machine does not open links from blocks.",
|
|
551
|
+
"no-unique-match": "The source found no single match for that name.",
|
|
552
|
+
"outside-project": "This is outside the project the agent wrote this in.",
|
|
553
|
+
"query-undeclared": "This block does not read that source.",
|
|
554
|
+
"refresh-limit": "This source reads at most once every ten seconds.",
|
|
555
|
+
"result-too-large": "What the source returned is too large.",
|
|
556
|
+
"snapshot-too-large": "The first readings were too large to keep.",
|
|
557
|
+
"source-unregistered": "The machine does not know this source.",
|
|
558
|
+
"stale-read": "Read the source again before you choose.",
|
|
559
|
+
"target-unavailable": "What this points to is no longer available.",
|
|
560
|
+
"timed-out": "The source took too long to answer.",
|
|
561
|
+
"unreadable": "The source could not be read."
|
|
562
|
+
}
|
|
563
|
+
}
|
|
463
564
|
}
|
|
464
565
|
}
|