Skip to content
Closed
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
Binary file added docs/assets/pagination-changes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/pagination-demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/pagination-tests.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 17 additions & 3 deletions packages/opencode/src/altimate/observability/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,11 @@ export class Recap {
return dir ?? DEFAULT_TRACES_DIR
}

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

traces.sort((a, b) => new Date(b.trace.startedAt).getTime() - new Date(a.trace.startedAt).getTime())
return traces

const total = traces.length
// Sanitize offset/limit: clamp to non-negative integers
const rawOffset = options?.offset ?? 0
const offset = Number.isFinite(rawOffset) ? Math.max(0, Math.floor(rawOffset)) : 0
const rawLimit = options?.limit
const limit = rawLimit != null && Number.isFinite(rawLimit) ? Math.max(0, Math.floor(rawLimit)) : undefined
const sliced = limit != null ? traces.slice(offset, offset + limit) : traces.slice(offset)

return { items: sliced, total }
} catch {
return []
return { items: [], total: 0 }
}
}
// altimate_change end

static async loadTrace(sessionId: string, dir?: string): Promise<TraceFile | null> {
const tracesDir = dir ?? DEFAULT_TRACES_DIR
Expand Down
56 changes: 45 additions & 11 deletions packages/opencode/src/cli/cmd/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,23 @@ function truncate(str: string, len: number): string {
return str.slice(0, len - 1) + "…"
}

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

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

UI.empty()
// altimate_change start — recap: renamed messages and Recap.getTracesDir
UI.println(UI.Style.TEXT_DIM + `${traces.length} recap(s) in ${Recap.getTracesDir(tracesDir)}` + UI.Style.TEXT_NORMAL)
// altimate_change start — recap: pagination-aware footer
if (pagination && pagination.total > 0) {
const start = pagination.offset + 1
const end = pagination.offset + traces.length
UI.println(UI.Style.TEXT_DIM + `Showing ${start}-${end} of ${pagination.total} recap(s) in ${Recap.getTracesDir(tracesDir)}` + UI.Style.TEXT_NORMAL)
if (pagination.offset + pagination.limit < pagination.total) {
const nextOffset = pagination.offset + pagination.limit
const limitFlag = pagination.limit !== 20 ? ` --limit ${pagination.limit}` : ""
UI.println(UI.Style.TEXT_DIM + `Next page: altimate-code recap list --offset ${nextOffset}${limitFlag}` + UI.Style.TEXT_NORMAL)
}
} else {
UI.println(UI.Style.TEXT_DIM + `${traces.length} recap(s) in ${Recap.getTracesDir(tracesDir)}` + UI.Style.TEXT_NORMAL)
}
UI.println(UI.Style.TEXT_DIM + "View a recap: altimate-code recap view <session-id>" + UI.Style.TEXT_NORMAL)
// altimate_change end
}
Expand Down Expand Up @@ -134,6 +155,13 @@ export const RecapCommand = cmd({
describe: "number of recaps to show",
default: 20,
})
// altimate_change start — recap: add offset option for pagination
.option("offset", {
type: "number",
describe: "number of recaps to skip (for pagination)",
default: 0,
})
// altimate_change end
.option("live", {
type: "boolean",
describe: "auto-refresh the viewer as the recap updates (for in-progress sessions)",
Expand All @@ -147,28 +175,34 @@ export const RecapCommand = cmd({
const cfg = await Config.get().catch(() => ({} as Record<string, any>))
const tracesDir = (cfg as any).tracing?.dir as string | undefined

// altimate_change start — recap: use paginated listTraces
if (action === "list") {
const traces = await Recap.listTraces(tracesDir)
listRecaps(traces.slice(0, args.limit || 20), tracesDir)
const limit = args.limit || 20
const offset = args.offset || 0
const { items, total } = await Recap.listTraces(tracesDir, { offset, limit })
listRecaps(items, tracesDir, { offset, limit, total })
return
}
// altimate_change end

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

// altimate_change start — recap: use paginated listTraces return type
// Support partial session ID matching
const traces = await Recap.listTraces(tracesDir)
const match = traces.find(
const { items: allTraces } = await Recap.listTraces(tracesDir)
const match = allTraces.find(
(t) => t.sessionId === args.id || t.sessionId.startsWith(args.id!) || t.file.startsWith(args.id!),
)

if (!match) {
UI.error(`Recap not found: ${args.id}`)
UI.println("Available recaps:")
listRecaps(traces.slice(0, 10), tracesDir)
listRecaps(allTraces.slice(0, 10), tracesDir)
// altimate_change end
process.exit(1)
}

Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ function getRecapViewerUrl(sessionID: string, tracesDir?: string): string {
const html = renderTraceViewer(trace, { live: true, apiPath: "/api/" + encodeURIComponent(sid) })
return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" } })
} catch {
return new Response("Trace not found. Try again after the agent responds.", { status: 404 })
// altimate_change start — recap: renamed error message
return new Response("Recap not found. Try again after the agent responds.", { status: 404 })
// altimate_change end
}
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ export function DialogRecapList(props: {
}) {
const dialog = useDialog()

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

Expand Down Expand Up @@ -61,7 +62,9 @@ export function DialogRecapList(props: {
})
}

result.push(...items.slice(0, 50).map((item) => {
// altimate_change start — recap: removed hardcoded slice(0,50) to show all recaps
result.push(...items.map((item) => {
// altimate_change end
const rawStartedAt = item.trace.startedAt
const parsedDate = typeof rawStartedAt === "string" || typeof rawStartedAt === "number"
? new Date(rawStartedAt)
Expand Down
6 changes: 3 additions & 3 deletions packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,14 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
<text fg={theme.textMuted}>{context()?.percentage ?? 0}% used</text>
<text fg={theme.textMuted}>{cost()} spent</text>
</box>
{/* altimate_change start - trace section */}
{/* altimate_change start - recap section */}
<box>
<text fg={theme.text}>
<b>Trace</b>
<b>Recap</b>
</text>
<text fg={theme.textMuted}>Every tool call, LLM request,</text>
<text fg={theme.textMuted}>and decision in a live view</text>
<text fg={theme.textMuted}>type <span style={{ fg: theme.accent }}>/trace</span> to open</text>
<text fg={theme.textMuted}>type <span style={{ fg: theme.accent }}>/recap</span> to open</text>
</box>
{/* altimate_change end */}
<Show when={mcpEntries().length > 0}>
Expand Down
Loading
Loading