@jx0/jmux 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jx0/jmux",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "The terminal workspace for agentic development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -235,4 +235,125 @@ describe("Sidebar", () => {
235
235
  }
236
236
  expect(found).toBe(true);
237
237
  });
238
+
239
+ test("scrolls to show active session when it overflows", () => {
240
+ // Height 10 = 2 header rows + 8 viewport rows
241
+ // Each session = 2 rows + 1 spacer = 3 rows, so 8 rows fits ~2.6 sessions
242
+ const sidebar = new Sidebar(SIDEBAR_WIDTH, 10);
243
+ sidebar.updateSessions(
244
+ makeSessions([
245
+ { name: "a" },
246
+ { name: "b" },
247
+ { name: "c" },
248
+ { name: "d" },
249
+ ]),
250
+ );
251
+ // Activate last session and scroll to it
252
+ sidebar.setActiveSession("$3");
253
+ sidebar.scrollToActive();
254
+ const grid = sidebar.getGrid();
255
+ // "d" should be visible somewhere in the grid
256
+ let found = false;
257
+ for (let r = 2; r < 10; r++) {
258
+ const text = Array.from(
259
+ { length: SIDEBAR_WIDTH },
260
+ (_, i) => grid.cells[r][i].char,
261
+ ).join("");
262
+ if (text.includes("d")) {
263
+ found = true;
264
+ break;
265
+ }
266
+ }
267
+ expect(found).toBe(true);
268
+ });
269
+
270
+ test("scrollBy moves viewport and clamps to bounds", () => {
271
+ const sidebar = new Sidebar(SIDEBAR_WIDTH, 10);
272
+ sidebar.updateSessions(
273
+ makeSessions([
274
+ { name: "a" },
275
+ { name: "b" },
276
+ { name: "c" },
277
+ { name: "d" },
278
+ ]),
279
+ );
280
+ // First session visible at start
281
+ let grid = sidebar.getGrid();
282
+ const row2 = Array.from(
283
+ { length: SIDEBAR_WIDTH },
284
+ (_, i) => grid.cells[2][i].char,
285
+ ).join("");
286
+ expect(row2).toContain("a");
287
+
288
+ // Scroll down
289
+ sidebar.scrollBy(3);
290
+ grid = sidebar.getGrid();
291
+ // "a" should no longer be on row 2
292
+ const row2After = Array.from(
293
+ { length: SIDEBAR_WIDTH },
294
+ (_, i) => grid.cells[2][i].char,
295
+ ).join("");
296
+ expect(row2After).not.toContain("a");
297
+
298
+ // Scroll way past the top — should clamp to 0
299
+ sidebar.scrollBy(-100);
300
+ grid = sidebar.getGrid();
301
+ const row2Reset = Array.from(
302
+ { length: SIDEBAR_WIDTH },
303
+ (_, i) => grid.cells[2][i].char,
304
+ ).join("");
305
+ expect(row2Reset).toContain("a");
306
+ });
307
+
308
+ test("shows scroll indicators when content overflows", () => {
309
+ const sidebar = new Sidebar(SIDEBAR_WIDTH, 10);
310
+ sidebar.updateSessions(
311
+ makeSessions([
312
+ { name: "a" },
313
+ { name: "b" },
314
+ { name: "c" },
315
+ { name: "d" },
316
+ ]),
317
+ );
318
+ // At top: should show down indicator but not up
319
+ let grid = sidebar.getGrid();
320
+ expect(grid.cells[2][SIDEBAR_WIDTH - 1].char).not.toBe("\u25b2");
321
+ expect(grid.cells[9][SIDEBAR_WIDTH - 1].char).toBe("\u25bc");
322
+
323
+ // Scroll to middle: should show both
324
+ sidebar.scrollBy(3);
325
+ grid = sidebar.getGrid();
326
+ expect(grid.cells[2][SIDEBAR_WIDTH - 1].char).toBe("\u25b2");
327
+ });
328
+
329
+ test("scrollToActive snaps back after manual scroll", () => {
330
+ const sidebar = new Sidebar(SIDEBAR_WIDTH, 10);
331
+ sidebar.updateSessions(
332
+ makeSessions([
333
+ { name: "a" },
334
+ { name: "b" },
335
+ { name: "c" },
336
+ { name: "d" },
337
+ ]),
338
+ );
339
+ sidebar.setActiveSession("$0"); // "a" is active
340
+ // Scroll away from active session
341
+ sidebar.scrollBy(6);
342
+ // Snap back
343
+ sidebar.scrollToActive();
344
+ const grid = sidebar.getGrid();
345
+ // "a" should be visible
346
+ let found = false;
347
+ for (let r = 2; r < 10; r++) {
348
+ const text = Array.from(
349
+ { length: SIDEBAR_WIDTH },
350
+ (_, i) => grid.cells[r][i].char,
351
+ ).join("");
352
+ if (text.includes("a")) {
353
+ found = true;
354
+ break;
355
+ }
356
+ }
357
+ expect(found).toBe(true);
358
+ });
238
359
  });
