makersclaw 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) 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 +2 -2
  5. package/r/audio-player.json +2 -2
  6. package/r/auth-form.json +2 -2
  7. package/r/board.json +2 -2
  8. package/r/calendar-view.json +3 -3
  9. package/r/cart.json +2 -2
  10. package/r/chart.json +2 -2
  11. package/r/chat.json +2 -2
  12. package/r/code-editor.json +2 -2
  13. package/r/comment-box.json +3 -3
  14. package/r/contact-section.json +2 -2
  15. package/r/core.json +12 -6
  16. package/r/csv-import.json +3 -3
  17. package/r/data-table.json +2 -2
  18. package/r/diagram.json +2 -2
  19. package/r/doc-editor.json +2 -2
  20. package/r/doc-page.json +3 -3
  21. package/r/email-render.json +2 -2
  22. package/r/faq.json +2 -2
  23. package/r/file-upload.json +2 -2
  24. package/r/flow-canvas.json +2 -2
  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 +3 -3
  32. package/r/navigation-menu.json +2 -2
  33. package/r/order-detail.json +2 -2
  34. package/r/org-chart.json +3 -3
  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 +2 -2
  39. package/r/product-detail.json +2 -2
  40. package/r/product-grid.json +3 -3
  41. package/r/record-form.json +2 -2
  42. package/r/record-notes.json +4 -4
  43. package/r/registry.json +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 +2 -2
  49. package/r/signature-pad.json +2 -2
  50. package/r/sketch-canvas.json +2 -2
  51. package/r/spreadsheet.json +2 -2
  52. package/r/stat-cards.json +3 -3
  53. package/r/task-list.json +3 -3
  54. package/r/task-model.json +2 -2
  55. package/r/testimonials.json +2 -2
  56. package/r/transaction-list.json +2 -2
  57. package/r/whiteboard.json +3 -3
  58. package/registry/blocks/job-panel/README.md +40 -3
  59. package/registry/blocks/job-panel/block.json +2 -2
  60. package/registry/blocks/job-panel/demo.jsx +75 -6
  61. package/registry/blocks/job-panel/job-panel.jsx +60 -11
  62. package/src/shadcn.mjs +1 -1
  63. package/templates/next/files/README.md +339 -73
  64. package/templates/next/files/app/auto-refresh.jsx +72 -10
  65. package/templates/next/files/app/claw-alive/route.js +17 -0
  66. package/templates/next/files/app/error-relay.jsx +9 -4
  67. package/templates/next/files/app/escape-relay.jsx +27 -0
  68. package/templates/next/files/app/frame.js +218 -0
  69. package/templates/next/files/app/layout.jsx +18 -3
  70. package/templates/next/files/check/run.mjs +25 -8
  71. package/templates/next/files/components/parts/fixture.jsx +29 -4
  72. package/templates/next/files/components/parts/index.js +2 -1
  73. package/templates/next/files/components/parts/live-work.jsx +235 -0
  74. package/templates/next/files/components/ui/button.jsx +6 -1
  75. package/templates/next/files/components/ui/date-picker.jsx +15 -2
  76. package/templates/next/files/components/ui/icon.jsx +37 -2
  77. package/templates/next/files/lib/claw.js +127 -28
  78. package/templates/next/files/lib/hooks.js +454 -33
  79. package/templates/next/files/lib/platform.js +113 -15
  80. package/templates/next/files/surfaces/README.md +3 -0
  81. package/templates/next/template.json +17 -6
@@ -62,13 +62,14 @@ components/depictions/ pictures of things that exist elsewhere, in their own col
62
62
  Keep — the contract; do not edit:
63
63
 
