This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

pty ls: sort by displayName (fallback to stable id), ASCII

Without an explicit sort, cmdList iterates listSessions() in whatever
order fs.readdirSync returns for the session dir — on APFS thats
roughly insertion order, which drifts as sessions come and go and
reads like accidental chaos. Now the output is stable and
predictable: alphabetical by the user-visible label (displayName when
set, stable id otherwise), applied to the JSON output and all three
status buckets (running/exited/vanished). Remote host sessions sort
the same way.

Scope is pty list only — the interactive TUI has its own ordering
semantics and is unaffected.

Nathan Herald (Apr 24, 2026, 10:47 AM +0200) eef3f1c1 82ed4463

+91 -1
+16 -1
src/cli.ts
··· 1282 1282 }); 1283 1283 } 1284 1284 1285 + // Stable display order: ASCII sort on the user-visible label (displayName 1286 + // when set, otherwise the stable id). Without this, `fs.readdirSync` on 1287 + // APFS returns sessions in roughly insertion order, which drifts as 1288 + // sessions come and go and reads like accidental chaos to the eye. 1289 + const sortKey = (s: SessionInfo): string => s.metadata?.displayName ?? s.name; 1290 + sessions = [...sessions].sort((a, b) => { 1291 + const ka = sortKey(a), kb = sortKey(b); 1292 + return ka < kb ? -1 : ka > kb ? 1 : 0; 1293 + }); 1294 + 1285 1295 // Fetch relay hosts if --remote 1286 1296 let remoteHosts: { 1287 1297 label: string; ··· 1485 1495 continue; 1486 1496 } 1487 1497 console.log(`\x1b[1m${host.label}\x1b[0m (${host.sessions.length} sessions):`); 1488 - for (const s of host.sessions) { 1498 + const sortedRemote = [...host.sessions].sort((a, b) => { 1499 + const ka = a.displayName ?? a.name; 1500 + const kb = b.displayName ?? b.name; 1501 + return ka < kb ? -1 : ka > kb ? 1 : 0; 1502 + }); 1503 + for (const s of sortedRemote) { 1489 1504 const icon = s.status === "running" ? "\u25cf" : "\u25cb"; 1490 1505 const cwd = s.cwd ? shortPath(s.cwd) : ""; 1491 1506 const cmd = s.command ?? "";
+75
tests/list-filters.test.ts
··· 306 306 expect(r.stdout).toContain("No matching sessions."); 307 307 }, 10000); 308 308 }); 309 + 310 + // Adding metadata files in a specific order and checking that `pty list` 311 + // sorts its output regardless of on-disk insertion order. Without an 312 + // explicit sort the output reflects readdir order (APFS insertion-ish), 313 + // which is near-meaningless when sessions come and go. 314 + function writeFakeMetadataWithDn( 315 + dir: string, 316 + name: string, 317 + opts: { createdAt: string; displayName?: string }, 318 + ) { 319 + const meta = { 320 + command: "cat", 321 + args: [], 322 + displayCommand: "cat", 323 + cwd: os.tmpdir(), 324 + createdAt: opts.createdAt, 325 + ...(opts.displayName ? { displayName: opts.displayName } : {}), 326 + }; 327 + fs.writeFileSync(path.join(dir, `${name}.json`), JSON.stringify(meta)); 328 + } 329 + 330 + describe("pty list — sort order", () => { 331 + it("JSON output is sorted ASCII by displayName, falling back to name", () => { 332 + const dir = makeSessionDir(); 333 + const now = new Date().toISOString(); 334 + 335 + // Intentionally create in non-alphabetical order to confirm the sort 336 + // isn't just insertion-order by accident. 337 + writeFakeMetadataWithDn(dir, "zzz-raw", { createdAt: now }); 338 + writeFakeMetadataWithDn(dir, "aaa-raw", { createdAt: now, displayName: "mmm-friendly" }); 339 + writeFakeMetadataWithDn(dir, "mmm-raw", { createdAt: now, displayName: "bbb-friendly" }); 340 + writeFakeMetadataWithDn(dir, "bbb-raw", { createdAt: now }); 341 + 342 + const r = runCli(dir, "list", "--json"); 343 + expect(r.status).toBe(0); 344 + const sessions = JSON.parse(r.stdout); 345 + // Expected sort keys: bbb-friendly, bbb-raw, mmm-friendly, zzz-raw 346 + const keys = sessions.map((s: any) => s.displayName ?? s.name); 347 + expect(keys).toEqual(["bbb-friendly", "bbb-raw", "mmm-friendly", "zzz-raw"]); 348 + }, 10000); 349 + 350 + it("text output renders grouped buckets in sorted order", () => { 351 + const dir = makeSessionDir(); 352 + const now = new Date().toISOString(); 353 + 354 + // All vanished (missing exitedAt/exitCode), so they land in a single 355 + // bucket and we can just scan for line order within it. 356 + writeFakeMetadataWithDn(dir, "z1", { createdAt: now }); 357 + writeFakeMetadataWithDn(dir, "a1", { createdAt: now }); 358 + writeFakeMetadataWithDn(dir, "m1", { createdAt: now }); 359 + 360 + const r = runCli(dir, "list"); 361 + expect(r.status).toBe(0); 362 + const ia = r.stdout.indexOf("a1"); 363 + const im = r.stdout.indexOf("m1"); 364 + const iz = r.stdout.indexOf("z1"); 365 + expect(ia).toBeGreaterThan(-1); 366 + expect(im).toBeGreaterThan(ia); 367 + expect(iz).toBeGreaterThan(im); 368 + }, 10000); 369 + 370 + it("displayName beats the stable id when sorting", () => { 371 + const dir = makeSessionDir(); 372 + const now = new Date().toISOString(); 373 + // id "aaa" but displayName "zebra" should sort AFTER id "mmm" with no 374 + // displayName — the displayName wins. 375 + writeFakeMetadataWithDn(dir, "aaa", { createdAt: now, displayName: "zebra" }); 376 + writeFakeMetadataWithDn(dir, "mmm", { createdAt: now }); 377 + 378 + const r = runCli(dir, "list", "--json"); 379 + const sessions = JSON.parse(r.stdout); 380 + const keys = sessions.map((s: any) => s.displayName ?? s.name); 381 + expect(keys).toEqual(["mmm", "zebra"]); 382 + }, 10000); 383 + });