Skip to content

Commit 459a893

Browse files
anandgupta42claude
andcommitted
fix: recap pagination broken for large lists
- Add `offset`/`limit` params to `Recap.listTraces()`, return `{ items, total }` - Add `--offset` CLI option with pagination-aware footer - Remove hardcoded `slice(0, 50)` cap in TUI dialog (replaced with limit: 200) - Sanitize inputs: clamp negative/NaN/Infinity/float to safe integers - Fix empty-page message: show "offset past end" instead of "No recaps found" - Include `--limit` in next-page hint when non-default Closes #418 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent a738165 commit 459a893

6 files changed

Lines changed: 593 additions & 28 deletions

File tree

‎packages/opencode/src/altimate/observability/tracing.ts‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -957,7 +957,11 @@ export class Recap {
957957
return dir ?? DEFAULT_TRACES_DIR
958958
}
959959

960-
static async listTraces(dir?: string): Promise<Array<{ sessionId: string; file: string; trace: TraceFile }>> {
960+
// altimate_change start — recap: pagination support for listTraces
961+
static async listTraces(
962+
dir?: string,
963+
options?: { offset?: number; limit?: number },
964+
): Promise<{ items: Array<{ sessionId: string; file: string; trace: TraceFile }>; total: number }> {
961965
const tracesDir = dir ?? DEFAULT_TRACES_DIR
962966
try {
963967
await fs.mkdir(tracesDir, { recursive: true })
@@ -976,11 +980,21 @@ export class Recap {
976980
}
977981

978982
traces.sort((a, b) => new Date(b.trace.startedAt).getTime() - new Date(a.trace.startedAt).getTime())
979-
return traces
983+
984+
const total = traces.length
985+
// Sanitize offset/limit: clamp to non-negative integers
986+
const rawOffset = options?.offset ?? 0
987+
const offset = Number.isFinite(rawOffset) ? Math.max(0, Math.floor(rawOffset)) : 0
988+
const rawLimit = options?.limit
989+
const limit = rawLimit != null && Number.isFinite(rawLimit) ? Math.max(0, Math.floor(rawLimit)) : undefined
990+
const sliced = limit != null ? traces.slice(offset, offset + limit) : traces.slice(offset)
991+
992+
return { items: sliced, total }
980993
} catch {
981-
return []
994+
return { items: [], total: 0 }
982995
}
983996
}
997+
// altimate_change end
984998

985999
static async loadTrace(sessionId: string, dir?: string): Promise<TraceFile | null> {
9861000
const tracesDir = dir ?? DEFAULT_TRACES_DIR

‎packages/opencode/src/cli/cmd/trace.ts‎

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,23 @@ function truncate(str: string, len: number): string {
4848
return str.slice(0, len - 1) + "…"
4949
}
5050

51-
// altimate_change start — recap: rename listTraces → listRecaps
52-
function listRecaps(traces: Array<{ sessionId: string; trace: TraceFile }>, tracesDir?: string) {
51+
// altimate_change start — recap: rename listTraces → listRecaps, add pagination info
52+
function listRecaps(
53+
traces: Array<{ sessionId: string; trace: TraceFile }>,
54+
tracesDir?: string,
55+
pagination?: { offset: number; limit: number; total: number },
56+
) {
57+
// altimate_change start — recap: distinguish empty page from no recaps at all
5358
if (traces.length === 0) {
54-
UI.println("No recaps found. Run a command with tracing enabled:")
55-
UI.println(" altimate-code run \"your prompt here\"")
59+
if (pagination && pagination.total > 0) {
60+
UI.println(`No recaps on this page (offset ${pagination.offset}). Total: ${pagination.total} in ${Recap.getTracesDir(tracesDir)}`)
61+
} else {
62+
UI.println("No recaps found. Run a command with tracing enabled:")
63+
UI.println(" altimate-code run \"your prompt here\"")
64+
}
5665
return
5766
}
67+
// altimate_change end
5868

5969
// Header
6070
const header = [
@@ -97,8 +107,19 @@ function listRecaps(traces: Array<{ sessionId: string; trace: TraceFile }>, trac
97107
}
98108

99109
UI.empty()
100-
// altimate_change start — recap: renamed messages and Recap.getTracesDir
101-
UI.println(UI.Style.TEXT_DIM + `${traces.length} recap(s) in ${Recap.getTracesDir(tracesDir)}` + UI.Style.TEXT_NORMAL)
110+
// altimate_change start — recap: pagination-aware footer
111+
if (pagination && pagination.total > 0) {
112+
const start = pagination.offset + 1
113+
const end = pagination.offset + traces.length
114+
UI.println(UI.Style.TEXT_DIM + `Showing ${start}-${end} of ${pagination.total} recap(s) in ${Recap.getTracesDir(tracesDir)}` + UI.Style.TEXT_NORMAL)
115+
if (pagination.offset + pagination.limit < pagination.total) {
116+
const nextOffset = pagination.offset + pagination.limit
117+
const limitFlag = pagination.limit !== 20 ? ` --limit ${pagination.limit}` : ""
118+
UI.println(UI.Style.TEXT_DIM + `Next page: altimate-code recap list --offset ${nextOffset}${limitFlag}` + UI.Style.TEXT_NORMAL)
119+
}
120+
} else {
121+
UI.println(UI.Style.TEXT_DIM + `${traces.length} recap(s) in ${Recap.getTracesDir(tracesDir)}` + UI.Style.TEXT_NORMAL)
122+
}
102123
UI.println(UI.Style.TEXT_DIM + "View a recap: altimate-code recap view <session-id>" + UI.Style.TEXT_NORMAL)
103124
// altimate_change end
104125
}
@@ -134,6 +155,13 @@ export const RecapCommand = cmd({
134155
describe: "number of recaps to show",
135156
default: 20,
136157
})
158+
// altimate_change start — recap: add offset option for pagination
159+
.option("offset", {
160+
type: "number",
161+
describe: "number of recaps to skip (for pagination)",
162+
default: 0,
163+
})
164+
// altimate_change end
137165
.option("live", {
138166
type: "boolean",
139167
describe: "auto-refresh the viewer as the recap updates (for in-progress sessions)",
@@ -147,28 +175,34 @@ export const RecapCommand = cmd({
147175
const cfg = await Config.get().catch(() => ({} as Record<string, any>))
148176
const tracesDir = (cfg as any).tracing?.dir as string | undefined
149177

178+
// altimate_change start — recap: use paginated listTraces
150179
if (action === "list") {
151-
const traces = await Recap.listTraces(tracesDir)
152-
listRecaps(traces.slice(0, args.limit || 20), tracesDir)
180+
const limit = args.limit || 20
181+
const offset = args.offset || 0
182+
const { items, total } = await Recap.listTraces(tracesDir, { offset, limit })
183+
listRecaps(items, tracesDir, { offset, limit, total })
153184
return
154185
}
186+
// altimate_change end
155187

156188
if (action === "view") {
157189
if (!args.id) {
158190
UI.error("Usage: altimate-code recap view <session-id>")
159191
process.exit(1)
160192
}
161193

194+
// altimate_change start — recap: use paginated listTraces return type
162195
// Support partial session ID matching
163-
const traces = await Recap.listTraces(tracesDir)
164-
const match = traces.find(
196+
const { items: allTraces } = await Recap.listTraces(tracesDir)
197+
const match = allTraces.find(
165198
(t) => t.sessionId === args.id || t.sessionId.startsWith(args.id!) || t.file.startsWith(args.id!),
166199
)
167200

168201
if (!match) {
169202
UI.error(`Recap not found: ${args.id}`)
170203
UI.println("Available recaps:")
171-
listRecaps(traces.slice(0, 10), tracesDir)
204+
listRecaps(allTraces.slice(0, 10), tracesDir)
205+
// altimate_change end
172206
process.exit(1)
173207
}
174208

‎packages/opencode/src/cli/cmd/tui/component/dialog-trace-list.tsx‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ export function DialogRecapList(props: {
2828
}) {
2929
const dialog = useDialog()
3030

31-
// altimate_change start — recap: use Recap.listTraces
31+
// altimate_change start — recap: use Recap.listTraces with pagination (cap at 200 for UI perf)
3232
const [traces] = createResource(async () => {
33-
return Recap.listTraces(props.tracesDir)
33+
const { items } = await Recap.listTraces(props.tracesDir, { limit: 200 })
34+
return items
3435
})
3536
// altimate_change end
3637

@@ -61,7 +62,9 @@ export function DialogRecapList(props: {
6162
})
6263
}
6364

64-
result.push(...items.slice(0, 50).map((item) => {
65+
// altimate_change start — recap: removed hardcoded slice(0,50) to show all recaps
66+
result.push(...items.map((item) => {
67+
// altimate_change end
6568
const rawStartedAt = item.trace.startedAt
6669
const parsedDate = typeof rawStartedAt === "string" || typeof rawStartedAt === "number"
6770
? new Date(rawStartedAt)

0 commit comments

Comments
 (0)