zeitzeuge 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +696 -15
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -747,7 +747,7 @@ When your analysis is complete, you MUST write ALL findings to a JSON file using
747
747
 
748
748
  1. Call write_file with path: \`/findings/<YOUR_AGENT_NAME>.json\`
749
749
  Use your agent name as the filename (e.g. memory-heap, page-load, runtime-blocking,
750
- code-pattern, cpu-hotspot, listener-leak, memory-closure).
750
+ code-pattern, rendering-fcp, cpu-hotspot, listener-leak, memory-closure).
751
751
 
752
752
  2. The file content MUST be a JSON object with this exact structure:
753
753
  { "findings": [ { "severity": "...", "title": "...", ... }, ... ] }
@@ -766,7 +766,9 @@ var STRUCTURED_OUTPUT_FIELDS = `## Structured output fields — REQUIRED for eve
766
766
 
767
767
  Every finding MUST include ALL of these fields:
768
768
 
769
- - \`sourceFile\` — (REQUIRED) the workspace path (e.g. src/utils/parser.ts or scripts/app.js)
769
+ - \`sourceFile\` — (REQUIRED) the EXACT workspace path you used with read_file
770
+ (e.g. /scripts/app.js, /styles/theme.css, /html/index).
771
+ NEVER use bare filenames, original URLs, or paths you did not read.
770
772
  - \`lineNumber\` — (REQUIRED) the 1-based line number, verified by reading the file
771
773
  - \`confidence\` — \`high\` if you read the source, \`medium\` if strongly suggested,
772
774
  \`low\` if inferred
@@ -2734,6 +2736,9 @@ function generateMarkdown(options) {
2734
2736
  const reqCount = trace.networkRequests.length;
2735
2737
  sections.push(`**Page load** ${loadSec}s · **FCP** ${fcpSec}s · **TBT** ${tbt}ms · ` + `**Heap** ${heapSize} · **${reqCount} requests** (${formatBytes(totalTransfer)} transferred)`);
2736
2738
  sections.push("");
2739
+ if (trace.renderingDiagnostic) {
2740
+ sections.push(...generateFilmstripSection(trace.renderingDiagnostic, trace.screencastFrames ?? []));
2741
+ }
2737
2742
  const counts = {
2738
2743
  critical: findings.filter((f) => f.severity === "critical").length,
2739
2744
  warning: findings.filter((f) => f.severity === "warning").length,
@@ -2819,6 +2824,78 @@ function generateMarkdown(options) {
2819
2824
  return sections.join(`
2820
2825
  `);
2821
2826
  }
2827
+ var MAX_FILMSTRIP_FRAMES = 10;
2828
+ function selectKeyFrames(diagnostic, frames) {
2829
+ if (frames.length === 0)
2830
+ return [];
2831
+ const changeIndices = new Set(diagnostic.visualChanges.map((vc) => vc.frameIndex));
2832
+ changeIndices.add(0);
2833
+ changeIndices.add(frames.length - 1);
2834
+ const selected = [...changeIndices].sort((a, b) => a - b).filter((i) => i >= 0 && i < frames.length).map((i) => frames[i]);
2835
+ if (selected.length <= MAX_FILMSTRIP_FRAMES)
2836
+ return selected;
2837
+ const step = Math.ceil(selected.length / MAX_FILMSTRIP_FRAMES);
2838
+ const sampled = [];
2839
+ for (let i = 0;i < selected.length; i += step) {
2840
+ sampled.push(selected[i]);
2841
+ }
2842
+ if (sampled[sampled.length - 1] !== selected[selected.length - 1]) {
2843
+ sampled.push(selected[selected.length - 1]);
2844
+ }
2845
+ return sampled.slice(0, MAX_FILMSTRIP_FRAMES);
2846
+ }
2847
+ function generateFilmstripSection(diagnostic, frames) {
2848
+ const sections = [];
2849
+ sections.push(`## Rendering Filmstrip`);
2850
+ sections.push("");
2851
+ const speedIdx = diagnostic.speedIndex;
2852
+ const fcpTs = diagnostic.fcpCorrelation.fcpTimestamp;
2853
+ sections.push(`**Speed Index** ${speedIdx}ms · **FCP** ${fcpTs}ms`);
2854
+ sections.push("");
2855
+ const keyFrames = selectKeyFrames(diagnostic, frames);
2856
+ if (keyFrames.length > 0) {
2857
+ const headerCells = keyFrames.map((f) => `${Math.round(f.timestamp)}ms`).join(" | ");
2858
+ const separatorCells = keyFrames.map(() => ":---:").join(" | ");
2859
+ const imageCells = keyFrames.map((f) => `<img src="data:image/jpeg;base64,${f.data}" width="120" alt="${Math.round(f.timestamp)}ms" />`).join(" | ");
2860
+ sections.push(`| ${headerCells} |`);
2861
+ sections.push(`| ${separatorCells} |`);
2862
+ sections.push(`| ${imageCells} |`);
2863
+ sections.push("");
2864
+ }
2865
+ if (diagnostic.visualChanges.length > 0) {
2866
+ sections.push(`### Visual Progress`);
2867
+ sections.push("");
2868
+ sections.push(`| Time | Visual Completeness |`);
2869
+ sections.push(`|------|:-------------------:|`);
2870
+ for (const vc of diagnostic.visualChanges) {
2871
+ const bar = "█".repeat(Math.round(vc.visualCompleteness / 5)) + "░".repeat(20 - Math.round(vc.visualCompleteness / 5));
2872
+ sections.push(`| ${Math.round(vc.timestamp)}ms | ${bar} ${vc.visualCompleteness}% |`);
2873
+ }
2874
+ sections.push("");
2875
+ }
2876
+ if (diagnostic.renderingPhases.length > 0) {
2877
+ sections.push(`### Rendering Phases`);
2878
+ sections.push("");
2879
+ sections.push(`| Phase | Duration | Description |`);
2880
+ sections.push(`|-------|----------|-------------|`);
2881
+ for (const phase of diagnostic.renderingPhases) {
2882
+ sections.push(`| ${phase.name} | ${phase.duration}ms | ${phase.description} |`);
2883
+ }
2884
+ sections.push("");
2885
+ }
2886
+ if (diagnostic.fcpBottlenecks.length > 0) {
2887
+ sections.push(`### FCP Bottlenecks`);
2888
+ sections.push("");
2889
+ sections.push(`| Type | Estimated Delay | Description |`);
2890
+ sections.push(`|------|:---------------:|-------------|`);
2891
+ for (const b of diagnostic.fcpBottlenecks) {
2892
+ const typeLabel = b.type.replace(/-/g, " ");
2893
+ sections.push(`| ${typeLabel} | ${b.estimatedDelayMs}ms | ${b.description} |`);
2894
+ }
2895
+ sections.push("");
2896
+ }
2897
+ return sections;
2898
+ }
2822
2899
  // src/analysis/agent.ts
