← Layout Lens
API
Tokens

Run Layout Lens from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page scans the Dart - every call matched to its closing bracket, every widget linked to its parent, and each widget's incoming width and height worked out as bounded, unbounded or unknown - for lists with nothing to fill, Expanded and Positioned in the wrong parent, double.infinity with no edge and rows that overflow the narrowest screen (measured), and ties a pasted error to its line, then hands it all to the run as facts. The run writes the review or the rewrite; the page then holds it to its own facts and re-scans a corrected widget tree with the same scanner (layoutreply.js).

Base URL and the response shape

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"layout-lens"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required key (task, screen, code, facts) is missing. The page always sends all six of task, screen, code, error, carry, facts; error and carry are plain sentences when there is nothing to send.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Two lanes, chosen by task, in the order the work runs - inspect, then produce:

A missing or unknown task is answered as review. Both lanes send the same six fields. The facts are the page's scan, one finding per line with its id, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the review's decisions into carry for the rewrite lane. A pasted error longer than 3,000 characters is sent as its head plus the error-causing widget line. The review sends at most 24,000 characters of code (the middle of a longer file is cut on whole lines and marked; every finding is still in the facts); the rewrite lane takes at most 16,000.

The input

FieldRequiredWhat
taskyesreview or rewrite. If it is missing or neither, do review and set "task": "review".
screenyesthe screen's name, the narrowest screen it ships to, and the widget class the page tests.
codeyesthe Flutter code of the screen. A line starting "// [... N lines cut by the page" marks code the page scanned but did not send.
errorsend it (a sentence when none)the layout error Flutter printed, as pasted, or a sentence saying none was pasted. A line starting "[... N lines of the error cut by the page" marks a cut.
carrysend it (a sentence when none)rewrite task: the review's decisions, one per line as id | confirmed or cleared or extra | widget | fix or reason; otherwise a sentence saying nothing was carried.
factsyesthe page's scan: one finding per line as id | area | severity | line | widget | evidence | path | source, the rows it measured (line | fixed children | room | result), what it read in the pasted error and which finding it matches, and what is present. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it.

The rules the model applies

From the source skill, @flutter/flutter-building-layouts - the only standard the run applies:

House rules

The replies

review

Confirm or clear each of the page's findings, say what throws the pasted error, add what a scan cannot see, and put the fixes in order.

  1. summary - one sentence, at most 40 words: what a person opening this screen meets today (a red error screen, a striped overflow, a list that never scrolls) and the first fix.
  2. cause - when an error was pasted: one sentence naming the finding id and line that throws it (the facts say which finding the page matched) and, if another finding throws right after it, that one too. "" when no error was pasted.
  3. findings - one object per page finding you confirm, in id order: id (the page's id), widget (which widget, e.g. "ListView.builder - the post list"), issue (one sentence: what the person sees - a red error screen, content cut off at the edge, a list that will not scroll), fix (one sentence: the change), code (the corrected Dart for that piece only, at most 6 lines; "" when the fix says it all).
  4. cleared - one object per page finding that is not a problem in this code, with id and why (one sentence tied to the code - for example the widget is always placed inside a parent that bounds it). Every page id appears exactly once, in findings or in cleared.
  5. extra - at most 5 issues the scan cannot see, each tied to a line of the code: id (X1, X2, ...), area (unbounded, parentdata, overflow, scrolling, structure or adaptive), line (a number), widget, issue, fix. Examples: a Column whose content is taller than a small phone and has no scroll view, a Text with no maxLines in a fixed-height box, a long list built with children instead of a builder, a layout that ignores the keyboard, a wide layout that never switches to a narrow one. [] when there are none.
  6. plan - 2 to 10 short strings, the fixes in the order to make them (what throws first, then what overflows, then polish), each naming the ids it covers (e.g. "Wrap the post list in Expanded (F3)"). Every confirmed id appears in a step. [] only when nothing is confirmed.
  7. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "review",
  "summary": "one sentence",
  "cause": "one sentence naming the id and line, or empty",
  "findings": [ { "id": "F1", "widget": "ListView - the story strip", "issue": "one sentence", "fix": "one sentence", "code": "SizedBox(height: 88, child: ListView(...))" } ],
  "cleared": [ { "id": "F4", "why": "one sentence" } ],
  "extra": [ { "id": "X1", "area": "overflow", "line": 18, "widget": "Column - the screen body", "issue": "one sentence", "fix": "one sentence" } ],
  "plan": [ "Give the story strip a height (F1)" ],
  "caveats": [ ]
}

