@adecore/agents-react 0.20.0 → 0.20.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.
Files changed (38) hide show
  1. package/dist/chat/chat-client.d.ts +1 -0
  2. package/dist/chat/logic/timeline-target.d.ts +6 -0
  3. package/dist/chat/logic/timeline-target.js +23 -3
  4. package/dist/chat/logic/timeline.js +9 -7
  5. package/dist/chat/ui/CodeBlock.d.ts +3 -1
  6. package/dist/chat/ui/CodeBlock.js +38 -7
  7. package/dist/chat/ui/Composer.js +13 -3
  8. package/dist/chat/ui/Markdown.d.ts +7 -1
  9. package/dist/chat/ui/Markdown.js +80 -23
  10. package/dist/chat/ui/MessageActions.js +6 -7
  11. package/dist/chat/ui/SubagentTimeline.js +1 -1
  12. package/dist/chat/ui/Timeline.js +1 -1
  13. package/dist/chat/ui/TimelineMenu.js +7 -3
  14. package/dist/chat/ui/file-links.d.ts +2 -1
  15. package/dist/chat/ui/file-links.js +8 -3
  16. package/dist/chat/ui/rows/MessageRows.js +6 -3
  17. package/dist/host.d.ts +14 -2
  18. package/dist/host.js +1 -0
  19. package/dist/locales/en/agent-chat.json +7 -1
  20. package/dist/locales/nl/agent-chat.json +7 -1
  21. package/dist/theme.css +19 -2
  22. package/package.json +3 -3
  23. package/src/chat/chat-client.ts +1 -0
  24. package/src/chat/logic/timeline-target.ts +28 -3
  25. package/src/chat/logic/timeline.ts +9 -7
  26. package/src/chat/ui/CodeBlock.tsx +70 -21
  27. package/src/chat/ui/Composer.tsx +22 -5
  28. package/src/chat/ui/Markdown.tsx +123 -26
  29. package/src/chat/ui/MessageActions.tsx +15 -8
  30. package/src/chat/ui/SubagentTimeline.tsx +2 -0
  31. package/src/chat/ui/Timeline.tsx +1 -1
  32. package/src/chat/ui/TimelineMenu.tsx +6 -3
  33. package/src/chat/ui/file-links.ts +9 -3
  34. package/src/chat/ui/rows/MessageRows.tsx +9 -3
  35. package/src/host.ts +19 -2
  36. package/src/locales/en/agent-chat.json +7 -1
  37. package/src/locales/nl/agent-chat.json +7 -1
  38. package/src/theme.css +19 -2
@@ -10,6 +10,7 @@ interface ChatOpenOptions {
10
10
  runtimeMode?: RuntimeMode;
11
11
  }
