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.
- package/README.md +8 -1
- package/package.json +1 -1
- package/r/annotate.json +4 -4
- package/r/approval-request.json +2 -2
- package/r/audio-player.json +2 -2
- package/r/auth-form.json +2 -2
- package/r/board.json +2 -2
- package/r/calendar-view.json +3 -3
- package/r/cart.json +2 -2
- package/r/chart.json +5 -5
- package/r/chat.json +3 -3
- package/r/code-editor.json +2 -2
- package/r/comment-box.json +3 -3
- package/r/contact-section.json +2 -2
- package/r/core.json +7 -1
- package/r/csv-import.json +3 -3
- package/r/data-table.json +2 -2
- package/r/diagram.json +2 -2
- package/r/doc-editor.json +2 -2
- package/r/doc-page.json +3 -3
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +2 -2
- package/r/flow-canvas.json +3 -3
- package/r/footer.json +3 -3
- package/r/gantt.json +3 -3
- package/r/job-panel.json +2 -2
- package/r/json-editor.json +3 -3
- package/r/map.json +2 -2
- package/r/menubar.json +2 -2
- package/r/mind-map.json +4 -4
- package/r/navigation-menu.json +2 -2
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +4 -4
- package/r/page-builder.json +3 -3
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +3 -3
- package/r/product-detail.json +2 -2
- package/r/product-grid.json +3 -3
- package/r/record-form.json +2 -2
- package/r/record-notes.json +4 -4
- package/r/registry.json +126 -121
- package/r/search-dialog.json +2 -2
- package/r/settings-list.json +2 -2
- package/r/setup-checklist.json +2 -2
- package/r/share-dialog.json +2 -2
- package/r/sidebar.json +3 -3
- package/r/signature-pad.json +2 -2
- package/r/sketch-canvas.json +3 -3
- package/r/spreadsheet.json +2 -2
- package/r/stat-cards.json +3 -3
- package/r/task-list.json +3 -3
- package/r/task-model.json +2 -2
- package/r/testimonials.json +2 -2
- package/r/transaction-list.json +2 -2
- package/r/whiteboard.json +4 -4
- package/registry/blocks/annotate/README.md +14 -11
- package/registry/blocks/chart/cartesian.jsx +1 -1
- package/registry/blocks/chart/core.jsx +33 -23
- package/registry/blocks/chart/pie.jsx +1 -1
- package/registry/blocks/chat/README.md +4 -1
- package/registry/blocks/chat/demo.jsx +2 -2
- package/registry/blocks/flow-canvas/README.md +15 -8
- package/registry/blocks/footer/README.md +3 -2
- package/registry/blocks/mind-map/README.md +4 -2
- package/registry/blocks/org-chart/README.md +4 -2
- package/registry/blocks/page-builder/README.md +1 -1
- package/registry/blocks/page-builder/demo.jsx +3 -3
- package/registry/blocks/pdf-viewer/README.md +4 -1
- package/registry/blocks/sidebar/README.md +23 -13
- package/registry/blocks/sketch-canvas/README.md +15 -9
- package/registry/blocks/whiteboard/README.md +20 -14
- package/src/cli.mjs +2 -2
- package/src/install.mjs +23 -6
- package/src/registry.mjs +34 -3
- package/templates/next/files/README.md +291 -11
- package/templates/next/files/app/shell.jsx +54 -37
- package/templates/next/files/check/run.mjs +76 -4
- package/templates/next/files/components/ui/index.js +1 -0
- package/templates/next/files/components/ui/layout.jsx +307 -0
- package/templates/next/files/lib/claw.js +4 -1
- package/templates/next/files/lib/hooks.js +71 -2
- package/templates/next/files/lib/platform.js +93 -4
- 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.
|
|
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
|
-
//
|
|
24
|
-
//
|
|
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",
|