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.
Files changed (121) hide show
  1. package/README.md +3 -1
  2. package/package.json +1 -1
  3. package/r/annotate.json +3 -3
  4. package/r/approval-request.json +4 -4
  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 +9 -9
  9. package/r/cart.json +2 -2
  10. package/r/chart.json +7 -7
  11. package/r/chat.json +3 -3
  12. package/r/code-editor.json +2 -2
  13. package/r/comment-box.json +5 -5
  14. package/r/contact-section.json +2 -2
  15. package/r/core.json +17 -11
  16. package/r/csv-import.json +3 -3
  17. package/r/data-table.json +4 -4
  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 +4 -4
  25. package/r/footer.json +2 -2
  26. package/r/gantt.json +3 -3
  27. package/r/job-panel.json +8 -5
  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 +6 -6
  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 +2 -2
  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 +6 -6
  43. package/r/registry.json +131 -123
  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 +4 -4
  49. package/r/signature-pad.json +3 -3
  50. package/r/sketch-canvas.json +2 -2
  51. package/r/spreadsheet.json +3 -3
  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 +3 -3
  58. package/registry/blocks/approval-request/README.md +2 -1
  59. package/registry/blocks/approval-request/approval-request.jsx +5 -3
  60. package/registry/blocks/calendar-view/README.md +8 -0
  61. package/registry/blocks/calendar-view/agenda.jsx +5 -4
  62. package/registry/blocks/calendar-view/calendar.jsx +23 -8
  63. package/registry/blocks/calendar-view/demo.jsx +2 -0
  64. package/registry/blocks/calendar-view/month.jsx +6 -5
  65. package/registry/blocks/calendar-view/time-grid.jsx +9 -7
  66. package/registry/blocks/calendar-view/tray.jsx +5 -4
  67. package/registry/blocks/chart/README.md +9 -4
  68. package/registry/blocks/chart/cartesian.jsx +31 -24
  69. package/registry/blocks/chart/core.jsx +21 -0
  70. package/registry/blocks/chart/pie.jsx +15 -10
  71. package/registry/blocks/chart/sparkline.jsx +32 -14
  72. package/registry/blocks/chat/message.jsx +4 -4
  73. package/registry/blocks/comment-box/README.md +1 -1
  74. package/registry/blocks/comment-box/comment-time.jsx +4 -6
  75. package/registry/blocks/data-table/README.md +4 -0
  76. package/registry/blocks/data-table/data-table.jsx +20 -6
  77. package/registry/blocks/flow-canvas/edges.jsx +4 -2
  78. package/registry/blocks/flow-canvas/theme.js +59 -48
  79. package/registry/blocks/job-panel/README.md +40 -3
  80. package/registry/blocks/job-panel/block.json +2 -2
  81. package/registry/blocks/job-panel/demo.jsx +75 -6
  82. package/registry/blocks/job-panel/job-panel.jsx +60 -11
  83. package/registry/blocks/mind-map/README.md +9 -2
  84. package/registry/blocks/mind-map/mind-map.jsx +51 -12
  85. package/registry/blocks/mind-map/tree.js +33 -0
  86. package/registry/blocks/org-chart/org-chart.jsx +1 -1
  87. package/registry/blocks/pdf-viewer/notes.jsx +4 -3
  88. package/registry/blocks/record-notes/README.md +1 -1
  89. package/registry/blocks/record-notes/record-notes.jsx +4 -5
  90. package/registry/blocks/sidebar/sidebar-panel.jsx +4 -4
  91. package/registry/blocks/sidebar/sidebar.jsx +13 -13
  92. package/registry/blocks/signature-pad/signature.js +8 -1
  93. package/registry/blocks/spreadsheet/cells.jsx +2 -2
  94. package/src/shadcn.mjs +1 -1
  95. package/templates/next/files/README.md +410 -86
  96. package/templates/next/files/app/auto-refresh.jsx +42 -15
  97. package/templates/next/files/app/claw-alive/route.js +17 -0
  98. package/templates/next/files/app/error-relay.jsx +9 -4
  99. package/templates/next/files/app/escape-relay.jsx +27 -0
  100. package/templates/next/files/app/frame.js +409 -0
  101. package/templates/next/files/app/layout.jsx +18 -3
  102. package/templates/next/files/check/run.mjs +51 -18
  103. package/templates/next/files/components/parts/fixture.jsx +29 -4
  104. package/templates/next/files/components/parts/index.js +2 -1
  105. package/templates/next/files/components/parts/live-work.jsx +235 -0
  106. package/templates/next/files/components/ui/button.jsx +6 -1
  107. package/templates/next/files/components/ui/calendar.jsx +24 -0
  108. package/templates/next/files/components/ui/date-picker.jsx +15 -2
  109. package/templates/next/files/components/ui/field.jsx +12 -3
  110. package/templates/next/files/components/ui/icon.jsx +37 -2
  111. package/templates/next/files/components/ui/index.js +1 -0
  112. package/templates/next/files/components/ui/sheet.jsx +38 -2
  113. package/templates/next/files/components/ui/toggle.jsx +20 -2
  114. package/templates/next/files/lib/actions.js +63 -5
  115. package/templates/next/files/lib/claw.js +127 -28
  116. package/templates/next/files/lib/data.js +4 -1
  117. package/templates/next/files/lib/db.js +18 -1
  118. package/templates/next/files/lib/hooks.js +493 -37
  119. package/templates/next/files/lib/platform.js +113 -15
  120. package/templates/next/files/surfaces/README.md +3 -0
  121. 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, error 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 watch it happen (`useJob`, §5). A verb on an object list —
