fix: trim trailing blank lines BEFORE slice, not after
Sessions with cursor near the top (e.g. a fresh tunnel/ssh session) have content at rows 1-2 and rows 3-40 blank in a 40-row terminal. The previous fix trimmed AFTER slice(-20), but slice(-20) of a 40-line snapshot grabs the last 20 rows — all blank. Trimming after slice then removed everything, leaving an empty <pre>. Fix: trim trailing blank lines from the full snapshot first, then slice the last N lines of what remains. This way a session with 2 content lines at the top is reduced to those 2 lines before slicing, and both appear in the preview. Fixes both buildTileHTML (grid tiles) and buildSidebarHTML (sidebar cards). Tests updated to cover the 40-row terminal scenario.
This commit is contained in:
+16
-14
@@ -526,14 +526,15 @@ function buildTileHTML(session, index, mobile) {
|
|||||||
// Last N lines of snapshot — show more in fit mode so tall tiles fill
|
// Last N lines of snapshot — show more in fit mode so tall tiles fill
|
||||||
const snapshot = session.snapshot || '';
|
const snapshot = session.snapshot || '';
|
||||||
var _lineCount = (ds.viewMode === 'fit') ? -80 : -20;
|
var _lineCount = (ds.viewMode === 'fit') ? -80 : -20;
|
||||||
var lines = snapshot.split('\n').slice(_lineCount);
|
// Trim trailing blank lines from the FULL snapshot FIRST — sessions with the cursor
|
||||||
// Trim trailing blank lines — sessions with cursor near the top have mostly empty
|
// near the top (e.g. fresh tunnel/ssh session) have content at rows 1-2 and rows 3-40
|
||||||
// lines at the bottom, making the preview appear blank. Removing them lets the
|
// blank. slice(-20) would grab the last 20 rows (all blank); trimming after slice
|
||||||
// CSS `position: absolute; bottom: 0` anchor the meaningful content correctly.
|
// then removes everything → empty preview. Trim first so slice sees only content rows.
|
||||||
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
var allLines = snapshot.split('\n');
|
||||||
lines.pop();
|
while (allLines.length > 0 && allLines[allLines.length - 1].trim() === '') {
|
||||||
|
allLines.pop();
|
||||||
}
|
}
|
||||||
const lastLines = lines.join('\n');
|
const lastLines = allLines.slice(_lineCount).join('\n');
|
||||||
|
|
||||||
const sourceUrlAttr = session.sourceUrl ? ` data-source-url="${escapeHtml(session.sourceUrl)}"` : '';
|
const sourceUrlAttr = session.sourceUrl ? ` data-source-url="${escapeHtml(session.sourceUrl)}"` : '';
|
||||||
return (
|
return (
|
||||||
@@ -582,15 +583,16 @@ function buildSidebarHTML(session, currentSession) {
|
|||||||
// Timestamp for meta line
|
// Timestamp for meta line
|
||||||
const timeStr = formatTimestamp(session.last_activity_at || null);
|
const timeStr = formatTimestamp(session.last_activity_at || null);
|
||||||
|
|
||||||
// Last 20 lines of snapshot
|
// Last 20 lines of snapshot — trim trailing blanks from the FULL snapshot FIRST,
|
||||||
|
// then slice. Sessions with the cursor near the top have content at rows 1-2 and
|
||||||
|
// rows 3-40 blank; slice(-20) would return only blank rows, then trim-after-slice
|
||||||
|
// removes everything → empty preview. Trim first to keep meaningful content.
|
||||||
const snapshot = session.snapshot || '';
|
const snapshot = session.snapshot || '';
|
||||||
var sidebarLines = snapshot.split('\n').slice(-20);
|
var allLines = snapshot.split('\n');
|
||||||
// Trim trailing blank lines — sessions with cursor near the top have mostly empty
|
while (allLines.length > 0 && allLines[allLines.length - 1].trim() === '') {
|
||||||
// lines at the bottom, making the preview appear blank.
|
allLines.pop();
|
||||||
while (sidebarLines.length > 0 && sidebarLines[sidebarLines.length - 1].trim() === '') {
|
|
||||||
sidebarLines.pop();
|
|
||||||
}
|
}
|
||||||
const lastLines = sidebarLines.join('\n');
|
const lastLines = allLines.slice(-20).join('\n');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
`<article class="${classes}" data-session="${escapedName}" data-source-url="${escapeHtml(session.sourceUrl || '')}" tabindex="0" role="listitem">` +
|
`<article class="${classes}" data-session="${escapedName}" data-source-url="${escapeHtml(session.sourceUrl || '')}" tabindex="0" role="listitem">` +
|
||||||
|
|||||||
@@ -4354,3 +4354,55 @@ test('buildSidebarHTML renders empty pre when snapshot is entirely blank lines',
|
|||||||
assert.ok(preMatch, '<pre> element must exist');
|
assert.ok(preMatch, '<pre> element must exist');
|
||||||
assert.strictEqual(preMatch[1], '', 'sidebar pre should be empty when snapshot has only blank lines');
|
assert.strictEqual(preMatch[1], '', 'sidebar pre should be empty when snapshot has only blank lines');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Trim BEFORE slice: 40-row terminal with content at top ---
|
||||||
|
// The real bug: a 40-row terminal with content at rows 1-2 and rows 3-40 blank.
|
||||||
|
// slice(-20) grabs the LAST 20 rows → all blank. Then trim-after-slice removes
|
||||||
|
// everything → empty pre. Fix: trim trailing blanks from the full snapshot FIRST,
|
||||||
|
// then slice the last 20 of what remains.
|
||||||
|
|
||||||
|
test('buildTileHTML shows content from top of 40-row terminal (trim BEFORE slice)', () => {
|
||||||
|
// 40 lines: 2 content lines + 38 trailing blank lines (realistic 40-row terminal,
|
||||||
|
// cursor near top — e.g. a fresh ssh tunnel session)
|
||||||
|
const snapshot = 'TUNNEL_CONTENT_LINE\nstatus: connected\n' + '\n'.repeat(38);
|
||||||
|
const session = { name: 'tunnel', snapshot };
|
||||||
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
|
assert.ok(
|
||||||
|
html.includes('TUNNEL_CONTENT_LINE'),
|
||||||
|
'tile must show content from top of terminal even with 38 trailing blank lines — trim full snapshot BEFORE slice(-20)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML shows content from top of 40-row terminal (trim BEFORE slice)', () => {
|
||||||
|
const snapshot = 'TUNNEL_CONTENT_LINE\nstatus: connected\n' + '\n'.repeat(38);
|
||||||
|
const session = { name: 'tunnel', snapshot, bell: { unseen_count: 0 } };
|
||||||
|
const html = app.buildSidebarHTML(session, '');
|
||||||
|
assert.ok(
|
||||||
|
html.includes('TUNNEL_CONTENT_LINE'),
|
||||||
|
'sidebar must show content from top of terminal even with 38 trailing blank lines — trim full snapshot BEFORE slice(-20)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildTileHTML trim happens BEFORE slice in source (structural order check)', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||||
|
const fnStart = source.indexOf('function buildTileHTML');
|
||||||
|
const fnBody = source.substring(fnStart, fnStart + 2000);
|
||||||
|
const trimIdx = fnBody.indexOf('.pop()');
|
||||||
|
const sliceIdx = fnBody.indexOf('.slice(');
|
||||||
|
assert.ok(
|
||||||
|
trimIdx < sliceIdx,
|
||||||
|
'trailing blank trim (.pop()) must appear BEFORE .slice() in buildTileHTML — trim the full snapshot first, then slice the last N lines',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML trim happens BEFORE slice in source (structural order check)', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||||
|
const fnStart = source.indexOf('function buildSidebarHTML');
|
||||||
|
const fnBody = source.substring(fnStart, fnStart + 2000);
|
||||||
|
const trimIdx = fnBody.indexOf('.pop()');
|
||||||
|
const sliceIdx = fnBody.indexOf('.slice(');
|
||||||
|
assert.ok(
|
||||||
|
trimIdx < sliceIdx,
|
||||||
|
'trailing blank trim (.pop()) must appear BEFORE .slice() in buildSidebarHTML — trim the full snapshot first, then slice the last 20 lines',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user