@byokit/ui 0.0.0-stage → 0.7.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.
@@ -0,0 +1,92 @@
1
+ import WORDS from './words.json';
2
+ import { keyView, type KeyAction, type KeyState } from './key.ts';
3
+ import type { UseSignIn } from './useSignIn.ts';
4
+ export type Billing = 'subscription' | 'api' | 'local' | 'free' | 'unknown';
5
+ export type Via = 'browser' | 'code' | 'paste' | 'key' | 'session' | 'setup_token' | 'cli' | 'plan_key' | 'cloud' | 'local' | 'endpoint';
6
+ export type Readiness = 'ready' | 'needs_binary' | 'needs_plugin' | 'needs_host' | 'needs_client' | 'unsupported_platform' | 'no_upstream_flow';
7
+ /** One route as any kit's `routes()` lists it, restated structurally (D3): `@byokit/accounts` and
8
+ * `@byokit/openclaw` route views fit as they are. Only these fields are read, so nothing else (a credential
9
+ * included) can reach the view. */
10
+ export type ConnectRoute = {
11
+ /** openclaw's is optional; a row without one is keyed `<provider>:<via>`. */
12
+ id?: string;
13
+ provider: string;
14
+ name?: string;
15
+ company?: string;
16
+ via: Via;
17
+ billing: Billing;
18
+ /** accounts: `'default' | 'explicit'`; openclaw: `offerPolicy` plus its legacy ready-only boolean `offer`. */
19
+ offer?: 'default' | 'explicit' | boolean;
20
+ offerPolicy?: 'default' | 'explicit';
21
+ /** openclaw: `services` marks speech, search, media and other non-chat rows. */
22
+ group?: 'models' | 'services';
23
+ upstream?: {
24
+ surface: 'accounts' | 'openclaw' | 'herdr';
25
+ };
26
+ readiness: Readiness;
27
+ };
28
+ export type ConnectGroupId = 'plans' | 'perUse' | 'local' | 'server' | 'cloud' | 'other' | 'services';
29
+ /** What choosing a row leads to: a sign-in sheet (`useSignIn`), a key card (`keyView`), the app's own setup form
30
+ * (cloud account, local model, server address and its billing), or the reason it cannot be used here. */
31
+ export type ConnectDoes = 'signin' | 'key' | 'setup' | 'unavailable';
32
+ export type ConnectRow = {
33
+ /** Unique across kits: `<surface>/<id>`. */
34
+ key: string;
35
+ id: string;
36
+ provider: string;
37
+ surface?: 'accounts' | 'openclaw' | 'herdr';
38
+ name: string;
39
+ company: string;
40
+ via: Via;
41
+ billing: Billing;
42
+ group: ConnectGroupId;
43
+ /** Offered by default: a ready subscription row. Never true for any other billing. */
44
+ offered: boolean;
45
+ ready: boolean;
46
+ readiness: Readiness;
47
+ does: ConnectDoes;
48
+ /** Plain words: how it is paid for, how you sign in, and whether it works here. */
49
+ billingWords: string;
50
+ method: string;
51
+ status: string;
52
+ };
53
+ export type ConnectGroup = {
54
+ id: ConnectGroupId;
55
+ title: string;
56
+ note: string;
57
+ rows: ConnectRow[];
58
+ };
59
+ export type ConnectStep = {
60
+ at: 'list';
61
+ } | {
62
+ at: 'route';
63
+ key: string;
64
+ entry: KeyState;
65
+ };
66
+ export type ConnectAction = {
67
+ type: 'pick';
68
+ key: string;
69
+ } | {
70
+ type: 'back';
71
+ } | KeyAction;
72
+ export type ConnectView = {
73
+ groups: ConnectGroup[];
74
+ /** The picked row and, for a key route, its key card. Undefined on the list or when the row is gone. */
75
+ chosen?: {
76
+ row: ConnectRow;
77
+ key?: ReturnType<typeof keyView>;
78
+ };
79
+ };
80
+ export type ConnectWords = typeof WORDS;
81
+ export declare function connectStep(step: ConnectStep, action: ConnectAction): ConnectStep;
82
+ /** Every route, grouped in a fixed order (empty groups left out), rows in the kits' own order. */
83
+ export declare function connectView(routes: readonly ConnectRoute[], step?: ConnectStep, words?: ConnectWords): ConnectView;
84
+ /** What the app does for the picked row, given that row; credentials go straight from the app to its kit. */
85
+ export type UseConnectSignIn = Omit<UseSignIn, 'read' | 'start' | 'cancel'> & {
86
+ read: (row: ConnectRow) => ReturnType<UseSignIn['read']>;
87
+ start?: (row: ConnectRow, body?: Parameters<NonNullable<UseSignIn['start']>>[0]) => Promise<unknown>;
88
+ cancel: (row: ConnectRow) => Promise<unknown>;
89
+ };
90
+ /** `useSignIn`'s options for the chosen sign-in row, or undefined for any other step. Draw the sheet as its own
91
+ * component keyed by `chosen.row.key` so a new pick starts with fresh sign-in state. */
92
+ export declare function signInFor(view: ConnectView, o: UseConnectSignIn): UseSignIn | undefined;
@@ -0,0 +1,74 @@
1
+ // "Connect an AI account" as headless view state: every route the kits list (D18), grouped by how the person pays,
2
+ // with plain words for the billing, the method and whether it works here. Plans come first and are the only rows
3
+ // offered by default; every other row is listed and used only when the person picks it. Nothing here names a
4
+ // provider: groups and words come from each row's billing, method and readiness alone. Framework-free.
5
+ import WORDS from './words.json' with { type: 'json' };
6
+ import { keyStep, keyView } from "./key.js";
7
+ const ORDER = ['plans', 'perUse', 'local', 'server', 'cloud', 'other', 'services'];
8
+ const SIGNIN = ['browser', 'code', 'paste', 'session', 'cli'];
9
+ const KEY = ['key', 'plan_key', 'setup_token'];
10
+ const fill = (s, r) => s.replace(/\{(name|company)\}/g, (_, k) => r[k]);
11
+ /** Cloud, server and local methods are always explicit and go to their own groups; then billing decides: only a
12
+ * subscription row named default is a plan, so an API, free or unknown row never lands in Plans whatever its
13
+ * `offer` says. */
14
+ function groupOf(r) {
15
+ if (r.group === 'services')
16
+ return 'services';
17
+ if (r.via === 'cloud')
18
+ return 'cloud';
19
+ if (r.via === 'endpoint')
20
+ return 'server';
21
+ if (r.via === 'local')
22
+ return 'local';
23
+ const byDefault = r.offer === 'default' || r.offerPolicy === 'default' || r.offer === true;
24
+ if (r.billing === 'subscription' && byDefault)
25
+ return 'plans';
26
+ if (r.billing === 'local')
27
+ return 'local';
28
+ if (r.billing === 'api')
29
+ return 'perUse';
30
+ return 'other';
31
+ }
32
+ function rowOf(r, w) {
33
+ const name = r.name || r.provider, company = r.company || name, id = r.id ?? `${r.provider}:${r.via}`;
34
+ const group = groupOf(r);
35
+ const ready = r.readiness === 'ready';
36
+ const billing = r.via === 'endpoint' ? w['billing.server'] : r.via === 'local' ? w['billing.local'] : w[`billing.${r.billing}`];
37
+ return {
38
+ key: `${r.upstream?.surface ?? ''}/${id}`, id, provider: r.provider,
39
+ ...(r.upstream ? { surface: r.upstream.surface } : {}),
40
+ name, company, via: r.via, billing: r.billing, group,
41
+ offered: group === 'plans' && ready, ready, readiness: r.readiness,
42
+ does: !ready ? 'unavailable' : SIGNIN.includes(r.via) ? 'signin' : KEY.includes(r.via) ? 'key' : 'setup',
43
+ billingWords: fill(billing, { name, company }), method: w[`via.${r.via}`], status: w[`ready.${r.readiness}`],
44
+ };
45
+ }
46
+ export function connectStep(step, action) {
47
+ if (action.type === 'pick')
48
+ return { at: 'route', key: action.key, entry: 'entry' };
49
+ if (action.type === 'back')
50
+ return { at: 'list' };
51
+ return step.at === 'route' ? { ...step, entry: keyStep(step.entry, action) } : step;
52
+ }
53
+ /** Every route, grouped in a fixed order (empty groups left out), rows in the kits' own order. */
54
+ export function connectView(routes, step = { at: 'list' }, words = WORDS) {
55
+ const rows = routes.map((r) => rowOf(r, words));
56
+ const groups = ORDER.map((id) => ({ id, title: words[`connect.${id}`], note: words[`connect.${id}.note`], rows: rows.filter((r) => r.group === id) }))
57
+ .filter((g) => g.rows.length);
58
+ const row = step.at === 'route' ? rows.find((r) => r.key === step.key) : undefined;
59
+ if (!row || step.at !== 'route')
60
+ return { groups };
61
+ if (row.does !== 'key')
62
+ return { groups, chosen: { row } };
63
+ const label = words[row.group === 'plans' ? 'key.label.plan' : row.billing === 'api' ? 'key.label.perUse' : 'key.label.other'];
64
+ return { groups, chosen: { row, key: keyView(step.entry, (k) => (k === 'key.label' ? label : words[k])) } };
65
+ }
66
+ /** `useSignIn`'s options for the chosen sign-in row, or undefined for any other step. Draw the sheet as its own
67
+ * component keyed by `chosen.row.key` so a new pick starts with fresh sign-in state. */
68
+ export function signInFor(view, o) {
69
+ const row = view.chosen?.row;
70
+ if (row?.does !== 'signin')
71
+ return undefined;
72
+ const { read, start, cancel, ...rest } = o;
73
+ return { ...rest, read: () => read(row), cancel: () => cancel(row), ...(start ? { start: (body) => start(row, body) } : {}) };
74
+ }
@@ -0,0 +1,17 @@
1
+ export type Stop = () => void;
2
+ /** A view any UI can draw from: the current state, and a way to hear each change (the returned function stops). */
3
+ export type Store<T> = {
4
+ get(): T;
5
+ subscribe(fn: (state: T) => void): () => void;
6
+ };
7
+ type Live = {
8
+ hold(it: AsyncIterator<unknown>): void;
9
+ stopped(): boolean;
10
+ ok(): void;
11
+ };
12
+ export declare function retrying(attempt: (live: Live) => Promise<void>, retryMs: number): Stop;
13
+ /** A store that follows its source only while someone listens: the first listener starts it, the last one stops it. */
14
+ export declare function store<T>(initial: T, follow: (set: (state: T) => void) => Stop): Store<T> & {
15
+ set(state: T): void;
16
+ };
17
+ export {};
package/dist/follow.js ADDED
@@ -0,0 +1,72 @@
1
+ // Keeping a live view going over a link: a view's event stream ends whenever the link drops or the host restarts, so
2
+ // it opens again after a pause until the view stops or the pairing is gone for good. Framework-free.
3
+ // Only a removed pairing never comes back; a stopped or refused link comes back with its `retry()`.
4
+ const final = (e) => typeof e === 'object' && e !== null && e.code === 'removed';
5
+ // ponytail: the pause doubles from 250 ms up to `retryMs` while tries fail, and starts short again once one works;
6
+ // the link paces its own reconnects, so a stream only reopens into a link that is either back (it works, soon after
7
+ // the link does) or still away (it fails at once, cheaply).
8
+ export function retrying(attempt, retryMs) {
9
+ let stopped = false;
10
+ let pause = 0;
11
+ let current;
12
+ let timer;
13
+ let wake;
14
+ const live = {
15
+ hold: (it) => { current = it; if (stopped)
16
+ void it.return?.(); },
17
+ stopped: () => stopped,
18
+ ok: () => { pause = 0; },
19
+ };
20
+ void (async () => {
21
+ while (!stopped) {
22
+ try {
23
+ await attempt(live);
24
+ }
25
+ catch (e) {
26
+ if (final(e))
27
+ return;
28
+ }
29
+ current = undefined;
30
+ if (stopped)
31
+ return;
32
+ pause = Math.min(pause ? pause * 2 : 250, retryMs);
33
+ await new Promise((resolve) => { wake = resolve; timer = setTimeout(resolve, pause); });
34
+ }
35
+ })();
36
+ return () => {
37
+ if (stopped)
38
+ return;
39
+ stopped = true;
40
+ clearTimeout(timer);
41
+ wake?.();
42
+ void current?.return?.();
43
+ };
44
+ }
45
+ /** A store that follows its source only while someone listens: the first listener starts it, the last one stops it. */
46
+ export function store(initial, follow) {
47
+ let state = initial;
48
+ let stop;
49
+ const listeners = new Set();
50
+ const set = (next) => {
51
+ if (next === state)
52
+ return;
53
+ state = next;
54
+ for (const fn of [...listeners])
55
+ fn(state);
56
+ };
57
+ return {
58
+ get: () => state,
59
+ set,
60
+ subscribe: (fn) => {
61
+ listeners.add(fn);
62
+ stop ??= follow(set);
63
+ return () => {
64
+ if (!listeners.delete(fn) || listeners.size > 0)
65
+ return;
66
+ const s = stop;
67
+ stop = undefined;
68
+ s?.();
69
+ };
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,103 @@
1
+ import { type Store } from './follow.ts';
2
+ export type AgentStatus = 'idle' | 'working' | 'blocked' | 'done' | 'unknown';
3
+ export type HerdrAgent = {
4
+ kind?: string;
5
+ name?: string;
6
+ status: AgentStatus;
7
+ revision: number;
8
+ launchPending?: boolean;
9
+ interactiveReady?: boolean;
10
+ };
11
+ /** The tree as the Herdr kit reports it. */
12
+ export type HerdrTree = {
13
+ connected: boolean;
14
+ workspaces: {
15
+ id: string;
16
+ label: string;
17
+ tabs: {
18
+ id: string;
19
+ label: string;
20
+ panes: {
21
+ id: string;
22
+ cwd?: string;
23
+ agent?: HerdrAgent;
24
+ }[];
25
+ }[];
26
+ }[];
27
+ };
28
+ /** An agent waiting for an answer, as the Herdr kit reports it; `revision` goes back with the answer. */
29
+ export type BlockedAgent = {
30
+ paneId: string;
31
+ workspaceId: string;
32
+ tabId: string;
33
+ kind?: string;
34
+ revision: number;
35
+ prompt: string;
36
+ since: number;
37
+ };
38
+ /** One `hd.events` frame. */
39
+ export type HerdrFrame = {
40
+ type: 'snapshot';
41
+ snapshot: HerdrTree;
42
+ } | {
43
+ type: 'blocked';
44
+ change: 'added' | 'resolved';
45
+ blocked: BlockedAgent;
46
+ } | {
47
+ type: 'raw';
48
+ line: string;
49
+ };
50
+ export type HerdrAction = HerdrFrame | {
51
+ type: 'listed';
52
+ blocked: BlockedAgent[];
53
+ };
54
+ /** `tree` is null until the computer first says what it has. */
55
+ export type HerdrState = {
56
+ tree: HerdrTree | null;
57
+ blocked: BlockedAgent[];
58
+ };
59
+ export declare const HERDR_EMPTY: HerdrState;
60
+ export declare function herdrStep(s: HerdrState, a: HerdrAction): HerdrState;
61
+ /** An agent as a list row: its status is what the kit's `agentWords` takes ('starting' while it launches). */
62
+ export type AgentRow = {
63
+ paneId: string;
64
+ name: string;
65
+ kind?: string;
66
+ status: AgentStatus | 'starting';
67
+ agent: HerdrAgent;
68
+ };
69
+ /** The agents grouped by where they run ("project · tab"), tabs without an agent left out. `project` is the
70
+ * folder's own name alone, for rows that name the agent without the tab's internal label. */
71
+ export declare function herdrTreeView(tree: HerdrTree | null): {
72
+ workspaceId: string;
73
+ tabId: string;
74
+ project: string;
75
+ where: string;
76
+ agents: AgentRow[];
77
+ }[];
78
+ /** The agent in one pane, if that pane has one. */
79
+ export declare function agentIn(tree: HerdrTree | null, paneId: string | undefined): AgentRow | undefined;
80
+ /** The waiting agents, each with the name to show for it. */
81
+ export declare const blockedView: (s: HerdrState) => {
82
+ name: string;
83
+ paneId: string;
84
+ workspaceId: string;
85
+ tabId: string;
86
+ kind?: string;
87
+ revision: number;
88
+ prompt: string;
89
+ since: number;
90
+ }[];
91
+ /** What `herdrStore` needs from a device client: `herdrDevice(link)` fits. */
92
+ export type HerdrSource = {
93
+ blocked(): Promise<BlockedAgent[]>;
94
+ events(): AsyncIterable<HerdrFrame>;
95
+ };
96
+ /**
97
+ * The tree and the waiting agents, live from one `hd.events` stream per device however many views watch it: the
98
+ * tree from its frames, the waiting list read once the stream is open and then kept by its frames. Opened again
99
+ * after the link comes back.
100
+ */
101
+ export declare function herdrStore(source: HerdrSource, { retryMs }?: {
102
+ retryMs?: number;
103
+ }): Store<HerdrState>;
package/dist/herdr.js ADDED
@@ -0,0 +1,88 @@
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
+ // for an answer, kept current from one `hd.events` stream. Framework-free; typed structurally against
3
+ // `@byokit/herdr/device`, so nothing here imports the kit.
4
+ import { retrying, store } from "./follow.js";
5
+ export const HERDR_EMPTY = { tree: null, blocked: [] };
6
+ export function herdrStep(s, a) {
7
+ if (a.type === 'snapshot') {
8
+ // A still-waiting agent can move on to a new revision with no question frame; the tree carries it, and the
9
+ // answer must go back with the current one.
10
+ const blocked = s.blocked.map((b) => {
11
+ const revision = agentIn(a.snapshot, b.paneId)?.agent.revision;
12
+ return revision === undefined || revision === b.revision ? b : { ...b, revision };
13
+ });
14
+ return { tree: a.snapshot, blocked: blocked.some((b, i) => b !== s.blocked[i]) ? blocked : s.blocked };
15
+ }
16
+ if (a.type === 'listed')
17
+ return { ...s, blocked: a.blocked };
18
+ if (a.type !== 'blocked')
19
+ return s;
20
+ const rest = s.blocked.filter((b) => b.paneId !== a.blocked.paneId);
21
+ if (a.change === 'resolved')
22
+ return rest.length === s.blocked.length ? s : { ...s, blocked: rest };
23
+ const at = s.blocked.findIndex((b) => b.paneId === a.blocked.paneId);
24
+ return { ...s, blocked: at < 0 ? [...s.blocked, a.blocked] : s.blocked.map((b, i) => (i === at ? a.blocked : b)) };
25
+ }
26
+ const row = (paneId, agent) => ({
27
+ paneId, name: agent.name ?? agent.kind ?? 'Agent', kind: agent.kind,
28
+ status: agent.launchPending ? 'starting' : agent.status, agent,
29
+ });
30
+ // A workspace labelled with its folder reads as the folder's own name.
31
+ const place = (label) => label.split('/').filter(Boolean).pop() ?? label;
32
+ /** The agents grouped by where they run ("project · tab"), tabs without an agent left out. `project` is the
33
+ * folder's own name alone, for rows that name the agent without the tab's internal label. */
34
+ export function herdrTreeView(tree) {
35
+ return (tree?.workspaces ?? []).flatMap((w) => w.tabs.map((t) => ({
36
+ workspaceId: w.id, tabId: t.id, project: place(w.label), where: `${place(w.label)} · ${t.label}`,
37
+ agents: t.panes.flatMap((p) => (p.agent ? [row(p.id, p.agent)] : [])),
38
+ }))).filter((g) => g.agents.length > 0);
39
+ }
40
+ /** The agent in one pane, if that pane has one. */
41
+ export function agentIn(tree, paneId) {
42
+ for (const w of tree?.workspaces ?? [])
43
+ for (const t of w.tabs)
44
+ for (const p of t.panes) {
45
+ if (p.id === paneId)
46
+ return p.agent ? row(p.id, p.agent) : undefined;
47
+ }
48
+ return undefined;
49
+ }
50
+ /** The waiting agents, each with the name to show for it. */
51
+ export const blockedView = (s) => s.blocked.map((b) => ({ ...b, name: agentIn(s.tree, b.paneId)?.name ?? b.kind ?? 'Agent' }));
52
+ const shared = new WeakMap();
53
+ /**
54
+ * The tree and the waiting agents, live from one `hd.events` stream per device however many views watch it: the
55
+ * tree from its frames, the waiting list read once the stream is open and then kept by its frames. Opened again
56
+ * after the link comes back.
57
+ */
58
+ export function herdrStore(source, { retryMs = 2000 } = {}) {
59
+ const known = shared.get(source);
60
+ if (known)
61
+ return known;
62
+ const made = store(HERDR_EMPTY, (set) => {
63
+ let s = made.get();
64
+ const apply = (a) => set(s = herdrStep(s, a));
65
+ return retrying(async (live) => {
66
+ const it = source.events()[Symbol.asyncIterator]();
67
+ live.hold(it);
68
+ try {
69
+ const r = await it.next(); // the first frame is the tree, sent once the stream is open
70
+ if (r.done || live.stopped())
71
+ return;
72
+ const listed = await source.blocked(); // frames meanwhile wait in the stream, applied after the list
73
+ if (live.stopped())
74
+ return;
75
+ apply(r.value);
76
+ apply({ type: 'listed', blocked: listed });
77
+ live.ok();
78
+ for (let r = await it.next(); !r.done && !live.stopped(); r = await it.next())
79
+ apply(r.value);
80
+ }
81
+ finally {
82
+ void it.return?.();
83
+ }
84
+ }, retryMs);
85
+ });
86
+ shared.set(source, made);
87
+ return made;
88
+ }
@@ -0,0 +1,11 @@
1
+ export { phaseOf, stepOf, type AccountView, type Phase, type SignInView } from './phase.ts';
2
+ export { connectedWords, describeRoute, routeChoices, routeShort, type Route, type RouteChoice, type RouteCode } from './route.ts';
3
+ export { useSignIn, type UseSignIn } from './useSignIn.ts';
4
+ export { stepsText, stepsView, type StepInput, type StepRow, type StepState } from './steps.ts';
5
+ export { consentWords, linkWords, pairErrorWords, pairingView, qrMatrix, qrText, type DeviceKind, type LinkStatus, type PairPhase, type Role } from './link.ts';
6
+ export * from './kits.ts';
7
+ export { useApprovals, useBlocked, useHerdrTree, useRun } from './useKits.ts';
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
+ export { useConnect, type UseConnect } from './useConnect.ts';
10
+ export { accountRows, nameSuggestions, rowsOf, type Account, type AccountAction, type AccountRow, type AccountsSource, type Billing, type Room, type RoomSpan, type SignInState } 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 ADDED
@@ -0,0 +1,11 @@
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/key.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /** A key-entry card never holds a credential: the host submits the input directly to kit.addKey. */
2
+ export type KeyState = 'entry' | 'checking' | 'ok' | 'invalid' | 'not_included';
3
+ export type KeyAction = {
4
+ type: 'submit';
5
+ } | {
6
+ type: 'edit';
7
+ } | {
8
+ type: 'result';
9
+ result: 'ok' | 'invalid' | 'not_included';
10
+ };
11
+ export type KeyWords = (key: 'key.label' | 'key.entry' | 'key.checking' | 'key.ok' | 'key.invalid' | 'key.notIncluded') => string;
12
+ export declare function keyStep(state: KeyState, action: KeyAction): KeyState;
13
+ export declare function keyView(state: KeyState, words: KeyWords): {
14
+ state: KeyState;
15
+ label: string;
16
+ message: string;
17
+ busy: boolean;
18
+ editable: boolean;
19
+ };
package/dist/key.js ADDED
@@ -0,0 +1,12 @@
1
+ export function keyStep(state, action) {
2
+ if (action.type === 'submit')
3
+ return 'checking';
4
+ if (action.type === 'edit')
5
+ return 'entry';
6
+ return state === 'checking' ? action.result : state;
7
+ }
8
+ export function keyView(state, words) {
9
+ const keys = { entry: 'key.entry', checking: 'key.checking', ok: 'key.ok', invalid: 'key.invalid', not_included: 'key.notIncluded' };
10
+ return { state, label: words('key.label'), message: words(keys[state]), busy: state === 'checking',
11
+ editable: state === 'entry' || state === 'invalid' };
12
+ }
package/dist/kits.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { RUN_IDLE, runStep, runStore, runView, type RunAction, type RunEnd, type RunEvent, type RunFrame, type RunPhase, type RunSource, type RunState, type RunWords, type RunWordsOptions } from './run.ts';
2
+ export { approvalsStep, approvalsStore, approvalWords, type Approval, type ApprovalFrame, type ApprovalsAction, type ApprovalsSource, type FollowOptions } from './approvals.ts';
3
+ export { HERDR_EMPTY, agentIn, blockedView, herdrStep, herdrStore, herdrTreeView, type AgentRow, type AgentStatus, type BlockedAgent, type HerdrAction, type HerdrAgent, type HerdrFrame, type HerdrSource, type HerdrState, type HerdrTree } from './herdr.ts';
4
+ export type { Store } from './follow.ts';
5
+ export { keyStep, keyView, type KeyAction, type KeyState, type KeyWords } from './key.ts';
6
+ export { livePanelView, signInSheetView, signInsStore, type BrowserFrame, type LivePanel, type LiveState, type SignInAction, type SignInRequest, type SignInSheet, type SignInsSource, type SignInVars, type SignInWordKey } from './browser.ts';
package/dist/kits.js ADDED
@@ -0,0 +1,7 @@
1
+ // View state for the runtime kits, with no React: reducers, views and live stores any UI can draw from (plain DOM,
2
+ // React, React Native). The hooks in `@byokit/ui` read the same stores.
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/link.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ /** The QR for a pairing offer's text (`offer().text` from @byokit/link) as rows of dark (true) and light modules, with
2
+ * the quiet border scanners need. Draw each true as a filled square in any UI: View grid, canvas, SVG or terminal.
3
+ * `border` is that quiet border in modules, 2 unless given. */
4
+ export declare const qrMatrix: (text: string, opts?: {
5
+ border?: number;
6
+ }) => boolean[][];
7
+ /** The same QR as text rows for a terminal: each line draws two module rows as half blocks (`█` both dark, `▀` top
8
+ * only, `▄` bottom only), so `console.log(qrText(offer.text))` in setup scripts shows a code a phone can scan. */
9
+ export declare function qrText(text: string, opts?: {
10
+ border?: number;
11
+ }): string;
12
+ /** What a pairing grants: `control` changes things on the computer, `view` only sees them. */
13
+ export type Role = 'control' | 'view';
14
+ /** Which device is pairing: a phone holding the app, or a browser that opened the pairing link. */
15
+ export type DeviceKind = 'phone' | 'browser';
16
+ /** The question before pairing: which computer, what this device may do, and for how long. `device` names the
17
+ * pairing side when it isn't the app ("This browser …"), `detail` adds one app sentence after ("It can also …"). */
18
+ export declare function consentWords(o: {
19
+ hostName: string;
20
+ role: Role;
21
+ device?: DeviceKind;
22
+ detail?: string;
23
+ }): string;
24
+ /** The pairing sheet's phases: `scan` the QR (or type the code), `compare` the two words with the computer, `waiting`
25
+ * for a yes there, `paired`, or `failed` with the link's own sentence. */
26
+ export type PairPhase = 'scan' | 'compare' | 'waiting' | 'paired' | 'failed';
27
+ export declare function pairingView(o: {
28
+ phase: PairPhase;
29
+ hostName?: string;
30
+ words?: string;
31
+ error?: string;
32
+ device?: DeviceKind;
33
+ detail?: string;
34
+ }): {
35
+ phase: PairPhase;
36
+ title: string;
37
+ words?: string;
38
+ };
39
+ /** A failed pairing as words for the phone's error line: `expired` when the code ran out (get a new one), `wrong`
40
+ * when it didn't match (check and retry), else the link's own sentence. Reads @byokit/link's error `code` when it is
41
+ * there (`wrong-code`, `expired`), else the message, so every path lands in the same plain words. */
42
+ export declare function pairErrorWords(error: unknown): {
43
+ kind: 'expired' | 'wrong' | 'other';
44
+ words: string;
45
+ };
46
+ /** The link's status (`LinkStatus` from @byokit/link) as one sentence. */
47
+ export type LinkStatus = 'connecting' | 'online' | 'offline' | 'refused' | 'removed';
48
+ export declare function linkWords(status: LinkStatus, hostName?: string): string;
package/dist/link.js ADDED
@@ -0,0 +1,63 @@
1
+ // What a pairing and a link put in front of a person, framework-free: the QR to show, the consent before pairing, the
2
+ // two words to compare and the link's status, each in plain words. Pairs with @byokit/link.
3
+ import { encode } from 'uqr';
4
+ /** The QR for a pairing offer's text (`offer().text` from @byokit/link) as rows of dark (true) and light modules, with
5
+ * the quiet border scanners need. Draw each true as a filled square in any UI: View grid, canvas, SVG or terminal.
6
+ * `border` is that quiet border in modules, 2 unless given. */
7
+ export const qrMatrix = (text, opts) => encode(text, { ecc: 'M', border: opts?.border ?? 2 }).data;
8
+ /** The same QR as text rows for a terminal: each line draws two module rows as half blocks (`█` both dark, `▀` top
9
+ * only, `▄` bottom only), so `console.log(qrText(offer.text))` in setup scripts shows a code a phone can scan. */
10
+ export function qrText(text, opts) {
11
+ const m = qrMatrix(text, opts);
12
+ const rows = [];
13
+ for (let y = 0; y < m.length; y += 2) {
14
+ const top = m[y], bottom = m[y + 1] ?? [];
15
+ rows.push(top.map((dark, x) => {
16
+ const below = bottom[x] ?? false;
17
+ return dark ? (below ? '█' : '▀') : (below ? '▄' : ' ');
18
+ }).join('').replace(/\s+$/, ''));
19
+ }
20
+ return rows.join('\n');
21
+ }
22
+ const deviceWords = (device) => device === 'phone' ? 'This phone' : device === 'browser' ? 'This browser' : 'This device';
23
+ /** The question before pairing: which computer, what this device may do, and for how long. `device` names the
24
+ * pairing side when it isn't the app ("This browser …"), `detail` adds one app sentence after ("It can also …"). */
25
+ export function consentWords(o) {
26
+ const may = o.role === 'control' ? 'see and change things on it' : 'see it, but not change anything';
27
+ const base = `Pair with ${o.hostName}? ${deviceWords(o.device)} will be able to ${may}, until you remove it there.`;
28
+ return o.detail ? `${base} ${o.detail}` : base;
29
+ }
30
+ export function pairingView(o) {
31
+ const name = o.hostName || 'your computer';
32
+ const title = (s) => (o.detail ? `${s} ${o.detail}` : s);
33
+ switch (o.phase) {
34
+ case 'scan': return { phase: o.phase, title: title(o.device === 'browser'
35
+ ? `Open the pairing link on ${name}, or type the code it shows.`
36
+ : `Scan the code on ${name}, or type the code it shows.`) };
37
+ case 'compare': return { phase: o.phase, title: title(`Check ${name} shows these two words, then say yes there.`), words: o.words };
38
+ case 'waiting': return { phase: o.phase, title: title(`Waiting for you to say yes on ${name}.`), words: o.words };
39
+ case 'paired': return { phase: o.phase, title: title(`${deviceWords(o.device)} is paired with ${name}.`) };
40
+ case 'failed': return { phase: o.phase, title: title(o.error || "Pairing didn't finish. Try again.") };
41
+ }
42
+ }
43
+ /** A failed pairing as words for the phone's error line: `expired` when the code ran out (get a new one), `wrong`
44
+ * when it didn't match (check and retry), else the link's own sentence. Reads @byokit/link's error `code` when it is
45
+ * there (`wrong-code`, `expired`), else the message, so every path lands in the same plain words. */
46
+ export function pairErrorWords(error) {
47
+ const code = typeof error === 'object' && error !== null && 'code' in error ? String(error.code) : '';
48
+ const message = error instanceof Error ? error.message : String(error ?? '');
49
+ if (code === 'expired' || (code !== 'wrong-code' && /run out|expired/i.test(message)))
50
+ return { kind: 'expired', words: 'That pairing code has run out. Show a new one on your computer.' };
51
+ if (code === 'wrong-code' || /isn't a pairing code|didn't match|wrong/i.test(message))
52
+ return { kind: 'wrong', words: "That code didn't match. Check it, or show a new one on your computer." };
53
+ return { kind: 'other', words: message || "Pairing didn't finish. Try again." };
54
+ }
55
+ export function linkWords(status, hostName = 'your computer') {
56
+ return {
57
+ connecting: `Connecting to ${hostName}…`,
58
+ online: `Connected to ${hostName}.`,
59
+ offline: `Can't reach ${hostName} right now. This device keeps trying by itself.`,
60
+ refused: `${hostName} didn't let this device in. Pair it again there.`,
61
+ removed: `This device was removed on ${hostName}.`,
62
+ }[status];
63
+ }