@roughapp/feature 0.4.0 → 0.4.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 (3) hide show
  1. package/CHANGELOG.md +40 -5
  2. package/README.md +206 -116
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,43 @@ All notable changes to `@roughapp/feature`.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
  This package is pre-1.0, so a minor version bump can contain breaking changes.
7
7
 
8
+ ## 0.4.1 - 2026-08-26
9
+
10
+ Documentation-only release. The README now has expanded setup, security,
11
+ lifecycle, theming and API guidance, and the changelog includes the release
12
+ notes that were missing from 0.4.0. There are no runtime, type or API changes.
13
+
14
+ ## 0.4.0 - 2026-08-26
15
+
16
+ The Feature Builder is now conversational: Rough can clarify what to build,
17
+ inspect the tools available on the surface and start a build when it has enough
18
+ context. The recommended client and surface APIs are unchanged, so host apps do
19
+ not need an integration migration.
20
+
21
+ ### Added
22
+
23
+ - **Feature Builder chat.** Conversations are saved per feature and remain
24
+ available when the builder is reopened. Follow-up messages can refine the
25
+ feature before Rough starts another build.
26
+ - **Markdown responses.** Assistant messages render headings, lists, links,
27
+ tables, blockquotes and code blocks.
28
+ - **Build cancellation.** Pending and in-progress builds have a Stop control.
29
+ Canceled builds remain in the conversation and build log with the time they
30
+ ran before stopping.
31
+
32
+ ### Changed
33
+
34
+ - **Build and publish updates arrive over an authenticated WebSocket when
35
+ available**, with Replicache polling retained as a fallback. Updates produced
36
+ by workers or another server process now reach an open Feature client without
37
+ waiting for the previous five-second polling interval.
38
+ - Opening the create flow no longer creates an empty feature immediately. Rough
39
+ creates it only after the first message is sent.
40
+ - Advanced consumers of the incidental `SpriteBuild` export must handle the new
41
+ `CANCELED` status. Build records also include
42
+ `cancelRequestedAt: number | null`; update exhaustive status switches and
43
+ test fixtures accordingly.
44
+
8
45
  ## 0.3.0 - 2026-08-10
9
46
 
10
47
  **This release has breaking API changes.** Rough now uses an explicit client for
@@ -89,9 +126,8 @@ export was added or removed. The work is in the type and theme changes below.
89
126
  ping. 0.2.0 calls none of them. It syncs over
90
127
  `/api/v1/internal/replicache/pull` and `/api/v1/internal/replicache/push`,
91
128
  and the session token exchange at `/api/v1/project/{projectId}/session-token`