12
12
  export interface ChatSendExtras {
13
+ delivery?: 'steer' | 'queue';
13
14
  mentions?: string[];
14
15
  skills?: string[];
15
16
  chats?: string[];
@@ -1,11 +1,17 @@
1
1
  import type { ChatItem } from '@adecore/agent-contracts';
2
2
  import type { TimelineRow } from './timeline.js';
3
+ import type { FileRef } from '../../host.js';
3
4
  export interface TimelineTarget {
4
5
  selection: string;
5
6
  row: TimelineRow | null;
6
7
  code: string | null;
7
8
  path: string | null;
8
9
  line: number | null;
10
+ file?: {
11
+ ref: FileRef;
12
+ cwd: string | null;
13
+ scopeId: string | null;
14
+ } | null;
9
15
  }
10
16
  export declare const EMPTY_TARGET: TimelineTarget;
11
17
  export declare function withCurrentText(rows: TimelineRow[], items: Record<string, ChatItem> | undefined): TimelineRow[];
@@ -23,12 +23,32 @@ export function withCurrentText(rows, items) {
23
23
  export function readTimelineTarget(element, scroller, rows) {
24
24
  const id = element.closest('[data-item-id]')?.dataset.itemId;
25
25
  const file = element.closest('[data-file-path]');
26
- const line = Number(file?.dataset.fileLine);
26
+ const line = positiveInteger(file?.dataset.fileLine);
27
+ const column = positiveInteger(file?.dataset.fileColumn);
28
+ const endLine = positiveInteger(file?.dataset.fileEndLine);
29
+ const path = file?.dataset.filePath ?? null;
27
30
  return {
28
31
  selection: selectionWithin(scroller),
29
32
  row: rows.find((row) => row.id === id) ?? null,
30
33
  code: element.closest('pre')?.textContent ?? null,
31
- path: file?.dataset.filePath ?? null,
32
- line: Number.isFinite(line) && line > 0 ? line : null
34
+ path,
35
+ line,
36
+ file: path === null
37
+ ? null
38
+ : {
39
+ ref: {
40
+ path,
41
+ ...(line === null ? {} : { line }),
42
+ ...(column === null ? {} : { column }),
43
+ ...(line === null || endLine === null || endLine < line ? {} : { endLine }),
44
+ directory: file?.dataset.fileDirectory === 'true'
45
+ },
46
+ cwd: file?.closest('[data-file-cwd]')?.dataset.fileCwd || null,
47
+ scopeId: file?.closest('[data-file-scope-id]')?.dataset.fileScopeId || null
48
+ }
33
49
  };
34
50
  }
51
+ function positiveInteger(value) {
52
+ const number = Number(value);
53
+ return Number.isSafeInteger(number) && number > 0 ? number : null;
54
+ }
@@ -289,23 +289,25 @@ export function deriveTimelineRows(items, options) {
289
289
  }
290
290
  const turnId = chunk.turnId;
291
291
  const turn = chunk.items.find((item) => item.kind === 'turn');
292
- const user = chunk.items.filter((item) => item.kind === 'user');
293
- const rest = chunk.items.filter((item) => item.kind !== 'user' && item.kind !== 'turn');
292
+ const conversation = chunk.items.filter((item) => item.kind !== 'turn');
294
293
  if (turn?.origin === 'agent') {
295
294
  // The agent started this one itself, so there is no message of the person to show above it.
296
295
  rows.push({ kind: 'turn-start', id: `start-${turnId}`, turn, label: agentTurnLabel(turn) });
297
296
  }
298
- rows.push(...rowsForItems(user, options, children));
299
297
  const active = turnId === options.activeTurnId || turn?.state === 'running';
300
- const work = rowsForItems(rest, options, children);
301
298
  if (!turn || active) {
302
- rows.push(...work);
299
+ rows.push(...rowsForItems(conversation, options, children));
303
300
  if (active) {
304
301
  rows.push({ kind: 'working', id: `working-${turnId}`, startedAt: turn?.createdAt ?? Date.now() });
305
302
  }
306
303
  continue;
307
304
  }
308
- // The closing answer stays visible; everything before it folds behind the label.
305
+ // Steering splits the conversation inside a turn. Earlier responses keep their place above it.
306
+ const lastUser = conversation.findLastIndex((item) => item.kind === 'user');
307
+ rows.push(...rowsForItems(conversation.slice(0, lastUser + 1), options, children));
308
+ const rest = conversation.slice(lastUser + 1);
309
+ const work = rowsForItems(rest, options, children);
310
+ // The closing answer stays visible; the work after the last input folds behind the label.
309
311
  const finalAssistant = lastAssistantRow(work);
310
312
  const before = finalAssistant ? work.slice(0, work.indexOf(finalAssistant)) : work;
311
313
  // A workflow runs on past the turn that launched it, so its row stays in sight until it ends.
@@ -328,7 +330,7 @@ export function deriveTimelineRows(items, options) {
328
330
  }
329
331
  }
330
332
  rows.push(...standing);
331
- const changed = changedFilesRow(turn, rest);
333
+ const changed = changedFilesRow(turn, conversation);
332
334
  if (changed) {
333
335
  rows.push(changed);
334
336
  }
@@ -1,4 +1,6 @@
1
- export declare function CodeBlock({ code, lang }: {
1
+ import { type ReactNode } from 'react';
2
+ export declare function CodeBlock({ code, lang, actions }: {
2
3
  code: string;
3
4
  lang: string;
5
+ actions?: ReactNode;
4
6
  }): import("react").JSX.Element;
@@ -1,12 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Fragment, memo, useContext, useEffect, useMemo, useState } from 'react';
2
+ import { Fragment, memo, useContext, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import clsx from 'clsx';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Check, Copy } from 'lucide-react';
6
+ import { ButtonGroup, IconButton } from '@adecore/ui';
4
7
  import { IncrementalLines } from './code-lines.js';
5
8
  import { CodeStreamingContext } from './code-streaming.js';
6
9
  import { WHOLE_FADE_CLASS } from './rehype-fade.js';
7
10
  import { shikiThemeOf, useCodeTheme } from './code-theme.js';
8
11
  // A plain language needs no grammar; shiki draws it in the theme's own foreground.
9
12
  const PLAIN = 'text';
13
+ const COPIED_MS = 1500;
10
14
  let highlighter = null;
11
15
  let highlighterLoad = null;
12
16
  const loadedLanguages = new Set([PLAIN]);
@@ -102,9 +106,12 @@ const CodeLine = memo(function CodeLine({ tokens }) {
102
106
  * fence that is still open is tokenized as it grows, and the same component carries on once it
103
107
  * closes, so closing it does not draw it again.
104
108
  */
105
- export function CodeBlock({ code, lang }) {
109
+ export function CodeBlock({ code, lang, actions }) {
110
+ const { t } = useTranslation('agent-chat');
106
111
  const theme = useCodeTheme();
107
112
  const streaming = useContext(CodeStreamingContext);
113
+ const [copyState, setCopyState] = useState('idle');
114
+ const copying = useRef(false);
108
115
  // What loaded last, so a block whose theme changes draws again once the new one is in.
109
116
  const [loaded, setLoaded] = useState(null);
110
117
  const tokenizer = tokenizerFor(lang, theme);
@@ -112,6 +119,31 @@ export function CodeBlock({ code, lang }) {
112
119
  const [waited] = useState(tokenizer === null);
113
120
  const lines = useMemo(() => (tokenizer ? new IncrementalLines(tokenizer.tokenize) : null), [tokenizer]);
114
121
  const tokens = useMemo(() => lines?.update(code, !streaming) ?? null, [lines, code, streaming]);
122
+ useEffect(() => {
123
+ if (copyState !== 'copied') {
124
+ return;
125
+ }
126
+ const timer = setTimeout(() => setCopyState('idle'), COPIED_MS);
127
+ return () => clearTimeout(timer);
128
+ }, [copyState]);
129
+ async function copy() {
130
+ if (copying.current) {
131
+ return;
132
+ }
133
+ copying.current = true;
134
+ setCopyState('copying');
135
+ try {
136
+ // The best-effort clipboard helper cannot distinguish a refusal from success.
137
+ await navigator.clipboard.writeText(code);
138
+ setCopyState('copied');
139
+ }
140
+ catch {
141
+ setCopyState('failed');
142
+ }
143
+ finally {
144
+ copying.current = false;
145
+ }
146
+ }
115
147
  useEffect(() => {
116
148
  if (tokenizer !== null) {
117
149
  return;
@@ -124,9 +156,8 @@ export function CodeBlock({ code, lang }) {
124
156
  cancelled = true;
125
157
  };
126
158
  }, [lang, theme, tokenizer]);
127
- if (tokenizer === null || tokens === null) {
128
- const failed = loaded === 'failed';
129
- return (_jsx("div", { className: clsx('chat-code', !failed && 'invisible'), "aria-hidden": failed ? undefined : true, children: _jsx("pre", { children: _jsx("code", { children: code }) }) }));
130
- }
131
- return (_jsx("div", { className: clsx('chat-code', waited && WHOLE_FADE_CLASS), children: _jsx("pre", { style: { color: tokenizer.fg }, children: _jsx("code", { children: tokens.map((line, index) => (_jsxs(Fragment, { children: [_jsx(CodeLine, { tokens: line }), index < tokens.length - 1 && '\n'] }, index))) }) }) }));
159
+ const pending = (tokenizer === null || tokens === null) && loaded !== 'failed';
160
+ return (_jsxs("div", { className: "chat-code", "data-copy-state": copyState, children: [_jsx("pre", { className: clsx(pending && 'invisible', !pending && tokens !== null && waited && WHOLE_FADE_CLASS), "aria-hidden": pending || undefined, style: { color: tokenizer?.fg }, children: _jsx("code", { children: tokens === null
161
+ ? code
162
+ : tokens.map((line, index) => (_jsxs(Fragment, { children: [_jsx(CodeLine, { tokens: line }), index < tokens.length - 1 && '\n'] }, index))) }) }), _jsxs(ButtonGroup, { className: "chat-code-actions", role: "group", "aria-label": t('code.actions'), children: [_jsx(IconButton, { icon: copyState === 'copied' ? Check : Copy, size: "sm", label: copyState === 'copied' ? t('timeline.actions.copied') : t('timeline.menu.copyCode'), busy: copyState === 'copying', "aria-busy": copyState === 'copying', "aria-disabled": copyState === 'copying', onClick: () => void copy() }), actions] }), _jsx("span", { className: "sr-only", role: "status", children: copyState === 'copied' ? t('timeline.actions.copied') : '' }), copyState === 'failed' && (_jsx("p", { className: "chat-code-error", role: "alert", children: t('code.copyFailed') }))] }));
132
163
  }
@@ -95,6 +95,8 @@ function splitPath(path) {
95
95
  // Keep the editor mounted while a pending request takes over, so its selection and draft survive.
96
96
  export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabled, readOnly = false, providerFixed, onSend, onRetarget }) {
97
97
  const { t } = useTranslation('agent-chat');
98
+ const sendDelivery = chatHost().useSendDelivery();
99
+ const canSteer = info.provider === 'claude' || info.provider === 'codex';
98
100
  const [draft, setDraft] = useState(() => readDraft(chatId));
99
101
  const [historyIndex, setHistoryIndex] = useState(null);
100
102
  const [menuIndex, setMenuIndex] = useState(0);
@@ -474,7 +476,8 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
474
476
  const removeAttachment = (index) => {
475
477
  setDraft((current) => ({ ...current, attachments: current.attachments.filter((_, i) => i !== index) }));
476
478
  };
477
- const submit = () => {
479
+ const submit = (alternate = false) => {
480
+ const delivery = canSteer && (alternate ? sendDelivery === 'queue' : sendDelivery === 'steer') ? 'steer' : 'queue';
478
481
  const trimmed = text.trim();
479
482
  if (isEmptyDraft(draft) || !writable || guard.tooLong) {
480
483
  return;
@@ -502,6 +505,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
502
505
  return;
503
506
  }
504
507
  onSend(withQuote(quote, trimmed), {
508
+ delivery,
505
509
  mentions: presentMentions(trimmed, draft.mentions),
506
510
  skills: presentSkills(trimmed, draft.skills),
507
511
  chats: draft.chats,
@@ -739,7 +743,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
739
743
  view.dispatch({ changes: { from: line.from, to: line.to }, selection: { anchor: line.from }, userEvent: 'delete' });
740
744
  return true;
741
745
  }
742
- submit();
746
+ submit(e.altKey);
743
747
  return true;
744
748
  }
745
749
  const { from, to } = view.state.selection.main;
@@ -852,5 +856,11 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
852
856
  addFiles(Array.from(e.target.files ?? []));
853
857
  e.target.value = '';
854
858
  inputRef.current?.focus();
855
- } }), _jsx(RunSettings, { disabled: configurationPending || !writable, providers: pickable, provider: info.provider, selection: info.selection, model: model, runtimeMode: info.runtimeMode, effectiveRuntimeMode: info.effectiveRuntimeMode, permissionMode: info.permissionMode, open: modelPickerOpen, onOpenChange: setModelPickerOpen, onModel: chooseModel, onOption: (id, value) => configure({ selection: { ...info.selection, options: { ...info.selection.options, [id]: value } } }), onMode: (runtimeMode) => configure({ runtimeMode }), usage: info.usage, compactDisabled: busy || !writable, onCompact: compact, account: accountChoice === null ? null : { choice: accountChoice, started, onPick: chooseAccount } }), _jsx(StashPicker, { onRestore: restoreStashed }), Slot !== null && (_jsx(ErrorBoundary, { label: t('composer.slotFailed'), resetKeys: [chatId], compact: true, className: "relative rounded-lg", children: _jsx(Slot, { scopeId: scope.id, chatId: chatId, disabled: !writable, insert: insertIntoDraft }) })), _jsx("span", { className: "grow" }), _jsxs("div", { className: "flex h-9 shrink-0 items-center gap-0.5 rounded-full bg-surface-hover", children: [_jsx("div", { ref: setDictationToolbar, className: "flex items-center pl-1 empty:hidden [&_.icon-btn]:rounded-full" }), busy && (_jsx(Tooltip, { label: composerStopLabel(), name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-status-error text-accent-text", onClick: (event) => stop(event.shiftKey), children: _jsx(Icon, { icon: Square, size: 16 }) }) })), (!busy || !isEmptyDraft(draft)) && (_jsx(Tooltip, { label: busy ? t('composer.queueButton') : t('composer.send'), kbd: KEY_SHORTCUTS.modEnter, name: true, children: _jsx("button", { 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", disabled: isEmptyDraft(draft) || !writable || guard.tooLong, onClick: submit, children: _jsx(Icon, { icon: ArrowUp, size: 16 }) }) }))] })] })] }) }), _jsx(PromptDialog, { open: confirmClear, title: t('composer.clear.title'), description: t('composer.clear.description'), confirmLabel: t('composer.clear.confirm'), danger: true, fallbackMessage: t('composer.notice.clearFailed'), onConfirm: forceClearThread, onOpenChange: () => setConfirmClear(false) })] }));
859
+ } }), _jsx(RunSettings, { disabled: configurationPending || !writable, providers: pickable, provider: info.provider, selection: info.selection, model: model, runtimeMode: info.runtimeMode, effectiveRuntimeMode: info.effectiveRuntimeMode, permissionMode: info.permissionMode, open: modelPickerOpen, onOpenChange: setModelPickerOpen, onModel: chooseModel, onOption: (id, value) => configure({ selection: { ...info.selection, options: { ...info.selection.options, [id]: value } } }), onMode: (runtimeMode) => configure({ runtimeMode }), usage: info.usage, compactDisabled: busy || !writable, onCompact: compact, account: accountChoice === null ? null : { choice: accountChoice, started, onPick: chooseAccount } }), _jsx(StashPicker, { onRestore: restoreStashed }), Slot !== null && (_jsx(ErrorBoundary, { label: t('composer.slotFailed'), resetKeys: [chatId], compact: true, className: "relative rounded-lg", children: _jsx(Slot, { scopeId: scope.id, chatId: chatId, disabled: !writable, insert: insertIntoDraft }) })), _jsx("span", { className: "grow" }), _jsxs("div", { className: "flex h-9 shrink-0 items-center gap-0.5 rounded-full bg-surface-hover", children: [_jsx("div", { ref: setDictationToolbar, className: "flex items-center pl-1 empty:hidden [&_.icon-btn]:rounded-full" }), busy && (_jsx(Tooltip, { label: composerStopLabel(), name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-status-error text-accent-text", onClick: (event) => stop(event.shiftKey), children: _jsx(Icon, { icon: Square, size: 16 }) }) })), (!busy || !isEmptyDraft(draft)) && (_jsx(Tooltip, { label: busy
860
+ ? t(canSteer
861
+ ? sendDelivery === 'steer'
862
+ ? 'composer.steerButton'
863
+ : 'composer.queueWithSteerButton'
864
+ : 'composer.queueButton')
865
+ : t('composer.send'), kbd: KEY_SHORTCUTS.modEnter, name: true, children: _jsx("button", { 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", disabled: isEmptyDraft(draft) || !writable || guard.tooLong, onClick: (event) => submit(event.altKey), children: _jsx(Icon, { icon: ArrowUp, size: 16 }) }) }))] })] })] }) }), _jsx(PromptDialog, { open: confirmClear, title: t('composer.clear.title'), description: t('composer.clear.description'), confirmLabel: t('composer.clear.confirm'), danger: true, fallbackMessage: t('composer.notice.clearFailed'), onConfirm: forceClearThread, onOpenChange: () => setConfirmClear(false) })] }));
856
866
  }
@@ -1,8 +1,13 @@
1
1
  import { type Components, type Options } from 'react-markdown';
2
- export declare const ReplyMarkdown: import("react").MemoExoticComponent<({ text, streaming, arriving }: {
2
+ import { type ShellCodeBlockContext } from '../../host.js';
3
+ type ShellCodeBlockReply = Pick<ShellCodeBlockContext, 'scopeId' | 'chatId' | 'itemId'> & {
4
+ streaming: boolean;
5
+ };
6
+ export declare const ReplyMarkdown: import("react").MemoExoticComponent<({ text, streaming, arriving, reply }: {
3
7
  text: string;
4
8
  streaming: boolean;
5
9
  arriving?: boolean;
10
+ reply?: ShellCodeBlockReply;
6
11
  }) => import("react").JSX.Element>;
7
12
  export declare const MessageMarkdown: import("react").MemoExoticComponent<({ text, mentions, skills }: {
8
13
  text: string;
@@ -16,3 +21,4 @@ export declare const Markdown: import("react").MemoExoticComponent<({ text, brea
16
21
  rehypePlugins?: Options['rehypePlugins'];
17
22
  componentOverrides?: Components;
18
23
  }) => import("react").JSX.Element>;
24
+ export {};
@@ -1,5 +1,5 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { memo, useMemo } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, memo, useContext, useMemo } from 'react';
3
3
  import clsx from 'clsx';
4
4
  import { Zap } from 'lucide-react';
5
5
  import ReactMarkdown, {} from 'react-markdown';
@@ -12,8 +12,43 @@ import { splitMarkdownBlocks } from './markdown-blocks.js';
12
12
  import { rehypeChips } from './rehype-chips.js';
13
13
  import { rehypeFadeWords } from './rehype-fade.js';
14
14
  import { remarkHtmlAsText } from './remark-html-as-text.js';
15
- import { openFileLink, useFileLinkCwd, useFileLinkTarget } from './file-links.js';
15
+ import { chatHost } from '../../host.js';
16
+ import { openFileLink, useFileLinkCwd, useFileLinkScopeId, useFileLinkTarget } from './file-links.js';
16
17
  import { FileIcon, Icon } from '@adecore/ui';
18
+ const ShellCodeBlockReplyContext = createContext(null);
19
+ const BlockCodeContext = createContext(false);
20
+ /* Only root children qualify; Markdown's parser decides whether a fence belongs to a list or quote. */
21
+ function remarkShellCodeBlocks() {
22
+ return (tree, file) => {
23
+ for (const node of tree.children ?? []) {
24
+ if (node.type !== 'code') {
25
+ continue;
26
+ }
27
+ const start = node.position?.start.offset;
28
+ const end = node.position?.end.offset;
29
+ if (start === undefined || end === undefined) {
30
+ continue;
31
+ }
32
+ const raw = String(file.value).slice(start, end);
33
+ const opening = /^ {0,3}(`{3,}|~{3,})[ \t]*(sh|bash|zsh)[ \t]*\r?\n/.exec(raw);
34
+ if (opening === null) {
35
+ continue;
36
+ }
37
+ const body = raw.slice(opening[0].length);
38
+ const closing = /(?:^|\r?\n) {0,3}(`{3,}|~{3,})[ \t]*$/.exec(body);
39
+ const closed = closing !== null && closing[1][0] === opening[1][0] && closing[1].length >= opening[1].length;
40
+ node.data = {
41
+ ...node.data,
42
+ hProperties: {
43
+ ...node.data?.hProperties,
44
+ 'data-shell-language': opening[2],
45
+ 'data-shell-code': closed ? body.slice(0, closing.index) : body,
46
+ 'data-shell-closed': closed
47
+ }
48
+ };
49
+ }
50
+ };
51
+ }
17
52
  function languageOf(className) {
18
53
  return /language-([\w-]+)/.exec(className ?? '')?.[1] ?? 'text';
19
54
  }
@@ -26,7 +61,8 @@ const INLINE_CODE = 'rounded-sm bg-surface-sunken px-1 py-px font-mono text-code
26
61
  */
27
62
  function FileLink({ target, className, children }) {
28
63
  const cwd = useFileLinkCwd();
29
- return (_jsx("button", { type: "button", className: clsx('cursor-pointer select-text', className), "data-file-path": target.path, "data-file-line": target.line, onClick: () => openFileLink(cwd, target), children: children }));
64
+ const scopeId = useFileLinkScopeId();
65
+ return (_jsx("button", { type: "button", className: clsx('cursor-pointer select-text', className), "data-file-path": target.path, "data-file-line": target.line, "data-file-column": target.column, "data-file-end-line": target.endLine, "data-file-directory": target.directory, "data-file-cwd": cwd ?? '', "data-file-scope-id": scopeId ?? '', onClick: () => openFileLink(cwd, target, scopeId), children: children }));
30
66
  }
31
67
  /* Inline code, which is a file reference often enough that it is worth asking every time. */
32
68
  function InlineCode({ text }) {
@@ -55,18 +91,19 @@ function MarkdownLink({ href, children }) {
55
91
  }
56
92
  return (_jsx("a", { href: href, target: "_blank", rel: "noreferrer", children: children }));
57
93
  }
94
+ function MarkdownCode({ className, children, plain = false }) {
95
+ const block = useContext(BlockCodeContext);
96
+ const text = String(children ?? '');
97
+ if (block) {
98
+ return _jsx(CodeBlock, { code: text.replace(/\n$/, ''), lang: languageOf(className) });
99
+ }
100
+ return plain ? _jsx("code", { className: INLINE_CODE, children: text }) : _jsx(InlineCode, { text: text });
101
+ }
58
102
  const components = {
59
103
  pre({ children }) {
60
- return _jsx(_Fragment, { children: children });
61
- },
62
- code({ className, children }) {
63
- const text = String(children ?? '');
64
- // Fenced blocks end with a newline and carry a language; anything else is inline.
65
- if (className?.startsWith('language-') || text.includes('\n')) {
66
- return _jsx(CodeBlock, { code: text.replace(/\n$/, ''), lang: languageOf(className) });
67
- }
68
- return _jsx(InlineCode, { text: text });
104
+ return _jsx(BlockCodeContext.Provider, { value: true, children: children });
69
105
  },
106
+ code: MarkdownCode,
70
107
  a({ href, children }) {
71
108
  return _jsx(MarkdownLink, { href: href, children: children });
72
109
  },
@@ -79,11 +116,7 @@ const components = {
79
116
  const plainComponents = {
80
117
  ...components,
81
118
  code({ className, children }) {
82
- const text = String(children ?? '');
83
- if (className?.startsWith('language-') || text.includes('\n')) {
84
- return _jsx(CodeBlock, { code: text.replace(/\n$/, ''), lang: languageOf(className) });
85
- }
86
- return _jsx("code", { className: INLINE_CODE, children: text });
119
+ return (_jsx(MarkdownCode, { className: className, plain: true, children: children }));
87
120
  },
88
121
  a({ href, children }) {
89
122
  return (_jsx("a", { href: href, target: "_blank", rel: "noreferrer", children: children }));
@@ -100,6 +133,7 @@ function replyHeading(level) {
100
133
  }
101
134
  const replyComponents = {
102
135
  ...components,
136
+ code: ReplyCode,
103
137
  h1: replyHeading(1),
104
138
  h2: replyHeading(2),
105
139
  h3: replyHeading(3),
@@ -107,12 +141,34 @@ const replyComponents = {
107
141
  h5: replyHeading(5),
108
142
  h6: replyHeading(6)
109
143
  };
144
+ function ReplyCode({ className, children, node }) {
145
+ const reply = useContext(ShellCodeBlockReplyContext);
146
+ const block = useContext(BlockCodeContext);
147
+ const properties = node?.properties;
148
+ const language = properties?.['data-shell-language'];
149
+ const code = properties?.['data-shell-code'];
150
+ const context = reply !== null && (language === 'sh' || language === 'bash' || language === 'zsh') && typeof code === 'string'
151
+ ? {
152
+ scopeId: reply.scopeId,
153
+ chatId: reply.chatId,
154
+ itemId: reply.itemId,
155
+ language,
156
+ code,
157
+ complete: !reply.streaming && properties?.['data-shell-closed'] === true
158
+ }
159
+ : null;
160
+ const text = String(children ?? '');
161
+ if (block) {
162
+ return (_jsx(CodeBlock, { code: text.replace(/\n$/, ''), lang: languageOf(className), actions: context !== null ? chatHost().renderShellCodeBlock?.(context) : undefined }));
163
+ }
164
+ return _jsx(InlineCode, { text: text });
165
+ }
110
166
  /* A picked file or skill in a sent message. The glyph stands in for the sigil the text still carries. */
111
167
  function Chip({ kind, value }) {
112
168
  if (kind === 'skill') {
113
169
  return (_jsxs("span", { className: clsx(CHIP_IN_MESSAGE, SKILL_TONE), children: [_jsx(Icon, { icon: Zap, size: 14, className: "shrink-0 opacity-85" }), _jsx("span", { className: "truncate", children: value })] }));
114
170
  }
115
- return (_jsxs("span", { className: clsx(CHIP_IN_MESSAGE, MENTION_TONE), "data-file-path": value, children: [_jsx(FileIcon, { path: value, size: 14 }), _jsx("span", { className: "truncate", children: value })] }));
171
+ return (_jsxs("span", { className: clsx(CHIP_IN_MESSAGE, MENTION_TONE), "data-file-path": value, "data-file-directory": /[\\/]$/.test(value), children: [_jsx(FileIcon, { path: value, size: 14 }), _jsx("span", { className: "truncate", children: value })] }));
116
172
  }
117
173
  const messageComponents = {
118
174
  ...replyComponents,
@@ -125,14 +181,15 @@ const messageComponents = {
125
181
  };
126
182
  // Module constants, so a render never hands react-markdown a fresh array and makes it parse again.
127
183
  const PLUGINS = [remarkGfm];
184
+ const REPLY_PLUGINS = [remarkGfm, remarkShellCodeBlocks];
128
185
  const PLUGINS_WITH_BREAKS = [remarkGfm, remarkBreaks];
129
186
  const MESSAGE_PLUGINS = [remarkGfm, remarkHtmlAsText, remarkBreaks];
130
187
  const FADE_PLUGINS = [rehypeFadeWords];
131
188
  const NO_PLUGINS = [];
132
189
  /* One block of a reply. It renders no element of its own, so prose still sees the paragraphs as
133
190
  direct children and keeps its first and last margins. */
134
- const ReplyBlock = memo(function ReplyBlock({ text, fade, open }) {
135
- return (_jsx(CodeStreamingContext.Provider, { value: open, children: _jsx(ReactMarkdown, { remarkPlugins: PLUGINS, rehypePlugins: fade ? FADE_PLUGINS : NO_PLUGINS, components: replyComponents, children: text }) }));
191
+ const ReplyBlock = memo(function ReplyBlock({ text, fade, open, reply }) {
192
+ return (_jsx(CodeStreamingContext.Provider, { value: open, children: _jsx(ShellCodeBlockReplyContext.Provider, { value: reply ?? null, children: _jsx(ReactMarkdown, { remarkPlugins: REPLY_PLUGINS, rehypePlugins: fade ? FADE_PLUGINS : NO_PLUGINS, components: replyComponents, children: text }) }) }));
136
193
  });
137
194
  /*
138
195
  * A reply in a chat thread, parsed a block at a time so a delta only parses the block that grows.
@@ -140,9 +197,9 @@ const ReplyBlock = memo(function ReplyBlock({ text, fade, open }) {
140
197
  * line at a time as it grows. `arriving` fades in each block as it is added instead, for a reply
141
198
  * that is shown a block at a time.
142
199
  */
143
- export const ReplyMarkdown = memo(function ReplyMarkdown({ text, streaming, arriving = false }) {
200
+ export const ReplyMarkdown = memo(function ReplyMarkdown({ text, streaming, arriving = false, reply }) {
144
201
  const blocks = useMemo(() => splitMarkdownBlocks(text), [text]);
145
- return (_jsx("div", { className: "chat-markdown prose prose-sm max-w-none text-sm", "data-arriving": arriving || undefined, children: blocks.map((block, index) => (_jsx(ReplyBlock, { text: block.text, fade: streaming, open: streaming && block.openFence }, index))) }));
202
+ return (_jsx("div", { className: "chat-markdown prose prose-sm max-w-none text-sm", "data-arriving": arriving || undefined, children: blocks.map((block, index) => (_jsx(ReplyBlock, { text: block.text, fade: streaming, open: streaming && block.openFence, reply: reply }, index))) }));
146
203
  });
147
204
  /*
148
205
  * A message a person sent. Enter meant a line break, a tag typed without backticks stays the text
@@ -9,15 +9,14 @@ import { markdownOf, messageTextOf } from '../logic/timeline-copy.js';
9
9
  import { chatHost } from '../../host.js';
10
10
  import { useChatScope } from '../../scope.js';
11
11
  import { useChatRow, useChats } from '../../state/chats.js';
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
  const COPIED_MS = 1500;
14
- /*
15
- * Bookmark, fork and copy under a message, shown while the pointer or the focus is on a
16
- * `group/message`. The row always takes its height, so the virtualizer never measures it again on a
17
- * hover. Like the strip's card, it leaves out an action the message cannot take right now.
18
- */
15
+ const TIMESTAMP_FORMAT = { dateStyle: 'full', timeStyle: 'medium' };
16
+ /* Hidden actions keep their height, so hovering never forces the virtualizer to remeasure. */
19
17
  export function MessageActions({ chatId, row }) {
20
18
  const { t } = useTranslation('agent-chat');
19
+ useFormatLocale();
21
20
  const scope = useChatScope();
22
21
  const { fork } = chatHost();
23
22
  const turnId = row.item.turnId;
@@ -49,5 +48,5 @@ export function MessageActions({ chatId, row }) {
49
48
  setCopied(true);
50
49
  }
51
50
  };
52
- return (_jsx("div", { role: "toolbar", "aria-label": t('timeline.actions.label'), className: clsx('flex h-7 items-center opacity-0 transition-opacity group-has-focus-visible/message:opacity-100 group-hover/message:opacity-100', row.kind === 'user' ? '-mr-1.5 mt-1.5 justify-end' : '-ml-1.5'), children: !streaming && (_jsxs(ButtonGroup, { render: _jsx("span", {}), children: [bookmark === null && (_jsx(IconButton, { icon: Bookmark, size: "sm", label: t('bookmarks.add'), onClick: () => void placeBookmark(scope, chatId, row.id) })), canFork && turnId !== null && fork !== null && (_jsx(IconButton, { icon: GitFork, size: "sm", label: t('timeline.menu.forkFromHere'), onClick: () => fork(chatId, turnId) })), hasText && (_jsx(IconButton, { icon: copied ? Check : Copy, size: "sm", label: copied ? t('timeline.actions.copied') : t('timeline.menu.copyMessage'), onClick: (e) => copy(e.shiftKey) }))] })) }));
51
+ return (_jsxs("div", { role: "toolbar", "aria-label": t('timeline.actions.label'), className: clsx('flex h-7 items-center gap-2 opacity-0 transition-opacity group-has-focus-visible/message:opacity-100 group-hover/message:opacity-100', row.kind === 'user' && 'mt-1.5 justify-end'), children: [!streaming && (_jsxs(ButtonGroup, { render: _jsx("span", {}), children: [bookmark === null && (_jsx(IconButton, { icon: Bookmark, size: "sm", label: t('bookmarks.add'), onClick: () => void placeBookmark(scope, chatId, row.id) })), canFork && turnId !== null && fork !== null && (_jsx(IconButton, { icon: GitFork, size: "sm", label: t('timeline.menu.forkFromHere'), onClick: () => fork(chatId, turnId) })), hasText && (_jsx(IconButton, { icon: copied ? Check : Copy, size: "sm", label: copied ? t('timeline.actions.copied') : t('timeline.menu.copyMessage'), onClick: (e) => copy(e.shiftKey) }))] })), _jsx(Tooltip, { label: formatDateTime(row.item.createdAt, TIMESTAMP_FORMAT), children: _jsx("time", { dateTime: new Date(row.item.createdAt).toISOString(), className: clsx('shrink-0 text-xs text-text-faint tabular-nums select-none', row.kind === 'assistant' && 'order-first'), children: formatClock(row.item.createdAt) }) })] }));
53
52
  }
@@ -94,7 +94,7 @@ export function SubagentTimeline({ chatId, toolUseId }) {
94
94
  const openChild = (step) => {
95
95
  show(openBelow(trail, step));
96
96
  };
97
- return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx(SubagentInfo, { chatId: chatId, toolUseId: toolUseId }), state.status === 'loading' && _jsx("div", { className: "chat-column-content px-4 pt-4 text-xs text-text-faint", children: t('subagents.loading') }), state.status === 'failed' && (_jsx(EmptyState, { icon: Bot, children: state.unsupported ? t('subagents.unsupported') : (state.error ?? t('subagents.unreadable')) })), state.status === 'ready' && (_jsx(FileLinkContext.Provider, { value: state.context?.cwd ?? cwd, children: _jsx(ReplyContext.Provider, { value: state.context ?? { provider: state.source === 'codex-thread' ? 'codex' : 'claude' }, children: _jsx("div", { ref: scrollRef, className: "chat-thread relative min-h-0 grow overflow-auto px-4 pt-4 pb-3",
97
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx(SubagentInfo, { chatId: chatId, toolUseId: toolUseId }), state.status === 'loading' && _jsx("div", { className: "chat-column-content px-4 pt-4 text-xs text-text-faint", children: t('subagents.loading') }), state.status === 'failed' && (_jsx(EmptyState, { icon: Bot, children: state.unsupported ? t('subagents.unsupported') : (state.error ?? t('subagents.unreadable')) })), state.status === 'ready' && (_jsx(FileLinkContext.Provider, { value: state.context?.cwd ?? cwd, children: _jsx(ReplyContext.Provider, { value: state.context ?? { provider: state.source === 'codex-thread' ? 'codex' : 'claude' }, children: _jsx("div", { ref: scrollRef, "data-file-cwd": state.context?.cwd ?? cwd ?? '', "data-file-scope-id": scope.id, className: "chat-thread relative min-h-0 grow overflow-auto px-4 pt-4 pb-3",
98
98
  // The thread keeps its reader in place itself when a page goes in above.
99
99
  style: { overflowAnchor: 'none' }, onScroll: (event) => {
100
100
  const element = event.currentTarget;
@@ -395,7 +395,7 @@ export function Timeline({ chatId, composer, overlay }) {
395
395
  followRef.current = false;
396
396
  virtualizer.scrollToIndex(found.index, { align: 'start' });
397
397
  };
398
- return (_jsx(FileLinkContext.Provider, { value: info?.cwd ?? null, children: _jsxs("div", { ref: frameRef, className: "relative flex min-h-0 grow flex-col", children: [searching && chatFind.bar, _jsxs(ContextMenu.Root, { children: [_jsx("div", { ref: scrollRef,
398
+ return (_jsx(FileLinkContext.Provider, { value: info?.cwd ?? null, children: _jsxs("div", { ref: frameRef, className: "relative flex min-h-0 grow flex-col", "data-file-cwd": info?.cwd ?? '', "data-file-scope-id": scope.id, children: [searching && chatFind.bar, _jsxs(ContextMenu.Root, { children: [_jsx("div", { ref: scrollRef,
399
399
  // No CSS scroll-padding here: Chromium counts the sticky composer's caret as hidden behind
400
400
  // it and scrolls the thread toward the end on every keystroke.
401
401
  className: "chat-scroll min-h-0 grow overflow-auto",
@@ -7,7 +7,7 @@ import { markdownOf, messageTextOf } from '../logic/timeline-copy.js';
7
7
  import { chatHost } from '../../host.js';
8
8
  import { useChatScope } from '../../scope.js';
9
9
  import { useChatRow } from '../../state/chats.js';
10
- import { openFileLink, useFileLinkCwd } from './file-links.js';
10
+ import { openFileLink, useFileLinkCwd, useFileLinkScopeId } from './file-links.js';
11
11
  import { ContextMenu, copyText, DisabledReason, EDIT_SHORTCUTS, Icon, Kbd, selectAllWithin } from '@adecore/ui';
12
12
  /*
13
13
  * The menu behind a right-click in a thread. Copy is the reason it exists. Everything in a thread
@@ -23,10 +23,14 @@ export function TimelineMenuPopup({ target, thread, chatId = null }) {
23
23
  const markable = chatId !== null && (target.row?.kind === 'user' || target.row?.kind === 'assistant') ? target.row.id : null;
24
24
  // A thread outside a chat (a sub-agent's transcript) has no cwd of its own; the app answers for it then.
25
25
  const cwd = useFileLinkCwd();
26
+ const scopeId = useFileLinkScopeId();
26
27
  const { fork, fileLinks } = chatHost();
27
28
  const openInPreview = () => {
28
- if (target.path !== null) {
29
- openFileLink(cwd, { path: target.path, ...(target.line === null ? {} : { line: target.line }), directory: false });
29
+ if (target.file != null) {
30
+ openFileLink(target.file.cwd, target.file.ref, target.file.scopeId);
31
+ }
32
+ else if (target.path !== null) {
33
+ openFileLink(cwd, { path: target.path, ...(target.line === null ? {} : { line: target.line }), directory: false }, scopeId);
30
34
  }
31
35
  };
32
36
  return (_jsxs(ContextMenu.Popup, { children: [_jsxs(ContextMenu.Item, { disabled: target.selection === '', onClick: () => copyText(target.selection), children: [_jsx(Icon, { icon: Copy, size: 14 }), " ", t('common.action.copy'), " ", _jsx(Kbd, { shortcut: EDIT_SHORTCUTS.copy })] }), message !== null && (_jsxs(ContextMenu.Item, { onClick: () => copyText(message), children: [_jsx(Icon, { icon: MessageSquare, size: 14 }), " ", t('timeline.menu.copyMessage')] })), target.code !== null && (_jsxs(ContextMenu.Item, { onClick: () => copyText(target.code ?? ''), children: [_jsx(Icon, { icon: Braces, size: 14 }), " ", t('timeline.menu.copyCode')] })), markdown !== null && (_jsxs(ContextMenu.Item, { onClick: () => copyText(markdown), children: [_jsx(Icon, { icon: FileText, size: 14 }), " ", t('timeline.menu.copyMarkdown')] })), chatId !== null && turnId !== null && fork !== null && (_jsx(DisabledReason, { reason: forkBlocked, children: _jsxs(ContextMenu.Item, { disabled: forkBlocked !== null, onClick: () => fork(chatId, turnId), children: [_jsx(Icon, { icon: GitFork, size: 14 }), " ", t('timeline.menu.forkFromHere')] }) })), chatId !== null && markable !== null && _jsx(BookmarkMenuItems, { chatId: chatId, itemId: markable }), _jsx(ContextMenu.Separator, {}), _jsxs(ContextMenu.Item, { onClick: () => selectAllWithin(thread.current), children: [_jsx(Icon, { icon: Scan, size: 14 }), " ", t('common.action.selectAll')] }), target.path !== null && fileLinks !== null && (_jsxs(_Fragment, { children: [_jsx(ContextMenu.Separator, {}), _jsxs(ContextMenu.Item, { onClick: openInPreview, children: [_jsx(Icon, { icon: Eye, size: 14 }), " ", t('timeline.menu.openInPreview')] })] }))] }));
@@ -1,5 +1,6 @@
1
1
  import { type FileRef } from '../../host.js';
2
2
  export declare const FileLinkContext: import("react").Context<string | null>;
3
3
  export declare function useFileLinkCwd(): string | null;
4
+ export declare function useFileLinkScopeId(): string | null;
4
5
  export declare function useFileLinkTarget(text: string): FileRef | null;
5
- export declare function openFileLink(cwd: string | null, ref: FileRef): void;
6
+ export declare function openFileLink(cwd: string | null, ref: FileRef, scopeId?: string | null): void;
@@ -1,5 +1,6 @@
1
1
  import { createContext, useContext, useMemo } from 'react';
2
2
  import { chatHost } from '../../host.js';
3
+ import { ChatScopeContext } from '../../scope.js';
3
4
  /*
4
5
  * The folder a relative reference in rendered text counts from: a chat's own cwd, which is a
5
6
  * worktree as often as it is the project, or the folder of the file an app is drawing. Null where
@@ -9,12 +10,16 @@ export const FileLinkContext = createContext(null);
9
10
  export function useFileLinkCwd() {
10
11
  return useContext(FileLinkContext);
11
12
  }
13
+ export function useFileLinkScopeId() {
14
+ return useContext(ChatScopeContext)?.id ?? null;
15
+ }
12
16
  /* The reference a piece of text names, only where this spot on screen could open it. */
13
17
  export function useFileLinkTarget(text) {
14
18
  const cwd = useFileLinkCwd();
15
- return useMemo(() => chatHost().fileLinks?.target(text, cwd) ?? null, [text, cwd]);
19
+ const scopeId = useFileLinkScopeId();
20
+ return useMemo(() => chatHost().fileLinks?.target(text, cwd, scopeId) ?? null, [text, cwd, scopeId]);
16
21
  }
17
22
  /* A reference followed, the way the app opens a file or a folder. */
18
- export function openFileLink(cwd, ref) {
19
- chatHost().fileLinks?.open(cwd, ref);
23
+ export function openFileLink(cwd, ref, scopeId = null) {
24
+ chatHost().fileLinks?.open(cwd, ref, scopeId);
20
25
  }
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useContext, useState } from 'react';
2
+ import { useContext, useMemo, useState } from 'react';
3
3
  import clsx from 'clsx';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Bot, Brain, Check, ChevronDown, CircleAlert, File, Info, MessageCircleQuestionMark, TriangleAlert, X } from 'lucide-react';
@@ -107,18 +107,21 @@ function Writing() {
107
107
  */
108
108
  export function AssistantRow({ chatId, item: derived }) {
109
109
  const item = useCurrentItem(chatId, derived);
110
+ const scope = useChatScope();
111
+ const context = useContext(ReplyContext);
112
+ const reply = useMemo(() => (context === null && !item.parentToolUseId ? { scopeId: scope.id, chatId, itemId: item.id, streaming: item.streaming } : undefined), [context, item.parentToolUseId, scope.id, chatId, item.id, item.streaming]);
110
113
  const mode = chatHost().useStreaming();
111
114
  const [sawWriting] = useState(item.streaming);
112
115
  const live = mode === 'words' && item.streaming;
113
116
  const reveal = useRevealedText(item.text, live);
114
117
  if (mode === 'blocks') {
115
118
  const settled = item.streaming ? settledBlocksText(item.text) : item.text;
116
- return (_jsxs("div", { "data-find-item": item.id, className: "-mx-1 px-1 pb-2", children: [_jsx(ReplyHeading, { chatId: chatId }), settled !== '' && (_jsx("div", { "data-find-field": "text", "data-quote-answer": true, className: "contents", children: _jsx(ReplyMarkdown, { text: settled, streaming: false, arriving: sawWriting }) })), item.streaming && _jsx(Writing, {})] }));
119
+ return (_jsxs("div", { "data-find-item": item.id, className: "-mx-1 px-1 pb-2", children: [_jsx(ReplyHeading, { chatId: chatId }), settled !== '' && (_jsx("div", { "data-find-field": "text", "data-quote-answer": true, className: "contents", children: _jsx(ReplyMarkdown, { text: settled, streaming: false, arriving: sawWriting, reply: reply }) })), item.streaming && _jsx(Writing, {})] }));
117
120
  }
118
121
  if (mode === 'whole' && item.streaming) {
119
122
  return (_jsxs("div", { className: "-mx-1 px-1 pb-2", children: [_jsx(ReplyHeading, { chatId: chatId }), _jsx(Writing, {})] }));
120
123
  }
121
- return (_jsxs("div", { "data-find-item": item.id, className: clsx('-mx-1 px-1 pb-2', mode === 'whole' && sawWriting && WHOLE_FADE_CLASS), children: [_jsx(ReplyHeading, { chatId: chatId }), _jsx("div", { "data-find-field": "text", "data-quote-answer": true, className: "contents", children: _jsx(ReplyMarkdown, { text: reveal.text, streaming: reveal.active }) }), live && item.text === '' && _jsx("span", { className: "inline-block h-3.5 w-1.5 animate-pulse rounded-sm bg-text-faint align-middle" })] }));
124
+ return (_jsxs("div", { "data-find-item": item.id, className: clsx('-mx-1 px-1 pb-2', mode === 'whole' && sawWriting && WHOLE_FADE_CLASS), children: [_jsx(ReplyHeading, { chatId: chatId }), _jsx("div", { "data-find-field": "text", "data-quote-answer": true, className: "contents", children: _jsx(ReplyMarkdown, { text: reveal.text, streaming: reveal.active, reply: reply }) }), live && item.text === '' && _jsx("span", { className: "inline-block h-3.5 w-1.5 animate-pulse rounded-sm bg-text-faint align-middle" })] }));
122
125
  }
123
126
  /*
124
127
  * What the model thought before it answered. It shimmers while it streams and folds itself away