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
@@ -48,6 +48,7 @@ export { Stat, StatGroup, Delta, StatusDot, Legend, Meter } from "./stat";
48
48
  export { Steps } from "./steps";
49
49
  export { Separator, Kbd, KbdGroup, AspectRatio } from "./separator";
50
50
  export { ScrollArea } from "./scroll-area";
51
+ export { AppLayout, DocumentLayout } from "./layout";
51
52
  export { Slider } from "./slider";
52
53
  export { InputGroup, PasswordInput } from "./input-group";
53
54
  export { Image } from "./image";
@@ -0,0 +1,307 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
4
+ import { cx } from "./cx";
5
+ import { Button, IconButton } from "./button";
6
+ import { Sheet } from "./sheet";
7
+
8
+ // Layouts — who owns the window's height, and who scrolls.
9
+ //
10
+ // Every design-first build of 2026-10-07 shipped a shell of `min-h-dvh`
11
+ // that grew with its content: the page itself scrolled, no pane scrolled
12
+ // on its own, and a footer, an inspector or a button below a long
13
+ // article sat past the end of the iframe, where nothing could reach it.
14
+ // So the kit owns the window's height, once, and the app's code never
15
+ // sizes anything by the window (`npm run check` refuses min-h-dvh,
16
+ // h-screen, 100vh, h-[80vh]… outside the kit, rule 12).
17
+ //
18
+ // AppLayout — the app frame. Exactly one owns the viewport: the
19
+ // outermost. A layout inside another fills the slot it is in instead,
20
+ // whatever `height` says, so there is never a second viewport owner. The
21
+ // shell (app/shell.jsx) is the outermost one in every app the template
22
+ // lays; a surface that needs an inspector or a fill of its own returns
23
+ // an AppLayout as its outermost element, and it fills the shell's
24
+ // content edge to edge (the content's padding drops for it).
25
+ //
26
+ // header the top bar, always in view (title, the surface's
27
+ // commands). The bar draws its own bottom border
28
+ // navigation the surfaces, a page tree: a rail beside the content
29
+ // from md (768px); below md a Sheet from the leading
30
+ // edge, opened by the menu button in the top bar. A
31
+ // press on a link or a button inside closes it
32
+ // navigationLabel the Sheet's title and the menu button's words
33
+ // ("Navigation")
34
+ // navigationWidth the rail, in px (176; 160 to 320)
35
+ // children the content. It scrolls inside its own region
36
+ // contentMode scroll (the default): the content region scrolls ·
37
+ // fill: the region is a bounded flex column and the
38
+ // child owns its own scrolling (a canvas, a grid, a
39
+ // spreadsheet, an editor that scrolls itself)
40
+ // contentLabel the content region's name for a screen reader ("Content")
41
+ // contentClassName spacing and token backgrounds for the content
42
+ // region (p-4 sm:p-6, bg-card). Never a height or an
43
+ // overflow: the layout owns those
44
+ // scrollKey when it changes the content scrolls back to its top
45
+ // and the navigation Sheet closes: pass the route
46
+ // (usePathname()), since the frame stays mounted while
47
+ // the surfaces change inside it
48
+ // inspector the details, review or feedback panel beside the
49
+ // content, scrolling on its own
50
+ // inspectorLabel the inspector region's name ("Details")
51
+ // inspectorWidth in px (360; 240 to 560). Builds have needed 300 and 440
52
+ // inspectorAt md · lg (the default) · xl: the width from which the
53
+ // inspector sits beside the content
54
+ // footer under the content, always in view (the save state,
55
+ // the composer, the commands for what is above)
56
+ // height viewport (the default): the frame is the window's
57
+ // height · parent: it fills a bounded parent. Nested,
58
+ // it is parent whatever this says
59
+ // className the frame's own tokens; never a height or an overflow
60
+ //
61
+ // Below `inspectorAt`, where there is no room for two columns:
62
+ // - contentMode scroll: the content and the inspector share ONE scroll
63
+ // region, the inspector after the content. Everything is reachable
64
+ // by scrolling, and nothing is hidden behind a button.
65
+ // - contentMode fill: the content keeps the room and the inspector is
66
+ // a bounded pane under it (at most 45% of the height), scrolling on
67
+ // its own. A fill child has no flow to stack into, and a pane with
68
+ // no bound under it is cut off (workspace-layout's fault, 2026-10-07).
69
+ // The footer stays under both, in view.
70
+ //
71
+ // In print the frame lets go: no bound, no overflow, no navigation, no
72
+ // header, inspector or footer; the content flows onto pages. A page meant
73
+ // for print or export is a DocumentLayout inside the content.
74
+
75
+ const Nested = createContext(false);
76
+
77
+ const px = (value, fallback, min, max) => {
78
+ const n = Number(value);
79
+ return `${Number.isFinite(n) && n > 0 ? Math.min(max, Math.max(min, Math.round(n))) : fallback}px`;
80
+ };
81
+
82
+ // Tailwind reads class names whole from the source, so every breakpoint
83
+ // is written out.
84
+ const BESIDE = {
85
+ md: {
86
+ main: "md:grid md:grid-cols-[minmax(0,1fr)_var(--app-inspector)] md:grid-rows-[minmax(0,1fr)_auto]",
87
+ flow: "md:contents",
88
+ content: "md:col-start-1 md:row-start-1",
89
+ scrolls: "md:min-h-0 md:overflow-y-auto md:overscroll-contain",
90
+ inspector: "md:col-start-2 md:row-span-2 md:row-start-1 md:max-h-none md:min-h-0 md:overflow-y-auto md:border-l md:border-t-0",
91
+ footer: "md:col-start-1 md:row-start-2",
92
+ },
93
+ lg: {
94
+ main: "lg:grid lg:grid-cols-[minmax(0,1fr)_var(--app-inspector)] lg:grid-rows-[minmax(0,1fr)_auto]",
95
+ flow: "lg:contents",
96
+ content: "lg:col-start-1 lg:row-start-1",
97
+ scrolls: "lg:min-h-0 lg:overflow-y-auto lg:overscroll-contain",
98
+ inspector: "lg:col-start-2 lg:row-span-2 lg:row-start-1 lg:max-h-none lg:min-h-0 lg:overflow-y-auto lg:border-l lg:border-t-0",
99
+ footer: "lg:col-start-1 lg:row-start-2",
100
+ },
101
+ xl: {
102
+ main: "xl:grid xl:grid-cols-[minmax(0,1fr)_var(--app-inspector)] xl:grid-rows-[minmax(0,1fr)_auto]",
103
+ flow: "xl:contents",
104
+ content: "xl:col-start-1 xl:row-start-1",
105
+ scrolls: "xl:min-h-0 xl:overflow-y-auto xl:overscroll-contain",
106
+ inspector: "xl:col-start-2 xl:row-span-2 xl:row-start-1 xl:max-h-none xl:min-h-0 xl:overflow-y-auto xl:border-l xl:border-t-0",
107
+ footer: "xl:col-start-1 xl:row-start-2",
108
+ },
109
+ };
110
+
111
+ const REGION = "outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring";
112
+ const LETS_GO = "print:block print:h-auto print:min-h-0 print:overflow-visible";
113
+
114
+ export function AppLayout({
115
+ header,
116
+ navigation,
117
+ navigationLabel = "Navigation",
118
+ navigationWidth,
119
+ children,
120
+ contentMode = "scroll",
121
+ contentLabel = "Content",
122
+ contentClassName,
123
+ scrollKey,
124
+ inspector,
125
+ inspectorLabel = "Details",
126
+ inspectorWidth,
127
+ inspectorAt = "lg",
128
+ footer,
129
+ height = "viewport",
130
+ className,
131
+ }) {
132
+ const nested = useContext(Nested);
133
+ const bounded = nested || height === "parent";
134
+ const fill = contentMode === "fill";
135
+ const at = BESIDE[inspectorAt] ?? BESIDE.lg;
136
+ const hasInspector = inspector != null && inspector !== false;
137
+ const hasNavigation = navigation != null && navigation !== false;
138
+ const shared = hasInspector && !fill; // below `at`: content and inspector in one scroll region
139
+ const [open, setOpen] = useState(false);
140
+ const flowRef = useRef(null);
141
+ const contentRef = useRef(null);
142
+
143
+ useEffect(() => {
144
+ if (flowRef.current) flowRef.current.scrollTop = 0;
145
+ if (contentRef.current) contentRef.current.scrollTop = 0;
146
+ setOpen(false);
147
+ }, [scrollKey]);
148
+
149
+ return (
150
+ <Nested.Provider value={true}>
151
+ <div
152
+ data-app-layout={bounded ? "parent" : "viewport"}
153
+ style={{ "--app-nav": px(navigationWidth, 176, 160, 320), "--app-inspector": px(inspectorWidth, 360, 240, 560) }}
154
+ className={cx(
155
+ "flex min-w-0 flex-col overflow-hidden bg-background text-foreground",
156
+ bounded ? "h-full min-h-0 flex-1" : "h-dvh",
157
+ LETS_GO,
158
+ className,
159
+ )}
160
+ >
161
+ {header != null && header !== false ? (
162
+ <div data-layout-header className="flex shrink-0 items-center gap-1 border-b border-border print:hidden">
163
+ {hasNavigation ? (
164
+ <IconButton icon="menu" label={`Open ${navigationLabel.toLowerCase()}`} onClick={() => setOpen(true)} className="ml-1 md:hidden" />
165
+ ) : null}
166
+ <div className="min-w-0 flex-1">{header}</div>
167
+ </div>
168
+ ) : hasNavigation ? (
169
+ <div data-layout-header className="flex shrink-0 items-center border-b border-border px-1 py-1 md:hidden print:hidden">
170
+ <Button variant="quiet" size="sm" icon="menu" onClick={() => setOpen(true)}>
171
+ {navigationLabel}
172
+ </Button>
173
+ </div>
174
+ ) : null}
175
+
176
+ {hasNavigation ? (
177
+ <Sheet open={open} onClose={() => setOpen(false)} title={navigationLabel} side="start">
178
+ {open ? (
179
+ <div
180
+ onClick={(e) => {
181
+ if (e.target instanceof Element && e.target.closest("a,button")) setOpen(false);
182
+ }}
183
+ >
184
+ {navigation}
185
+ </div>
186
+ ) : null}
187
+ </Sheet>
188
+ ) : null}
189
+
190
+ <div data-layout-body className={cx("flex min-h-0 min-w-0 flex-1", LETS_GO)}>
191
+ {hasNavigation ? (
192
+ <div
193
+ data-layout-navigation
194
+ className="hidden shrink-0 flex-col overflow-y-auto overscroll-contain border-r border-border md:flex md:w-(--app-nav) print:hidden"
195
+ >
196
+ {navigation}
197
+ </div>
198
+ ) : null}
199
+
200
+ <div data-layout-main className={cx("flex min-h-0 min-w-0 flex-1 flex-col", hasInspector && at.main, LETS_GO)}>
201
+ <div
202
+ ref={flowRef}
203
+ data-layout-flow
204
+ className={cx(
205
+ "flex min-h-0 min-w-0 flex-1 flex-col",
206
+ shared && "overflow-y-auto overscroll-contain",
207
+ hasInspector && at.flow,
208
+ LETS_GO,
209
+ )}
210
+ >
211
+ <div
212
+ ref={contentRef}
213
+ data-layout-content
214
+ role="region"
215
+ aria-label={contentLabel}
216
+ tabIndex={0}
217
+ className={cx(
218
+ "flex min-w-0 flex-col",
219
+ REGION,
220
+ fill
221
+ ? "min-h-0 flex-1 overflow-hidden"
222
+ : shared
223
+ ? cx("shrink-0", at.scrolls)
224
+ : "min-h-0 flex-1 overflow-y-auto overscroll-contain",
225
+ hasInspector && at.content,
226
+ LETS_GO,
227
+ contentClassName,
228
+ // A surface that is a frame of its own runs to the edges.
229
+ "has-[>[data-app-layout]]:p-0",
230
+ )}
231
+ >
232
+ {children}
233
+ </div>
234
+ {hasInspector ? (
235
+ <div
236
+ data-layout-inspector
237
+ role="region"
238
+ aria-label={inspectorLabel}
239
+ tabIndex={0}
240
+ className={cx(
241
+ "min-w-0 shrink-0 border-t border-border print:hidden",
242
+ REGION,
243
+ fill && "max-h-[45%] overflow-y-auto overscroll-contain",
244
+ at.inspector,
245
+ )}
246
+ >
247
+ {inspector}
248
+ </div>
249
+ ) : null}
250
+ </div>
251
+ {footer != null && footer !== false ? (
252
+ <div data-layout-footer className={cx("shrink-0 border-t border-border print:hidden", hasInspector && at.footer)}>
253
+ {footer}
254
+ </div>
255
+ ) : null}
256
+ </div>
257
+ </div>
258
+ </div>
259
+ </Nested.Provider>
260
+ );
261
+ }
262
+
263
+ // DocumentLayout — a page that flows: a report, an invoice, an article
264
+ // meant for print or export. It owns no height and scrolls nothing; it
265
+ // sits in the AppLayout's content (contentMode scroll), which scrolls it,
266
+ // and in print the frame lets go and it flows onto pages.
267
+ //
268
+ // children the document
269
+ // toolbar a bar held at the top while the document scrolls under it
270
+ // (Print, Export, the zoom); never printed
271
+ // width prose (the default: a measure for reading) · page (A4,
272
+ // 210mm) · letter (8.5in) · full. In print it is the paper's
273
+ // paper true: drawn as a sheet on the desk on screen (bg-card, a
274
+ // hairline, padding); plain in print
275
+ // align start (the default) · center
276
+ // label the document's name for a screen reader ("Document")
277
+ // className the document's own spacing and tokens; never a height
278
+ const WIDTHS = {
279
+ prose: "max-w-prose",
280
+ page: "max-w-[210mm]",
281
+ letter: "max-w-[8.5in]",
282
+ full: "max-w-none",
283
+ };
284
+
285
+ export function DocumentLayout({ children, toolbar, width = "prose", paper = false, align = "start", label = "Document", className }) {
286
+ return (
287
+ <div data-document-layout className="flex min-w-0 flex-col">
288
+ {toolbar != null && toolbar !== false ? (
289
+ <div data-document-toolbar className="sticky top-0 z-10 border-b border-border bg-background print:hidden">
290
+ {toolbar}
291
+ </div>
292
+ ) : null}
293
+ <article
294
+ aria-label={label}
295
+ className={cx(
296
+ "w-full min-w-0 print:max-w-none",
297
+ WIDTHS[width] ?? WIDTHS.prose,
298
+ align === "center" && "mx-auto",
299
+ paper && "my-6 rounded-lg border border-border bg-card p-8 text-card-foreground shadow-sm print:my-0 print:rounded-none print:border-0 print:p-0 print:shadow-none",
300
+ className,
301
+ )}
302
+ >
303
+ {children}
304
+ </article>
305
+ </div>
306
+ );
307
+ }
@@ -18,7 +18,10 @@
18
18
  // lib/platform.js wraps them as ask / requests / stopRequest and