64
64
  ```
65
- app/layout.jsx <html data-theme>, the _claw/theme.css link, AutoRefresh, ErrorRelay, Shell
65
+ app/layout.jsx <html data-theme>, the _claw/theme.css link, AutoRefresh, ErrorRelay, EscapeRelay, Examples, Shell
66
66
  app/globals.css the workspace theme's tokens (§1) — no palette of your own goes here
67
67
  app/page.jsx / → the home surface (or "No surfaces yet" while the manifest is empty)
68
68
  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
69
+ app/error.jsx, app/global-error.jsx, app/not-found.jsx, app/error-relay.jsx, app/auto-refresh.jsx,
70
+ app/escape-relay.jsx, app/frame.js, app/claw-alive/route.js the app inside its window: refresh while shown, Escape from the desk
70
71
  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)
72
+ components/parts/ the parts: RecordSwitcher, LiveEmpty + LiveWork, useFixture + FixtureNotice (§3a, §3b)
72
73
  check/pins.json the fixed files by hash; `npm run check` refuses one that differs
73
74
  lib/* db, data, actions, claw, platform, hooks, sample (§5)
74
75
  check/ `npm run check` (§6)
@@ -95,7 +96,8 @@ Before any code, write `app/DESIGN.md` — one screen of text:
95
96
  `font-mono`, `font-display`). Numbers tabular (`data-nums` on the element).
96
97
  - Layout and nav: where the surfaces live, what the home surface shows in
97
98
  the first second.
98
- - Density, empty states, error states.
99
+ - Density, empty states (§3a: what each surface says while its table is
100
+ empty), error states.
99
101
  - What makes this app look like ITS domain and not another.
100
102
 
101
103
  Then `app/shell.jsx`, then the surfaces in the order the founder uses
@@ -109,15 +111,74 @@ The starter `app/shell.jsx` is a placeholder; replace it first.
109
111
 
110
112
  What the OS does **not** own is asking the agent for something. Those
111
113
  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.
114
+ work going and see where it has got (`useJob`, `ask()`, §5). A verb on an
115
+ object list — "Generate a launch plan" beside "New launch plan" — is
116
+ part of the App tab, and so is the panel it opens while the work runs.
117
+ The conversation is not: since 0.4.0 an ask is a request, and its
118
+ conversation — the agent's questions, the founder's answers, the yes
119
+ before it acts — lives in the desk's agent column, which the ask opens.
115
120
 
116
121
  **The rule: no two apps in a workspace should be mistaken for each other
117
122
  at a glance.** The design comes from the domain, not from a default. The
118
123
  colours are the same across the workspace by design; what tells the apps
119
124
  apart is the shape of each surface — which is your work.
120
125
 
126
+ ### 3a. Empty states — the work coming, never an example
127
+
128
+ **A workspace app opens empty, and its empty state shows the live work
129
+ — never an example.** The workspace's tables are shared by every app and
130
+ every teammate, so an example there reads as a real row; and the member
131
+ asked for results, not a demo. A table is empty because its agent has
132
+ not got to it yet, or is on it right now. So an empty surface says which:
133
+
134
+ ```jsx
135
+ // components/Deals.jsx (client)
136
+ "use client";
137
+ import { useState } from "react";
138
+ import { useRequests } from "../lib/hooks";
139
+ import { LiveEmpty } from "./parts";
140
+
141
+ export default function Deals({ rows }) {
142
+ const work = useRequests({ surface: "deals" });
143
+ const [adding, setAdding] = useState(false);
144
+ if (!rows.length && !adding)
145
+ return <LiveEmpty work={work} surface="deals" icon="inbox" title="No deals yet"
146
+ line="Ask the agent to find some, or add one."
147
+ action={{ label: "Add a deal", onClick: () => setAdding(true) }} />;
148
+ /* … the deals, and the form while adding */
149
+ }
150
+ ```
151
+
152
+ With work in flight for this surface or its job — or the app's scheduled
153
+ run going — `LiveEmpty` shows it as one row (its line, whose ask it is,
154
+ what it waits behind, Answer when it needs the founder) over "Rows appear
155
+ here as they land". With none, it is the plain empty block: what would
156
+ be here, how it gets here, and the manual way to add one. Props: §3b.
157
+
158
+ Name the job as well as the surface when its work can be asked for from
159
+ the desk — `job: "find-deals"` to `useRequests` and `job="find-deals"`
160
+ to `LiveEmpty`. An ask made on this surface carries the surface; one the
161
+ desk hands to the app carries only its job, so a surface alone misses
162
+ it. With neither, any of the app's live work shows.
163
+
164
+ Examples show only OUTSIDE a workspace: in the store's authoring loop,
165
+ where the app runs on its own with nothing but `DATABASE_URL`, and in a
166
+ shadcn project. There a surface may still open on a worked example —
167
+ `useFixture(rows, SAMPLE)`, §3b — so its author sees it filled.
168
+ `app/layout.jsx` decides per request: examples are off whenever
169
+ `CLAW_ARTIFACT_ID` is set (`product_run` sets it for every workspace app)
170
+ and on otherwise; `CLAW_EXAMPLES=1` forces them on. Inside a workspace
171
+ `useFixture` hands back the real rows — none — so the same surface falls
172
+ through to `LiveEmpty`. Up to 0.3.x a workspace app opened on its example
173
+ until its table had a row; since 0.4.0 it opens empty.
174
+
175
+ An example is never rendered any other way: `npm run check` refuses a
176
+ `SAMPLE` constant (any `…SAMPLE…`, `…EXAMPLE…`, `…FIXTURE…` or `…DEMO…`
177
+ name) in a file that renders without calling `useFixture()`, because only
178
+ `useFixture` knows it is inside a workspace. `markSample` alone does not
179
+ count (it did up to 0.3.x): it only gives rows their ids, so an example
180
+ marked by hand would show to every member.
181
+
121
182
  ### 3b. Parts — assemble, never hand-roll
122
183
 
123
184
  Every control in the app is a part from `components/ui`; the pieces that
@@ -143,10 +204,12 @@ import { Button, IconButton, ButtonGroup, ButtonGroupSeparator, Toggle, ToggleGr
143
204
  Stat, StatGroup, Delta, StatusDot, Legend, Meter, Distribution, Swatch, Steps,
144
205
  toast, Toaster, Depiction, Icon, ICON_NAMES, usePopoverPosition, panelClass, cx }
145
206
  from "../components/ui"; // "./ui" from a component
146
- import { RecordSwitcher, useFixture, FixtureNotice, isSample } from "../components/parts";
207
+ import { RecordSwitcher, LiveEmpty, LiveWork, liveWords, useFixture, FixtureNotice, isSample,
208
+ Examples, useExamples } from "../components/parts";
147
209
  ```
