@adecore/agents-react 0.20.0 → 0.21.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/logic/generated-image.d.ts +27 -0
- package/dist/chat/logic/generated-image.js +32 -0
- package/dist/chat/logic/timeline-target.d.ts +19 -1
- package/dist/chat/logic/timeline-target.js +49 -3
- package/dist/chat/logic/timeline.js +25 -17
- 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/CodeBlock.d.ts +3 -1
- package/dist/chat/ui/CodeBlock.js +38 -7
- package/dist/chat/ui/Composer.js +13 -3
- package/dist/chat/ui/ImageView.d.ts +8 -2
- package/dist/chat/ui/ImageView.js +12 -6
- package/dist/chat/ui/Markdown.d.ts +7 -1
- package/dist/chat/ui/Markdown.js +80 -23
- package/dist/chat/ui/MessageActions.js +6 -7
- package/dist/chat/ui/SubagentTimeline.js +7 -3
- package/dist/chat/ui/Timeline.js +68 -8
- package/dist/chat/ui/TimelineMenu.js +7 -3
- package/dist/chat/ui/file-links.d.ts +2 -1
- package/dist/chat/ui/file-links.js +8 -3
- package/dist/chat/ui/intelligent-ui/UiBlockFooter.d.ts +15 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFooter.js +50 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.d.ts +10 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +61 -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 +13 -0
- package/dist/chat/ui/intelligent-ui/UiReply.js +325 -0
- package/dist/chat/ui/intelligent-ui/block-local.d.ts +2 -0
- package/dist/chat/ui/intelligent-ui/block-local.js +13 -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 +9 -0
- package/dist/chat/ui/intelligent-ui/node-text.js +52 -0
- package/dist/chat/ui/intelligent-ui/registry.d.ts +2 -0
- package/dist/chat/ui/intelligent-ui/registry.js +50 -0
- package/dist/chat/ui/intelligent-ui/render-context.d.ts +50 -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 +125 -0
- package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +4 -0
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +41 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +14 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.js +66 -0
- package/dist/chat/ui/intelligent-ui/renderers/data.d.ts +7 -0
- package/dist/chat/ui/intelligent-ui/renderers/data.js +87 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +7 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +60 -0
- package/dist/chat/ui/intelligent-ui/renderers/links.d.ts +10 -0
- package/dist/chat/ui/intelligent-ui/renderers/links.js +86 -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 +107 -0
- package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +20 -0
- package/dist/chat/ui/intelligent-ui/use-ui-queries.js +259 -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 +27 -7
- 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 +25 -3
- package/dist/host.js +1 -0
- package/dist/locales/en/agent-chat.json +89 -1
- package/dist/locales/nl/agent-chat.json +89 -1
- package/dist/state/chats.js +9 -0
- package/dist/theme.css +38 -2
- package/package.json +133 -7
- package/src/chat/chat-client.ts +1 -0
- package/src/chat/logic/generated-image.ts +48 -0
- package/src/chat/logic/timeline-target.ts +69 -4
- package/src/chat/logic/timeline.ts +28 -17
- package/src/chat/subagent-conversation.ts +13 -2
- package/src/chat/timeline-flash.ts +5 -3
- package/src/chat/ui/CodeBlock.tsx +70 -21
- package/src/chat/ui/Composer.tsx +22 -5
- package/src/chat/ui/ImageView.tsx +51 -8
- package/src/chat/ui/Markdown.tsx +123 -26
- package/src/chat/ui/MessageActions.tsx +15 -8
- package/src/chat/ui/SubagentTimeline.tsx +26 -1
- package/src/chat/ui/Timeline.tsx +98 -22
- package/src/chat/ui/TimelineMenu.tsx +6 -3
- package/src/chat/ui/file-links.ts +9 -3
- package/src/chat/ui/intelligent-ui/UiBlockFooter.tsx +139 -0
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +89 -0
- package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +21 -0
- package/src/chat/ui/intelligent-ui/UiReply.tsx +531 -0
- package/src/chat/ui/intelligent-ui/block-local.ts +16 -0
- package/src/chat/ui/intelligent-ui/chart-data.ts +67 -0
- package/src/chat/ui/intelligent-ui/node-text.ts +59 -0
- package/src/chat/ui/intelligent-ui/registry.ts +52 -0
- package/src/chat/ui/intelligent-ui/render-context.ts +78 -0
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +321 -0
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +98 -0
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +145 -0
- package/src/chat/ui/intelligent-ui/renderers/data.tsx +187 -0
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +121 -0
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +157 -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 +123 -0
- package/src/chat/ui/intelligent-ui/use-ui-queries.ts +282 -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 +69 -7
- 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 +46 -3
- package/src/locales/en/agent-chat.json +89 -1
- package/src/locales/nl/agent-chat.json +89 -1
- package/src/state/chats.ts +9 -0
- package/src/theme.css +38 -2
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { Fragment, memo, useContext, useEffect, useMemo, useState, type CSSProperties } from 'react';
|
|
1
|
+
import { Fragment, memo, useContext, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { Check, Copy } from 'lucide-react';
|
|
5
|
+
import { ButtonGroup, IconButton } from '@adecore/ui';
|
|
3
6
|
import type { BundledLanguage, BundledTheme, createHighlighter, ThemeRegistration } from 'shiki/bundle/web';
|
|
4
7
|
import { IncrementalLines, type CodeToken, type Tokenize } from './code-lines';
|
|
5
8
|
import { CodeStreamingContext } from './code-streaming';
|
|
@@ -10,6 +13,7 @@ type Highlighter = Awaited<ReturnType<typeof createHighlighter>>;
|
|
|
10
13
|
type GrammarState = ReturnType<Highlighter['getLastGrammarState']>;
|
|
11
14
|
// A plain language needs no grammar; shiki draws it in the theme's own foreground.
|
|
12
15
|
const PLAIN = 'text';
|
|
16
|
+
const COPIED_MS = 1500;
|
|
13
17
|
|
|
14
18
|
let highlighter: Highlighter | null = null;
|
|
15
19
|
let highlighterLoad: Promise<Highlighter> | null = null;
|
|
@@ -123,9 +127,12 @@ const CodeLine = memo(function CodeLine({ tokens }: { tokens: CodeToken[] }) {
|
|
|
123
127
|
* fence that is still open is tokenized as it grows, and the same component carries on once it
|
|
124
128
|
* closes, so closing it does not draw it again.
|
|
125
129
|
*/
|
|
126
|
-
export function CodeBlock({ code, lang }: { code: string; lang: string }) {
|
|
130
|
+
export function CodeBlock({ code, lang, actions }: { code: string; lang: string; actions?: ReactNode }) {
|
|
131
|
+
const { t } = useTranslation('agent-chat');
|
|
127
132
|
const theme = useCodeTheme();
|
|
128
133
|
const streaming = useContext(CodeStreamingContext);
|
|
134
|
+
const [copyState, setCopyState] = useState<'idle' | 'copying' | 'copied' | 'failed'>('idle');
|
|
135
|
+
const copying = useRef(false);
|
|
129
136
|
// What loaded last, so a block whose theme changes draws again once the new one is in.
|
|
130
137
|
const [loaded, setLoaded] = useState<string | null>(null);
|
|
131
138
|
const tokenizer = tokenizerFor(lang, theme);
|
|
@@ -134,6 +141,31 @@ export function CodeBlock({ code, lang }: { code: string; lang: string }) {
|
|
|
134
141
|
const lines = useMemo(() => (tokenizer ? new IncrementalLines(tokenizer.tokenize) : null), [tokenizer]);
|
|
135
142
|
const tokens = useMemo(() => lines?.update(code, !streaming) ?? null, [lines, code, streaming]);
|
|
136
143
|
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (copyState !== 'copied') {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const timer = setTimeout(() => setCopyState('idle'), COPIED_MS);
|
|
149
|
+
return () => clearTimeout(timer);
|
|
150
|
+
}, [copyState]);
|
|
151
|
+
|
|
152
|
+
async function copy(): Promise<void> {
|
|
153
|
+
if (copying.current) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
copying.current = true;
|
|
157
|
+
setCopyState('copying');
|
|
158
|
+
try {
|
|
159
|
+
// The best-effort clipboard helper cannot distinguish a refusal from success.
|
|
160
|
+
await navigator.clipboard.writeText(code);
|
|
161
|
+
setCopyState('copied');
|
|
162
|
+
} catch {
|
|
163
|
+
setCopyState('failed');
|
|
164
|
+
} finally {
|
|
165
|
+
copying.current = false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
137
169
|
useEffect(() => {
|
|
138
170
|
if (tokenizer !== null) {
|
|
139
171
|
return;
|
|
@@ -147,29 +179,46 @@ export function CodeBlock({ code, lang }: { code: string; lang: string }) {
|
|
|
147
179
|
};
|
|
148
180
|
}, [lang, theme, tokenizer]);
|
|
149
181
|
|
|
150
|
-
|
|
151
|
-
const failed = loaded === 'failed';
|
|
152
|
-
return (
|
|
153
|
-
<div className={clsx('chat-code', !failed && 'invisible')} aria-hidden={failed ? undefined : true}>
|
|
154
|
-
<pre>
|
|
155
|
-
<code>{code}</code>
|
|
156
|
-
</pre>
|
|
157
|
-
</div>
|
|
158
|
-
);
|
|
159
|
-
}
|
|
182
|
+
const pending = (tokenizer === null || tokens === null) && loaded !== 'failed';
|
|
160
183
|
return (
|
|
161
|
-
<div className=
|
|
162
|
-
<pre
|
|
184
|
+
<div className="chat-code" data-copy-state={copyState}>
|
|
185
|
+
<pre
|
|
186
|
+
className={clsx(pending && 'invisible', !pending && tokens !== null && waited && WHOLE_FADE_CLASS)}
|
|
187
|
+
aria-hidden={pending || undefined}
|
|
188
|
+
style={{ color: tokenizer?.fg }}
|
|
189
|
+
>
|
|
163
190
|
<code>
|
|
164
|
-
{tokens
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
191
|
+
{tokens === null
|
|
192
|
+
? code
|
|
193
|
+
: tokens.map((line, index) => (
|
|
194
|
+
// Lines only ever grow at the end, so the place of a line is a stable key.
|
|
195
|
+
<Fragment key={index}>
|
|
196
|
+
<CodeLine tokens={line} />
|
|
197
|
+
{index < tokens.length - 1 && '\n'}
|
|
198
|
+
</Fragment>
|
|
199
|
+
))}
|
|
171
200
|
</code>
|
|
172
201
|
</pre>
|
|
202
|
+
<ButtonGroup className="chat-code-actions" role="group" aria-label={t('code.actions')}>
|
|
203
|
+
<IconButton
|
|
204
|
+
icon={copyState === 'copied' ? Check : Copy}
|
|
205
|
+
size="sm"
|
|
206
|
+
label={copyState === 'copied' ? t('timeline.actions.copied') : t('timeline.menu.copyCode')}
|
|
207
|
+
busy={copyState === 'copying'}
|
|
208
|
+
aria-busy={copyState === 'copying'}
|
|
209
|
+
aria-disabled={copyState === 'copying'}
|
|
210
|
+
onClick={() => void copy()}
|
|
211
|
+
/>
|
|
212
|
+
{actions}
|
|
213
|
+
</ButtonGroup>
|
|
214
|
+
<span className="sr-only" role="status">
|
|
215
|
+
{copyState === 'copied' ? t('timeline.actions.copied') : ''}
|
|
216
|
+
</span>
|
|
217
|
+
{copyState === 'failed' && (
|
|
218
|
+
<p className="chat-code-error" role="alert">
|
|
219
|
+
{t('code.copyFailed')}
|
|
220
|
+
</p>
|
|
221
|
+
)}
|
|
173
222
|
</div>
|
|
174
223
|
);
|
|
175
224
|
}
|
package/src/chat/ui/Composer.tsx
CHANGED
|
@@ -166,6 +166,8 @@ function splitPath(path: string): { name: string; dir: string } {
|
|
|
166
166
|
// Keep the editor mounted while a pending request takes over, so its selection and draft survive.
|
|
167
167
|
export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabled, readOnly = false, providerFixed, onSend, onRetarget }: ComposerProps) {
|
|
168
168
|
const { t } = useTranslation('agent-chat');
|
|
169
|
+
const sendDelivery = chatHost().useSendDelivery();
|
|
170
|
+
const canSteer = info.provider === 'claude' || info.provider === 'codex';
|
|
169
171
|
const [draft, setDraft] = useState<ChatDraft>(() => readDraft(chatId));
|
|
170
172
|
const [historyIndex, setHistoryIndex] = useState<number | null>(null);
|
|
171
173
|
const [menuIndex, setMenuIndex] = useState(0);
|
|
@@ -602,7 +604,8 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
602
604
|
setDraft((current) => ({ ...current, attachments: current.attachments.filter((_, i) => i !== index) }));
|
|
603
605
|
};
|
|
604
606
|
|
|
605
|
-
const submit = (): void => {
|
|
607
|
+
const submit = (alternate = false): void => {
|
|
608
|
+
const delivery = canSteer && (alternate ? sendDelivery === 'queue' : sendDelivery === 'steer') ? 'steer' : 'queue';
|
|
606
609
|
const trimmed = text.trim();
|
|
607
610
|
if (isEmptyDraft(draft) || !writable || guard.tooLong) {
|
|
608
611
|
return;
|
|
@@ -633,6 +636,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
633
636
|
return;
|
|
634
637
|
}
|
|
635
638
|
onSend(withQuote(quote, trimmed), {
|
|
639
|
+
delivery,
|
|
636
640
|
mentions: presentMentions(trimmed, draft.mentions),
|
|
637
641
|
skills: presentSkills(trimmed, draft.skills),
|
|
638
642
|
chats: draft.chats,
|
|
@@ -871,7 +875,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
871
875
|
view.dispatch({ changes: { from: line.from, to: line.to }, selection: { anchor: line.from }, userEvent: 'delete' });
|
|
872
876
|
return true;
|
|
873
877
|
}
|
|
874
|
-
submit();
|
|
878
|
+
submit(e.altKey);
|
|
875
879
|
return true;
|
|
876
880
|
}
|
|
877
881
|
const { from, to } = view.state.selection.main;
|
|
@@ -1295,13 +1299,26 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
|
|
|
1295
1299
|
</button>
|
|
1296
1300
|
</Tooltip>
|
|
1297
1301
|
)}
|
|
1298
|
-
{/* While a turn runs the same button queues the message instead of sending it. */}
|
|
1299
1302
|
{(!busy || !isEmptyDraft(draft)) && (
|
|
1300
|
-
<Tooltip
|
|
1303
|
+
<Tooltip
|
|
1304
|
+
label={
|
|
1305
|
+
busy
|
|
1306
|
+
? t(
|
|
1307
|
+
canSteer
|
|
1308
|
+
? sendDelivery === 'steer'
|
|
1309
|
+
? 'composer.steerButton'
|
|
1310
|
+
: 'composer.queueWithSteerButton'
|
|
1311
|
+
: 'composer.queueButton'
|
|
1312
|
+
)
|
|
1313
|
+
: t('composer.send')
|
|
1314
|
+
}
|
|
1315
|
+
kbd={KEY_SHORTCUTS.modEnter}
|
|
1316
|
+
name
|
|
1317
|
+
>
|
|
1301
1318
|
<button
|
|
1302
1319
|
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent text-accent-text hover:brightness-90 disabled:opacity-50 disabled:hover:brightness-100"
|
|
1303
1320
|
disabled={isEmptyDraft(draft) || !writable || guard.tooLong}
|
|
1304
|
-
onClick={submit}
|
|
1321
|
+
onClick={(event) => submit(event.altKey)}
|
|
1305
1322
|
>
|
|
1306
1323
|
<Icon icon={ArrowUp} size={16} />
|
|
1307
1324
|
</button>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useState } from 'react';
|
|
1
|
+
import { useRef, useState, type ReactNode } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { ImageOff, Minus, Plus, RotateCcw } from 'lucide-react';
|
|
@@ -34,7 +34,19 @@ function zoomed(view: View, factor: number, pointX: number, pointY: number): Vie
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/* An image on its own, large: the wheel and the buttons zoom, dragging pans, Escape closes. */
|
|
37
|
-
function Lightbox({
|
|
37
|
+
function Lightbox({
|
|
38
|
+
src,
|
|
39
|
+
alt,
|
|
40
|
+
open,
|
|
41
|
+
onOpenChange,
|
|
42
|
+
actions
|
|
43
|
+
}: {
|
|
44
|
+
src: string;
|
|
45
|
+
alt: string;
|
|
46
|
+
open: boolean;
|
|
47
|
+
onOpenChange(open: boolean): void;
|
|
48
|
+
actions?: ReactNode;
|
|
49
|
+
}) {
|
|
38
50
|
const { t } = useTranslation('agent-chat');
|
|
39
51
|
const [view, setView] = useState<View>(START);
|
|
40
52
|
// The alt of an image an agent read is its path; the header shows the file and keeps the path on a tooltip.
|
|
@@ -84,6 +96,7 @@ function Lightbox({ src, alt, open, onOpenChange }: { src: string; alt: string;
|
|
|
84
96
|
/>
|
|
85
97
|
<IconButton icon={RotateCcw} size="sm" label={t('image.reset')} disabled={view.scale === MIN_SCALE} onClick={() => setView(START)} />
|
|
86
98
|
</ButtonGroup>
|
|
99
|
+
{actions}
|
|
87
100
|
<CloseButton label={t('common.action.close')} kbd="esc" className="ml-1" dialog />
|
|
88
101
|
</div>
|
|
89
102
|
<div
|
|
@@ -130,16 +143,41 @@ function Lightbox({ src, alt, open, onOpenChange }: { src: string; alt: string;
|
|
|
130
143
|
);
|
|
131
144
|
}
|
|
132
145
|
|
|
146
|
+
export interface ImageThumbProps {
|
|
147
|
+
source: ResourceUrl;
|
|
148
|
+
alt: string;
|
|
149
|
+
className?: string;
|
|
150
|
+
/* Width over height, known before the bytes: the thumbnail then fills its container's width at that ratio and holds its place while they load. */
|
|
151
|
+
aspect?: number;
|
|
152
|
+
/* Drawn in the lightbox's header before Close, such as icon buttons that act on the image. */
|
|
153
|
+
actions?: ReactNode;
|
|
154
|
+
/* Whether the lightbox is open, for a caller that opens it from a control of its own; left out, the thumbnail keeps it. */
|
|
155
|
+
open?: boolean;
|
|
156
|
+
onOpenChange?(open: boolean): void;
|
|
157
|
+
}
|
|
158
|
+
|
|
133
159
|
/*
|
|
134
160
|
* A thumbnail that opens the same image large. The button is the picture, so there is nothing to aim at.
|
|
135
161
|
* The image is bytes where the host keeps them; until they are here a quiet square stands in for it.
|
|
136
162
|
*/
|
|
137
|
-
export function ImageThumb({ source, alt, className
|
|
138
|
-
const [
|
|
163
|
+
export function ImageThumb({ source, alt, className, aspect, actions, open: openProp, onOpenChange }: ImageThumbProps) {
|
|
164
|
+
const [ownOpen, setOwnOpen] = useState(false);
|
|
165
|
+
const open = openProp ?? ownOpen;
|
|
166
|
+
const setOpen = (next: boolean): void => {
|
|
167
|
+
setOwnOpen(next);
|
|
168
|
+
onOpenChange?.(next);
|
|
169
|
+
};
|
|
139
170
|
const { url: src, failure } = source;
|
|
171
|
+
const ratio = aspect === undefined ? undefined : { aspectRatio: aspect };
|
|
140
172
|
if (src === null) {
|
|
141
173
|
return (
|
|
142
|
-
<span
|
|
174
|
+
<span
|
|
175
|
+
className={clsx(
|
|
176
|
+
'grid place-items-center rounded-lg border border-border bg-surface-sunken text-text-faint',
|
|
177
|
+
aspect === undefined ? 'size-16' : 'w-full'
|
|
178
|
+
)}
|
|
179
|
+
style={ratio}
|
|
180
|
+
>
|
|
143
181
|
{failure !== null && (
|
|
144
182
|
<Tooltip label={failure}>
|
|
145
183
|
<Icon icon={ImageOff} size={14} />
|
|
@@ -150,10 +188,15 @@ export function ImageThumb({ source, alt, className }: { source: ResourceUrl; al
|
|
|
150
188
|
}
|
|
151
189
|
return (
|
|
152
190
|
<>
|
|
153
|
-
<button
|
|
154
|
-
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
className={clsx('block overflow-hidden rounded-lg border border-border', aspect !== undefined && 'w-full')}
|
|
194
|
+
style={ratio}
|
|
195
|
+
onClick={() => setOpen(true)}
|
|
196
|
+
>
|
|
197
|
+
<img src={src} alt={alt} className={clsx(aspect !== undefined && 'size-full object-contain', className)} />
|
|
155
198
|
</button>
|
|
156
|
-
<Lightbox src={src} alt={alt} open={open} onOpenChange={setOpen} />
|
|
199
|
+
<Lightbox src={src} alt={alt} open={open} onOpenChange={setOpen} actions={actions} />
|
|
157
200
|
</>
|
|
158
201
|
);
|
|
159
202
|
}
|
package/src/chat/ui/Markdown.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { memo, useMemo, type ReactNode } from 'react';
|
|
1
|
+
import { createContext, memo, useContext, useMemo, type ReactNode } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { Zap } from 'lucide-react';
|
|
4
|
-
import ReactMarkdown, { type Components, type Options } from 'react-markdown';
|
|
4
|
+
import ReactMarkdown, { type Components, type ExtraProps, type Options } from 'react-markdown';
|
|
5
5
|
import remarkBreaks from 'remark-breaks';
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
7
7
|
import { CHIP_IN_MESSAGE, MENTION_TONE, SKILL_TONE } from './chips';
|
|
@@ -11,10 +11,55 @@ import { splitMarkdownBlocks } from './markdown-blocks';
|
|
|
11
11
|
import { rehypeChips, type ChipOptions } from './rehype-chips';
|
|
12
12
|
import { rehypeFadeWords } from './rehype-fade';
|
|
13
13
|
import { remarkHtmlAsText } from './remark-html-as-text';
|
|
14
|
-
import type
|
|
15
|
-
import { openFileLink, useFileLinkCwd, useFileLinkTarget } from './file-links';
|
|
14
|
+
import { chatHost, type FileRef, type ShellCodeBlockContext } from '../../host';
|
|
15
|
+
import { openFileLink, useFileLinkCwd, useFileLinkScopeId, useFileLinkTarget } from './file-links';
|
|
16
16
|
import { FileIcon, Icon } from '@adecore/ui';
|
|
17
17
|
|
|
18
|
+
type ShellCodeBlockReply = Pick<ShellCodeBlockContext, 'scopeId' | 'chatId' | 'itemId'> & { streaming: boolean };
|
|
19
|
+
|
|
20
|
+
const ShellCodeBlockReplyContext = createContext<ShellCodeBlockReply | null>(null);
|
|
21
|
+
const BlockCodeContext = createContext(false);
|
|
22
|
+
|
|
23
|
+
interface MarkdownNode {
|
|
24
|
+
type: string;
|
|
25
|
+
children?: MarkdownNode[];
|
|
26
|
+
position?: { start: { offset?: number }; end: { offset?: number } };
|
|
27
|
+
data?: { hProperties?: Record<string, unknown> };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Only root children qualify; Markdown's parser decides whether a fence belongs to a list or quote. */
|
|
31
|
+
function remarkShellCodeBlocks() {
|
|
32
|
+
return (tree: MarkdownNode, file: { value: unknown }) => {
|
|
33
|
+
for (const node of tree.children ?? []) {
|
|
34
|
+
if (node.type !== 'code') {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const start = node.position?.start.offset;
|
|
38
|
+
const end = node.position?.end.offset;
|
|
39
|
+
if (start === undefined || end === undefined) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const raw = String(file.value).slice(start, end);
|
|
43
|
+
const opening = /^ {0,3}(`{3,}|~{3,})[ \t]*(sh|bash|zsh)[ \t]*\r?\n/.exec(raw);
|
|
44
|
+
if (opening === null) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const body = raw.slice(opening[0].length);
|
|
48
|
+
const closing = /(?:^|\r?\n) {0,3}(`{3,}|~{3,})[ \t]*$/.exec(body);
|
|
49
|
+
const closed = closing !== null && closing[1]![0] === opening[1]![0] && closing[1]!.length >= opening[1]!.length;
|
|
50
|
+
node.data = {
|
|
51
|
+
...node.data,
|
|
52
|
+
hProperties: {
|
|
53
|
+
...node.data?.hProperties,
|
|
54
|
+
'data-shell-language': opening[2],
|
|
55
|
+
'data-shell-code': closed ? body.slice(0, closing.index) : body,
|
|
56
|
+
'data-shell-closed': closed
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
18
63
|
function languageOf(className: string | undefined): string {
|
|
19
64
|
return /language-([\w-]+)/.exec(className ?? '')?.[1] ?? 'text';
|
|
20
65
|
}
|
|
@@ -29,6 +74,7 @@ const INLINE_CODE = 'rounded-sm bg-surface-sunken px-1 py-px font-mono text-code
|
|
|
29
74
|
*/
|
|
30
75
|
function FileLink({ target, className, children }: { target: FileRef; className?: string; children: ReactNode }) {
|
|
31
76
|
const cwd = useFileLinkCwd();
|
|
77
|
+
const scopeId = useFileLinkScopeId();
|
|
32
78
|
return (
|
|
33
79
|
// `select-text` because a thread is copied as often as it is clicked, and a button is not
|
|
34
80
|
// selectable on its own.
|
|
@@ -37,7 +83,12 @@ function FileLink({ target, className, children }: { target: FileRef; className?
|
|
|
37
83
|
className={clsx('cursor-pointer select-text', className)}
|
|
38
84
|
data-file-path={target.path}
|
|
39
85
|
data-file-line={target.line}
|
|
40
|
-
|
|
86
|
+
data-file-column={target.column}
|
|
87
|
+
data-file-end-line={target.endLine}
|
|
88
|
+
data-file-directory={target.directory}
|
|
89
|
+
data-file-cwd={cwd ?? ''}
|
|
90
|
+
data-file-scope-id={scopeId ?? ''}
|
|
91
|
+
onClick={() => openFileLink(cwd, target, scopeId)}
|
|
41
92
|
>
|
|
42
93
|
{children}
|
|
43
94
|
</button>
|
|
@@ -85,18 +136,20 @@ function MarkdownLink({ href, children }: { href?: string; children?: ReactNode
|
|
|
85
136
|
);
|
|
86
137
|
}
|
|
87
138
|
|
|
139
|
+
function MarkdownCode({ className, children, plain = false }: { className?: string; children?: ReactNode; plain?: boolean }) {
|
|
140
|
+
const block = useContext(BlockCodeContext);
|
|
141
|
+
const text = String(children ?? '');
|
|
142
|
+
if (block) {
|
|
143
|
+
return <CodeBlock code={text.replace(/\n$/, '')} lang={languageOf(className)} />;
|
|
144
|
+
}
|
|
145
|
+
return plain ? <code className={INLINE_CODE}>{text}</code> : <InlineCode text={text} />;
|
|
146
|
+
}
|
|
147
|
+
|
|
88
148
|
const components = {
|
|
89
149
|
pre({ children }: { children?: ReactNode }) {
|
|
90
|
-
return
|
|
91
|
-
},
|
|
92
|
-
code({ className, children }: { className?: string; children?: ReactNode }) {
|
|
93
|
-
const text = String(children ?? '');
|
|
94
|
-
// Fenced blocks end with a newline and carry a language; anything else is inline.
|
|
95
|
-
if (className?.startsWith('language-') || text.includes('\n')) {
|
|
96
|
-
return <CodeBlock code={text.replace(/\n$/, '')} lang={languageOf(className)} />;
|
|
97
|
-
}
|
|
98
|
-
return <InlineCode text={text} />;
|
|
150
|
+
return <BlockCodeContext.Provider value={true}>{children}</BlockCodeContext.Provider>;
|
|
99
151
|
},
|
|
152
|
+
code: MarkdownCode,
|
|
100
153
|
a({ href, children }: { href?: string; children?: ReactNode }) {
|
|
101
154
|
return <MarkdownLink href={href}>{children}</MarkdownLink>;
|
|
102
155
|
},
|
|
@@ -114,11 +167,11 @@ const components = {
|
|
|
114
167
|
const plainComponents = {
|
|
115
168
|
...components,
|
|
116
169
|
code({ className, children }: { className?: string; children?: ReactNode }) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
170
|
+
return (
|
|
171
|
+
<MarkdownCode className={className} plain>
|
|
172
|
+
{children}
|
|
173
|
+
</MarkdownCode>
|
|
174
|
+
);
|
|
122
175
|
},
|
|
123
176
|
a({ href, children }: { href?: string; children?: ReactNode }) {
|
|
124
177
|
return (
|
|
@@ -142,6 +195,7 @@ function replyHeading(level: 1 | 2 | 3 | 4 | 5 | 6) {
|
|
|
142
195
|
|
|
143
196
|
const replyComponents = {
|
|
144
197
|
...components,
|
|
198
|
+
code: ReplyCode,
|
|
145
199
|
h1: replyHeading(1),
|
|
146
200
|
h2: replyHeading(2),
|
|
147
201
|
h3: replyHeading(3),
|
|
@@ -150,6 +204,36 @@ const replyComponents = {
|
|
|
150
204
|
h6: replyHeading(6)
|
|
151
205
|
};
|
|
152
206
|
|
|
207
|
+
function ReplyCode({ className, children, node }: { className?: string; children?: ReactNode } & ExtraProps) {
|
|
208
|
+
const reply = useContext(ShellCodeBlockReplyContext);
|
|
209
|
+
const block = useContext(BlockCodeContext);
|
|
210
|
+
const properties = node?.properties;
|
|
211
|
+
const language = properties?.['data-shell-language'];
|
|
212
|
+
const code = properties?.['data-shell-code'];
|
|
213
|
+
const context: ShellCodeBlockContext | null =
|
|
214
|
+
reply !== null && (language === 'sh' || language === 'bash' || language === 'zsh') && typeof code === 'string'
|
|
215
|
+
? {
|
|
216
|
+
scopeId: reply.scopeId,
|
|
217
|
+
chatId: reply.chatId,
|
|
218
|
+
itemId: reply.itemId,
|
|
219
|
+
language,
|
|
220
|
+
code,
|
|
221
|
+
complete: !reply.streaming && properties?.['data-shell-closed'] === true
|
|
222
|
+
}
|
|
223
|
+
: null;
|
|
224
|
+
const text = String(children ?? '');
|
|
225
|
+
if (block) {
|
|
226
|
+
return (
|
|
227
|
+
<CodeBlock
|
|
228
|
+
code={text.replace(/\n$/, '')}
|
|
229
|
+
lang={languageOf(className)}
|
|
230
|
+
actions={context !== null ? chatHost().renderShellCodeBlock?.(context) : undefined}
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return <InlineCode text={text} />;
|
|
235
|
+
}
|
|
236
|
+
|
|
153
237
|
/* A picked file or skill in a sent message. The glyph stands in for the sigil the text still carries. */
|
|
154
238
|
function Chip({ kind, value }: { kind: string; value: string }) {
|
|
155
239
|
if (kind === 'skill') {
|
|
@@ -162,7 +246,7 @@ function Chip({ kind, value }: { kind: string; value: string }) {
|
|
|
162
246
|
}
|
|
163
247
|
return (
|
|
164
248
|
// The path is what the thread's menu opens in the preview from here.
|
|
165
|
-
<span className={clsx(CHIP_IN_MESSAGE, MENTION_TONE)} data-file-path={value}>
|
|
249
|
+
<span className={clsx(CHIP_IN_MESSAGE, MENTION_TONE)} data-file-path={value} data-file-directory={/[\\/]$/.test(value)}>
|
|
166
250
|
<FileIcon path={value} size={14} />
|
|
167
251
|
<span className="truncate">{value}</span>
|
|
168
252
|
</span>
|
|
@@ -181,6 +265,7 @@ const messageComponents = {
|
|
|
181
265
|
|
|
182
266
|
// Module constants, so a render never hands react-markdown a fresh array and makes it parse again.
|
|
183
267
|
const PLUGINS = [remarkGfm];
|
|
268
|
+
const REPLY_PLUGINS = [remarkGfm, remarkShellCodeBlocks];
|
|
184
269
|
const PLUGINS_WITH_BREAKS = [remarkGfm, remarkBreaks];
|
|
185
270
|
const MESSAGE_PLUGINS = [remarkGfm, remarkHtmlAsText, remarkBreaks];
|
|
186
271
|
const FADE_PLUGINS = [rehypeFadeWords];
|
|
@@ -188,14 +273,16 @@ const NO_PLUGINS: typeof FADE_PLUGINS = [];
|
|
|
188
273
|
|
|
189
274
|
/* One block of a reply. It renders no element of its own, so prose still sees the paragraphs as
|
|
190
275
|
direct children and keeps its first and last margins. */
|
|
191
|
-
const ReplyBlock = memo(function ReplyBlock({ text, fade, open }: { text: string; fade: boolean; open: boolean }) {
|
|
276
|
+
const ReplyBlock = memo(function ReplyBlock({ text, fade, open, reply }: { text: string; fade: boolean; open: boolean; reply?: ShellCodeBlockReply }) {
|
|
192
277
|
return (
|
|
193
278
|
// A context rather than a second set of components. A component that changed would mount the
|
|
194
279
|
// code block again the moment its fence closes, and it would lose the lines it already has.
|
|
195
280
|
<CodeStreamingContext.Provider value={open}>
|
|
196
|
-
<
|
|
197
|
-
{
|
|
198
|
-
|
|
281
|
+
<ShellCodeBlockReplyContext.Provider value={reply ?? null}>
|
|
282
|
+
<ReactMarkdown remarkPlugins={REPLY_PLUGINS} rehypePlugins={fade ? FADE_PLUGINS : NO_PLUGINS} components={replyComponents}>
|
|
283
|
+
{text}
|
|
284
|
+
</ReactMarkdown>
|
|
285
|
+
</ShellCodeBlockReplyContext.Provider>
|
|
199
286
|
</CodeStreamingContext.Provider>
|
|
200
287
|
);
|
|
201
288
|
});
|
|
@@ -206,13 +293,23 @@ const ReplyBlock = memo(function ReplyBlock({ text, fade, open }: { text: string
|
|
|
206
293
|
* line at a time as it grows. `arriving` fades in each block as it is added instead, for a reply
|
|
207
294
|
* that is shown a block at a time.
|
|
208
295
|
*/
|
|
209
|
-
export const ReplyMarkdown = memo(function ReplyMarkdown({
|
|
296
|
+
export const ReplyMarkdown = memo(function ReplyMarkdown({
|
|
297
|
+
text,
|
|
298
|
+
streaming,
|
|
299
|
+
arriving = false,
|
|
300
|
+
reply
|
|
301
|
+
}: {
|
|
302
|
+
text: string;
|
|
303
|
+
streaming: boolean;
|
|
304
|
+
arriving?: boolean;
|
|
305
|
+
reply?: ShellCodeBlockReply;
|
|
306
|
+
}) {
|
|
210
307
|
const blocks = useMemo(() => splitMarkdownBlocks(text), [text]);
|
|
211
308
|
return (
|
|
212
309
|
<div className="chat-markdown prose prose-sm max-w-none text-sm" data-arriving={arriving || undefined}>
|
|
213
310
|
{blocks.map((block, index) => (
|
|
214
311
|
// Blocks only ever grow at the end, so the place of a block is a stable key.
|
|
215
|
-
<ReplyBlock key={index} text={block.text} fade={streaming} open={streaming && block.openFence} />
|
|
312
|
+
<ReplyBlock key={index} text={block.text} fade={streaming} open={streaming && block.openFence} reply={reply} />
|
|
216
313
|
))}
|
|
217
314
|
</div>
|
|
218
315
|
);
|
|
@@ -9,19 +9,18 @@ import { markdownOf, messageTextOf } from '../logic/timeline-copy';
|
|
|
9
9
|
import { chatHost } from '../../host';
|
|
10
10
|
import { useChatScope } from '../../scope';
|
|
11
11
|
import { useChatRow, useChats } from '../../state/chats';
|
|
12
|
-
import { ButtonGroup, copyText, IconButton } from '@adecore/ui';
|
|
12
|
+
import { ButtonGroup, copyText, IconButton, Tooltip } from '@adecore/ui';
|
|
13
|
+
import { formatClock, formatDateTime, useFormatLocale } from '@adecore/ui/format';
|
|
13
14
|
|
|
14
15
|
const COPIED_MS = 1500;
|
|
16
|
+
const TIMESTAMP_FORMAT: Intl.DateTimeFormatOptions = { dateStyle: 'full', timeStyle: 'medium' };
|
|
15
17
|
|
|
16
18
|
type MessageRow = Extract<TimelineRow, { kind: 'user' | 'assistant' }>;
|
|
17
19
|
|
|
18
|
-
/*
|
|
19
|
-
* Bookmark, fork and copy under a message, shown while the pointer or the focus is on a
|
|
20
|
-
* `group/message`. The row always takes its height, so the virtualizer never measures it again on a
|
|
21
|
-
* hover. Like the strip's card, it leaves out an action the message cannot take right now.
|
|
22
|
-
*/
|
|
20
|
+
/* Hidden actions keep their height, so hovering never forces the virtualizer to remeasure. */
|
|
23
21
|
export function MessageActions({ chatId, row }: { chatId: string; row: MessageRow }) {
|
|
24
22
|
const { t } = useTranslation('agent-chat');
|
|
23
|
+
useFormatLocale();
|
|
25
24
|
const scope = useChatScope();
|
|
26
25
|
const { fork } = chatHost();
|
|
27
26
|
const turnId = row.item.turnId;
|
|
@@ -61,8 +60,8 @@ export function MessageActions({ chatId, row }: { chatId: string; row: MessageRo
|
|
|
61
60
|
role="toolbar"
|
|
62
61
|
aria-label={t('timeline.actions.label')}
|
|
63
62
|
className={clsx(
|
|
64
|
-
'flex h-7 items-center opacity-0 transition-opacity group-has-focus-visible/message:opacity-100 group-hover/message:opacity-100',
|
|
65
|
-
row.kind === 'user'
|
|
63
|
+
'flex h-7 items-center gap-2 opacity-0 transition-opacity group-has-focus-visible/message:opacity-100 group-hover/message:opacity-100',
|
|
64
|
+
row.kind === 'user' && 'mt-1.5 justify-end'
|
|
66
65
|
)}
|
|
67
66
|
>
|
|
68
67
|
{!streaming && (
|
|
@@ -83,6 +82,14 @@ export function MessageActions({ chatId, row }: { chatId: string; row: MessageRo
|
|
|
83
82
|
)}
|
|
84
83
|
</ButtonGroup>
|
|
85
84
|
)}
|
|
85
|
+
<Tooltip label={formatDateTime(row.item.createdAt, TIMESTAMP_FORMAT)}>
|
|
86
|
+
<time
|
|
87
|
+
dateTime={new Date(row.item.createdAt).toISOString()}
|
|
88
|
+
className={clsx('shrink-0 text-xs text-text-faint tabular-nums select-none', row.kind === 'assistant' && 'order-first')}
|
|
89
|
+
>
|
|
90
|
+
{formatClock(row.item.createdAt)}
|
|
91
|
+
</time>
|
|
92
|
+
</Tooltip>
|
|
86
93
|
</div>
|
|
87
94
|
);
|
|
88
95
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import { Bot, LoaderCircle } from 'lucide-react';
|
|
4
4
|
import { deriveTimelineRows } from '../logic/timeline';
|
|
@@ -9,6 +9,8 @@ import { EMPTY_TARGET, readTimelineTarget, type TimelineTarget } from '../logic/
|
|
|
9
9
|
import { openBelow, useSubagentTrail, type SubagentStep } from '../subagent-view';
|
|
10
10
|
import { Row } from './rows/Rows';
|
|
11
11
|
import { ReplyContext } from './reply-context';
|
|
12
|
+
import { PromptComposer } from './PromptComposer';
|
|
13
|
+
import { useProviders } from '../../state/providers';
|
|
12
14
|
import { SubagentInfo } from './SubagentInfo';
|
|
13
15
|
import { TimelineMenuPopup } from './TimelineMenu';
|
|
14
16
|
import { FOLLOW_THRESHOLD_PX, rowRhythm } from './rows/row-rhythm';
|
|
@@ -27,8 +29,14 @@ const NO_TURNS = new Set<string>();
|
|
|
27
29
|
export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUseId: string }) {
|
|
28
30
|
const { t } = useTranslation('agent-chat');
|
|
29
31
|
const scope = useChatScope();
|
|
32
|
+
const connected = useSyncExternalStore(
|
|
33
|
+
useCallback((changed) => scope.transport.subscribeStatus(changed), [scope]),
|
|
34
|
+
useCallback(() => scope.transport.status === 'open', [scope]),
|
|
35
|
+
() => false
|
|
36
|
+
);
|
|
30
37
|
const { trail, show } = useSubagentTrail(chatId);
|
|
31
38
|
const [state, setState] = useState<SubagentConversationState>(INITIAL_CONVERSATION);
|
|
39
|
+
const denyReason = useProviders((row) => row.providers.find((provider) => provider.kind === state.context?.provider)?.capabilities.denyReason === true);
|
|
32
40
|
const controller = useRef<SubagentConversation | null>(null);
|
|
33
41
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
34
42
|
const threadRef = useRef<HTMLDivElement>(null);
|
|
@@ -117,6 +125,8 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
|
|
|
117
125
|
<ReplyContext.Provider value={state.context ?? { provider: state.source === 'codex-thread' ? 'codex' : 'claude' }}>
|
|
118
126
|
<div
|
|
119
127
|
ref={scrollRef}
|
|
128
|
+
data-file-cwd={state.context?.cwd ?? cwd ?? ''}
|
|
129
|
+
data-file-scope-id={scope.id}
|
|
120
130
|
className="chat-thread relative min-h-0 grow overflow-auto px-4 pt-4 pb-3"
|
|
121
131
|
// The thread keeps its reader in place itself when a page goes in above.
|
|
122
132
|
style={{ overflowAnchor: 'none' }}
|
|
@@ -165,6 +175,21 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
|
|
|
165
175
|
</ReplyContext.Provider>
|
|
166
176
|
</FileLinkContext.Provider>
|
|
167
177
|
)}
|
|
178
|
+
{state.context?.chatId !== undefined && state.pending.length > 0 && (
|
|
179
|
+
<div className="chat-column-content shrink-0">
|
|
180
|
+
<PromptComposer
|
|
181
|
+
key={state.context.chatId}
|
|
182
|
+
chatId={state.context.chatId}
|
|
183
|
+
pending={state.pending}
|
|
184
|
+
focused={false}
|
|
185
|
+
disabled={!connected}
|
|
186
|
+
hasDraft={false}
|
|
187
|
+
denyReason={denyReason}
|
|
188
|
+
>
|
|
189
|
+
{null}
|
|
190
|
+
</PromptComposer>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
168
193
|
</div>
|
|
169
194
|
);
|
|
170
195
|
}
|