makersclaw 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +8 -1
  2. package/package.json +1 -1
  3. package/r/annotate.json +4 -4
  4. package/r/approval-request.json +2 -2
  5. package/r/audio-player.json +2 -2
  6. package/r/auth-form.json +2 -2
  7. package/r/board.json +2 -2
  8. package/r/calendar-view.json +3 -3
  9. package/r/cart.json +2 -2
  10. package/r/chart.json +5 -5
  11. package/r/chat.json +3 -3
  12. package/r/code-editor.json +2 -2
  13. package/r/comment-box.json +3 -3
  14. package/r/contact-section.json +2 -2
  15. package/r/core.json +7 -1
  16. package/r/csv-import.json +3 -3
  17. package/r/data-table.json +2 -2
  18. package/r/diagram.json +2 -2
  19. package/r/doc-editor.json +2 -2
  20. package/r/doc-page.json +3 -3
  21. package/r/email-render.json +2 -2
  22. package/r/faq.json +2 -2
  23. package/r/file-upload.json +2 -2
  24. package/r/flow-canvas.json +3 -3
  25. package/r/footer.json +3 -3
  26. package/r/gantt.json +3 -3
  27. package/r/job-panel.json +2 -2
  28. package/r/json-editor.json +3 -3
  29. package/r/map.json +2 -2
  30. package/r/menubar.json +2 -2
  31. package/r/mind-map.json +4 -4
  32. package/r/navigation-menu.json +2 -2
  33. package/r/order-detail.json +2 -2
  34. package/r/org-chart.json +4 -4
  35. package/r/page-builder.json +3 -3
  36. package/r/payment-methods.json +2 -2
  37. package/r/pdf-document.json +2 -2
  38. package/r/pdf-viewer.json +3 -3
  39. package/r/product-detail.json +2 -2
  40. package/r/product-grid.json +3 -3
  41. package/r/record-form.json +2 -2
  42. package/r/record-notes.json +4 -4
  43. package/r/registry.json +126 -121
  44. package/r/search-dialog.json +2 -2
  45. package/r/settings-list.json +2 -2
  46. package/r/setup-checklist.json +2 -2
  47. package/r/share-dialog.json +2 -2
  48. package/r/sidebar.json +3 -3
  49. package/r/signature-pad.json +2 -2
  50. package/r/sketch-canvas.json +3 -3
  51. package/r/spreadsheet.json +2 -2
  52. package/r/stat-cards.json +3 -3
  53. package/r/task-list.json +3 -3
  54. package/r/task-model.json +2 -2
  55. package/r/testimonials.json +2 -2
  56. package/r/transaction-list.json +2 -2
  57. package/r/whiteboard.json +4 -4
  58. package/registry/blocks/annotate/README.md +14 -11
  59. package/registry/blocks/chart/cartesian.jsx +1 -1
  60. package/registry/blocks/chart/core.jsx +33 -23
  61. package/registry/blocks/chart/pie.jsx +1 -1
  62. package/registry/blocks/chat/README.md +4 -1
  63. package/registry/blocks/chat/demo.jsx +2 -2
  64. package/registry/blocks/flow-canvas/README.md +15 -8
  65. package/registry/blocks/footer/README.md +3 -2
  66. package/registry/blocks/mind-map/README.md +4 -2
  67. package/registry/blocks/org-chart/README.md +4 -2
  68. package/registry/blocks/page-builder/README.md +1 -1
  69. package/registry/blocks/page-builder/demo.jsx +3 -3
  70. package/registry/blocks/pdf-viewer/README.md +4 -1
  71. package/registry/blocks/sidebar/README.md +23 -13
  72. package/registry/blocks/sketch-canvas/README.md +15 -9
  73. package/registry/blocks/whiteboard/README.md +20 -14
  74. package/src/cli.mjs +2 -2
  75. package/src/install.mjs +23 -6
  76. package/src/registry.mjs +34 -3
  77. package/templates/next/files/README.md +291 -11
  78. package/templates/next/files/app/shell.jsx +54 -37
  79. package/templates/next/files/check/run.mjs +76 -4
  80. package/templates/next/files/components/ui/index.js +1 -0
  81. package/templates/next/files/components/ui/layout.jsx +307 -0
  82. package/templates/next/files/lib/claw.js +4 -1
  83. package/templates/next/files/lib/hooks.js +71 -2
  84. package/templates/next/files/lib/platform.js +93 -4
  85. package/templates/next/template.json +10 -1
@@ -60,7 +60,7 @@ leaves out four (Per mode, below). Which to build: §4.
60
60
  Yours — write and rewrite freely:
61
61
 
62
62
  ```
63
- app/shell.jsx the inside of the window: nav, layout, type, density
63
+ app/shell.jsx the inside of the window: nav, layout, type, density — an AppLayout (§3b, Layouts)
64
64
  app/DESIGN.md the design, written before the code (§3)
65
65
  surfaces/<key>.jsx one file per domain surface
66
66
  surfaces.js the manifest entries (import + component when built)
@@ -78,7 +78,7 @@ app/page.jsx / → the home surface (or "No surfaces yet" while the ma
78
78
  app/s/[key]/page.jsx renders the surface with that key; planned ones say so
79
79
  app/error.jsx, app/global-error.jsx, app/not-found.jsx, app/error-relay.jsx, app/auto-refresh.jsx,
80
80
  app/escape-relay.jsx, app/frame.js, app/claw-alive/route.js the app inside its window: refresh while shown (a surface opts out, §7), Escape from the desk
81
- components/ui/ the kit: Button, Field, Input, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction… (§3b, §3c)
81
+ components/ui/ the kit: AppLayout, DocumentLayout, Button, Field, Input, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction… (§3b, §3c)
82
82
  components/parts/ the parts: RecordSwitcher, LiveEmpty + LiveWork, useFixture + FixtureNotice (§3a, §3b)
83
83
  check/pins.json the fixed files by hash; `npm run check` refuses one that differs
84
84
  check/ `npm run check` (§6)
@@ -99,7 +99,7 @@ lib/brain.js the company brain from the browser, in both modes; its ho
99
99
  lib/brain-server.js the brain's reads for a server component — a server app's
100
100
  lib/claw.js the _claw API the gateway answers, from the browser
101
101
  lib/platform.js typed wrappers over lib/claw.js (Platform)
102
- lib/hooks.js the React hooks over them: useJob, useArtifact… (Hooks)
102
+ lib/hooks.js the React hooks over them: useJob, useLlm, useArtifact… (Hooks)
103
103
  lib/sample.js markSample, for sample rows (§3a)
104
104
  ```
105
105
 
@@ -147,7 +147,12 @@ Before any code, write `app/DESIGN.md` — one screen of text:
147
147
  - Type: scale and weight within the workspace's faces (`font-sans`,
148
148
  `font-mono`, `font-display`). Numbers tabular (`data-nums` on the element).
149
149
  - Layout and nav: where the surfaces live, what the home surface shows in
150
- the first second.
150
+ the first second. Who scrolls: the shell is an `AppLayout` (§3b,
151
+ Layouts), the one owner of the window's height; say for each surface
152
+ whether its content scrolls (a list, an article) or fills (a canvas, a
153
+ grid that scrolls itself), whether it has an inspector and how wide,
154
+ and what sits in a footer that must stay in view. Decide it before
155
+ the shell, not after a long page has pushed a button out of reach.
151
156
  - Density, empty states (§3a: what each surface says while its table is
152
157
  empty), error states.
153
158
  - What makes this app look like ITS domain and not another.
@@ -159,7 +164,8 @@ to design: Agent, Runs and Activity are tabs the OS draws on the window,
159
164
  and the app's settings — model, prompt, skills, connections, schedule —
160
165
  are the window's Settings tab, the same for every artifact. The keys
161
166
  `agent`, `runs` and `activity` are reserved; a surface may not take one.
162
- The starter `app/shell.jsx` is a placeholder; replace it first.
167
+ The starter `app/shell.jsx` is a placeholder; replace it first, and
168
+ keep it an `AppLayout`.
163
169
 
164
170
  What the OS does **not** own is asking the agent for something. Those
165
171
  tabs are where the founder goes to look back; the app is where they set
@@ -256,12 +262,64 @@ import { Button, IconButton, ButtonGroup, ButtonGroupSeparator, Toggle, ToggleGr
256
262
  Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption,
257
263
  Spinner, Skeleton, Progress, Separator, Kbd, KbdGroup, AspectRatio,
258
264
  Stat, StatGroup, Delta, StatusDot, Legend, Meter, Distribution, Swatch, Steps,
259
- toast, Toaster, Depiction, Icon, ICON_NAMES, usePopoverPosition, panelClass, cx }
265
+ toast, Toaster, Depiction, Icon, ICON_NAMES, usePopoverPosition, panelClass, cx,
266
+ AppLayout, DocumentLayout }
260
267
  from "../components/ui"; // "./ui" from a component
261
268
  import { RecordSwitcher, LiveEmpty, LiveWork, liveWords, useFixture, FixtureNotice, isSample,
262
269
  Examples, useExamples } from "../components/parts";
263
270
  ```
264
271
 
272
+ - **Layouts** — who owns the window's height, and who scrolls. Every
273
+ design-first build of 2026-10-07 shipped a shell of `min-h-dvh` that
274
+ grew with its content: no pane scrolled on its own, and a footer or a
275
+ button after a long article sat past the end of the window, where
276
+ nothing could reach it. So the kit owns the window, once, and `npm
277
+ run check` refuses anything else that sizes by it (`viewport`, §6).
278
+
279
+ `AppLayout` is the app frame and the one viewport owner: the
280
+ outermost one is exactly the window's height; one inside another
281
+ fills the slot it is in instead, so there is never a second owner.
282
+ The shell is one. Slots and props:
283
+ `header` (the top bar, in view; it draws its own bottom border) ·
284
+ `navigation` (a rail from 768px, `navigationWidth` px, 176 by default;
285
+ below 768px a Sheet behind the menu button, titled `navigationLabel`;
286
+ a press on a link closes it) · `children`, the content, with
287
+ `contentMode` `scroll` (the default: its region scrolls) or `fill` (a
288
+ bounded column whose child scrolls itself: a canvas, a grid, a
289
+ spreadsheet), `contentLabel`, `contentClassName` (spacing and token
290
+ backgrounds only) and `scrollKey` (pass the route: when it changes the
291
+ content scrolls back to its top and the drawer closes) · `inspector`
292
+ (beside the content from `inspectorAt` md · lg · xl, lg by default;
293
+ `inspectorWidth` px, 360 by default, 240 to 560; `inspectorLabel`) ·
294
+ `footer` (under the content, in view) · `height` viewport · parent.
295
+ Below `inspectorAt` there is no room for two columns: in `scroll` mode
296
+ the content and the inspector share one scroll, the inspector after
297
+ the content, so everything is reachable and nothing hides behind a
298
+ button; in `fill` mode the inspector is a bounded pane under the
299
+ content (at most 45% of the height) that scrolls on its own. A
300
+ surface that needs an inspector or a fill of its own returns an
301
+ `AppLayout` as its outermost element; inside the shell it fills the
302
+ content edge to edge (the content's padding drops for it):
303
+ ```jsx
304
+ <AppLayout
305
+ header={<EditorBar draft={draft} />}
306
+ inspector={<ReviewPanel notes={notes} />}
307
+ inspectorWidth={440}
308
+ footer={<SaveState saved={saved} />}
309
+ contentClassName="p-6"
310
+ contentLabel="Draft"
311
+ >
312
+ <DraftText draft={draft} />
313
+ </AppLayout>
314
+ ```
315
+ `DocumentLayout` is a page that flows, for print and export: no
316
+ height of its own, it sits in the content and the content scrolls it.
317
+ `toolbar` (held at the top, never printed), `width` prose · page (A4)
318
+ · letter · full, `paper` (a sheet on screen, plain in print), `align`
319
+ start · center, `label`. In print the frame lets go (no bound, no
320
+ navigation, header, inspector or footer) and the document flows onto
321
+ pages. Inside either, bound a box by its parent (`h-full`, `flex-1
322
+ min-h-0`) or by a size (`max-h-96`, `ScrollArea`), never by the window.
265
323
  - `Button` — `variant` primary (one per surface, in the app's accent —
266
324
  say it: a Button with no `variant` is secondary) · secondary · quiet ·
267
325
  danger · link (inline text: no padding or height, wraps and truncates
@@ -1274,6 +1332,8 @@ import { platform } from "../lib/platform"; // or the named functions
1274
1332
  platform.me() GET _claw/me → { artifact: { id, name, vehicle, status, home }, user: { id, email }, blueprint, desk_origin }
1275
1333
  platform.ask({ text, row, how, job, list, surface, sessionId, open })
1276
1334
  POST _claw/ask → { ok, request, desk_origin }, and the column opens on it
1335
+ platform.llm(input, { system, schema, maxTokens, tier, task, signal })
1336
+ POST _claw/llm → { ok: true, text } | { ok: true, data } | { ok: false, error, code } (Calling a model)
1277
1337
  platform.requests() GET _claw/requests → { requests, run, desk_origin }
1278
1338
  platform.requests({ id }) GET _claw/requests?id=<id> → { request }
1279
1339
  platform.stopRequest(id) POST _claw/requests/<id>/stop → { ok, request }
@@ -1297,13 +1357,16 @@ didnt_finish. `line` is the one line every screen shows for it
1297
1357
  show it as it is, never a status word of your own. `app_session_id` is
1298
1358
  set only on your own requests.
1299
1359
 
1300
- Named exports: `me, ask, requests, stopRequest, openRequest,
1360
+ Named exports: `me, ask, llm, requests, stopRequest, openRequest,
1301
1361
  surfaceOfPath, startJob, cancelJob, reportError, platform`. That is the
1302
1362
  whole list. Note what is **not** there: the runs, the chat history, Run
1303
1363
  now, Pause / Resume. Those are MONITORING and they are the OS's — the
1304
1364
  window's tabs and title bar — so an app never calls `_claw/agent`,
1305
1365
  `_claw/runs`, `_claw/run`, `_claw/pause` or `_claw/resume`. Asking is the
1306
1366
  opposite thing, and it belongs here (`ask()` and `useJob`, below).
1367
+ `llm()` is neither: one call to a model, answered in the page (Calling a
1368
+ model, below). It is the one wrapper that resolves the platform's answer
1369
+ as it is, `{ ok: true, text }` and not `{ ok: true, status, data }`.
1307
1370
  `requests` is not monitoring either: it is this app's asks in flight, so
1308
1371
  an empty surface can show the work coming (§3a). `startJob` and
1309
1372
  `cancelJob` are the 0.3.x door to the same work, kept so `useJob` can
@@ -1316,8 +1379,8 @@ that exact origin — never `"*"` — and only while the app is framed.
1316
1379
 
1317
1380
  ### Hooks — `lib/hooks.js` ("use client")
1318
1381
 
1319
- Four hooks: a generic fetch over one `_claw` route, the app's identity,
1320
- a job, and the app's requests. Each owns fetching, polling (while the
1382
+ Five hooks: a generic fetch over one `_claw` route, the app's identity,
1383
+ a job, the app's requests, and a model call. Each owns fetching, polling (while the
1321
1384
  tab is visible; once a minute while `unavailable`), streaming and cleanup
1322
1385
  on unmount. A surface renders what comes back and owns every pixel.
1323
1386
 
@@ -1328,6 +1391,7 @@ useJob() → { running, status, queued, que
1328
1391
  steps, text, progress, error, sessionId, requestId, request, finishedAt,
1329
1392
  start, cancel, reset, open }
1330
1393
  useRequests({ surface, job, every = 5000 }) → { requests, live, run, loading, error, unavailable, reload, open, stop }
1394
+ useLlm() → { run, loading, result, error, unavailable } (Calling a model)
1331
1395
  jobFromRequest(request) → what useJob shows for one request (pure; for tests)
1332
1396
  ```
1333
1397
 
@@ -1504,6 +1568,198 @@ if (!r.ok) setError(r.error); // r.code: needs_answer, queue_fu
1504
1568
  It resolves once the platform took the ask (`{ ok: true, data: {
1505
1569
  request, desk_origin } }`) or refused it; it never waits for the work.
1506
1570
 
1571
+ ### Calling a model — `llm()`
1572
+
1573
+ `llm()` is for one-shot text work on what is on screen: sort these
1574
+ mails, draft this reply, translate this note, summarise this thread,
1575
+ pull the date out of this line. The answer comes back in the page, and
1576
+ the screen does what it likes with it.
1577
+
1578
+ **`llm()` for one-shot text work on what the screen holds; `ask()` for
1579
+ work that needs tools, the workspace, or more than one step.** Finding
1580
+ leads, reading the Wiki, writing rows for forty threads, anything that
1581
+ should show in the agent column and the Requests list: that is `ask()`.
1582
+ The app's agent does it, with its tools. `llm()` has none: it sees the
1583
+ words you hand it and nothing else.
1584
+
1585
+ ```js
1586
+ "use client";
1587
+ import { updateRow } from "../lib/actions";
1588
+ import { llm } from "../lib/platform";
1589
+
1590
+ // Text.
1591
+ const r = await llm(thread.body, {
1592
+ system: "Draft a short, warm reply from the founder. Plain text, no greeting.",
1593
+ task: "draft-reply",
1594
+ });
1595
+ if (r.ok) setDraft(r.text); // r.error says why not
1596
+
1597
+ // Data: a schema in, an answer checked against it out.
1598
+ const SORT = {
1599
+ type: "object",
1600
+ required: ["label", "urgent"],
1601
+ properties: { label: { enum: ["sales", "support", "billing", "other"] }, urgent: { type: "boolean" } },
1602
+ };
1603
+ const s = await llm(`From: ${mail.from}\nSubject: ${mail.subject}\n\n${mail.body}`, { system: "Sort this mail.", schema: SORT, task: "sort-mail" });
1604
+ if (s.ok) await updateRow({ source: "mail", id: mail.id, patch: { label: s.data.label, urgent: s.data.urgent } });
1605
+ ```
1606
+
1607
+ `data` is the model's JSON as it came: **a key your schema does not name
1608
+ is kept**. Read the keys you asked for, as above, and do not spread
1609
+ `data` into a row.
1610
+
1611
+ `llm(input, opts)` takes:
1612
+
1613
+ - `input` — what to work on: a string, or a conversation, `[{ role:
1614
+ "user" | "assistant", content }]`: at most 100 messages, none empty,
1615
+ the last one the user's (the one to answer). Required. An empty one
1616
+ asks nothing of the platform and resolves `bad_input`.
1617
+ - `system` — how to go about it, at most 8,000 characters.
1618
+ - `schema` — a JSON Schema for the answer, an object, at most 16 KB
1619
+ once written out. The answer then comes back as `data`, checked
1620
+ against it, not as `text`. If the model's reply misses, the platform
1621
+ sends it back once with what was wrong; a second miss is
1622
+ `schema_mismatch`. The platform runs your schema on the model's words,
1623
+ so what it may hold is bounded (below).
1624
+ - `maxTokens` — the answer's ceiling, a whole number, 1 or more. 1,024
1625
+ unless you say; over 4,096 is cut to 4,096. (It travels as
1626
+ `max_tokens`.)
1627
+ - `tier` — `"fast"` (the default) or `"smart"` for the harder job. An
1628
+ app never names a model. `smart` costs more and is slower: a long
1629
+ answer from it can run into the 60 second limit, so keep `maxTokens`
1630
+ modest there.
1631
+ - `task` — a short label for the usage line, `a-z`, `0-9` and `-`, at
1632
+ most 40 characters (`"sort-mail"`).
1633
+ - `signal` — an `AbortSignal`. An abort resolves `{ ok: false, aborted:
1634
+ true }`.
1635
+
1636
+ **What a schema may hold.** Write it out as a tree, small. The call
1637
+ refuses a schema with:
1638
+
1639
+ - `pattern` or `patternProperties` (`bad_input`): a regular expression
1640
+ run on a model's words can hang the platform. Use `enum`, `format` or
1641
+ length bounds.
1642
+ - `$ref`, `$defs` or `definitions` (`bad_input`; `$dynamicRef` and
1643
+ `$recursiveRef` too): each reference is checked in full where it is
1644
+ used, so a few can mean a tree of any size. Write the shape out where
1645
+ it is used.
1646
+ - nesting deeper than 20 levels, a list inside a list counted
1647
+ (`bad_input`).
1648
+ - more than 1,000 parts (`too_large`): every object in it, every `enum`
1649
+ value and every type listed in a `type` array. A real answer's schema
1650
+ is a few dozen.
1651
+ - a schema it cannot read, such as a `type` it does not know
1652
+ (`bad_input`).
1653
+
1654
+ A reply with so many values that checking it against the schema would
1655
+ cost too much is a miss like any other (`schema_mismatch`, and the
1656
+ model is told why): ask for fewer values, or a smaller schema. Make the
1657
+ root an object (`type: "object"`): providers' structured answers mostly
1658
+ want one, and when one refuses the format the platform asks again in
1659
+ words alone, which is less sure. Everything else in JSON Schema is let
1660
+ through.
1661
+
1662
+ It resolves, and never throws:
1663
+
1664
+ - `{ ok: true, text }` — no schema.
1665
+ - `{ ok: true, data }` — with a schema.
1666
+ - `{ ok: false, error, code, status }` — a refusal, as the platform
1667
+ sent it. Branch on `code`. **Show `error` as it is for the codes that
1668
+ speak to the member, and a plain line of your own for the rest.** The
1669
+ rest speak to you, the developer ("schema uses "pattern""), and a
1670
+ member should not read them.
1671
+
1672
+ Words for the member, shown as they are:
1673
+
1674
+ - `no_credits` (402): the workspace's wallet is empty, the workspace
1675
+ is suspended, or the Free plan's daily limit is used up.
1676
+ `member_cap` (402): the member's monthly spend cap is used up.
1677
+ `app_cap` (402): this app's weekly cap is used up. Say so and stop;
1678
+ do not retry.
1679
+ - `rate_limited` (429): 30 calls a minute per member. Wait a moment,
1680
+ then again.
1681
+ - `model_failed` (502), `try_again` (503) and `failed` (500): the
1682
+ model, the membership check or the platform could not answer just
1683
+ now. Try again.
1684
+ - `timeout` (504): a call is held to 60 seconds, a schema retry
1685
+ included, and one the clock cuts is still charged. Ask for less (a
1686
+ lower `maxTokens`, a shorter input) rather than the same again.
1687
+ - `no_member_identity` (401) and `not_a_member` (403): the page is not
1688
+ running in a member's own session (`no_member_identity` says to open
1689
+ the app from the desk again), or the member was removed from the
1690
+ workspace.
1691
+
1692
+ Words for you, not shown:
1693
+
1694
+ - `bad_input` (400): the call itself is wrong; fix the code.
1695
+ - `too_large` (413): the body is over 128 KB, `system` over 8,000
1696
+ characters, `schema` over 16 KB or 1,000 parts, or `input` over 100
1697
+ messages. Send less.
1698
+ - `schema_mismatch` (422): the model could not fit the schema, twice;
1699
+ `error` names where it missed. Loosen the schema or ask for less.
1700
+ - `cross_origin` (403): not the app's own page (a typed URL, a form
1701
+ post).
1702
+ - `not_configured` (503): the platform has no model to give.
1703
+ - `{ ok: false, unavailable: true }` — the platform has no such door (a
1704
+ claw from before it) or there is no network. Render the quiet "not
1705
+ connected yet" of every `_claw` door, or leave the button out.
1706
+
1707
+ `useLlm()` is the same call from a component: `{ run, loading, result,
1708
+ error, unavailable }`. `run(input, opts)` takes the same arguments and
1709
+ resolves the same answer; `result` is that answer when it was ok, `error`
1710
+ the refusal's words, and `unavailable` is true on a claw with no door (or
1711
+ no network), when `error` stays null: that is the quiet "not connected
1712
+ yet", not an alert, as for `useClaw`. A new run aborts the one before it,
1713
+ so a slow answer never lands over a newer one, and leaving the page
1714
+ aborts the call in flight. `error` is the sentence whatever the code, so
1715
+ a screen that must tell the money (`no_credits`, `member_cap`, `app_cap`)
1716
+ from the rest reads `code` from what `run()` resolves.
1717
+
1718
+ ```jsx
1719
+ "use client";
1720
+ import { Button, Notice } from "../components/ui";
1721
+ import { useLlm } from "../lib/hooks";
1722
+
1723
+ export default function Translate({ note }) {
1724
+ const { run, loading, result, error, unavailable } = useLlm();
1725
+ if (unavailable) return <p className="text-sm text-muted-foreground">Translation is not connected yet.</p>;
1726
+ return (
1727
+ <div className="grid gap-2">
1728
+ <Button loading={loading} onClick={() => run(`Translate to French:\n${note.text}`, { task: "translate" })}>
1729
+ Translate
1730
+ </Button>
1731
+ {error ? <Notice tone="danger">{error}</Notice> : result ? <p className="text-sm">{result.text}</p> : null}
1732
+ </div>
1733
+ );
1734
+ }
1735
+ ```
1736
+
1737
+ What to keep in mind:
1738
+
1739
+ - **Browser only, in both modes.** The call rides the member's session:
1740
+ `llm()` and `useLlm()` run in a `"use client"` file. A server
1741
+ component or a server action has no member to charge and cannot call
1742
+ it.
1743
+ - **It spends the workspace's credits**, like any inference, and counts
1744
+ against the member's monthly cap and the app's weekly cap. A schema
1745
+ call the model misses is asked again once, and both are charged; so is
1746
+ a call the 60 second clock cuts. Call it on a press, never on every
1747
+ render or every keystroke, and never once per row of a long list: that
1748
+ is a batch, and a batch is `ask()`'s.
1749
+ - **The answer is words, not markup.** Render `text` as text. Never put a
1750
+ model's answer in `dangerouslySetInnerHTML`.
1751
+ - **The answer is not saved.** What matters goes into a row
1752
+ (`updateRow`, `insertRow`) before the page closes.
1753
+ - **No streaming.** The answer arrives whole.
1754
+ - **Nothing appears on the desk.** No request, no thread, no run: the
1755
+ agent column and the Requests list are `ask()`'s. The call is metered
1756
+ to the member who made it; the usage line names the app, the tier and
1757
+ the `task` label. Claw does not log the request or the reply.
1758
+ - **Never by its path.** A string naming `_claw/llm` fails
1759
+ `npm run check` (§6, `llm`): the two functions are the contract, and
1760
+ they handle what a bare `fetch` would not (a claw without the door, a
1761
+ dropped network, an abort).
1762
+
1507
1763
  ### Activity — the contract
1508
1764
 
1509
1765
  `claw_app.activity` is how the founder sees the work. It is the
@@ -1577,11 +1833,13 @@ ok controls every control is a kit part
1577
1833
  ok examples examples show only outside a workspace (useFixture); a workspace app opens empty
1578
1834
  ok manifest 2 surfaces: drafts, sent
1579
1835
  ok icons every icon name exists
1580
- ok kit 70 fixed files match the template
1836
+ ok kit 74 fixed files match the template
1581
1837
  ok depictions 1 picture of elsewhere, framed on 1 surface: x-post
1582
1838
  ok agent no SQL write in app code; the agent's brief names every action with its call (1), every table written (2) and every server action that writes (0)
1583
1839
  ok tables its own schema (makersclaw.json): every table named bare, and each bare name one of the 2 its schema makes (../install/schema.sql); claw. and claw_app. are the platform's
1584
1840
  ok brain no brain block in makersclaw.json; no lib/brain.js call names a kind
1841
+ ok viewport nothing in the app sizes itself by the window
1842
+ ok llm no screen names _claw/llm: a model is called through llm() or useLlm() (README §5)
1585
1843
  ok smoke built in 41s · / → 307 · /s/drafts 200 · /s/sent 200 (0.6s)
1586
1844
  all clear
1587
1845
  ```