148
210
 
149
- - `Button` — `variant` primary (one per surface) · secondary · quiet ·
211
+ - `Button` — `variant` primary (one per surface, in the app's accent —
212
+ say it: a Button with no `variant` is secondary) · secondary · quiet ·
150
213
  danger · link (inline text: no padding or height, wraps and truncates
151
214
  with its sentence; `size="inherit"` takes the sentence's size;
152
215
  `tone="muted"` for a secondary link) · nav (a full-width navigation row, start-aligned, normal
@@ -214,7 +277,8 @@ import { RecordSwitcher, useFixture, FixtureNotice, isSample } from "../componen
214
277
  `numberOfMonths`, `captionLayout` dropdown · none (you show and move a
215
278
  controlled `month`), `showWeekNumbers`, `weekStartsOn`, `marked` with
216
279
  `emphasizeMarked` and `onlyMarked`) and
217
- `DateInput` (a typed date with the Calendar a button away). Dates are
280
+ `DateInput` (a typed date with the Calendar a button away;
281
+ `clearable` for an ×). Dates are
218
282
  `"YYYY-MM-DD"` strings in and out — never a Date — and `addDays`,
219
283
  `addMonths`, `formatDate`… do the arithmetic.
220
284
  - `RadioGroup` (`options` or `<Radio>` children, each with a
@@ -248,7 +312,8 @@ import { RecordSwitcher, useFixture, FixtureNotice, isSample } from "../componen
248
312
  fill · code — a version tag or a commit hash in the mono face; `hue`
249
313
  1..5 for a KIND of thing — a platform, a content type — never a state:
250
314
  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).
315
+ or search with no matches — not the landing state: an empty table is
316
+ `LiveEmpty`, below).
252
317
  - `Spinner` (size sm · md · lg), `Skeleton` (shape line · circle · rect,
253
318
  sized by className), `Progress` (`value` of `max`, no value for
254
319
  indeterminate; tone; `label`) — waiting, never a hand-drawn loader.
@@ -285,17 +350,46 @@ import { RecordSwitcher, useFixture, FixtureNotice, isSample } from "../componen
285
350
  counter; `loop`, `autoplay` only when asked), `ResizablePanelGroup`
286
351
  + `ResizablePanel defaultSize minSize` + `ResizableHandle grip`
