diff --git a/web_src/js/components/RepoActionView.test.js b/web_src/js/components/RepoActionView.test.js
index 313732cc61..6f575ca8da 100644
--- a/web_src/js/components/RepoActionView.test.js
+++ b/web_src/js/components/RepoActionView.test.js
@@ -623,3 +623,73 @@ test('view with pre-execution error', async () => {
expect(block.exists()).toBe(true);
expect(block.text()).toBe('pre-execution error Oops, I dropped it.');
});
+
+test('Offset index', async () => {
+ Object.defineProperty(document.documentElement, 'lang', {value: 'en'});
+ vi.spyOn(global, 'fetch').mockImplementation((url, opts) => {
+ const stepsLog_value = [
+ {
+ step: 0,
+ cursor: 0,
+ lines: [
+ {index: 1, message: '\u001b]9;4;3\u0007\r\u001bM\u001b[?2026l\u001b[?2026h\u001b[J', timestamp: 0},
+ {index: 2, message: 'second line', timestamp: 0},
+ {index: 3, message: '\u001b]9;4;3\u0007\r\u001bM\u001b[?2026l\u001b[J\u001b]9;4;0\u0007\u001b[?2026h\u001b[J\u001b]9;4;1;0\u0007\u001b[?2026l\u001b[J\u001b]9;4;0\u0007', timestamp: 0},
+ {index: 4, message: 'fourth line', timestamp: 0},
+ ],
+ },
+ ];
+ const jobs_value = {
+ state: {
+ run: {
+ status: 'success',
+ commit: {
+ pusher: {},
+ },
+ },
+ currentJob: {
+ steps: [
+ {
+ summary: 'Test Job',
+ duration: '1s',
+ status: 'success',
+ },
+ ],
+ allAttempts: [{number: 1, time_since_started_html: '', status: 'success'}],
+ },
+ },
+ logs: {
+ stepsLog: opts.body?.includes('"cursor":null') ? stepsLog_value : [],
+ },
+ };
+
+ return Promise.resolve({
+ ok: true,
+ json: vi.fn().mockResolvedValue(
+ url.endsWith('/artifacts') ? [] : jobs_value,
+ ),
+ });
+ });
+
+ const wrapper = mount(RepoActionView, {
+ props: defaultTestProps,
+ });
+ await flushPromises();
+ await wrapper.get('.job-step-summary').trigger('click');
+ await flushPromises();
+
+ // Check if two lines where rendered
+ expect(wrapper.findAll('.job-log-line').length).toEqual(2);
+
+ // Check line one.
+ expect(wrapper.get('.job-log-line:nth-of-type(1)').attributes('id')).toEqual('jobstep-0-1');
+ expect(wrapper.get('.job-log-line:nth-of-type(1) .line-num').text()).toEqual('1');
+ expect(wrapper.get('.job-log-line:nth-of-type(1) .line-num').attributes('href')).toEqual('#jobstep-0-1');
+ expect(wrapper.get('.job-log-line:nth-of-type(1) .log-msg').text()).toEqual('second line');
+
+ // Check line two.
+ expect(wrapper.get('.job-log-line:nth-of-type(2)').attributes('id')).toEqual('jobstep-0-2');
+ expect(wrapper.get('.job-log-line:nth-of-type(2) .line-num').text()).toEqual('2');
+ expect(wrapper.get('.job-log-line:nth-of-type(2) .line-num').attributes('href')).toEqual('#jobstep-0-2');
+ expect(wrapper.get('.job-log-line:nth-of-type(2) .log-msg').text()).toEqual('fourth line');
+});
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index f9d6028b81..0db9432167 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -33,6 +33,7 @@ const sfc = {
initialLoadComplete: false,
needLoadingWithLogCursors: null,
intervalID: null,
+ lineNumberOffset: [],
currentJobStepsStates: [],
artifacts: [],
menuVisible: undefined,
@@ -203,15 +204,16 @@ const sfc = {
},
createLogLine(line, startTime, stepIndex, group) {
+ const lineNo = line.index - this.lineNumberOffset[stepIndex];
const div = document.createElement('div');
div.classList.add('job-log-line');
- div.setAttribute('id', `jobstep-${stepIndex}-${line.index}`);
+ div.setAttribute('id', `jobstep-${stepIndex}-${lineNo}`);
div._jobLogTime = line.timestamp;
const lineNumber = document.createElement('a');
lineNumber.classList.add('line-num', 'muted');
- lineNumber.textContent = line.index;
- lineNumber.setAttribute('href', `#jobstep-${stepIndex}-${line.index}`);
+ lineNumber.textContent = lineNo;
+ lineNumber.setAttribute('href', `#jobstep-${stepIndex}-${lineNo}`);
div.append(lineNumber);
// for "Show timestamps"
@@ -230,6 +232,13 @@ const sfc = {
let logMessage = document.createElement('span');
logMessage.innerHTML = renderAnsi(line.message);
+ // If the input to renderAnsi is not empty and the output is empty we can
+ // assume the input was only ANSI escape codes that have been removed. In
+ // that case we should not display this message
+ if (line.message !== '' && logMessage.innerHTML === '') {
+ this.lineNumberOffset[stepIndex]++;
+ return [];
+ }
if (group.isHeader) {
const details = document.createElement('details');
details.addEventListener('toggle', this.toggleGroupLogs);
@@ -378,6 +387,7 @@ const sfc = {
// append logs to the UI
for (const logs of job.logs.stepsLog) {
// save the cursor, it will be passed to backend next time
+ this.lineNumberOffset[logs.step] = 0;
this.currentJobStepsStates[logs.step].cursor = logs.cursor;
this.appendLogs(logs.step, logs.lines, logs.started);
}
diff --git a/web_src/js/render/ansi.js b/web_src/js/render/ansi.js
index bb622dd1eb..4a1d83fe19 100644
--- a/web_src/js/render/ansi.js
+++ b/web_src/js/render/ansi.js
@@ -2,7 +2,9 @@ import {AnsiUp} from 'ansi_up';
const replacements = [
[/\x1b\[\d+[A-H]/g, ''], // Move cursor, treat them as no-op
+ [/\x1bM/g, ''], // Move cursor one line up, threat them as no-op.
[/\x1b\[\d?[JK]/g, '\r'], // Erase display/line, treat them as a Carriage Return
+ [/\x1b\]9;\d+.*?(\x07|\x1b\\)/g, ''], // ConEmu, treat them as no-op.
];
// render ANSI to HTML
diff --git a/web_src/js/render/ansi.test.js b/web_src/js/render/ansi.test.js
index 5afff71c29..f0201f1526 100644
--- a/web_src/js/render/ansi.test.js
+++ b/web_src/js/render/ansi.test.js
@@ -17,4 +17,13 @@ test('renderAnsi', () => {
// treat "\033[0K" and "\033[0J" (Erase display/line) as "\r", then it will be covered to "\n" finally.
expect(renderAnsi('a\x1b[Kb\x1b[2Jc')).toEqual('a\nb\nc');
expect(renderAnsi('\x1b[48;5;88ma\x1b[38;208;48;5;159mb\x1b[m')).toEqual(`ab`);
+
+ expect(renderAnsi('\x1b]9;4;0\x07')).toEqual('');
+ expect(renderAnsi('\x1b]9;4;1;25\x07compiling main.zig')).toEqual('compiling main.zig');
+ expect(renderAnsi('\x1b]9;4;1;25\x1b\\compiling main.zig')).toEqual('compiling main.zig');
+ expect(renderAnsi('\x1b]9;4;3\x07waiting...')).toEqual('waiting...');
+ expect(renderAnsi('\x1b]9;1;500\x07sleeping...')).toEqual('sleeping...');
+ expect(renderAnsi('\x1b]9;4;3\x07waiting...\x1b]9;4;3\x07')).toEqual('waiting...');
+ expect(renderAnsi('\x1b]9;12\x07')).toEqual('');
+ expect(renderAnsi('\x1b]9;4;1;25\x07\x1bMcompiling main.zig')).toEqual('compiling main.zig');
});