@@ -1595,7 +1853,13 @@ ok smoke built in 38s (copy at ~/.claw-check/app) · export 2.1 MB in 64
1595
1853
 
1596
1854
  `kit` refuses a fixed file that differs from the template (they are
1597
1855
  hashed in `check/pins.json`) and any file of yours under `components/ui`
1598
- or `components/parts`. `depictions` reads both directions: nothing of
1856
+ or `components/parts`. `viewport` refuses anything in your code that
1857
+ sizes itself by the window: `min-h-dvh`, `min-h-screen`, `h-screen`,
1858
+ `h-dvh` (and their `svh`, `lvh` and `max-h-` kin), and any `vh` unit —
1859
+ `h-[80vh]`, `max-h-[60vh]`, `h-[calc(100vh-4rem)]`, `style={{ height:
1860
+ "100vh" }}`. The window has one owner, the kit's `AppLayout` (§3b,
1861
+ Layouts); a box inside it is bounded by its parent (`h-full`, `flex-1
1862
+ min-h-0`) or by a size (`max-h-96`). `depictions` reads both directions: nothing of
1599
1863
  the workspace inside a depiction (a token, `font-sans`, `dark:`, a rem
1600
1864
  utility), nothing of the depiction outside its frame (a named import, a
1601
1865
  picture in the shell or a `Card`, a dead file nobody renders).