287
352
  (`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.
353
+ - `Icon name="…" size` — one sprite, `ICON_NAMES` lists it; `size` is
354
+ pixels or xs · sm · md (16, the default) · lg · xl. Missing one? Pick the nearest in
355
+ `ICON_NAMES`: the sprite is fixed like the rest of the kit, so a new
356
+ glyph comes with a template release. Never an `<svg>` in a surface.
290
357
  - `RecordSwitcher` — pick a saved record or make a new one. With nothing
291
358
  saved it shows only New. A disabled dropdown reading "No saved…" is the
292
359
  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.
360
+ - `LiveEmpty work surface job icon title line action children` — what
361
+ an empty collection shows (§3a). `work` is what `useRequests()`
362
+ returns (§5); with none (a shadcn project) it is only the empty block.
363
+ `surface` and `job` pick the work that lands here: an ask made on this
364
+ surface, or work on this job wherever it was asked for — either is a
365
+ way in; neither means any live work. With work in flight it draws a
366
+ `LiveWork` row and "Rows appear here as they land"; else the empty
367
+ block — `icon` (default `inbox`), `title` (what would be here: "No
368
+ deals yet"), `line` (how it gets here), `action` (`{ label, onClick,
369
+ href, icon, variant }` for a Button — the manual way in — or an
370
+ element as it is). `children` go under either state.
371
+ - `LiveWork request run onOpen onStop` — one piece of work in flight, as
372
+ a row: a pulsing dot, the request's line, "from Ana's request" when it
373
+ is not yours, "Queued · after …" while it waits its turn. A request
374
+ that needs an answer takes the warning tone and, for the one who
375
+ asked, **Answer** (calls `onOpen(request)`: the question is in the
376
+ desk's agent column); your own ask that is still going gets **Stop**
377
+ (calls `onStop(id)`). With no `request`, a `run` that is going shows
378
+ its title. `LiveEmpty` draws it for you; draw it yourself for work
379
+ beside a list that is not empty. `liveWords(requestOrStatus)` is the
380
+ status word alone: Working, Needs you, Answered, Done, Stopped, Didn't
381
+ finish.
382
+ - `useFixture(rows, SAMPLE)` → `{ rows, isFixture, clear, restore }` +
383
+ `FixtureNotice` — the worked example, for the store's authoring loop
384
+ only (§3a): shown only outside a workspace, only while the real
385
+ collection is empty, and **never INSERTed**. Inside a workspace it
386
+ returns the real rows (none) with `isFixture` false, and `clear` does
387
+ nothing. The workspace database is shared by every app; an invented
388
+ row is a real row somewhere else. Clearing writes nothing. Editing a
389
+ fixture row copies its values into a new draft and saves THAT (the
390
+ platform refuses an ask about one: `row._sample` is not the founder's
391
+ data). `<Examples allowed>` is the switch `app/layout.jsx` sets and
392
+ `useExamples()` reads; the layout is fixed, so you never mount it.
299
393
 
300
394
  A control with nothing to act on is not drawn.
301
395
 
@@ -437,7 +531,7 @@ import { moveRow } from "../lib/actions";
437
531
  import { Card, Notice } from "./ui";
438
532
  import { FixtureNotice, useFixture } from "./parts";
439
533
 
440
- const SAMPLE = [{ name: "Ada Ventures", stage: "warm" }, /* … */];
534
+ const SAMPLE = [{ name: "Ada Ventures", stage: "warm" }, /* … */]; // shown only outside a workspace (§3a)
441
535
 
442
536
  export default function InvestorBoard({ rows: real, error, highlight }) {
443
537
  const { rows, isFixture, clear } = useFixture(real, SAMPLE);
@@ -449,6 +543,27 @@ Every surface component receives `surface` (`{ key, kind, label, source
449
543
  }`) and `searchParams` (the URL query; `?row=<id>` is the convention for
450
544
  "highlight this row" — an activity line opens its surface with it).
451
545
 
546
+ ### Views by link
547
+
548
+ A result points into the app. When a job is done, the desk tells the
549
+ member where it landed ("50 emails sent · Pipeline CRM › Sales") and
550
+ that link opens the app's window on one surface with a query: `/s/<key>?…`
551
+ inside the app, `/a/<id>/app/s/<key>?…` on the desk. So the query is
552
+ part of the contract, the same way `?row=` is:
553
+
554
+ - `?list=<value>` shows one list. An app that keeps separate lists in
555
+ shared tables declares which column names them (`lists: { table,
556
+ column }`), and every surface over that table reads `list` and filters
557
+ by that column — `loadRows({ source, filter: searchParams.list ? {
558
+ [column]: searchParams.list } : undefined })`. With no `list` it shows
559
+ every list: a filter of `{ [column]: undefined }` matches no row at
560
+ all. It is the one key every app with lists honours.
561
+ - A job's view may declare a filter (`week=this&list={list}`; `{list}`
562
+ and `{since}` are filled in per request). Its keys are query params
563
+ the surface ALREADY reads: declare one only for a param the surface
564
+ honours, and honour every one you declare. `t`, `theme`, `chat` and
565
+ any key starting `_` belong to the platform.
566
+
452
567
  ### Linking — `next/link`, never a bare `<a href="/…">`
453
568
 
454
569
  The app is served under a prefix (`/p/<label>` in local dev, nothing in
@@ -560,40 +675,87 @@ is a new function in `lib/actions.js` that also calls `logActivity`.
560
675
 
561
676
  Typed wrappers over `lib/claw.js`, one per `_claw` route an app has a
562
677
  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.
678
+ false, status, error, unavailable, code }`; nothing throws. `unavailable:
679
+ true` (a 404 that names no refusal, or no network) means the gateway has
680
+ no such route yet: show a quiet "not connected yet", not an error.
681
+ `code` names a refusal a caller can act on (`not_your_thread`,
682
+ `needs_answer`, `queue_full`, `unknown_job`, …).
566
683
 
567
684
  ```js
568
685
  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
686
+ platform.me() GET _claw/me → { artifact: { id, name, vehicle, status, home }, user: { id, email }, blueprint, desk_origin }
687
+ platform.ask({ text, row, how, job, list, surface, sessionId, open })
688
+ POST _claw/ask → { ok, request, desk_origin }, and the column opens on it
689
+ platform.requests() GET _claw/requests → { requests, run, desk_origin }
690
+ platform.requests({ id }) GET _claw/requests?id=<id> → { request }
691
+ platform.stopRequest(id) POST _claw/requests/<id>/stop → { ok, request }
692
+ platform.openRequest(requestOrId) open its conversation in the desk's agent column → true when the desk was asked
693
+ platform.surfaceOfPath(pathname) the <key> of /s/<key> in a path, or undefined
572
694
  platform.reportError({ message, stack, url, source }) POST _claw/events
695
+ platform.startJob(message, { sessionId, onEvent }) POST _claw/chat → streams session · delta · tool · done · error (legacy)
696
+ platform.cancelJob(sessionId) POST _claw/cancel (legacy)
573
697
  ```
574
698
 
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
699
+ A request, as `ask` and `requests` hand it back: `{ id, title, words,
700
+ status, worker, line, live_line, job_key, list_key, surface, progress: {
701
+ n, of, noun, at } | null, result: { sentence, count, badge, view: {
702
+ surface, label, path, window_path } | null } | null, queued: { reason,
703
+ after_id, after_title } | null, app_session_id, mine, asker_name,
704
+ created_at, updated_at, ended_at }`. `status` is one of eleven: seven
705
+ live — routing, queued, installing, setting_up, building, working,
706
+ needs_you — then answered (open, but no work is coming), done, stopped,
707
+ didnt_finish. `line` is the one line every screen shows for it
708
+ ("Drafting · 12 of 50", "Needs you · Send 50 emails from you@acme.com?");
709
+ show it as it is, never a status word of your own. `app_session_id` is
710
+ set only on your own requests.
711
+
712
+ Named exports: `me, ask, requests, stopRequest, openRequest,
713
+ surfaceOfPath, startJob, cancelJob, reportError, platform`. That is the
714
+ whole list. Note what is **not** there: the runs, the chat history, Run
715
+ now, Pause / Resume. Those are MONITORING and they are the OS's — the
578
716
  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.
717
+ `_claw/runs`, `_claw/run`, `_claw/pause` or `_claw/resume`. Asking is the
718
+ opposite thing, and it belongs here (`ask()` and `useJob`, below).
719
+ `requests` is not monitoring either: it is this app's asks in flight, so
720
+ an empty surface can show the work coming (§3a). `startJob` and
721
+ `cancelJob` are the 0.3.x door to the same work, kept so `useJob` can
722
+ fall back to it on a claw that has no `_claw/ask`; new code asks. The
723
+ artifact's status lives on `<html data-claw-status>` and the desk's
724
+ origin on `<html data-claw-desk-origin>` (`publishStatus`, `publishDesk`
725
+ in `lib/claw.js`); `app/error-relay.jsx` and `useArtifact()` keep both
726
+ current. An ask tells the desk which request to open with a message to
727
+ that exact origin — never `"*"` — and only while the app is framed.
583
728
 
584
729
  ### Hooks — `lib/hooks.js` ("use client")
585
730
 
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.
731
+ Four hooks: a generic fetch over one `_claw` route, the app's identity,
732
+ a job, and the app's requests. Each owns fetching, polling (while the
733
+ tab is visible; once a minute while `unavailable`), streaming and cleanup
734
+ on unmount. A surface renders what comes back and owns every pixel.
590
735
 
591
736
  ```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 }
737
+ useClaw(path, { every, enabled = true }) → { data, error, loading, unavailable, reload }
738
+ useArtifact() → { artifact, user, blueprint, loading, error, unavailable, reload }
739
+ useJob() → { running, status, queued, queuedAfter, needsYou, liveLine, count, target,
740
+ steps, text, progress, error, sessionId, requestId, request, finishedAt,
741
+ start, cancel, reset, open }
742
+ useRequests({ surface, job, every = 5000 }) → { requests, live, run, loading, error, unavailable, reload, open, stop }
743
+ jobFromRequest(request) → what useJob shows for one request (pure; for tests)
595
744
  ```
596
745
 
746
+ `useRequests` is the work asked of this app's agent — from a screen or
747
+ from the desk, by anyone in the workspace. `requests` is every one the
748
+ gateway returned (the live ones and those that ended in the last day);
749
+ `live` is the ones not ended that belong here — `surface`: asks made on
750
+ this surface; `job`: work on this job wherever it was asked for; either
751
+ is a way in, neither means all; `run` is the app's scheduled run while
752
+ one is going, else null. `open(request)` opens its conversation in the
753
+ desk's agent column; `stop(id)` stops one and looks again. It looks
754
+ every `every` ms while work goes, every 20 s while none does, and only
755
+ while the App tab is on screen (behind the window's other tabs it asks
756
+ nothing; coming back looks at once); a start through `useJob` on the
757
+ same page makes it look at once. Hand it to `LiveEmpty` (§3a).
758
+
597
759
  There is no chat, runs or activity hook: the OS draws those tabs.
598
760
  Activity has no hook either — it is a Postgres table, read server-side
599
761
  with `loadActivity` by any domain surface that wants a slice of it (a
@@ -602,9 +764,21 @@ row's history beside the row, say).
602
764
  ### Commissioning work — `useJob()`
603
765
 
604
766
  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.
767
+ and sees it happen where the result will land. This is SOP §2.3 and the
768
+ reason it exists is blunt: with invocation only on the window's Agent
769
+ tab, a founder starts work and then stares at an app where nothing
770
+ changes.
771
+
772
+ Since 0.4.0 a job is a request. `job.start(text)` asks through `ask()`
773
+ (below): the platform makes the ask a request, hands it to the app's
774
+ agent, and the desk opens its conversation in the agent column. **The
775
+ conversation and the yes live in the desk's agent column** — the agent's
776
+ questions, the founder's answers, the "Send 50 emails?" before anything
777
+ public or paid. The app follows the request's status and draws the
778
+ panel from it. Up to 0.3.x the job streamed into the app over
779
+ `_claw/chat` and the panel carried the agent's running text; on a claw
780
+ that has no `_claw/ask` yet, `useJob` still does exactly that (`text`
781
+ filled, `requestId` null), so a 0.4.0 app runs on an older claw.
608
782
 
609
783
  ```jsx
610
784
  const job = useJob();
@@ -612,24 +786,64 @@ const products = useProducts(); // your own data hook
612
786
 
613
787
  useEffect(() => { products.reload(); }, [job.progress]); // rows as they land
614
788
 
615
- <button onClick={() => job.start("Draft a launch checklist for Rowhouse")}>
789
+ <Button onClick={() => job.start("Draft a launch checklist for Rowhouse")} disabled={job.running}>
616
790
  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}
791
+ </Button>
792
+ <JobPanel job={job} title="Drafting the launch plan" doneTitle="Launch plan drafted" />
627
793
  ```
628
794
 
629
795
  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.
796
+ rules below for you and says what the request says: its status and its
797
+ count ("Drafting · 12 of 50"), "Needs your answer" with **Answer** while
798
+ the agent waits on the founder, "Queued · after …" while the job waits
799
+ its turn. Draw your own only when it cannot say what the founder needs
800
+ to see, and from the same fields: mounted on `job.running ||
801
+ job.finishedAt || job.error`, `job.liveLine` as its line, `job.needsYou`
802
+ → a button calling `job.open()`, `job.queued` →
803
+ `job.queuedAfter?.after_title`, Stop → `job.cancel()`, Close →
804
+ `job.reset()`.
805
+
806
+ **Which job.** `start` sends `job: true` unless you pass a key, and the
807
+ platform picks: the job whose view is this surface, else the app's only
808
+ job, else none. Pass `job.start(text, { job: "<key>" })` when a surface
809
+ starts a job whose view is ANOTHER surface — "Find 20 leads" pressed on
810
+ Pipeline, landing on Leads — or it picks the wrong one, or none. A key
811
+ the app does not declare is refused (`unknown_job`).
812
+
813
+ **One job per list runs at a time.** A start on a job that is already
814
+ running for that list does not run beside it: it answers `queued`
815
+ (`job.queued`; `job.queuedAfter.after_title` names what it waits for)
816
+ and starts on its own when that one ends. A job that repeats on a
817
+ schedule waits the same way behind the app's scheduled run. It counts as
818
+ running meanwhile — the panel stays, and Stop takes it out of the
819
+ queue. With ten asks waiting, the app refuses the next (`queue_full`).
820
+
821
+ **`start` resolves on the ask, not on the work:** `{ ok: true, data: {
822
+ request } }` once the platform took it, or the refusal (`{ ok: false,
823
+ error, code }` — `needs_answer` while the thread waits on an answer in
824
+ the column, `queue_full`, `unknown_job`, `no_credits`, …; the panel
825
+ shows the error). The hook then follows the request — every 2.5 s while
826
+ the App tab is on screen — until it ends. On the 0.3.x path it resolves
827
+ when the job ends, as it always did.
828
+
829
+ What it holds, for the panel:
830
+
831
+ - `running` — the work is live: any of the seven live statuses, queued
832
+ and needs-you included. `status` is the request's own.
833
+ - `needsYou` — the agent waits on the founder; `open()` takes them to
834
+ the question in the column.
835
+ - `queued`, `queuedAfter` (`{ reason, after_id, after_title }`).
836
+ - `liveLine` — the desk's line for it; `count`, `target` — the counted
837
+ progress (12 and 50), or null.
838
+ - `steps` — one: the request, its line as the `label`. `text` — empty:
839
+ the account is in the column.
840
+ - `progress` — bumps whenever the work moves (its status, its count).
841
+ - `error` — the line when it didn't finish, "Stopped" when stopped.
842
+ - `sessionId`, `requestId`, `request` (the whole request), `finishedAt`.
843
+
844
+ Actions: `start(text, { sessionId, row, how, job, list, open })` — the
845
+ options are `ask()`'s; `cancel()` stops the request itself, not only the
846
+ panel; `reset()` closes the panel; `open()` opens the conversation.
633
847
 
634
848
  Four rules, and a build is wrong without them:
635
849
 
@@ -638,18 +852,65 @@ Four rules, and a build is wrong without them:
638
852
  Never a nav item named after the agent, and never the only way to
639
853
  create that object — the manual form has to exist too.
640
854
  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.
855
+ `job.running || job.finishedAt || job.error` (a refused ask has an
856
+ error and no finish); `job.reset()` closes it. A panel that is always
857
+ there is the monitoring console (§2.7).
858
+ 3. **The deliverable is ROWS.** Not `job.text` — the agent's own
859
+ account, and empty on a request, whose account is in the column —
860
+ and not the request's line. That is why `job.progress` reloads your
861
+ data hook, and why the panel closing leaves a fuller list behind it.
647
862
  4. **Show `step.label`, never `step.name`.** `label` is the founder's
648
- phrase ("Searching the web"); `name` is `web_search`.
863
+ phrase ("Drafting · 12 of 50"; "Searching the web" on the 0.3.x
864
+ path); `name` is `request`, or a tool's `web_search`.
865
+
866
+ `job.sessionId` is the thread, and on a request it is set only when the
867
+ thread is yours. Keep it on the row the job produced and pass it back as
868
+ `job.start(msg, { sessionId })` to refine that one thing where it lives
869
+ — one conversation per object, never a global chat. A thread that is a
870
+ teammate's, or one the claw does not know (a 0.3.x session kept on a
871
+ row), is refused; `start` then asks once more without it, and the ask
872
+ gets a thread of its own.
873
+
874
+ ### Asking the agent — `ask()`
875
+
876
+ `useJob` is `ask()` plus a panel to follow it. Call `ask()` from
877
+ `lib/platform.js` yourself when there is nothing for the app to follow:
878
+ a question about a row ("What do we know about Acme?"), a one-off whose
879
+ answer is words in the column, not rows. It takes one object:
880
+
881
+ - `text` — what the member reads. It is the request's words on the desk
882
+ (the Requests list, the column), so write it the way they would say
883
+ it: "Find 20 leads like Acme", never "You are a lead finder…".
884
+ Required; at most 2,000 characters.
885
+ - `how` — for the agent only: the steps, the table to write, the
886
+ format. The member never sees it. At most 4,000 characters.
887
+ - `row` — the row it is about: the agent's context (at most 8 KB as
888
+ JSON). Never an example row — one marked `_sample` is refused.
889
+ - `job`, `list` — name the job: a declared key, or `true` to let the
890
+ platform pick (what `useJob` sends by default), and the list its work
891
+ belongs to, for an app that declares lists (§4, Views by link). With
892
+ no `job` the ask is not a job: nothing queues, nothing is counted.
893
+ - `surface` — defaults to the `<key>` of `/s/<key>` in the page's path.
894
+ - `sessionId` — continue your own thread: while its request is open the
895
+ ask joins it; once it has closed, a new request follows it in the
896
+ same thread. While that request waits on an answer (`needs_answer`)
897
+ or in the queue (`queued`, 409 both) the ask is refused: the answer
898
+ goes in the column, and a queued one starts on its own. A teammate's
899
+ thread is refused (`not_your_thread`, 403) — ask again without it to
900
+ start your own; `useJob` does that for you.
901
+ - `open` — the ask opens its request in the desk's agent column, where
902
+ the conversation lives; `false` leaves the column as it is.
903
+
904
+ ```js
905
+ "use client";
906
+ import { ask } from "../lib/platform";
907
+
908
+ const r = await ask({ text: `What do we know about ${row.name}?`, row, how: "Read the Wiki's customer pages first." });
909
+ if (!r.ok) setError(r.error); // r.code: needs_answer, queue_full, …
910
+ ```
649
911
 
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.
912
+ It resolves once the platform took the ask (`{ ok: true, data: {
913
+ request, desk_origin } }`) or refused it; it never waits for the work.
653
914
 
