From f6cf3240a1915488407807973ff83264bd7887c9 Mon Sep 17 00:00:00 2001 From: Vijay Yadav Date: Mon, 13 Apr 2026 21:11:00 -0400 Subject: [PATCH 1/2] feat: add --page convenience option for trace list CLI (closes #669) The pagination UX previously required users to manually calculate offsets (--offset 20 --limit 20, then --offset 40 --limit 20). This adds --page N (-p) which computes the offset automatically: offset = (page - 1) * limit. - --page takes precedence over --offset when both are provided - Invalid values (0, negative, fractional) are clamped/truncated - Footer now shows "page X/Y" and "Next page: --page N" hints - 14 unit tests covering conversion logic and footer formatting --- packages/opencode/src/cli/cmd/trace.ts | 23 ++-- .../test/cli/trace-page-option.test.ts | 103 ++++++++++++++++++ 2 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 packages/opencode/test/cli/trace-page-option.test.ts diff --git a/packages/opencode/src/cli/cmd/trace.ts b/packages/opencode/src/cli/cmd/trace.ts index faf1e39f2..a5c36a91e 100644 --- a/packages/opencode/src/cli/cmd/trace.ts +++ b/packages/opencode/src/cli/cmd/trace.ts @@ -61,8 +61,9 @@ function listTraces( } if (traces.length === 0 && pagination.total > 0) { + const totalPages = Math.ceil(pagination.total / pagination.limit) UI.println(`No traces on this page (offset ${pagination.offset} past end of ${pagination.total} traces).`) - UI.println(UI.Style.TEXT_DIM + `Try: altimate-code trace list --offset 0 --limit ${pagination.limit}` + UI.Style.TEXT_NORMAL) + UI.println(UI.Style.TEXT_DIM + `Try: altimate-code trace list --page 1 (${totalPages} page(s) available)` + UI.Style.TEXT_NORMAL) return } @@ -110,9 +111,11 @@ function listTraces( // altimate_change start — trace: session trace messages with pagination footer const rangeStart = pagination.offset + 1 const rangeEnd = pagination.offset + traces.length - UI.println(UI.Style.TEXT_DIM + `Showing ${rangeStart}-${rangeEnd} of ${pagination.total} trace(s) in ${Trace.getTracesDir(tracesDir)}` + UI.Style.TEXT_NORMAL) + const currentPage = Math.floor(pagination.offset / pagination.limit) + 1 + const totalPages = Math.ceil(pagination.total / pagination.limit) + UI.println(UI.Style.TEXT_DIM + `Showing ${rangeStart}-${rangeEnd} of ${pagination.total} trace(s) (page ${currentPage}/${totalPages}) in ${Trace.getTracesDir(tracesDir)}` + UI.Style.TEXT_NORMAL) if (rangeEnd < pagination.total) { - UI.println(UI.Style.TEXT_DIM + `Next page: altimate-code trace list --offset ${rangeEnd} --limit ${pagination.limit}` + UI.Style.TEXT_NORMAL) + UI.println(UI.Style.TEXT_DIM + `Next page: altimate-code trace list --page ${currentPage + 1}` + UI.Style.TEXT_NORMAL) } UI.println(UI.Style.TEXT_DIM + "View a trace: altimate-code trace view " + UI.Style.TEXT_NORMAL) // altimate_change end @@ -154,6 +157,11 @@ export const TraceCommand = cmd({ describe: "number of traces to skip (for pagination)", default: 0, }) + .option("page", { + alias: ["p"], + type: "number", + describe: "page number (1-based, converts to offset automatically)", + }) .option("live", { type: "boolean", describe: "auto-refresh the viewer as the trace updates (for in-progress sessions)", @@ -173,10 +181,11 @@ export const TraceCommand = cmd({ // treat `--offset 0` as unset (no semantic change, harmless), but // `args.limit || 20` would promote `--limit 0` to 20 instead of // letting the API clamp it to 1. - const page = await Trace.listTracesPaginated(tracesDir, { - offset: args.offset ?? 0, - limit: args.limit ?? 20, - }) + const limit = args.limit ?? 20 + const offset = args.page != null + ? (Math.max(1, Math.trunc(args.page)) - 1) * limit + : (args.offset ?? 0) + const page = await Trace.listTracesPaginated(tracesDir, { offset, limit }) listTraces(page.traces, page, tracesDir) return } diff --git a/packages/opencode/test/cli/trace-page-option.test.ts b/packages/opencode/test/cli/trace-page-option.test.ts new file mode 100644 index 000000000..651fcef66 --- /dev/null +++ b/packages/opencode/test/cli/trace-page-option.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, test } from "bun:test" + +/** + * Tests for the --page convenience option added to `trace list`. + * + * The page-to-offset conversion lives inline in the CLI handler, so we test + * the formula directly here rather than spawning a subprocess. + */ + +// --- page-to-offset conversion (mirrors the handler logic) --- + +function pageToOffset(page: number | undefined, offset: number, limit: number): number { + if (page != null) { + return (Math.max(1, Math.trunc(page)) - 1) * limit + } + return offset +} + +describe("trace list --page to offset conversion", () => { + test("page 1 maps to offset 0", () => { + expect(pageToOffset(1, 0, 20)).toBe(0) + }) + + test("page 2 with default limit maps to offset 20", () => { + expect(pageToOffset(2, 0, 20)).toBe(20) + }) + + test("page 3 with limit 10 maps to offset 20", () => { + expect(pageToOffset(3, 0, 10)).toBe(20) + }) + + test("page 5 with limit 5 maps to offset 20", () => { + expect(pageToOffset(5, 0, 5)).toBe(20) + }) + + test("page takes precedence over explicit offset", () => { + // --page 2 --offset 99 → page wins + expect(pageToOffset(2, 99, 20)).toBe(20) + }) + + test("undefined page falls back to provided offset", () => { + expect(pageToOffset(undefined, 40, 20)).toBe(40) + }) + + test("page 0 is clamped to page 1 (offset 0)", () => { + expect(pageToOffset(0, 0, 20)).toBe(0) + }) + + test("negative page is clamped to page 1 (offset 0)", () => { + expect(pageToOffset(-5, 0, 20)).toBe(0) + }) + + test("fractional page is truncated", () => { + // page 2.7 → trunc to 2 → offset 20 + expect(pageToOffset(2.7, 0, 20)).toBe(20) + }) +}) + +// --- pagination footer formatting (mirrors listTraces footer) --- + +function formatFooter(offset: number, limit: number, total: number, shown: number) { + const rangeStart = offset + 1 + const rangeEnd = offset + shown + const currentPage = Math.floor(offset / limit) + 1 + const totalPages = Math.ceil(total / limit) + const summary = `Showing ${rangeStart}-${rangeEnd} of ${total} trace(s) (page ${currentPage}/${totalPages})` + const nextHint = rangeEnd < total + ? `Next page: altimate-code trace list --page ${currentPage + 1}` + : null + return { summary, nextHint } +} + +describe("trace list pagination footer", () => { + test("first page of multiple shows page 1/N and next hint", () => { + const { summary, nextHint } = formatFooter(0, 20, 50, 20) + expect(summary).toBe("Showing 1-20 of 50 trace(s) (page 1/3)") + expect(nextHint).toBe("Next page: altimate-code trace list --page 2") + }) + + test("middle page shows correct range and next hint", () => { + const { summary, nextHint } = formatFooter(20, 20, 50, 20) + expect(summary).toBe("Showing 21-40 of 50 trace(s) (page 2/3)") + expect(nextHint).toBe("Next page: altimate-code trace list --page 3") + }) + + test("last page has no next hint", () => { + const { summary, nextHint } = formatFooter(40, 20, 50, 10) + expect(summary).toBe("Showing 41-50 of 50 trace(s) (page 3/3)") + expect(nextHint).toBeNull() + }) + + test("single page has no next hint", () => { + const { summary, nextHint } = formatFooter(0, 20, 5, 5) + expect(summary).toBe("Showing 1-5 of 5 trace(s) (page 1/1)") + expect(nextHint).toBeNull() + }) + + test("custom limit changes page calculation", () => { + const { summary, nextHint } = formatFooter(10, 10, 35, 10) + expect(summary).toBe("Showing 11-20 of 35 trace(s) (page 2/4)") + expect(nextHint).toBe("Next page: altimate-code trace list --page 3") + }) +}) From 779496a2dd2071400eca1c6608dd2aeadb13798f Mon Sep 17 00:00:00 2001 From: Vijay Yadav Date: Mon, 13 Apr 2026 21:52:48 -0400 Subject: [PATCH 2/2] fix: guard against NaN --page and preserve --limit in next-page hint Address PR review feedback: - Guard against NaN in --page arg (e.g. --page foo) using Number.isFinite() - Include --limit in next-page hint so custom limits are preserved - Fall back to offset-based hint when offset isn't page-aligned --- packages/opencode/src/cli/cmd/trace.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/trace.ts b/packages/opencode/src/cli/cmd/trace.ts index a5c36a91e..4abd19d2f 100644 --- a/packages/opencode/src/cli/cmd/trace.ts +++ b/packages/opencode/src/cli/cmd/trace.ts @@ -115,7 +115,11 @@ function listTraces( const totalPages = Math.ceil(pagination.total / pagination.limit) UI.println(UI.Style.TEXT_DIM + `Showing ${rangeStart}-${rangeEnd} of ${pagination.total} trace(s) (page ${currentPage}/${totalPages}) in ${Trace.getTracesDir(tracesDir)}` + UI.Style.TEXT_NORMAL) if (rangeEnd < pagination.total) { - UI.println(UI.Style.TEXT_DIM + `Next page: altimate-code trace list --page ${currentPage + 1}` + UI.Style.TEXT_NORMAL) + const isPageAligned = pagination.offset % pagination.limit === 0 + const nextHint = isPageAligned + ? `altimate-code trace list --page ${currentPage + 1} --limit ${pagination.limit}` + : `altimate-code trace list --offset ${rangeEnd} --limit ${pagination.limit}` + UI.println(UI.Style.TEXT_DIM + `Next page: ${nextHint}` + UI.Style.TEXT_NORMAL) } UI.println(UI.Style.TEXT_DIM + "View a trace: altimate-code trace view " + UI.Style.TEXT_NORMAL) // altimate_change end @@ -182,8 +186,9 @@ export const TraceCommand = cmd({ // `args.limit || 20` would promote `--limit 0` to 20 instead of // letting the API clamp it to 1. const limit = args.limit ?? 20 - const offset = args.page != null - ? (Math.max(1, Math.trunc(args.page)) - 1) * limit + const rawPage = args.page != null ? args.page : undefined + const offset = rawPage != null && Number.isFinite(rawPage) + ? (Math.max(1, Math.trunc(rawPage)) - 1) * limit : (args.offset ?? 0) const page = await Trace.listTracesPaginated(tracesDir, { offset, limit }) listTraces(page.traces, page, tracesDir)