makersclaw 0.3.2 → 0.5.1
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 +3 -1
- package/package.json +1 -1
- package/r/annotate.json +3 -3
- package/r/approval-request.json +4 -4
- 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 +9 -9
- package/r/cart.json +2 -2
- package/r/chart.json +7 -7
- package/r/chat.json +3 -3
- package/r/code-editor.json +2 -2
- package/r/comment-box.json +5 -5
- package/r/contact-section.json +2 -2
- package/r/core.json +17 -11
- package/r/csv-import.json +3 -3
- package/r/data-table.json +4 -4
- 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 +4 -4
- package/r/footer.json +2 -2
- package/r/gantt.json +3 -3
- package/r/job-panel.json +8 -5
- package/r/json-editor.json +3 -3
- package/r/map.json +2 -2
- package/r/menubar.json +2 -2
- package/r/mind-map.json +6 -6
- 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 +2 -2
- 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 +6 -6
- package/r/registry.json +131 -123
- 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 +4 -4
- package/r/signature-pad.json +3 -3
- package/r/sketch-canvas.json +2 -2
- package/r/spreadsheet.json +3 -3
- 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 +3 -3
- package/registry/blocks/approval-request/README.md +2 -1
- package/registry/blocks/approval-request/approval-request.jsx +5 -3
- package/registry/blocks/calendar-view/README.md +8 -0
- package/registry/blocks/calendar-view/agenda.jsx +5 -4
- package/registry/blocks/calendar-view/calendar.jsx +23 -8
- package/registry/blocks/calendar-view/demo.jsx +2 -0
- package/registry/blocks/calendar-view/month.jsx +6 -5
- package/registry/blocks/calendar-view/time-grid.jsx +9 -7
- package/registry/blocks/calendar-view/tray.jsx +5 -4
- package/registry/blocks/chart/README.md +9 -4
- package/registry/blocks/chart/cartesian.jsx +31 -24
- package/registry/blocks/chart/core.jsx +21 -0
- package/registry/blocks/chart/pie.jsx +15 -10
- package/registry/blocks/chart/sparkline.jsx +32 -14
- package/registry/blocks/chat/message.jsx +4 -4
- package/registry/blocks/comment-box/README.md +1 -1
- package/registry/blocks/comment-box/comment-time.jsx +4 -6
- package/registry/blocks/data-table/README.md +4 -0
- package/registry/blocks/data-table/data-table.jsx +20 -6
- package/registry/blocks/flow-canvas/edges.jsx +4 -2
- package/registry/blocks/flow-canvas/theme.js +59 -48
- package/registry/blocks/job-panel/README.md +40 -3
- package/registry/blocks/job-panel/block.json +2 -2
- package/registry/blocks/job-panel/demo.jsx +75 -6
- package/registry/blocks/job-panel/job-panel.jsx +60 -11
- package/registry/blocks/mind-map/README.md +9 -2
- package/registry/blocks/mind-map/mind-map.jsx +51 -12
- package/registry/blocks/mind-map/tree.js +33 -0
- package/registry/blocks/org-chart/org-chart.jsx +1 -1
- package/registry/blocks/pdf-viewer/notes.jsx +4 -3
- package/registry/blocks/record-notes/README.md +1 -1
- package/registry/blocks/record-notes/record-notes.jsx +4 -5
- package/registry/blocks/sidebar/sidebar-panel.jsx +4 -4
- package/registry/blocks/sidebar/sidebar.jsx +13 -13
- package/registry/blocks/signature-pad/signature.js +8 -1
- package/registry/blocks/spreadsheet/cells.jsx +2 -2
- package/src/shadcn.mjs +1 -1
- package/templates/next/files/README.md +410 -86
- package/templates/next/files/app/auto-refresh.jsx +42 -15
- package/templates/next/files/app/claw-alive/route.js +17 -0
- package/templates/next/files/app/error-relay.jsx +9 -4
- package/templates/next/files/app/escape-relay.jsx +27 -0
- package/templates/next/files/app/frame.js +409 -0
- package/templates/next/files/app/layout.jsx +18 -3
- package/templates/next/files/check/run.mjs +51 -18
- package/templates/next/files/components/parts/fixture.jsx +29 -4
- package/templates/next/files/components/parts/index.js +2 -1
- package/templates/next/files/components/parts/live-work.jsx +235 -0
- package/templates/next/files/components/ui/button.jsx +6 -1
- package/templates/next/files/components/ui/calendar.jsx +24 -0
- package/templates/next/files/components/ui/date-picker.jsx +15 -2
- package/templates/next/files/components/ui/field.jsx +12 -3
- package/templates/next/files/components/ui/icon.jsx +37 -2
- package/templates/next/files/components/ui/index.js +1 -0
- package/templates/next/files/components/ui/sheet.jsx +38 -2
- package/templates/next/files/components/ui/toggle.jsx +20 -2
- package/templates/next/files/lib/actions.js +63 -5
- package/templates/next/files/lib/claw.js +127 -28
- package/templates/next/files/lib/data.js +4 -1
- package/templates/next/files/lib/db.js +18 -1
- package/templates/next/files/lib/hooks.js +493 -37
- package/templates/next/files/lib/platform.js +113 -15
- package/templates/next/files/surfaces/README.md +3 -0
- package/templates/next/template.json +19 -8
|
@@ -56,19 +56,21 @@ app/DESIGN.md the design, written before the code (§3)
|
|
|
56
56
|
surfaces/<key>.jsx one file per domain surface
|
|
57
57
|
surfaces.js the manifest entries (import + component when built)
|
|
58
58
|
components/*.jsx anything shared between surfaces
|
|
59
|
+
components/*-actions.js your own server actions, for a write lib/actions.js cannot make (§5)
|
|
59
60
|
components/depictions/ pictures of things that exist elsewhere, in their own colours (§3c)
|
|
60
61
|
```
|
|
61
62
|
|
|
62
63
|
Keep — the contract; do not edit:
|
|
63
64
|
|
|
64
65
|
```
|
|
65
|
-
app/layout.jsx <html data-theme>, the _claw/theme.css link, AutoRefresh, ErrorRelay, Shell
|
|
66
|
+
app/layout.jsx <html data-theme>, the _claw/theme.css link, AutoRefresh, ErrorRelay, EscapeRelay, Examples, Shell
|
|
66
67
|
app/globals.css the workspace theme's tokens (§1) — no palette of your own goes here
|
|
67
68
|
app/page.jsx / → the home surface (or "No surfaces yet" while the manifest is empty)
|
|
68
69
|
app/s/[key]/page.jsx renders the surface with that key; planned ones say so
|
|
69
|
-
app/error.jsx, app/global-error.jsx, app/not-found.jsx, app/error-relay.jsx, app/auto-refresh.jsx
|
|
70
|
+
app/error.jsx, app/global-error.jsx, app/not-found.jsx, app/error-relay.jsx, app/auto-refresh.jsx,
|
|
71
|
+
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
|
|
70
72
|
components/ui/ the kit: Button, Field, Input, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction… (§3b, §3c)
|
|
71
|
-
components/parts/ the parts: RecordSwitcher, useFixture + FixtureNotice (§3b)
|
|
73
|
+
components/parts/ the parts: RecordSwitcher, LiveEmpty + LiveWork, useFixture + FixtureNotice (§3a, §3b)
|
|
72
74
|
check/pins.json the fixed files by hash; `npm run check` refuses one that differs
|
|
73
75
|
lib/* db, data, actions, claw, platform, hooks, sample (§5)
|
|
74
76
|
check/ `npm run check` (§6)
|
|
@@ -95,7 +97,8 @@ Before any code, write `app/DESIGN.md` — one screen of text:
|
|
|
95
97
|
`font-mono`, `font-display`). Numbers tabular (`data-nums` on the element).
|
|
96
98
|
- Layout and nav: where the surfaces live, what the home surface shows in
|
|
97
99
|
the first second.
|
|
98
|
-
- Density, empty states
|
|
100
|
+
- Density, empty states (§3a: what each surface says while its table is
|
|
101
|
+
empty), error states.
|
|
99
102
|
- What makes this app look like ITS domain and not another.
|
|
100
103
|
|
|
101
104
|
Then `app/shell.jsx`, then the surfaces in the order the founder uses
|
|
@@ -109,15 +112,74 @@ The starter `app/shell.jsx` is a placeholder; replace it first.
|
|
|
109
112
|
|
|
110
113
|
What the OS does **not** own is asking the agent for something. Those
|
|
111
114
|
tabs are where the founder goes to look back; the app is where they set
|
|
112
|
-
work going and
|
|
113
|
-
"Generate a launch plan" beside "New launch plan" — is
|
|
114
|
-
tab, and so is the panel it opens while the work runs.
|
|
115
|
+
work going and see where it has got (`useJob`, `ask()`, §5). A verb on an
|
|
116
|
+
object list — "Generate a launch plan" beside "New launch plan" — is
|
|
117
|
+
part of the App tab, and so is the panel it opens while the work runs.
|
|
118
|
+
The conversation is not: since 0.4.0 an ask is a request, and its
|
|
119
|
+
conversation — the agent's questions, the founder's answers, the yes
|
|
120
|
+
before it acts — lives in the desk's agent column, which the ask opens.
|
|
115
121
|
|
|
116
122
|
**The rule: no two apps in a workspace should be mistaken for each other
|
|
117
123
|
at a glance.** The design comes from the domain, not from a default. The
|
|
118
124
|
colours are the same across the workspace by design; what tells the apps
|
|
119
125
|
apart is the shape of each surface — which is your work.
|
|
120
126
|
|
|
127
|
+
### 3a. Empty states — the work coming, never an example
|
|
128
|
+
|
|
129
|
+
**A workspace app opens empty, and its empty state shows the live work
|
|
130
|
+
— never an example.** The workspace's tables are shared by every app and
|
|
131
|
+
every teammate, so an example there reads as a real row; and the member
|
|
132
|
+
asked for results, not a demo. A table is empty because its agent has
|
|
133
|
+
not got to it yet, or is on it right now. So an empty surface says which:
|
|
134
|
+
|
|
135
|
+
```jsx
|
|
136
|
+
// components/Deals.jsx (client)
|
|
137
|
+
"use client";
|
|
138
|
+
import { useState } from "react";
|
|
139
|
+
import { useRequests } from "../lib/hooks";
|
|
140
|
+
import { LiveEmpty } from "./parts";
|
|
141
|
+
|
|
142
|
+
export default function Deals({ rows }) {
|
|
143
|
+
const work = useRequests({ surface: "deals" });
|
|
144
|
+
const [adding, setAdding] = useState(false);
|
|
145
|
+
if (!rows.length && !adding)
|
|
146
|
+
return <LiveEmpty work={work} surface="deals" icon="inbox" title="No deals yet"
|
|
147
|
+
line="Ask the agent to find some, or add one."
|
|
148
|
+
action={{ label: "Add a deal", onClick: () => setAdding(true) }} />;
|
|
149
|
+
/* … the deals, and the form while adding */
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
With work in flight for this surface or its job — or the app's scheduled
|
|
154
|
+
run going — `LiveEmpty` shows it as one row (its line, whose ask it is,
|
|
155
|
+
what it waits behind, Answer when it needs the founder) over "Rows appear
|
|
156
|
+
here as they land". With none, it is the plain empty block: what would
|
|
157
|
+
be here, how it gets here, and the manual way to add one. Props: §3b.
|
|
158
|
+
|
|
159
|
+
Name the job as well as the surface when its work can be asked for from
|
|
160
|
+
the desk — `job: "find-deals"` to `useRequests` and `job="find-deals"`
|
|
161
|
+
to `LiveEmpty`. An ask made on this surface carries the surface; one the
|
|
162
|
+
desk hands to the app carries only its job, so a surface alone misses
|
|
163
|
+
it. With neither, any of the app's live work shows.
|
|
164
|
+
|
|
165
|
+
Examples show only OUTSIDE a workspace: in the store's authoring loop,
|
|
166
|
+
where the app runs on its own with nothing but `DATABASE_URL`, and in a
|
|
167
|
+
shadcn project. There a surface may still open on a worked example —
|
|
168
|
+
`useFixture(rows, SAMPLE)`, §3b — so its author sees it filled.
|
|
169
|
+
`app/layout.jsx` decides per request: examples are off whenever
|
|
170
|
+
`CLAW_ARTIFACT_ID` is set (`product_run` sets it for every workspace app)
|
|
171
|
+
and on otherwise; `CLAW_EXAMPLES=1` forces them on. Inside a workspace
|
|
172
|
+
`useFixture` hands back the real rows — none — so the same surface falls
|
|
173
|
+
through to `LiveEmpty`. Up to 0.3.x a workspace app opened on its example
|
|
174
|
+
until its table had a row; since 0.4.0 it opens empty.
|
|
175
|
+
|
|
176
|
+
An example is never rendered any other way: `npm run check` refuses a
|
|
177
|
+
`SAMPLE` constant (any `…SAMPLE…`, `…EXAMPLE…`, `…FIXTURE…` or `…DEMO…`
|
|
178
|
+
name) in a file that renders without calling `useFixture()`, because only
|
|
179
|
+
`useFixture` knows it is inside a workspace. `markSample` alone does not
|
|
180
|
+
count (it did up to 0.3.x): it only gives rows their ids, so an example
|
|
181
|
+
marked by hand would show to every member.
|
|
182
|
+
|
|
121
183
|
### 3b. Parts — assemble, never hand-roll
|
|
122
184
|
|
|
123
185
|
Every control in the app is a part from `components/ui`; the pieces that
|
|
@@ -133,7 +195,7 @@ import { Button, IconButton, ButtonGroup, ButtonGroupSeparator, Toggle, ToggleGr
|
|
|
133
195
|
Field, Input, TimeInput, Textarea, Select, Switch, Checkbox, RadioGroup, Radio, Rating,
|
|
134
196
|
InputGroup, PasswordInput, InputOTP, Slider, Image, FileInput, FileDrop, fileMatches, Combobox, Command, CommandDialog, Calendar, DatePicker, DateInput, DATE_PRESETS,
|
|
135
197
|
parseDate, isDateString, toDateString, todayString, addDays, addMonths,
|
|
136
|
-
startOfMonth, endOfMonth, dayOfWeek, formatDate, firstDayOfWeek,
|
|
198
|
+
startOfMonth, endOfMonth, dayOfWeek, formatDate, toLocalDate, firstDayOfWeek,
|
|
137
199
|
Card, Notice, Badge, Kicker, Empty, Item, ItemGroup, Avatar, AvatarGroup,
|
|
138
200
|
Sheet, Confirm, Tabs, TabPanel, Menu, ContextMenu, Popover, Tooltip, HoverCard,
|
|
139
201
|
Breadcrumb, Pagination, Collapsible, Accordion, Carousel, ScrollArea,
|
|
@@ -143,10 +205,12 @@ import { Button, IconButton, ButtonGroup, ButtonGroupSeparator, Toggle, ToggleGr
|
|
|
143
205
|
Stat, StatGroup, Delta, StatusDot, Legend, Meter, Distribution, Swatch, Steps,
|
|
144
206
|
toast, Toaster, Depiction, Icon, ICON_NAMES, usePopoverPosition, panelClass, cx }
|
|
145
207
|
from "../components/ui"; // "./ui" from a component
|
|
146
|
-
import { RecordSwitcher, useFixture, FixtureNotice, isSample
|
|
208
|
+
import { RecordSwitcher, LiveEmpty, LiveWork, liveWords, useFixture, FixtureNotice, isSample,
|
|
209
|
+
Examples, useExamples } from "../components/parts";
|
|
147
210
|
```
|
|
148
211
|
|
|
149
|
-
- `Button` — `variant` primary (one per surface
|
|
212
|
+
- `Button` — `variant` primary (one per surface, in the app's accent —
|
|
213
|
+
say it: a Button with no `variant` is secondary) · secondary · quiet ·
|
|
150
214
|
danger · link (inline text: no padding or height, wraps and truncates
|
|
151
215
|
with its sentence; `size="inherit"` takes the sentence's size;
|
|
152
216
|
`tone="muted"` for a secondary link) · nav (a full-width navigation row, start-aligned, normal
|
|
@@ -155,8 +219,10 @@ import { RecordSwitcher, useFixture, FixtureNotice, isSample } from "../componen
|
|
|
155
219
|
`IconButton` for an icon alone (`label` required). `loading` while its
|
|
156
220
|
action runs: a spinner, the same width, `aria-busy`, disabled.
|
|
157
221
|
- `Field` wraps `Input`, `Textarea`, `Select`, `Switch`, `Checkbox` with
|
|
158
|
-
its label, hint and error. `
|
|
159
|
-
|
|
222
|
+
its label, hint and error. An `Input` is as wide as its column; a width
|
|
223
|
+
in its `className` (`w-24`, `w-auto`) sets its own. `Select` with
|
|
224
|
+
empty `options` renders a muted line, never a disabled dropdown.
|
|
225
|
+
`TimeInput` — a time of day
|
|
160
226
|
("HH:MM"; `min max step`), the kit's clock in place of the browser's.
|
|
161
227
|
- `Rating` — stars: read-only `value` of `max` (halves drawn, "4.5 out
|
|
162
228
|
of 5" to a reader), or with `onChange` a radio-group input; `size`, `label`.
|
|
@@ -183,8 +249,10 @@ import { RecordSwitcher, useFixture, FixtureNotice, isSample } from "../componen
|
|
|
183
249
|
- `Sheet` — the one overlay, a rail beside the work (`side="end"`), a
|
|
184
250
|
navigation drawer (`start`), a dialog (`center`) or a bottom sheet; on a narrow window every side
|
|
185
251
|
becomes a bottom sheet; `bare` + `label` for a panel that draws its own
|
|
186
|
-
top (no header, flush body). `
|
|
187
|
-
|
|
252
|
+
top (no header, flush body). A field inside with `autoFocus` takes the
|
|
253
|
+
focus once it opens (since 0.5.0; before, Close had it). `Confirm` is
|
|
254
|
+
the "delete this?" case; `consequences`, `requireText` and
|
|
255
|
+
`acknowledge` hold it until met.
|
|
188
256
|
- `Image src alt ratio fit rounded` — a picture of a thing (thumbnail,
|
|
189
257
|
cover): Skeleton while it loads, a glyph when it fails, lazy, `alt`
|
|
190
258
|
required. Never a raw `<img>`; a face is an `Avatar`.
|
|
@@ -214,9 +282,16 @@ import { RecordSwitcher, useFixture, FixtureNotice, isSample } from "../componen
|
|
|
214
282
|
`numberOfMonths`, `captionLayout` dropdown · none (you show and move a
|
|
215
283
|
controlled `month`), `showWeekNumbers`, `weekStartsOn`, `marked` with
|
|
216
284
|
`emphasizeMarked` and `onlyMarked`) and
|
|
217
|
-
`DateInput` (a typed date with the Calendar a button away
|
|
285
|
+
`DateInput` (a typed date with the Calendar a button away;
|
|
286
|
+
`clearable` for an ×). Dates are
|
|
218
287
|
`"YYYY-MM-DD"` strings in and out — never a Date — and `addDays`,
|
|
219
|
-
`addMonths`, `formatDate`… do the arithmetic.
|
|
288
|
+
`addMonths`, `formatDate`… do the arithmetic. A `date` column reads
|
|
289
|
+
as that same string (§4), so a row's day goes into a `DatePicker` and
|
|
290
|
+
back unchanged. Never `new Date("2026-09-28")`: that is UTC's
|
|
291
|
+
midnight, the 27th west of Greenwich, and a form that saves what it
|
|
292
|
+
shows moves the day back. Showing a day beside times (a due date by
|
|
293
|
+
a sent-at), `toLocalDate(value)` gives a Date at that day's LOCAL
|
|
294
|
+
midnight (a timestamp or a Date passes through).
|
|
220
295
|
- `RadioGroup` (`options` or `<Radio>` children, each with a
|
|
221
296
|
`description`; `variant` list · card; `orientation`; in a `Field` with
|
|
222
297
|
the same `id`), `Toggle` (`pressed` + `onPressedChange`, aria-pressed),
|
|
@@ -248,7 +323,8 @@ import { RecordSwitcher, useFixture, FixtureNotice, isSample } from "../componen
|
|
|
248
323
|
fill · code — a version tag or a commit hash in the mono face; `hue`
|
|
249
324
|
1..5 for a KIND of thing — a platform, a content type — never a state:
|
|
250
325
|
tone is status, hue is category), `Kicker` (the small line above a heading), `Empty` (a filter
|
|
251
|
-
or search with no matches — not the landing state
|
|
326
|
+
or search with no matches — not the landing state: an empty table is
|
|
327
|
+
`LiveEmpty`, below).
|
|
252
328
|
- `Spinner` (size sm · md · lg), `Skeleton` (shape line · circle · rect,
|
|
253
329
|
sized by className), `Progress` (`value` of `max`, no value for
|
|
254
330
|
indeterminate; tone; `label`) — waiting, never a hand-drawn loader.
|
|
@@ -285,17 +361,46 @@ import { RecordSwitcher, useFixture, FixtureNotice, isSample } from "../componen
|
|
|
285
361
|
counter; `loop`, `autoplay` only when asked), `ResizablePanelGroup`
|
|
286
362
|
+ `ResizablePanel defaultSize minSize` + `ResizableHandle grip`
|
|
287
363
|
(`storageKey` remembers; a horizontal group stacks below md).
|
|
288
|
-
- `Icon name="…"` — one sprite, `ICON_NAMES` lists it
|
|
289
|
-
the
|
|
364
|
+
- `Icon name="…" size` — one sprite, `ICON_NAMES` lists it; `size` is
|
|
365
|
+
pixels or xs · sm · md (16, the default) · lg · xl. Missing one? Pick the nearest in
|
|
366
|
+
`ICON_NAMES`: the sprite is fixed like the rest of the kit, so a new
|
|
367
|
+
glyph comes with a template release. Never an `<svg>` in a surface.
|
|
290
368
|
- `RecordSwitcher` — pick a saved record or make a new one. With nothing
|
|
291
369
|
saved it shows only New. A disabled dropdown reading "No saved…" is the
|
|
292
370
|
failure this part exists to prevent.
|
|
293
|
-
- `
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
371
|
+
- `LiveEmpty work surface job icon title line action children` — what
|
|
372
|
+
an empty collection shows (§3a). `work` is what `useRequests()`
|
|
373
|
+
returns (§5); with none (a shadcn project) it is only the empty block.
|
|
374
|
+
`surface` and `job` pick the work that lands here: an ask made on this
|
|
375
|
+
surface, or work on this job wherever it was asked for — either is a
|
|
376
|
+
way in; neither means any live work. With work in flight it draws a
|
|
377
|
+
`LiveWork` row and "Rows appear here as they land"; else the empty
|
|
378
|
+
block — `icon` (default `inbox`), `title` (what would be here: "No
|
|
379
|
+
deals yet"), `line` (how it gets here), `action` (`{ label, onClick,
|
|
380
|
+
href, icon, variant }` for a Button — the manual way in — or an
|
|
381
|
+
element as it is). `children` go under either state.
|
|
382
|
+
- `LiveWork request run onOpen onStop` — one piece of work in flight, as
|
|
383
|
+
a row: a pulsing dot, the request's line, "from Ana's request" when it
|
|
384
|
+
is not yours, "Queued · after …" while it waits its turn. A request
|
|
385
|
+
that needs an answer takes the warning tone and, for the one who
|
|
386
|
+
asked, **Answer** (calls `onOpen(request)`: the question is in the
|
|
387
|
+
desk's agent column); your own ask that is still going gets **Stop**
|
|
388
|
+
(calls `onStop(id)`). With no `request`, a `run` that is going shows
|
|
389
|
+
its title. `LiveEmpty` draws it for you; draw it yourself for work
|
|
390
|
+
beside a list that is not empty. `liveWords(requestOrStatus)` is the
|
|
391
|
+
status word alone: Working, Needs you, Answered, Done, Stopped, Didn't
|
|
392
|
+
finish.
|
|
393
|
+
- `useFixture(rows, SAMPLE)` → `{ rows, isFixture, clear, restore }` +
|
|
394
|
+
`FixtureNotice` — the worked example, for the store's authoring loop
|
|
395
|
+
only (§3a): shown only outside a workspace, only while the real
|
|
396
|
+
collection is empty, and **never INSERTed**. Inside a workspace it
|
|
397
|
+
returns the real rows (none) with `isFixture` false, and `clear` does
|
|
398
|
+
nothing. The workspace database is shared by every app; an invented
|
|
399
|
+
row is a real row somewhere else. Clearing writes nothing. Editing a
|
|
400
|
+
fixture row copies its values into a new draft and saves THAT (the
|
|
401
|
+
platform refuses an ask about one: `row._sample` is not the founder's
|
|
402
|
+
data). `<Examples allowed>` is the switch `app/layout.jsx` sets and
|
|
403
|
+
`useExamples()` reads; the layout is fixed, so you never mount it.
|
|
299
404
|
|
|
300
405
|
A control with nothing to act on is not drawn.
|
|
301
406
|
|
|
@@ -412,7 +517,14 @@ surfaces.js server files only (entries import components)
|
|
|
412
517
|
```
|
|
413
518
|
|
|
414
519
|
Pass plain data across the boundary — strings, numbers, arrays, objects.
|
|
415
|
-
`plain(row)` already turns
|
|
520
|
+
`plain(row)` already turns a timestamp into its ISO string. A `date`
|
|
521
|
+
column is its day, `"2026-09-28"`: text, the same day for every reader,
|
|
522
|
+
from `loadRows` and from `query()` alike (`lib/db.js` reads it so). Up to
|
|
523
|
+
0.4.x `query()` gave a Date at the server's midnight and the reads an ISO
|
|
524
|
+
timestamp of it, a day off on either side of UTC. A server file that
|
|
525
|
+
called a Date method on a `date` value now holds the day's string:
|
|
526
|
+
compare days as strings (`"2026-09-28" < "2026-10-01"`), and build a
|
|
527
|
+
Date only where a sum needs one. A client component can
|
|
416
528
|
receive a server action as a prop, and nothing else that is a function.
|
|
417
529
|
|
|
418
530
|
A surface file that uses hooks or event handlers starts with `"use
|
|
@@ -437,7 +549,7 @@ import { moveRow } from "../lib/actions";
|
|
|
437
549
|
import { Card, Notice } from "./ui";
|
|
438
550
|
import { FixtureNotice, useFixture } from "./parts";
|
|
439
551
|
|
|
440
|
-
const SAMPLE = [{ name: "Ada Ventures", stage: "warm" }, /* … */];
|
|
552
|
+
const SAMPLE = [{ name: "Ada Ventures", stage: "warm" }, /* … */]; // shown only outside a workspace (§3a)
|
|
441
553
|
|
|
442
554
|
export default function InvestorBoard({ rows: real, error, highlight }) {
|
|
443
555
|
const { rows, isFixture, clear } = useFixture(real, SAMPLE);
|
|
@@ -449,6 +561,27 @@ Every surface component receives `surface` (`{ key, kind, label, source
|
|
|
449
561
|
}`) and `searchParams` (the URL query; `?row=<id>` is the convention for
|
|
450
562
|
"highlight this row" — an activity line opens its surface with it).
|
|
451
563
|
|
|
564
|
+
### Views by link
|
|
565
|
+
|
|
566
|
+
A result points into the app. When a job is done, the desk tells the
|
|
567
|
+
member where it landed ("50 emails sent · Pipeline CRM › Sales") and
|
|
568
|
+
that link opens the app's window on one surface with a query: `/s/<key>?…`
|
|
569
|
+
inside the app, `/a/<id>/app/s/<key>?…` on the desk. So the query is
|
|
570
|
+
part of the contract, the same way `?row=` is:
|
|
571
|
+
|
|
572
|
+
- `?list=<value>` shows one list. An app that keeps separate lists in
|
|
573
|
+
shared tables declares which column names them (`lists: { table,
|
|
574
|
+
column }`), and every surface over that table reads `list` and filters
|
|
575
|
+
by that column — `loadRows({ source, filter: searchParams.list ? {
|
|
576
|
+
[column]: searchParams.list } : undefined })`. With no `list` it shows
|
|
577
|
+
every list: a filter of `{ [column]: undefined }` matches no row at
|
|
578
|
+
all. It is the one key every app with lists honours.
|
|
579
|
+
- A job's view may declare a filter (`week=this&list={list}`; `{list}`
|
|
580
|
+
and `{since}` are filled in per request). Its keys are query params
|
|
581
|
+
the surface ALREADY reads: declare one only for a param the surface
|
|
582
|
+
honours, and honour every one you declare. `t`, `theme`, `chat` and
|
|
583
|
+
any key starting `_` belong to the platform.
|
|
584
|
+
|
|
452
585
|
### Linking — `next/link`, never a bare `<a href="/…">`
|
|
453
586
|
|
|
454
587
|
The app is served under a prefix (`/p/<label>` in local dev, nothing in
|
|
@@ -521,7 +654,8 @@ const { counts } = await countBy({ source: "public.investors", column: "stage" }
|
|
|
521
654
|
const { rows: log } = await loadActivity({ surface: "drafts", limit: 50 });
|
|
522
655
|
```
|
|
523
656
|
|
|
524
|
-
Helpers: `plain(row)` (
|
|
657
|
+
Helpers: `plain(row)` (timestamps → ISO, buffers → base64; a `date` is
|
|
658
|
+
already its `"YYYY-MM-DD"`), `orderClause(orderBy,
|
|
525
659
|
fallback)` (validated `ORDER BY` text), `markSample(sample, idColumn)`
|
|
526
660
|
(stable ids + `_sample: true`), `currentSurface(explicit)` (the surface
|
|
527
661
|
key of the request, from `x-claw-surface`, unless one is passed).
|
|
@@ -546,6 +680,13 @@ saveSetting({ mode = "table" | "json", source, keyColumn = "key", valueColumn =
|
|
|
546
680
|
when missing — a plain `key text, value text` table is enough. JSON mode
|
|
547
681
|
sets `key` inside the jsonb `column` of row `id`.
|
|
548
682
|
|
|
683
|
+
Every update (`updateRow`, `moveRow`, `saveSetting`) sets `updated_at =
|
|
684
|
+
now()` when the table has an `updated_at` timestamp it can write (since
|
|
685
|
+
0.5.0; before, only a trigger of the app's own kept it). A patch that
|
|
686
|
+
sets `updated_at` itself wins, and a column of that name that is text, a
|
|
687
|
+
number or a view's computed value is left alone. An insert leaves it to
|
|
688
|
+
the column's default: `updated_at timestamptz not null default now()`.
|
|
689
|
+
|
|
549
690
|
```js
|
|
550
691
|
"use client";
|
|
551
692
|
import { moveRow } from "../lib/actions";
|
|
@@ -553,47 +694,101 @@ const r = await moveRow({ source: "public.investors", id, stateColumn: "stage",
|
|
|
553
694
|
if (!r.ok) setError(r.error);
|
|
554
695
|
```
|
|
555
696
|
|
|
556
|
-
Never write raw SQL from a surface.
|
|
557
|
-
|
|
697
|
+
Never write raw SQL from a surface. `lib/*.js` is fixed (`npm run check`
|
|
698
|
+
refuses an edit, and `makersclaw update` lays the file again), so a write
|
|
699
|
+
these cannot make goes in a server-action file of the app's own:
|
|
700
|
+
`"use server"` on its first line, beside the components that call it
|
|
701
|
+
(`components/deal-actions.js`). There it composes the actions above, or
|
|
702
|
+
runs its UPDATE or DELETE through `query()` from `lib/db.js`, records it
|
|
703
|
+
with `logActivity()` and ends with `revalidatePath("/", "layout")`, as
|
|
704
|
+
`lib/actions.js` does. A new row still goes through `insertRow`: the
|
|
705
|
+
check refuses a raw `INSERT INTO` anywhere outside `lib/`.
|
|
558
706
|
|
|
559
707
|
### Platform — `lib/platform.js` (browser)
|
|
560
708
|
|
|
561
709
|
Typed wrappers over `lib/claw.js`, one per `_claw` route an app has a
|
|
562
710
|
reason to call. Every call resolves `{ ok, status, data }` or `{ ok:
|
|
563
|
-
false, status, error, unavailable }`; nothing throws. `unavailable:
|
|
564
|
-
(404 or no network) means the gateway has
|
|
565
|
-
quiet "not connected yet", not an error.
|
|
711
|
+
false, status, error, unavailable, code }`; nothing throws. `unavailable:
|
|
712
|
+
true` (a 404 that names no refusal, or no network) means the gateway has
|
|
713
|
+
no such route yet: show a quiet "not connected yet", not an error.
|
|
714
|
+
`code` names a refusal a caller can act on (`not_your_thread`,
|
|
715
|
+
`needs_answer`, `queue_full`, `unknown_job`, …).
|
|
566
716
|
|
|
567
717
|
```js
|
|
568
718
|
import { platform } from "../lib/platform"; // or the named functions
|
|
569
|
-
platform.me() GET _claw/me
|
|
570
|
-
platform.
|
|
571
|
-
|
|
719
|
+
platform.me() GET _claw/me → { artifact: { id, name, vehicle, status, home }, user: { id, email }, blueprint, desk_origin }
|
|
720
|
+
platform.ask({ text, row, how, job, list, surface, sessionId, open })
|
|
721
|
+
POST _claw/ask → { ok, request, desk_origin }, and the column opens on it
|
|
722
|
+
platform.requests() GET _claw/requests → { requests, run, desk_origin }
|
|
723
|
+
platform.requests({ id }) GET _claw/requests?id=<id> → { request }
|
|
724
|
+
platform.stopRequest(id) POST _claw/requests/<id>/stop → { ok, request }
|
|
725
|
+
platform.openRequest(requestOrId) open its conversation in the desk's agent column → true when the desk was asked
|
|
726
|
+
platform.surfaceOfPath(pathname) the <key> of /s/<key> in a path, or undefined
|
|
572
727
|
platform.reportError({ message, stack, url, source }) POST _claw/events
|
|
728
|
+
platform.startJob(message, { sessionId, onEvent }) POST _claw/chat → streams session · delta · tool · done · error (legacy)
|
|
729
|
+
platform.cancelJob(sessionId) POST _claw/cancel (legacy)
|
|
573
730
|
```
|
|
574
731
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
732
|
+
A request, as `ask` and `requests` hand it back: `{ id, title, words,
|
|
733
|
+
status, worker, line, live_line, job_key, list_key, surface, progress: {
|
|
734
|
+
n, of, noun, at } | null, result: { sentence, count, badge, view: {
|
|
735
|
+
surface, label, path, window_path } | null } | null, queued: { reason,
|
|
736
|
+
after_id, after_title } | null, app_session_id, mine, asker_name,
|
|
737
|
+
created_at, updated_at, ended_at }`. `status` is one of eleven: seven
|
|
738
|
+
live — routing, queued, installing, setting_up, building, working,
|
|
739
|
+
needs_you — then answered (open, but no work is coming), done, stopped,
|
|
740
|
+
didnt_finish. `line` is the one line every screen shows for it
|
|
741
|
+
("Drafting · 12 of 50", "Needs you · Send 50 emails from you@acme.com?");
|
|
742
|
+
show it as it is, never a status word of your own. `app_session_id` is
|
|
743
|
+
set only on your own requests.
|
|
744
|
+
|
|
745
|
+
Named exports: `me, ask, requests, stopRequest, openRequest,
|
|
746
|
+
surfaceOfPath, startJob, cancelJob, reportError, platform`. That is the
|
|
747
|
+
whole list. Note what is **not** there: the runs, the chat history, Run
|
|
748
|
+
now, Pause / Resume. Those are MONITORING and they are the OS's — the
|
|
578
749
|
window's tabs and title bar — so an app never calls `_claw/agent`,
|
|
579
|
-
`_claw/runs`, `_claw/run`, `_claw/pause` or `_claw/resume`.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
750
|
+
`_claw/runs`, `_claw/run`, `_claw/pause` or `_claw/resume`. Asking is the
|
|
751
|
+
opposite thing, and it belongs here (`ask()` and `useJob`, below).
|
|
752
|
+
`requests` is not monitoring either: it is this app's asks in flight, so
|
|
753
|
+
an empty surface can show the work coming (§3a). `startJob` and
|
|
754
|
+
`cancelJob` are the 0.3.x door to the same work, kept so `useJob` can
|
|
755
|
+
fall back to it on a claw that has no `_claw/ask`; new code asks. The
|
|
756
|
+
artifact's status lives on `<html data-claw-status>` and the desk's
|
|
757
|
+
origin on `<html data-claw-desk-origin>` (`publishStatus`, `publishDesk`
|
|
758
|
+
in `lib/claw.js`); `app/error-relay.jsx` and `useArtifact()` keep both
|
|
759
|
+
current. An ask tells the desk which request to open with a message to
|
|
760
|
+
that exact origin — never `"*"` — and only while the app is framed.
|
|
583
761
|
|
|
584
762
|
### Hooks — `lib/hooks.js` ("use client")
|
|
585
763
|
|
|
586
|
-
|
|
587
|
-
and
|
|
588
|
-
minute while `unavailable`), streaming and cleanup
|
|
589
|
-
renders what comes back and owns every pixel.
|
|
764
|
+
Four hooks: a generic fetch over one `_claw` route, the app's identity,
|
|
765
|
+
a job, and the app's requests. Each owns fetching, polling (while the
|
|
766
|
+
tab is visible; once a minute while `unavailable`), streaming and cleanup
|
|
767
|
+
on unmount. A surface renders what comes back and owns every pixel.
|
|
590
768
|
|
|
591
769
|
```js
|
|
592
|
-
useClaw(path, { every, enabled = true })
|
|
593
|
-
useArtifact()
|
|
594
|
-
useJob()
|
|
770
|
+
useClaw(path, { every, enabled = true }) → { data, error, loading, unavailable, reload }
|
|
771
|
+
useArtifact() → { artifact, user, blueprint, loading, error, unavailable, reload }
|
|
772
|
+
useJob() → { running, status, queued, queuedAfter, needsYou, liveLine, count, target,
|
|
773
|
+
steps, text, progress, error, sessionId, requestId, request, finishedAt,
|
|
774
|
+
start, cancel, reset, open }
|
|
775
|
+
useRequests({ surface, job, every = 5000 }) → { requests, live, run, loading, error, unavailable, reload, open, stop }
|
|
776
|
+
jobFromRequest(request) → what useJob shows for one request (pure; for tests)
|
|
595
777
|
```
|
|
596
778
|
|
|
779
|
+
`useRequests` is the work asked of this app's agent — from a screen or
|
|
780
|
+
from the desk, by anyone in the workspace. `requests` is every one the
|
|
781
|
+
gateway returned (the live ones and those that ended in the last day);
|
|
782
|
+
`live` is the ones not ended that belong here — `surface`: asks made on
|
|
783
|
+
this surface; `job`: work on this job wherever it was asked for; either
|
|
784
|
+
is a way in, neither means all; `run` is the app's scheduled run while
|
|
785
|
+
one is going, else null. `open(request)` opens its conversation in the
|
|
786
|
+
desk's agent column; `stop(id)` stops one and looks again. It looks
|
|
787
|
+
every `every` ms while work goes, every 20 s while none does, and only
|
|
788
|
+
while the App tab is on screen (behind the window's other tabs it asks
|
|
789
|
+
nothing; coming back looks at once); a start through `useJob` on the
|
|
790
|
+
same page makes it look at once. Hand it to `LiveEmpty` (§3a).
|
|
791
|
+
|
|
597
792
|
There is no chat, runs or activity hook: the OS draws those tabs.
|
|
598
793
|
Activity has no hook either — it is a Postgres table, read server-side
|
|
599
794
|
with `loadActivity` by any domain surface that wants a slice of it (a
|
|
@@ -602,9 +797,21 @@ row's history beside the row, say).
|
|
|
602
797
|
### Commissioning work — `useJob()`
|
|
603
798
|
|
|
604
799
|
The founder asks the app's agent to make something, from inside the app,
|
|
605
|
-
and
|
|
606
|
-
blunt: with invocation only on the window's Agent
|
|
607
|
-
work and then stares at an app where nothing
|
|
800
|
+
and sees it happen where the result will land. This is SOP §2.3 and the
|
|
801
|
+
reason it exists is blunt: with invocation only on the window's Agent
|
|
802
|
+
tab, a founder starts work and then stares at an app where nothing
|
|
803
|
+
changes.
|
|
804
|
+
|
|
805
|
+
Since 0.4.0 a job is a request. `job.start(text)` asks through `ask()`
|
|
806
|
+
(below): the platform makes the ask a request, hands it to the app's
|
|
807
|
+
agent, and the desk opens its conversation in the agent column. **The
|
|
808
|
+
conversation and the yes live in the desk's agent column** — the agent's
|
|
809
|
+
questions, the founder's answers, the "Send 50 emails?" before anything
|
|
810
|
+
public or paid. The app follows the request's status and draws the
|
|
811
|
+
panel from it. Up to 0.3.x the job streamed into the app over
|
|
812
|
+
`_claw/chat` and the panel carried the agent's running text; on a claw
|
|
813
|
+
that has no `_claw/ask` yet, `useJob` still does exactly that (`text`
|
|
814
|
+
filled, `requestId` null), so a 0.4.0 app runs on an older claw.
|
|
608
815
|
|
|
609
816
|
```jsx
|
|
610
817
|
const job = useJob();
|
|
@@ -612,24 +819,68 @@ const products = useProducts(); // your own data hook
|
|
|
612
819
|
|
|
613
820
|
useEffect(() => { products.reload(); }, [job.progress]); // rows as they land
|
|
614
821
|
|
|
615
|
-
<
|
|
822
|
+
<Button onClick={() => job.start("Draft a launch checklist for Rowhouse")} disabled={job.running}>
|
|
616
823
|
Generate a launch plan
|
|
617
|
-
</
|
|
618
|
-
|
|
619
|
-
{job.running || job.finishedAt ? (
|
|
620
|
-
<aside> {/* your design, not ours */}
|
|
621
|
-
{job.steps.map((s, i) => <li key={i} data-status={s.status}>{s.label}</li>)}
|
|
622
|
-
<p>{job.text}</p>
|
|
623
|
-
{job.running ? <button onClick={job.cancel}>Stop</button>
|
|
624
|
-
: <button onClick={job.reset}>Close</button>}
|
|
625
|
-
</aside>
|
|
626
|
-
) : null}
|
|
824
|
+
</Button>
|
|
825
|
+
<JobPanel job={job} title="Drafting the launch plan" doneTitle="Launch plan drafted" />
|
|
627
826
|
```
|
|
628
827
|
|
|
629
828
|
The panel is a block — `makersclaw add job-panel` — that keeps the four
|
|
630
|
-
rules below for you
|
|
631
|
-
|
|
632
|
-
|
|
829
|
+
rules below for you and says what the request says: its status and its
|
|
830
|
+
count ("Drafting · 12 of 50"), "Needs your answer" with **Answer** while
|
|
831
|
+
the agent waits on the founder, "Queued · after …" while the job waits
|
|
832
|
+
its turn. Draw your own only when it cannot say what the founder needs
|
|
833
|
+
to see, and from the same fields: mounted on `job.running ||
|
|
834
|
+
job.finishedAt || job.error`, `job.liveLine` as its line, `job.needsYou`
|
|
835
|
+
→ a button calling `job.open()`, `job.queued` →
|
|
836
|
+
`job.queuedAfter?.after_title`, Stop → `job.cancel()`, Close →
|
|
837
|
+
`job.reset()`.
|
|
838
|
+
|
|
839
|
+
**Which job.** `start` sends `job: true` unless you pass a key, and the
|
|
840
|
+
platform picks: the job whose view is this surface, else the app's only
|
|
841
|
+
job, else none. Pass `job.start(text, { job: "<key>" })` when a surface
|
|
842
|
+
starts a job whose view is ANOTHER surface — "Find 20 leads" pressed on
|
|
843
|
+
Pipeline, landing on Leads — or it picks the wrong one, or none. A key
|
|
844
|
+
the app does not declare is refused (`unknown_job`).
|
|
845
|
+
|
|
846
|
+
**One job per list runs at a time.** A start on a job that is already
|
|
847
|
+
running for that list does not run beside it: it answers `queued`
|
|
848
|
+
(`job.queued`; `job.queuedAfter.after_title` names what it waits for)
|
|
849
|
+
and starts on its own when that one ends. A job that repeats on a
|
|
850
|
+
schedule waits the same way behind the app's scheduled run. It counts as
|
|
851
|
+
running meanwhile — the panel stays, and Stop takes it out of the
|
|
852
|
+
queue. With ten asks waiting, the app refuses the next (`queue_full`).
|
|
853
|
+
|
|
854
|
+
**`start` resolves on the ask, not on the work:** `{ ok: true, data: {
|
|
855
|
+
request } }` once the platform took it, or the refusal (`{ ok: false,
|
|
856
|
+
error, code }` — `needs_answer` while the thread waits on an answer in
|
|
857
|
+
the column, `queue_full`, `unknown_job`, `no_credits`, …; the panel
|
|
858
|
+
shows the error). The hook then follows the request — every 2.5 s while
|
|
859
|
+
the App tab is on screen — until it ends. On the 0.3.x path it resolves
|
|
860
|
+
when the job ends, as it always did. React's development double mount
|
|
861
|
+
(and a fast refresh) cleans the hook up and mounts it again: it follows
|
|
862
|
+
a live request on from there, and an ask it cut off before an answer
|
|
863
|
+
came closes the panel — neither done nor failed. A job started from an
|
|
864
|
+
effect runs twice there; start it from a press.
|
|
865
|
+
|
|
866
|
+
What it holds, for the panel:
|
|
867
|
+
|
|
868
|
+
- `running` — the work is live: any of the seven live statuses, queued
|
|
869
|
+
and needs-you included. `status` is the request's own.
|
|
870
|
+
- `needsYou` — the agent waits on the founder; `open()` takes them to
|
|
871
|
+
the question in the column.
|
|
872
|
+
- `queued`, `queuedAfter` (`{ reason, after_id, after_title }`).
|
|
873
|
+
- `liveLine` — the desk's line for it; `count`, `target` — the counted
|
|
874
|
+
progress (12 and 50), or null.
|
|
875
|
+
- `steps` — one: the request, its line as the `label`. `text` — empty:
|
|
876
|
+
the account is in the column.
|
|
877
|
+
- `progress` — bumps whenever the work moves (its status, its count).
|
|
878
|
+
- `error` — the line when it didn't finish, "Stopped" when stopped.
|
|
879
|
+
- `sessionId`, `requestId`, `request` (the whole request), `finishedAt`.
|
|
880
|
+
|
|
881
|
+
Actions: `start(text, { sessionId, row, how, job, list, open })` — the
|
|
882
|
+
options are `ask()`'s; `cancel()` stops the request itself, not only the
|
|
883
|
+
panel; `reset()` closes the panel; `open()` opens the conversation.
|
|
633
884
|
|
|
634
885
|
Four rules, and a build is wrong without them:
|
|
635
886
|
|
|
@@ -638,18 +889,65 @@ Four rules, and a build is wrong without them:
|
|
|
638
889
|
Never a nav item named after the agent, and never the only way to
|
|
639
890
|
create that object — the manual form has to exist too.
|
|
640
891
|
2. **The panel does not exist when no job is running.** Mount it from
|
|
641
|
-
`job.running || job.finishedAt
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
892
|
+
`job.running || job.finishedAt || job.error` (a refused ask has an
|
|
893
|
+
error and no finish); `job.reset()` closes it. A panel that is always
|
|
894
|
+
there is the monitoring console (§2.7).
|
|
895
|
+
3. **The deliverable is ROWS.** Not `job.text` — the agent's own
|
|
896
|
+
account, and empty on a request, whose account is in the column —
|
|
897
|
+
and not the request's line. That is why `job.progress` reloads your
|
|
898
|
+
data hook, and why the panel closing leaves a fuller list behind it.
|
|
647
899
|
4. **Show `step.label`, never `step.name`.** `label` is the founder's
|
|
648
|
-
phrase ("Searching the web"
|
|
900
|
+
phrase ("Drafting · 12 of 50"; "Searching the web" on the 0.3.x
|
|
901
|
+
path); `name` is `request`, or a tool's `web_search`.
|
|
902
|
+
|
|
903
|
+
`job.sessionId` is the thread, and on a request it is set only when the
|
|
904
|
+
thread is yours. Keep it on the row the job produced and pass it back as
|
|
905
|
+
`job.start(msg, { sessionId })` to refine that one thing where it lives
|
|
906
|
+
— one conversation per object, never a global chat. A thread that is a
|
|
907
|
+
teammate's, or one the claw does not know (a 0.3.x session kept on a
|
|
908
|
+
row), is refused; `start` then asks once more without it, and the ask
|
|
909
|
+
gets a thread of its own.
|
|
910
|
+
|
|
911
|
+
### Asking the agent — `ask()`
|
|
912
|
+
|
|
913
|
+
`useJob` is `ask()` plus a panel to follow it. Call `ask()` from
|
|
914
|
+
`lib/platform.js` yourself when there is nothing for the app to follow:
|
|
915
|
+
a question about a row ("What do we know about Acme?"), a one-off whose
|
|
916
|
+
answer is words in the column, not rows. It takes one object:
|
|
917
|
+
|
|
918
|
+
- `text` — what the member reads. It is the request's words on the desk
|
|
919
|
+
(the Requests list, the column), so write it the way they would say
|
|
920
|
+
it: "Find 20 leads like Acme", never "You are a lead finder…".
|
|
921
|
+
Required; at most 2,000 characters.
|
|
922
|
+
- `how` — for the agent only: the steps, the table to write, the
|
|
923
|
+
format. The member never sees it. At most 4,000 characters.
|
|
924
|
+
- `row` — the row it is about: the agent's context (at most 8 KB as
|
|
925
|
+
JSON). Never an example row — one marked `_sample` is refused.
|
|
926
|
+
- `job`, `list` — name the job: a declared key, or `true` to let the
|
|
927
|
+
platform pick (what `useJob` sends by default), and the list its work
|
|
928
|
+
belongs to, for an app that declares lists (§4, Views by link). With
|
|
929
|
+
no `job` the ask is not a job: nothing queues, nothing is counted.
|
|
930
|
+
- `surface` — defaults to the `<key>` of `/s/<key>` in the page's path.
|
|
931
|
+
- `sessionId` — continue your own thread: while its request is open the
|
|
932
|
+
ask joins it; once it has closed, a new request follows it in the
|
|
933
|
+
same thread. While that request waits on an answer (`needs_answer`)
|
|
934
|
+
or in the queue (`queued`, 409 both) the ask is refused: the answer
|
|
935
|
+
goes in the column, and a queued one starts on its own. A teammate's
|
|
936
|
+
thread is refused (`not_your_thread`, 403) — ask again without it to
|
|
937
|
+
start your own; `useJob` does that for you.
|
|
938
|
+
- `open` — the ask opens its request in the desk's agent column, where
|
|
939
|
+
the conversation lives; `false` leaves the column as it is.
|
|
940
|
+
|
|
941
|
+
```js
|
|
942
|
+
"use client";
|
|
943
|
+
import { ask } from "../lib/platform";
|
|
944
|
+
|
|
945
|
+
const r = await ask({ text: `What do we know about ${row.name}?`, row, how: "Read the Wiki's customer pages first." });
|
|
946
|
+
if (!r.ok) setError(r.error); // r.code: needs_answer, queue_full, …
|
|
947
|
+
```
|
|
649
948
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
where it lives — one conversation per object, never a global chat.
|
|
949
|
+
It resolves once the platform took the ask (`{ ok: true, data: {
|
|
950
|
+
request, desk_origin } }`) or refused it; it never waits for the work.
|
|
653
951
|
|
|
654
952
|
### Activity — the contract
|
|
655
953
|
|
|
@@ -706,10 +1004,10 @@ second build there dies with `EACCES: .next/trace`.
|
|
|
706
1004
|
check: 5 files · 2 surfaces
|
|
707
1005
|
ok colours no colour outside the workspace theme (1 depiction excepted)
|
|
708
1006
|
ok controls every control is a kit part
|
|
709
|
-
ok examples
|
|
1007
|
+
ok examples examples show only outside a workspace (useFixture); a workspace app opens empty
|
|
710
1008
|
ok manifest 2 surfaces: drafts, sent
|
|
711
1009
|
ok icons every icon name exists
|
|
712
|
-
ok kit
|
|
1010
|
+
ok kit 70 fixed files match the template
|
|
713
1011
|
ok depictions 1 picture of elsewhere, framed on 1 surface: x-post
|
|
714
1012
|
ok smoke built in 41s · / → 307 · /s/drafts 200 · /s/sent 200 (0.6s)
|
|
715
1013
|
all clear
|
|
@@ -726,11 +1024,14 @@ The smoke test starts that build on a loopback port and loads every
|
|
|
726
1024
|
surface; `--static` skips the build and the smoke. Then, by eye:
|
|
727
1025
|
|
|
728
1026
|
- Renders at 360px, 768px and 1280px without horizontal scroll.
|
|
729
|
-
-
|
|
1027
|
+
- In a workspace, an empty table shows `LiveEmpty`: the work in flight,
|
|
1028
|
+
else what would be here and the manual way to add it — never an
|
|
1029
|
+
example (§3a). A saved row replaces it.
|
|
730
1030
|
- Error state shows the text from `{ error }`.
|
|
731
1031
|
- Keyboard focus visible on every control.
|
|
732
1032
|
- Numbers tabular (`data-nums`).
|
|
733
|
-
- Every write
|
|
1033
|
+
- Every write is a server action — `lib/actions.js`, or the app's own
|
|
1034
|
+
`"use server"` file (§5) — never raw SQL in a surface.
|
|
734
1035
|
- Every surface reachable from the nav.
|
|
735
1036
|
|
|
736
1037
|
## 7. Run
|
|
@@ -747,14 +1048,37 @@ surface; `--static` skips the build and the smoke. Then, by eye:
|
|
|
747
1048
|
server — `command "npx next dev -H 127.0.0.1"`, `build ""` — so each
|
|
748
1049
|
edit shows in the founder's live preview without a rebuild.
|
|
749
1050
|
`product_run` sets `CLAW_BASE_PATH`, `CLAW_PRODUCT`,
|
|
750
|
-
`CLAW_PRODUCT_TOKEN` and `CLAW_ARTIFACT_ID
|
|
1051
|
+
`CLAW_PRODUCT_TOKEN` and `CLAW_ARTIFACT_ID` — the last is what turns
|
|
1052
|
+
examples off (§3a).
|
|
751
1053
|
|
|
752
1054
|
Every `artifact_files` write under `app/` comes back with the console's
|
|
753
1055
|
state; a 5xx carries the log tail. Fix it before moving on — the founder
|
|
754
1056
|
is watching the preview.
|
|
755
1057
|
|
|
756
1058
|
Every page is `dynamic = "force-dynamic"`: nothing is prerendered, so a
|
|
757
|
-
build never needs the database. `app/auto-refresh.jsx` re-renders
|
|
758
|
-
the server
|
|
759
|
-
|
|
760
|
-
|
|
1059
|
+
build never needs the database. `app/auto-refresh.jsx` re-renders the
|
|
1060
|
+
page from the server while it is shown, so the agent's writes appear:
|
|
1061
|
+
one refresh at a time, 5 s after the last one landed plus what that one
|
|
1062
|
+
took (a minute at most), and only while the App tab is on screen in a
|
|
1063
|
+
visible browser tab (hidden, it asks nothing; back, it refreshes at
|
|
1064
|
+
once), nobody is typing (never while a sheet is open, a field they typed
|
|
1065
|
+
in holds focus, or within 10 s of a keystroke), and the app answers
|
|
1066
|
+
`/claw-alive` (a fixed route) with a 204.
|
|
1067
|
+
|
|
1068
|
+
**A surface that must not be refreshed opts out with
|
|
1069
|
+
`data-claw-refresh="off"` on its root element**: one whose rows only its
|
|
1070
|
+
own writes change, or a table too big to send every few seconds. While
|
|
1071
|
+
any element on the page carries it, nothing is refreshed and nothing is
|
|
1072
|
+
probed; the surface's own writes still re-render it (`lib/actions.js`
|
|
1073
|
+
revalidates), and so does every navigation. Set it from state to pause
|
|
1074
|
+
only while it matters: `data-claw-refresh={importing ? "off" : undefined}`.
|
|
1075
|
+
|
|
1076
|
+
**A page left open stops refreshing after 30 minutes with no sign of
|
|
1077
|
+
anyone** in the app — a key, the pointer, a touch, a wheel, focus. The
|
|
1078
|
+
platform stops an app nobody used for 30 minutes, and every refresh is a
|
|
1079
|
+
use, so an open tab nobody looked at kept its app running all night. The
|
|
1080
|
+
next sign of someone refreshes at once and the refresh goes on; so does
|
|
1081
|
+
coming back to the tab. Nothing on the page says so: the first touch
|
|
1082
|
+
brings it up to date.
|
|
1083
|
+
|
|
1084
|
+
The founder reads "artifact", "app", "agent", "automation" — nothing else.
|