@@ -33,6 +33,7 @@ export interface InputRouterOptions {
33
33
  sidebarCols: number;
34
34
  onPtyData: (data: string) => void;
35
35
  onSidebarClick: (row: number) => void;
36
+ onSidebarScroll?: (delta: number) => void;
36
37
  onSessionPrev?: () => void;
37
38
  onSessionNext?: () => void;
38
39
  }
@@ -65,6 +66,12 @@ export class InputRouter {
65
66
  const mouse = parseSgrMouse(data);
66
67
  if (mouse && this.sidebarVisible) {
67
68
  if (mouse.x <= this.opts.sidebarCols) {
69
+ // Wheel events: button 64 = up, 65 = down
70
+ if ((mouse.button & 64) !== 0) {
71
+ const delta = (mouse.button & 1) ? 3 : -3;
72
+ this.opts.onSidebarScroll?.(delta);
73
+ return;
74
+ }
68
75
  // Click in sidebar region (ignore drags — button bit 5 = motion)
69
76
  if (!mouse.release && (mouse.button & 32) === 0) {
70
77
  this.opts.onSidebarClick(mouse.y - 1); // 0-indexed row
package/src/main.ts CHANGED
@@ -12,7 +12,7 @@ import { homedir } from "os";
12
12
 
13
13
  // --- CLI commands (run and exit before TUI) ---
14
14
 
15
- const VERSION = "0.3.3";
15
+ const VERSION = "0.3.5";
16
16
 
17
17
  const HELP = `jmux — a persistent session sidebar for tmux
18
18
 
@@ -255,6 +255,7 @@ async function switchSession(sessionId: string): Promise<void> {
255
255
  sidebar.setActivity(sessionId, false);
256
256
  currentSessionId = sessionId;
257
257
  sidebar.setActiveSession(sessionId);
258
+ sidebar.scrollToActive();
258
259
  renderFrame();
259
260
 
260
261
  // Clear attention flag if set
@@ -298,6 +299,10 @@ const inputRouter = new InputRouter(
298
299
  const session = sidebar.getSessionByRow(row);
299
300
  if (session) switchSession(session.id);
300
301
  },
302
+ onSidebarScroll: (delta) => {
303
+ sidebar.scrollBy(delta);
304
+ scheduleRender();
305
+ },
301
306
  onSessionPrev: () => switchByOffset(-1),
302
307
  onSessionNext: () => switchByOffset(1),
303
308
  },
@@ -308,17 +313,51 @@ const inputRouter = new InputRouter(
308
313
 
309
314
  let writesPending = 0;
310
315
 
311
- // OSC 52 clipboard: \x1b]52;...;...\x07 or \x1b]52;...;...\x1b\\
312
- const OSC52_RE = /\x1b\]52;[^;]*;[^\x07\x1b]*(?:\x07|\x1b\\)/g;
316
+ // OSC 52 clipboard passthrough buffers across split chunks
317
+ const OSC52_START = "\x1b]52;";
318
+ let osc52Pending = "";
319
+
320
+ function forwardOsc52(data: string): void {
321
+ let search = osc52Pending ? osc52Pending + data : data;
322
+ osc52Pending = "";
323
+
324
+ let pos = 0;
325
+ while (pos < search.length) {
326
+ const start = search.indexOf(OSC52_START, pos);
327
+ if (start < 0) break;
328
+
329
+ // Find terminator: BEL (\x07) or ST (\x1b\\)
330
+ let end = -1;
331
+ let endLen = 0;
332
+ for (let i = start + OSC52_START.length; i < search.length; i++) {
333
+ if (search[i] === "\x07") {
334
+ end = i;
335
+ endLen = 1;
336
+ break;
337
+ }
338
+ if (search[i] === "\x1b" && i + 1 < search.length && search[i + 1] === "\\") {
339
+ end = i;
340
+ endLen = 2;
341
+ break;
342
+ }
343
+ }
313
344
 
314
- pty.onData((data: string) => {
315
- // Pass OSC 52 clipboard sequences directly to the outer terminal
316
- const osc52Matches = data.match(OSC52_RE);
317
- if (osc52Matches) {
318
- for (const seq of osc52Matches) {
319
- process.stdout.write(seq);
345
+ if (end >= 0) {
346
+ process.stdout.write(search.slice(start, end + endLen));
347
+ pos = end + endLen;
348
+ } else {
349
+ // Incomplete — buffer for next chunk (cap at 512KB to avoid leaks)
350
+ const remainder = search.slice(start);
351
+ if (remainder.length < 512 * 1024) {
352
+ osc52Pending = remainder;
353
+ }
354
+ return;
320
355
  }
321
356
  }
357
+ }
358
+
359
+ pty.onData((data: string) => {
360
+ forwardOsc52(data);
322
361
 
323
362
  writesPending++;
324
363
  bridge.write(data).then(() => {
package/src/renderer.ts CHANGED
@@ -67,7 +67,8 @@ export function compositeGrids(
67
67
  grid.cells[y][borderCol] = {
68
68
  ...DEFAULT_CELL,
69
69
  char: BORDER_CHAR,
70
- dim: true,
70
+ fg: 8,
71
+ fgMode: ColorMode.Palette,
71
72
  };
72
73
  // Copy main cells
73
74
  for (let x = 0; x < main.cols; x++) {
package/src/sidebar.ts CHANGED
@@ -9,10 +9,14 @@ const ACCENT_ATTRS: CellAttrs = {
9
9
  fg: 2,
10
10
  fgMode: ColorMode.Palette,
11
11
  };
12
+ // #1e2a35 as packed RGB for subtle active row background
13
+ const ACTIVE_BG = (0x1e << 16) | (0x2a << 8) | 0x35;
12
14
  const ACTIVE_MARKER_ATTRS: CellAttrs = {
13
15
  fg: 2,
14
16
  fgMode: ColorMode.Palette,
15
17
  bold: true,
18
+ bg: ACTIVE_BG,
19
+ bgMode: ColorMode.RGB,
16
20
  };
17
21
  const ACTIVITY_ATTRS: CellAttrs = {
18
22
  fg: 2,
@@ -24,9 +28,16 @@ const ATTENTION_ATTRS: CellAttrs = {
24
28
  bold: true,
25
29
  };
26
30
  const ACTIVE_NAME_ATTRS: CellAttrs = {
27
- fg: 15,
31
+ fg: 2,
28
32
  fgMode: ColorMode.Palette,
29
33
  bold: true,
34
+ bg: ACTIVE_BG,
35
+ bgMode: ColorMode.RGB,
36
+ };
37
+ const ACTIVE_DETAIL_ATTRS: CellAttrs = {
38
+ dim: true,
39
+ bg: ACTIVE_BG,
40
+ bgMode: ColorMode.RGB,
30
41
  };
31
42
  const INACTIVE_NAME_ATTRS: CellAttrs = {
32
43
  fg: 7,
@@ -145,6 +156,10 @@ function buildRenderPlan(sessions: SessionInfo[]): {
145
156
  return { items, displayOrder };
146
157
  }
147
158
 
159
+ function itemHeight(item: RenderItem): number {
160
+ return item.type === "session" ? 2 : 1;
161
+ }
162
+
148
163
  // --- Sidebar class ---
149
164
 
150
165
  export class Sidebar {
@@ -152,9 +167,11 @@ export class Sidebar {
152
167
  private height: number;
153
168
  private sessions: SessionInfo[] = [];
154
169
  private activeSessionId: string | null = null;
170
+ private items: RenderItem[] = [];
155
171
  private displayOrder: number[] = [];
156
172
  private rowToSessionIndex = new Map<number, number>();
157
173
  private activitySet = new Set<string>();
174
+ private scrollOffset = 0;
158
175
 
159
176
  constructor(width: number, height: number) {
160
177
  this.width = width;
@@ -163,8 +180,10 @@ export class Sidebar {
163
180
 
164
181
  updateSessions(sessions: SessionInfo[]): void {
165
182
  this.sessions = sessions;
166
- const { displayOrder } = buildRenderPlan(sessions);
183
+ const { items, displayOrder } = buildRenderPlan(sessions);
184
+ this.items = items;
167
185
  this.displayOrder = displayOrder;
186
+ this.clampScroll();
168
187
  }
169
188
 
170
189
  setActiveSession(id: string): void {
@@ -194,6 +213,41 @@ export class Sidebar {
194
213
  resize(width: number, height: number): void {
195
214
  this.width = width;
196
215
  this.height = height;
216
+ this.clampScroll();
217
+ }
218
+
219
+ scrollBy(delta: number): void {
220
+ this.scrollOffset += delta;
221
+ this.clampScroll();
222
+ }
223
+
224
+ scrollToActive(): void {
225
+ if (!this.activeSessionId) return;
226
+ const viewportHeight = this.height - HEADER_ROWS;
227
+ let vRow = 0;
228
+ for (const item of this.items) {
229
+ const h = itemHeight(item);
230
+ if (item.type === "session") {
231
+ const session = this.sessions[item.sessionIndex];
232
+ if (session?.id === this.activeSessionId) {
233
+ if (vRow < this.scrollOffset) {
234
+ this.scrollOffset = vRow;
235
+ } else if (vRow + h > this.scrollOffset + viewportHeight) {
236
+ this.scrollOffset = vRow + h - viewportHeight;
237
+ }
238
+ this.clampScroll();
239
+ return;
240
+ }
241
+ }
242
+ vRow += h;
243
+ }
244
+ }
245
+
246
+ private clampScroll(): void {
247
+ const totalRows = this.items.reduce((sum, item) => sum + itemHeight(item), 0);
248
+ const viewportHeight = this.height - HEADER_ROWS;
249
+ const maxOffset = Math.max(0, totalRows - viewportHeight);
250
+ this.scrollOffset = Math.max(0, Math.min(maxOffset, this.scrollOffset));
197
251
  }
198
252
 
199
253
  getGrid(): CellGrid {
@@ -204,152 +258,146 @@ export class Sidebar {
204
258
  writeString(grid, 0, 1, "jmux", { ...ACCENT_ATTRS, bold: true });
205
259
  writeString(grid, 1, 0, "\u2500".repeat(this.width), DIM_ATTRS);
206
260
 
207
- const { items } = buildRenderPlan(this.sessions);
208
- let row = HEADER_ROWS;
261
+ const viewportHeight = this.height - HEADER_ROWS;
262
+ let vRow = 0;
263
+ let totalRows = 0;
209
264
 
210
- for (const item of items) {
211
- if (row >= this.height) break;
265
+ for (const item of this.items) {
266
+ const h = itemHeight(item);
267
+ const screenRow = HEADER_ROWS + vRow - this.scrollOffset;
212
268
 
213
- if (item.type === "group-header") {
214
- let label = item.label;
215
- if (label.length > this.width - 2) {
216
- label = label.slice(0, this.width - 3) + "\u2026";
217
- }
218
- writeString(grid, row, 1, label, GROUP_HEADER_ATTRS);
219
- row++;
269
+ // Skip items entirely above viewport
270
+ if (screenRow + h <= HEADER_ROWS) {
271
+ vRow += h;
272
+ totalRows += h;
220
273
  continue;
221
274
  }
222
-
223
- if (item.type === "spacer") {
224
- row++;
275
+ // Track total rows even after viewport
276
+ if (screenRow >= this.height) {
277
+ vRow += h;
278
+ totalRows += h;
225
279
  continue;
226
280
  }
227
281
 
228
- const sessionIdx = item.sessionIndex;
229
- const session = this.sessions[sessionIdx];
230
- if (!session) continue;
231
-
232
- const nameRow = row;
233
- const isActive = session.id === this.activeSessionId;
234
- const hasActivity = this.activitySet.has(session.id);
235
-
236
- if (item.grouped) {
237
- // Grouped: two rows — name + window count, then subdirectory + branch
238
- const detailRow = row + 1;
239
-
240
- this.rowToSessionIndex.set(nameRow, sessionIdx);
241
- if (detailRow < this.height) {
242
- this.rowToSessionIndex.set(detailRow, sessionIdx);
282
+ if (item.type === "group-header") {
283
+ let label = item.label;
284
+ if (label.length > this.width - 2) {
285
+ label = label.slice(0, this.width - 3) + "\u2026";
243
286
  }
287
+ writeString(grid, screenRow, 1, label, GROUP_HEADER_ATTRS);
288
+ } else if (item.type === "spacer") {
289
+ // nothing to render
290
+ } else {
291
+ this.renderSession(grid, screenRow, item);
292
+ }
244
293
 
245
- if (isActive) {
246
- writeString(grid, nameRow, 0, "\u258e", ACTIVE_MARKER_ATTRS);
247
- if (detailRow < this.height) {
248
- writeString(grid, detailRow, 0, "\u258e", ACTIVE_MARKER_ATTRS);
249
- }
250
- }
294
+ vRow += h;
295
+ totalRows += h;
296
+ }
251
297
 
252
- if (session.attention) {
253
- writeString(grid, nameRow, 1, "!", ATTENTION_ATTRS);
254
- } else if (hasActivity) {
255
- writeString(grid, nameRow, 1, "\u25CF", ACTIVITY_ATTRS);
256
- }
298
+ // Scroll indicators
299
+ if (this.scrollOffset > 0) {
300
+ writeString(grid, HEADER_ROWS, this.width - 1, "\u25b2", DIM_ATTRS);
301
+ }
302
+ if (this.scrollOffset + viewportHeight < totalRows) {
303
+ writeString(grid, this.height - 1, this.width - 1, "\u25bc", DIM_ATTRS);
304
+ }
257
305
 
258
- const windowCountStr = `${session.windowCount}w`;
259
- const windowCountCol = this.width - windowCountStr.length - 1;
260
- const nameStart = 3;
261
- const nameMaxLen = windowCountCol - 1 - nameStart;
262
- let displayName = session.name;
263
- if (displayName.length > nameMaxLen) {
264
- displayName = displayName.slice(0, nameMaxLen - 1) + "\u2026";
265
- }
306
+ return grid;
307
+ }
266
308
 
267
- const nameAttrs: CellAttrs = isActive
268
- ? { ...ACTIVE_NAME_ATTRS }
269
- : { ...INACTIVE_NAME_ATTRS };
270
- writeString(grid, nameRow, nameStart, displayName, nameAttrs);
309
+ private renderSession(
310
+ grid: CellGrid,
311
+ nameRow: number,
312
+ item: Extract<RenderItem, { type: "session" }>,
313
+ ): void {
314
+ const sessionIdx = item.sessionIndex;
315
+ const session = this.sessions[sessionIdx];
316
+ if (!session) return;
317
+
318
+ const detailRow = nameRow + 1;
319
+ const isActive = session.id === this.activeSessionId;
320
+ const hasActivity = this.activitySet.has(session.id);
321
+
322
+ // Map rows to session for click handling
323
+ this.rowToSessionIndex.set(nameRow, sessionIdx);
324
+ if (detailRow < this.height) {
325
+ this.rowToSessionIndex.set(detailRow, sessionIdx);
326
+ }
271
327
 
272
- if (windowCountCol > nameStart) {
273
- writeString(grid, nameRow, windowCountCol, windowCountStr, DIM_ATTRS);
274
- }
328
+ // Paint active background across both rows
329
+ if (isActive) {
330
+ const bgFill = " ".repeat(this.width);
331
+ const bgAttrs: CellAttrs = { bg: ACTIVE_BG, bgMode: ColorMode.RGB };
332
+ writeString(grid, nameRow, 0, bgFill, bgAttrs);
333
+ writeString(grid, detailRow, 0, bgFill, bgAttrs);
334
+ }
275
335
 
276
- // Detail line: branch name
277
- if (detailRow < this.height && session.gitBranch) {
278
- const detailStart = 3;
279
- const maxLen = this.width - detailStart - 1;
280
- let branch = session.gitBranch;
281
- if (branch.length > maxLen) {
282
- branch = branch.slice(0, maxLen - 1) + "\u2026";
283
- }
284
- writeString(grid, detailRow, detailStart, branch, DIM_ATTRS);
285
- }
336
+ // Active marker
337
+ if (isActive) {
338
+ writeString(grid, nameRow, 0, "\u258e", ACTIVE_MARKER_ATTRS);
339
+ writeString(grid, detailRow, 0, "\u258e", ACTIVE_MARKER_ATTRS);
340
+ }
286
341
 
287
- row += 2;
288
- } else {
289
- // Ungrouped: two rows (name + detail)
290
- const detailRow = row + 1;
342
+ // Indicator
343
+ if (session.attention) {
344
+ writeString(grid, nameRow, 1, "!", ATTENTION_ATTRS);
345
+ } else if (hasActivity) {
346
+ writeString(grid, nameRow, 1, "\u25CF", ACTIVITY_ATTRS);
347
+ }
291
348
 
292
- this.rowToSessionIndex.set(nameRow, sessionIdx);
293
- if (detailRow < this.height) {
294
- this.rowToSessionIndex.set(detailRow, sessionIdx);
295
- }
349
+ // Name row: name + window count
350
+ const windowCountStr = `${session.windowCount}w`;
351
+ const windowCountCol = this.width - windowCountStr.length - 1;
352
+ const nameStart = 3;
353
+ const nameMaxLen = windowCountCol - 1 - nameStart;
354
+ let displayName = session.name;
355
+ if (displayName.length > nameMaxLen) {
356
+ displayName = displayName.slice(0, nameMaxLen - 1) + "\u2026";
357
+ }
296
358
 
297
- if (isActive) {
298
- writeString(grid, nameRow, 0, "\u258e", ACTIVE_MARKER_ATTRS);
299
- if (detailRow < this.height) {
300
- writeString(grid, detailRow, 0, "\u258e", ACTIVE_MARKER_ATTRS);
301
- }
302
- }
359
+ const nameAttrs: CellAttrs = isActive
360
+ ? { ...ACTIVE_NAME_ATTRS }
361
+ : { ...INACTIVE_NAME_ATTRS };
362
+ writeString(grid, nameRow, nameStart, displayName, nameAttrs);
303
363
 
304
- if (session.attention) {
305
- writeString(grid, nameRow, 1, "!", ATTENTION_ATTRS);
306
- } else if (hasActivity) {
307
- writeString(grid, nameRow, 1, "\u25CF", ACTIVITY_ATTRS);
308
- }
364
+ const wcAttrs: CellAttrs = isActive
365
+ ? { ...DIM_ATTRS, bg: ACTIVE_BG, bgMode: ColorMode.RGB }
366
+ : DIM_ATTRS;
367
+ if (windowCountCol > nameStart) {
368
+ writeString(grid, nameRow, windowCountCol, windowCountStr, wcAttrs);
369
+ }
309
370
 
310
- const windowCountStr = `${session.windowCount}w`;
311
- const windowCountCol = this.width - windowCountStr.length - 1;
312
- const nameStart = 3;
313
- const nameMaxLen = windowCountCol - 1 - nameStart;
314
- let displayName = session.name;
315
- if (displayName.length > nameMaxLen) {
316
- displayName = displayName.slice(0, nameMaxLen - 1) + "\u2026";
371
+ // Detail line
372
+ const detailAttrs: CellAttrs = isActive ? ACTIVE_DETAIL_ATTRS : DIM_ATTRS;
373
+ if (item.grouped) {
374
+ if (session.gitBranch) {
375
+ const detailStart = 3;
376
+ const maxLen = this.width - detailStart - 1;
377
+ let branch = session.gitBranch;
378
+ if (branch.length > maxLen) {
379
+ branch = branch.slice(0, maxLen - 1) + "\u2026";
317
380
  }
318
-
319
- const nameAttrs: CellAttrs = isActive
320
- ? { ...ACTIVE_NAME_ATTRS }
321
- : { ...INACTIVE_NAME_ATTRS };
322
- writeString(grid, nameRow, nameStart, displayName, nameAttrs);
323
-
324
- if (windowCountCol > nameStart) {
325
- writeString(grid, nameRow, windowCountCol, windowCountStr, DIM_ATTRS);
381
+ writeString(grid, detailRow, detailStart, branch, detailAttrs);
382
+ }
383
+ } else {
384
+ const detailStart = 3;
385
+ let branchCols = 0;
386
+ if (session.gitBranch) {
387
+ const branchCol = this.width - session.gitBranch.length - 1;
388
+ if (branchCol > detailStart + 1) {
389
+ writeString(grid, detailRow, branchCol, session.gitBranch, detailAttrs);
390
+ branchCols = session.gitBranch.length + 2;
326
391
  }
327
-
328
- // Detail line
329
- if (detailRow < this.height) {
330
- const detailStart = 3;
331
- let branchCols = 0;
332
- if (session.gitBranch) {
333
- const branchCol = this.width - session.gitBranch.length - 1;
334
- if (branchCol > detailStart + 1) {
335
- writeString(grid, detailRow, branchCol, session.gitBranch, DIM_ATTRS);
336
- branchCols = session.gitBranch.length + 2;
337
- }
338
- }
339
- if (session.directory !== undefined) {
340
- const dirMaxLen = this.width - detailStart - branchCols - 1;
341
- let displayDir = session.directory;
342
- if (displayDir.length > dirMaxLen) {
343
- displayDir = displayDir.slice(0, dirMaxLen - 1) + "\u2026";
344
- }
345
- writeString(grid, detailRow, detailStart, displayDir, DIM_ATTRS);
346
- }
392
+ }
393
+ if (session.directory !== undefined) {
394
+ const dirMaxLen = this.width - detailStart - branchCols - 1;
395
+ let displayDir = session.directory;
396
+ if (displayDir.length > dirMaxLen) {
397
+ displayDir = displayDir.slice(0, dirMaxLen - 1) + "\u2026";
347
398
  }
348
-
349
- row += 2;
399
+ writeString(grid, detailRow, detailStart, displayDir, detailAttrs);
350
400
  }
351
401
  }
352
-
353
- return grid;
354
402
  }
355
403
  }