@byokit/ui 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,58 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ - Run views accept structured-output failures and display the kit’s plain validation message.
6
+
7
+ ## 0.6.0 (2026-10-01)
8
+
9
+
10
+
11
+ - API key (billed per use): framework-free `keyStep` and `keyView` entry-card states and plain billing/error words, without keeping a credential in view state.
12
+
13
+ ## 0.5.0 (2026-10-01)
14
+
15
+
16
+
17
+ - `routeChoices()`: every onboarding route (`tailscale`, `tailscale-direct`, `private`, `lan`, plus `cloudflare`
18
+ and `external`) in everyday words (`{ code, title, sentence, needs }`), in recommendation order. The one word
19
+ table reach's `recommend()` draws `sentence` and `needs` from.
20
+
21
+ ## 0.4.0 (2026-09-30)
22
+
23
+ - `@byokit/ui-core/steps` (no React): `stepsView(steps)` turns the caller's own steps into the rows a stepper or
24
+ inline checks draw (`{ id, state: todo | checking | ok | failed, words, fix }`, same in a terminal and on a
25
+ phone); `stepsText(rows, { title })` draws the same rows as text lines for a terminal. Step ids stay
26
+ caller-defined, and a failed step without its own `fix` says "Try again."
27
+
28
+ ## 0.3.0 (2026-09-30)
29
+
30
+ - FIX: pairing links parse on React Native, where `URL.canParse` is missing and `hostname` is empty for `ws://`:
31
+ `describeRoute` reads the scheme and host itself.
32
+ - `@byokit/ui-core/kits` (no React): view state for the runtime kits, typed structurally against
33
+ `@byokit/openclaw/device` and `@byokit/herdr/device` so neither kit is a dependency. Reducers `runStep`,
34
+ `approvalsStep` and `herdrStep`; views `runView` (the reply, the tool at work, and how a run ended in the kit's own
35
+ sentence), `approvalWords`, `herdrTreeView`, `agentIn` and `blockedView`; live stores `runStore`, `approvalsStore`
36
+ (listed once the event stream is open, dropped as each approval expires) and `herdrStore` (one `hd.events` stream
37
+ per device however many views watch it; a waiting agent's revision follows the tree, so an answer is never stale).
38
+ Stores reopen their stream soon after the link comes back (a stopped or refused link included, after its `retry()`)
39
+ and stop for good once the pairing is removed.
40
+ - React hooks `useRun`, `useApprovals`, `useHerdrTree` and `useBlocked` over those stores, for React and React Native.
41
+ - `qrMatrix(text, { border })` takes the quiet border in modules (2 unless given, as before), and `qrText(text,
42
+ { border })` draws the same QR as half-block text rows for a terminal, so a setup script shows a code a phone can scan.
43
+ - `consentWords` and `pairingView` take an optional `device` (`phone`, or `browser`, which opens the pairing link
44
+ instead of scanning) and one app sentence (`detail`) after the title.
45
+ - `useSignIn` works without `start`: when something else starts the sign-in, the sheet only watches, and nothing
46
+ begins as it opens.
47
+
48
+ ## 0.2.0
49
+
50
+ - `@byokit/ui-core/link`: `qrMatrix`, `consentWords`, `pairingView` and `linkWords` for pairing with `@byokit/link` in any UI.
51
+
52
+ ## 0.1.1
53
+
54
+ - The Apache-2.0 LICENSE ships in the tarball.
55
+
56
+ ## 0.1.0
57
+
58
+ - Headless sign-in state and plain-words copy for any UI.
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,310 @@
1
- # Temporary Holding Version
1
+ <h1 align="center">@byokit/ui</h1>
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <p align="center">
4
+ <a href="https://www.npmjs.com/package/@byokit/ui"><img alt="npm" src="https://img.shields.io/npm/v/@byokit/ui?style=flat&label=npm" /></a>
5
+ <a href="https://github.com/umeranjum17/byokit/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/umeranjum17/byokit/ci.yml?style=flat&branch=main" /></a>
6
+ <a href="LICENSE"><img alt="Apache 2.0" src="https://img.shields.io/badge/license-Apache--2.0-666?style=flat" /></a>
7
+ </p>
8
+
9
+ <p align="center"><strong>Headless sign-in, pairing and runtime-kit state, in plain words, for any UI.</strong><br/>
10
+ It turns what your back end reports into the phase to draw and the sentence to show: a "Sign in with …" sheet, a
11
+ pairing sheet, the link's status, the route a phone takes home, and a runtime kit's runs, approvals and agents. Your
12
+ app keeps its own look. Everything except the React hooks is framework-free.</p>
13
+
14
+ <p align="center">
15
+ <img src="https://raw.githubusercontent.com/umeranjum17/byokit/main/docs/images/pwa-2-code.png" width="240" alt="A phone-width page titled 'byokit in a browser' reading 'Signing in to ChatGPT…' and 'On the ChatGPT page, type this code:', the code WDJB-MJHT in large letters, an 'Open ChatGPT' link and a Cancel button." />
16
+ <img src="https://raw.githubusercontent.com/umeranjum17/byokit/main/examples/herdr-kit/docs/2-compare.png" width="240" alt="A phone-width page titled 'Agents' with a 'Pair this phone' card reading 'Check your computer shows these two words, then say yes there.' above the words 'coast comet'." />
17
+ </p>
18
+
19
+ <p align="center"><sub>Left: the <code>code</code> phase from <code>phaseOf()</code>, drawn by <a href="../../examples/pwa">examples/pwa</a> against the mock OpenAI sign-in. Right: <code>pairingView()</code>'s <code>compare</code> words, drawn by <a href="../../examples/herdr-kit">examples/herdr-kit</a>.</sub></p>
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ npm install @byokit/ui
25
+ ```
26
+
27
+ [![npm](https://img.shields.io/npm/v/@byokit/ui?style=flat&label=)](https://www.npmjs.com/package/@byokit/ui) · [Latest release](https://github.com/umeranjum17/byokit/releases?q=ui-v) · [All releases](https://github.com/umeranjum17/byokit/releases)
28
+
29
+ Migrating from `@byokit/ui-core`? See the [deprecated shim's migration guide](../ui-core/README.md#migrate).
30
+
31
+ ## Quickstart
32
+
33
+ ```sh
34
+ npm install @byokit/ui
35
+ ```
36
+
37
+ React is an optional peer dependency, needed only for the hooks (`useSignIn`, `useRun`, `useApprovals`,
38
+ `useHerdrTree`, `useBlocked`).
39
+
40
+ ```ts
41
+ import { phaseOf, stepOf } from '@byokit/ui/phase';
42
+ import { describeRoute } from '@byokit/ui/route';
43
+ import { consentWords, linkWords, pairingView, qrMatrix } from '@byokit/ui/link';
44
+
45
+ // What the back end reports: a sign-in waiting on a typed code.
46
+ const phase = phaseOf({ ready: false, signIn: { state: 'waiting', via: 'code', code: 'ABCD-1234' } });
47
+ console.log(phase, stepOf(phase));
48
+ console.log(phaseOf({ ready: true, work: true }));
49
+ console.log(phaseOf({ signIn: { state: 'failed', why: 'busy' } }));
50
+
51
+ console.log(describeRoute('wss://home.tail1234.ts.net/link'));
52
+ console.log(describeRoute('ws://100.101.102.103:7777'));
53
+
54
+ console.log(consentWords({ hostName: 'Kitchen computer', role: 'control' }));
55
+ console.log(pairingView({ phase: 'compare', hostName: 'Kitchen computer', words: 'coast comet' }));
56
+ console.log(linkWords('offline', 'Kitchen computer'));
57
+ console.log(qrMatrix('hello').length, 'rows');
58
+ ```
59
+
60
+ Output from running it with Node:
61
+
62
+ ```text
63
+ code 1
64
+ work
65
+ busy
66
+ Tailscale
67
+ Private network
68
+ Pair with Kitchen computer? This device will be able to see and change things on it, until you remove it there.
69
+ {
70
+ phase: 'compare',
71
+ title: 'Check Kitchen computer shows these two words, then say yes there.',
72
+ words: 'coast comet'
73
+ }
74
+ Can't reach Kitchen computer right now. This device keeps trying by itself.
75
+ 25 rows
76
+ ```
77
+
78
+ In React, `useSignIn` drives the whole sheet from three calls to your back end:
79
+
80
+ ```ts
81
+ import { useSignIn } from '@byokit/ui';
82
+ import type { AccountView } from '@byokit/ui';
83
+
84
+ // Your back end's routes; the hook only needs these three calls.
85
+ declare const api: {
86
+ account(): Promise<AccountView | null>;
87
+ signIn(body?: { via?: 'code'; fresh?: boolean }): Promise<unknown>;
88
+ cancel(): Promise<unknown>;
89
+ };
90
+
91
+ export function useChatGptSheet() {
92
+ const s = useSignIn({ read: () => api.account(), start: (b) => api.signIn(b), cancel: () => api.cancel() });
93
+ // s.phase: what to draw. s.code / s.url: what to show. s.start({ via: 'code' }): "Having trouble? Use a code instead".
94
+ // s.cancel(), s.close(), s.keepWork(): the sheet's buttons. Leave `start` out when something else starts the
95
+ // sign-in: the sheet then only watches, and nothing begins as it opens.
96
+ return s;
97
+ }
98
+ ```
99
+
100
+ ## API at a glance
101
+
102
+ | Export | What it does |
103
+ |---|---|
104
+ | `phaseOf(account, { offline, cancelled, keepWork })` | The sign-in phase to draw, from an `@byokit/accounts` `view()` plus whether the account is signed in |
105
+ | `stepOf(phase)` | Where the three-step progress bar ("Open", "Say yes", "Done") stands |
106
+ | `useSignIn({ read, start?, cancel, offline?, ms?, pinned? })` | React hook: starts the sign-in as the sheet opens, polls, and handles cancel, close, keep-work and "use a code instead"; without `start` it only watches and nothing begins |
107
+ | `describeRoute(url, kind?)` | Names the route a dial address takes, for a pairing or settings screen |
108
+ | `routeShort(route)` | The route as words inside a connected line (`same Wi-Fi`) |
109
+ | `connectedWords(hostName, url?, kind?)` | The connected line to render (`Connected to Kitchen computer - same Wi-Fi.`), plain with no route |
110
+ | `routeChoices()` | Every onboarding route in everyday words (`{ code, title, sentence, needs }`), in recommendation order |
111
+ | `qrMatrix(text, { border }?)` | The pairing QR as rows of dark and light modules, with its quiet border (2 unless given) |
112
+ | `qrText(text, { border }?)` | The same QR as half-block text rows for a terminal |
113
+ | `consentWords({ hostName, role, device?, detail? })` | The question before pairing |
114
+ | `pairingView({ phase, hostName?, words?, error?, device?, detail? })` | The pairing sheet's title and words for each phase |
115
+ | `linkWords(status, hostName)` | The link's status in one sentence |
116
+ | `pairErrorWords(error)` | A failed pairing as `expired`, `wrong` or the link's own sentence |
117
+ | `stepsView(steps)` | The stepper rows to draw, from the caller's own step ids and states |
118
+ | `stepsText(rows, { title })` | The same rows as text lines for a terminal |
119
+ | `runStore(oc)`, `runView(state, { words, name })` | One run: the reply streaming in, the tool at work, how it ended in the kit's words |
120
+ | `approvalsStore(oc)`, `approvalWords(a, words, helper)` | The approvals waiting for a yes, live, each gone when answered or expired |
121
+ | `herdrStore(hd)`, `herdrTreeView(tree)`, `blockedView(state)` | Herdr's agents by where they run, and the ones waiting for an answer, live |
122
+ | `signInsStore(oc)`, `signInSheetView(request, { name, holder })`, `livePanelView(state)` | The browser sign-in sheet (sentence, buttons, chat chip) and live panel for the OpenClaw kit's handoff |
123
+ | `useRun`, `useApprovals`, `useHerdrTree`, `useBlocked` | React hooks over those stores |
124
+ | `connectView(routes, step?)`, `connectStep(step, action)` | Every account route the kits list, grouped by how the person pays, with the picked row and its key card |
125
+ | `signInFor(view, { read, start?, cancel })`, `useConnect({ routes, read, start?, cancel })` | `useSignIn`'s options for the picked sign-in row; the hook holds the step |
126
+ | Types | `Phase`, `SignInView`, `AccountView`, `UseSignIn`, `Route`, `RouteChoice`, `RouteCode`, `PairPhase`, `Role`, `DeviceKind`, `LinkStatus`; `RouteKind` from `@byokit/ui/route` |
127
+
128
+ Entry points: `@byokit/ui` (everything, including the React hooks), `@byokit/ui/phase`,
129
+ `@byokit/ui/route`, `@byokit/ui/link`, `@byokit/ui/kits`, `@byokit/ui/steps` and
130
+ `@byokit/ui/connect` (no React dependency).
131
+
132
+ ## Sign-in phases
133
+
134
+ `phaseOf()` turns what your back end reports (an `@byokit/accounts` `view()` plus whether the account is signed in)
135
+ into the phase to draw:
136
+
137
+ | Phase | Meaning |
138
+ |---|---|
139
+ | `opening` | getting the provider's page or a code ready |
140
+ | `waiting` | say yes on the provider's page |
141
+ | `code` | type this code on the provider's page instead |
142
+ | `done` | signed in, and it works |
143
+ | `work` | signed in with a work plan: offer a personal one |
144
+ | `cancelled` | declined on the page, or cancelled here |
145
+ | `busy` | something else on this computer is signing in |
146
+ | `expired` | the page or code ran out of time |
147
+ | `failed` | anything else, with a plain sentence |
148
+ | `offline` | the home computer isn't answering |
149
+
150
+ `phaseOf()` and `stepOf()` also come from `@byokit/ui/phase`, which has no React dependency.
151
+
152
+ ## Routes
153
+
154
+ `describeRoute(url, kind?)` (also `@byokit/ui/route`, no React) names the route a dial address takes, for a
155
+ pairing or settings screen. Pass `tailscale`, `direct`, `private`, or `lan` when known (map reach's `tailscale-direct`
156
+ to `direct`); without provenance, 100.64/10 reads as a private network rather than assumed to be Tailscale, and the
157
+ other home ranges read as same Wi-Fi.
158
+
159
+ `routeChoices()` (same entry, no React) is the one word table for the onboarding route list: `{ code, title,
160
+ sentence, needs }` per route in recommendation order, a superset of reach's `Via` (it also covers the temporary
161
+ public link and your own server, which reach does not probe). Reach's `recommend()` takes `sentence` and
162
+ `needs` from here and only adds availability.
163
+
164
+ `connectedWords(hostName, url?, kind?)` (same entry) is the connected line a consumer renders: the computer with
165
+ the route just dialled (`Connected to Kitchen computer - same Wi-Fi.`), or plain (`Connected to Kitchen computer.`)
166
+ when there is no route to name.
167
+
168
+ ```ts
169
+ import { routeChoices } from '@byokit/ui/route';
170
+
171
+ console.log(routeChoices().map((c) => `${c.title}: ${c.sentence}`));
172
+ ```
173
+
174
+ ## Connect an account
175
+
176
+ `connectView(routes, step?)` (also `@byokit/ui/connect`, no React) draws the "Connect an AI account" list from
177
+ the routes the kits discover (D18 in `docs/runtime-kits.md`): pass `@byokit/accounts` `routes(host)` and
178
+ `@byokit/openclaw` `routes(facts)` together, unfiltered. Every route is listed, unavailable ones too, in these groups
179
+ (empty ones left out):
180
+
181
+ | Group | Rows | Offered by default |
182
+ |---|---|---|
183
+ | Plans | subscription and plan-key routes the kit offers by default | yes, when ready |
184
+ | Pay per use | API-billed routes | no, only when picked |
185
+ | On this computer | local runtimes | no |
186
+ | Your own server | endpoint routes; the person says how it is paid for | no |
187
+ | Cloud account | cloud credentials | no |
188
+ | Other ways to connect | free or unknown billing ("Billing set by …") | no |
189
+ | Speech, search and media | non-chat services | no |
190
+
191
+ Billing decides the group, never the route's `offer` alone: an API, cloud, local, server, free or unknown row can
192
+ never sit among Plans. Each row has `billingWords` ("Uses your ChatGPT plan", "Charged per use to your OpenAI
193
+ account"), `method` ("Sign in with a code") and `status` (a sentence per readiness), plus `offered`, `ready` and
194
+ `does`: `signin`, `key`, `setup` (the app's own cloud, local or server form) or `unavailable`. The view copies only
195
+ these fields, so a credential on a route never reaches it; nothing in it names a provider.
196
+
197
+ ```ts
198
+ import { connectStep, connectView, signInFor } from '@byokit/ui/connect';
199
+ import { routes } from '@byokit/accounts';
200
+
201
+ declare const api: { account(id: string): Promise<null>; signIn(id: string, body?: object): Promise<void>; cancel(id: string): Promise<void> };
202
+ const step = connectStep({ at: 'list' }, { type: 'pick', key: 'accounts/openai:code' });
203
+ const view = connectView(routes({ platform: 'browser' }), step);
204
+ // view.groups → the list; view.chosen.row.does === 'signin' → draw the sign-in sheet:
205
+ const options = signInFor(view, { read: (row) => api.account(row.id), start: (row, body) => api.signIn(row.id, body), cancel: (row) => api.cancel(row.id) });
206
+ ```
207
+
208
+ A key route's `chosen.key` is the existing `keyView` card, labelled for its billing ("Key from your plan" or "Key
209
+ (charged per use)"); submit the input straight to the kit and dispatch only `{ type: 'result', result }`.
210
+ `useConnect({ routes, read, start?, cancel })` holds the step in React and returns the view with `pick`, `back`,
211
+ `act` and `signIn`, the options to pass to `useSignIn` in a sheet keyed by `chosen.row.key`.
212
+
213
+ ## Pairing words
214
+
215
+ Pairing with `@byokit/link`, from `@byokit/ui/link` (no React either):
216
+
217
+ - `qrMatrix(offer.text, { border }?)`: the pairing QR as rows of dark and light modules, with its quiet border
218
+ (2 unless given), to draw in any UI.
219
+ - `qrText(offer.text, { border }?)`: the same QR as half-block text rows, so `console.log` in a setup script shows a
220
+ code a phone can scan.
221
+ - `consentWords({ hostName, role, device?, detail? })`: the question before pairing ("Pair with Kitchen computer? This device
222
+ will be able to see and change things on it, until you remove it there."). `device` names the pairing side when it
223
+ isn't the app (`phone`, or `browser` for the web pairing link); `detail` adds one app sentence after.
224
+ - `pairingView({ phase, hostName, words, error, device?, detail? })`: scan, compare the two words, waiting for a yes, paired, failed.
225
+ - `linkWords(status, hostName)`: the link's status in one sentence.
226
+
227
+ ## Stepper rows
228
+
229
+ `stepsView()` (also `@byokit/ui/steps`, no React) turns the caller's own steps into the rows a stepper or
230
+ inline checks draw, so a terminal and a phone show the same words. Each step keeps the id its caller chose, the
231
+ state it is in (`todo`, `checking`, `ok`, `failed`), the plain words to show, and the one next action when it
232
+ failed. `stepsText()` draws the same rows as text lines for a terminal; a phone draws the rows in its own list.
233
+ A failed step without its own `fix` says "Try again."
234
+
235
+ ```ts
236
+ import { stepsText, stepsView } from '@byokit/ui/steps';
237
+
238
+ // Onboarding "Checking this computer": ids are the app's own, states come from its probes.
239
+ const rows = stepsView([
240
+ { id: 'runner', state: 'ok', label: 'Agent runner', detail: 'ready' },
241
+ { id: 'agents', state: 'ok', label: 'Agents found', detail: 'Claude Code (signed in) · Codex (needs sign-in)' },
242
+ { id: 'wifi', state: 'ok', label: 'Wi-Fi', detail: 'this computer is at 192.168.1.144' },
243
+ { id: 'tailscale', state: 'todo', label: 'Tailscale', detail: 'not installed on this computer' },
244
+ ]);
245
+ console.log(stepsText(rows, { title: 'Checking this computer' }));
246
+
247
+ // "Setting up" reuses the same rows with its own ids; a failed check names its fix:
248
+ console.log(stepsView([
249
+ { id: 'serve', state: 'failed', label: 'muxr', detail: 'not answering', fix: "muxr didn't start. Press r to retry." },
250
+ ])[0]);
251
+ ```
252
+
253
+ Output from running it with Node:
254
+
255
+ ```text
256
+ Checking this computer
257
+ ✓ Agent runner: ready
258
+ ✓ Agents found: Claude Code (signed in) · Codex (needs sign-in)
259
+ ✓ Wi-Fi: this computer is at 192.168.1.144
260
+ ○ Tailscale: not installed on this computer
261
+ {
262
+ id: 'serve',
263
+ state: 'failed',
264
+ words: 'muxr: not answering',
265
+ fix: "muxr didn't start. Press r to retry."
266
+ }
267
+ ```
268
+
269
+ ## Runtime kits' state
270
+
271
+ Runtime kits' state, from `@byokit/ui/kits` (no React), typed to fit `@byokit/openclaw/device` and
272
+ `@byokit/herdr/device` as they are:
273
+
274
+ - Runs: `runStore(oc)` with `send(message)` and `stop()`; `runView(state, { words, name })` gives the reply so far, the
275
+ tool at work and, for a failed run, the kit's sentence (`words` from `@byokit/openclaw/device`, `name` "ChatGPT").
276
+ OpenClaw's `started` and `thinking` frames are accepted without changing the reply, tools or rendered state;
277
+ the running phase begins at `send`, not Gateway acceptance.
278
+ - Approvals: `approvalsStore(oc)` keeps the waiting list live (each leaves when answered anywhere or when it expires);
279
+ `approvalWords(approval, words, 'Your helper')` asks the question.
280
+ - Herdr: `herdrStore(hd)` keeps the tree and the agents waiting for an answer live from one stream;
281
+ `herdrTreeView(tree)` groups the agents by where they run, with the status the kit's `agentWords` takes;
282
+ `blockedView(state)` names each waiting agent.
283
+ - Browser sign-in (`@byokit/openclaw` spec 5.17): `signInsStore(oc)` keeps the requests live from sign-in pings;
284
+ `signInSheetView(request, { name, holder })` gives the sentence (a kit word key and its values), the buttons and the
285
+ chat chip, which reads "entered" until a verified sign-in; `livePanelView(state)` says when to draw frames, take
286
+ input, show "Reconnecting…", or ask to confirm an address the person didn't expect. Watching asks the helper nothing.
287
+ - Each store is `{ get, subscribe }`: draw from `get()` in any UI; the first `subscribe` starts following, the last
288
+ unsubscribe stops. `runStep`, `approvalsStep` and `herdrStep` are the reducers underneath.
289
+
290
+ In React or React Native: `useRun(oc, { words, name })`, `useApprovals(oc)`, `useHerdrTree(hd)` and `useBlocked(hd)`,
291
+ with the device client made once (`useMemo(() => herdrDevice(link), [link])`).
292
+
293
+ ## Links
294
+
295
+ - [byokit](../../README.md): the monorepo and its other packages, including
296
+ [`@byokit/accounts`](../accounts) and [`@byokit/link`](../link).
297
+ - Examples: [`examples/pwa`](../../examples/pwa) (sign-in phases in a browser),
298
+ [`examples/herdr-kit`](../../examples/herdr-kit) (pairing words and `herdrStore` from a phone browser),
299
+ [`examples/expo`](../../examples/expo) (React Native).
300
+ - [CHANGELOG.md](CHANGELOG.md)
301
+
302
+ ## License
303
+
304
+ Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](../../NOTICE).
305
+
306
+ `@byokit/ui/kits` also exports `keyStep(state, action)` and `keyView(state, words)` for an explicit
307
+ **API key (billed per use)** entry card. States are `entry`, `checking`, `ok`, `invalid`, `not_included`;
308
+ actions are `submit`, `edit`, and `result` with the kit's add-key outcome. Supply the OpenClaw kit's `words`
309
+ function for the label and sentences. These views never contain the entered key; submit it directly to the host
310
+ and clear the input afterwards.
@@ -0,0 +1,47 @@
1
+ import { type Store } from './follow.ts';
2
+ /** One approval as the OpenClaw kit reports it. */
3
+ export type Approval = {
4
+ id: string;
5
+ source: 'gate' | 'exec' | 'plugin' | 'question';
6
+ member: string;
7
+ sessionKey?: string;
8
+ tool?: string;
9
+ summary: string;
10
+ input?: unknown;
11
+ at: number;
12
+ /** When the computer stops waiting and says no by itself. */
13
+ expires: number;
14
+ };
15
+ /** An `oc.events` frame: an approval added or resolved, or any other event (ignored here). */
16
+ export type ApprovalFrame = {
17
+ event: string;
18
+ change?: 'added' | 'resolved';
19
+ approval?: Approval;
20
+ };
21
+ export type ApprovalsAction = ApprovalFrame | {
22
+ type: 'set';
23
+ list: Approval[];
24
+ } | {
25
+ type: 'tick';
26
+ now: number;
27
+ };
28
+ export declare function approvalsStep(list: Approval[], a: ApprovalsAction): Approval[];
29
+ /** The question for one approval, in the kit's own sentence (`@byokit/openclaw/device`'s `words` fits). */
30
+ export declare const approvalWords: (a: Approval, words: (key: "approval.ask", vars: {
31
+ helper: string;
32
+ summary: string;
33
+ }) => string, helper: string) => string;
34
+ /** What `approvalsStore` needs from a device client: `openclawDevice(link)` fits. */
35
+ export type ApprovalsSource = {
36
+ approvals(): Promise<Approval[]>;
37
+ events(): AsyncIterable<ApprovalFrame>;
38
+ };
39
+ export type FollowOptions = {
40
+ retryMs?: number;
41
+ now?: () => number;
42
+ };
43
+ /**
44
+ * The approvals still waiting, live: listed once the event stream is open (so nothing falls between the two), then
45
+ * added and resolved from its frames, and each dropped as it expires. Opened again after the link comes back.
46
+ */
47
+ export declare function approvalsStore(source: ApprovalsSource, { retryMs, now }?: FollowOptions): Store<Approval[]>;