654
915
  ### Activity — the contract
655
916
 
@@ -706,10 +967,10 @@ second build there dies with `EACCES: .next/trace`.
706
967
  check: 5 files · 2 surfaces
707
968
  ok colours no colour outside the workspace theme (1 depiction excepted)
708
969
  ok controls every control is a kit part
709
- ok examples fixtures stay in code (useFixture), nothing INSERTs one
970
+ ok examples examples show only outside a workspace (useFixture); a workspace app opens empty
710
971
  ok manifest 2 surfaces: drafts, sent
711
972
  ok icons every icon name exists
712
- ok kit 33 fixed files match the template
973
+ ok kit 70 fixed files match the template
713
974
  ok depictions 1 picture of elsewhere, framed on 1 surface: x-post
714
975
  ok smoke built in 41s · / → 307 · /s/drafts 200 · /s/sent 200 (0.6s)
715
976
  all clear
@@ -726,7 +987,9 @@ The smoke test starts that build on a loopback port and loads every
726
987
  surface; `--static` skips the build and the smoke. Then, by eye:
727
988
 
728
989
  - Renders at 360px, 768px and 1280px without horizontal scroll.
729
- - The fixture shows while the table is empty; a saved row replaces it.
990
+ - In a workspace, an empty table shows `LiveEmpty`: the work in flight,
991
+ else what would be here and the manual way to add it — never an
992
+ example (§3a). A saved row replaces it.
730
993
  - Error state shows the text from `{ error }`.
731
994
  - Keyboard focus visible on every control.
732
995
  - Numbers tabular (`data-nums`).
@@ -747,7 +1010,8 @@ surface; `--static` skips the build and the smoke. Then, by eye:
747
1010
  server — `command "npx next dev -H 127.0.0.1"`, `build ""` — so each
748
1011
  edit shows in the founder's live preview without a rebuild.
749
1012
  `product_run` sets `CLAW_BASE_PATH`, `CLAW_PRODUCT`,
750
- `CLAW_PRODUCT_TOKEN` and `CLAW_ARTIFACT_ID`.
1013
+ `CLAW_PRODUCT_TOKEN` and `CLAW_ARTIFACT_ID` — the last is what turns
1014
+ examples off (§3a).
751
1015
 
752
1016
  Every `artifact_files` write under `app/` comes back with the console's
753
1017
  state; a 5xx carries the log tail. Fix it before moving on — the founder
@@ -755,6 +1019,8 @@ is watching the preview.
755
1019
 
756
1020
  Every page is `dynamic = "force-dynamic"`: nothing is prerendered, so a
757
1021
  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" —
1022
+ the server every 5 s while the App tab is on screen and nobody is typing
1023
+ (never while a sheet is open, a field they typed in holds focus, or
1024
+ within 10 s of a keystroke) and the app answers `/claw-alive` (a fixed
1025
+ route) with a 204, so the agent's writes appear. The founder reads "artifact", "app", "agent", "automation" —
760
1026
  nothing else.