The page holds it to its own facts (check() in layoutreply.js; any warning makes the reply flagged): the reply answers the task it was sent; every page finding id appears exactly once across findings and cleared, and no id the page never found; a cleared id has a reason; a confirmed id has an issue and a fix; a pasted error has a cause (a cause that names neither the matched id nor its line is noted); at most five extra issues, each on a line inside the paste; a fix order of 2 to 10 steps that names every confirmed id; a summary; no web link and no talk of the request's own field names in the prose.

rewrite

Rewrite the file with the fixes applied.

  1. summary - one sentence, at most 40 words: what the rewrite changes and what it leaves for the team.
  2. code - the complete rewritten file. Keep every import, class, constructor parameter, callback, string and behaviour; change only what the findings (and the carried decisions) name. Never shorten the file with "..." or "unchanged" - write every line. If the code holds a line starting "// [... N lines cut by the page", keep that line exactly where it is. A vertical list in a Column - Expanded(child: the list). If the Column itself sits in a scroll view, give the list shrinkWrap: true and physics: const NeverScrollableScrollPhysics() instead, or turn the scroll view into a CustomScrollView with slivers. A horizontal list in a Column or a vertical list - SizedBox(height: a number, child: the list). An Expanded in a Column that sits in a scroll view - remove the Expanded (or use a CustomScrollView with SliverFillRemaining(hasScrollBody: false)). An Expanded, Flexible or Spacer that is not the direct child of a Row, Column or Flex - make it the direct child (Expanded(child: Padding(...)), never Padding(child: Expanded(...))), or remove it. A Positioned outside a Stack - put it directly in a Stack's children, or replace it with Align or Padding. double.infinity in an unbounded direction - Expanded in a Row or Column, or a fixed size. A TextField in a Row - Expanded(child: the TextField). A Row whose fixed widths overflow - turn the fixed-width children into Expanded (or Flexible) children, or switch to a Column below the width the facts give, with LayoutBuilder. A Text that can run past the edge of a Row - Expanded(child: Text(..., maxLines: 1, overflow: TextOverflow.ellipsis)). A Container that only sets a size - SizedBox with the same width, height and child. IntrinsicHeight or IntrinsicWidth in a list item - remove it and keep the item's alignment with crossAxisAlignment or a fixed height. A SingleChildScrollView in a Column - Expanded(child: the SingleChildScrollView). A side panel beside an Expanded child at every width - LayoutBuilder: the Row when constraints.maxWidth > 600, otherwise a Column.
  3. changes - one object per page finding you fixed, in id order: id, change (one sentence: what changed, naming the widget). Extra ids (X1, ...) from the carried review may appear too.
  4. kept - one object per page finding you did not change: id, why (one sentence - the review cleared it, or it needs a decision the code cannot make). Every page id appears exactly once, in changes or in kept; an id the carried review cleared belongs in kept.
  5. caveats - at most 4 short strings; [] when there is nothing to say.
{
  "task": "rewrite",
  "summary": "one sentence",
  "code": "the complete rewritten Dart file",
  "changes": [ { "id": "F1", "change": "one sentence" } ],
  "kept": [ { "id": "F4", "why": "one sentence" } ],
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; every page finding id appears exactly once across changes and kept; every widget class of the paste survives; the file is not shortened (at least 70% of its lines) and elides nothing; and the corrected code is scanned again - a rule the changes claim fixed that still fires, or a rule the paste never had, is flagged. A Text string the rewrite drops is noted.

Worked examples

Three runs from the page's free samples (invented apps): the review of a feed that throws because two lists sit in a Column, the rewrite of the same screen carrying that review, and the review of a profile whose button row overflows a phone by 26 dp. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the code, the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.

review - A feed that throws: two lists in a Column

The page read it as: Throws a layout error - 2 blocker, 0 major, 2 minor; findings F1 viewport-unbounded, F2 container-size-only, F3 viewport-unbounded, F4 text-in-row.

Input (code is 1678 characters; the first 1392 are shown; facts is 2519 characters; the first 1399 are shown):

{
 "task": "review",
 "screen": "Screen: Feed\nNarrowest screen: a phone (360 dp)\nScreen widget: FeedScreen",
 "code": "import 'package:flutter/material.dart';\n\nclass Post {\n  const Post(this.author, this.title);\n  final String author;\n  final String title;\n}\n\nclass FeedScreen extends StatelessWidget {\n  const FeedScreen({super.key, required this.posts, required this.stories});\n  final List<Post> posts;\n  final List<String> stories;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Feed')),\n      body: Column(\n        children: [\n          const Padding(\n            padding: EdgeInsets.all(16),\n            child: Text('Stories', style: TextStyle(fontWeight: FontWeight.bold)),\n          ),\n          ListView(\n            scrollDirection: Axis.horizontal,\n            children: [\n              for (final name in stories)\n                Container(\n                  width: 72,\n                  height: 72,\n                  child: CircleAvatar(child: Text(name[0])),\n                ),\n            ],\n          ),\n          ListView.builder(\n            itemCount: posts.length,\n            itemBuilder: (context, i) => _PostTile(post: posts[i]),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _PostTile extends StatelessWidget {\n  const _PostTile({required this.post});\n  final Post post;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),\n\n[... 286 more characters cut from this display ...]",
 "error": "======== Exception caught by rendering library =====================================\nThe following assertion was thrown during performResize():\nHorizontal viewport was given unbounded height.\n\nViewports expand in the cross axis to fill their container and constrain their children to match their extent in the cross axis. In this case, a horizontal viewport was given an unlimited amount of vertical space in which to expand.\n\nThe relevant error-causing widget was:\n  ListView ListView:file:///Users/dev/social_app/lib/feed_screen.dart:24:11\n\nWhen the exception was thrown, this was the stack:\n#0      RenderViewport.computeDryLayout.<anonymous closure> (package:flutter/src/rendering/viewport.dart:1453:15)\n#1      RenderViewport.computeDryLayout (package:flutter/src/rendering/viewport.dart:1514:6)\n...\n====================================================================================",
 "carry": "Nothing is carried into a review - it works from the code and the facts.",
 "facts": "The page's scan of 65 lines (26 calls) at the narrowest width, a phone (360 dp); verdict: Throws a layout error - 2 blocker, 0 major, 2 minor.\nFindings (id | area | severity | line | where | evidence | path | source):\n- F1 | unbounded | blocker | line 24 | ListView | ListView gets unbounded height across its scrolling direction: a Column gives its children unbounded height (Column, line 18). shrinkWrap does not help across the axis: 'Horizontal viewport was given unbounded height.' | path: Scaffold.body (line 16) > Column (line 18) > ListView (line 24) | source: ListView(\n- F2 | structure | minor | line 28 | Container | This Container only sets a size (width: 72, height: 72) - the skill uses SizedBox for a tight size and keeps Container for padding, margins, borders and colour. | path: Scaffold.body (line 16) > Column (line 18) > ListView (line 24) > Container (line 28) | source: Container(\n- F3 | unbounded | blocker | line 35 | ListView.builder | ListView.builder gets unbounded height in its scrolling direction: a Column gives its children unbounded height (Column, line 18). A viewport expands to fill its parent, and there is nothing to fill: 'Vertical viewport was given unbounded height.' | path: Scaffold.body (line 16) > Column (line 18) > ListView.builder (line 35) | source: ListView.builder(\n- F4 | overflow | minor | line 57 | Text | Text(post.title) is in the Row on line\n\n[... 1120 more characters cut from this display ...]"
}

Reply:

{
 "task": "review",
 "summary": "The feed opens on a red error screen: the story strip and the post list both sit in a Column with no height to fill. Give the strip a fixed height and let the post list fill the rest with Expanded.",
 "cause": "F1 on line 24 throws it - the horizontal story ListView gets unbounded height from the Column on line 18; once that is fixed, F3, the ListView.builder on line 35, throws the vertical form of the same error.",
 "findings": [
  {
   "id": "F1",
   "widget": "ListView - the horizontal story strip",
   "issue": "The person sees an error screen instead of the feed, because a sideways list has no height of its own and the Column offers it unlimited height.",
   "fix": "Wrap the story ListView in a SizedBox with a fixed height of 88 (my number: a 72 dp avatar plus room around it).",
   "code": "SizedBox(\n  height: 88,\n  child: ListView(\n    scrollDirection: Axis.horizontal,\n    children: [/* the stories */],\n  ),\n)"
  },
  {
   "id": "F2",
   "widget": "Container - each story avatar's box",
   "issue": "Nothing breaks, but a Container that only sets a size hides the intent that the avatar box is a fixed 72 by 72.",
   "fix": "Use SizedBox with the same width, height and child.",
   "code": "SizedBox(width: 72, height: 72, child: CircleAvatar(child: Text(name[0])))"
  },
  {
   "id": "F3",
   "widget": "ListView.builder - the post list",
   "issue": "Right after the strip is fixed, the post list throws the same way, since it also asks the Column for unlimited height.",
   "fix": "Wrap the ListView.builder in Expanded so it takes the Column's remaining height - the skill's own fix for a list in a Column.",
   "code": "Expanded(\n  child: ListView.builder(\n    itemCount: posts.length,\n    itemBuilder: (context, i) => _PostTile(post: posts[i]),\n  ),\n)"
  },
  {
   "id": "F4",
   "widget": "Text - the post title in _PostTile",
   "issue": "A long title is laid out on one line at its full width and pushes past the right edge, so the person sees a striped overflow and loses the like button.",
   "fix": "Wrap the title in Expanded with maxLines: 1 and an ellipsis, and drop the Spacer, which would otherwise take half of the free width.",
   "code": "Expanded(\n  child: Text(post.title, maxLines: 1, overflow: TextOverflow.ellipsis),\n)"
  }
 ],
 "cleared": [],
 "extra": [
  {
   "id": "X1",
   "area": "adaptive",
   "line": 18,
   "widget": "Column - the feed body",
   "issue": "On a tablet or desktop window each post row stretches across the whole width, so titles and like buttons end up far apart.",
   "fix": "Centre the post list and cap it with ConstrainedBox(constraints: BoxConstraints(maxWidth: 600)) when the window is wider than 600 dp."
  }
 ],
 "plan": [
  "Give the story strip a fixed height with SizedBox (F1)",
  "Wrap the post list in Expanded so it fills the rest of the Column (F3)",
  "Wrap each post title in Expanded with an ellipsis and remove the Spacer (F4)",
  "Replace the size-only Container around each avatar with SizedBox (F2)"
 ],
 "caveats": [
  "The 88 dp strip height is a chosen value; match it to the avatar size the design uses."
 ]
}

What step 5 prints for it: F1: Wrap the story ListView in a SizedBox with a fixed height of 88 (my number: a 72 dp avatar plus room around it). | F2: Use SizedBox with the same width, height and child. | F3: Wrap the ListView.builder in Expanded so it takes the Column's remaining height - the skill's own fix for a list in a Column. | F4: Wrap the title in Expanded with maxLines: 1 and an ellipsis, and drop the Spacer, which would otherwise take half of the free width.

The page's check: kept - nothing to flag

rewrite - A feed that throws: two lists in a Column

The input carries the saved review (R.laneScreen fills carry from the review reply), as the page's handoff does. The page re-scanned the corrected code: 4 findings before, 0 after.

Input (code is 1678 characters; the first 1392 are shown; facts is 2519 characters; the first 1399 are shown):

{
 "task": "rewrite",
 "screen": "Screen: Feed\nNarrowest screen: a phone (360 dp)\nScreen widget: FeedScreen",
 "code": "import 'package:flutter/material.dart';\n\nclass Post {\n  const Post(this.author, this.title);\n  final String author;\n  final String title;\n}\n\nclass FeedScreen extends StatelessWidget {\n  const FeedScreen({super.key, required this.posts, required this.stories});\n  final List<Post> posts;\n  final List<String> stories;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Feed')),\n      body: Column(\n        children: [\n          const Padding(\n            padding: EdgeInsets.all(16),\n            child: Text('Stories', style: TextStyle(fontWeight: FontWeight.bold)),\n          ),\n          ListView(\n            scrollDirection: Axis.horizontal,\n            children: [\n              for (final name in stories)\n                Container(\n                  width: 72,\n                  height: 72,\n                  child: CircleAvatar(child: Text(name[0])),\n                ),\n            ],\n          ),\n          ListView.builder(\n            itemCount: posts.length,\n            itemBuilder: (context, i) => _PostTile(post: posts[i]),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _PostTile extends StatelessWidget {\n  const _PostTile({required this.post});\n  final Post post;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),\n\n[... 286 more characters cut from this display ...]",
 "error": "======== Exception caught by rendering library =====================================\nThe following assertion was thrown during performResize():\nHorizontal viewport was given unbounded height.\n\nViewports expand in the cross axis to fill their container and constrain their children to match their extent in the cross axis. In this case, a horizontal viewport was given an unlimited amount of vertical space in which to expand.\n\nThe relevant error-causing widget was:\n  ListView ListView:file:///Users/dev/social_app/lib/feed_screen.dart:24:11\n\nWhen the exception was thrown, this was the stack:\n#0      RenderViewport.computeDryLayout.<anonymous closure> (package:flutter/src/rendering/viewport.dart:1453:15)\n#1      RenderViewport.computeDryLayout (package:flutter/src/rendering/viewport.dart:1514:6)\n...\n====================================================================================",
 "carry": "F1 | confirmed | ListView - the horizontal story strip | Wrap the story ListView in a SizedBox with a fixed height of 88 (my number: a 72 dp avatar plus room around it).\nF2 | confirmed | Container - each story avatar's box | Use SizedBox with the same width, height and child.\nF3 | confirmed | ListView.builder - the post list | Wrap the ListView.builder in Expanded so it takes the Column's remaining height - the skill's own fix for a list in a Column.\nF4 | confirmed | Text - the post title in _PostTile | Wrap the title in Expanded with maxLines: 1 and an ellipsis, and drop the Spacer, which would otherwise take half of the free width.\nX1 | extra | Column - the feed body (line 18) | Centre the post list and cap it with ConstrainedBox(constraints: BoxConstraints(maxWidth: 600)) when the window is wider than 600 dp.",
 "facts": "The page's scan of 65 lines (26 calls) at the narrowest width, a phone (360 dp); verdict: Throws a layout error - 2 blocker, 0 major, 2 minor.\nFindings (id | area | severity | line | where | evidence | path | source):\n- F1 | unbounded | blocker | line 24 | ListView | ListView gets unbounded height across its scrolling direction: a Column gives its children unbounded height (Column, line 18). shrinkWrap does not help across the axis: 'Horizontal viewport was given unbounded height.' | path: Scaffold.body (line 16) > Column (line 18) > ListView (line 24) | source: ListView(\n- F2 | structure | minor | line 28 | Container | This Container only sets a size (width: 72, height: 72) - the skill uses SizedBox for a tight size and keeps Container for padding, margins, borders and colour. | path: Scaffold.body (line 16) > Column (line 18) > ListView (line 24) > Container (line 28) | source: Container(\n- F3 | unbounded | blocker | line 35 | ListView.builder | ListView.builder gets unbounded height in its scrolling direction: a Column gives its children unbounded height (Column, line 18). A viewport expands to fill its parent, and there is nothing to fill: 'Vertical viewport was given unbounded height.' | path: Scaffold.body (line 16) > Column (line 18) > ListView.builder (line 35) | source: ListView.builder(\n- F4 | overflow | minor | line 57 | Text | Text(post.title) is in the Row on line\n\n[... 1120 more characters cut from this display ...]"
}

Reply (long strings shortened for display - see the cuts below):

{
 "task": "rewrite",
 "summary": "The story strip now has a fixed 88 dp height, the post list fills the rest of the Column through Expanded, titles ellipsize, and avatar boxes use SizedBox; the wide-screen layout is left for the team.",
 "code": "import 'package:flutter/material.dart';\n\nclass Post {\n  const Post(this.author, this.title);\n  final String author;\n  final String title;\n}\n\nclass FeedScreen extends StatelessWidget {\n  const FeedScreen({super.key, required this.posts, required this.stories});\n  final List<Post> posts;\n  final List<String> stories; [... 1557 more characters cut from this display]",
 "changes": [
  {
   "id": "F1",
   "change": "Wrapped the horizontal story ListView in SizedBox(height: 88) so it no longer asks the Column for unlimited height."
  },
  {
   "id": "F2",
   "change": "Replaced the size-only Container around each story avatar with a SizedBox of the same 72 by 72."
  },
  {
   "id": "F3",
   "change": "Wrapped the ListView.builder in Expanded so the post list takes the Column's remaining height."
  },
  {
   "id": "F4",
   "change": "Wrapped the post title in Expanded with maxLines: 1 and an ellipsis, and removed the Spacer."
  }
 ],
 "kept": [],
 "caveats": [
  "The wider-screen layout from the review (X1) is not part of this rewrite; it needs a design decision on the maximum width."
 ]
}

Cut from this display only (the saved reply and the check use the full text): code (1873 characters, 316 shown).

What step 5 prints for it: F1: Wrapped the horizontal story ListView in SizedBox(height: 88) so it no longer asks the Column for unlimited height. | F2: Replaced the size-only Container around each story avatar with a SizedBox of the same 72 by 72. | F3: Wrapped the ListView.builder in Expanded so the post list takes the Column's remaining height. | F4: Wrapped the post title in Expanded with maxLines: 1 and an ellipsis, and removed the Spacer.

The page's check: kept - nothing to flag

review - A profile whose buttons overflow a phone

The page read it as: Overflows on a narrow screen - 0 blocker, 1 major, 1 minor; findings F1 text-in-row, F2 row-overflow.

Input (code is 1593 characters; the first 1381 are shown; facts is 2010 characters; the first 1397 are shown):

{
 "task": "review",
 "screen": "Screen: Profile\nNarrowest screen: a phone (360 dp)\nScreen widget: ProfileScreen",
 "code": "import 'package:flutter/material.dart';\n\nclass ProfileScreen extends StatelessWidget {\n  const ProfileScreen({super.key, required this.name, required this.bio});\n  final String name;\n  final String bio;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: SafeArea(\n        child: Padding(\n          padding: const EdgeInsets.all(16),\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Row(\n                children: [\n                  const CircleAvatar(radius: 40),\n                  const SizedBox(width: 16),\n                  Text(name, style: Theme.of(context).textTheme.headlineSmall),\n                ],\n              ),\n              const SizedBox(height: 16),\n              Text(bio),\n              const SizedBox(height: 24),\n              Row(\n                children: [\n                  SizedBox(\n                    width: 110,\n                    child: OutlinedButton(onPressed: () {}, child: const Text('Posts')),\n                  ),\n                  const SizedBox(width: 12),\n                  SizedBox(\n                    width: 110,\n                    child: OutlinedButton(onPressed: () {}, child: const Text('Followers')),\n                  ),\n                  const SizedBox(width: 12),\n                  SizedBox(\n                    width: 110,\n\n[... 212 more characters cut from this display ...]",
 "error": "======== Exception caught by rendering library =====================================\nThe following assertion was thrown during layout:\nA RenderFlex overflowed by 26 pixels on the right.\n\nThe relevant error-causing widget was:\n  Row Row:file:///Users/dev/social_app/lib/profile_screen.dart:27:15\n\nThe overflowing RenderFlex has an orientation of Axis.horizontal.\nThe edge of the RenderFlex that is overflowing has been marked in the rendering with a yellow and black striped pattern.\n====================================================================================",
 "carry": "Nothing is carried into a review - it works from the code and the facts.",
 "facts": "The page's scan of 52 lines (26 calls) at the narrowest width, a phone (360 dp); verdict: Overflows on a narrow screen - 0 blocker, 1 major, 1 minor.\nFindings (id | area | severity | line | where | evidence | path | source):\n- F1 | overflow | minor | line 21 | Text | Text(name) is in the Row on line 17 with no Expanded or Flexible, so it is laid out at its full one-line width - a long value runs past the screen edge: 'A RenderFlex overflowed by ... pixels on the right.' | path: Scaffold.body (line 10) > SafeArea (line 11) > Padding (line 12) > Column (line 14) > Row (line 17) > Text (line 21) | source: Text(name, style: Theme.of(context).textTheme.headlineSmall),\n- F2 | overflow | major | line 27 | Row | The Row's fixed children add up to SizedBox 110 + SizedBox 12 + SizedBox 110 + SizedBox 12 + SizedBox 110 = 354 dp; at 360 dp it has 360 dp less 32 dp of padding = 328 dp, so it overflows by 26 dp: 'A RenderFlex overflowed by 26 pixels on the right.' | path: Scaffold.body (line 10) > SafeArea (line 11) > Padding (line 12) > Column (line 14) > Row (line 27) | source: Row(\nRows measured by the page (line | fixed children | room | result):\n- line 17 | CircleAvatar 80 + SizedBox 16 = 96 dp | 360 dp less 32 dp of padding = 328 dp | fits with 232 dp to spare\n- line 27 | SizedBox 110 + SizedBox 12 + SizedBox 110 + SizedBox 12 + SizedBox 110 = 354 dp | 360 dp less 32 dp of padding =\n\n[... 613 more characters cut from this display ...]"
}

Reply:

{
 "task": "review",
 "summary": "On a 360 dp phone the button row overflows with yellow and black stripes, 26 dp too wide for its space; make the three buttons share the row with Expanded.",
 "cause": "F2 on line 27 throws it: three 110 dp buttons and two 12 dp gaps need 354 dp, the Row has 328 dp after the 16 dp padding on each side, and the 26 dp difference is exactly the overflow Flutter printed.",
 "findings": [
  {
   "id": "F1",
   "widget": "Text - the person's name in the header row",
   "issue": "A long name is drawn at its full width beside the avatar and runs past the right edge with an overflow stripe.",
   "fix": "Wrap the name in Expanded and give it maxLines: 1 with an ellipsis.",
   "code": "Expanded(\n  child: Text(name, maxLines: 1, overflow: TextOverflow.ellipsis),\n)"
  },
  {
   "id": "F2",
   "widget": "Row - the Posts, Followers and Following buttons",
   "issue": "The person sees the last button cut off with a striped overflow, because the fixed widths need 354 dp where the row has 328 dp.",
   "fix": "Replace each SizedBox(width: 110) with Expanded so the three buttons share the 304 dp left after the two gaps.",
   "code": "Expanded(\n  child: OutlinedButton(onPressed: () {}, child: const Text('Posts')),\n)"
  }
 ],
 "cleared": [],
 "extra": [
  {
   "id": "X1",
   "area": "overflow",
   "line": 25,
   "widget": "Text - the bio",
   "issue": "The Column has no scroll view, so a long bio on a small phone pushes content past the bottom of the screen.",
   "fix": "Wrap the Column in a SingleChildScrollView, or limit the bio with maxLines and an ellipsis."
  }
 ],
 "plan": [
  "Let the three profile buttons share the row with Expanded instead of fixed 110 dp widths (F2)",
  "Wrap the name in Expanded with an ellipsis (F1)"
 ],
 "caveats": [
  "At 360 dp each shared button is about 101 dp wide, so a label as long as Followers may wrap to two lines."
 ]
}

What step 5 prints for it: F1: Wrap the name in Expanded and give it maxLines: 1 with an ellipsis. | F2: Replace each SizedBox(width: 110) with Expanded so the three buttons share the 304 dp left after the two gaps.

The page's check: kept - nothing to flag

Build the input yourself

Load the engine in node (global.window = global; require("./layoutkit.js"); require("./layoutreply.js")), then:

const K = LayoutKit, R = LayoutReply;
const f = K.screen({ name: "Inbox", width: "360", carry: "", error: "",
  code: "Widget build(BuildContext c) {\n  return Scaffold(body: Column(children: [Text('Inbox'), ListView(children: []), Row(children: [Text('To'), TextField()])]));\n}" });
// f.findings (id, area, sev, line, widget, evidence, fix, path), f.scan.rows, f.err, f.checklist, f.test (the two-size widget test)
// K.findingsCsv(f), K.checklistMd(f) - the free exports
const b = K.buildInput(f, "review");                         // or "rewrite" (R.laneScreen(screen, "rewrite", review) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "review");
// R.check(reply, b)                                         // {status: kept or flags, notes}

Get a token for the steps below from /tokens.html.

Step 1 — a tiny client helper

One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"   # from https://layout-lens.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"layout-lens"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "review", "screen": "Screen: Feed
Narrowest screen: a phone (360 dp)
Screen widget: FeedScreen", "code": "import 'package:flutter/material.dart';
...", "error": "Horizontal viewport was given unbounded height. ...", "carry": "Nothing is carried into a review - it works from the code and the facts.", "facts": "The page's scan of 65 lines ...
Findings (id | area | severity | line | widget | evidence | path | source):
- F1 | unbounded | blocker | line 24 | ListView | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses layout-lens:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A review reply carries summary, cause, findings, cleared, extra, plan and caveats; a rewrite reply carries summary, code, changes, kept and caveats. The code below prints the gist of either: each confirmed finding and its fix, or each change.

KEY="layout-lens:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "rewrite" then (.changes[] | "\(.id): \(.change)") 
       else (.findings[] | "\(.id): \(.fix)") end' reply.json

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: layout-lens:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json