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.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The 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. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. 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:
review- each of the page's findings confirmed (with the issue, the fix and a short code fix) or cleared (with why), the finding and line behind the pasted error, up to five issues a scan cannot see on real lines, and the fixes in the order to make them, each naming its ids.rewrite- the whole corrected widget file - lists bounded, Expanded and Positioned in the right parents, rows that fit the narrowest screen - with what changed and what was kept.
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
| Field | Required | What |
|---|---|---|
task | yes | review or rewrite. If it is missing or neither, do review and set "task": "review". |
screen | yes | the screen's name, the narrowest screen it ships to, and the widget class the page tests. |
code | yes | the Flutter code of the screen. A line starting "// [... N lines cut by the page" marks code the page scanned but did not send. |
error | send 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. |
carry | send 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. |
facts | yes | the 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_note | no | optional; 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:
- Constraints go down, sizes go up, the parent sets the position. A widget cannot choose its own size outside the constraints its parent passes down, and a child never sets its own position.
- Never pass an unbounded constraint in a flex box's axes or inside a scrollable region. A Row gives its children unbounded width; a Column, a ListView and a vertical SingleChildScrollView give their children unbounded height. A ListView, GridView or PageView placed there throws; so does double.infinity, an Expanded with nothing to share, or a TextField with unbounded width.
- Row lays children out horizontally, Column vertically; align them with mainAxisAlignment and crossAxisAlignment.
- Expanded makes a Row's or Column's child fill the remaining space; Flexible lets it size itself up to that space. Both work only as the direct child of a Row, Column or Flex.
- Container is for padding, margins, borders and background colour. SizedBox with an explicit width or height is for a tight size.
- Stack overlaps widgets; Positioned anchors a child to the Stack's edges and works only as the direct child of a Stack.
- Responsive: LayoutBuilder, Expanded and Flexible size the layout from its parent's constraints. Adaptive: swap whole structures by width - a side rail and a Row beside the content when constraints.maxWidth > 600, a Column and bottom navigation on a phone.
- The workflow: break the screen into rows, columns and grids; mark overlap (Stack) and scrolling regions; plan which widgets need tight and which loose constraints and where an unbounded constraint could arise (a ListView inside a Column); build from the Scaffold inward and extract deeply nested sections into their own widgets; validate on target sizes. If something overflows, wrap the overflowing widget in Expanded inside a flex box, or wrap its parent in a scrollable.
- The skill's own example: a ListView placed directly in a Column throws an unbounded height error; wrap the ListView in Expanded so it takes the Column's remaining height.
House rules
- Use only the input. Never invent a widget, a callback, a value or a string the code does not hold. When a fix needs a number the code does not give (a list's height), choose a plain one and name it as yours in the fix or change.
- The page's ids, lines and measurements are facts: never renumber an id, and use the page's arithmetic for a row's widths rather than your own.
- Write about the screen and the person using it. Never mention the request's plumbing - not its field names, not that something was or was not carried or pasted.
- No web links (http or https) in prose.
- Write in English. Keep the JSON keys and the ids exactly as given.
- Never use backticks. Name widgets and parameters plainly (Expanded, shrinkWrap:). Words from the rules above are free to use; what you never do is paste a whole sentence from this prompt.
- Never write a gendered pronoun for a user; write they, the user or the person.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
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.
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.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.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).cleared- one object per page finding that is not a problem in this code, withidandwhy(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.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.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.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.
summary- one sentence, at most 40 words: what the rewrite changes and what it leaves for the team.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.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.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.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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://layout-lens.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// layout-lens.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://layout-lens.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from https://layout-lens.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// LayoutLens.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class LayoutLens {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://layout-lens.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://layout-lens.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN"; // from https://layout-lens.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "YOUR_TOKEN"; // from https://layout-lens.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "layout-lens"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "layout-lens" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "layout-lens"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "layout-lens"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "layout-lens" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "layout-lens"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "layout-lens" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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}'
inp = json.load(open("input.json")) # {"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
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "review" or "rewrite", screen, code, error, carry, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
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
import hashlib, time
key = "layout-lens:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply["task"])
if reply["task"] == "rewrite":
for x in reply["changes"]:
print(x["id"], x["change"])
else:
for x in reply["findings"]:
print(x["id"], x["fix"])
import { createHash } from "node:crypto";
const key = `layout-lens:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.task);
if (reply.task === "rewrite") { for (const x of reply.changes) console.log(x.id, x.change); }
else { for (const x of reply.findings) console.log(x.id, x.fix); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "layout-lens:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"])
key, b := "findings", "fix"
if reply["task"] == "rewrite" {
key, b = "changes", "change"
}
xs, _ := reply[key].([]any)
for _, x := range xs {
xx := x.(map[string]any)
fmt.Println(xx["id"], xx[b])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "layout-lens:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.path("task").asText());
if (reply.path("task").asText().equals("rewrite")) {
for (JsonNode x : reply.path("changes")) System.out.println(x.path("id").asText() + " " + x.path("change").asText());
} else {
for (JsonNode x : reply.path("findings")) System.out.println(x.path("id").asText() + " " + x.path("fix").asText());
}
require "digest"
key = "layout-lens:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["task"]].inspect
if reply["task"] == "rewrite"
reply["changes"].each { |x| puts "#{x["id"]} #{x["change"]}" }
else
reply["findings"].each { |x| puts "#{x["id"]} #{x["fix"]}" }
end
$key = "layout-lens:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["task"], "\n";
if ($reply["task"] === "rewrite") { foreach ($reply["changes"] as $x) echo $x["id"], " ", $x["change"], "\n"; }
else { foreach ($reply["findings"] as $x) echo $x["id"], " ", $x["fix"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"layout-lens:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "rewrite") { foreach (var x in reply["changes"]!.AsArray()) Console.WriteLine($"{x!["id"]} {x["change"]}"); }
else { foreach (var x in reply["findings"]!.AsArray()) Console.WriteLine($"{x!["id"]} {x["fix"]}"); }
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
rev = json.load(open("input.json")) # the same run input as input.json: {"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
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "layout-lens:" + rev["task"] + ":" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `layout-lens:${rev.task}:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "layout-lens:"+input["task"].(string)+":"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "layout-lens:" + input.get("task") + ":" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "layout-lens:#{JSON.parse(rev)["task"]}:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: layout-lens:" . json_decode($rev, true)["task"] . ":" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"layout-lens:{input["task"]}:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);