92
- is the only other request it makes. The ten legacy routes are removed in
93
- [#2181](https://github.com/build-great-products/rough.app/pull/2181), which
94
- is what makes this upgrade time sensitive.
129
+ is the only other request it makes. The ten legacy routes have now been
130
+ removed, which is what makes this upgrade time sensitive.
95
131
 
96
132
  One consequence worth planning for: the SDK now keeps a sync loop open and
97
133
  polls for changes every 5 seconds for as long as the page is mounted, where
@@ -216,5 +252,4 @@ export was added or removed. The work is in the type and theme changes below.
216
252
  - A light and dark mode toggle in the Feature Builder. The stylesheet now
217
253
  carries a `.rough-dark` class holding the dark values for every token.
218
254
  - The Feature Builder's build log now reports token usage and estimated cost
219
- per agent step, formatted in cents below one dollar
220
- ([#2187](https://github.com/build-great-products/rough.app/pull/2187)).
255
+ per agent step, formatted in cents below one dollar.
package/README.md CHANGED
@@ -1,210 +1,300 @@
1
1
  # @roughapp/feature
2
2
 
3
- Embeddable Web Components SDK for integrating Rough Features into a host
4
- product. It ships compiled, minified ESM plus a stylesheet, and registers a set
5
- of custom elements (including `<rough-surface>`).
3
+ `@roughapp/feature` is the browser SDK for adding Rough Features to your
4
+ product. Your app defines where features can appear (a **surface**) and the
5
+ typed tools available there. The SDK renders published features and opens the
6
+ Feature Builder where users create them.
6
7
 
7
- > **Pre-1.0.** The API surface may change between minor versions during the
8
- > `0.x` series. Pin an exact version if you need stability.
8
+ The package ships as compiled ESM and Web Components, with TypeScript
9
+ declarations and a separate stylesheet.
10
+
11
+ > **Pre-1.0:** minor releases may contain breaking changes. Pin
12
+ > `@roughapp/feature` to an exact version if you want to control when you
13
+ > upgrade. See the [changelog](./CHANGELOG.md) before updating.
14
+
15
+ ## Before you start
16
+
17
+ You will need:
18
+
19
+ - a Rough project ID;
20
+ - a Rough signing key, with its private key kept on your server; and
21
+ - a backend endpoint that returns a short-lived Rough identity token for the
22
+ signed-in user.
23
+
24
+ Never put the private key in browser code. `fetchUserToken` should call your
25
+ backend, which authenticates the current user and mints the identity token.
9
26
 
10
27
  ## Install
11
28
 
12
29
  ```bash
13
- pnpm add @roughapp/feature zod
30
+ npm install @roughapp/feature zod
14
31
  ```
15
32
 
16
- This package targets **npm + a modern bundler** (Vite, Webpack, Rspack, …). It
17
- is ESM-only and does not support direct `<script>`/CDN usage in this release.
33
+ The package is ESM-only and expects a modern browser and bundler such as Vite,
34
+ Webpack or Rspack. Direct `<script>` and CDN usage is not supported.
18
35
 
19
- Install `zod` directly because your tool definitions import it. The package
20
- installs its other runtime dependencies automatically.
36
+ `@roughapp/feature` is browser-only: its root module registers custom elements
37
+ when it is evaluated. In an SSR app, import it and create the client from a
38
+ client-only boundary rather than from server-rendered code.
21
39
 
22
- ## Import the stylesheet
40
+ Install `zod` directly because your surface definitions import it. Other
41
+ runtime dependencies are installed with the package.
23
42
 
24
- The package does not inject a global CSS reset. Import the stylesheet once, near
25
- your app entry, to get the Rough theme variables:
43
+ Import the stylesheet once, near your app entry:
26
44
 
27
45
  ```ts
28
46
  import '@roughapp/feature/style.css'
29
47
  ```
30
48
 
31
- Component styles are registered automatically by the custom elements at runtime.
49
+ It provides Rough's theme variables but does not apply a global CSS reset.
50
+ Component styles are registered by the custom elements when the JavaScript
51
+ package loads.
32
52
 
33
- ## Recommended API
53
+ ## Quick start
34
54
 
35
- ### Define a surface
55
+ ### 1. Define a surface
36
56
 
37
- A surface describes a place in your product where Rough features appear, plus
38
- the tools the feature can call. Import `z` from `zod` directly for tool schemas:
57
+ A surface represents one place in your product where Rough Features can appear.
58
+ Give it a stable key and describe the tools a feature may call there.
39
59
 
40
60
  ```ts
41
- import {
42
- defineRoughSurface,
43
- Query,
44
- Mutation,
45
- Subscription,
46
- } from '@roughapp/feature'
61
+ import { defineRoughSurface, Query } from '@roughapp/feature'
47
62
  import { z } from 'zod'
48
63
 
64
+ const Message = z.object({
65
+ id: z.string(),
66
+ subject: z.string(),
67
+ })
68
+
49
69
  export const inboxSurface = defineRoughSurface({
50
70
  key: 'inbox',
51
71
  name: 'Inbox',
52
- description: 'The main message inbox.',
72
+ description: 'The signed-in user’s message inbox.',
53
73
  tools: [
54
74
  new Query({
55
75
  id: 'listMessages',
56
- name: 'List Messages',
57
- description: 'List messages in the inbox.',
58
- inputSchema: z.object({ limit: z.number().default(20) }),
59
- outputSchema: z.array(z.object({ id: z.string(), subject: z.string() })),
76
+ name: 'List messages',
77
+ description: 'Return the newest messages in the inbox.',
78
+ inputSchema: z.object({
79
+ limit: z.number().int().min(1).max(100).default(20),
80
+ }),
81
+ outputSchema: z.array(Message),
60
82
  outputSample: [{ id: 'msg_1', subject: 'Welcome' }],
61
83
  implementation: async ({ limit }) => {
62
- /* return messages */
63
- return []
84
+ const response = await fetch(`/api/messages?limit=${limit}`)
85
+ if (!response.ok) {
86
+ throw new Error('Unable to load messages')
87
+ }
88
+ return response.json()
64
89
  },
65
90
  }),
66
- // Mutation uses the same fields. A Subscription implementation receives
67
- // a callback before the input.
68
91
  ],
69
92
  })
70
93
  ```
71
94
 
72
- `Query`, `Mutation`, and `Subscription` are re-exported from
73
- `@roughapp/bridge` for convenience.
95
+ Tool inputs and outputs are validated against their Zod schemas at runtime.
96
+ `outputSample` should be representative, but must not contain real customer
97
+ data or secrets.
98
+
99
+ Use `Query` for reads and `Mutation` for writes; both implementations receive
100
+ the parsed input and return a promise. A `Subscription` implementation receives
101
+ an output callback first and the parsed input second, then returns a promise for
102
+ an unsubscribe function. All three classes are exported from
103
+ `@roughapp/feature`.
74
104
 
75
- `defineRoughSurface()` validates and freezes the surface and its tools array.
76
- The definition holds no client, project id or connection state, so you can hand
77
- the same value to clients for two different projects at once.
105
+ `defineRoughSurface()` validates and freezes the definition. The definition has
106
+ no project or connection state, so the same value can be used with clients for
107
+ different projects. Within one client, a surface key identifies one contract;
108
+ reuse the same definition anywhere that surface is active.
78
109
 
79
- ### Create a client
110
+ ### 2. Create a client
80
111
 
81
- `createRoughClient()` returns synchronously and immediately starts that
82
- project's authentication, local database and first sync. You own the client
83
- until you destroy it.
112
+ Create one client at the part of your app that owns the Rough project: a route,
113
+ provider, page controller or similar boundary.
84
114
 
85
115
  ```ts
86
116
  import { createRoughClient } from '@roughapp/feature'
87
117
 
88
- const client = createRoughClient({
118
+ export const roughClient = createRoughClient({
89
119
  projectId: 'proj_123',
90
- fetchUserToken: async () => myAppSession.getRoughToken(),
91
- // baseUrl is optional; defaults to the Rough production API.
120
+ fetchUserToken: async () => {
121
+ const response = await fetch('/api/rough/user-token', {
122
+ method: 'POST',
123
+ credentials: 'include',
124
+ })
125
+
126
+ if (!response.ok) {
127
+ throw new Error('Unable to authenticate with Rough')
128
+ }
129
+
130
+ const { token } = (await response.json()) as { token: string }
131
+ return token
132
+ },
133
+ // baseUrl is optional. Omit it when using the Rough production API.
92
134
  })
93
135
  ```
94
136
 
95
- Create the client at whatever owns the project in your app: a route, a provider
96
- component, a page controller. Share that one client with everything below it
97
- rather than creating a second. Within one JavaScript realm, create only one live
98
- client for each `<baseUrl, projectId, personId>` identity. If you create another,
99
- its startup fails with `RoughReplicacheIdentityConflictError`.
100
-
101
- ### Destroy it when you are done
137
+ `createRoughClient()` returns immediately, then starts authentication, the local
138
+ browser database and the first sync in the background. Operations wait for
139
+ startup themselves. If your UI needs an explicit ready state, use:
102
140
 
103
141
  ```ts
104
- await client.destroy()
142
+ import { whenRoughClientReady } from '@roughapp/feature'
143
+
144
+ await whenRoughClientReady({ client: roughClient })
105
145
  ```
106
146
 
107
- `destroy()` is the only lifecycle method, and it works everywhere. It is
108
- idempotent, returns the same promise every time, closes every subscription and
109
- modal the client still owns, and does not resolve until all of that has
110
- finished. A framework unmount hook may use `void client.destroy()`; tests and
111
- controlled route transitions should await it.
147
+ A failed client is not restarted. Destroy it and create a new one after the
148
+ underlying problem has been handled.
112
149
 
113
- The client implements no disposal protocol. Teardown here is genuinely
114
- asynchronous, and there is one way to ask for it.
150
+ ### 3. Render the surface
115
151
 
116
- Startup runs in the background. If you want to observe it:
152
+ Importing `@roughapp/feature` registers its custom elements. Set the `client`
153
+ and `surface` properties on `<rough-surface>`; these are JavaScript values, not
154
+ HTML attributes.
117
155
 
118
156
  ```ts
119
- import { whenRoughClientReady } from '@roughapp/feature'
157
+ import '@roughapp/feature'
158
+
159
+ const element = document.createElement('rough-surface')
160
+ element.client = roughClient
161
+ element.surface = inboxSurface
120
162
 
121
- await whenRoughClientReady({ client })
163
+ document.querySelector('#rough-slot')?.append(element)
122
164
  ```
123
165
 
124
- A client that fails startup keeps that error and replays it from every later
125
- operation. It is not restarted; destroy it and create a new one.
166
+ The package also exports `RoughSurface` for Svelte apps. As with the custom
167
+ element, import and render it only on the client when the app uses SSR.
126
168
 
127
- ### Render a surface
169
+ ## Open the Feature Builder
128
170
 
129
- Use the `<rough-surface>` custom element, or the `RoughSurface` component
130
- export. Set both the client and the surface:
171
+ `openRoughCreate()` opens the create flow without requiring a mounted surface.
172
+ It returns a handle you can close during route teardown.
131
173
 
132
174
  ```ts
133
- import '@roughapp/feature'
175
+ import { openRoughCreate } from '@roughapp/feature'
134
176
 
135
- const el = document.createElement('rough-surface')
136
- el.client = client
137
- el.surface = inboxSurface
138
- document.querySelector('#rough-slot')?.append(el)
177
+ const modal = await openRoughCreate({
178
+ client: roughClient,
179
+ surface: inboxSurface,
180
+ // Defaults to document.body. Use a target when your Rough theme is scoped
181
+ // to a subtree.
182
+ target: document.querySelector('#rough-root') ?? undefined,
183
+ })
184
+
185
+ // If the host needs to close it programmatically:
186
+ await modal.close()
139
187
  ```
140
188
 
141
- ### List features and open the create flow
189
+ The user can also close the modal from the UI. Calling `close()` again is safe.
190
+
191
+ ## Subscribe to published features
142
192
 
143
- Operations are direct exports and take the client in their options. Neither
144
- requires anything to have mounted first.
193
+ Use `getRoughFeatures()` when you need the feature list rather than the rendered
194
+ `<rough-surface>`. The callback runs with the initial list, even when it is
195
+ empty, and runs again as published features change.
145
196
 
146
197
  ```ts
147
- import { getRoughFeatures, openRoughCreate } from '@roughapp/feature'
198
+ import { getRoughFeatures } from '@roughapp/feature'
148
199
 
149
- // Subscribe to the published features for a surface.
150
200
  const subscription = getRoughFeatures({
151
- client,
201
+ client: roughClient,
152
202
  surface: inboxSurface,
153
203
  onFeatures: (features) => {
154
204
  console.log(features)
155
205
  },
156
206
  onError: (error) => {
157
- console.error(error)
207
+ console.error('Unable to load Rough Features', error)
158
208
  },
159
209
  })
160
210
 
161
- // Optional: wait for the first feature batch.
211
+ // Optional: wait until the first feature list has been delivered.
162
212
  await subscription.ready
163
213
 
164
- // Open the "create a feature" modal.
165
- const modal = await openRoughCreate({
166
- client,
167
- surface: inboxSurface,
168
- // Where the modal attaches. Defaults to document.body. Pass the element that
169
- // scopes your Rough theme if you scope it to a subtree.
170
- target: document.querySelector('#rough-root') ?? undefined,
171
- })
172
-
173
- await modal.close()
214
+ // Later:
174
215
  await subscription.unsubscribe()
175
216
  ```
176
217
 
177
- Every cleanup handle returns a promise, is safe to call twice, and returns the
178
- same promise on the second call.
218
+ Both `getRoughFeatures()` and `openRoughCreate()` also accept an `AbortSignal`.
219
+ Their cleanup methods are asynchronous, idempotent and return the same promise
220
+ when called more than once.
179
221
 
180
- ## Exports
222
+ ## Client lifetime
181
223
 
182
- Recommended, stable-ish customer API:
224
+ Share one client with everything that uses the same Rough project and signed-in
225
+ person. In one JavaScript realm, only one live client may own a given
226
+ `<baseUrl, projectId, personId>` identity. Starting a duplicate client fails
227
+ with `RoughReplicacheIdentityConflictError`.
183
228
 
184
- - `createRoughClient`, `whenRoughClientReady` (+ `RoughClient`,
185
- `RoughClientOptions` types)
186
- - `defineRoughSurface` (+ `RoughSurfaceDefinition` type)
187
- - `getRoughFeatures` (+ `GetRoughFeaturesOptions`,
188
- `RoughFeatureSubscription` types)
189
- - `openRoughCreate` (+ `OpenRoughCreateOptions`, `RoughModalHandle` types)
190
- - `RoughSurface` / `<rough-surface>`
191
- - `Query`, `Mutation`, `Subscription` (re-exported from `@roughapp/bridge`)
192
- - Errors worth branching on: `RoughClientDestroyedError`,
193
- `RoughReplicacheIdentityConflictError`, `RoughSurfaceContractConflictError`,
194
- `RoughClientDestroyError`, `RoughInvalidClientError`
229
+ Destroy the client when its owning route or provider goes away, or before
230
+ replacing it after the user or project changes:
195
231
 
196
- Advanced / incidental exports are **not** intended as stable customer
197
- dependencies during the pre-1.0 series:
232
+ ```ts
233
+ await roughClient.destroy()
234
+ ```
235
+
236
+ `destroy()` closes any subscriptions and modals still owned by the client and
237
+ waits for cleanup to finish. It is safe to call more than once. Framework
238
+ unmount hooks that cannot await cleanup may use `void roughClient.destroy()`;
239
+ tests and controlled transitions should await it.
240
+
241
+ ## Theming
242
+
243
+ The stylesheet defines a light theme on `:root`. Add `rough-dark` to an
244
+ ancestor to use the bundled dark values:
245
+
246
+ ```html
247
+ <div id="rough-root" class="rough-dark"></div>
248
+ ```
249
+
250
+ Override variables on `:root` or on the subtree containing your Rough elements:
251
+
252
+ ```css
253
+ #rough-root {
254
+ --rough-primary: oklch(0.52 0.2 264);
255
+ --rough-primary-foreground: white;
256
+ --rough-brand: #635bff;
257
+ --rough-radius: 0.75rem;
258
+ }
259
+ ```
198
260
 
199
- - Custom-element components: `RoughCreateModal` (`<rough-create-modal>`),
200
- `RoughEditButton` (`<rough-edit-button>`), `RoughFeature` (`<rough-feature>`)
201
- (the `<rough-edit-modal>` element registers transitively).
202
- - UI building blocks: `PrimaryButton`, `SecondaryButton`, `ResizeHandle`,
203
- `SpriteBuildMenu`, `SpriteFrame`.
204
- - Types: `Sprite`, `SpriteBuild`, `JsonValue`, `RoughCleanup`,
205
- `FetchUserTokenFn`, `SpriteFrameDatastore`, `SpriteFrameDatastoreContext`.
261
+ Common variables include `--rough-background`, `--rough-foreground`,
262
+ `--rough-card`, `--rough-primary`, `--rough-secondary`, `--rough-muted`,
263
+ `--rough-border`, `--rough-ring`, `--rough-brand` and `--rough-radius`.
264
+
265
+ Create and edit modals attach to `document.body` by default. If your overrides
266
+ live below `body`, pass that themed element as the modal's `target` so the modal
267
+ inherits them.
268
+
269
+ ## Public API
270
+
271
+ The recommended integration surface is:
272
+
273
+ - `createRoughClient`, `whenRoughClientReady` and the `RoughClient`,
274
+ `RoughClientOptions` types;
275
+ - `defineRoughSurface` and the `RoughSurfaceDefinition` type;
276
+ - `getRoughFeatures` and the `GetRoughFeaturesOptions`,
277
+ `RoughFeatureSubscription` types;
278
+ - `openRoughCreate` and the `OpenRoughCreateOptions`, `RoughModalHandle` types;
279
+ - `RoughSurface` / `<rough-surface>`;
280
+ - `Query`, `Mutation`, `Subscription`; and
281
+ - `RoughClientDestroyedError`, `RoughReplicacheIdentityConflictError`,
282
+ `RoughSurfaceContractConflictError`, `RoughClientDestroyError` and
283
+ `RoughInvalidClientError`.
284
+
285
+ The package also exports lower-level components and data types used to build its
286
+ own UI. During the pre-1.0 series, do not treat these as a stable integration
287
+ surface:
288
+
289
+ - `RoughCreateModal` (`<rough-create-modal>`), `RoughEditButton`
290
+ (`<rough-edit-button>`) and `RoughFeature` (`<rough-feature>`). The
291
+ `<rough-edit-modal>` element is registered transitively.
292
+ - `PrimaryButton`, `SecondaryButton`, `ResizeHandle`, `SpriteBuildMenu` and
293
+ `SpriteFrame`.
294
+ - `Sprite`, `SpriteBuild`, `JsonValue`, `RoughCleanup`, `FetchUserTokenFn`,
295
+ `SpriteFrameDatastore` and `SpriteFrameDatastoreContext`.
206
296
 
207
297
  ## License
208
298
 
209
- `UNLICENSED`. This package is publicly installable but is not open source; use
299
+ `UNLICENSED`. This package is publicly installable but is not open source. Use
210
300
  is governed by your Rough customer terms.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roughapp/feature",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {