@byokit/ui 0.6.0 → 0.8.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 +30 -0
- package/README.md +73 -14
- package/dist/accounts.d.ts +183 -0
- package/dist/accounts.js +209 -0
- package/dist/approvals.d.ts +1 -1
- package/dist/approvals.js +1 -1
- package/dist/browser.js +1 -1
- package/dist/connect.js +5 -2
- package/dist/herdr.d.ts +2 -2
- package/dist/herdr.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +11 -9
- package/dist/kits.js +5 -5
- package/dist/phase.d.ts +1 -1
- package/dist/run.d.ts +2 -0
- package/dist/run.js +3 -2
- package/dist/useAccounts.d.ts +48 -0
- package/dist/useAccounts.js +156 -0
- package/dist/useConnect.d.ts +5 -5
- package/dist/useConnect.js +1 -1
- package/dist/useKits.d.ts +5 -5
- package/dist/useKits.js +3 -3
- package/dist/useSignIn.d.ts +1 -1
- package/dist/useSignIn.js +1 -1
- package/dist/words.json +27 -1
- package/package.json +9 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Dependency update: pins @byokit/accounts 0.24.1.
|
|
6
|
+
|
|
7
|
+
## 0.8.0 (2026-10-16)
|
|
8
|
+
|
|
9
|
+
- Dependency update: pins @byokit/accounts 0.24.0.
|
|
10
|
+
|
|
11
|
+
- FEAT: `accountRows(accounts, room, now, windows?)` and `AccountsSource.windows` take an account's `windowsView` from `@byokit/usage/view`: a ready row gains `windows` (each usage window, tightest first, with its line, its reset line and its millisecond reset) and `asOf` (`Read 2 minutes ago`, `Read 2 days ago (stale)` past a day). A window whose reset is already past reads `Session: reset since this reading` (no percent, no countdown), and so does the row's room line (`Plus · Reset since this reading`). A stale row is never `low`, and a reading without its own time (an openclaw room) shows its resets and no age. New types `UsageWindow`, `UsageWindows` and `AsOf`. The PWA example's account card draws both windows with their resets and age, keeps the last reading when a refresh fails so it shows as stale instead of unknown, and fits the longest address at 320px with larger text.
|
|
12
|
+
- FEAT: `runStore` tolerates an account run's first frame: the run stream may open with an `account` event (O14m) naming the account the reply bills to, which the run view ignores while the reply, tools and end behave as before.
|
|
13
|
+
- FIX: a subscription row whose route name already says plan ("Qwen Token Plan", "Qwen Token Plan CN", "Qwen Token Plan Individual") no longer reads "Uses your ... plan" with the word twice: `connectView` leaves the template's trailing plan out when the name already carries the word (any case). Names without it ("Qwen") still read "Uses your Qwen plan".
|
|
14
|
+
|
|
15
|
+
- Dependency update: pins @byokit/accounts 0.23.0.
|
|
16
|
+
|
|
17
|
+
## 0.7.0 (2026-10-12)
|
|
18
|
+
|
|
19
|
+
- Dependency update: pins @byokit/accounts 0.22.0.
|
|
20
|
+
|
|
21
|
+
- FEAT: Add `useAccounts` and `useModelPicker` to `@byokit/ui` (with the `accountsSource`/`cliSource` one-line-per-member adapters): live rows with `refresh()`, a failing room leaving only its row `Room left unknown`, a `remove()` landing mid-refresh still dropping the row, and a picker whose `select()` marks the selection at once and keeps it across refreshes. The openclaw adapter stays pending O14.
|
|
22
|
+
- FEAT: Add `accountRows`, `rowsOf`, `nameSuggestions` and the `AccountsSource`, `AccountRow`, `Account` and `Room` types (also `@byokit/ui/accounts`, no React): one plain-words row per account (`ChatGPT · Work` / `Plus · 72% left this week`), honest words for every sign-in state, `low` at or below 20%, a `sign_in`/`wait` action, and untaken account-name suggestions from an email's local part that never put an email in a title.
|
|
23
|
+
- FEAT: `herdrTreeView` groups carry `project`, the workspace folder's own name without the tab label, so rows can name an agent (computer, project, agent) without internal tab labels.
|
|
24
|
+
- FEAT: Browser sign-in sheet and live panel views: `signInSheetView(request, { name, holder })` gives the sentence, buttons and chat chip for each request state; `signInsStore(oc)` keeps requests live from sign-in pings; `livePanelView(state)` says when to draw frames, take input, show "Reconnecting…" or ask to confirm an unexpected address. Typed structurally against `@byokit/openclaw` spec 5.17.
|
|
25
|
+
- FEAT: Add `connectView(routes, step)`, `connectStep`, `signInFor` (also `@byokit/ui/connect`, no React) and `useConnect`: every account route the kits list, grouped as Plans (offered by default), Pay per use, On this computer, Your own server, Cloud account, Other ways to connect, and Speech, search and media, each row with plain billing, method and readiness words. Billing decides the group, so no API, cloud, local, server or unknown-billing row is ever in Plans; key routes reuse `keyView`, sign-in routes hand `useSignIn` their row.
|
|
26
|
+
- FEAT: Add `modelPicker` and `bestMoveTarget` to `@byokit/ui/accounts` (no React, no Node): the run picker with the `Auto — most room left` row carrying the pick's own reason, one plain row per account with its model rows and the selection marked, unavailable rows with a typed why and `until`, Auto disabled with a typed reason when no account can be named, and the Auto-ordered account a running session should move to (`Move to Work` / `Couldn't move to Work`).
|
|
27
|
+
- FEAT: plain pairing words for the phone page: `connectedWords(hostName, url?, kind?)` renders the connected line with the route just dialled (`Connected to Kitchen computer - same Wi-Fi.`), `routeShort(route)` gives the inline label, and `pairErrorWords(error)` reads a failed pairing as expired, wrong-code or the link's own sentence from @byokit/link's error code.
|
|
28
|
+
- FEAT: `describeRoute` labels in plain words: `Same Wi-Fi`, `Temporary public link`, `Away from home` (this kit's relay address) and `Your own server`. No route, choice or status sentence carries an address, a protocol name or relay jargon.
|
|
29
|
+
- The PWA example's account card now runs on the shared views: `accountRows`/`modelPicker` from `@byokit/ui/accounts` draw the rows and the run picker (Auto carries its own reason, a signed-out pick shows `Sign in first` and runs nothing), and `bestMoveTarget`/`moveWords` suggest the roomier account when the chosen one is low.
|
|
30
|
+
- `@byokit/ui-core` is now published as `@byokit/ui`, with the same API and the same entries (`.`, `./phase`, `./route`, `./link`, `./kits`, `./steps`, `./connect`); its earlier versions are the `@byokit/ui-core` releases below. **Deprecated:** the `@byokit/ui-core` name; it stays published as a shim that re-exports this package through 0.7.x and is removed in 0.8.0.
|
|
31
|
+
- Accept OpenClaw's admitted/thinking progress frames structurally without changing the existing run reducer's rendered state.
|
|
32
|
+
|
|
33
|
+
- Dependency update: pins @byokit/accounts 0.21.0.
|
|
34
|
+
|
|
5
35
|
- Run views accept structured-output failures and display the kit’s plain validation message.
|
|
6
36
|
|
|
7
37
|
## 0.6.0 (2026-10-01)
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ pairing sheet, the link's status, the route a phone takes home, and a runtime ki
|
|
|
12
12
|
app keeps its own look. Everything except the React hooks is framework-free.</p>
|
|
13
13
|
|
|
14
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 '
|
|
15
|
+
<img src="https://raw.githubusercontent.com/umeranjum17/byokit/main/docs/images/pwa-2-code.png" width="240" alt="A phone-width page titled Umer's AI plans, with the ChatGPT card reading 'Signing in to ChatGPT…' and 'On the ChatGPT page, type this code:', the code MOCK-10001 in large letters, 'Code expires in 14:59', and Open ChatGPT and Cancel buttons." />
|
|
16
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
17
|
</p>
|
|
18
18
|
|
|
@@ -35,7 +35,7 @@ npm install @byokit/ui
|
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
React is an optional peer dependency, needed only for the hooks (`useSignIn`, `useRun`, `useApprovals`,
|
|
38
|
-
`useHerdrTree`, `useBlocked`).
|
|
38
|
+
`useHerdrTree`, `useBlocked`, `useConnect`, `useAccounts`, `useModelPicker`).
|
|
39
39
|
|
|
40
40
|
```ts
|
|
41
41
|
import { phaseOf, stepOf } from '@byokit/ui/phase';
|
|
@@ -123,11 +123,13 @@ export function useChatGptSheet() {
|
|
|
123
123
|
| `useRun`, `useApprovals`, `useHerdrTree`, `useBlocked` | React hooks over those stores |
|
|
124
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
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
|
-
|
|
|
126
|
+
| `accountRows(accounts, room, now, windows?)`, `rowsOf(source, now)`, `nameSuggestions(email, taken, provider?)` | One plain-words row per account (`ChatGPT · Work` / `Plus · 72% left this week`), the honest words for every sign-in state, and untaken account-name suggestions |
|
|
127
|
+
| `useAccounts(source)`, `useModelPicker(source, initial?)` | React hooks: the rows live with `refresh()` and `remove()` (a failing room leaves only its row unknown; a mid-refresh remove still drops it), and the run picker with `select()` keeping the caller's selection across refreshes; `accountsSource(accounts, member, room?)` and `cliSource(cli, room?)` fill the source with one line per member (the openclaw adapter stays pending O14) |
|
|
128
|
+
| Types | `Phase`, `SignInView`, `AccountView`, `UseSignIn`, `Route`, `RouteChoice`, `RouteCode`, `PairPhase`, `Role`, `DeviceKind`, `LinkStatus`; `RouteKind` from `@byokit/ui/route`; `Account`, `AccountRow`, `AccountAction`, `AccountsSource`, `AsOf`, `Billing`, `Room`, `RoomSpan`, `SignInState`, `UsageWindow`, `UsageWindows` from `@byokit/ui/accounts` |
|
|
127
129
|
|
|
128
130
|
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
|
|
130
|
-
`@byokit/ui/connect` (no React dependency).
|
|
131
|
+
`@byokit/ui/route`, `@byokit/ui/link`, `@byokit/ui/kits`, `@byokit/ui/steps`,
|
|
132
|
+
`@byokit/ui/connect` and `@byokit/ui/accounts` (no React dependency).
|
|
131
133
|
|
|
132
134
|
## Sign-in phases
|
|
133
135
|
|
|
@@ -152,14 +154,14 @@ into the phase to draw:
|
|
|
152
154
|
## Routes
|
|
153
155
|
|
|
154
156
|
`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
|
|
156
|
-
to `direct`); without provenance, 100.64/10 reads as a private
|
|
157
|
-
other home ranges read as same Wi-Fi.
|
|
157
|
+
pairing or settings screen. Pass `tailscale`, `direct`, `private`, or `lan` when known (map
|
|
158
|
+
[`@byokit/discover`](../discover)'s `tailscale-direct` to `direct`); without provenance, 100.64/10 reads as a private
|
|
159
|
+
network rather than assumed to be Tailscale, and the other home ranges read as same Wi-Fi.
|
|
158
160
|
|
|
159
161
|
`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
|
|
161
|
-
public link and your own server, which reach does not probe).
|
|
162
|
-
`needs` from here and only adds availability.
|
|
162
|
+
sentence, needs }` per route in recommendation order, a superset of `@byokit/discover`'s `Via` (it also covers the
|
|
163
|
+
temporary public link and your own server, which `reach()` there does not probe). `recommend()` from the same package
|
|
164
|
+
takes `sentence` and `needs` from here and only adds availability.
|
|
163
165
|
|
|
164
166
|
`connectedWords(hostName, url?, kind?)` (same entry) is the connected line a consumer renders: the computer with
|
|
165
167
|
the route just dialled (`Connected to Kitchen computer - same Wi-Fi.`), or plain (`Connected to Kitchen computer.`)
|
|
@@ -210,9 +212,66 @@ A key route's `chosen.key` is the existing `keyView` card, labelled for its bill
|
|
|
210
212
|
`useConnect({ routes, read, start?, cancel })` holds the step in React and returns the view with `pick`, `back`,
|
|
211
213
|
`act` and `signIn`, the options to pass to `useSignIn` in a sheet keyed by `chosen.row.key`.
|
|
212
214
|
|
|
215
|
+
## Account rows
|
|
216
|
+
|
|
217
|
+
`accountRows(accounts, room, now, windows?)` (also `@byokit/ui/accounts`, no React) turns a list of accounts and a room reading
|
|
218
|
+
into one plain row each: the provider and the person's own name for the account (`ChatGPT · Work`), then the plan and
|
|
219
|
+
the room left (`Plus · 72% left this week`). A row also carries `state`, `billing`, `low` (room left at or below 20%,
|
|
220
|
+
exactly 20 included, never true for an unknown room) and, when it helps, an `action`: `sign_in` for a signed-out or
|
|
221
|
+
lapsed account, `wait` plus the `until` refill time for a resting one. `accountRows` reads only the fields it names,
|
|
222
|
+
so a credential on an account never reaches a row. A row that is not ready shows its own honest words ("Checking…",
|
|
223
|
+
"Cooling down", "Not signed in", "Sign in again", "Not in your plan") instead of the room.
|
|
224
|
+
|
|
225
|
+
Pass `windows`, the account's `windowsView(reading, now)` from `@byokit/usage/view`, as a fourth argument and a ready
|
|
226
|
+
row also carries `windows` (each usage window, tightest first: `Week: 72% left` with `resets in 4d`) and `asOf`
|
|
227
|
+
(`Read 2 minutes ago`, or `Read 2 days ago (stale)` past a day). A window whose reset is already past reads
|
|
228
|
+
`Session: reset since this reading`, with no percent or countdown, and the room line `Reset since this reading`. A stale row is never `low`; a reading without its own
|
|
229
|
+
time, such as an openclaw room, shows its resets and no `asOf`, never a made-up age.
|
|
230
|
+
|
|
231
|
+
`AccountsSource` is the shape an app fills from its own back end — `accounts()`, `room(account)` and optionally
|
|
232
|
+
`windows(account)` — and `rowsOf(source, now)` awaits them and builds the same rows; `@byokit/accounts` `list()` and a room reading fit it with one line per member.
|
|
233
|
+
`nameSuggestions(email, taken, provider?)` offers up to three untaken account names from an email's local part
|
|
234
|
+
(deduped case-insensitively against `taken`), falling back to the provider's name; the email itself is never offered.
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { accountRows, nameSuggestions, type Account, type Room } from '@byokit/ui/accounts';
|
|
238
|
+
|
|
239
|
+
declare const list: () => readonly Account[];
|
|
240
|
+
declare const reading: (account: Account) => Room;
|
|
241
|
+
const rows = accountRows(list(), reading, Date.now());
|
|
242
|
+
// rows → [{ title: 'ChatGPT · Work', detail: 'Plus · 72% left this week', state: 'ready', low: false, ... }]
|
|
243
|
+
console.log(nameSuggestions('umer@example.com', rows.map((r) => r.title)));
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
In React, `useAccounts(source)` keeps those rows live (`refresh()` re-reads; `remove(id)` removes through
|
|
247
|
+
the back end and drops the row even mid-refresh), and `useModelPicker(source)` keeps the run picker live
|
|
248
|
+
(`select()` marks the selection at once and keeps it across refreshes). `accountsSource(accounts, member,
|
|
249
|
+
room?)` fills the source from a live `@byokit/accounts` member and `cliSource(cli, room?)` from a live
|
|
250
|
+
`cliAccounts()` kit, each with one line per member; the openclaw adapter stays pending O14:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import { accountsSource, cliSource, useAccounts, useModelPicker } from '@byokit/ui';
|
|
254
|
+
import type { Accounts } from '@byokit/accounts';
|
|
255
|
+
import { cliAccounts } from '@byokit/accounts/cli';
|
|
256
|
+
import type { Account, Room } from '@byokit/ui/accounts';
|
|
257
|
+
|
|
258
|
+
declare const accounts: Accounts<any, number>;
|
|
259
|
+
declare const cli: ReturnType<typeof cliAccounts>;
|
|
260
|
+
declare const reading: (account: Account) => Room;
|
|
261
|
+
const fromMember = accountsSource(accounts, 1, reading);
|
|
262
|
+
const fromCli = cliSource(cli, reading);
|
|
263
|
+
|
|
264
|
+
export function useAccountList() {
|
|
265
|
+
return useAccounts(fromCli); // { rows, ready, error, refresh, remove }
|
|
266
|
+
}
|
|
267
|
+
export function useRunPicker() {
|
|
268
|
+
return useModelPicker(fromMember); // { rows, picker, selection, ready, error, select, refresh, remove }
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
213
272
|
## Pairing words
|
|
214
273
|
|
|
215
|
-
Pairing with `@byokit/link
|
|
274
|
+
Pairing with `@byokit/pair` (published as `@byokit/link` up to 0.8.x), from `@byokit/ui/link` (no React either):
|
|
216
275
|
|
|
217
276
|
- `qrMatrix(offer.text, { border }?)`: the pairing QR as rows of dark and light modules, with its quiet border
|
|
218
277
|
(2 unless given), to draw in any UI.
|
|
@@ -273,7 +332,7 @@ Runtime kits' state, from `@byokit/ui/kits` (no React), typed to fit `@byokit/op
|
|
|
273
332
|
|
|
274
333
|
- Runs: `runStore(oc)` with `send(message)` and `stop()`; `runView(state, { words, name })` gives the reply so far, the
|
|
275
334
|
tool at work and, for a failed run, the kit's sentence (`words` from `@byokit/openclaw/device`, `name` "ChatGPT").
|
|
276
|
-
OpenClaw's `started` and `
|
|
335
|
+
OpenClaw's `started`, `thinking` and `account` frames are accepted without changing the reply, tools or rendered state;
|
|
277
336
|
the running phase begins at `send`, not Gateway acceptance.
|
|
278
337
|
- Approvals: `approvalsStore(oc)` keeps the waiting list live (each leaves when answered anywhere or when it expires);
|
|
279
338
|
`approvalWords(approval, words, 'Your helper')` asks the question.
|
|
@@ -293,7 +352,7 @@ with the device client made once (`useMemo(() => herdrDevice(link), [link])`).
|
|
|
293
352
|
## Links
|
|
294
353
|
|
|
295
354
|
- [byokit](../../README.md): the monorepo and its other packages, including
|
|
296
|
-
[`@byokit/accounts`](../accounts) and [`@byokit/
|
|
355
|
+
[`@byokit/accounts`](../accounts) and [`@byokit/pair`](../pair).
|
|
297
356
|
- Examples: [`examples/pwa`](../../examples/pwa) (sign-in phases in a browser),
|
|
298
357
|
[`examples/herdr-kit`](../../examples/herdr-kit) (pairing words and `herdrStore` from a phone browser),
|
|
299
358
|
[`examples/expo`](../../examples/expo) (React Native).
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
export type Billing = 'subscription' | 'api' | 'local' | 'free' | 'unknown';
|
|
2
|
+
export type SignInState = 'ready' | 'signing' | 'resting' | 'signed_out' | 'needs_again' | 'not_included';
|
|
3
|
+
export type RoomSpan = 'session' | 'week' | 'month' | 'tightest';
|
|
4
|
+
/** A room-left reading, restated structurally from `@byokit/accounts` (multi.ts): a known percentage or 'unknown'. */
|
|
5
|
+
export type Room = {
|
|
6
|
+
left: number;
|
|
7
|
+
span: RoomSpan;
|
|
8
|
+
resetsAt?: number;
|
|
9
|
+
at?: number;
|
|
10
|
+
} | {
|
|
11
|
+
left: 'unknown';
|
|
12
|
+
at?: number;
|
|
13
|
+
};
|
|
14
|
+
/** One account as `@byokit/accounts` `list()` reports it; only these fields are read, extra ones (a token included) are ignored. */
|
|
15
|
+
export type Account = {
|
|
16
|
+
id: string;
|
|
17
|
+
provider: string;
|
|
18
|
+
name: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
state: SignInState;
|
|
21
|
+
billing: Billing;
|
|
22
|
+
plan?: string;
|
|
23
|
+
until?: number;
|
|
24
|
+
};
|
|
25
|
+
/** What an app supplies: the accounts it lists and one room reading per account. `AccountsSource` is the seam between
|
|
26
|
+
* any back end and these rows; an at-most-one-line-per-member adapter fits it. */
|
|
27
|
+
export type AccountsSource = {
|
|
28
|
+
accounts: () => Promise<readonly Account[]> | readonly Account[];
|
|
29
|
+
room: (account: Account) => Promise<Room> | Room;
|
|
30
|
+
/** The account's `windowsView`, when the app keeps its usage reading. */
|
|
31
|
+
windows?: (account: Account) => Promise<UsageWindows | undefined> | UsageWindows | undefined;
|
|
32
|
+
};
|
|
33
|
+
/** One usage window, restated structurally from `@byokit/usage/view` `windowsView`: its line (`Week: 58% left`), its
|
|
34
|
+
* reset line (`resets in 4h 40m`) and the unchanged millisecond reset. */
|
|
35
|
+
export type UsageWindow = {
|
|
36
|
+
kind: 'session' | 'weekly' | 'monthly' | 'rolling' | 'custom';
|
|
37
|
+
text: string;
|
|
38
|
+
resetText: string;
|
|
39
|
+
resetsAt?: number;
|
|
40
|
+
usedPercent?: number;
|
|
41
|
+
limited?: boolean;
|
|
42
|
+
};
|
|
43
|
+
/** An account's `windowsView(reading, now)`, restated structurally; only these fields are read. */
|
|
44
|
+
export type UsageWindows = {
|
|
45
|
+
windows: readonly UsageWindow[];
|
|
46
|
+
at?: number;
|
|
47
|
+
ageMs?: number;
|
|
48
|
+
ageText?: string;
|
|
49
|
+
stale: boolean;
|
|
50
|
+
};
|
|
51
|
+
/** How old a row's windows are, taken only from the reading's own time: `Read 2 minutes ago`, with `(stale)` past a day. */
|
|
52
|
+
export type AsOf = {
|
|
53
|
+
at: number;
|
|
54
|
+
ageMs: number;
|
|
55
|
+
text: string;
|
|
56
|
+
stale: boolean;
|
|
57
|
+
};
|
|
58
|
+
export type AccountAction = 'sign_in' | 'wait';
|
|
59
|
+
export type AccountRow = {
|
|
60
|
+
id: string;
|
|
61
|
+
title: string;
|
|
62
|
+
detail: string;
|
|
63
|
+
state: SignInState;
|
|
64
|
+
billing: Billing;
|
|
65
|
+
/** Room left is at or below 20%: true at exactly 20, false above it, when the room is unknown or the windows are stale. */
|
|
66
|
+
low: boolean;
|
|
67
|
+
/** A ready account's usage windows, tightest first, each with its reset; one that reset since the reading reads
|
|
68
|
+
* `Session: reset since this reading`, with an empty `resetText` and no `usedPercent`. Present when the app passed them. */
|
|
69
|
+
windows?: UsageWindow[];
|
|
70
|
+
/** The windows' age; absent when the reading carries no time of its own (never a made-up one) or a future one. */
|
|
71
|
+
asOf?: AsOf;
|
|
72
|
+
/** What the person can do: sign in again, or wait for a resting account to refill. Absent when nothing helps. */
|
|
73
|
+
action?: AccountAction;
|
|
74
|
+
/** A resting account's next refill, epoch milliseconds. */
|
|
75
|
+
until?: number;
|
|
76
|
+
};
|
|
77
|
+
/** One row per account, reading one room per account; a missing or throwing reading is 'Room left unknown'. `windows`
|
|
78
|
+
* is the account's `windowsView` from `@byokit/usage/view`, when the app keeps the reading; a missing or throwing one
|
|
79
|
+
* leaves the row without windows. */
|
|
80
|
+
export declare function accountRows(accounts: readonly Account[], room: (account: Account) => Room, now: number, windows?: (account: Account) => UsageWindows | undefined): AccountRow[];
|
|
81
|
+
/** `accountRows` over an `AccountsSource`: the app's own `accounts()`, `room()` and `windows()`, awaited; a failing room
|
|
82
|
+
* reads unknown and failing windows leave the row without them. */
|
|
83
|
+
export declare function rowsOf(source: AccountsSource, now: number): Promise<AccountRow[]>;
|
|
84
|
+
/** Untaken names an app can offer for a new account, from the email's local part (never the email itself), else the
|
|
85
|
+
* provider's name. Dedupes case-insensitively against `taken`; up to three. */
|
|
86
|
+
export declare function nameSuggestions(email: string | undefined, taken: readonly string[], provider?: string): string[];
|
|
87
|
+
/** Why one of an account's models cannot be used right now, restated from `@byokit/accounts` (multi.ts). */
|
|
88
|
+
export type ModelWhy = 'plan' | 'resting' | 'signed_out';
|
|
89
|
+
/** One model as `@byokit/accounts` `models()` lists it: a strong/fast tier, whether it can run, and, when it can't, why. */
|
|
90
|
+
export type ModelInfo = {
|
|
91
|
+
id: string;
|
|
92
|
+
name: string;
|
|
93
|
+
tier?: 'strong' | 'fast';
|
|
94
|
+
available: boolean;
|
|
95
|
+
why?: ModelWhy;
|
|
96
|
+
until?: number;
|
|
97
|
+
};
|
|
98
|
+
/** What a run asks for, restated structurally: an account (or `default`/`auto`) and one model. */
|
|
99
|
+
export type RunSelection = {
|
|
100
|
+
account: string | 'default' | 'auto';
|
|
101
|
+
model?: string;
|
|
102
|
+
};
|
|
103
|
+
/** Why Auto named (or could not name) an account, restated from `@byokit/accounts` `resolveSelection`. */
|
|
104
|
+
export type PickWhy = 'chosen' | 'default' | 'first_ready' | 'only' | 'most_room' | 'earlier_reset' | 'list_order' | 'no_reading' | 'refills_first';
|
|
105
|
+
/** Why an account cannot be used, as Auto judges it, restated from `@byokit/accounts` `Considered`. */
|
|
106
|
+
export type PickOut = 'state' | 'resting' | 'billing' | 'model' | 'provider';
|
|
107
|
+
/** One account's line in the pick's explanation; only the id, the why and the refill time are read here. */
|
|
108
|
+
type ConsideredRow = {
|
|
109
|
+
id: string;
|
|
110
|
+
out?: PickOut;
|
|
111
|
+
until?: number;
|
|
112
|
+
};
|
|
113
|
+
/** `@byokit/accounts` `resolveSelection`/`pick`, restated structurally: what Auto chose, or why it could not. */
|
|
114
|
+
export type Pick = {
|
|
115
|
+
ok: true;
|
|
116
|
+
account: Account;
|
|
117
|
+
model: string;
|
|
118
|
+
how: 'chosen' | 'default' | 'auto';
|
|
119
|
+
why: PickWhy;
|
|
120
|
+
reason: string;
|
|
121
|
+
considered?: readonly ConsideredRow[];
|
|
122
|
+
} | {
|
|
123
|
+
ok: false;
|
|
124
|
+
code: 'none' | 'not_included' | 'unknown_account';
|
|
125
|
+
reason: string;
|
|
126
|
+
considered?: readonly ConsideredRow[];
|
|
127
|
+
};
|
|
128
|
+
/** One model row under an account, marked `selected` when it is the run's model. */
|
|
129
|
+
export type PickerModel = ModelInfo & {
|
|
130
|
+
selected: boolean;
|
|
131
|
+
};
|
|
132
|
+
/** One picker row: the `Auto — most room left` row first, then one per account in list order. */
|
|
133
|
+
export type PickerRow = {
|
|
134
|
+
/** `'auto'`, or the account id. */
|
|
135
|
+
key: string;
|
|
136
|
+
kind: 'auto' | 'account';
|
|
137
|
+
/** `Auto — most room left`, or the account's own plain title (`ChatGPT · Work`). */
|
|
138
|
+
title: string;
|
|
139
|
+
/** Auto's own reason in words on the Auto row; an account row's why, or its status when it can be used. */
|
|
140
|
+
detail: string;
|
|
141
|
+
/** True for the one row the run is set to use. */
|
|
142
|
+
selected: boolean;
|
|
143
|
+
/** True when the row cannot be chosen; only Auto, when it has no account to name. */
|
|
144
|
+
disabled: boolean;
|
|
145
|
+
/** Why Auto is disabled, typed from the pick's failure code; present only when `disabled`. */
|
|
146
|
+
disabledReason?: 'none' | 'not_included' | 'unknown_account';
|
|
147
|
+
/** Why the account cannot be used right now: a sign-in problem, a rest, per-use billing, a missing model, or
|
|
148
|
+
* another service. Absent when it can be used. */
|
|
149
|
+
out?: PickOut;
|
|
150
|
+
/** A resting account's next refill, epoch milliseconds. */
|
|
151
|
+
until?: number;
|
|
152
|
+
/** This account's models, each marked when it is the run's model; empty for the Auto row. */
|
|
153
|
+
models: PickerModel[];
|
|
154
|
+
};
|
|
155
|
+
/** What a run or settings sheet draws: the rows, the named selection, and what it resolves to. */
|
|
156
|
+
export type PickerView = {
|
|
157
|
+
rows: PickerRow[];
|
|
158
|
+
/** Exactly what `sel.account` named: `'auto'` or an account id. */
|
|
159
|
+
selection: string;
|
|
160
|
+
/** True when the selection resolves to a usable account and model. */
|
|
161
|
+
ok: boolean;
|
|
162
|
+
/** The account the run uses, when `ok`. */
|
|
163
|
+
account?: string;
|
|
164
|
+
/** The model the run uses, when `ok`. */
|
|
165
|
+
model?: string;
|
|
166
|
+
};
|
|
167
|
+
/** The one sentence a Move sheet shows: `Move to Work` after a tap, `Couldn't move to Work` after a refusal. */
|
|
168
|
+
export declare function moveWords(name: string, moved?: boolean): string;
|
|
169
|
+
/**
|
|
170
|
+
* The picker rows for a run selection, with the selection marked: the `Auto — most room left` row carrying Auto's
|
|
171
|
+
* own `pick.reason`, then one row per account with its model rows and, when it cannot be used, Auto's why and
|
|
172
|
+
* `until`. `auto` is the pick `@byokit/accounts` `resolveSelection` made for `{ account: 'auto' }`; a signed-out
|
|
173
|
+
* explicit pick is shown as `Sign in first` and never replaced by another account.
|
|
174
|
+
*/
|
|
175
|
+
export declare function modelPicker(accounts: readonly Account[], models: Record<string, readonly ModelInfo[]>, sel: RunSelection, auto: Pick, now?: number): PickerView;
|
|
176
|
+
/**
|
|
177
|
+
* The account a running session should move to, or undefined when there is nowhere better to go: `@byokit/accounts`
|
|
178
|
+
* `chooseAccount`'s own winner (`moveToAccount` carries the session there). Nothing to move to with one account,
|
|
179
|
+
* with only signed-out peers, when nobody has room left, or when the list is API keys only; the current account is
|
|
180
|
+
* never named back to itself. `now` defaults to the current time, as `accountRows` does.
|
|
181
|
+
*/
|
|
182
|
+
export declare function bestMoveTarget(accounts: readonly Account[], room: Record<string, Room>, current: string, now?: number): string | undefined;
|
|
183
|
+
export {};
|
package/dist/accounts.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// One row per account, in plain words: the provider and the person's own name for it, then the plan and the room
|
|
2
|
+
// left, or the honest reason it is not usable right now. Framework-free and portable: it reads only the fields it
|
|
3
|
+
// names (D3), so no credential on an account ever reaches a row. `AccountsSource` is the shape an app fills from its
|
|
4
|
+
// own back end; @byokit/accounts `list()` and a room reading fit it with at most one line per member.
|
|
5
|
+
import WORDS from './words.json' with { type: 'json' };
|
|
6
|
+
const UNKNOWN = { left: 'unknown' };
|
|
7
|
+
const LOW_AT = 20;
|
|
8
|
+
const roomWords = (r) => r.left === 'unknown' ? WORDS['account.room.unknown'] : WORDS[`account.room.${r.span}`].replace('{left}', `${Math.round(r.left)}%`);
|
|
9
|
+
const planLabel = (plan) => (plan ? plan.charAt(0).toUpperCase() + plan.slice(1) : '');
|
|
10
|
+
/** 'ChatGPT · Work', or just the provider's own name when the account keeps it. An email is never used as a name. */
|
|
11
|
+
function titleOf(a) {
|
|
12
|
+
const provider = a.label || a.provider;
|
|
13
|
+
const name = a.name && !a.name.includes('@') && a.name.toLowerCase() !== provider.toLowerCase() ? a.name : '';
|
|
14
|
+
return name ? `${provider} · ${name}` : provider;
|
|
15
|
+
}
|
|
16
|
+
/** A resting account whose refill time has passed reads as ready again, exactly as the Auto chooser treats it. */
|
|
17
|
+
const refilled = (a, now) => a.state === 'resting' && typeof a.until === 'number' && a.until <= now;
|
|
18
|
+
/** A reset at or before now happened after the reading, so its numbers no longer hold. */
|
|
19
|
+
const resetSince = (resetsAt, now) => typeof resetsAt === 'number' && resetsAt <= now;
|
|
20
|
+
/** Only the named fields, so nothing else on the app's view (a scope, a provider) reaches a row. A window that reset
|
|
21
|
+
* since the reading says so, with no percent and no countdown. */
|
|
22
|
+
const windowOf = (x, now) => resetSince(x.resetsAt, now)
|
|
23
|
+
? { kind: x.kind, text: WORDS['account.window.resetSince'].replace('{label}', WORDS[`account.window.${x.kind}`]), resetText: '', resetsAt: x.resetsAt }
|
|
24
|
+
: { kind: x.kind, text: x.text, resetText: x.resetText, ...(typeof x.resetsAt === 'number' ? { resetsAt: x.resetsAt } : {}),
|
|
25
|
+
...(typeof x.usedPercent === 'number' ? { usedPercent: x.usedPercent } : {}), ...(x.limited ? { limited: true } : {}) };
|
|
26
|
+
function asOfOf(w) {
|
|
27
|
+
if (typeof w.at !== 'number' || typeof w.ageMs !== 'number' || !w.ageText)
|
|
28
|
+
return undefined;
|
|
29
|
+
return { at: w.at, ageMs: w.ageMs, text: w.stale ? WORDS['account.stale'].replace('{age}', w.ageText) : w.ageText, stale: w.stale };
|
|
30
|
+
}
|
|
31
|
+
function rowOf(a, room, now, w) {
|
|
32
|
+
const state = refilled(a, now) ? 'ready' : a.state;
|
|
33
|
+
const title = titleOf(a);
|
|
34
|
+
if (state === 'ready') {
|
|
35
|
+
// A room whose window reset since the reading no longer holds its number: say so, and it is not low.
|
|
36
|
+
const reset = room.left !== 'unknown' && resetSince(room.resetsAt, now);
|
|
37
|
+
const words = reset ? WORDS['account.room.resetSince'] : roomWords(room);
|
|
38
|
+
const asOf = w && asOfOf(w);
|
|
39
|
+
return { id: a.id, title, detail: a.plan && room.left !== 'unknown' ? `${planLabel(a.plan)} · ${words}` : words, state, billing: a.billing,
|
|
40
|
+
low: room.left !== 'unknown' && room.left <= LOW_AT && !w?.stale && !reset,
|
|
41
|
+
...(w ? { windows: w.windows.map((x) => windowOf(x, now)) } : {}), ...(asOf ? { asOf } : {}) };
|
|
42
|
+
}
|
|
43
|
+
if (state === 'resting') {
|
|
44
|
+
return { id: a.id, title, detail: WORDS['account.resting'], state, billing: a.billing, low: false, action: 'wait',
|
|
45
|
+
...(typeof a.until === 'number' ? { until: a.until } : {}) };
|
|
46
|
+
}
|
|
47
|
+
const action = state === 'signed_out' || state === 'needs_again' ? 'sign_in' : undefined;
|
|
48
|
+
return { id: a.id, title, detail: WORDS[`account.${state}`], state, billing: a.billing, low: false, ...(action ? { action } : {}) };
|
|
49
|
+
}
|
|
50
|
+
/** One row per account, reading one room per account; a missing or throwing reading is 'Room left unknown'. `windows`
|
|
51
|
+
* is the account's `windowsView` from `@byokit/usage/view`, when the app keeps the reading; a missing or throwing one
|
|
52
|
+
* leaves the row without windows. */
|
|
53
|
+
export function accountRows(accounts, room, now, windows) {
|
|
54
|
+
return accounts.map((a) => {
|
|
55
|
+
let r = UNKNOWN;
|
|
56
|
+
try {
|
|
57
|
+
r = room(a) ?? UNKNOWN;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
r = UNKNOWN;
|
|
61
|
+
}
|
|
62
|
+
let w;
|
|
63
|
+
try {
|
|
64
|
+
w = windows?.(a);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
w = undefined;
|
|
68
|
+
}
|
|
69
|
+
return rowOf(a, r, now, w);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** `accountRows` over an `AccountsSource`: the app's own `accounts()`, `room()` and `windows()`, awaited; a failing room
|
|
73
|
+
* reads unknown and failing windows leave the row without them. */
|
|
74
|
+
export async function rowsOf(source, now) {
|
|
75
|
+
const accounts = await source.accounts();
|
|
76
|
+
const rooms = new Map();
|
|
77
|
+
const windows = new Map();
|
|
78
|
+
for (const a of accounts) {
|
|
79
|
+
try {
|
|
80
|
+
rooms.set(a.id, (await source.room(a)) ?? UNKNOWN);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
rooms.set(a.id, UNKNOWN);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
windows.set(a.id, await source.windows?.(a));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
windows.set(a.id, undefined);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return accountRows(accounts, (a) => rooms.get(a.id) ?? UNKNOWN, now, (a) => windows.get(a.id));
|
|
93
|
+
}
|
|
94
|
+
/** Untaken names an app can offer for a new account, from the email's local part (never the email itself), else the
|
|
95
|
+
* provider's name. Dedupes case-insensitively against `taken`; up to three. */
|
|
96
|
+
export function nameSuggestions(email, taken, provider = 'Account') {
|
|
97
|
+
const used = new Set(taken.map((t) => t.trim().toLowerCase()).filter(Boolean));
|
|
98
|
+
const local = email?.split('@')[0]?.split(/[._-]+/).find(Boolean) ?? '';
|
|
99
|
+
const base = (local ? local.charAt(0).toUpperCase() + local.slice(1).toLowerCase() : provider).slice(0, 64) || provider;
|
|
100
|
+
const out = [];
|
|
101
|
+
for (let n = 1; out.length < 3 && n < 100; n++) {
|
|
102
|
+
const name = n === 1 ? base : `${base} ${n}`;
|
|
103
|
+
if (!used.has(name.toLowerCase())) {
|
|
104
|
+
out.push(name);
|
|
105
|
+
used.add(name.toLowerCase());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
/** An account is usable when it is ready, or resting with its refill time already passed, exactly as a row reads. */
|
|
111
|
+
const usableNow = (a, now) => a.state === 'ready' || refilled(a, now);
|
|
112
|
+
/** The one sentence a Move sheet shows: `Move to Work` after a tap, `Couldn't move to Work` after a refusal. */
|
|
113
|
+
export function moveWords(name, moved = true) {
|
|
114
|
+
return WORDS[moved ? 'move.to' : 'move.failed'].replace('{name}', name);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The picker rows for a run selection, with the selection marked: the `Auto — most room left` row carrying Auto's
|
|
118
|
+
* own `pick.reason`, then one row per account with its model rows and, when it cannot be used, Auto's why and
|
|
119
|
+
* `until`. `auto` is the pick `@byokit/accounts` `resolveSelection` made for `{ account: 'auto' }`; a signed-out
|
|
120
|
+
* explicit pick is shown as `Sign in first` and never replaced by another account.
|
|
121
|
+
*/
|
|
122
|
+
export function modelPicker(accounts, models, sel, auto, now = Date.now()) {
|
|
123
|
+
const considered = new Map((auto.considered ?? []).map((r) => [r.id, r]));
|
|
124
|
+
let account;
|
|
125
|
+
let model;
|
|
126
|
+
if (sel.account === 'auto') {
|
|
127
|
+
if (auto.ok) {
|
|
128
|
+
account = auto.account.id;
|
|
129
|
+
model = auto.model;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const chosen = accounts.find((a) => a.id === sel.account);
|
|
134
|
+
if (chosen && usableNow(chosen, now)) {
|
|
135
|
+
const available = (models[chosen.id] ?? []).filter((m) => m.available);
|
|
136
|
+
const pick = sel.model && available.some((m) => m.id === sel.model) ? sel.model : available[0]?.id;
|
|
137
|
+
if (pick) {
|
|
138
|
+
account = chosen.id;
|
|
139
|
+
model = pick;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const rows = [{
|
|
144
|
+
key: 'auto', kind: 'auto', title: WORDS['picker.auto'], detail: auto.reason,
|
|
145
|
+
selected: sel.account === 'auto', disabled: !auto.ok, ...(auto.ok ? {} : { disabledReason: auto.code }), models: [],
|
|
146
|
+
}];
|
|
147
|
+
for (const a of accounts) {
|
|
148
|
+
const isSelected = sel.account === a.id;
|
|
149
|
+
const why = considered.get(a.id);
|
|
150
|
+
const out = why?.out ?? (usableNow(a, now) ? undefined : a.state === 'resting' ? 'resting' : 'state');
|
|
151
|
+
const rowModels = (models[a.id] ?? []).map((m) => ({ ...m, selected: a.id === account && m.id === model && m.available }));
|
|
152
|
+
rows.push({
|
|
153
|
+
key: a.id, kind: 'account', title: titleOf(a), detail: out ? WORDS[`picker.out.${out}`] : '', selected: isSelected, disabled: false,
|
|
154
|
+
...(out ? { out } : {}), ...(out && why?.until !== undefined ? { until: why.until } : {}), models: rowModels,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const ok = account !== undefined && model !== undefined;
|
|
158
|
+
return { rows, selection: sel.account, ok, ...(ok ? { account, model } : {}) };
|
|
159
|
+
}
|
|
160
|
+
const MOVE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
161
|
+
const TIER_ORDER = { room: 0, unknown: 1, exhausted: 2 };
|
|
162
|
+
/**
|
|
163
|
+
* Auto's winner among `accounts`, mirroring `@byokit/accounts` `chooseAccount` (multi.ts): subscriptions only, most
|
|
164
|
+
* room, unknown above exhausted, earliest reset then list order. Used by `bestMoveTarget`, and cross-checked against
|
|
165
|
+
* the real chooser over `fixtures/conformance/auto-pick-typescript.json`.
|
|
166
|
+
*/
|
|
167
|
+
function autoCandidate(accounts, room, now) {
|
|
168
|
+
const rows = accounts.map((a) => {
|
|
169
|
+
const r = room(a);
|
|
170
|
+
const age = typeof r.at === 'number' ? Math.max(0, now - r.at) : 'unknown';
|
|
171
|
+
const stale = typeof age === 'number' && age > MOVE_DAY_MS;
|
|
172
|
+
const left = r.left === 'unknown' || typeof r.left !== 'number' || stale ? 'unknown' : Math.max(0, Math.min(100, r.left));
|
|
173
|
+
let out;
|
|
174
|
+
if (a.state === 'resting' && typeof a.until === 'number' && a.until > now)
|
|
175
|
+
out = 'resting';
|
|
176
|
+
else if (a.state !== 'ready' && !(a.state === 'resting' && typeof a.until === 'number' && a.until <= now))
|
|
177
|
+
out = 'state';
|
|
178
|
+
else if (a.billing !== 'subscription')
|
|
179
|
+
out = 'billing';
|
|
180
|
+
const reset = r.left !== 'unknown' && typeof r.resetsAt === 'number' ? r.resetsAt : undefined;
|
|
181
|
+
const tier = left === 'unknown' ? 'unknown' : left > 0 || (reset !== undefined && reset <= now) ? 'room' : 'exhausted';
|
|
182
|
+
return { account: a, tier, left, ...(out ? { out } : {}), ...(reset !== undefined ? { resetsAt: reset } : {}) };
|
|
183
|
+
});
|
|
184
|
+
const ranked = rows.filter((c) => !c.out).sort((x, y) => {
|
|
185
|
+
const tier = TIER_ORDER[x.tier] - TIER_ORDER[y.tier];
|
|
186
|
+
if (tier)
|
|
187
|
+
return tier;
|
|
188
|
+
if (x.tier === 'unknown')
|
|
189
|
+
return 0;
|
|
190
|
+
if (x.tier === 'room' && x.left !== y.left)
|
|
191
|
+
return Number(y.left) - Number(x.left);
|
|
192
|
+
const rx = x.resetsAt ?? Infinity;
|
|
193
|
+
const ry = y.resetsAt ?? Infinity;
|
|
194
|
+
return rx === ry ? 0 : rx < ry ? -1 : 1;
|
|
195
|
+
});
|
|
196
|
+
return ranked[0];
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The account a running session should move to, or undefined when there is nowhere better to go: `@byokit/accounts`
|
|
200
|
+
* `chooseAccount`'s own winner (`moveToAccount` carries the session there). Nothing to move to with one account,
|
|
201
|
+
* with only signed-out peers, when nobody has room left, or when the list is API keys only; the current account is
|
|
202
|
+
* never named back to itself. `now` defaults to the current time, as `accountRows` does.
|
|
203
|
+
*/
|
|
204
|
+
export function bestMoveTarget(accounts, room, current, now = Date.now()) {
|
|
205
|
+
const pick = autoCandidate(accounts, (a) => room[a.id] ?? { left: 'unknown' }, now);
|
|
206
|
+
if (!pick || pick.account.id === current || pick.tier === 'exhausted')
|
|
207
|
+
return undefined;
|
|
208
|
+
return pick.account.id;
|
|
209
|
+
}
|
package/dist/approvals.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export type ApprovalsAction = ApprovalFrame | {
|
|
|
27
27
|
};
|
|
28
28
|
export declare function approvalsStep(list: Approval[], a: ApprovalsAction): Approval[];
|
|
29
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:
|
|
30
|
+
export declare const approvalWords: (a: Approval, words: (key: 'approval.ask', vars: {
|
|
31
31
|
helper: string;
|
|
32
32
|
summary: string;
|
|
33
33
|
}) => string, helper: string) => string;
|
package/dist/approvals.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// What is waiting for a person's yes: the approvals list kept current from the kit's event stream, each dropped when
|
|
2
2
|
// it is answered anywhere or runs out of time. Framework-free; typed structurally against `@byokit/openclaw/device`.
|
|
3
|
-
import { retrying, store } from
|
|
3
|
+
import { retrying, store } from './follow.js';
|
|
4
4
|
export function approvalsStep(list, a) {
|
|
5
5
|
if ('type' in a) {
|
|
6
6
|
if (a.type === 'set')
|
package/dist/browser.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// the chip under the chat. Framework-free; typed structurally against `@byokit/openclaw`'s `NeedSignIn` and
|
|
3
3
|
// `LiveViewState` (spec 5.17), so nothing here imports a kit. Sentences are the kit's word keys plus their values,
|
|
4
4
|
// for the kit's own `words` to say. Nothing here ever holds what the person types.
|
|
5
|
-
import { retrying, store } from
|
|
5
|
+
import { retrying, store } from './follow.js';
|
|
6
6
|
const settledLine = (s) => {
|
|
7
7
|
switch (s.state) {
|
|
8
8
|
case 'verified':
|
package/dist/connect.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// offered by default; every other row is listed and used only when the person picks it. Nothing here names a
|
|
4
4
|
// provider: groups and words come from each row's billing, method and readiness alone. Framework-free.
|
|
5
5
|
import WORDS from './words.json' with { type: 'json' };
|
|
6
|
-
import { keyStep, keyView } from
|
|
6
|
+
import { keyStep, keyView } from './key.js';
|
|
7
7
|
const ORDER = ['plans', 'perUse', 'local', 'server', 'cloud', 'other', 'services'];
|
|
8
8
|
const SIGNIN = ['browser', 'code', 'paste', 'session', 'cli'];
|
|
9
9
|
const KEY = ['key', 'plan_key', 'setup_token'];
|
|
@@ -34,13 +34,16 @@ function rowOf(r, w) {
|
|
|
34
34
|
const group = groupOf(r);
|
|
35
35
|
const ready = r.readiness === 'ready';
|
|
36
36
|
const billing = r.via === 'endpoint' ? w['billing.server'] : r.via === 'local' ? w['billing.local'] : w[`billing.${r.billing}`];
|
|
37
|
+
// A route name can already carry the word the subscription line adds ("Qwen Token Plan CN"):
|
|
38
|
+
// then leave it out, so the row never reads "... Plan ... plan".
|
|
39
|
+
const line = r.billing === 'subscription' && /\bplan\b/i.test(name) ? billing.replace(/(\{name\})\s+plan\b/i, '$1') : billing;
|
|
37
40
|
return {
|
|
38
41
|
key: `${r.upstream?.surface ?? ''}/${id}`, id, provider: r.provider,
|
|
39
42
|
...(r.upstream ? { surface: r.upstream.surface } : {}),
|
|
40
43
|
name, company, via: r.via, billing: r.billing, group,
|
|
41
44
|
offered: group === 'plans' && ready, ready, readiness: r.readiness,
|
|
42
45
|
does: !ready ? 'unavailable' : SIGNIN.includes(r.via) ? 'signin' : KEY.includes(r.via) ? 'key' : 'setup',
|
|
43
|
-
billingWords: fill(
|
|
46
|
+
billingWords: fill(line, { name, company }), method: w[`via.${r.via}`], status: w[`ready.${r.readiness}`],
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
export function connectStep(step, action) {
|
package/dist/herdr.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Store } from './follow.ts';
|
|
2
|
-
export type AgentStatus = 'idle' | 'working' | 'blocked' | 'done' | 'unknown';
|
|
2
|
+
export type AgentStatus = 'idle' | 'working' | 'blocked' | 'done' | 'failed' | 'unknown';
|
|
3
3
|
export type HerdrAgent = {
|
|
4
4
|
kind?: string;
|
|
5
5
|
name?: string;
|
|
@@ -79,7 +79,6 @@ export declare function herdrTreeView(tree: HerdrTree | null): {
|
|
|
79
79
|
export declare function agentIn(tree: HerdrTree | null, paneId: string | undefined): AgentRow | undefined;
|
|
80
80
|
/** The waiting agents, each with the name to show for it. */
|
|
81
81
|
export declare const blockedView: (s: HerdrState) => {
|
|
82
|
-
name: string;
|
|
83
82
|
paneId: string;
|
|
84
83
|
workspaceId: string;
|
|
85
84
|
tabId: string;
|
|
@@ -87,6 +86,7 @@ export declare const blockedView: (s: HerdrState) => {
|
|
|
87
86
|
revision: number;
|
|
88
87
|
prompt: string;
|
|
89
88
|
since: number;
|
|
89
|
+
name: string;
|
|
90
90
|
}[];
|
|
91
91
|
/** What `herdrStore` needs from a device client: `herdrDevice(link)` fits. */
|
|
92
92
|
export type HerdrSource = {
|
package/dist/herdr.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Herdr's agents as a person sees them: the tree (where each agent is, and how it is doing) and the agents waiting
|
|
2
2
|
// for an answer, kept current from one `hd.events` stream. Framework-free; typed structurally against
|
|
3
3
|
// `@byokit/herdr/device`, so nothing here imports the kit.
|
|
4
|
-
import { retrying, store } from
|
|
4
|
+
import { retrying, store } from './follow.js';
|
|
5
5
|
export const HERDR_EMPTY = { tree: null, blocked: [] };
|
|
6
6
|
export function herdrStep(s, a) {
|
|
7
7
|
if (a.type === 'snapshot') {
|
package/dist/index.d.ts
CHANGED
|
@@ -7,3 +7,5 @@ export * from './kits.ts';
|
|
|
7
7
|
export { useApprovals, useBlocked, useHerdrTree, useRun } from './useKits.ts';
|
|
8
8
|
export { connectStep, connectView, signInFor, type ConnectAction, type ConnectDoes, type ConnectGroup, type ConnectGroupId, type ConnectRoute, type ConnectRow, type ConnectStep, type ConnectView, type ConnectWords, type UseConnectSignIn } from './connect.ts';
|
|
9
9
|
export { useConnect, type UseConnect } from './useConnect.ts';
|
|
10
|
+
export { accountRows, nameSuggestions, rowsOf, type Account, type AccountAction, type AccountRow, type AccountsSource, type AsOf, type Billing, type Room, type RoomSpan, type SignInState, type UsageWindow, type UsageWindows } from './accounts.ts';
|
|
11
|
+
export { accountsSource, cliSource, useAccounts, useModelPicker, type AccountHooksSource, type CliAccounts, type MemberAccounts, type ModelPickerSource, type UseAccounts, type UseModelPicker } from './useAccounts.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
export { phaseOf, stepOf } from
|
|
2
|
-
export { connectedWords, describeRoute, routeChoices, routeShort } from
|
|
3
|
-
export { useSignIn } from
|
|
4
|
-
export { stepsText, stepsView } from
|
|
5
|
-
export { consentWords, linkWords, pairErrorWords, pairingView, qrMatrix, qrText } from
|
|
6
|
-
export * from
|
|
7
|
-
export { useApprovals, useBlocked, useHerdrTree, useRun } from
|
|
8
|
-
export { connectStep, connectView, signInFor } from
|
|
9
|
-
export { useConnect } from
|
|
1
|
+
export { phaseOf, stepOf } from './phase.js';
|
|
2
|
+
export { connectedWords, describeRoute, routeChoices, routeShort } from './route.js';
|
|
3
|
+
export { useSignIn } from './useSignIn.js';
|
|
4
|
+
export { stepsText, stepsView } from './steps.js';
|
|
5
|
+
export { consentWords, linkWords, pairErrorWords, pairingView, qrMatrix, qrText } from './link.js';
|
|
6
|
+
export * from './kits.js';
|
|
7
|
+
export { useApprovals, useBlocked, useHerdrTree, useRun } from './useKits.js';
|
|
8
|
+
export { connectStep, connectView, signInFor } from './connect.js';
|
|
9
|
+
export { useConnect } from './useConnect.js';
|
|
10
|
+
export { accountRows, nameSuggestions, rowsOf } from './accounts.js';
|
|
11
|
+
export { accountsSource, cliSource, useAccounts, useModelPicker } from './useAccounts.js';
|
package/dist/kits.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// View state for the runtime kits, with no React: reducers, views and live stores any UI can draw from (plain DOM,
|
|
2
2
|
// React, React Native). The hooks in `@byokit/ui` read the same stores.
|
|
3
|
-
export { RUN_IDLE, runStep, runStore, runView } from
|
|
4
|
-
export { approvalsStep, approvalsStore, approvalWords } from
|
|
5
|
-
export { HERDR_EMPTY, agentIn, blockedView, herdrStep, herdrStore, herdrTreeView } from
|
|
6
|
-
export { keyStep, keyView } from
|
|
7
|
-
export { livePanelView, signInSheetView, signInsStore } from
|
|
3
|
+
export { RUN_IDLE, runStep, runStore, runView } from './run.js';
|
|
4
|
+
export { approvalsStep, approvalsStore, approvalWords } from './approvals.js';
|
|
5
|
+
export { HERDR_EMPTY, agentIn, blockedView, herdrStep, herdrStore, herdrTreeView } from './herdr.js';
|
|
6
|
+
export { keyStep, keyView } from './key.js';
|
|
7
|
+
export { livePanelView, signInSheetView, signInsStore } from './browser.js';
|
package/dist/phase.d.ts
CHANGED
|
@@ -15,9 +15,9 @@ export type AccountView = {
|
|
|
15
15
|
signIn?: SignInView | null;
|
|
16
16
|
};
|
|
17
17
|
export declare function phaseOf(a: AccountView | null, { offline, cancelled, keepWork }?: {
|
|
18
|
-
offline?: boolean | undefined;
|
|
19
18
|
cancelled?: boolean | undefined;
|
|
20
19
|
keepWork?: boolean | undefined;
|
|
20
|
+
offline?: boolean | undefined;
|
|
21
21
|
}): Phase;
|
|
22
22
|
/** Where the three-step progress bar ("Open", "Say yes", "Done") stands. */
|
|
23
23
|
export declare const stepOf: (p: Phase) => 0 | 1 | 3;
|
package/dist/run.d.ts
CHANGED
package/dist/run.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// idle nothing sent yet running the reply is streaming in
|
|
4
4
|
// done the reply is complete stopped stopped before the end (here, or aborted on the computer)
|
|
5
5
|
// failed it ended with a problem: `words` says which, in the kit's own sentence
|
|
6
|
-
import { store } from
|
|
6
|
+
import { store } from './follow.js';
|
|
7
7
|
export const RUN_IDLE = { phase: 'idle', text: '', tools: [] };
|
|
8
8
|
export function runStep(s, a) {
|
|
9
9
|
if (a.type === 'start')
|
|
@@ -12,7 +12,8 @@ export function runStep(s, a) {
|
|
|
12
12
|
return s; // a finished run hears nothing more
|
|
13
13
|
switch (a.type) {
|
|
14
14
|
case 'started':
|
|
15
|
-
case 'thinking':
|
|
15
|
+
case 'thinking':
|
|
16
|
+
case 'account': return s;
|
|
16
17
|
case 'text': return { ...s, text: a.text };
|
|
17
18
|
case 'tool': {
|
|
18
19
|
if (a.phase === 'start')
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type Account, type AccountRow, type AccountsSource, type ModelInfo, type Pick, type PickerView, type Room, type RunSelection } from './accounts.ts';
|
|
2
|
+
/** What the hooks remove through: an `AccountsSource` plus the back end's own removal, when the app allows it. */
|
|
3
|
+
export type AccountHooksSource = AccountsSource & {
|
|
4
|
+
remove?: (id: string) => Promise<unknown>;
|
|
5
|
+
};
|
|
6
|
+
/** What the picker reads: the rows' source plus one model list per account and Auto's own pick. */
|
|
7
|
+
export type ModelPickerSource = AccountHooksSource & {
|
|
8
|
+
models: (account: Account) => Promise<readonly ModelInfo[]> | readonly ModelInfo[];
|
|
9
|
+
auto: () => Promise<Pick> | Pick;
|
|
10
|
+
};
|
|
11
|
+
/** A live `@byokit/accounts` member: `list()`/`models()`/`pick()`/`remove()` fit with one line per member. */
|
|
12
|
+
export type MemberAccounts<M> = {
|
|
13
|
+
list: (member: M) => Promise<readonly Account[]>;
|
|
14
|
+
models: (member: M, id: string) => Promise<readonly ModelInfo[]>;
|
|
15
|
+
pick: (member: M, sel: RunSelection, room?: (account: Account, demand: readonly string[]) => Room | Promise<Room>) => Promise<Pick>;
|
|
16
|
+
remove: (member: M, id: string) => Promise<unknown>;
|
|
17
|
+
};
|
|
18
|
+
/** A live `cliAccounts()` kit: `list()` fits with one line; `remove` when the app lets the person remove one. */
|
|
19
|
+
export type CliAccounts = {
|
|
20
|
+
list: () => Promise<readonly Account[]>;
|
|
21
|
+
remove?: (id: string) => Promise<unknown>;
|
|
22
|
+
};
|
|
23
|
+
/** One line per member: the rows' source (with the picker's models and Auto pick) over a live `Accounts`. */
|
|
24
|
+
export declare function accountsSource<M>(accounts: MemberAccounts<M>, member: M, room?: (account: Account) => Room | Promise<Room>): ModelPickerSource;
|
|
25
|
+
/** One line: the rows' source over a live `cliAccounts()` kit, with each provider's plain name on its rows. */
|
|
26
|
+
export declare function cliSource(cli: CliAccounts, room?: (account: Account) => Room | Promise<Room>): AccountHooksSource;
|
|
27
|
+
export type UseAccounts = {
|
|
28
|
+
rows: AccountRow[];
|
|
29
|
+
ready: boolean;
|
|
30
|
+
error: unknown;
|
|
31
|
+
refresh: () => Promise<void>;
|
|
32
|
+
remove: (id: string) => Promise<void>;
|
|
33
|
+
};
|
|
34
|
+
/** The rows live: `refresh()` re-reads the source; the caller's removal of an id sticks across refreshes. */
|
|
35
|
+
export declare function useAccounts(source: AccountHooksSource): UseAccounts;
|
|
36
|
+
export type UseModelPicker = {
|
|
37
|
+
rows: AccountRow[];
|
|
38
|
+
picker: PickerView | null;
|
|
39
|
+
selection: RunSelection;
|
|
40
|
+
ready: boolean;
|
|
41
|
+
error: unknown;
|
|
42
|
+
select: (sel: RunSelection) => void;
|
|
43
|
+
refresh: () => Promise<void>;
|
|
44
|
+
remove: (id: string) => Promise<void>;
|
|
45
|
+
};
|
|
46
|
+
/** The run picker live: `select()` marks the selection at once (kept across refreshes); `refresh()` re-reads
|
|
47
|
+
* accounts, models and Auto's pick together. `picker` is null until the first refresh resolves. */
|
|
48
|
+
export declare function useModelPicker(source: ModelPickerSource, initial?: RunSelection): UseModelPicker;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Live account rows and the run picker as React hooks over `./accounts.ts`. An app fills an `AccountsSource`
|
|
2
|
+
// (or the fuller `ModelPickerSource`) from its own back end — one line per member — and draws the rows its own
|
|
3
|
+
// way. A failing room leaves only its row unknown; a mid-refresh `remove()` still drops the row. No openclaw
|
|
4
|
+
// adapter yet (pending O14).
|
|
5
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
6
|
+
import { accountRows, modelPicker, rowsOf } from './accounts.js';
|
|
7
|
+
const UNKNOWN_ROOM = { left: 'unknown' };
|
|
8
|
+
const CLI_LABEL = { claude: 'Claude', codex: 'Codex', pi: 'Pi' };
|
|
9
|
+
/** One line per member: the rows' source (with the picker's models and Auto pick) over a live `Accounts`. */
|
|
10
|
+
export function accountsSource(accounts, member, room = () => UNKNOWN_ROOM) {
|
|
11
|
+
return {
|
|
12
|
+
accounts: () => accounts.list(member),
|
|
13
|
+
room,
|
|
14
|
+
models: (a) => accounts.models(member, a.id),
|
|
15
|
+
auto: () => accounts.pick(member, { account: 'auto' }, room),
|
|
16
|
+
remove: (id) => accounts.remove(member, id),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** One line: the rows' source over a live `cliAccounts()` kit, with each provider's plain name on its rows. */
|
|
20
|
+
export function cliSource(cli, room = () => UNKNOWN_ROOM) {
|
|
21
|
+
const remove = cli.remove;
|
|
22
|
+
return {
|
|
23
|
+
accounts: async () => (await cli.list()).map((c) => ({ ...c, label: CLI_LABEL[c.provider] ?? c.provider })),
|
|
24
|
+
room,
|
|
25
|
+
...(remove ? { remove: (id) => remove(id) } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** The rows live: `refresh()` re-reads the source; the caller's removal of an id sticks across refreshes. */
|
|
29
|
+
export function useAccounts(source) {
|
|
30
|
+
const [rows, setRows] = useState([]);
|
|
31
|
+
const [ready, setReady] = useState(false);
|
|
32
|
+
const [error, setError] = useState(undefined);
|
|
33
|
+
const removed = useRef(new Set());
|
|
34
|
+
const gen = useRef(0);
|
|
35
|
+
const refresh = useCallback(async () => {
|
|
36
|
+
const g = ++gen.current;
|
|
37
|
+
let next;
|
|
38
|
+
try {
|
|
39
|
+
next = await rowsOf(source, Date.now());
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
if (gen.current === g)
|
|
43
|
+
setError(e);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (gen.current !== g)
|
|
47
|
+
return;
|
|
48
|
+
setRows(next.filter((r) => !removed.current.has(r.id)));
|
|
49
|
+
setError(undefined);
|
|
50
|
+
setReady(true);
|
|
51
|
+
}, [source]);
|
|
52
|
+
useEffect(() => { void refresh(); }, [refresh]);
|
|
53
|
+
const remove = useCallback(async (id) => {
|
|
54
|
+
removed.current.add(id);
|
|
55
|
+
gen.current++;
|
|
56
|
+
setRows((prev) => prev.filter((r) => r.id !== id));
|
|
57
|
+
try {
|
|
58
|
+
await source.remove?.(id);
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
removed.current.delete(id);
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
await refresh();
|
|
65
|
+
}, [source, refresh]);
|
|
66
|
+
return { rows, ready, error, refresh, remove };
|
|
67
|
+
}
|
|
68
|
+
/** The run picker live: `select()` marks the selection at once (kept across refreshes); `refresh()` re-reads
|
|
69
|
+
* accounts, models and Auto's pick together. `picker` is null until the first refresh resolves. */
|
|
70
|
+
export function useModelPicker(source, initial = { account: 'auto' }) {
|
|
71
|
+
const [rows, setRows] = useState([]);
|
|
72
|
+
const [picker, setPicker] = useState(null);
|
|
73
|
+
const [selection, setSelection] = useState(initial);
|
|
74
|
+
const [ready, setReady] = useState(false);
|
|
75
|
+
const [error, setError] = useState(undefined);
|
|
76
|
+
const sel = useRef(initial);
|
|
77
|
+
const cache = useRef(null);
|
|
78
|
+
const removed = useRef(new Set());
|
|
79
|
+
const gen = useRef(0);
|
|
80
|
+
const refresh = useCallback(async () => {
|
|
81
|
+
const g = ++gen.current;
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
let list;
|
|
84
|
+
try {
|
|
85
|
+
list = [...(await source.accounts())];
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
if (gen.current === g)
|
|
89
|
+
setError(e);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const live = list.filter((a) => !removed.current.has(a.id));
|
|
93
|
+
const rooms = new Map();
|
|
94
|
+
for (const a of live) {
|
|
95
|
+
try {
|
|
96
|
+
rooms.set(a.id, (await source.room(a)) ?? UNKNOWN_ROOM);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
rooms.set(a.id, UNKNOWN_ROOM);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const models = {};
|
|
103
|
+
for (const a of live) {
|
|
104
|
+
try {
|
|
105
|
+
models[a.id] = await source.models(a);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
models[a.id] = [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
let auto;
|
|
112
|
+
try {
|
|
113
|
+
auto = await source.auto();
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
if (gen.current === g)
|
|
117
|
+
setError(e);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (gen.current !== g)
|
|
121
|
+
return;
|
|
122
|
+
cache.current = { accounts: live, models, auto };
|
|
123
|
+
setRows(accountRows(live, (a) => rooms.get(a.id) ?? UNKNOWN_ROOM, now).filter((r) => !removed.current.has(r.id)));
|
|
124
|
+
setPicker(modelPicker(live, models, sel.current, auto, now));
|
|
125
|
+
setError(undefined);
|
|
126
|
+
setReady(true);
|
|
127
|
+
}, [source]);
|
|
128
|
+
useEffect(() => { void refresh(); }, [refresh]);
|
|
129
|
+
const select = useCallback((next) => {
|
|
130
|
+
sel.current = next;
|
|
131
|
+
setSelection(next);
|
|
132
|
+
const c = cache.current;
|
|
133
|
+
if (c)
|
|
134
|
+
setPicker(modelPicker(c.accounts, c.models, next, c.auto, Date.now()));
|
|
135
|
+
}, []);
|
|
136
|
+
const remove = useCallback(async (id) => {
|
|
137
|
+
removed.current.add(id);
|
|
138
|
+
gen.current++;
|
|
139
|
+
setRows((prev) => prev.filter((r) => r.id !== id));
|
|
140
|
+
const c = cache.current;
|
|
141
|
+
if (c) {
|
|
142
|
+
const accounts = c.accounts.filter((a) => a.id !== id);
|
|
143
|
+
cache.current = { ...c, accounts };
|
|
144
|
+
setPicker(modelPicker(accounts, c.models, sel.current, c.auto, Date.now()));
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
await source.remove?.(id);
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
removed.current.delete(id);
|
|
151
|
+
throw e;
|
|
152
|
+
}
|
|
153
|
+
await refresh();
|
|
154
|
+
}, [source, refresh]);
|
|
155
|
+
return { rows, picker, selection, ready, error, select, refresh, remove };
|
|
156
|
+
}
|
package/dist/useConnect.d.ts
CHANGED
|
@@ -4,15 +4,15 @@ export type UseConnect = UseConnectSignIn & {
|
|
|
4
4
|
words?: ConnectWords;
|
|
5
5
|
};
|
|
6
6
|
export declare function useConnect({ routes, words, ...o }: UseConnect): {
|
|
7
|
+
groups: import("./connect.ts").ConnectGroup[];
|
|
8
|
+
chosen?: {
|
|
9
|
+
row: import("./connect.ts").ConnectRow;
|
|
10
|
+
key?: ReturnType<typeof import("./key.ts").keyView>;
|
|
11
|
+
};
|
|
7
12
|
step: ConnectStep;
|
|
8
13
|
act: (a: ConnectAction) => void;
|
|
9
14
|
pick: (key: string) => void;
|
|
10
15
|
back: () => void;
|
|
11
16
|
/** Pass to `useSignIn` in a sheet keyed by `chosen.row.key`; undefined unless a ready sign-in row is chosen. */
|
|
12
17
|
signIn: import("./useSignIn.ts").UseSignIn | undefined;
|
|
13
|
-
groups: import("./connect.ts").ConnectGroup[];
|
|
14
|
-
chosen?: {
|
|
15
|
-
row: import("./connect.ts").ConnectRow;
|
|
16
|
-
key?: ReturnType<typeof import("./key.ts").keyView>;
|
|
17
|
-
};
|
|
18
18
|
};
|
package/dist/useConnect.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// the chosen sign-in row's options to `useSignIn`. Keys never enter it: the app sends the input to its kit and
|
|
3
3
|
// reports only the result.
|
|
4
4
|
import { useState } from 'react';
|
|
5
|
-
import { connectStep, connectView, signInFor } from
|
|
5
|
+
import { connectStep, connectView, signInFor } from './connect.js';
|
|
6
6
|
export function useConnect({ routes, words, ...o }) {
|
|
7
7
|
const [step, setStep] = useState({ at: 'list' });
|
|
8
8
|
const view = connectView(routes, step, words);
|
package/dist/useKits.d.ts
CHANGED
|
@@ -3,10 +3,6 @@ import { type HerdrSource } from './herdr.ts';
|
|
|
3
3
|
import { type RunSource, type RunWordsOptions } from './run.ts';
|
|
4
4
|
/** One run at a time: `send(message)` streams the reply in; `words` says how a failed run ended. */
|
|
5
5
|
export declare function useRun(source: RunSource, o: RunWordsOptions): {
|
|
6
|
-
send: (message: string, o?: {
|
|
7
|
-
sessionKey?: string;
|
|
8
|
-
}) => void;
|
|
9
|
-
stop: () => void;
|
|
10
6
|
phase: import("./run.ts").RunPhase;
|
|
11
7
|
text: string;
|
|
12
8
|
tools: {
|
|
@@ -15,6 +11,10 @@ export declare function useRun(source: RunSource, o: RunWordsOptions): {
|
|
|
15
11
|
}[];
|
|
16
12
|
tool: string | undefined;
|
|
17
13
|
words: string;
|
|
14
|
+
send: (message: string, o?: {
|
|
15
|
+
sessionKey?: string;
|
|
16
|
+
}) => void;
|
|
17
|
+
stop: () => void;
|
|
18
18
|
};
|
|
19
19
|
/** The approvals waiting for a yes, live; `decide` answers one (it leaves the list when the computer says so). */
|
|
20
20
|
export declare function useApprovals(source: ApprovalsSource & {
|
|
@@ -49,7 +49,6 @@ export declare function useBlocked(source: HerdrSource & {
|
|
|
49
49
|
answer(paneId: string, keys: string[], revision: number): Promise<void>;
|
|
50
50
|
}): {
|
|
51
51
|
blocked: {
|
|
52
|
-
name: string;
|
|
53
52
|
paneId: string;
|
|
54
53
|
workspaceId: string;
|
|
55
54
|
tabId: string;
|
|
@@ -57,6 +56,7 @@ export declare function useBlocked(source: HerdrSource & {
|
|
|
57
56
|
revision: number;
|
|
58
57
|
prompt: string;
|
|
59
58
|
since: number;
|
|
59
|
+
name: string;
|
|
60
60
|
}[];
|
|
61
61
|
answer: (paneId: string, keys: string[], revision: number) => Promise<void>;
|
|
62
62
|
};
|
package/dist/useKits.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// framework-free stores from `@byokit/ui/kits`; pass the same device client each render (e.g. from useMemo),
|
|
3
3
|
// since a new one starts following afresh.
|
|
4
4
|
import { useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
5
|
-
import { approvalsStore } from
|
|
6
|
-
import { agentIn, blockedView, herdrStore, herdrTreeView } from
|
|
7
|
-
import { runStore, runView } from
|
|
5
|
+
import { approvalsStore } from './approvals.js';
|
|
6
|
+
import { agentIn, blockedView, herdrStore, herdrTreeView } from './herdr.js';
|
|
7
|
+
import { runStore, runView } from './run.js';
|
|
8
8
|
const useStore = (s) => useSyncExternalStore(s.subscribe, s.get, s.get);
|
|
9
9
|
/** One run at a time: `send(message)` streams the reply in; `words` says how a failed run ended. */
|
|
10
10
|
export function useRun(source, o) {
|
package/dist/useSignIn.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export declare function useSignIn({ read, start, cancel, offline: isOffline, ms,
|
|
|
21
21
|
url: string | undefined;
|
|
22
22
|
/** Start again: `{ via: 'code' }` for "Having trouble?", `{ fresh: true }` to pick the account again. */
|
|
23
23
|
start: (body?: {
|
|
24
|
-
via?:
|
|
24
|
+
via?: 'code';
|
|
25
25
|
fresh?: boolean;
|
|
26
26
|
}) => void;
|
|
27
27
|
cancel: () => void;
|
package/dist/useSignIn.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// reports the phase to draw. The app keeps its own look (and its own tab for the provider's page); this is the
|
|
3
3
|
// behaviour every sign-in sheet shares.
|
|
4
4
|
import { useEffect, useState } from 'react';
|
|
5
|
-
import { phaseOf } from
|
|
5
|
+
import { phaseOf } from './phase.js';
|
|
6
6
|
export function useSignIn({ read, start, cancel, offline: isOffline = () => true, ms = 1500, pinned }) {
|
|
7
7
|
const [value, setValue] = useState(null);
|
|
8
8
|
const [offline, setOffline] = useState(false);
|
package/dist/words.json
CHANGED
|
@@ -44,5 +44,31 @@
|
|
|
44
44
|
"key.checking": "Checking your key…",
|
|
45
45
|
"key.ok": "Your key is ready. It is used only when you choose this option.",
|
|
46
46
|
"key.invalid": "This key could not be saved. Check it and try again.",
|
|
47
|
-
"key.notIncluded": "This option is not available here."
|
|
47
|
+
"key.notIncluded": "This option is not available here.",
|
|
48
|
+
"account.signing": "Checking…",
|
|
49
|
+
"account.resting": "Cooling down",
|
|
50
|
+
"account.signed_out": "Not signed in",
|
|
51
|
+
"account.needs_again": "Sign in again",
|
|
52
|
+
"account.not_included": "Not in your plan",
|
|
53
|
+
"account.room.unknown": "Room left unknown",
|
|
54
|
+
"account.room.session": "{left} left this session",
|
|
55
|
+
"account.room.week": "{left} left this week",
|
|
56
|
+
"account.room.month": "{left} left this month",
|
|
57
|
+
"account.room.tightest": "{left} left for now",
|
|
58
|
+
"account.stale": "{age} (stale)",
|
|
59
|
+
"account.room.resetSince": "Reset since this reading",
|
|
60
|
+
"account.window.resetSince": "{label}: reset since this reading",
|
|
61
|
+
"account.window.session": "Session",
|
|
62
|
+
"account.window.weekly": "Week",
|
|
63
|
+
"account.window.monthly": "Month",
|
|
64
|
+
"account.window.rolling": "Rolling window",
|
|
65
|
+
"account.window.custom": "Limit",
|
|
66
|
+
"picker.auto": "Auto — most room left",
|
|
67
|
+
"picker.out.state": "Sign in first",
|
|
68
|
+
"picker.out.resting": "Taking a break",
|
|
69
|
+
"picker.out.billing": "Pay per use, so picked only when you choose it",
|
|
70
|
+
"picker.out.model": "Doesn't include this run's model",
|
|
71
|
+
"picker.out.provider": "A different service",
|
|
72
|
+
"move.to": "Move to {name}",
|
|
73
|
+
"move.failed": "Couldn't move to {name}"
|
|
48
74
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Headless sign-in, pairing and runtime-kit view state for any UI, plus plain-words copy.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
"./connect": {
|
|
41
41
|
"types": "./dist/connect.d.ts",
|
|
42
42
|
"default": "./dist/connect.js"
|
|
43
|
+
},
|
|
44
|
+
"./accounts": {
|
|
45
|
+
"types": "./dist/accounts.d.ts",
|
|
46
|
+
"default": "./dist/accounts.js"
|
|
43
47
|
}
|
|
44
48
|
},
|
|
45
49
|
"files": [
|
|
@@ -58,8 +62,11 @@
|
|
|
58
62
|
}
|
|
59
63
|
},
|
|
60
64
|
"devDependencies": {
|
|
65
|
+
"@byokit/accounts": "0.24.1",
|
|
61
66
|
"@types/react": "^19.3.0",
|
|
62
|
-
"react": "^19.3.0"
|
|
67
|
+
"@types/react-test-renderer": "^19.3.0",
|
|
68
|
+
"react": "^19.3.0",
|
|
69
|
+
"react-test-renderer": "^19.3.0"
|
|
63
70
|
},
|
|
64
71
|
"publishConfig": {
|
|
65
72
|
"access": "public"
|