2823
2900
  import { createDeepAgent as createDeepAgent2 } from "deepagents";
2824
2901
 
@@ -2832,7 +2909,7 @@ skill. Do NOT read data JSON files directly with read_file.
2832
2909
 
2833
2910
  After your analysis scripts identify specific issues, read at most 1-3
2834
2911
  source files that are directly implicated. Derive paths from script URLs
2835
- in the data (e.g. a URL ending in "abc123.js" → scripts/abc123.js).
2912
+ in the data (e.g. a URL ending in "abc123.js" → /scripts/abc123.js).
2836
2913
 
2837
2914
  PREFERRED actions:
2838
2915
  - execute_command with pre-built helper scripts or custom Node.js scripts
@@ -2843,7 +2920,22 @@ FORBIDDEN actions:
2843
2920
  - glob — NEVER call glob.
2844
2921
  - read_file on data JSON files — use scripts to extract what you need.
2845
2922
  - Reading ALL source files — only read the specific ones your scripts point to.
2846
- - Reading more than 3 source files — focus on the most impactful issues.`;
2923
+ - Reading more than 3 source files — focus on the most impactful issues.
2924
+
2925
+ ## CRITICAL: sourceFile path format
2926
+
2927
+ When setting \`sourceFile\` in a finding, use the EXACT WORKSPACE PATH you
2928
+ used when calling read_file. Workspace paths start with a leading slash:
2929
+ - \`/scripts/App.tsx\` — JavaScript/TypeScript source files
2930
+ - \`/styles/theme.css\` — CSS source files
2931
+ - \`/html/index\` — HTML document
2932
+
2933
+ If you read a file at path \`/scripts/App.tsx\`, set \`sourceFile\` to exactly
2934
+ \`/scripts/App.tsx\`. Do NOT use bare filenames like \`App.tsx\`, original
2935
+ URLs like \`http://localhost:.../src/components/App.tsx\`, or made-up paths.
2936
+
2937
+ NEVER reference a file you did not actually read — only files you verified
2938
+ with read_file should appear in sourceFile.`;
2847
2939
  var MINIFIED_SOURCE_HANDLING = `## Handling minified / compiled source files
2848
2940
 
2849
2941
  The JavaScript files in scripts/ are captured from a PRODUCTION page. They are
@@ -3452,6 +3544,140 @@ Images without explicit width/height that cause Cumulative Layout Shift (CLS).
3452
3544
  Check HTML and CSS completely. For scripts, focus on the ones that HTML/CSS
3453
3545
  point to as potentially problematic. A typical page has 3-8 issues.
3454
3546
 
3547
+ ${BROWSER_TOOL_CALL_STRATEGY}
3548
+ ${VERIFICATION_RULES}
3549
+ ${SEVERITY_RULES2}
3550
+ ${FINDING_CATEGORIES}
3551
+ ${OUTPUT_FORMAT}
3552
+ ${STRUCTURED_OUTPUT_FIELDS}
3553
+ ${MINIFIED_SOURCE_HANDLING}
3554
+ ${IMPACT_ESTIMATION}
3555
+ ${WRITE_FINDINGS_REQUIREMENT}`;
3556
+ // src/analysis/prompts/rendering-fcp.ts
3557
+ var RENDERING_FCP_PROMPT = `You are a specialist in analyzing rendering performance and First Contentful Paint (FCP) behavior.
3558
+
3559
+ You have access to a workspace with rendering diagnostic data captured via Chrome DevTools screencast, along with trace, network, and source files from a real page load.
3560
+
3561
+ ## Your focus areas
3562
+
3563
+ ### 1. FCP Bottleneck Analysis (HIGHEST PRIORITY)
3564
+
3565
+ Identify what delays First Contentful Paint — the moment the browser renders the first piece of DOM content (text, image, SVG, or non-white canvas).
3566
+
3567
+ \`\`\`html
3568
+ <!-- BAD: synchronous script in <head> blocks FCP -->
3569
+ <head>
3570
+ <script src="/heavy-analytics.js"></script>
3571
+ <link rel="stylesheet" href="/all-styles.css">
3572
+ </head>
3573
+
3574
+ <!-- GOOD: defer non-critical scripts, inline critical CSS -->
3575
+ <head>
3576
+ <style>/* critical above-fold CSS only */</style>
3577
+ <script src="/heavy-analytics.js" defer></script>
3578
+ <link rel="stylesheet" href="/all-styles.css" media="print" onload="this.media='all'">
3579
+ </head>
3580
+ \`\`\`
3581
+
3582
+ **How to detect:** Read /trace/rendering/fcp-diagnostic.json for pre-computed FCP bottlenecks with estimated delay times. Cross-reference with the render-blocking chain and main-thread blockers. For each bottleneck, read the corresponding source file to verify the root cause and propose a fix.
3583
+
3584
+ ### 2. Rendering Order & Visual Progress
3585
+
3586
+ Analyze the order in which content appears on screen during page load. Identify elements that render late but should appear early (e.g. hero content, navigation, above-the-fold text).
3587
+
3588
+ \`\`\`javascript
3589
+ // BAD: hero image loaded lazily despite being above the fold
3590
+ <img src="hero.jpg" loading="lazy">
3591
+
3592
+ // GOOD: eager-load above-fold content, lazy-load below-fold
3593
+ <img src="hero.jpg" fetchpriority="high">
3594
+ <img src="below-fold.jpg" loading="lazy">
3595
+ \`\`\`
3596
+
3597
+ **How to detect:** Read /trace/rendering/visual-progress.json for the visual change timeline. Each visual change point shows when new content appeared and the estimated visual completeness. Correlate visual change timestamps with network-waterfall.json to identify which resources trigger which visual changes.
3598
+
3599
+ ### 3. Render-Blocking Resource Chains
3600
+
3601
+ Sequential chains of render-blocking resources that compound FCP delay. Each resource in the chain must complete before the next starts, creating a waterfall that delays first render.
3602
+
3603
+ \`\`\`html
3604
+ <!-- BAD: CSS imports create sequential chains -->
3605
+ <!-- main.css: @import url('reset.css'); @import url('theme.css'); -->
3606
+ <link rel="stylesheet" href="/main.css">
3607
+
3608
+ <!-- GOOD: flatten CSS imports into parallel <link> tags -->
3609
+ <link rel="stylesheet" href="/reset.css">
3610
+ <link rel="stylesheet" href="/theme.css">
3611
+ <link rel="stylesheet" href="/main.css">
3612
+ \`\`\`
3613
+
3614
+ **How to detect:** The fcp-diagnostic.json file lists sequential resource chains. Read the CSS files to check for @import chains. Check HTML for script/stylesheet ordering that creates unnecessary serialization.
3615
+
3616
+ ### 4. Speed Index & Visual Completeness
3617
+
3618
+ Speed Index measures how quickly the visible page area is filled with content. A high Speed Index means content renders slowly or in large bursts rather than progressively.
3619
+
3620
+ **How to detect:** Read /trace/rendering/visual-progress.json for the speedIndex value and the rendering phases. Identify phases with disproportionately long duration. Compare visual change frequency — long gaps between changes indicate rendering stalls.
3621
+
3622
+ ### 5. Layout Thrashing Before FCP
3623
+
3624
+ Excessive layout recalculations during the critical rendering path that delay first paint.
3625
+
3626
+ \`\`\`javascript
3627
+ // BAD: script in <head> forces layout before FCP
3628
+ document.querySelectorAll('.hero').forEach(el => {
3629
+ el.style.height = el.offsetHeight * 2 + 'px'; // forced layout
3630
+ });
3631
+
3632
+ // GOOD: defer DOM manipulation to after DOMContentLoaded
3633
+ document.addEventListener('DOMContentLoaded', () => {
3634
+ requestAnimationFrame(() => {
3635
+ document.querySelectorAll('.hero').forEach(el => {
3636
+ el.style.height = el.offsetHeight * 2 + 'px';
3637
+ });
3638
+ });
3639
+ });
3640
+ \`\`\`
3641
+
3642
+ **How to detect:** The fcp-diagnostic.json includes layoutTimeBeforeFCP. If significant (>100ms), check trace/runtime/blocking-functions.json for Layout events before FCP and read the triggering scripts.
3643
+
3644
+ ## Your workflow
3645
+
3646
+ 1. In your FIRST turn, run BOTH of these:
3647
+ a. Run the workspace overview:
3648
+ execute_command: node skills/browser-analysis/helpers/analyze-browser-workspace.js
3649
+ b. Read the FCP diagnostic:
3650
+ read_file: /trace/rendering/fcp-diagnostic.json
3651
+ c. Read the visual progress:
3652
+ read_file: /trace/rendering/visual-progress.json
3653
+ 2. From the FCP diagnostic, identify bottlenecks sorted by estimated delay.
3654
+ 3. For each bottleneck, determine the root cause:
3655
+ - Render-blocking resources → read the source file and HTML
3656
+ - Long tasks before FCP → read the blocking script
3657
+ - Sequential chains → check CSS for @import and HTML for script ordering
3658
+ - Slow server response → note in finding, suggest CDN/caching
3659
+ 4. Cross-reference visual progress with network waterfall to explain
3660
+ rendering order and identify missed optimization opportunities.
3661
+ 5. Report each bottleneck and improvement opportunity as a separate finding.
3662
+
3663
+ ### Source file path mapping
3664
+
3665
+ The FCP diagnostic data contains resource URLs (e.g. http://localhost:.../src/utils/foo.js).
3666
+ To read the actual source, map the URL's last path segment to its workspace path:
3667
+ - Scripts: URL ending in \`foo.js\` → workspace path \`/scripts/foo.js\`
3668
+ - Stylesheets: URL ending in \`bar.css\` → workspace path \`/styles/bar.css\`
3669
+ - HTML document: \`/html/index\`
3670
+
3671
+ Always use the WORKSPACE PATH (with leading /) in your finding's \`sourceFile\` field.
3672
+
3673
+ ### CRITICAL: Report EACH issue as a SEPARATE finding
3674
+
3675
+ - Each render-blocking resource → separate finding
3676
+ - Each sequential resource chain → separate finding
3677
+ - Each long task before FCP → separate finding
3678
+ - Slow server response → separate finding
3679
+ - Poor visual progress pattern → separate finding
3680
+
3455
3681
  ${BROWSER_TOOL_CALL_STRATEGY}
3456
3682
  ${VERIFICATION_RULES}
3457
3683
  ${SEVERITY_RULES2}
@@ -3467,12 +3693,12 @@ var BROWSER_ORCHESTRATOR_PROMPT = `You are a performance analysis orchestrator.
3467
3693
 
