Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions packages/opencode/src/cli/cmd/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -110,9 +111,15 @@ 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)
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 <session-id>" + UI.Style.TEXT_NORMAL)
// altimate_change end
Expand Down Expand Up @@ -154,6 +161,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)",
Expand All @@ -173,10 +185,12 @@ 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 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)
Comment on lines +188 to +192
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Normalize limit before converting page to offset.

Line 186 multiplies by the raw CLI value, but the comment above says Trace.listTracesPaginated() clamps invalid limits. That makes --page 2 --limit 0 compute offset = 0, then behave like page 1 after the downstream clamp to limit = 1. Compute the offset from the same sanitized limit you pass to pagination.

Suggested fix
-      const limit = args.limit ?? 20
+      const limit = Math.max(1, Math.trunc(args.limit ?? 20))
       const offset = args.page != null
         ? (Math.max(1, Math.trunc(args.page)) - 1) * limit
         : (args.offset ?? 0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/cli/cmd/trace.ts` around lines 184 - 187, Compute and
use a sanitized limit before calculating offset so the page->offset math matches
the downstream pagination clamp: replace the current two-step use of const limit
= args.limit ?? 20 and offset calculation with logic that first derives a
sanitizedLimit (apply the same clamping/validation you pass to
Trace.listTracesPaginated, e.g., ensure minimum 1 and default 20) and then
compute offset = args.page != null ? (Math.max(1, Math.trunc(args.page)) - 1) *
sanitizedLimit : (args.offset ?? 0); use sanitizedLimit wherever limit is passed
to Trace.listTracesPaginated/listing so offset and limit are consistent.

const page = await Trace.listTracesPaginated(tracesDir, { offset, limit })
listTraces(page.traces, page, tracesDir)
return
}
Expand Down
103 changes: 103 additions & 0 deletions packages/opencode/test/cli/trace-page-option.test.ts
Original file line number Diff line number Diff line change
@@ -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")
})
})
Loading