Skip to content

Commit 74d2157

Browse files
commit
1 parent f3940cf commit 74d2157

4 files changed

Lines changed: 127 additions & 11 deletions

File tree

src/App/src/components/content/PlanChat.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,10 @@ const PlanChat: React.FC<SimplifiedPlanChatProps> = ({
8787
{renderAgentMessages(agentMessages, undefined, undefined, finalResultRef)}
8888

8989
{showProcessingPlanSpinner && renderPlanExecutionMessage()}
90-
{/* Streaming plan updates */}
91-
{showBufferingText && (
90+
{/* Streaming plan updates — hidden while an approval prompt is pending so
91+
the approval action is presented at the appropriate step instead of
92+
after the thinking process visibly completes. */}
93+
{showBufferingText && !showApprovalButtons && (
9294
<StreamingBufferMessage
9395
streamingMessageBuffer={streamingMessageBuffer}
9496
isStreaming={true}

src/App/src/components/content/streaming/StreamingAgentMessage.tsx

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ const useStyles = makeStyles({
8282
backgroundColor: 'var(--colorNeutralBackground2)',
8383
color: 'var(--colorNeutralForeground1)',
8484
maxWidth: '100%',
85+
width: '100%',
86+
boxSizing: 'border-box',
87+
overflowX: 'hidden',
8588
alignSelf: 'flex-start',
8689

8790
},
@@ -219,10 +222,10 @@ const renderAgentMessages = (
219222
/>
220223
),
221224
img: ({ node: _imgNode, ...props }) => (
222-
<div style={{ position: 'relative', display: 'inline-block', marginTop: '8px' }}>
225+
<div style={{ position: 'relative', display: 'block', width: '100%', maxWidth: '100%', marginTop: '8px', overflow: 'hidden' }}>
223226
<img
224227
{...props}
225-
style={{ maxWidth: '100%', borderRadius: '8px', display: 'block' }}
228+
style={{ display: 'block', width: '100%', maxWidth: '100%', height: 'auto', borderRadius: '8px' }}
226229
/>
227230
<Button
228231
appearance="subtle"
@@ -239,13 +242,27 @@ const renderAgentMessages = (
239242
color: 'white',
240243
borderRadius: '4px',
241244
}}
242-
onClick={() => {
245+
onClick={async () => {
243246
const url = props.src;
244-
if (url) {
247+
if (!url) return;
248+
const filename = `ad-image-${Date.now()}.png`;
249+
try {
250+
const response = await fetch(url, { mode: 'cors' });
251+
if (!response.ok) throw new Error(`Failed to fetch image (${response.status})`);
252+
const blob = await response.blob();
253+
const blobUrl = URL.createObjectURL(blob);
254+
const link = document.createElement('a');
255+
link.href = blobUrl;
256+
link.download = filename;
257+
document.body.appendChild(link);
258+
link.click();
259+
document.body.removeChild(link);
260+
URL.revokeObjectURL(blobUrl);
261+
} catch (err) {
262+
// Fallback: trigger direct download (works for same-origin or CORS-enabled URLs)
245263
const link = document.createElement('a');
246264
link.href = url;
247-
link.download = `ad-image-${Date.now()}.png`;
248-
link.target = '_blank';
265+
link.download = filename;
249266
document.body.appendChild(link);
250267
link.click();
251268
document.body.removeChild(link);
@@ -254,6 +271,38 @@ const renderAgentMessages = (
254271
title="Download image"
255272
/>
256273
</div>
274+
),
275+
p: ({ node: _pNode, ...props }) => (
276+
<p {...props} style={{ margin: '0 0 8px 0' }} />
277+
),
278+
h1: ({ node: _hNode, ...props }) => (
279+
<h1 {...props} style={{ fontSize: '20px', fontWeight: 600, margin: '16px 0 8px 0', lineHeight: '1.3' }} />
280+
),
281+
h2: ({ node: _hNode, ...props }) => (
282+
<h2 {...props} style={{ fontSize: '17px', fontWeight: 600, margin: '14px 0 8px 0', lineHeight: '1.3' }} />
283+
),
284+
h3: ({ node: _hNode, ...props }) => (
285+
<h3 {...props} style={{ fontSize: '15px', fontWeight: 600, margin: '12px 0 6px 0', lineHeight: '1.3' }} />
286+
),
287+
ul: ({ node: _ulNode, ...props }) => (
288+
<ul {...props} style={{ margin: '8px 0', paddingLeft: '24px' }} />
289+
),
290+
ol: ({ node: _olNode, ...props }) => (
291+
<ol {...props} style={{ margin: '8px 0', paddingLeft: '24px' }} />
292+
),
293+
li: ({ node: _liNode, ...props }) => (
294+
<li {...props} style={{ margin: '4px 0', lineHeight: '1.5' }} />
295+
),
296+
blockquote: ({ node: _bqNode, ...props }) => (
297+
<blockquote
298+
{...props}
299+
style={{
300+
margin: '8px 0',
301+
padding: '8px 12px',
302+
borderLeft: '3px solid var(--colorNeutralStroke1)',
303+
color: 'var(--colorNeutralForeground2)'
304+
}}
305+
/>
257306
)
258307
}}
259308
>

src/App/src/components/content/streaming/StreamingBufferMessage.tsx

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,67 @@ interface StreamingBufferMessageProps {
1212
isStreaming?: boolean;
1313
}
1414

15+
/**
16+
* Wrap any raw JSON object/array blocks in markdown fenced code blocks so the
17+
* Details section renders them as formatted code rather than plain text.
18+
* Lines that already sit inside an existing fenced block are left alone.
19+
*/
20+
const formatBufferContent = (content: string): string => {
21+
if (!content) return content;
22+
23+
const lines = content.split('\n');
24+
const out: string[] = [];
25+
let insideExistingFence = false;
26+
let i = 0;
27+
28+
while (i < lines.length) {
29+
const line = lines[i];
30+
const trimmed = line.trim();
31+
32+
if (trimmed.startsWith('```')) {
33+
insideExistingFence = !insideExistingFence;
34+
out.push(line);
35+
i++;
36+
continue;
37+
}
38+
39+
if (!insideExistingFence && (trimmed.startsWith('{') || trimmed.startsWith('['))) {
40+
// Try to capture a balanced JSON block starting at this line
41+
let depth = 0;
42+
let endIdx = -1;
43+
for (let j = i; j < lines.length; j++) {
44+
const l = lines[j];
45+
for (const ch of l) {
46+
if (ch === '{' || ch === '[') depth++;
47+
else if (ch === '}' || ch === ']') depth--;
48+
}
49+
if (depth === 0) {
50+
endIdx = j;
51+
break;
52+
}
53+
}
54+
if (endIdx !== -1) {
55+
const block = lines.slice(i, endIdx + 1).join('\n');
56+
try {
57+
const parsed = JSON.parse(block);
58+
out.push('```json');
59+
out.push(JSON.stringify(parsed, null, 2));
60+
out.push('```');
61+
i = endIdx + 1;
62+
continue;
63+
} catch {
64+
// Not valid JSON — fall through and keep the original line
65+
}
66+
}
67+
}
68+
69+
out.push(line);
70+
i++;
71+
}
72+
73+
return out.join('\n');
74+
};
75+
1576
// Convert to a proper React component instead of a function
1677
const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
1778
streamingMessageBuffer,
@@ -42,6 +103,8 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
42103

43104
if (!streamingMessageBuffer || streamingMessageBuffer.trim() === "") return null;
44105

106+
const formattedBuffer = formatBufferContent(streamingMessageBuffer);
107+
45108
return (
46109
<div style={{
47110
maxWidth: '800px',
@@ -182,7 +245,7 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
182245
)
183246
}}
184247
>
185-
{streamingMessageBuffer}
248+
{formattedBuffer}
186249
</ReactMarkdown>
187250
</div>
188251
</div>
@@ -216,7 +279,7 @@ const StreamingBufferMessage: React.FC<StreamingBufferMessageProps> = ({
216279
)
217280
}}
218281
>
219-
{streamingMessageBuffer}
282+
{formattedBuffer}
220283
</ReactMarkdown>
221284
</div>
222285
)}

src/App/src/components/content/streaming/StreamingPlanResponse.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ const useStyles = makeStyles({
6161
borderRadius: '8px',
6262
fontSize: '14px',
6363
lineHeight: '1.5',
64-
wordWrap: 'break-word'
64+
wordWrap: 'break-word',
65+
marginLeft: '48px',
66+
boxSizing: 'border-box'
6567
},
6668
factsSection: {
6769
backgroundColor: 'var(--colorNeutralBackground2)',

0 commit comments

Comments
 (0)