@@ -1692,6 +1956,16 @@ kind has `kind`, `label`, `plural` and `fields`. A read's kind
1692
1956
  hands the app only what `reads` and `writes` name. A kind held in a
1693
1957
  variable is not read here; the door refuses it on the member's screen.
1694
1958
 
1959
+ `llm` reads every app, in both modes: no string in the code names
1960
+ `_claw/llm`, and none handed to `claw()`, `clawUrl()` or `clawStream()`
1961
+ starts `llm`. A model is called with `llm()` or `useLlm()` (§5, Calling a
1962
+ model), which handle a claw without the door, a dropped network and an
1963
+ abort; a `fetch` of the path handles none of them. The path is read as
1964
+ a browser reads it (`claw("//llm")`, `claw("./llm")` and `"/_claw/./llm"`
1965
+ are the door too). A path built from pieces or held in a variable is not
1966
+ read here, as for rule 8. A longer name (`_claw/llm-notes`) is another
1967
+ route, and a comment or text in JSX is not a string of code.
1968
+
1695
1969
  `browser` reads a browser app only (`"mode": "browser"`): its pages are
1696
1970
  files built once, and everything they read or write goes through the
1697
1971
  platform's data door. So it refuses, file and line, a `"use server"`
@@ -1713,6 +1987,12 @@ page. That proves every page prerenders, not that a surface renders
1713
1987
  with data: the rows come in the browser. Then, by eye:
1714
1988
 
1715
1989
  - Renders at 360px, 768px and 1280px without horizontal scroll.
1990
+ - The page itself never scrolls: the shell is the window's height
1991
+ (`AppLayout`), and each pane scrolls inside it. With the longest
1992
+ content a surface can hold (a 30-page article, a 40-message thread,
1993
+ a tall inspector), the last line and every button after it are
1994
+ reachable, at 375px and at 1280px; the header and the footer stay in
1995
+ view.
1716
1996
  - In a workspace, an empty table shows `LiveEmpty`: the work in flight,
1717
1997
  else what would be here and the manual way to add it — never an
1718
1998
  example (§3a). A saved row replaces it.
@@ -9,14 +9,26 @@
9
9
  // Activity; never repeat any of them. This shell is the App tab: the
10
10
  // domain surfaces and nothing else.
11
11
  //
12
- // Mobile first: this starter is one column with the surfaces as a strip
13
- // along the top, and grows a side rail from 640px. Start narrow, widen.
12
+ // TWO THINGS TO KEEP when you replace it.
14
13
  //
15
- // ONE THING TO KEEP when you replace it: `next/link`. The app runs under
16
- // a base path and <Link> applies it; a plain <a href="/s/..."> resolves
17
- // against the origin instead and the click leaves the app, loading the
18
- // Makersclaw dashboard inside this frame. The page still answers 200, so
19
- // nothing catches it but a click (README §4).
14
+ // 1. AppLayout (components/ui) as the outermost element. It is the one
15
+ // owner of the window's height: the shell is exactly the window, and
16
+ // the content scrolls inside it, so a footer, an inspector or a button
17
+ // after a long page stays reachable. Fill its slots — header,
18
+ // navigation, inspector, footer — and pick contentMode, but never
19
+ // size anything by the window yourself (min-h-dvh, h-screen, 100vh,
20
+ // h-[80vh]): `npm run check` refuses it (README §3b, Layouts). A
21
+ // surface that needs an inspector or a canvas of its own returns an
22
+ // AppLayout too; inside this one it fills the content, edge to edge.
23
+ //
24
+ // 2. `next/link`. The app runs under a base path and <Link> applies it; a
25
+ // plain <a href="/s/..."> resolves against the origin instead and the
26
+ // click leaves the app, loading the Makersclaw dashboard inside this
27
+ // frame. The page still answers 200, so nothing catches it but a click
28
+ // (README §4).
29
+ //
30
+ // Mobile first: below 768px the surfaces sit behind the menu button, in
31
+ // a drawer; from 768px they are a rail beside the content.
20
32
  //
21
33
  // Props: name (the app's name — for aria labels, not for display),
22
34
  // surfaces (surfaceNav() from surfaces.js: { key, label, kind, source,
@@ -24,41 +36,46 @@
24
36
 
25
37
  import Link from "next/link";
26
38
  import { usePathname } from "next/navigation";
27
- import { cx } from "../components/ui";
39
+ import { AppLayout, cx } from "../components/ui";
28
40
 