19
19
  // lib/hooks.js as useJob and useRequests. `chat` and `cancel` are the
20
20
  // 0.3.x door to the same work, streamed into the app (startJob /
21
- // cancelJob): kept for a claw that has no `ask` yet. What an app still
21
+ // cancelJob): kept for a claw that has no `ask` yet. `llm` (0.8.0) is one
22
+ // shot of model work answered in the page, not asking an agent: lib/
23
+ // platform.js wraps it as llm and lib/hooks.js as useLlm, and the check
24
+ // refuses a screen that names its path. What an app still
22
25
  // never calls is monitoring: the OS draws Agent, Runs and Activity as
23
26
  // tabs on its window. The rest of the gateway's routes (§5) stay
24
27
  // reachable through claw() / clawStream() for a surface that has a real
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { claw, publishDesk, publishStatus } from "./claw";
5
- import { ask, cancelJob, openRequest, requests as readRequests, startJob, stopRequest } from "./platform";
5
+ import { ask, cancelJob, llm, openRequest, requests as readRequests, startJob, stopRequest } from "./platform";
6
6
 
7
7
  // The React side of the _claw API, without any look: fetching, polling,
8
8
  // streaming and cleanup. A surface renders what comes back and owns every
@@ -17,7 +17,9 @@ import { ask, cancelJob, openRequest, requests as readRequests, startJob, stopRe
17
17
  // a request: the conversation lives in the desk's agent column, and the
18
18
  // app follows only its status. `useRequests` is the same requests seen
19
19
  // from a surface: the asks in flight on this app, so an empty table can
20
- // show the work coming instead of a blank.
20
+ // show the work coming instead of a blank. `useLlm` (0.8.0) is neither:
21
+ // one call to a model, answered in the page, for text work on what the
22
+ // screen holds — nothing to follow, nothing on the desk.
21
23
  //
22
24
  // While the gateway has no _claw routes yet (`unavailable`), polls back
23
25
  // off to once a minute so a preview that is still being wired does not
@@ -672,3 +674,70 @@ export function useJob() {
672
674
 
673
675
  return { ...state, start, cancel, reset, open };
674
676
  }
677
+
678
+ /** What useLlm shows with no call in flight and nothing to show. */
679
+ const QUIET = { loading: false, result: null, error: null, unavailable: false };
680
+
681
+ /**
682
+ * One model call from a component (lib/platform.js, llm). `run(input,
683
+ * opts)` takes llm()'s arguments and resolves llm()'s answer, so a caller
684
+ * that needs the refusal's `code` reads it there; the state is for the
685
+ * screen:
686
+ *
687
+ * loading a call is out
688
+ * result the answer when it was ok: { ok: true, text } or { ok: true,
689
+ * data } (null while loading and after a failure)
690
+ * error the failure's words (null while loading, after an answer,
691
+ * and on a claw with no door)
692
+ * unavailable the platform has no model door yet (a claw from before
693
+ * it) or there is no network: show the quiet "not connected
694
+ * yet", as for useClaw's `unavailable`. Then `error` stays
695
+ * null, so a refusal and a missing door are never both set
696
+ *
697
+ * A run supersedes the one before it: that one is aborted, resolves
698
+ * { ok: false, aborted: true } and changes nothing, so a slow answer never
699
+ * lands over a newer one. The hook owns the signal (one in opts is
700
+ * replaced). Unmounting aborts the call in flight. Nothing is called until
701
+ * `run` is.
702
+ *
703
+ * const { run, loading, result, error, unavailable } = useLlm();
704
+ * run(`Translate to French: ${text}`, { task: "translate" });
705
+ * …{loading ? <Spinner /> : result?.text}
706
+ */
707
+ export function useLlm() {
708
+ const [state, setState] = useState(QUIET);
709
+ const abortRef = useRef(null);
710
+ const runRef = useRef(0); // the run that owns the state
711
+ const aliveRef = useRef(true);
712
+
713
+ // React runs this cleanup and mounts the hook again in development (its
714
+ // double mount; a fast refresh does the same): a call cut off by that
715
+ // cleanup is closed quietly in run(), below, once the answer comes back.
716
+ useEffect(() => {
717
+ aliveRef.current = true;
718
+ return () => {
719
+ aliveRef.current = false;
720
+ abortRef.current?.abort();
721
+ };
722
+ }, []);
723
+
724
+ const run = useCallback(async (input, opts) => {
725
+ abortRef.current?.abort();
726
+ const controller = new AbortController();
727
+ abortRef.current = controller;
728
+ const mine = ++runRef.current;
729
+ setState({ loading: true, result: null, error: null, unavailable: false });
730
+ const answer = await llm(input, { ...opts, signal: controller.signal });
731
+ if (runRef.current !== mine || !aliveRef.current) return answer; // a newer run owns the state, or the page is gone
732
+ if (controller.signal.aborted) {
733
+ setState(QUIET); // cut off by an unmount the hook came back from, with nothing started since
734
+ return answer;
735
+ }
736
+ if (answer.ok) setState({ loading: false, result: answer, error: null, unavailable: false });
737
+ else if (answer.unavailable) setState({ loading: false, result: null, error: null, unavailable: true });
738
+ else setState({ loading: false, result: null, error: answer.error || "the model did not answer", unavailable: false });
739
+ return answer;
740
+ }, []);
741
+
742
+ return { ...state, run };
743
+ }
@@ -8,7 +8,9 @@ import { claw, clawBasePath, clawStream, openInDesk, publishDesk, reportError }
8
8
  // unavailable, code? }; nothing throws. `unavailable: true` (404 or no
9
9
  // network) means the gateway has no such route yet — render a quiet "not
10
10
  // connected yet" state, not an error. `code` names a refusal a caller
11
- // can act on (`not_your_thread`, `needs_answer`, `queue_full`).
11
+ // can act on (`not_your_thread`, `needs_answer`, `queue_full`). `llm` is
12
+ // the one that resolves the platform's answer as it is — { ok: true, text }
13
+ // or { ok: true, data } — with no { status, data } around it.
12
14
  //
13
15
  // What lives here: who the app is (`me`), the error relay
14
16
  // (`reportError`), and ASKING — a screen asks its agent for work with a
@@ -20,8 +22,12 @@ import { claw, clawBasePath, clawStream, openInDesk, publishDesk, reportError }
20
22
  // Activity as tabs on the window, and nothing here lists chats, runs or
21
23
  // activity. `startJob` / `cancelJob` are the 0.3.x door to the same work,
22
24
  // streamed into the app; they stay for a claw that has no _claw/ask yet.
23
- // `claw(path, opts)` and `clawStream` in lib/claw.js are the generic
24
- // doors for the rest of §5.
25
+ // And CALLING A MODEL (0.8.0): `llm` is one shot of text work on what is
26
+ // on screen — sort these, draft that, translate this — answered in the
27
+ // page, with no thread, no tools and no request on the desk. It is the
28
+ // opposite of `ask`, which hands work to the app's agent. `claw(path,
29
+ // opts)` and `clawStream` in lib/claw.js are the generic doors for the
30
+ // rest of §5.
25
31
 
26
32
  /** GET _claw/me → { artifact: { id, name, vehicle, status, home },
27
33
  * user: { id, email }, blueprint, desk_origin } */
@@ -150,8 +156,91 @@ export function cancelJob(sessionId) {
150
156
  return claw("cancel", { method: "POST", body: { session_id: sessionId } });
151
157
  }
152
158
 
159
+ /**
160
+ * Call a model: POST _claw/llm. One shot of text work on what the screen
161
+ * holds — sort, draft, translate, summarise, pull a field out — answered
162
+ * here and now. Work that needs tools, the workspace or more than one
163
+ * step is `ask()`'s: that is the app's agent. The app never names a model
164
+ * (`tier` picks the size) and never holds a key; the platform meters the
165
+ * call to the member who made it.
166
+ *
167
+ * input what to work on: a string, or a conversation, [{ role:
168
+ * "user" | "assistant", content }] — at most 100 messages,
169
+ * none empty, the last one the user's (required)
170
+ * system how to go about it (at most 8,000 characters)
171
+ * schema a JSON Schema for the answer (an object, at most 16 KB
172
+ * serialised): the answer comes back as `data`, checked
173
+ * against it, not as `text`. A miss is sent back to the
174
+ * model once; a second is `schema_mismatch`. No `pattern`,
175
+ * `patternProperties`, `$ref`, `$defs` or `definitions`
176
+ * (bad_input); nesting at most 20 levels (bad_input); at
177
+ * most 1,000 parts, each object, `enum` value and listed
178
+ * type (too_large). A reply too large to check against the
179
+ * schema is a miss. `data` keeps keys the schema does not
180
+ * name
181
+ * maxTokens the answer's ceiling, a whole number (default 1,024; over
182
+ * 4,096 is cut to 4,096). Sent as `max_tokens`
183
+ * tier "fast" (the default) or "smart", for the harder job
184
+ * task a short label for the usage line, /^[a-z0-9-]{1,40}$/
185
+ * signal an AbortSignal (an abort resolves { ok: false, aborted:
186
+ * true })
187
+ *
188
+ * → { ok: true, text } no schema
189
+ * { ok: true, data } with a schema
190
+ * { ok: false, error, code } a refusal, passed through as the platform
191
+ * sent it (`status` and `unavailable: false` come with it). `code` is
192
+ * one of, with its status:
193
+ * for the member (show `error` as it is) —
194
+ * no_credits 402 (the workspace wallet, a suspension, or the
195
+ * Free plan's daily limit), member_cap 402 (the member's monthly
196
+ * cap), app_cap 402 (the app's weekly cap), rate_limited 429
197
+ * (30 calls a minute per member), model_failed 502, timeout 504
198
+ * (60 s, a schema retry included; a call the clock cuts is still
199
+ * charged), try_again 503, failed 500, no_member_identity 401,
200
+ * not_a_member 403;
201
+ * for the developer (`error` is not for the member) —
202
+ * bad_input 400, too_large 413 (the body over 128 KB, or a field
203
+ * over its cap), schema_mismatch 422, cross_origin 403,
204
+ * not_configured 503
205
+ * { ok: false, unavailable: true } a claw from before this door (a 404
206
+ * that names no refusal) or no network: render a quiet "not
207
+ * connected yet", as for every door
208
+ *
209
+ * Nothing throws. An empty input asks nothing of the platform.
210
+ */
211
+ export async function llm(input, opts) {
212
+ const { system, schema, maxTokens, tier, task, signal } = opts ?? {};
213
+ const said = typeof input === "string" ? input.trim() !== "" : Array.isArray(input) && input.length > 0;
214
+ if (!said) return refused("nothing to send");
215
+ const body = {
216
+ input,
217
+ system: typeof system === "string" && system ? system : undefined,
218
+ schema: schema && typeof schema === "object" ? schema : undefined,
219
+ max_tokens: typeof maxTokens === "number" ? maxTokens : undefined,
220
+ tier: tier || undefined,
221
+ task: task || undefined,
222
+ };
223
+ // claw() would read a body it cannot serialise as "not reachable".
224
+ try {
225
+ JSON.stringify(body);
226
+ } catch {
227
+ return refused("that cannot be sent as JSON");
228
+ }
229
+ const r = await claw("llm", { method: "POST", body, signal });
230
+ if (!r.ok) return r;
231
+ const answer = r.data;
232
+ if (answer && typeof answer === "object" && !Array.isArray(answer) && typeof answer.ok === "boolean") return answer;
233
+ return { ok: false, status: r.status, error: "the platform answered something unexpected", unavailable: false };
234
+ }
235
+
236
+ /** What llm() resolves for an input it will not send: the platform's own
237
+ * word for it, with no request made. */
238
+ function refused(error) {
239
+ return { ok: false, status: 0, error, code: "bad_input", unavailable: false };
240
+ }
241
+
153
242
  export { reportError };
154
243
 
155
- export const platform = { me, ask, requests, stopRequest, openRequest, surfaceOfPath, startJob, cancelJob, reportError };
244
+ export const platform = { me, ask, llm, requests, stopRequest, openRequest, surfaceOfPath, startJob, cancelJob, reportError };
156
245
 
157
246
  export default platform;
@@ -32,7 +32,8 @@
32
32
  "modes": {
33
33
  "browser": {
34
34
  "overlay": "browser",
35
- "drop": ["proxy.js", "app/claw-alive/route.js", "lib/db.js", "lib/brain-server.js"]
35
+ "drop": ["proxy.js", "app/claw-alive/route.js", "lib/db.js", "lib/brain-server.js"],
36
+ "dropScripts": ["start"]
36
37
  }
37
38
  },
38
39
  "components": [
@@ -332,6 +333,14 @@
332
333
  "tags": ["scroll", "scroll area", "overflow", "list", "carousel", "rail"],
333
334
  "files": ["components/ui/scroll-area.jsx"]
334
335
  },
336
+ {
337
+ "name": "layout",
338
+ "type": "component",
339
+ "title": "AppLayout, DocumentLayout",
340
+ "description": "Who owns the window's height, and who scrolls. AppLayout is the app frame, the one viewport owner (the outermost; one inside another fills its slot instead): header (the top bar, in view), navigation (a rail from md, navigationWidth px, 176; below md a Sheet behind the menu button, navigationLabel), the content (contentMode scroll: its region scrolls · fill: a bounded column for a canvas or a grid that scrolls itself; contentLabel, contentClassName for spacing, scrollKey = the route to scroll back to the top), inspector (beside the content from inspectorAt md · lg · xl, inspectorWidth px, 360, 240 to 560; below it the content and the inspector share one scroll in scroll mode, and in fill mode the inspector is a bounded pane under the content), footer (under the content, in view), height viewport · parent. In print it lets go. DocumentLayout is a page that flows, for print and export: toolbar (held at the top, never printed), width prose · page (A4) · letter · full, paper, align start · center. Nothing in an app sizes itself by the window: npm run check refuses min-h-dvh, h-screen, 100vh and h-[80vh] outside the kit.",
341
+ "tags": ["layout", "app frame", "shell", "viewport", "scroll", "inspector", "sidebar", "navigation", "footer", "fill", "canvas", "document", "print", "export", "page"],
342
+ "files": ["components/ui/layout.jsx"]
343
+ },
335
344
  {
336
345
  "name": "toast",
337
346
  "type": "component",