3468
3694
  ## Instructions
3469
3695
 
3470
- 1. Read the user message — it contains exactly 4 task descriptions.
3471
- 2. In your FIRST response, call the \`task\` tool exactly 4 times.
3696
+ 1. Read the user message — it contains task descriptions (4 or 5 depending on available data).
3697
+ 2. In your FIRST response, call the \`task\` tool once per task description.
3472
3698
  For each, set subagent_type and description EXACTLY as written in the
3473
3699
  user message. Copy the FULL multi-line description verbatim, including
3474
3700
  every file path listed.
3475
- 3. After all 4 subagents return, respond with: "All subagents complete."
3701
+ 3. After all subagents return, respond with: "All subagents complete."
3476
3702
 
3477
3703
  ## CRITICAL rules
3478
3704
 
@@ -3536,6 +3762,22 @@ function buildAgentFileListSection(agentName, ctx) {
3536
3762
  ...files.styles.map((f) => ({ path: f, description: "(CSS source — check first)" }))
3537
3763
  ];
3538
3764
  break;
3765
+ case "rendering-fcp":
3766
+ dataFiles = [
3767
+ {
3768
+ path: "/trace/rendering/fcp-diagnostic.json",
3769
+ description: "(FCP bottlenecks and correlation data — your PRIMARY data)"
3770
+ },
3771
+ {
3772
+ path: "/trace/rendering/visual-progress.json",
3773
+ description: "(visual change timeline, speed index, rendering phases)"
3774
+ },
3775
+ {
3776
+ path: "/trace/rendering/filmstrip.json",
3777
+ description: "(frame-by-frame rendering progress)"
3778
+ }
3779
+ ];
3780
+ break;
3539
3781
  }
3540
3782
  const additionalSections = [];
3541
3783
  if (agentName === "code-pattern") {
@@ -3553,6 +3795,14 @@ function buildAgentFileListSection(agentName, ctx) {
3553
3795
  files: allSource
3554
3796
  });
3555
3797
  }
3798
+ } else if (agentName === "rendering-fcp") {
3799
+ const allSource = [...files.scripts, ...files.styles, ...files.html];
3800
+ if (allSource.length > 0) {
3801
+ additionalSections.push({
3802
+ title: "Available source files — read ONLY the ones implicated in FCP bottlenecks or visual delays",
3803
+ files: allSource
3804
+ });
3805
+ }
3556
3806
  }
3557
3807
  return buildFileListPromptSection({
3558
3808
  dataFiles,
@@ -3566,13 +3816,28 @@ function buildBrowserUserMessage(ctx) {
3566
3816
  const renderBlocking = traceResult.networkRequests.filter((r) => r.isRenderBlocking).length;
3567
3817
  const totalTransfer = traceResult.networkRequests.reduce((s, r) => s + r.encodedSize, 0);
3568
3818
  const hasRuntime = !!traceResult.runtimeTrace;
3819
+ const hasRendering = !!traceResult.renderingDiagnostic;
3569
3820
  let runtimeInfo = "";
3570
3821
  if (hasRuntime) {
3571
3822
  const rt = traceResult.runtimeTrace;
3572
3823
  runtimeInfo = `