29
41
  export default function Shell({ name, surfaces, children }) {
30
42
  const pathname = usePathname() ?? "";
31
43
  const activeKey = decodeURIComponent(/^\/s\/([^/?#]+)/.exec(pathname)?.[1] ?? "");
44
+ const active = surfaces.find((s) => s.key === activeKey);
32
45
 
33
46
  return (
34
- <div className="flex min-h-dvh flex-col bg-background text-foreground sm:flex-row">
35
- <nav
36
- aria-label={`${name} surfaces`}
37
- className="shrink-0 overflow-x-auto border-b border-border p-2 sm:w-44 sm:overflow-visible sm:border-b-0 sm:border-r sm:p-3"
38
- >
39
- <ul className="flex gap-1 sm:flex-col">
40
- {surfaces.map((s) => {
41
- const active = s.key === activeKey;
42
- return (
43
- <li key={s.key} className="shrink-0">
44
- <Link
45
- href={`/s/${encodeURIComponent(s.key)}`}
46
- aria-current={active ? "page" : undefined}
47
- className={cx(
48
- "flex min-h-9 items-center justify-between gap-2 rounded-md px-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring",
49
- active ? "bg-accent text-accent-foreground" : "hover:bg-accent",
50
- !s.built && "text-muted-foreground",
51
- )}
52
- >
53
- <span>{s.label}</span>
54
- {s.built ? null : <span className="text-2xs uppercase tracking-wide">planned</span>}
55
- </Link>
56
- </li>
57
- );
58
- })}
59
- </ul>
60
- </nav>
61
- <main className="min-w-0 flex-1 p-4 sm:p-6">{children}</main>
62
- </div>
47
+ <AppLayout
48
+ navigation={
49
+ <nav aria-label={`${name} surfaces`} className="p-2 md:p-3">
50
+ <ul className="flex flex-col gap-1">
51
+ {surfaces.map((s) => {
52
+ const current = s.key === activeKey;
53
+ return (
54
+ <li key={s.key}>
55
+ <Link
56
+ href={`/s/${encodeURIComponent(s.key)}`}
57
+ aria-current={current ? "page" : undefined}
58
+ className={cx(
59
+ "flex min-h-9 items-center justify-between gap-2 rounded-md px-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring",
60
+ current ? "bg-accent text-accent-foreground" : "hover:bg-accent",
61
+ !s.built && "text-muted-foreground",
62
+ )}
63
+ >
64
+ <span>{s.label}</span>
65
+ {s.built ? null : <span className="text-2xs uppercase tracking-wide">planned</span>}
66
+ </Link>
67
+ </li>
68
+ );
69
+ })}
70
+ </ul>
71
+ </nav>
72
+ }
73
+ navigationLabel="Surfaces"
74
+ contentLabel={active?.label ?? name}
75
+ contentClassName="p-4 sm:p-6"
76
+ scrollKey={pathname}
77
+ >
78
+ {children}
79
+ </AppLayout>
63
80
  );
64
81
  }
@@ -18,6 +18,10 @@
18
18
  // screen, long after the build (brain). Number 10 is browser mode's rule
19
19
  // (claw's STATIC-SPEC §3): a browser app is files and a door, and the
20
20
  // build refuses its server code late and unclearly, or not at all.
21
+ // Number 12 is the shell that grew with its content: a footer and the
22
+ // buttons after a long article sat past the window's end (viewport).
23
+ // Number 13 (llm, 0.8.0) keeps a model call on llm(): a screen that
24
+ // fetched the model door by its path got none of what the function does.
21
25
  //
22
26
  // npm run check source rules, then smoke
23
27
  // npm run check -- --no-build source rules only: no build, no smoke
@@ -94,7 +98,7 @@ const ACTIONS_ONLY = process.argv.includes("--actions");
94
98
  // .jsx (a components/ui/depiction.js beside depiction.jsx won); Next 16's
95
99
  // Turbopack tries .jsx before .js (seen on 16.3.7), so the kit's own file
96
100
  // wins there, but the rule holds for every bundler.
97
- const KIT = /^components\/(?:ui\/(?:cx|index)\.js|ui\/(?:radio|toggle|button-group|avatar|breadcrumb|pagination|button|collapsible|item|table|depiction|feedback|stat|distribution|swatch|steps|separator|scroll-area|field|icon|menu|context-menu|popover|calendar|date-picker|tooltip|notice|sheet|tabs|slider|input-group|image|file-input|input-otp|command|combobox|rating|toast|carousel|resizable)\.jsx|parts\/(?:index|sample)\.js|parts\/(?:fixture|record-switcher|live-work)\.jsx)$/;
101
+ const KIT = /^components\/(?:ui\/(?:cx|index)\.js|ui\/(?:radio|toggle|button-group|avatar|breadcrumb|pagination|button|collapsible|item|table|depiction|feedback|stat|distribution|swatch|steps|separator|scroll-area|layout|field|icon|menu|context-menu|popover|calendar|date-picker|tooltip|notice|sheet|tabs|slider|input-group|image|file-input|input-otp|command|combobox|rating|toast|carousel|resizable)\.jsx|parts\/(?:index|sample)\.js|parts\/(?:fixture|record-switcher|live-work)\.jsx)$/;
98
102
 
99
103
  // The pins: sha256 of every fixed file, written by `makersclaw init` and
100
104
  // `makersclaw add` (check/pins.json). Read first, because a BLOCK is
@@ -228,13 +232,20 @@ const COLOUR_PREFIXES = "bg|text|border(?:-[xytrbles])?|ring(?:-offset)?|inset-r
228
232
 
229
233
  /** Source without its comments, newlines kept so line numbers hold: a
230
234
  * rule reads code, not the prose beside it (a header that says
231
- * "window." or names <a> is not browser access or a link). */
232
- function stripComments(text) {
235
+ * "window." or names <a> is not browser access or a link). A line comment
236
+ * is a `//` that follows anything but a `:` or a backslash, which also
237
+ * blanks a string that opens with two slashes ("//llm") or holds a
238
+ * doubled one ("/_claw//llm") to the end of its line. `slashStrings`
239
+ * reads those as strings: a `//` is a comment only at the start of a line
240
+ * or after a space or one of ; , ( ) { } [ ]. For a rule that reads a
241
+ * URL's slashes (12, llm). */
242
+ function stripComments(text, { slashStrings = false } = {}) {
233
243
  const blank = (m) => m.replace(/[^\n]/g, " ");
244
+ const line = slashStrings ? /(^|[\s;,(){}[\]])\/\/[^\n]*/g : /(^|[^:\\])\/\/[^\n]*/g;
234
245
  return text
235
246
  .replace(/\{\/\*[\s\S]*?\*\/\}/g, blank)
236
247
  .replace(/\/\*[\s\S]*?\*\//g, blank)
237
- .replace(/(^|[^:\\])\/\/[^\n]*/g, (m, lead) => lead + blank(m.slice(lead.length)));
248
+ .replace(line, (m, lead) => lead + blank(m.slice(lead.length)));
238
249
  }
239
250
  /** JSX attributes up to the tag's `>`: strings and balanced braces may hold a `>` or `=>`. */
240
251
  const ATTRS = String.raw`(?:"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}|[^>"'{])*`;
@@ -1871,6 +1882,67 @@ function columnFrom(code, at) {
1871
1882
  }
1872
1883
  }
1873
1884
 
1885
+ // 12. One viewport owner: the kit's AppLayout. Every design-first build
1886
+ // of 2026-10-07 shipped a shell of min-h-dvh that grew with its
1887
+ // content: the page scrolled, no pane scrolled on its own, and a
1888
+ // footer or a button after a 30-page article sat past the end of the
1889
+ // window, where nothing could reach it. So the app's code never sizes
1890
+ // anything by the window: no min-h-dvh, min-h-screen, h-screen, h-dvh
1891
+ // (nor their svh, lvh and max-h- kin), and no vh unit at all
1892
+ // (h-[80vh], max-h-[60vh], h-[calc(100vh-4rem)], style={{ height:
1893
+ // "100vh" }}). The kit is skipped: AppLayout in components/ui is the
1894
+ // one place the window's height is written. Depictions are read too:
1895
+ // a picture is drawn in px, never by the window.
1896
+ {
1897
+ const L = limiter("viewport");
1898
+ const DO =
1899
+ 'the window has one owner, AppLayout from components/ui (the shell\'s frame: the content scrolls inside it, contentMode="fill" for a canvas, an inspector beside it), and a page for print is DocumentLayout; inside it bound a box by its parent (h-full, flex-1 min-h-0) or by a size (max-h-96)';
1900
+ scan(/(?<![\w-])(?:min-|max-)?h-(?:screen|dvh|svh|lvh)(?![\w-])/, (f, m, line) => L.hit(`${f.path}:${line}`, m[0], `${m[0]} sizes by the window, so what follows a long page cannot be reached; ${DO}`), files);
1901
+ scan(/(?<![\w.])\d+(?:\.\d+)?[dsl]?vh(?![A-Za-z0-9_])/, (f, m, line) => {
1902
+ // The whole class or value it sits in: h-[calc(100vh-4rem)], "100vh".
1903
+ const from = f.code.slice(0, m.index).search(/[^\s"'`{}]*$/);
1904
+ const token = f.code.slice(from).match(/^[^\s"'`{}]*/)[0];
1905
+ L.hit(`${f.path}:${line}`, token, `${token} is a fraction of the window, so what follows it cannot be reached; ${DO}`);
1906
+ }, files);
1907
+ if (L.count > 4) fail("viewport", `${L.count - 4} more`, "more of the same: sizes taken from the window");
1908
+ if (!L.count) ok("viewport", "nothing in the app sizes itself by the window");
1909
+ }
1910
+
1911
+ // 13. A model is called through llm() (0.8.0, the platform's _claw/llm
1912
+ // door). A screen that wants one answer from a model (sort, draft,
1913
+ // translate) calls llm() from lib/platform.js, or useLlm() from
1914
+ // lib/hooks.js, and never reaches the door by its URL: the path is the
1915
+ // platform's to move, the two functions are the contract, and an
1916
+ // app that fetches its own copy skips what they do (nothing throws, a
1917
+ // claw from before the door comes back `unavailable`, an abort
1918
+ // answers `aborted`). So no string in the app's code names `_claw/llm`,
1919
+ // and none handed to claw(), clawUrl() or clawStream() starts `llm`.
1920
+ // "Names" is as the browser reads it: clawUrl() drops every leading
1921
+ // slash, and a `./` or a doubled slash in a URL is dropped on the way
1922
+ // (`claw("//llm")`, `claw("./llm")`, `/_claw/./llm` all reach the door;
1923
+ // it reads the code with `slashStrings` so a `//` inside a string is
1924
+ // not taken for a comment). It reads literals, so a path built from
1925
+ // pieces is not seen, as in rule 8. Every mode and every tables
1926
+ // setting: the door is the same for all.
1927
+ {
1928
+ const L = limiter("llm");
1929
+ const LLM_PATH = /_claw\/(?:\.?\/)*llm(?![\w-])/;
1930
+ const LLM_NAME = /^(?:\.?\/)*llm(?![\w-])/;
1931
+ for (const f of jsFiles) {
1932
+ const code = stripComments(f.text, { slashStrings: true });
1933
+ const toClaw = new Set();
1934
+ for (const m of code.matchAll(/(?<![\w$.])claw(?:Url|Stream)?\s*(?:\?\.\s*)?\(\s*(?=["'`])/g)) toClaw.add(m.index + m[0].length);
1935
+ for (const { start, end } of literalsOf(code)) {
1936
+ const inner = code.slice(start + 1, end);
1937
+ if (!(LLM_PATH.test(inner) || (toClaw.has(start) && LLM_NAME.test(inner)))) continue;
1938
+ const path = inner.replace(/\s+/g, " ").slice(0, 60);
1939
+ L.hit(`${f.path}:${lineOf(code, start)}`, "path", `"${path}": the model door is reached through llm(input, { system, schema }) in lib/platform.js, or useLlm() in lib/hooks.js, never by its path (README §5, Calling a model)`);
1940
+ }
1941
+ }
1942
+ if (L.count > 4) fail("llm", `${L.count - 4} more`, "strings that name the model door's path instead of calling llm()");
1943
+ if (!L.count) ok("llm", "no screen names _claw/llm: a model is called through llm() or useLlm() (README §5)");
1944
+ }
1945
+
1874
1946
  // 6. Smoke: the app, built and started, answers every surface.
1875
1947
 
1876
1948
  /** Newest mtime among the app's own sources (what a build reads): walk