@@ -38,6 +38,11 @@ function parseLineCommand(line: LogLine): LogLineCommand | null {
3838 return null ;
3939}
4040
41+ function isLogElementInViewport(el : HTMLElement ): boolean {
42+ const rect = el .getBoundingClientRect ();
43+ return rect .top >= 0 && rect .bottom <= window .innerHeight ; // only check height but not width
44+ }
45+
4146const sfc = {
4247 name: ' RepoActionView' ,
4348 components: {
@@ -142,9 +147,14 @@ const sfc = {
142147 },
143148
144149 methods: {
145- // get the active container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group`
146- getLogsContainer(stepIndex : number ) {
147- const el = this .$refs .logs [stepIndex ];
150+ // get the job step logs container ('.job-step-logs')
151+ getJobStepLogsContainer(stepIndex : number ): HTMLElement {
152+ return this .$refs .logs [stepIndex ];
153+ },
154+
155+ // get the active logs container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group`
156+ getActiveLogsContainer(stepIndex : number ): HTMLElement {
157+ const el = this .getJobStepLogsContainer (stepIndex );
148158 return el ._stepLogsActiveContainer ?? el ;
149159 },
150160 // begin a log group
@@ -217,9 +227,15 @@ const sfc = {
217227 );
218228 },
219229
230+ shouldAutoScroll(stepIndex : number ): boolean {
231+ const el = this .getJobStepLogsContainer (stepIndex );
232+ if (! el .lastChild ) return false ;
233+ return isLogElementInViewport (el .lastChild );
234+ },
235+
220236 appendLogs(stepIndex : number , startTime : number , logLines : LogLine []) {
221237 for (const line of logLines ) {
222- const el = this .getLogsContainer (stepIndex );
238+ const el = this .getActiveLogsContainer (stepIndex );
223239 const cmd = parseLineCommand (line );
224240 if (cmd ?.name === ' group' ) {
225241 this .beginLogGroup (stepIndex , startTime , line , cmd );
@@ -278,13 +294,30 @@ const sfc = {
278294 this .currentJobStepsStates [i ] = {cursor: null , expanded: false };
279295 }
280296 }
297+
298+ // find the step indexes that need to auto-scroll
299+ const autoScrollStepIndexes = new Map <number , boolean >();
300+ for (const logs of job .logs .stepsLog ?? []) {
301+ if (autoScrollStepIndexes .has (logs .step )) continue ;
302+ autoScrollStepIndexes .set (logs .step , this .shouldAutoScroll (logs .step ));
303+ }
304+
281305 // append logs to the UI
282306 for (const logs of job .logs .stepsLog ?? []) {
283307 // save the cursor, it will be passed to backend next time
284308 this .currentJobStepsStates [logs .step ].cursor = logs .cursor ;
285309 this .appendLogs (logs .step , logs .started , logs .lines );
286310 }
287311
312+ // auto-scroll to the last log line of the last step
313+ let autoScrollJobStepElement: HTMLElement ;
314+ for (let stepIndex = 0 ; stepIndex < this .currentJob .steps .length ; stepIndex ++ ) {
315+ if (! autoScrollStepIndexes .get (stepIndex )) continue ;
316+ autoScrollJobStepElement = this .getJobStepLogsContainer (stepIndex );
317+ }
318+ autoScrollJobStepElement ?.lastElementChild .scrollIntoView ({behavior: ' smooth' , block: ' nearest' });
319+
320+ // clear the interval timer if the job is done
288321 if (this .run .done && this .intervalID ) {
289322 clearInterval (this .intervalID );
290323 this .intervalID = null ;
0 commit comments