linear-grab-bridge 0.23.1 → 0.24.1

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.
@@ -28,7 +28,7 @@ const flag = (name, fallback) => {
28
28
  const PORT = Number(flag('--port', '4577'));
29
29
  const DIR = flag('--dir', process.cwd());
30
30
  const CLAUDE_BIN = flag('--claude', 'claude');
31
- const VERSION = '0.23.1';
31
+ const VERSION = '0.24.1';
32
32
 
33
33
  /** Best-effort command runner (git/gh introspection). Never throws. */
34
34
  function run(cmd, args, cwd = DIR) {
@@ -674,6 +674,9 @@ createServer(async (req, res) => {
674
674
  const cutoff = Date.now() - windowMs;
675
675
  const byComponent = new Map();
676
676
  const interactions = [];
677
+ let bursts = 0;
678
+ let worstFps = 60;
679
+ const pages = new Set();
677
680
  for (const line of raw.split('\n')) {
678
681
  if (!line) continue;
679
682
  let e;
@@ -683,51 +686,84 @@ createServer(async (req, res) => {
683
686
  continue;
684
687
  }
685
688
  if ((e.at ?? 0) < cutoff) continue;
689
+ if (e.page) pages.add(e.page);
686
690
  if (e.kind === 'interaction') interactions.push(e);
691
+ if (e.kind === 'long-render') {
692
+ bursts += 1;
693
+ if (typeof e.fps === 'number' && e.fps > 0) worstFps = Math.min(worstFps, e.fps);
694
+ }
687
695
  for (const c of e.components ?? []) {
688
696
  const cur = byComponent.get(c.name) ?? {
689
697
  name: c.name,
690
698
  renders: 0,
691
699
  selfTime: 0,
692
700
  source: c.source ?? null,
693
- unnecessary: 0,
701
+ element: c.element ?? null,
702
+ pages: new Set(),
703
+ memoizableRenders: 0,
694
704
  changes: {},
695
705
  };
696
706
  cur.renders += c.renders ?? 1;
697
707
  cur.selfTime += c.selfTime ?? 0;
698
- if (c.source) cur.source = c.source;
699
- if (c.unnecessary) cur.unnecessary += c.renders ?? 1;
708
+ cur.source ??= c.source ?? null;
709
+ cur.element ??= c.element ?? null;
710
+ if (e.page) cur.pages.add(e.page);
711
+ if (c.memoizable) cur.memoizableRenders += c.renders ?? 1;
700
712
  for (const ch of c.changes ?? []) cur.changes[ch] = (cur.changes[ch] ?? 0) + 1;
701
713
  byComponent.set(c.name, cur);
702
714
  }
703
715
  }
704
- const components = [...byComponent.values()].sort((a, b) => b.selfTime - a.selfTime).slice(0, 25);
716
+ const components = [...byComponent.values()]
717
+ .sort((a, b) => b.selfTime - a.selfTime)
718
+ .slice(0, 25)
719
+ .map((c) => ({ ...c, pages: [...c.pages] }));
705
720
  const md = [
706
721
  `# react-scan report (last ${Math.round(windowMs / 1000)}s)`,
707
722
  '',
723
+ `Target 60 FPS — worst observed: ${worstFps} FPS across ${bursts} slow-frame burst(s).` +
724
+ (pages.size ? ` Pages: ${[...pages].join(', ')}` : ''),
725
+ '',
726
+ '## Components (fix top-down; each line = who, where, and WHY it re-rendered)',
708
727
  ...components.map(
709
728
  (c) =>
710
729
  `- **${c.name}** — ${c.renders} renders · ${c.selfTime.toFixed(1)}ms self` +
711
- (c.unnecessary ? ` · ${c.unnecessary} unnecessary` : '') +
712
- (c.source ? ` · \`${c.source}\`` : '') +
730
+ (c.memoizableRenders
731
+ ? ` · ${c.memoizableRenders} renders with ZERO changes (memo() candidate)`
732
+ : '') +
733
+ (c.source ? ` · src \`${c.source}\`` : '') +
734
+ (c.element ? ` · el \`${c.element}\`` : '') +
735
+ (c.pages.length ? ` · on ${c.pages.join(', ')}` : '') +
713
736
  (Object.keys(c.changes).length
714
737
  ? ` · causes: ${Object.entries(c.changes)
715
738
  .sort((a, b) => b[1] - a[1])
716
- .slice(0, 3)
739
+ .slice(0, 4)
717
740
  .map(([k, v]) => `${k}×${v}`)
718
- .join(', ')}`
741
+ .join(', ')} — (fn)=unstable callback→useCallback, (ref)=new identity→useMemo/hoist`
719
742
  : ''),
720
743
  ),
721
744
  '',
722
- ...interactions
723
- .slice(-10)
724
- .map(
725
- (i) =>
726
- `- interaction ${i.type ?? '?'} on ${i.target ?? '?'} — ${Math.round(i.duration ?? 0)}ms` +
727
- (i.slow ? ' ⚠ SLOW' : ''),
728
- ),
745
+ '## Recent interactions (what the user did → what re-rendered because of it)',
746
+ ...interactions.slice(-10).flatMap((i) => [
747
+ `- ${i.type ?? '?'} on **${i.target ?? '?'}**${i.page ? ` (${i.page})` : ''} — ${Math.round(i.duration ?? 0)}ms` +
748
+ (i.slow ? ' ⚠ SLOW (>150ms INP)' : ''),
749
+ ...(i.components ?? [])
750
+ .slice(0, 4)
751
+ .map(
752
+ (c) =>
753
+ ` ↳ ${c.name} ×${c.renders} · ${(c.selfTime ?? 0).toFixed(1)}ms` +
754
+ (c.changes?.length ? ` · ${c.changes.join(', ')}` : '') +
755
+ (c.source ? ` · \`${c.source}\`` : ''),
756
+ ),
757
+ ]),
729
758
  ].join('\n');
730
- return json(res, 200, { report: md, components, interactions: interactions.slice(-20) });
759
+ return json(res, 200, {
760
+ report: md,
761
+ worstFps,
762
+ bursts,
763
+ pages: [...pages],
764
+ components,
765
+ interactions: interactions.slice(-20),
766
+ });
731
767
  }
732
768
  // Reset the staging branch: delete + recreate from the default branch.
733
769
  if (req.method === 'POST' && url.pathname === '/branch/reset') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linear-grab-bridge",
3
- "version": "0.23.1",
3
+ "version": "0.24.1",
4
4
  "description": "Local bridge for Linear Grab — delegate issues from the browser panel to headless Claude Code sessions running in your repo, with live status and an upload relay.",
5
5
  "type": "module",
6
6
  "bin": {