3573
3824
  Runtime trace: ${rt.blockingFunctions.length} blocking functions, ${rt.gcEvents.length} GC events (${Math.round(rt.gcEvents.reduce((s, e) => s + e.duration, 0))}ms total)`;
3574
3825
  }
3575
- return `Dispatch all 4 subagent tasks NOW in a single response.
3826
+ let renderingInfo = "";
3827
+ if (hasRendering) {
3828
+ const rd = traceResult.renderingDiagnostic;
3829
+ renderingInfo = `
3830
+ Rendering: Speed Index ${rd.speedIndex}ms, ${rd.fcpBottlenecks.length} FCP bottlenecks, ${rd.visualChanges.length} visual changes detected`;
3831
+ }
3832
+ const taskCount = hasRendering ? 5 : 4;
3833
+ let renderingTask = "";
3834
+ if (hasRendering) {
3835
+ renderingTask = `
3836
+
3837
+ TASK 5 — subagent_type: "rendering-fcp"
3838
+ description: "Analyze rendering speed and FCP behavior. Read /trace/rendering/fcp-diagnostic.json, /trace/rendering/visual-progress.json, and /trace/rendering/filmstrip.json FIRST. Identify FCP bottlenecks (render-blocking resources, long tasks before FCP, sequential resource chains, excessive layout). Cross-reference with network waterfall and source files. For each bottleneck, verify root cause and estimate delay. Report each bottleneck and rendering improvement as a separate finding. Do NOT suggest code fixes for minified/compiled JS."`;
3839
+ }
3840
+ return `Dispatch all ${taskCount} subagent tasks NOW in a single response.
3576
3841
  Use these EXACT descriptions (copy them verbatim):
3577
3842
 
3578
3843
  TASK 1 — subagent_type: "memory-heap"
@@ -3585,14 +3850,15 @@ TASK 3 — subagent_type: "runtime-blocking"
3585
3850
  description: "Find runtime issues: main-thread blocking functions, event listener imbalances, GC pressure, layout thrashing, and unthrottled event handlers. Read /trace/runtime/ data files FIRST (do NOT read source files yet). Analyze blocking-functions.json for functions >50ms, event-listeners.json for add/remove imbalances. Then read ONLY the source files at the reported locations. Check for compound blockers (A calls blocking B — report BOTH). Report each distinct issue as a separate finding. Do NOT suggest code fixes for minified/compiled JS."
3586
3851
 
3587
3852
  TASK 4 — subagent_type: "code-pattern"
3588
- description: "Find frontend code anti-patterns: inline scripts, DOM manipulation in loops, missing event delegation, synchronous XHR, non-passive listeners, CSS issues, and missing image dimensions. Read HTML and CSS files FIRST. Check for inline <script> blocks, <img> without width/height, CSS @import. Then read ONLY the script files referenced by issues found. Report each pattern as a separate finding. Do NOT suggest code fixes for minified/compiled JS."
3853
+ description: "Find frontend code anti-patterns: inline scripts, DOM manipulation in loops, missing event delegation, synchronous XHR, non-passive listeners, CSS issues, and missing image dimensions. Read HTML and CSS files FIRST. Check for inline <script> blocks, <img> without width/height, CSS @import. Then read ONLY the script files referenced by issues found. Report each pattern as a separate finding. Do NOT suggest code fixes for minified/compiled JS."${renderingTask}
3589
3854
 
3590
3855
  URL: ${url}
3591
3856
  Page load: ${Math.round(m.loadComplete)}ms | FCP: ${Math.round(m.firstContentfulPaint)}ms | LCP: ${Math.round(m.largestContentfulPaint)}ms | TBT: ${Math.round(m.totalBlockingTime)}ms
3592
3857
  Heap: ${formatBytes(heapSummary.metadata.totalSize)} total, ${heapSummary.metadata.nodeCount.toLocaleString()} nodes, ${heapSummary.detachedNodes.count} detached DOM nodes
3593
- Network: ${reqCount} requests, ${formatBytes(totalTransfer)} transferred, ${renderBlocking} render-blocking${runtimeInfo}`;
3858
+ Network: ${reqCount} requests, ${formatBytes(totalTransfer)} transferred, ${renderBlocking} render-blocking${runtimeInfo}${renderingInfo}`;
3594
3859
  }
3595
3860
  function buildSubagents(ctx) {
3861
+ const hasRendering = !!ctx?.traceResult?.renderingDiagnostic;
3596
3862
  const agentDefs = [
3597
3863
  {
3598
3864
  name: "memory-heap",
@@ -3613,7 +3879,14 @@ function buildSubagents(ctx) {
3613
3879
  name: "code-pattern",
3614
3880
  description: "Detects frontend code anti-patterns: inline scripts, DOM manipulation in loops, missing event delegation, non-passive listeners.",
3615
3881
  prompt: CODE_PATTERN_PROMPT2
3616
- }
3882
+ },
3883
+ ...hasRendering ? [
3884
+ {
3885
+ name: "rendering-fcp",
3886
+ description: "Analyzes rendering speed and FCP behavior: identifies bottlenecks delaying first contentful paint, analyzes visual rendering order, and suggests rendering optimizations.",
3887
+ prompt: RENDERING_FCP_PROMPT
3888
+ }
3889
+ ] : []
3617
3890
  ];
3618
3891
  return agentDefs.map(({ name, description, prompt }) => {
3619
3892
  const fileSection = ctx ? buildAgentFileListSection(name, ctx) : "";
@@ -3912,6 +4185,363 @@ function emptyRuntimeTrace() {
3912
4185
  };
3913
4186
  }
3914
4187
 
4188
+ // src/browser/screencast.ts
4189
+ var VISUAL_CHANGE_THRESHOLD = 0.1;
4190
+ var MAX_FRAMES = 200;
4191
+ async function startScreencast(cdpSession, navigationStartTs) {
4192
+ const frames = [];
4193
+ cdpSession.on("Page.screencastFrame", (params) => {
4194
+ if (frames.length >= MAX_FRAMES)
4195
+ return;
4196
+ const dataLength = Math.ceil((params.data?.length ?? 0) * 0.75);
4197
+ frames.push({
4198
+ timestamp: params.metadata?.timestamp ? params.metadata.timestamp * 1000 - navigationStartTs : Date.now() - navigationStartTs,
4199
+ data: params.data ?? "",
4200
+ sessionId: params.sessionId ?? 0,
4201
+ dataLength
4202
+ });
4203
+ try {
4204
+ cdpSession.send("Page.screencastFrameAck", {
4205
+ sessionId: params.sessionId
4206
+ });
4207
+ } catch {}
4208
+ });
4209
+ try {
4210
+ await cdpSession.send("Page.startScreencast", {
4211
+ format: "jpeg",
4212
+ quality: 40,
4213
+ maxWidth: 800,
4214
+ maxHeight: 600,
4215
+ everyNthFrame: 1
4216
+ });
4217
+ } catch {
4218
+ return { stop: async () => [] };
4219
+ }
4220
+ return {
4221
+ async stop() {
4222
+ try {
4223
+ await cdpSession.send("Page.stopScreencast");
4224
+ } catch {}
4225
+ return frames;
4226
+ }
4227
+ };
4228
+ }
4229
+ function detectVisualChanges(frames) {
4230
+ if (frames.length === 0)
4231
+ return [];
4232
+ const changes = [];
4233
+ const finalSize = frames[frames.length - 1].dataLength;
4234
+ if (finalSize === 0)
4235
+ return [];
4236
+ let prevSize = 0;
4237
+ for (let i = 0;i < frames.length; i++) {
4238
+ const frame = frames[i];
4239
+ const sizeDiff = Math.abs(frame.dataLength - prevSize);
4240
+ const relativeChange = prevSize > 0 ? sizeDiff / prevSize : frame.dataLength > 0 ? 1 : 0;
4241
+ if (relativeChange >= VISUAL_CHANGE_THRESHOLD || i === 0 && frame.dataLength > 0) {
4242
+ changes.push({
4243
+ timestamp: frame.timestamp,
4244
+ frameIndex: i,
4245
+ visualCompleteness: Math.min(100, Math.round(frame.dataLength / finalSize * 100)),
4246
+ changeMagnitude: Math.min(1, relativeChange)
4247
+ });
4248
+ }
4249
+ prevSize = frame.dataLength;
4250
+ }
4251
+ return changes;
4252
+ }
4253
+ function buildFilmstrip(frames, visualChanges) {
4254
+ const changeIndices = new Set(visualChanges.map((vc) => vc.frameIndex));
4255
+ return frames.map((frame, i) => ({
4256
+ index: i,
4257
+ timestamp: Math.round(frame.timestamp),
4258
+ dataLength: frame.dataLength,
4259
+ isVisualChange: changeIndices.has(i)
4260
+ }));
4261
+ }
4262
+ function approximateSpeedIndex(visualChanges) {
4263
+ if (visualChanges.length === 0)
4264
+ return 0;
4265
+ const sorted = [...visualChanges].sort((a, b) => a.timestamp - b.timestamp);
4266
+ const lastChange = sorted[sorted.length - 1];
4267
+ const totalTime = lastChange.timestamp;
4268
+ if (totalTime <= 0)
4269
+ return 0;
4270
+ let speedIndex = 0;
4271
+ let prevTimestamp = 0;
4272
+ let prevCompleteness = 0;
4273
+ for (const change of sorted) {
4274
+ const duration = change.timestamp - prevTimestamp;
4275
+ speedIndex += duration * (1 - prevCompleteness / 100);
4276
+ prevTimestamp = change.timestamp;
4277
+ prevCompleteness = change.visualCompleteness;
4278
+ }
4279
+ if (prevCompleteness < 100) {
4280
+ speedIndex += (totalTime - prevTimestamp) * (1 - prevCompleteness / 100);
4281
+ }
4282
+ return Math.round(speedIndex);
4283
+ }
4284
+
4285
+ // src/browser/fcp-analysis.ts
4286
+ function buildRenderingDiagnostic(traceResult, frames) {
4287
+ const { metrics, runtimeTrace, rawTraceEvents } = traceResult;
4288
+ const fcpTs = metrics.firstContentfulPaint;
4289
+ const visualChanges = detectVisualChanges(frames);
4290
+ const filmstrip = buildFilmstrip(frames, visualChanges);
4291
+ const speedIndex = approximateSpeedIndex(visualChanges);
4292
+ const fcpCorrelation = buildFCPCorrelation(traceResult, frames, fcpTs);
4293
+ const renderingPhases = buildRenderingPhases(metrics, rawTraceEvents ?? [], runtimeTrace?.mainThreadId ?? 0);
4294
+ const fcpBottlenecks = identifyFCPBottlenecks(fcpCorrelation, traceResult);
4295
+ return {
4296
+ filmstrip,
4297
+ visualChanges,
4298
+ fcpCorrelation,
4299
+ renderingPhases,
4300
+ speedIndex,
4301
+ fcpBottlenecks
4302
+ };
4303
+ }
4304
+ function buildFCPCorrelation(traceResult, frames, fcpTs) {
4305
+ const { networkRequests, runtimeTrace } = traceResult;
4306
+ let nearestFrameIndex = 0;
4307
+ let minDelta = Infinity;
4308
+ for (let i = 0;i < frames.length; i++) {
4309
+ const delta = Math.abs(frames[i].timestamp - fcpTs);
4310
+ if (delta < minDelta) {
4311
+ minDelta = delta;
4312
+ nearestFrameIndex = i;
4313
+ }
4314
+ }
4315
+ const resourcesLoadedBeforeFCP = networkRequests.filter((r) => r.endTime > 0 && r.endTime <= fcpTs).sort((a, b) => a.endTime - b.endTime).map((r) => ({
4316
+ url: r.url,
4317
+ resourceType: r.resourceType,
4318
+ duration: Math.round(r.duration),
4319
+ isRenderBlocking: r.isRenderBlocking,
4320
+ endTime: Math.round(r.endTime)
4321
+ }));
4322
+ const resourcesLoadingAtFCP = networkRequests.filter((r) => r.startTime <= fcpTs && (r.endTime === 0 || r.endTime > fcpTs)).map((r) => ({
4323
+ url: r.url,
4324
+ resourceType: r.resourceType,
4325
+ startTime: Math.round(r.startTime)
4326
+ }));
4327
+ const renderBlockingChain = networkRequests.filter((r) => r.isRenderBlocking && r.endTime > 0 && r.endTime <= fcpTs).sort((a, b) => a.startTime - b.startTime).map((r) => ({
4328
+ url: r.url,
4329
+ resourceType: r.resourceType,
4330
+ duration: Math.round(r.duration),
4331
+ size: r.decodedSize
4332
+ }));
4333
+ const mainThreadBlockersBeforeFCP = (runtimeTrace?.blockingFunctions ?? []).filter((bf) => bf.startTime + bf.duration <= fcpTs).map((bf) => ({
4334
+ functionName: bf.functionName,
4335
+ scriptUrl: bf.scriptUrl,
4336
+ duration: Math.round(bf.duration),
4337
+ startTime: Math.round(bf.startTime)
4338
+ }));
4339
+ const totalBlockingTimeBeforeFCP = mainThreadBlockersBeforeFCP.reduce((sum, bf) => sum + Math.max(0, bf.duration - 50), 0);
4340
+ const layoutTimeBeforeFCP = computeLayoutTimeBeforeFCP(traceResult, fcpTs);
4341
+ return {
4342
+ fcpTimestamp: Math.round(fcpTs),
4343
+ nearestFrameIndex,
4344
+ resourcesLoadedBeforeFCP,
4345
+ resourcesLoadingAtFCP,
4346
+ renderBlockingChain,
4347
+ mainThreadBlockersBeforeFCP,
4348
+ totalBlockingTimeBeforeFCP: Math.round(totalBlockingTimeBeforeFCP),
4349
+ layoutTimeBeforeFCP: Math.round(layoutTimeBeforeFCP)
4350
+ };
4351
+ }
4352
+ function computeLayoutTimeBeforeFCP(traceResult, fcpTs) {
4353
+ const { rawTraceEvents, runtimeTrace } = traceResult;
4354
+ if (!rawTraceEvents || rawTraceEvents.length === 0)
4355
+ return 0;
4356
+ const mainTid = runtimeTrace?.mainThreadId ?? 0;
4357
+ const navEvent = rawTraceEvents.find((e) => e.name === "navigationStart" || e.name === "NavigationStart");
4358
+ const navTs = navEvent?.ts ?? rawTraceEvents[0]?.ts ?? 0;
4359
+ const fcpUs = fcpTs * 1000 + navTs;
4360
+ const layoutEvents = new Set(["Layout", "UpdateLayoutTree", "RecalculateStyles"]);
4361
+ let layoutTime = 0;
4362
+ for (const e of rawTraceEvents) {
4363
+ if (e.tid !== mainTid || e.ph !== "X" || !e.dur)
4364
+ continue;
4365
+ if (!layoutEvents.has(e.name))
4366
+ continue;
4367
+ if (e.ts + e.dur <= fcpUs) {
4368
+ layoutTime += e.dur / 1000;
4369
+ }
4370
+ }
4371
+ return layoutTime;
4372
+ }
4373
+ function buildRenderingPhases(metrics, rawTraceEvents, mainThreadId) {
4374
+ const phases = [];
4375
+ const fp = metrics.firstPaint;
4376
+ const fcp = metrics.firstContentfulPaint;
4377
+ const dcl = metrics.domContentLoaded;
4378
+ const load = metrics.loadComplete;
4379
+ if (fp > 0) {
4380
+ phases.push({
4381
+ name: "Server & Network",
4382
+ startTime: 0,
4383
+ endTime: Math.round(fp),
4384
+ duration: Math.round(fp),
4385
+ description: "Time from navigation start to first paint — includes DNS, TCP, TLS, server response, and HTML parsing."
4386
+ });
4387
+ }
4388
+ if (fcp > fp && fp > 0) {
4389
+ phases.push({
4390
+ name: "First Paint to FCP",
4391
+ startTime: Math.round(fp),
4392
+ endTime: Math.round(fcp),
4393
+ duration: Math.round(fcp - fp),
4394
+ description: "Time between first paint (background/border) and first contentful paint (text/image). Render-blocking resources and large DOM delay this phase."
4395
+ });
4396
+ } else if (fcp > 0) {
4397
+ phases.push({
4398
+ name: "Navigation to FCP",
4399
+ startTime: 0,
4400
+ endTime: Math.round(fcp),
4401
+ duration: Math.round(fcp),
4402
+ description: "Time from navigation start to first contentful paint. Includes all network, parsing, and rendering work."
4403
+ });
4404
+ }
4405
+ if (dcl > fcp && fcp > 0) {
4406
+ phases.push({
4407
+ name: "FCP to DOMContentLoaded",
4408
+ startTime: Math.round(fcp),
4409
+ endTime: Math.round(dcl),
4410
+ duration: Math.round(dcl - fcp),
4411
+ description: "Time between FCP and DOMContentLoaded. Deferred scripts execute during this phase."
4412
+ });
4413
+ }
4414
+ if (load > dcl && dcl > 0) {
4415
+ phases.push({
4416
+ name: "DOMContentLoaded to Load",
4417
+ startTime: Math.round(dcl),
4418
+ endTime: Math.round(load),
4419
+ duration: Math.round(load - dcl),
4420
+ description: "Time between DOMContentLoaded and load event. Images, fonts, and async resources finish loading."
4421
+ });
4422
+ }
4423
+ if (rawTraceEvents.length > 0) {
4424
+ annotatePhaseActivity(phases, rawTraceEvents, mainThreadId);
4425
+ }
4426
+ return phases;
4427
+ }
4428
+ function annotatePhaseActivity(phases, rawTraceEvents, mainThreadId) {
4429
+ const navEvent = rawTraceEvents.find((e) => e.name === "navigationStart" || e.name === "NavigationStart");
4430
+ const navTs = navEvent?.ts ?? rawTraceEvents[0]?.ts ?? 0;
4431
+ const scriptingEvents = new Set([
4432
+ "FunctionCall",
4433
+ "EvaluateScript",
4434
+ "TimerFire",
4435
+ "RequestAnimationFrame"
4436
+ ]);
4437
+ const layoutEvts = new Set(["Layout", "UpdateLayoutTree", "RecalculateStyles"]);
4438
+ for (const phase of phases) {
4439
+ const startUs = phase.startTime * 1000 + navTs;
4440
+ const endUs = phase.endTime * 1000 + navTs;
4441
+ let scripting = 0;
4442
+ let layout = 0;
4443
+ let painting = 0;
4444
+ for (const e of rawTraceEvents) {
4445
+ if (e.tid !== mainThreadId || e.ph !== "X" || !e.dur)
4446
+ continue;
4447
+ if (e.ts >= endUs || e.ts + e.dur <= startUs)
4448
+ continue;
4449
+ const overlapStart = Math.max(e.ts, startUs);
4450
+ const overlapEnd = Math.min(e.ts + e.dur, endUs);
4451
+ const overlapMs = (overlapEnd - overlapStart) / 1000;
4452
+ if (scriptingEvents.has(e.name))
4453
+ scripting += overlapMs;
4454
+ else if (layoutEvts.has(e.name))
4455
+ layout += overlapMs;
4456
+ else if (e.name === "Paint" || e.name === "CompositeLayers")
4457
+ painting += overlapMs;
4458
+ }
4459
+ if (scripting > 0 || layout > 0 || painting > 0) {
4460
+ const parts = [];
4461
+ if (scripting > 0)
4462
+ parts.push(`${Math.round(scripting)}ms scripting`);
4463
+ if (layout > 0)
4464
+ parts.push(`${Math.round(layout)}ms layout`);
4465
+ if (painting > 0)
4466
+ parts.push(`${Math.round(painting)}ms painting`);
4467
+ phase.description += ` Main-thread breakdown: ${parts.join(", ")}.`;
4468
+ }
4469
+ }
4470
+ }
4471
+ function identifyFCPBottlenecks(correlation, traceResult) {
4472
+ const bottlenecks = [];
4473
+ for (const rb of correlation.renderBlockingChain) {
4474
+ bottlenecks.push({
4475
+ type: "render-blocking-resource",
4476
+ description: `Render-blocking ${rb.resourceType.toLowerCase()} (${formatSize(rb.size)}) delayed FCP by ~${rb.duration}ms. Consider async/defer loading or inlining critical content.`,
4477
+ estimatedDelayMs: rb.duration,
4478
+ source: rb.url
4479
+ });
4480
+ }
4481
+ for (const bf of correlation.mainThreadBlockersBeforeFCP) {
4482
+ if (bf.duration >= 50) {
4483
+ bottlenecks.push({
4484
+ type: "long-task-before-fcp",
4485
+ description: `Long task "${bf.functionName}" blocked the main thread for ${bf.duration}ms before FCP. This prevents the browser from rendering content.`,
4486
+ estimatedDelayMs: bf.duration - 50,
4487
+ source: bf.scriptUrl || bf.functionName
4488
+ });
4489
+ }
4490
+ }
4491
+ const docRequest = traceResult.networkRequests.find((r) => r.resourceType === "Document");
4492
+ if (docRequest && docRequest.duration > 600) {
4493
+ bottlenecks.push({
4494
+ type: "slow-server-response",
4495
+ description: `Server took ${Math.round(docRequest.duration)}ms to respond with the HTML document. Consider server-side caching, CDN, or SSR optimization.`,
4496
+ estimatedDelayMs: Math.round(docRequest.duration - 200),
4497
+ source: docRequest.url
4498
+ });
4499
+ }
4500
+ const renderBlockingRequests = traceResult.networkRequests.filter((r) => r.isRenderBlocking).sort((a, b) => a.startTime - b.startTime);
4501
+ for (let i = 1;i < renderBlockingRequests.length; i++) {
4502
+ const prev = renderBlockingRequests[i - 1];
4503
+ const curr = renderBlockingRequests[i];
4504
+ const gap = curr.startTime - prev.endTime;
4505
+ if (gap < 50 && prev.endTime > 0 && curr.startTime > prev.endTime - 10) {
4506
+ const chainDelay = curr.endTime - prev.startTime;
4507
+ const parallelTime = Math.max(prev.duration, curr.duration);
4508
+ const savings = chainDelay - parallelTime;
4509
+ if (savings > 50) {
4510
+ bottlenecks.push({
4511
+ type: "sequential-resource-chain",
4512
+ description: `Sequential render-blocking chain: "${basename(prev.url)}" → "${basename(curr.url)}" adds ~${Math.round(savings)}ms. Preloading or parallelizing would reduce FCP.`,
4513
+ estimatedDelayMs: Math.round(savings),
4514
+ source: curr.url
4515
+ });
4516
+ }
4517
+ }
4518
+ }
4519
+ if (correlation.layoutTimeBeforeFCP > 100) {
4520
+ bottlenecks.push({
4521
+ type: "excessive-layout",
4522
+ description: `${Math.round(correlation.layoutTimeBeforeFCP)}ms spent in layout/style recalculation before FCP. Reduce CSS complexity or DOM size for the initial render.`,
4523
+ estimatedDelayMs: Math.round(correlation.layoutTimeBeforeFCP * 0.5),
4524
+ source: "layout/style-recalculation"
4525
+ });
4526
+ }
4527
+ bottlenecks.sort((a, b) => b.estimatedDelayMs - a.estimatedDelayMs);
4528
+ return bottlenecks;
4529
+ }
4530
+ function formatSize(bytes) {
4531
+ if (bytes < 1024)
4532
+ return `${bytes}B`;
4533
+ if (bytes < 1024 * 1024)
4534
+ return `${(bytes / 1024).toFixed(1)}KB`;
4535
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
4536
+ }
4537
+ function basename(url) {
4538
+ try {
4539
+ return new URL(url).pathname.split("/").pop() || url;
4540
+ } catch {
4541
+ return url;
4542
+ }
4543
+ }
4544
+
3915
4545
  // src/browser/trace.ts
3916
4546
  var TEXT_RESOURCE_TYPES = new Set(["Script", "Stylesheet", "Document", "XHR", "Fetch"]);
3917
4547
  var MAX_BODY_SIZE = 2 * 1024 * 1024;
@@ -3927,6 +4557,7 @@ var TRACING_COMPLETE_TIMEOUT = 1e4;
3927
4557
  async function tracePageLoad(cdpSession, _options = {}) {
3928
4558
  const requests = new Map;
3929
4559
  const finishedIds = new Set;
4560
+ const enableScreencast = _options.screencast !== false;
3930
4561
  const traceEvents = [];
3931
4562
  let tracingStarted = false;
3932
4563
  try {
@@ -3944,6 +4575,13 @@ async function tracePageLoad(cdpSession, _options = {}) {
3944
4575
  });
3945
4576
  tracingStarted = true;
3946
4577
  } catch {}
4578
+ let screencastHandle;
4579
+ const navigationStartMs = Date.now();
4580
+ if (enableScreencast) {
4581
+ try {
4582
+ screencastHandle = await startScreencast(cdpSession, navigationStartMs);
4583
+ } catch {}
4584
+ }
3947
4585
  await cdpSession.send("Network.enable");
3948
4586
  cdpSession.on("Network.requestWillBeSent", (params) => {
3949
4587
  requests.set(params.requestId, {
@@ -4069,6 +4707,12 @@ async function tracePageLoad(cdpSession, _options = {}) {
4069
4707
  } catch {}
4070
4708
  const fp = paintEntries.find((e) => e.name === "first-paint");
4071
4709
  const fcp = paintEntries.find((e) => e.name === "first-contentful-paint");
4710
+ let screencastFrames = [];
4711
+ if (screencastHandle) {
4712
+ try {
4713
+ screencastFrames = await screencastHandle.stop();
4714
+ } catch {}
4715
+ }
4072
4716
  await cdpSession.send("Network.disable");
4073
4717
  const metrics = {
4074
4718
  navigationStart: perfMetrics.navigationStart ?? 0,
@@ -4080,12 +4724,19 @@ async function tracePageLoad(cdpSession, _options = {}) {
4080
4724
  totalBlockingTime: longTasks.reduce((sum, t) => sum + Math.max(0, t.duration - 50), 0),
4081
4725
  longTasks
4082
4726
  };
4083
- return {
4727
+ const traceResult = {
4084
4728
  networkRequests,
4085
4729
  metrics,
4086
4730
  runtimeTrace,
4087
4731
  rawTraceEvents: tracingStarted ? traceEvents : undefined
4088
4732
  };
4733
+ if (screencastFrames.length > 0) {
4734
+ try {
4735
+ traceResult.renderingDiagnostic = buildRenderingDiagnostic(traceResult, screencastFrames);
4736
+ } catch {}
4737
+ traceResult.screencastFrames = screencastFrames;
4738
+ }
4739
+ return traceResult;
4089
4740
  }
4090
4741
  };
4091
4742
  }
@@ -4463,6 +5114,27 @@ async function createWorkspace(options) {
4463
5114
  files["/trace/runtime/event-listeners.json"] = JSON.stringify(rt.eventListeners.filter((l) => l.addCount > 0), null, 2);
4464
5115
  files["/trace/runtime/frame-breakdown.json"] = JSON.stringify(rt.frameBreakdown, null, 2);
4465
5116
  }
5117
+ if (traceResult.renderingDiagnostic) {
5118
+ const rd = traceResult.renderingDiagnostic;
5119
+ files["/trace/rendering/fcp-diagnostic.json"] = JSON.stringify({
5120
+ fcpTimestamp: rd.fcpCorrelation.fcpTimestamp,
5121
+ speedIndex: rd.speedIndex,
5122
+ bottleneckCount: rd.fcpBottlenecks.length,
5123
+ totalEstimatedDelay: rd.fcpBottlenecks.reduce((s, b) => s + b.estimatedDelayMs, 0),
5124
+ fcpCorrelation: rd.fcpCorrelation,
5125
+ fcpBottlenecks: rd.fcpBottlenecks
5126
+ }, null, 2);
5127
+ files["/trace/rendering/filmstrip.json"] = JSON.stringify({
5128
+ frameCount: rd.filmstrip.length,
5129
+ visualChangeCount: rd.visualChanges.length,
5130
+ frames: rd.filmstrip
5131
+ }, null, 2);
5132
+ files["/trace/rendering/visual-progress.json"] = JSON.stringify({
5133
+ speedIndex: rd.speedIndex,
5134
+ visualChanges: rd.visualChanges,
5135
+ renderingPhases: rd.renderingPhases
5136
+ }, null, 2);
5137
+ }
4466
5138
  if (traceResult.rawTraceEvents && traceResult.rawTraceEvents.length > 0) {
4467
5139
  const rawJson = JSON.stringify(traceResult.rawTraceEvents);
4468
5140
  if (rawJson.length < 5 * 1024 * 1024) {
@@ -4616,10 +5288,12 @@ async function main() {
4616
5288
  const longTaskCount = captureResult.trace.metrics.longTasks.length;
4617
5289
  const runtimeTraceInfo = captureResult.trace.runtimeTrace ? `