113
- "Generate a launch plan" beside "New launch plan" — is part of the App
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 } from "../components/parts";
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) · secondary · quiet ·
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. `Select` with empty `options` renders a
159
- muted line, never a disabled dropdown. `TimeInput` — a time of day
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). `Confirm` is the "delete this?" case;
187
- `consequences`, `requireText` and `acknowledge` hold it until met.
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). Dates are
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, that is the fixture).
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. Missing one? Add
289
- the glyph to `components/ui/icon.jsx`; never an `<svg>` in a surface.
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
- - `useFixture(rows, SAMPLE)` → `{ rows, isFixture, clear }` +
294
- `FixtureNotice` — the worked example (§3: it opens populated), shown
295
- only while the real collection is empty and **never INSERTed**. The
296
- workspace database is shared by every app; an invented row is a real
297
- row somewhere else. Clearing writes nothing. Editing a fixture row
298
- copies its values into a new draft and saves THAT.
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 dates into ISO strings. A client component can
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)` (dates → ISO, buffers → base64), `orderClause(orderBy,
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. A write that needs more than these
557
- is a new function in `lib/actions.js` that also calls `logActivity`.
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: true`
564
- (404 or no network) means the gateway has no such route yet: show a
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 → { artifact: { id, name, vehicle, status, home }, user: { id, email }, blueprint }
570
- platform.startJob(message, { sessionId, onEvent }) POST _claw/chat → streams session · delta · tool · done · error
571
- platform.cancelJob(sessionId) POST _claw/cancel
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
- Named exports: `me, startJob, cancelJob, reportError, platform`. That is
576
- the whole list. Note what is **not** there: the runs, the chat history,
577
- Run now, Pause / Resume. Those are MONITORING and they are the OS's — the
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`. Starting work
580
- is the opposite thing, and it belongs here (see `useJob`). The artifact's
581
- status lives on `<html data-claw-status>` (`publishStatus` in
582
- `lib/claw.js`); `useArtifact()` keeps it current.
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
- Three hooks: a generic fetch over one `_claw` route, the app's identity,
587
- and a job. Each owns fetching, polling (while the tab is visible; once a
588
- minute while `unavailable`), streaming and cleanup on unmount. A surface
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 }) → { data, error, loading, unavailable, reload }
593
- useArtifact() → { artifact, user, blueprint, loading, error, unavailable, reload }
594
- useJob() → { running, steps, text, progress, error, sessionId, finishedAt, start, cancel, reset }
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 watches it happen. This is SOP §2.3 and the reason it exists is
606
- blunt: with invocation only on the window's Agent tab, a founder starts
607
- work and then stares at an app where nothing changes.
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
- <button onClick={() => job.start("Draft a launch checklist for Rowhouse")}>
822
+ <Button onClick={() => job.start("Draft a launch checklist for Rowhouse")} disabled={job.running}>
616
823
  Generate a launch plan
617
- </button>
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: `<JobPanel job={job} title="Drafting the launch
631
- plan" doneTitle="Launch plan drafted" />`. Draw your own only when it
632
- cannot say what the founder needs to see.
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`; `job.reset()` closes it. A panel
642
- that is always there is the monitoring console (§2.7).
643
- 3. **`job.text` is not the deliverable.** It is the agent's account of
644
- what it did. The deliverable is ROWS — that is why `job.progress`
645
- reloads your data hook, and why the panel closing leaves a fuller
646
- list behind it.
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"); `name` is `web_search`.
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
- `job.sessionId` is the thread. Keep it on the row the job produced and
651
- pass it back as `job.start(msg, { sessionId })` to refine that one thing
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 fixtures stay in code (useFixture), nothing INSERTs one
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 33 fixed files match the template
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
- - The fixture shows while the table is empty; a saved row replaces it.
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 goes through `lib/actions.js`; no raw SQL from a surface.
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 from
758
- the server every 5 s while the tab is visible, so the agent's writes
759
- appear. The founder reads "artifact", "app", "agent", "automation" —
760
- nothing else.
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.