@adecore/agents-react 0.37.2 → 0.38.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.
- package/dist/chat/chat-client.d.ts +1 -0
- package/dist/chat/chat-client.js +2 -1
- package/dist/chat/chat-references.d.ts +16 -1
- package/dist/chat/chat-references.js +29 -3
- package/dist/chat/drafts.d.ts +2 -0
- package/dist/chat/drafts.js +21 -5
- package/dist/chat/index.d.ts +1 -1
- package/dist/chat/logic/visual-height.d.ts +1 -0
- package/dist/chat/logic/visual-height.js +1 -1
- package/dist/chat/mentions.d.ts +31 -14
- package/dist/chat/mentions.js +186 -41
- package/dist/chat/stash.d.ts +1 -0
- package/dist/chat/stash.js +4 -2
- package/dist/chat/ui/Composer.js +438 -429
- package/dist/chat/ui/ComposerInput.d.ts +1 -0
- package/dist/chat/ui/ComposerInput.js +2 -0
- package/dist/chat/ui/HostMenuItems.d.ts +4 -0
- package/dist/chat/ui/HostMenuItems.js +15 -0
- package/dist/chat/ui/Markdown.d.ts +3 -1
- package/dist/chat/ui/Markdown.js +194 -120
- package/dist/chat/ui/MessageActions.js +70 -66
- package/dist/chat/ui/ReferenceChip.d.ts +15 -0
- package/dist/chat/ui/ReferenceChip.js +255 -0
- package/dist/chat/ui/Timeline.js +52 -29
- package/dist/chat/ui/TimelineMenu.js +84 -78
- package/dist/chat/ui/VisualFrame.d.ts +8 -0
- package/dist/chat/ui/VisualFrame.js +217 -115
- package/dist/chat/ui/chip-clipboard.d.ts +9 -0
- package/dist/chat/ui/chip-clipboard.js +105 -0
- package/dist/chat/ui/chips.d.ts +5 -0
- package/dist/chat/ui/chips.js +15 -1
- package/dist/chat/ui/composer/ComposerMenu.js +140 -79
- package/dist/chat/ui/composer/DraftParts.d.ts +0 -4
- package/dist/chat/ui/composer/DraftParts.js +0 -41
- package/dist/chat/ui/composer/chip-paste.d.ts +15 -0
- package/dist/chat/ui/composer/chip-paste.js +104 -0
- package/dist/chat/ui/composer/chips.d.ts +5 -0
- package/dist/chat/ui/composer/chips.js +59 -9
- package/dist/chat/ui/composer/key-handler.js +9 -0
- package/dist/chat/ui/composer/use-composer-draft.d.ts +0 -1
- package/dist/chat/ui/composer/use-composer-draft.js +2 -10
- package/dist/chat/ui/composer/use-composer-menus.d.ts +12 -2
- package/dist/chat/ui/composer/use-composer-menus.js +192 -149
- package/dist/chat/ui/composer/use-composer-submit.js +17 -12
- package/dist/chat/ui/composer/use-draft-files.d.ts +7 -1
- package/dist/chat/ui/composer/use-draft-files.js +90 -58
- package/dist/chat/ui/composer/use-take-back.js +2 -1
- package/dist/chat/ui/index.d.ts +1 -1
- package/dist/chat/ui/index.js +1 -1
- package/dist/chat/ui/reference-icons.d.ts +3 -0
- package/dist/chat/ui/reference-icons.js +15 -0
- package/dist/chat/ui/reference-lookup.d.ts +7 -0
- package/dist/chat/ui/reference-lookup.js +6 -0
- package/dist/chat/ui/rehype-chips.d.ts +8 -1
- package/dist/chat/ui/rehype-chips.js +35 -11
- package/dist/chat/ui/rows/GeneratedImageRow.js +8 -4
- package/dist/chat/ui/rows/MessageRows.js +218 -158
- package/dist/chat/ui/rows/VisualRow.js +184 -142
- package/dist/chat/ui/rows/row-rhythm.js +6 -4
- package/dist/chat/ui/thread-references.d.ts +4 -0
- package/dist/chat/ui/thread-references.js +78 -0
- package/dist/host-hooks.d.ts +2 -0
- package/dist/host-hooks.js +7 -0
- package/dist/host.d.ts +14 -1
- package/dist/index.d.ts +1 -1
- package/dist/locales/en/agent-chat.d.ts +20 -3
- package/dist/locales/en/agent-chat.js +24 -7
- package/dist/locales/nl/agent-chat.d.ts +20 -3
- package/dist/locales/nl/agent-chat.js +24 -7
- package/package.json +8 -8
- package/src/chat/chat-client.ts +4 -1
- package/src/chat/chat-references.ts +44 -3
- package/src/chat/drafts.ts +24 -6
- package/src/chat/index.ts +1 -1
- package/src/chat/logic/visual-height.ts +1 -1
- package/src/chat/mentions.ts +202 -36
- package/src/chat/stash.ts +6 -2
- package/src/chat/ui/Composer.tsx +14 -4
- package/src/chat/ui/ComposerInput.tsx +4 -0
- package/src/chat/ui/HostMenuItems.tsx +11 -0
- package/src/chat/ui/Markdown.tsx +57 -25
- package/src/chat/ui/MessageActions.tsx +5 -2
- package/src/chat/ui/ReferenceChip.tsx +74 -0
- package/src/chat/ui/Timeline.tsx +24 -2
- package/src/chat/ui/TimelineMenu.tsx +7 -3
- package/src/chat/ui/VisualFrame.tsx +68 -21
- package/src/chat/ui/chip-clipboard.ts +115 -0
- package/src/chat/ui/chips.ts +20 -1
- package/src/chat/ui/composer/ComposerMenu.tsx +53 -37
- package/src/chat/ui/composer/DraftParts.tsx +0 -12
- package/src/chat/ui/composer/chip-paste.ts +122 -0
- package/src/chat/ui/composer/chips.ts +60 -10
- package/src/chat/ui/composer/key-handler.ts +9 -0
- package/src/chat/ui/composer/use-composer-draft.ts +1 -7
- package/src/chat/ui/composer/use-composer-menus.ts +68 -29
- package/src/chat/ui/composer/use-composer-submit.ts +13 -6
- package/src/chat/ui/composer/use-draft-files.ts +39 -5
- package/src/chat/ui/composer/use-take-back.ts +2 -1
- package/src/chat/ui/index.ts +1 -1
- package/src/chat/ui/reference-icons.ts +17 -0
- package/src/chat/ui/reference-lookup.ts +13 -0
- package/src/chat/ui/rehype-chips.ts +38 -13
- package/src/chat/ui/rows/GeneratedImageRow.tsx +9 -4
- package/src/chat/ui/rows/MessageRows.tsx +88 -64
- package/src/chat/ui/rows/VisualRow.tsx +5 -1
- package/src/chat/ui/rows/row-rhythm.ts +6 -4
- package/src/chat/ui/thread-references.ts +47 -0
- package/src/host-hooks.ts +9 -0
- package/src/host.ts +24 -1
- package/src/index.ts +1 -0
- package/src/locales/en/agent-chat.ts +24 -7
- package/src/locales/nl/agent-chat.ts +24 -7
- package/dist/chat/ui/ChatReferenceChip.d.ts +0 -4
- package/dist/chat/ui/ChatReferenceChip.js +0 -110
- package/src/chat/ui/ChatReferenceChip.tsx +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chipText, tokenizeChips, type ChipSegment } from '../mentions';
|
|
1
|
+
import { chipText, tokenizeChips, tokenizeNamedReferences, type ChipSegment, type ReferenceToken } from '../mentions';
|
|
2
2
|
|
|
3
3
|
/* Just the part of hast this plugin touches; the package's own types are not a dependency here. */
|
|
4
4
|
interface HastText {
|
|
@@ -22,41 +22,66 @@ interface HastParent {
|
|
|
22
22
|
export interface ChipOptions {
|
|
23
23
|
mentions?: string[];
|
|
24
24
|
skills?: string[];
|
|
25
|
+
references?: readonly ReferenceToken[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface NamedReferenceOptions {
|
|
29
|
+
items: readonly ReferenceToken[];
|
|
30
|
+
/* Ids a person pointed at with `#` in the thread, which win over another of the same name. */
|
|
31
|
+
referenced?: readonly string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function valueOf(segment: Exclude<ChipSegment, { kind: 'text' }>): string {
|
|
35
|
+
if (segment.kind === 'mention') {
|
|
36
|
+
return segment.path;
|
|
37
|
+
}
|
|
38
|
+
return segment.kind === 'skill' ? segment.name : segment.id;
|
|
25
39
|
}
|
|
26
40
|
|
|
27
41
|
// Inline code overrules a chip: `@a.ts` between backticks is code, not a mention.
|
|
28
42
|
const SKIPPED = new Set(['code', 'pre']);
|
|
29
43
|
|
|
30
|
-
|
|
44
|
+
// A link already leads somewhere of its own.
|
|
45
|
+
const SKIPPED_IN_REPLIES = new Set(['code', 'pre', 'a']);
|
|
46
|
+
|
|
47
|
+
/* A chip as an element: the renderer reads `data-chip` and `data-value` (a reference's id), the text keeps the sigil. */
|
|
31
48
|
function chipElement(segment: Exclude<ChipSegment, { kind: 'text' }>): HastElement {
|
|
32
49
|
return {
|
|
33
50
|
type: 'element',
|
|
34
51
|
tagName: 'span',
|
|
35
|
-
properties: { dataChip: segment.kind, dataValue: segment
|
|
52
|
+
properties: { dataChip: segment.kind, dataValue: valueOf(segment) },
|
|
36
53
|
children: [{ type: 'text', value: chipText(segment) }]
|
|
37
54
|
};
|
|
38
55
|
}
|
|
39
56
|
|
|
40
|
-
function split(parent: HastParent,
|
|
57
|
+
function split(parent: HastParent, tokenize: (text: string) => ChipSegment[], skipped: ReadonlySet<string>): void {
|
|
41
58
|
parent.children = parent.children.flatMap((child): HastNode[] => {
|
|
42
59
|
if (child.type === 'text') {
|
|
43
|
-
return
|
|
44
|
-
segment.kind === 'text' ? { type: 'text', value: segment.text } : chipElement(segment)
|
|
45
|
-
);
|
|
60
|
+
return tokenize(child.value).map((segment) => (segment.kind === 'text' ? { type: 'text', value: segment.text } : chipElement(segment)));
|
|
46
61
|
}
|
|
47
|
-
if (child.type === 'element' && !
|
|
48
|
-
split(child,
|
|
62
|
+
if (child.type === 'element' && !skipped.has(child.tagName)) {
|
|
63
|
+
split(child, tokenize, skipped);
|
|
49
64
|
}
|
|
50
65
|
return [child];
|
|
51
66
|
});
|
|
52
67
|
}
|
|
53
68
|
|
|
54
|
-
/* The `@path` and
|
|
55
|
-
export function rehypeChips({ mentions = [], skills = [] }: ChipOptions = {}) {
|
|
69
|
+
/* The `@path`, `$name` and `#name` tokens a person picked in the composer, cut out of the text as chips. */
|
|
70
|
+
export function rehypeChips({ mentions = [], skills = [], references = [] }: ChipOptions = {}) {
|
|
71
|
+
return (tree: HastParent) => {
|
|
72
|
+
if (mentions.length === 0 && skills.length === 0 && references.length === 0) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
split(tree, (text) => tokenizeChips(text, mentions, skills, references), SKIPPED);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* The `#Name` a reply spells for a view, node or chat by exactly its name, cut out of the text as chips. */
|
|
80
|
+
export function rehypeNamedReferences({ items, referenced = [] }: NamedReferenceOptions) {
|
|
56
81
|
return (tree: HastParent) => {
|
|
57
|
-
if (
|
|
82
|
+
if (items.length === 0) {
|
|
58
83
|
return;
|
|
59
84
|
}
|
|
60
|
-
split(tree,
|
|
85
|
+
split(tree, (text) => (text.includes('#') ? tokenizeNamedReferences(text, items, referenced) : [{ kind: 'text', text }]), SKIPPED_IN_REPLIES);
|
|
61
86
|
};
|
|
62
87
|
}
|
|
@@ -13,6 +13,7 @@ import { attachmentAspect, generatedImageView, type GeneratedImageFailure } from
|
|
|
13
13
|
import { toolStartedAt } from '../../logic/tools';
|
|
14
14
|
import { CHIP_IN_MESSAGE, MENTION_TONE } from '../chips';
|
|
15
15
|
import { useFileLinkCwd, useOpenFileLink } from '../file-links';
|
|
16
|
+
import { HostMenuItems } from '../HostMenuItems';
|
|
16
17
|
import { ImageThumb } from '../ImageView';
|
|
17
18
|
import { ROW_GUTTER } from '../icons';
|
|
18
19
|
import { RunningFor, WorkRow } from './WorkRows';
|
|
@@ -104,6 +105,7 @@ function ReadyImage({ chatId, attachment, prompt, transparent }: { chatId: strin
|
|
|
104
105
|
const missing = source.failure !== null;
|
|
105
106
|
const aspect = attachmentAspect(attachment);
|
|
106
107
|
const { open, saveToProject } = attachments;
|
|
108
|
+
const extra = attachments.actions?.(scopeId, chatId, attachment) ?? [];
|
|
107
109
|
|
|
108
110
|
const detail = [
|
|
109
111
|
attachment.width !== undefined && attachment.height !== undefined
|
|
@@ -171,13 +173,16 @@ function ReadyImage({ chatId, attachment, prompt, transparent }: { chatId: strin
|
|
|
171
173
|
>
|
|
172
174
|
<ButtonGroup>
|
|
173
175
|
<IconButton icon={Maximize2} size="sm" label={t('generatedImage.openLarge')} onClick={() => setLightbox(true)} />
|
|
174
|
-
{prompt !== null && (
|
|
176
|
+
{(prompt !== null || extra.length > 0) && (
|
|
175
177
|
<Menu.Root>
|
|
176
178
|
<IconButton icon={Ellipsis} size="sm" label={t('generatedImage.more')} render={<Menu.Trigger />} />
|
|
177
179
|
<Menu.Popup align="end">
|
|
178
|
-
|
|
179
|
-
<
|
|
180
|
-
|
|
180
|
+
{prompt !== null && (
|
|
181
|
+
<Menu.Item onClick={() => copyText(prompt)}>
|
|
182
|
+
<Icon icon={Copy} size={14} /> {t('generatedImage.copyPrompt')}
|
|
183
|
+
</Menu.Item>
|
|
184
|
+
)}
|
|
185
|
+
<HostMenuItems actions={extra} />
|
|
181
186
|
</Menu.Popup>
|
|
182
187
|
</Menu.Root>
|
|
183
188
|
)}
|
|
@@ -18,7 +18,10 @@ import { useChatScope } from '../../../scope';
|
|
|
18
18
|
import { splitQuote } from '../../quote';
|
|
19
19
|
import { jumpToTimelineItem } from '../../timeline-scroll';
|
|
20
20
|
import { useProviders } from '../../../state/providers';
|
|
21
|
-
import {
|
|
21
|
+
import { referenceTokens } from '../../chat-references';
|
|
22
|
+
import { presentReferences } from '../../mentions';
|
|
23
|
+
import { ReferenceChip } from '../ReferenceChip';
|
|
24
|
+
import { ReferenceLookupContext } from '../reference-lookup';
|
|
22
25
|
import { ImageThumb } from '../ImageView';
|
|
23
26
|
import { Markdown, MessageMarkdown, ReplyMarkdown } from '../Markdown';
|
|
24
27
|
import { FadingWords } from '../FadingWords';
|
|
@@ -34,6 +37,8 @@ import { useOpenForFind } from '../find-reveal';
|
|
|
34
37
|
import { ReplyContext, UiReplyNavigationContext } from '../reply-context';
|
|
35
38
|
import { UiReply } from '../intelligent-ui/UiReply';
|
|
36
39
|
import { useHostAttachmentUrl, useHostChatPlace, useHostReplyAuthor, useHostStreaming } from '../../../host-hooks';
|
|
40
|
+
import { copySelectionWithChips } from '../chip-clipboard';
|
|
41
|
+
import { useReferenceLookup } from '../thread-references';
|
|
37
42
|
|
|
38
43
|
// A long prompt folds so the answer stays in view; the reader can open it.
|
|
39
44
|
const USER_FOLD_LINES = 8;
|
|
@@ -89,9 +94,14 @@ export function UserRow({ chatId, item }: { chatId: string; item: ChatUserItem }
|
|
|
89
94
|
const [open, setOpen] = useState(false);
|
|
90
95
|
const long = item.text.split('\n').length > USER_FOLD_LINES || item.text.length > USER_FOLD_CHARS;
|
|
91
96
|
const attachments = item.attachments ?? [];
|
|
92
|
-
const chats = item.chats ?? [];
|
|
93
97
|
const [missing, setMissing] = useState(false);
|
|
94
98
|
const { keyOf } = useChatScope();
|
|
99
|
+
const lookup = useReferenceLookup(chatId);
|
|
100
|
+
const referenced = [...(item.chats ?? []), ...(item.references ?? [])];
|
|
101
|
+
const references = referenceTokens(referenced, lookup.items);
|
|
102
|
+
// A chat attached before `#` existed, or a view renamed or gone since, is not in the text; it stands beside it.
|
|
103
|
+
const placed = presentReferences(item.text, referenced, references);
|
|
104
|
+
const beside = referenced.filter((id) => !placed.includes(id));
|
|
95
105
|
useOpenForFind(item.id, 'text', setOpen);
|
|
96
106
|
// A quote only leads somewhere when the message still opens with the blockquote it was sent with.
|
|
97
107
|
const quoted = item.quoteSource === undefined ? null : splitQuote(item.text);
|
|
@@ -104,65 +114,69 @@ export function UserRow({ chatId, item }: { chatId: string; item: ChatUserItem }
|
|
|
104
114
|
}
|
|
105
115
|
}
|
|
106
116
|
return (
|
|
107
|
-
<
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
</div>
|
|
119
|
-
)}
|
|
120
|
-
{chats.length > 0 && (
|
|
121
|
-
<div className="mb-1.5 flex max-w-[80%] flex-wrap justify-end gap-1.5">
|
|
122
|
-
{chats.map((id) => (
|
|
123
|
-
<ChatReferenceChip key={id} chatId={id} />
|
|
124
|
-
))}
|
|
125
|
-
</div>
|
|
126
|
-
)}
|
|
127
|
-
{item.uiChoice !== undefined && <ChoiceLine chatId={chatId} choice={item.uiChoice} />}
|
|
128
|
-
{item.text !== '' && (
|
|
129
|
-
<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">
|
|
130
|
-
<div data-find-field="text" className={clsx(long && !open && FOLD)}>
|
|
131
|
-
{quoted !== null && source !== undefined ? (
|
|
132
|
-
<>
|
|
133
|
-
{/* A div, since the quote is block markdown and a button holds none. */}
|
|
134
|
-
<Tooltip label={t('composer.quote.goToSource')}>
|
|
135
|
-
<div
|
|
136
|
-
role="button"
|
|
137
|
-
tabIndex={0}
|
|
138
|
-
className="mb-2 cursor-pointer rounded-sm border-l-2 border-border-strong pl-2.5 text-text-muted hover:bg-text/5 hover:text-text focus-visible:outline-2 focus-visible:outline-accent"
|
|
139
|
-
onClick={goToSource}
|
|
140
|
-
onKeyDown={(e) => {
|
|
141
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
142
|
-
e.preventDefault();
|
|
143
|
-
goToSource();
|
|
144
|
-
}
|
|
145
|
-
}}
|
|
146
|
-
>
|
|
147
|
-
<MessageMarkdown text={quoted.quote} />
|
|
148
|
-
</div>
|
|
149
|
-
</Tooltip>
|
|
150
|
-
{missing && <div className="mb-2 text-xs text-text-faint">{t('rows.user.quoteGone')}</div>}
|
|
151
|
-
{quoted.rest !== '' && <MessageMarkdown text={quoted.rest} mentions={item.mentions} skills={item.skills} />}
|
|
152
|
-
</>
|
|
153
|
-
) : (
|
|
154
|
-
<MessageMarkdown text={item.text} mentions={item.mentions} skills={item.skills} />
|
|
117
|
+
<ReferenceLookupContext value={lookup}>
|
|
118
|
+
<div data-find-item={item.id} className="flex flex-col items-end" onCopy={(e) => copySelectionWithChips(e.nativeEvent)}>
|
|
119
|
+
<MessageHeading>{t('rows.user.heading')}</MessageHeading>
|
|
120
|
+
{attachments.length > 0 && (
|
|
121
|
+
<div className="mb-1.5 flex max-w-[80%] flex-wrap justify-end gap-1.5">
|
|
122
|
+
{attachments.map((attachment) =>
|
|
123
|
+
isImageAttachment(attachment.mime) ? (
|
|
124
|
+
<AttachmentThumb key={attachment.id} chatId={chatId} attachment={attachment} />
|
|
125
|
+
) : (
|
|
126
|
+
<AttachmentLink key={attachment.id} chatId={chatId} attachment={attachment} />
|
|
127
|
+
)
|
|
155
128
|
)}
|
|
156
129
|
</div>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
130
|
+
)}
|
|
131
|
+
{beside.length > 0 && (
|
|
132
|
+
<div className="mb-1.5 flex max-w-[80%] flex-wrap justify-end gap-1.5">
|
|
133
|
+
{beside.map((id) => (
|
|
134
|
+
<ReferenceChip key={id} id={id} />
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
{item.uiChoice !== undefined && <ChoiceLine chatId={chatId} choice={item.uiChoice} />}
|
|
139
|
+
{item.text !== '' && (
|
|
140
|
+
<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">
|
|
141
|
+
<div data-find-field="text" className={clsx(long && !open && FOLD)}>
|
|
142
|
+
{quoted !== null && source !== undefined ? (
|
|
143
|
+
<>
|
|
144
|
+
{/* A div, since the quote is block markdown and a button holds none. */}
|
|
145
|
+
<Tooltip label={t('composer.quote.goToSource')}>
|
|
146
|
+
<div
|
|
147
|
+
role="button"
|
|
148
|
+
tabIndex={0}
|
|
149
|
+
className="mb-2 cursor-pointer rounded-sm border-l-2 border-border-strong pl-2.5 text-text-muted hover:bg-text/5 hover:text-text focus-visible:outline-2 focus-visible:outline-accent"
|
|
150
|
+
onClick={goToSource}
|
|
151
|
+
onKeyDown={(e) => {
|
|
152
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
goToSource();
|
|
155
|
+
}
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
<MessageMarkdown text={quoted.quote} />
|
|
159
|
+
</div>
|
|
160
|
+
</Tooltip>
|
|
161
|
+
{missing && <div className="mb-2 text-xs text-text-faint">{t('rows.user.quoteGone')}</div>}
|
|
162
|
+
{quoted.rest !== '' && (
|
|
163
|
+
<MessageMarkdown text={quoted.rest} mentions={item.mentions} skills={item.skills} references={references} />
|
|
164
|
+
)}
|
|
165
|
+
</>
|
|
166
|
+
) : (
|
|
167
|
+
<MessageMarkdown text={item.text} mentions={item.mentions} skills={item.skills} references={references} />
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
{long && (
|
|
171
|
+
<button className="mt-1 flex items-center gap-1 text-xs text-text-muted hover:text-text" onClick={() => setOpen((o) => !o)}>
|
|
172
|
+
<Icon icon={ChevronDown} size={12} className={clsx('transition-transform', open && 'rotate-180')} />{' '}
|
|
173
|
+
{open ? t('rows.user.showLess') : t('rows.user.showAll')}
|
|
174
|
+
</button>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
</ReferenceLookupContext>
|
|
166
180
|
);
|
|
167
181
|
}
|
|
168
182
|
|
|
@@ -262,10 +276,12 @@ function Writing() {
|
|
|
262
276
|
* A reply. A word at a time, it follows the text as it arrives. A block at a time, the blocks that
|
|
263
277
|
* closed are drawn and fade in as they land, with "Writing…" where the next one will stand. Whole,
|
|
264
278
|
* it says it is being written and fades in once it is done. Only a row that saw the reply being
|
|
265
|
-
* written fades anything: an old reply scrolled back into view just stands there.
|
|
279
|
+
* written fades anything: an old reply scrolled back into view just stands there. A `#Name` it
|
|
280
|
+
* spells for a view, node or chat of the project is a chip that opens it.
|
|
266
281
|
*/
|
|
267
282
|
export function AssistantRow({ chatId, item: derived }: { chatId: string; item: ChatAssistantItem }) {
|
|
268
283
|
const item = useCurrentItem(chatId, derived);
|
|
284
|
+
const lookup = useReferenceLookup(chatId);
|
|
269
285
|
const scope = useChatScope();
|
|
270
286
|
const context = useContext(ReplyContext);
|
|
271
287
|
const reply = context === null && !item.parentToolUseId ? { scopeId: scope.id, chatId, itemId: item.id, streaming: item.streaming } : undefined;
|
|
@@ -295,11 +311,13 @@ export function AssistantRow({ chatId, item: derived }: { chatId: string; item:
|
|
|
295
311
|
if (mode === 'blocks') {
|
|
296
312
|
const settled = item.streaming ? settledBlocksText(item.text) : item.text;
|
|
297
313
|
return (
|
|
298
|
-
<div data-find-item={item.id} className="-mx-1 px-1 pb-2">
|
|
314
|
+
<div data-find-item={item.id} className="-mx-1 px-1 pb-2" onCopy={(e) => copySelectionWithChips(e.nativeEvent)}>
|
|
299
315
|
<ReplyHeading chatId={chatId} />
|
|
300
316
|
{settled !== '' && (
|
|
301
317
|
<div data-find-field="text" data-quote-answer className="contents">
|
|
302
|
-
<
|
|
318
|
+
<ReferenceLookupContext value={lookup}>
|
|
319
|
+
<ReplyMarkdown text={settled} streaming={false} arriving={sawWriting} reply={reply} />
|
|
320
|
+
</ReferenceLookupContext>
|
|
303
321
|
</div>
|
|
304
322
|
)}
|
|
305
323
|
{item.streaming && <Writing />}
|
|
@@ -315,10 +333,16 @@ export function AssistantRow({ chatId, item: derived }: { chatId: string; item:
|
|
|
315
333
|
);
|
|
316
334
|
}
|
|
317
335
|
return (
|
|
318
|
-
<div
|
|
336
|
+
<div
|
|
337
|
+
data-find-item={item.id}
|
|
338
|
+
className={clsx('-mx-1 px-1 pb-2', mode === 'whole' && sawWriting && WHOLE_FADE_CLASS)}
|
|
339
|
+
onCopy={(e) => copySelectionWithChips(e.nativeEvent)}
|
|
340
|
+
>
|
|
319
341
|
<ReplyHeading chatId={chatId} />
|
|
320
342
|
<div data-find-field="text" data-quote-answer className="contents">
|
|
321
|
-
<
|
|
343
|
+
<ReferenceLookupContext value={lookup}>
|
|
344
|
+
<ReplyMarkdown text={reveal.text} streaming={reveal.active} reply={reply} />
|
|
345
|
+
</ReferenceLookupContext>
|
|
322
346
|
</div>
|
|
323
347
|
{live && item.text === '' && <span className="inline-block h-3.5 w-1.5 animate-pulse rounded-sm bg-text-faint align-middle" />}
|
|
324
348
|
</div>
|
|
@@ -5,6 +5,7 @@ import { ButtonGroup, ErrorBoundary, Icon, IconButton, Menu, Surface } from '@ad
|
|
|
5
5
|
import { useChatHost } from '../../../host';
|
|
6
6
|
import { useChatScope } from '../../../scope';
|
|
7
7
|
import { useVisualDialog } from '../../visuals';
|
|
8
|
+
import { HostMenuItems } from '../HostMenuItems';
|
|
8
9
|
import { VisualFrame } from '../VisualFrame';
|
|
9
10
|
|
|
10
11
|
/*
|
|
@@ -16,10 +17,11 @@ import { VisualFrame } from '../VisualFrame';
|
|
|
16
17
|
export function VisualRow({ chatId, visual }: { chatId: string; visual: ChatVisual }) {
|
|
17
18
|
const host = useChatHost();
|
|
18
19
|
const { t } = useTranslation('agent-chat');
|
|
19
|
-
const { keyOf } = useChatScope();
|
|
20
|
+
const { id: scopeId, keyOf } = useChatScope();
|
|
20
21
|
if (host.visuals === null) {
|
|
21
22
|
return null;
|
|
22
23
|
}
|
|
24
|
+
const extra = host.visuals.actions?.(scopeId, chatId, visual) ?? [];
|
|
23
25
|
const open = (kind: 'expand' | 'remove'): void => useVisualDialog.getState().open(keyOf(chatId), visual.id, kind);
|
|
24
26
|
return (
|
|
25
27
|
<ErrorBoundary label={t('visuals.renderFailed')} resetKeys={[visual]} compact className="relative rounded-lg">
|
|
@@ -35,6 +37,8 @@ export function VisualRow({ chatId, visual }: { chatId: string; visual: ChatVisu
|
|
|
35
37
|
<Menu.Root>
|
|
36
38
|
<IconButton icon={Ellipsis} size="sm" label={t('visuals.more')} render={<Menu.Trigger />} />
|
|
37
39
|
<Menu.Popup align="end">
|
|
40
|
+
<HostMenuItems actions={extra} />
|
|
41
|
+
{extra.length > 0 && <Menu.Separator />}
|
|
38
42
|
<Menu.Item onClick={() => open('remove')}>
|
|
39
43
|
<Icon icon={Trash2} size={14} /> {t('visuals.remove')}
|
|
40
44
|
</Menu.Item>
|
|
@@ -7,13 +7,15 @@ export const FOLLOW_THRESHOLD_PX = 40;
|
|
|
7
7
|
|
|
8
8
|
/*
|
|
9
9
|
* The gaps a row wears for where it sits: a question is followed by its answer gap, and a seam where
|
|
10
|
-
* the list rhythm of tool lines meets the block rhythm of prose gets the block gap. A visual
|
|
11
|
-
* margin of
|
|
12
|
-
* already carries the gap before it, and the row after one is the answer, so neither
|
|
10
|
+
* the list rhythm of tool lines meets the block rhythm of prose gets the block gap. A visual and an
|
|
11
|
+
* app's card have no margin of their own like prose does, so the row under one takes the block gap
|
|
12
|
+
* too. A question already carries the gap before it, and the row after one is the answer, so neither
|
|
13
|
+
* takes a seam.
|
|
13
14
|
*/
|
|
14
15
|
export function rowRhythm(row: TimelineRow, previous: TimelineRow | null): string {
|
|
15
16
|
const question = row.kind === 'user';
|
|
16
|
-
const
|
|
17
|
+
const bare = previous?.kind === 'visual' || previous?.kind === 'app-card';
|
|
18
|
+
const seam = !question && previous !== null && previous.kind !== 'user' && (isBlock(row) !== isBlock(previous) || bare);
|
|
17
19
|
return clsx(question && 'pb-(--chat-answer-gap)', seam && 'pt-(--chat-block-gap)');
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ChatItem } from '@adecore/agent-contracts';
|
|
2
|
+
import { useChatHost } from '../../host';
|
|
3
|
+
import { useHostReferables } from '../../host-hooks';
|
|
4
|
+
import { useChatScope } from '../../scope';
|
|
5
|
+
import { useChatRow } from '../../state/chats';
|
|
6
|
+
import type { ReferenceLookup } from './reference-lookup';
|
|
7
|
+
|
|
8
|
+
const referencedOf = new WeakMap<Record<string, ChatItem>, string>();
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* The ids a person pointed at with `#` in a thread, one per line. A string, so the selector that reads
|
|
12
|
+
* it answers the same value until a message with another one comes; the structure a streamed word
|
|
13
|
+
* leaves alone is what it is cached on.
|
|
14
|
+
*/
|
|
15
|
+
export function threadReferencedKey(structure: Record<string, ChatItem> | undefined): string {
|
|
16
|
+
if (structure === undefined) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
const cached = referencedOf.get(structure);
|
|
20
|
+
if (cached !== undefined) {
|
|
21
|
+
return cached;
|
|
22
|
+
}
|
|
23
|
+
const ids = new Set<string>();
|
|
24
|
+
for (const item of Object.values(structure)) {
|
|
25
|
+
if (item.kind === 'user') {
|
|
26
|
+
for (const id of [...(item.chats ?? []), ...(item.references ?? [])]) {
|
|
27
|
+
ids.add(id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const key = [...ids].join('\n');
|
|
32
|
+
referencedOf.set(structure, key);
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* What the views, nodes and chats of a thread stand for now, and how one opens, for the rows that draw chips. */
|
|
37
|
+
export function useReferenceLookup(chatId: string): ReferenceLookup {
|
|
38
|
+
const { id: scopeId } = useChatScope();
|
|
39
|
+
const { openReference } = useChatHost();
|
|
40
|
+
const referables = useHostReferables(scopeId, chatId);
|
|
41
|
+
const key = useChatRow(chatId, (chat) => threadReferencedKey(chat?.structure));
|
|
42
|
+
return {
|
|
43
|
+
items: referables.items,
|
|
44
|
+
open: openReference ? (id: string) => openReference(scopeId, id) : null,
|
|
45
|
+
referenced: key === '' ? [] : key.split('\n')
|
|
46
|
+
};
|
|
47
|
+
}
|
package/src/host-hooks.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* key, so they never change under a mounted view; only these wrappers go uncompiled for it.
|
|
7
7
|
*/
|
|
8
8
|
import type { ProviderPlatform } from '@adecore/agent-contracts';
|
|
9
|
+
import { referablesOfChats, type Referables } from './chat/chat-references';
|
|
9
10
|
import { useChatHost, type ChatHost, type CommandLook } from './host';
|
|
10
11
|
|
|
11
12
|
export function useHostCodeMode(): 'light' | 'dark' {
|
|
@@ -48,6 +49,14 @@ export function useHostReferableChats(): ReturnType<ChatHost['useReferableChats'
|
|
|
48
49
|
return useChatHost().useReferableChats();
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
const useReferablesFromChats = (_scopeId: string, _chatId: string): Referables => referablesOfChats(useHostReferableChats());
|
|
53
|
+
|
|
54
|
+
/* A host without views of its own offers its chats. */
|
|
55
|
+
export function useHostReferables(scopeId: string, chatId: string): Referables {
|
|
56
|
+
const useReferables = useChatHost().useReferables ?? useReferablesFromChats;
|
|
57
|
+
return useReferables(scopeId, chatId);
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
export function useHostLoginBlocked(...args: Parameters<ChatHost['useLoginBlocked']>): ReturnType<ChatHost['useLoginBlocked']> {
|
|
52
61
|
return useChatHost().useLoginBlocked(...args);
|
|
53
62
|
}
|
package/src/host.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { createContext, createElement, useContext, useState, type ComponentProps
|
|
|
2
2
|
import type { Extension } from '@codemirror/state';
|
|
3
3
|
import type { EditorView } from '@codemirror/view';
|
|
4
4
|
import type { Highlighter, ThemeRegistration } from 'shiki';
|
|
5
|
+
import type { LucideIcon } from 'lucide-react';
|
|
5
6
|
import type { EditorEngine } from '@adecore/editor';
|
|
6
7
|
import type {
|
|
7
8
|
AgentKind,
|
|
9
|
+
ChatAttachment,
|
|
8
10
|
ChatCheckpointDiff,
|
|
9
11
|
ChatConfigurePayload,
|
|
10
12
|
ChatGoalAction,
|
|
@@ -15,9 +17,11 @@ import type {
|
|
|
15
17
|
ChatUiQueryReading,
|
|
16
18
|
ChatUiLinkPayload,
|
|
17
19
|
ChatUiLinkReading,
|
|
20
|
+
ChatVisual,
|
|
18
21
|
ProviderPlatform
|
|
19
22
|
} from '@adecore/agent-contracts';
|
|
20
23
|
import { isApplePlatform } from '@adecore/ui';
|
|
24
|
+
import type { Referables } from './chat/chat-references';
|
|
21
25
|
import type { TimelineRow } from './chat/logic/timeline';
|
|
22
26
|
import type { FindReveal } from './chat/ui/find-reveal';
|
|
23
27
|
import type { PromptAction } from './prompts/logic/prompts';
|
|
@@ -187,6 +191,19 @@ export interface VisualHost {
|
|
|
187
191
|
frameUrl: string;
|
|
188
192
|
/* Opens an http or https link a person followed in a visual, outside the app. */
|
|
189
193
|
openLink(url: string): void;
|
|
194
|
+
/* Actions of the app's own in a visual's menu, above Remove; left out, the menu holds Remove alone. */
|
|
195
|
+
actions?(scopeId: string, chatId: string, visual: ChatVisual): readonly HostMenuAction[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* An action of the app's own in the menu of something a chat made, such as keeping a copy somewhere of the app's. */
|
|
199
|
+
export interface HostMenuAction {
|
|
200
|
+
/* Unique among the actions of one menu. */
|
|
201
|
+
id: string;
|
|
202
|
+
label: string;
|
|
203
|
+
icon?: LucideIcon;
|
|
204
|
+
disabled?: boolean;
|
|
205
|
+
/* Runs once a person picks it; the host shows a failure itself. */
|
|
206
|
+
run(): void;
|
|
190
207
|
}
|
|
191
208
|
|
|
192
209
|
/* Who writes a chat's replies, as the header over each one names them. */
|
|
@@ -328,6 +345,8 @@ export interface ChatHost {
|
|
|
328
345
|
* Resolves with the path as a file link opens it, or null when the person cancels; left out, a generated image offers no Save.
|
|
329
346
|
*/
|
|
330
347
|
saveToProject?(scopeId: string, chatId: string, attachmentId: string, suggestedName: string): Promise<string | null>;
|
|
348
|
+
/* Actions of the app's own in a generated image's menu, after the chat's own; left out, the menu holds those alone. */
|
|
349
|
+
actions?(scopeId: string, chatId: string, attachment: ChatAttachment): readonly HostMenuAction[];
|
|
331
350
|
};
|
|
332
351
|
/* The image a tool read, drawn under its row; null draws none. */
|
|
333
352
|
ReadImage: ComponentType<{ path: string }> | null;
|
|
@@ -379,8 +398,12 @@ export interface ChatHost {
|
|
|
379
398
|
render(prompt: HostPrompt, props: PromptViewProps): ReactNode;
|
|
380
399
|
answer(prompt: HostPrompt, action: PromptAction): Promise<void>;
|
|
381
400
|
};
|
|
382
|
-
/* Other chats a message may point at with
|
|
401
|
+
/* Other chats a message may point at with `#`, for a host that has no views; `useReferables` takes over where it is given. */
|
|
383
402
|
useReferableChats(): ReadonlyArray<{ id: string; title: string }>;
|
|
403
|
+
/* The views and nodes a message of this chat may point at with `#`, under the names they have right now. */
|
|
404
|
+
useReferables?(scopeId: string, chatId: string): Referables;
|
|
405
|
+
/* Opens a view or zooms to a node a message points at; left out, a chip opens nothing. */
|
|
406
|
+
openReference?(scopeId: string, id: string): void;
|
|
384
407
|
/* Opens the CLI's own login for an account, somewhere the app can run a command; null offers none. */
|
|
385
408
|
openLogin: ((scopeId: string, kind: AgentKind, accountId: string, name: string) => Promise<void>) | null;
|
|
386
409
|
/* Why a login cannot start from here right now, or null when it can. */
|
package/src/index.ts
CHANGED
|
@@ -16,10 +16,9 @@ export default {
|
|
|
16
16
|
unreadable: 'Could not read {{name}}',
|
|
17
17
|
unreadableThumbnail: 'Could not read the thumbnail'
|
|
18
18
|
},
|
|
19
|
-
|
|
20
|
-
gone: '
|
|
21
|
-
untitled: 'Untitled
|
|
22
|
-
remove: 'Remove {{title}}'
|
|
19
|
+
references: {
|
|
20
|
+
gone: 'Not in this project',
|
|
21
|
+
untitled: 'Untitled'
|
|
23
22
|
},
|
|
24
23
|
fork: {
|
|
25
24
|
refusal: {
|
|
@@ -304,7 +303,8 @@ export default {
|
|
|
304
303
|
menu: {
|
|
305
304
|
commands: 'Commands',
|
|
306
305
|
skills: 'Skills',
|
|
307
|
-
mentions: 'Mentions'
|
|
306
|
+
mentions: 'Mentions',
|
|
307
|
+
references: 'Views and nodes'
|
|
308
308
|
},
|
|
309
309
|
held: {
|
|
310
310
|
question: 'Question waiting. Opens when you pause typing.',
|
|
@@ -349,10 +349,27 @@ export default {
|
|
|
349
349
|
mentions: {
|
|
350
350
|
here: 'Files in this folder',
|
|
351
351
|
files: 'Files',
|
|
352
|
-
|
|
353
|
-
noMatch: 'No files or chats match',
|
|
352
|
+
noMatch: 'No files match',
|
|
354
353
|
empty: 'No files here'
|
|
355
354
|
},
|
|
355
|
+
references: {
|
|
356
|
+
canvas: 'On this canvas',
|
|
357
|
+
views: 'Views',
|
|
358
|
+
noMatch: 'No view or node matches',
|
|
359
|
+
kinds: {
|
|
360
|
+
canvas: 'canvas',
|
|
361
|
+
chat: 'chat',
|
|
362
|
+
terminal: 'terminal',
|
|
363
|
+
note: 'note',
|
|
364
|
+
text: 'text',
|
|
365
|
+
drawing: 'drawing',
|
|
366
|
+
diagram: 'diagram',
|
|
367
|
+
browser: 'browser',
|
|
368
|
+
device: 'device',
|
|
369
|
+
file: 'file',
|
|
370
|
+
database: 'table'
|
|
371
|
+
}
|
|
372
|
+
},
|
|
356
373
|
queue: {
|
|
357
374
|
attachments_one: '{{count}} attachment',
|
|
358
375
|
attachments_other: '{{count}} attachments',
|
|
@@ -16,10 +16,9 @@ export default {
|
|
|
16
16
|
unreadable: 'Kon {{name}} niet lezen',
|
|
17
17
|
unreadableThumbnail: 'Kon de miniatuur niet lezen'
|
|
18
18
|
},
|
|
19
|
-
|
|
20
|
-
gone: '
|
|
21
|
-
untitled: '
|
|
22
|
-
remove: '{{title}} verwijderen'
|
|
19
|
+
references: {
|
|
20
|
+
gone: 'Niet in dit project',
|
|
21
|
+
untitled: 'Naamloos'
|
|
23
22
|
},
|
|
24
23
|
fork: {
|
|
25
24
|
refusal: {
|
|
@@ -304,7 +303,8 @@ export default {
|
|
|
304
303
|
menu: {
|
|
305
304
|
commands: "Commando's",
|
|
306
305
|
skills: 'Skills',
|
|
307
|
-
mentions: 'Vermeldingen'
|
|
306
|
+
mentions: 'Vermeldingen',
|
|
307
|
+
references: 'Views en nodes'
|
|
308
308
|
},
|
|
309
309
|
held: {
|
|
310
310
|
question: 'Er wacht een vraag. Opent zodra je stopt met typen.',
|
|
@@ -349,10 +349,27 @@ export default {
|
|
|
349
349
|
mentions: {
|
|
350
350
|
here: 'Bestanden in deze map',
|
|
351
351
|
files: 'Bestanden',
|
|
352
|
-
|
|
353
|
-
noMatch: 'Geen bestanden of chats gevonden',
|
|
352
|
+
noMatch: 'Geen bestanden gevonden',
|
|
354
353
|
empty: 'Hier staan geen bestanden'
|
|
355
354
|
},
|
|
355
|
+
references: {
|
|
356
|
+
canvas: 'Op dit canvas',
|
|
357
|
+
views: 'Views',
|
|
358
|
+
noMatch: 'Geen view of node gevonden',
|
|
359
|
+
kinds: {
|
|
360
|
+
canvas: 'canvas',
|
|
361
|
+
chat: 'chat',
|
|
362
|
+
terminal: 'terminal',
|
|
363
|
+
note: 'notitie',
|
|
364
|
+
text: 'tekst',
|
|
365
|
+
drawing: 'tekening',
|
|
366
|
+
diagram: 'diagram',
|
|
367
|
+
browser: 'browser',
|
|
368
|
+
device: 'apparaat',
|
|
369
|
+
file: 'bestand',
|
|
370
|
+
database: 'tabel'
|
|
371
|
+
}
|
|
372
|
+
},
|
|
356
373
|
queue: {
|
|
357
374
|
attachments_one: '{{count}} bijlage',
|
|
358
375
|
attachments_other: '{{count}} bijlagen',
|