Skip to content

Commit 7b86786

Browse files
committed
frontend: fix latex, qmd, rmd to improve streaming exec output
1 parent 52ddb37 commit 7b86786

File tree

4 files changed

+213
-28
lines changed

4 files changed

+213
-28
lines changed

src/packages/frontend/frame-editors/latex-editor/build.tsx

Lines changed: 178 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const Build: React.FC<Props> = React.memo((props) => {
5151
);
5252
const [error_tab, set_error_tab] = useState<string | null>(null);
5353
const logContainerRef = useRef<HTMLDivElement>(null);
54+
const stderrContainerRef = useRef<HTMLDivElement>(null);
5455
const [shownLog, setShownLog] = useState<string>("");
5556

5657
// Compute whether we have running jobs - this determines UI precedence
@@ -68,6 +69,9 @@ export const Build: React.FC<Props> = React.memo((props) => {
6869
if (logContainerRef.current) {
6970
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
7071
}
72+
if (stderrContainerRef.current) {
73+
stderrContainerRef.current.scrollTop = stderrContainerRef.current.scrollHeight;
74+
}
7175
}, [shownLog]);
7276

7377
let no_errors = true;
@@ -86,12 +90,19 @@ export const Build: React.FC<Props> = React.memo((props) => {
8690

8791
function render_tab_item(
8892
title: string,
89-
value: string,
93+
stdout: string,
94+
stderr: string,
9095
error?: boolean,
9196
job_info_str?: string,
9297
): AntdTabItem {
9398
const err_style = error ? { background: COLORS.ANTD_BG_RED_L } : undefined;
9499
const tab_button = <div style={err_style}>{title}</div>;
100+
101+
// Determine if stderr is informational (not actual errors)
102+
const hasStdout = stdout.trim().length > 0;
103+
const hasStderr = stderr.trim().length > 0;
104+
const stderrIsInformational = hasStderr && !error;
105+
95106
return Tab({
96107
key: title,
97108
eventKey: title,
@@ -110,7 +121,75 @@ export const Build: React.FC<Props> = React.memo((props) => {
110121
{job_info_str}
111122
</div>
112123
) : undefined}
113-
<Ansi>{value}</Ansi>
124+
<div
125+
style={{ display: "flex", flexDirection: "column", height: "100%" }}
126+
>
127+
{hasStdout && (
128+
<div
129+
style={{
130+
flex: 1,
131+
display: "flex",
132+
flexDirection: "column",
133+
marginBottom: hasStderr ? "10px" : "0",
134+
}}
135+
>
136+
<div
137+
style={{
138+
fontWeight: "bold",
139+
color: COLORS.GRAY_M,
140+
borderBottom: `1px solid ${COLORS.GRAY_LL}`,
141+
paddingBottom: "5px",
142+
marginBottom: "5px",
143+
fontSize: `${font_size * 0.9}px`,
144+
}}
145+
>
146+
Standard output
147+
</div>
148+
<div
149+
style={{
150+
flex: 1,
151+
overflowY: "auto",
152+
background: COLORS.GRAY_LLL,
153+
padding: "5px",
154+
borderRadius: "3px",
155+
}}
156+
>
157+
<Ansi>{stdout}</Ansi>
158+
</div>
159+
</div>
160+
)}
161+
{hasStderr && (
162+
<div
163+
style={{ flex: 1, display: "flex", flexDirection: "column" }}
164+
>
165+
<div
166+
style={{
167+
fontWeight: "bold",
168+
color: error ? COLORS.ANTD_RED : COLORS.GRAY_M,
169+
borderBottom: `1px solid ${
170+
error ? COLORS.ANTD_RED_WARN : COLORS.GRAY_LL
171+
}`,
172+
paddingBottom: "5px",
173+
marginBottom: "5px",
174+
fontSize: `${font_size * 0.9}px`,
175+
}}
176+
>
177+
{stderrIsInformational ? "Output messages" : "Error output"}
178+
</div>
179+
<div
180+
style={{
181+
flex: 1,
182+
overflowY: "auto",
183+
background: error ? COLORS.ANTD_BG_RED_L : COLORS.GRAY_LLL,
184+
padding: "5px",
185+
borderRadius: "3px",
186+
}}
187+
>
188+
<Ansi>{stderr}</Ansi>
189+
</div>
190+
</div>
191+
)}
192+
</div>
114193
</>
115194
),
116195
});
@@ -122,8 +201,9 @@ export const Build: React.FC<Props> = React.memo((props) => {
122201
// const y: ExecOutput | undefined = job_infos.get(stage)?.toJS();
123202

124203
if (!x) return;
125-
const value = (x.stdout ?? "") + (x.stderr ?? "");
126-
if (!value) return;
204+
const stdout = x.stdout ?? "";
205+
const stderr = x.stderr ?? "";
206+
if (!stdout && !stderr) return;
127207
// const time: number | undefined = x.get("time");
128208
// const time_str = time ? `(${(time / 1000).toFixed(1)} seconds)` : "";
129209
let job_info_str = "";
@@ -147,14 +227,14 @@ export const Build: React.FC<Props> = React.memo((props) => {
147227
set_error_tab(title);
148228
}
149229
}
150-
return render_tab_item(title, value, error, job_info_str);
230+
return render_tab_item(title, stdout, stderr, error, job_info_str);
151231
}
152232

153233
function render_clean(): AntdTabItem | undefined {
154234
const value = build_logs?.getIn(["clean", "output"]) as any;
155235
if (!value) return;
156236
const title = "Clean Auxiliary Files";
157-
return render_tab_item(title, value);
237+
return render_tab_item(title, value, "", false);
158238
}
159239

160240
function render_logs(): Rendered {
@@ -209,17 +289,22 @@ export const Build: React.FC<Props> = React.memo((props) => {
209289
if (!build_logs) return;
210290
const infos: React.JSX.Element[] = [];
211291
let isLongRunning = false;
212-
let logTail = "";
292+
let stdoutTail = "";
293+
let stderrTail = "";
213294

214295
build_logs.forEach((infoI, key) => {
215296
const info: ExecuteCodeOutput = infoI?.toJS();
216297
if (!info || info.type !== "async" || info.status !== "running") return;
217298
const stats_str = getResourceUsage(info.stats, "last");
218299
const start = info.start;
219-
logTail = tail((info.stdout ?? "") + (info.stderr ?? ""), 100);
220-
// Update state for auto-scrolling effect
221-
if (logTail !== shownLog) {
222-
setShownLog(logTail);
300+
stdoutTail = tail(info.stdout ?? "", 100);
301+
stderrTail = tail(info.stderr ?? "", 100);
302+
// Update state for auto-scrolling effect - combine for backward compatibility
303+
const combinedLog =
304+
stdoutTail +
305+
(stderrTail ? "\n--- Error Output ---\n" + stderrTail : "");
306+
if (combinedLog !== shownLog) {
307+
setShownLog(combinedLog);
223308
}
224309
isLongRunning ||=
225310
typeof start === "number" &&
@@ -245,6 +330,9 @@ export const Build: React.FC<Props> = React.memo((props) => {
245330

246331
if (infos.length === 0) return;
247332

333+
const hasStdout = stdoutTail.trim().length > 0;
334+
const hasStderr = stderrTail.trim().length > 0;
335+
248336
return (
249337
<>
250338
<div
@@ -293,14 +381,85 @@ export const Build: React.FC<Props> = React.memo((props) => {
293381
{status}
294382
{"\n"}
295383
</div>
296-
<div
297-
ref={logContainerRef}
298-
style={{
299-
overflowY: "auto",
300-
flexGrow: 1,
301-
}}
302-
>
303-
<Ansi>{shownLog}</Ansi>
384+
<div style={{
385+
flex: 1,
386+
display: "flex",
387+
flexDirection: "column",
388+
gap: hasStdout && hasStderr ? "10px" : "0",
389+
overflow: "hidden"
390+
}}>
391+
{hasStdout && (
392+
<div
393+
style={{
394+
flex: 1,
395+
display: "flex",
396+
flexDirection: "column",
397+
overflow: "hidden",
398+
}}
399+
>
400+
<div
401+
style={{
402+
fontWeight: "bold",
403+
color: COLORS.GRAY_M,
404+
borderBottom: `1px solid ${COLORS.GRAY_LL}`,
405+
paddingBottom: "5px",
406+
marginBottom: "5px",
407+
fontSize: `${font_size * 0.9}px`,
408+
flexShrink: 0,
409+
}}
410+
>
411+
Standard output (stdout)
412+
</div>
413+
<div
414+
ref={logContainerRef}
415+
style={{
416+
flex: 1,
417+
overflowY: "auto",
418+
background: COLORS.GRAY_LLL,
419+
padding: "5px",
420+
borderRadius: "3px",
421+
}}
422+
>
423+
<Ansi>{stdoutTail}</Ansi>
424+
</div>
425+
</div>
426+
)}
427+
{hasStderr && (
428+
<div
429+
style={{
430+
flex: 1,
431+
display: "flex",
432+
flexDirection: "column",
433+
overflow: "hidden",
434+
}}
435+
>
436+
<div
437+
style={{
438+
fontWeight: "bold",
439+
color: COLORS.GRAY_M,
440+
borderBottom: `1px solid ${COLORS.GRAY_LL}`,
441+
paddingBottom: "5px",
442+
marginBottom: "5px",
443+
fontSize: `${font_size * 0.9}px`,
444+
flexShrink: 0,
445+
}}
446+
>
447+
Error output (stderr)
448+
</div>
449+
<div
450+
ref={stderrContainerRef}
451+
style={{
452+
flex: 1,
453+
overflowY: "auto",
454+
background: COLORS.GRAY_LLL,
455+
padding: "5px",
456+
borderRadius: "3px",
457+
}}
458+
>
459+
<Ansi>{stderrTail}</Ansi>
460+
</div>
461+
</div>
462+
)}
304463
</div>
305464
</div>
306465
</>

src/packages/frontend/frame-editors/qmd-editor/build-log.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import Anser from "anser";
7-
import React from "react";
7+
import React, { useEffect, useRef, useState } from "react";
88

99
import { Button } from "@cocalc/frontend/antd-bootstrap";
1010
import { Rendered, useRedux } from "@cocalc/frontend/app-framework";
@@ -43,15 +43,27 @@ export const BuildLog: React.FC<BuildLogProps> = React.memo((props) => {
4343
: build_log_out;
4444
const build_err = have_err ? build_err_out : "";
4545

46-
const [showStdout, setShowStdout] = React.useState(false);
46+
const [showStdout, setShowStdout] = useState(false);
47+
const [shownLog, setShownLog] = useState("");
48+
const logContainerRef = useRef<HTMLDivElement>(null);
4749

4850
// Reset showStdout when a new build starts
49-
React.useEffect(() => {
51+
useEffect(() => {
5052
if (status) {
5153
setShowStdout(false);
5254
}
5355
}, [status]);
5456

57+
// Auto-scroll to bottom when log updates during build
58+
useEffect(() => {
59+
if (status && build_log !== shownLog) {
60+
setShownLog(build_log);
61+
if (logContainerRef.current) {
62+
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
63+
}
64+
}
65+
}, [build_log, status, shownLog]);
66+
5567
function style(type: "log" | "err") {
5668
const style = type == "log" ? STYLE_LOG : STYLE_ERR;
5769
return { ...{ fontSize: `${font_size}px` }, ...style };
@@ -157,7 +169,7 @@ export const BuildLog: React.FC<BuildLogProps> = React.memo((props) => {
157169

158170
if (status) {
159171
return (
160-
<div style={STYLE_OUTER}>
172+
<div ref={logContainerRef} style={STYLE_OUTER}>
161173
<div
162174
style={{
163175
margin: "10px",

src/packages/frontend/frame-editors/rmd-editor/build-log.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import Anser from "anser";
7-
import React from "react";
7+
import React, { useEffect, useRef, useState } from "react";
88

99
import { Button } from "@cocalc/frontend/antd-bootstrap";
1010
import { Rendered, useRedux } from "@cocalc/frontend/app-framework";
@@ -38,15 +38,27 @@ export const BuildLog: React.FC<BuildLogProps> = React.memo(
3838
const build_err = useRedux([name, "build_err"]) || "";
3939
const have_err = (useRedux([name, "build_exit"]) || 0) != 0;
4040
const stats = useRedux([name, "job_info"])?.get("stats")?.toJS();
41-
const [showStdout, setShowStdout] = React.useState(false);
41+
const [showStdout, setShowStdout] = useState(false);
42+
const [shownLog, setShownLog] = useState("");
43+
const logContainerRef = useRef<HTMLDivElement>(null);
4244

4345
// Reset showStdout when a new build starts
44-
React.useEffect(() => {
46+
useEffect(() => {
4547
if (status) {
4648
setShowStdout(false);
4749
}
4850
}, [status]);
4951

52+
// Auto-scroll to bottom when log updates during build
53+
useEffect(() => {
54+
if (status && build_log !== shownLog) {
55+
setShownLog(build_log);
56+
if (logContainerRef.current) {
57+
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
58+
}
59+
}
60+
}, [build_log, status, shownLog]);
61+
5062
function style(type: "log" | "err") {
5163
const style = type == "log" ? STYLE_LOG : STYLE_ERR;
5264
return { ...{ fontSize: `${font_size}px` }, ...style };
@@ -162,7 +174,7 @@ export const BuildLog: React.FC<BuildLogProps> = React.memo(
162174
);
163175
} else {
164176
return (
165-
<div style={STYLE_OUTER}>
177+
<div ref={status ? logContainerRef : undefined} style={STYLE_OUTER}>
166178
{status && (
167179
<div
168180
style={{
@@ -172,6 +184,7 @@ export const BuildLog: React.FC<BuildLogProps> = React.memo(
172184
}}
173185
>
174186
Running rmarkdown::render ...
187+
<br />
175188
{stats && getResourceUsage(stats, "last")}
176189
</div>
177190
)}

src/packages/util/aggregate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function aggregate(options, f?: any) {
105105

106106
const state = {}; // in the closure, so scope is that of this function we are making below.
107107
const done = {};
108-
const omitted_fields = ["cb", "aggregate"];
108+
const omitted_fields = ["cb", "aggregate", "streamCB"];
109109
if (options != null && options.omit) {
110110
for (let field of options.omit) {
111111
omitted_fields.push(field);
@@ -127,6 +127,7 @@ export function aggregate(options, f?: any) {
127127
const recent = done[key];
128128
if (recent != null && leq(opts.aggregate, recent.aggregate)) {
129129
// result is known from a previous call.
130+
// Let the normal callback flow handle streaming events - don't bypass executeStream's logic
130131
opts.cb(...recent.args);
131132
return;
132133
}

0 commit comments

Comments
 (0)