From 2d84c181aa363e1636603fe3f11e0628f77e393a Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Wed, 1 Apr 2026 11:49:46 +0200 Subject: [PATCH 1/7] fix(skill): cap profile timeline example with --limit in Essential Commands Uncapped `profile timeline` can dump 300+ lines for a typical session. Add `--limit 5` to the example and a comment flagging the risk, consistent with how `profile slow` and `profile rerenders` are already shown with limits. Co-Authored-By: Claude Sonnet 4.6 --- packages/agent-react-devtools/skills/react-devtools/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agent-react-devtools/skills/react-devtools/SKILL.md b/packages/agent-react-devtools/skills/react-devtools/SKILL.md index f740c0c..77025a4 100644 --- a/packages/agent-react-devtools/skills/react-devtools/SKILL.md +++ b/packages/agent-react-devtools/skills/react-devtools/SKILL.md @@ -48,7 +48,7 @@ agent-react-devtools profile slow # Slowest components by avg rend agent-react-devtools profile slow --limit 10 # Top 10 agent-react-devtools profile rerenders # Most re-rendered components agent-react-devtools profile report @c5 # Detailed report for one component -agent-react-devtools profile timeline # Chronological commit list +agent-react-devtools profile timeline --limit 5 # Chronological commit list (use --limit; uncapped can dump 300+ lines) agent-react-devtools profile commit 3 # Detail for commit #3 agent-react-devtools profile export profile.json # Export as React DevTools Profiler JSON agent-react-devtools profile diff before.json after.json # Compare two exports From 2dccf5ffbb971be61162ba08cede9b042e6f4ca2 Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Wed, 1 Apr 2026 11:53:52 +0200 Subject: [PATCH 2/7] feat(profile): add --sort duration|timeline to profile timeline Adds a --sort flag to `profile timeline`: - --sort duration: sorts commits by duration descending before applying --limit, so the top N most expensive commits surface immediately - --sort timeline: explicit chronological order (same as the default) Default behavior (no --sort) remains chronological, unchanged. Co-Authored-By: Claude Sonnet 4.6 --- .../agent-react-devtools/skills/react-devtools/SKILL.md | 4 +++- .../skills/react-devtools/references/commands.md | 4 +++- packages/agent-react-devtools/src/cli.ts | 6 +++++- packages/agent-react-devtools/src/daemon.ts | 2 +- packages/agent-react-devtools/src/profiler.ts | 3 ++- packages/agent-react-devtools/src/types.ts | 2 +- 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/agent-react-devtools/skills/react-devtools/SKILL.md b/packages/agent-react-devtools/skills/react-devtools/SKILL.md index 77025a4..372abe6 100644 --- a/packages/agent-react-devtools/skills/react-devtools/SKILL.md +++ b/packages/agent-react-devtools/skills/react-devtools/SKILL.md @@ -48,7 +48,9 @@ agent-react-devtools profile slow # Slowest components by avg rend agent-react-devtools profile slow --limit 10 # Top 10 agent-react-devtools profile rerenders # Most re-rendered components agent-react-devtools profile report @c5 # Detailed report for one component -agent-react-devtools profile timeline --limit 5 # Chronological commit list (use --limit; uncapped can dump 300+ lines) +agent-react-devtools profile timeline --limit 5 # Chronological commit list (use --limit; uncapped can dump 300+ lines) +agent-react-devtools profile timeline --sort duration --limit 5 # Top 5 most expensive commits +agent-react-devtools profile timeline --sort timeline --limit 5 # Explicit chronological order (same as default) agent-react-devtools profile commit 3 # Detail for commit #3 agent-react-devtools profile export profile.json # Export as React DevTools Profiler JSON agent-react-devtools profile diff before.json after.json # Compare two exports diff --git a/packages/agent-react-devtools/skills/react-devtools/references/commands.md b/packages/agent-react-devtools/skills/react-devtools/references/commands.md index a48407b..01ab860 100644 --- a/packages/agent-react-devtools/skills/react-devtools/references/commands.md +++ b/packages/agent-react-devtools/skills/react-devtools/references/commands.md @@ -88,9 +88,11 @@ Output columns: label, type tag, component name, render count, all causes, chang ### `agent-react-devtools profile report <@cN | id>` Detailed render report for a single component: render count, avg/max/total duration, all render causes, changed keys. -### `agent-react-devtools profile timeline [--limit N]` +### `agent-react-devtools profile timeline [--limit N] [--sort duration|timeline]` Chronological list of React commits during the profiling session. Each entry: index, duration, component count. +Default order is chronological (timeline order). `--sort duration` re-orders entries by duration descending (most expensive first) before applying `--limit` — use this to find the heaviest commits. `--sort timeline` explicitly requests chronological order (same as the default). + ### `agent-react-devtools profile commit [--limit N]` Detail for a specific commit by index. Shows per-component self/total duration, render causes, and changed keys. diff --git a/packages/agent-react-devtools/src/cli.ts b/packages/agent-react-devtools/src/cli.ts index ed9279a..eaacabf 100644 --- a/packages/agent-react-devtools/src/cli.ts +++ b/packages/agent-react-devtools/src/cli.ts @@ -402,7 +402,11 @@ async function main(): Promise { if (cmd0 === 'profile' && cmd1 === 'timeline') { const limit = parseNumericFlag(flags, 'limit'); - const resp = await sendCommand({ type: 'profile-timeline', limit }); + const sortFlag = flags['sort']; + const sort = sortFlag === 'duration' ? 'duration' as const + : sortFlag === 'timeline' ? 'timeline' as const + : undefined; + const resp = await sendCommand({ type: 'profile-timeline', limit, sort }); if (resp.ok) { console.log(formatTimeline(resp.data as any)); } else { diff --git a/packages/agent-react-devtools/src/daemon.ts b/packages/agent-react-devtools/src/daemon.ts index a374d6f..6b0bd79 100644 --- a/packages/agent-react-devtools/src/daemon.ts +++ b/packages/agent-react-devtools/src/daemon.ts @@ -262,7 +262,7 @@ class Daemon { case 'profile-timeline': return { ok: true, - data: this.profiler.getTimeline(cmd.limit), + data: this.profiler.getTimeline(cmd.limit, cmd.sort), }; case 'profile-commit': { diff --git a/packages/agent-react-devtools/src/profiler.ts b/packages/agent-react-devtools/src/profiler.ts index 3fb9704..0cf52b1 100644 --- a/packages/agent-react-devtools/src/profiler.ts +++ b/packages/agent-react-devtools/src/profiler.ts @@ -307,7 +307,7 @@ export class Profiler { }; } - getTimeline(limit?: number): TimelineEntry[] { + getTimeline(limit?: number, sort?: 'duration' | 'timeline'): TimelineEntry[] { if (!this.session) return []; const entries = this.session.commits.map((commit, index) => ({ @@ -317,6 +317,7 @@ export class Profiler { componentCount: commit.fiberActualDurations.size, })); + if (sort === 'duration') entries.sort((a, b) => b.duration - a.duration); if (limit) return entries.slice(0, limit); return entries; } diff --git a/packages/agent-react-devtools/src/types.ts b/packages/agent-react-devtools/src/types.ts index 89af69c..96f29e3 100644 --- a/packages/agent-react-devtools/src/types.ts +++ b/packages/agent-react-devtools/src/types.ts @@ -189,7 +189,7 @@ export type IpcCommand = | { type: 'profile-report'; componentId: number | string } | { type: 'profile-slow'; limit?: number } | { type: 'profile-rerenders'; limit?: number } - | { type: 'profile-timeline'; limit?: number } + | { type: 'profile-timeline'; limit?: number; sort?: 'duration' | 'timeline' } | { type: 'profile-commit'; index: number; limit?: number } | { type: 'profile-export' } | { type: 'errors' } From be2bc1ae4e471beaef0109b6f323d975f426a9bb Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Wed, 1 Apr 2026 11:58:52 +0200 Subject: [PATCH 3/7] feat(profile): add --offset to profile timeline Allows skipping the first N entries (after sorting), enabling pagination through long sessions and skipping known-good warm-up commits. profile timeline --limit 20 # commits 0-19 profile timeline --limit 20 --offset 20 # commits 20-39 profile timeline --offset 30 --limit 10 # skip warm-up Co-Authored-By: Claude Sonnet 4.6 --- .../skills/react-devtools/SKILL.md | 17 ++++++++++++++--- .../react-devtools/references/commands.md | 4 +++- packages/agent-react-devtools/src/cli.ts | 3 ++- packages/agent-react-devtools/src/daemon.ts | 2 +- packages/agent-react-devtools/src/profiler.ts | 7 ++++--- packages/agent-react-devtools/src/types.ts | 2 +- 6 files changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/agent-react-devtools/skills/react-devtools/SKILL.md b/packages/agent-react-devtools/skills/react-devtools/SKILL.md index 372abe6..af5896c 100644 --- a/packages/agent-react-devtools/skills/react-devtools/SKILL.md +++ b/packages/agent-react-devtools/skills/react-devtools/SKILL.md @@ -48,9 +48,10 @@ agent-react-devtools profile slow # Slowest components by avg rend agent-react-devtools profile slow --limit 10 # Top 10 agent-react-devtools profile rerenders # Most re-rendered components agent-react-devtools profile report @c5 # Detailed report for one component -agent-react-devtools profile timeline --limit 5 # Chronological commit list (use --limit; uncapped can dump 300+ lines) -agent-react-devtools profile timeline --sort duration --limit 5 # Top 5 most expensive commits -agent-react-devtools profile timeline --sort timeline --limit 5 # Explicit chronological order (same as default) +agent-react-devtools profile timeline --limit 10 # First 10 commits (use --limit; uncapped can dump 300+ lines) +agent-react-devtools profile timeline --limit 10 --offset 10 # Next 10 (pagination) +agent-react-devtools profile timeline --sort duration --limit 5 # Top 5 most expensive commits +agent-react-devtools profile timeline --sort timeline --limit 5 # Explicit chronological order (same as default) agent-react-devtools profile commit 3 # Detail for commit #3 agent-react-devtools profile export profile.json # Export as React DevTools Profiler JSON agent-react-devtools profile diff before.json after.json # Compare two exports @@ -127,6 +128,16 @@ agent-react-devtools profile rerenders --limit 5 Then inspect the worst offenders with `get component @cN` and `profile report @cN`. +### Browse a long timeline in chunks + +```bash +agent-react-devtools profile timeline --limit 20 # commits 0–19 +agent-react-devtools profile timeline --limit 20 --offset 20 # commits 20–39 +agent-react-devtools profile timeline --offset 30 --limit 10 # skip warm-up, show 30–39 +``` + +Use `profile commit ` to drill into a specific commit once you spot a spike. + ### Find a component and check its state ```bash diff --git a/packages/agent-react-devtools/skills/react-devtools/references/commands.md b/packages/agent-react-devtools/skills/react-devtools/references/commands.md index 01ab860..1fbcfec 100644 --- a/packages/agent-react-devtools/skills/react-devtools/references/commands.md +++ b/packages/agent-react-devtools/skills/react-devtools/references/commands.md @@ -88,11 +88,13 @@ Output columns: label, type tag, component name, render count, all causes, chang ### `agent-react-devtools profile report <@cN | id>` Detailed render report for a single component: render count, avg/max/total duration, all render causes, changed keys. -### `agent-react-devtools profile timeline [--limit N] [--sort duration|timeline]` +### `agent-react-devtools profile timeline [--limit N] [--offset N] [--sort duration|timeline]` Chronological list of React commits during the profiling session. Each entry: index, duration, component count. Default order is chronological (timeline order). `--sort duration` re-orders entries by duration descending (most expensive first) before applying `--limit` — use this to find the heaviest commits. `--sort timeline` explicitly requests chronological order (same as the default). +`--offset N` skips the first N entries (after sorting). Use with `--limit` to page through commits or skip a known-good warm-up region. + ### `agent-react-devtools profile commit [--limit N]` Detail for a specific commit by index. Shows per-component self/total duration, render causes, and changed keys. diff --git a/packages/agent-react-devtools/src/cli.ts b/packages/agent-react-devtools/src/cli.ts index eaacabf..fdc5210 100644 --- a/packages/agent-react-devtools/src/cli.ts +++ b/packages/agent-react-devtools/src/cli.ts @@ -402,11 +402,12 @@ async function main(): Promise { if (cmd0 === 'profile' && cmd1 === 'timeline') { const limit = parseNumericFlag(flags, 'limit'); + const offset = parseNumericFlag(flags, 'offset'); const sortFlag = flags['sort']; const sort = sortFlag === 'duration' ? 'duration' as const : sortFlag === 'timeline' ? 'timeline' as const : undefined; - const resp = await sendCommand({ type: 'profile-timeline', limit, sort }); + const resp = await sendCommand({ type: 'profile-timeline', limit, offset, sort }); if (resp.ok) { console.log(formatTimeline(resp.data as any)); } else { diff --git a/packages/agent-react-devtools/src/daemon.ts b/packages/agent-react-devtools/src/daemon.ts index 6b0bd79..6eea6a2 100644 --- a/packages/agent-react-devtools/src/daemon.ts +++ b/packages/agent-react-devtools/src/daemon.ts @@ -262,7 +262,7 @@ class Daemon { case 'profile-timeline': return { ok: true, - data: this.profiler.getTimeline(cmd.limit, cmd.sort), + data: this.profiler.getTimeline(cmd.limit, cmd.offset, cmd.sort), }; case 'profile-commit': { diff --git a/packages/agent-react-devtools/src/profiler.ts b/packages/agent-react-devtools/src/profiler.ts index 0cf52b1..7695386 100644 --- a/packages/agent-react-devtools/src/profiler.ts +++ b/packages/agent-react-devtools/src/profiler.ts @@ -307,7 +307,7 @@ export class Profiler { }; } - getTimeline(limit?: number, sort?: 'duration' | 'timeline'): TimelineEntry[] { + getTimeline(limit?: number, offset?: number, sort?: 'duration' | 'timeline'): TimelineEntry[] { if (!this.session) return []; const entries = this.session.commits.map((commit, index) => ({ @@ -318,8 +318,9 @@ export class Profiler { })); if (sort === 'duration') entries.sort((a, b) => b.duration - a.duration); - if (limit) return entries.slice(0, limit); - return entries; + const start = offset ?? 0; + if (limit) return entries.slice(start, start + limit); + return offset ? entries.slice(start) : entries; } getExportData(tree: ComponentTree): ProfilingDataExport | null { diff --git a/packages/agent-react-devtools/src/types.ts b/packages/agent-react-devtools/src/types.ts index 96f29e3..bdc2fe0 100644 --- a/packages/agent-react-devtools/src/types.ts +++ b/packages/agent-react-devtools/src/types.ts @@ -189,7 +189,7 @@ export type IpcCommand = | { type: 'profile-report'; componentId: number | string } | { type: 'profile-slow'; limit?: number } | { type: 'profile-rerenders'; limit?: number } - | { type: 'profile-timeline'; limit?: number; sort?: 'duration' | 'timeline' } + | { type: 'profile-timeline'; limit?: number; offset?: number; sort?: 'duration' | 'timeline' } | { type: 'profile-commit'; index: number; limit?: number } | { type: 'profile-export' } | { type: 'errors' } From 6669d2e22209e82be8857137ebb4bc876c0d72e3 Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Wed, 1 Apr 2026 12:07:06 +0200 Subject: [PATCH 4/7] feat(profile): default limit 20 and total count header for profile timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Default limit changed from unlimited to 20, consistent with profile slow and profile rerenders (both default to 10; timeline entries are more compact so 20 is a natural page size) - Output header now shows total commit count and range: Commit timeline (showing 1–20 of 87): When all commits fit: Commit timeline (42 commits): - Introduces TimelineResult { entries, total, offset } returned by getTimeline() Co-Authored-By: Claude Sonnet 4.6 --- .../react-devtools/references/commands.md | 4 +- .../src/__tests__/formatters.test.ts | 59 +++++++++++++++---- .../agent-react-devtools/src/formatters.ts | 18 ++++-- packages/agent-react-devtools/src/profiler.ts | 21 +++++-- 4 files changed, 80 insertions(+), 22 deletions(-) diff --git a/packages/agent-react-devtools/skills/react-devtools/references/commands.md b/packages/agent-react-devtools/skills/react-devtools/references/commands.md index 1fbcfec..70a7a2a 100644 --- a/packages/agent-react-devtools/skills/react-devtools/references/commands.md +++ b/packages/agent-react-devtools/skills/react-devtools/references/commands.md @@ -89,7 +89,9 @@ Output columns: label, type tag, component name, render count, all causes, chang Detailed render report for a single component: render count, avg/max/total duration, all render causes, changed keys. ### `agent-react-devtools profile timeline [--limit N] [--offset N] [--sort duration|timeline]` -Chronological list of React commits during the profiling session. Each entry: index, duration, component count. +Chronological list of React commits during the profiling session. Each entry: index, duration, component count. Default limit: 20. + +The header shows how many commits were returned and the total: `Commit timeline (showing 1–20 of 87):`. When all commits fit, it shows `Commit timeline (42 commits):`. Default order is chronological (timeline order). `--sort duration` re-orders entries by duration descending (most expensive first) before applying `--limit` — use this to find the heaviest commits. `--sort timeline` explicitly requests chronological order (same as the default). diff --git a/packages/agent-react-devtools/src/__tests__/formatters.test.ts b/packages/agent-react-devtools/src/__tests__/formatters.test.ts index 39f4cb2..179cf27 100644 --- a/packages/agent-react-devtools/src/__tests__/formatters.test.ts +++ b/packages/agent-react-devtools/src/__tests__/formatters.test.ts @@ -17,7 +17,7 @@ import { } from '../formatters.js'; import type { TreeNode } from '../component-tree.js'; import type { InspectedElement, StatusInfo, ComponentRenderReport, ConnectionHealth, ChangedKeys } from '../types.js'; -import type { ProfileSummary, TimelineEntry, CommitDetail } from '../profiler.js'; +import type { ProfileSummary, TimelineResult, CommitDetail } from '../profiler.js'; describe('formatTree', () => { it('should format empty tree', () => { @@ -356,17 +356,54 @@ describe('formatRerenders', () => { }); describe('formatTimeline', () => { - it('should format timeline entries', () => { - const entries: TimelineEntry[] = [ - { index: 0, timestamp: 1000, duration: 12.5, componentCount: 5 }, - { index: 1, timestamp: 2000, duration: 8.3, componentCount: 3 }, - ]; + it('should format timeline entries showing all commits', () => { + const result: TimelineResult = { + entries: [ + { index: 0, timestamp: 1000, duration: 12.5, componentCount: 5 }, + { index: 1, timestamp: 2000, duration: 8.3, componentCount: 3 }, + ], + total: 2, + offset: 0, + }; + + const output = formatTimeline(result); + expect(output).toContain('2 commits'); + expect(output).toContain('#0'); + expect(output).toContain('12.5ms'); + expect(output).toContain('#1'); + expect(output).toContain('8.3ms'); + }); + + it('should show pagination info when limited', () => { + const result: TimelineResult = { + entries: [ + { index: 0, timestamp: 1000, duration: 12.5, componentCount: 5 }, + { index: 1, timestamp: 2000, duration: 8.3, componentCount: 3 }, + ], + total: 50, + offset: 0, + }; + + const output = formatTimeline(result); + expect(output).toContain('showing 1–2 of 50'); + }); + + it('should show correct range with offset', () => { + const result: TimelineResult = { + entries: [ + { index: 20, timestamp: 1000, duration: 5.0, componentCount: 2 }, + ], + total: 50, + offset: 20, + }; + + const output = formatTimeline(result); + expect(output).toContain('showing 21–21 of 50'); + }); - const result = formatTimeline(entries); - expect(result).toContain('#0'); - expect(result).toContain('12.5ms'); - expect(result).toContain('#1'); - expect(result).toContain('8.3ms'); + it('should return no data message when empty', () => { + const result: TimelineResult = { entries: [], total: 0, offset: 0 }; + expect(formatTimeline(result)).toBe('No profiling data'); }); }); diff --git a/packages/agent-react-devtools/src/formatters.ts b/packages/agent-react-devtools/src/formatters.ts index 63b5d81..82e05d4 100644 --- a/packages/agent-react-devtools/src/formatters.ts +++ b/packages/agent-react-devtools/src/formatters.ts @@ -5,7 +5,7 @@ import type { ChangedKeys, } from './types.js'; import type { TreeNode } from './component-tree.js'; -import type { ProfileSummary, TimelineEntry, CommitDetail } from './profiler.js'; +import type { ProfileSummary, TimelineResult, CommitDetail } from './profiler.js'; import type { ProfileDiffResult, DiffEntry } from './profile-diff.js'; // ── Abbreviations for component types ── @@ -261,10 +261,20 @@ export function formatRerenders(reports: ComponentRenderReport[]): string { return lines.join('\n'); } -export function formatTimeline(entries: TimelineEntry[]): string { - if (entries.length === 0) return 'No profiling data'; +export function formatTimeline(result: TimelineResult): string { + if (result.total === 0) return 'No profiling data'; + + const { entries, total, offset } = result; + let header: string; + if (entries.length === total) { + header = `Commit timeline (${total} commits):`; + } else { + const from = offset + 1; + const to = offset + entries.length; + header = `Commit timeline (showing ${from}–${to} of ${total}):`; + } - const lines: string[] = ['Commit timeline:']; + const lines: string[] = [header]; for (const e of entries) { lines.push( ` #${e.index} ${e.duration.toFixed(1)}ms ${e.componentCount} components`, diff --git a/packages/agent-react-devtools/src/profiler.ts b/packages/agent-react-devtools/src/profiler.ts index 7695386..18e3679 100644 --- a/packages/agent-react-devtools/src/profiler.ts +++ b/packages/agent-react-devtools/src/profiler.ts @@ -24,6 +24,12 @@ export interface TimelineEntry { componentCount: number; } +export interface TimelineResult { + entries: TimelineEntry[]; + total: number; + offset: number; +} + export interface CommitDetail { index: number; timestamp: number; @@ -307,20 +313,23 @@ export class Profiler { }; } - getTimeline(limit?: number, offset?: number, sort?: 'duration' | 'timeline'): TimelineEntry[] { - if (!this.session) return []; + getTimeline(limit: number = 20, offset?: number, sort?: 'duration' | 'timeline'): TimelineResult { + if (!this.session) return { entries: [], total: 0, offset: offset ?? 0 }; - const entries = this.session.commits.map((commit, index) => ({ + const all = this.session.commits.map((commit, index) => ({ index, timestamp: commit.timestamp, duration: commit.duration, componentCount: commit.fiberActualDurations.size, })); - if (sort === 'duration') entries.sort((a, b) => b.duration - a.duration); + if (sort === 'duration') all.sort((a, b) => b.duration - a.duration); const start = offset ?? 0; - if (limit) return entries.slice(start, start + limit); - return offset ? entries.slice(start) : entries; + return { + entries: all.slice(start, start + limit), + total: all.length, + offset: start, + }; } getExportData(tree: ComponentTree): ProfilingDataExport | null { From 7dc3211e340ffd1c0f078cac9394f15a038c8148 Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Thu, 2 Apr 2026 12:47:40 +0200 Subject: [PATCH 5/7] fix: update profiler test for TimelineResult shape and handle offset past end in formatTimeline --- .../src/__tests__/formatters.test.ts | 7 +++++++ .../src/__tests__/profiler.test.ts | 11 ++++++----- packages/agent-react-devtools/src/formatters.ts | 5 +++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/agent-react-devtools/src/__tests__/formatters.test.ts b/packages/agent-react-devtools/src/__tests__/formatters.test.ts index 179cf27..6938d87 100644 --- a/packages/agent-react-devtools/src/__tests__/formatters.test.ts +++ b/packages/agent-react-devtools/src/__tests__/formatters.test.ts @@ -405,6 +405,13 @@ describe('formatTimeline', () => { const result: TimelineResult = { entries: [], total: 0, offset: 0 }; expect(formatTimeline(result)).toBe('No profiling data'); }); + + it('should handle offset past end gracefully', () => { + const result: TimelineResult = { entries: [], total: 50, offset: 50 }; + const output = formatTimeline(result); + expect(output).toContain('0 of 50'); + expect(output).not.toMatch(/showing \d+–\d+/); + }); }); describe('formatCommitDetail', () => { diff --git a/packages/agent-react-devtools/src/__tests__/profiler.test.ts b/packages/agent-react-devtools/src/__tests__/profiler.test.ts index 36f2cb4..d092199 100644 --- a/packages/agent-react-devtools/src/__tests__/profiler.test.ts +++ b/packages/agent-react-devtools/src/__tests__/profiler.test.ts @@ -293,11 +293,12 @@ describe('Profiler', () => { }); const timeline = profiler.getTimeline(); - expect(timeline).toHaveLength(2); - expect(timeline[0].duration).toBe(10); - expect(timeline[0].componentCount).toBe(1); - expect(timeline[1].duration).toBe(20); - expect(timeline[1].componentCount).toBe(2); + expect(timeline.entries).toHaveLength(2); + expect(timeline.total).toBe(2); + expect(timeline.entries[0].duration).toBe(10); + expect(timeline.entries[0].componentCount).toBe(1); + expect(timeline.entries[1].duration).toBe(20); + expect(timeline.entries[1].componentCount).toBe(2); }); describe('getExportData', () => { diff --git a/packages/agent-react-devtools/src/formatters.ts b/packages/agent-react-devtools/src/formatters.ts index 82e05d4..31c30e9 100644 --- a/packages/agent-react-devtools/src/formatters.ts +++ b/packages/agent-react-devtools/src/formatters.ts @@ -265,6 +265,11 @@ export function formatTimeline(result: TimelineResult): string { if (result.total === 0) return 'No profiling data'; const { entries, total, offset } = result; + + if (entries.length === 0) { + return `Commit timeline (showing 0 of ${total}): offset past end`; + } + let header: string; if (entries.length === total) { header = `Commit timeline (${total} commits):`; From facde939a3de43ceb418d79047d900466d5077df Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Thu, 2 Apr 2026 23:59:00 +0200 Subject: [PATCH 6/7] fix: clamp negative offset to 0 in getTimeline Negative offset values passed through to Array.slice() use JS tail-indexing semantics, returning commits from the wrong end of the array and producing nonsensical pagination headers. --- packages/agent-react-devtools/src/profiler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agent-react-devtools/src/profiler.ts b/packages/agent-react-devtools/src/profiler.ts index 18e3679..b947fea 100644 --- a/packages/agent-react-devtools/src/profiler.ts +++ b/packages/agent-react-devtools/src/profiler.ts @@ -324,7 +324,7 @@ export class Profiler { })); if (sort === 'duration') all.sort((a, b) => b.duration - a.duration); - const start = offset ?? 0; + const start = Math.max(0, offset ?? 0); return { entries: all.slice(start, start + limit), total: all.length, From 441c65663022d127f5ff4088f0bfb4493a84c35f Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Fri, 3 Apr 2026 00:23:27 +0200 Subject: [PATCH 7/7] chore: add changeset for profile timeline pagination --- .changeset/profile-timeline-pagination.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/profile-timeline-pagination.md diff --git a/.changeset/profile-timeline-pagination.md b/.changeset/profile-timeline-pagination.md new file mode 100644 index 0000000..357a5dd --- /dev/null +++ b/.changeset/profile-timeline-pagination.md @@ -0,0 +1,12 @@ +--- +"agent-react-devtools": minor +--- + +Pagination and sorting for `profile timeline` + +Large profiling sessions no longer flood agent context with hundreds of commits: + +- **Default limit of 20**: `profile timeline` returns at most 20 entries unless `--limit N` is specified. +- **`--offset N` flag**: Skip the first N commits for pagination. +- **`--sort duration`**: Sort commits by render duration (slowest first) instead of chronological order. +- **Paginated header**: Output shows `Commit timeline (showing 1–20 of 87):` when paginated, or `Commit timeline (87 commits):` when all results fit on one page.