4618
5290
  Runtime trace: ${captureResult.trace.runtimeTrace.totalEvents.toLocaleString()} events captured` : "";
5291
+ const renderingInfo = captureResult.trace.renderingDiagnostic ? `
5292
+ Rendering: FCP ${Math.round(captureResult.trace.metrics.firstContentfulPaint)}ms, Speed Index ${captureResult.trace.renderingDiagnostic.speedIndex}ms, ${captureResult.trace.renderingDiagnostic.fcpBottlenecks.length} bottlenecks, ${captureResult.trace.renderingDiagnostic.filmstrip.length} frames` : "";
4619
5293
  captureSpinner.succeed(`Page loaded in ${(captureResult.trace.metrics.loadComplete / 1000).toFixed(1)}s
4620
5294
  ` + ` Heap snapshot: ${heapSizeMB} MB
4621
5295
  ` + ` Network requests: ${reqCount} captured
4622
- ` + ` Long tasks: ${longTaskCount} detected` + runtimeTraceInfo);
5296
+ ` + ` Long tasks: ${longTaskCount} detected` + runtimeTraceInfo + renderingInfo);
4623
5297
  } catch (err) {
4624
5298
  captureSpinner.fail("Failed to capture page data");
4625
5299
  throw new Error(`Failed to capture data from ${url}.
@@ -4641,7 +5315,9 @@ async function main() {
4641
5315
  const totalSize = captureResult.trace.networkRequests.filter((r) => r.responseBody).reduce((sum, r) => sum + (r.responseBody?.length ?? 0), 0);
4642
5316
  const runtimeWorkspaceInfo = captureResult.trace.runtimeTrace ? `
4643
5317
  Runtime trace: summaries + raw events` : "";
4644
- workspaceSpinner.succeed(`${storedCount} assets stored in workspace (${formatBytes(totalSize)} total)` + runtimeWorkspaceInfo);
5318
+ const renderingWorkspaceInfo = captureResult.trace.renderingDiagnostic ? `
5319
+ Rendering diagnostics: FCP correlation + visual progress + filmstrip` : "";
5320
+ workspaceSpinner.succeed(`${storedCount} assets stored in workspace (${formatBytes(totalSize)} total)` + runtimeWorkspaceInfo + renderingWorkspaceInfo);
4645
5321
  } catch (err) {
4646
5322
  workspaceSpinner.fail("Failed to build workspace");
4647
5323
  throw new Error(`Failed to create workspace.
@@ -4683,7 +5359,12 @@ async function main() {
4683
5359
  heapNodeCount: heapSummary.metadata.nodeCount,
4684
5360
  detachedDomNodes: heapSummary.detachedNodes.count,
4685
5361
  networkRequests: captureResult.trace.networkRequests.length,
4686
- totalTransferSize: captureResult.trace.networkRequests.reduce((s, r) => s + r.encodedSize, 0)
5362
+ totalTransferSize: captureResult.trace.networkRequests.reduce((s, r) => s + r.encodedSize, 0),
5363
+ ...captureResult.trace.renderingDiagnostic ? {
5364
+ speedIndex: captureResult.trace.renderingDiagnostic.speedIndex,
5365
+ fcpBottlenecks: captureResult.trace.renderingDiagnostic.fcpBottlenecks.length,
5366
+ renderingPhases: captureResult.trace.renderingDiagnostic.renderingPhases.length
5367
+ } : {}
4687
5368
  }
4688
5369
  };
4689
5370
  const { writeFileSync: writeFileSync2 } = await import("node:fs");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeitzeuge",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "A deepagent to witnessing slowdowns in your test runs.",
5
5
  "keywords": [
6
6
  "analysis",