@soba-so/react 0.0.17 → 0.0.18

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,75 @@
1
+ import type { Machine } from "./types.js";
2
+ import { type ComputeMachine } from "./useCompute.js";
3
+ /**
4
+ * WHAT THE CONTROL SAYS AND WHAT PRESSING IT DOES — the headless half of
5
+ * `<ConnectButton />`.
6
+ *
7
+ * Almost every app that drew its own version of this button got the same thing
8
+ * wrong, and it is not a styling question: **a button that still asks after
9
+ * they have connected is the screen telling them it did not work.** So the
10
+ * label is a function of what is attached, and so is what the press means —
11
+ * before anything is paired it opens the flow, and afterwards the honest
12
+ * reading of a press is "show me what I have".
13
+ *
14
+ * The word and its tone come from the same function the panel's own badge uses,
15
+ * so a dot an app draws here and the badge inside the sheet it opens cannot end
16
+ * up disagreeing about a laptop.
17
+ *
18
+ * WHAT THIS DELIBERATELY DOES NOT CARRY is the popover: the panel that hangs
19
+ * off our pill is a component, not a decision, and an app building its own
20
+ * control has its own idea of where that goes. `connect.open()` raises the
21
+ * flow — the part that genuinely needs the screen — and `<ConnectCompute />`
22
+ * or `useCompute()` draws the machines wherever the app wants them.
23
+ */
24
+ export interface ConnectButtonView {
25
+ /** "Connected", "Serving", "Asleep" — or "Connect compute" with nothing
26
+ * paired. The whole label, ready to print. */
27
+ label: string;
28
+ /**
29
+ * The accessible name, which is not the label: on a narrow bar a host hides
30
+ * the words and leaves a mark and a dot, and a control whose only accessible
31
+ * name was those words then has none.
32
+ */
33
+ ariaLabel: string;
34
+ /** Something is paired. The label is about a machine when this is true. */
35
+ connected: boolean;
36
+ /** The machine the label is about, named and worded. Null until one is. */
37
+ machine: ComputeMachine | null;
38
+ /** One word plus a tone, for a dot beside the label. */
39
+ state: {
40
+ tone: "ok" | "busy" | "off";
41
+ word: string;
42
+ };
43
+ /** Raise the connect flow — the modal the provider mounts. */
44
+ open: () => void;
45
+ /** Whether `open()` does anything: false under `<SobaProvider modal={false}>`
46
+ * with no modal placed, which is the honest state for a control with nothing
47
+ * to control. */
48
+ canOpen: boolean;
49
+ /** The first read of the account is still in flight, so `connected` is not
50
+ * yet an answer. */
51
+ loading: boolean;
52
+ }
53
+ export declare function useConnectButton(): ConnectButtonView;
54
+ /**
55
+ * WHAT THE CONTROL SAYS ABOUT ONE MACHINE — the rule itself, with no hooks in
56
+ * it, because `<ConnectButton />` cannot read it off this hook.
57
+ *
58
+ * While its panel is open the pill's state is THIS APP'S RUNS rather than the
59
+ * machine's own counters: the run list is read every three seconds and is
60
+ * exactly this app's, where `state` and `active` arrive by heartbeat and count
61
+ * every app on that laptop. So the component substitutes its own machine list
62
+ * for those few seconds, and a hook reading the provider cannot.
63
+ *
64
+ * Both call this, so the words and the dot are one decision with two readers
65
+ * rather than two compositions that happen to agree today.
66
+ */
67
+ export declare function connectSays(machine: Machine | null): {
68
+ label: string;
69
+ ariaLabel: string;
70
+ connected: boolean;
71
+ state: {
72
+ tone: "ok" | "busy" | "off";
73
+ word: string;
74
+ };
75
+ };
@@ -0,0 +1,204 @@
1
+ import type { Launch, Plan } from "./types.js";
2
+ /**
3
+ * THE NUDGE'S DECISION, WITHOUT THE CARD.
4
+ *
5
+ * `<ConnectNudge />` is one rendering of this and renders nothing else, so a
6
+ * line an app writes from these values and the card we draw cannot disagree
7
+ * about whether there is anything worth saying.
8
+ *
9
+ * The hard part of this component was never the markup. It is knowing WHEN to
10
+ * speak: a nudge that shows up when it cannot help is an advertisement, and the
11
+ * four states below are four different reasons, three of which withdraw by
12
+ * themselves and one of which needs a way out. An app that wanted its own
13
+ * wording had to re-derive all of that from `plan` and `compute`, and the parts
14
+ * that are easy to get wrong — a credit is not an allowance, a paired machine
15
+ * is not a reachable one, `ask_to_connect` is the app's own switch — are
16
+ * exactly the parts nothing would have told it about.
17
+ */
18
+ export type NudgeReason =
19
+ /** Nothing can run until something is attached. A prerequisite, not an offer. */
20
+ "needed"
21
+ /** The allowance is nearly gone and connecting would lift it. */
22
+ | "running"
23
+ /** It runs either way, and the plan would rather it ran on theirs. */
24
+ | "preferred"
25
+ /** A machine they already have has stopped answering. Not an offer at all. */
26
+ | "stalled";
27
+ export interface NudgeStep {
28
+ lead: string;
29
+ detail: string;
30
+ /** The lead is a control that raises the connect flow, not a heading. */
31
+ opens?: boolean;
32
+ linksLead?: string;
33
+ links: {
34
+ label: string;
35
+ href: string;
36
+ }[];
37
+ }
38
+ /**
39
+ * The machine that went quiet, and what can be done about it from here.
40
+ * Non-null on `stalled` and null on every other reason.
41
+ */
42
+ export interface NudgeStalled {
43
+ /** What to call it, or null when the list did not say. */
44
+ machine: string | null;
45
+ /** The plan can only reach compute this person owns, so there is no
46
+ * elsewhere for a run to go while this one is quiet. */
47
+ ownCompute: boolean;
48
+ /** Bring the Soba app forward, which wakes a machine that was only asleep. */
49
+ wake: () => void;
50
+ /** What that attempt looked like, or null before one. See `useOpenSobaConnect`. */
51
+ launch: Launch | null;
52
+ /** The one line worth showing about the attempt, or null. */
53
+ note: string | null;
54
+ /**
55
+ * OPENED, AND STILL NOT BACK after twenty seconds.
56
+ *
57
+ * Waking cannot bring back a machine whose pairing with this app is gone: the
58
+ * app opens, says "Ready", and the machine this card names never reports in.
59
+ * While this is false the card's own offer is to wake it; once it is true the
60
+ * honest offer is to connect something again.
61
+ */
62
+ stuck: boolean;
63
+ /** Pair another machine, for when waking this one is not going to work. */
64
+ connectAnother: () => void;
65
+ }
66
+ export interface NudgeView {
67
+ /** The one claim worth making, or null for silence — which is most of the
68
+ * time, and is the whole point of the component. */
69
+ reason: NudgeReason | null;
70
+ /** `reason !== null`. The name a host actually wants at the call site. */
71
+ visible: boolean;
72
+ title: string | null;
73
+ detail: string | null;
74
+ /**
75
+ * The allowance behind a `running` claim, null on every other reason.
76
+ * Not drawn by our own card any more — the title already says the number —
77
+ * but it is still what decides whether this person is stuck or merely warned.
78
+ */
79
+ meter: {
80
+ left: number;
81
+ of: number;
82
+ } | null;
83
+ /** True on `preferred` alone: the other three withdraw by themselves when the
84
+ * fact behind them changes, and that one is true for as long as it works. */
85
+ dismissible: boolean;
86
+ /** Already waved off. `visible` is false when it is. */
87
+ dismissed: boolean;
88
+ dismiss: () => void;
89
+ /** Raise the connect flow. */
90
+ open: () => void;
91
+ /**
92
+ * Whether somebody is STOPPED rather than warned, which is what sizes the
93
+ * card: at the wall the steps are the way out, and above it they are clutter
94
+ * over a composer somebody can still use.
95
+ */
96
+ atTheWall: boolean;
97
+ /** Download, connect, allow — the three steps, for a host drawing its own. */
98
+ steps: NudgeStep[];
99
+ stalled: NudgeStalled | null;
100
+ /** The plan or the machines have not been read yet. Silent until they are. */
101
+ loading: boolean;
102
+ }
103
+ export interface UseConnectNudgeOptions {
104
+ /**
105
+ * How little has to be left before the `running` claim speaks. A fraction of
106
+ * the allowance rather than a count, so it means the same on a plan of 20 and
107
+ * a plan of 2,000.
108
+ */
109
+ at?: number;
110
+ /**
111
+ * WHAT THE LAST RUN CAME BACK WITH, and passing it at all changes which of
112
+ * the two integrations this is.
113
+ *
114
+ * Anything your catch block caught. Read through `sobaErrorCode`, so an
115
+ * unrelated failure is silence here and still renders however your app
116
+ * renders errors.
117
+ */
118
+ error?: unknown;
119
+ /** Both halves at once. Defaults to true exactly when `error` is not given —
120
+ * see the note on `errorGiven`. */
121
+ predict?: boolean;
122
+ /**
123
+ * WHETHER AN `error` KEY WAS PRESENT AT ALL, which a hook cannot see for
124
+ * itself and a component can.
125
+ *
126
+ * `useState<unknown>(null)` is how every app holds its last failure, so a
127
+ * slot holding nothing has to mean "nothing has failed yet" and not "no
128
+ * argument was given". `<ConnectNudge />` answers this with `"error" in
129
+ * props`; a hook is passed one object and cannot tell an absent key from an
130
+ * explicit `undefined`, so a caller that holds a failure slot says so here.
131
+ * Leaving it out and passing `error` reads as the attempted integration,
132
+ * which is what passing an error means.
133
+ */
134
+ errorGiven?: boolean;
135
+ /**
136
+ * THE CEILING THE RUN WAS SENT UNDER, when your app narrows `route.allow`
137
+ * per run rather than leaving the question to the plan.
138
+ *
139
+ * An explicit `null` is no ceiling; `undefined` falls back to the plan rather
140
+ * than claiming anything. It narrows what this SAYS, never what a run may do.
141
+ */
142
+ allow?: string[] | null;
143
+ }
144
+ /**
145
+ * THE WHOLE DECISION, AS A PURE FUNCTION over facts an app already holds.
146
+ *
147
+ * Exported for the reason `useRunLimit` exports `foreseen`: this is the part
148
+ * worth testing and the part worth reading, and neither should need React to
149
+ * run. The hook below is the wiring — context, the dismissal in storage, and
150
+ * the twenty seconds after which waking a machine has visibly not worked.
151
+ */
152
+ export interface NudgeSaid {
153
+ reason: NudgeReason;
154
+ title: string;
155
+ detail: string;
156
+ meter: {
157
+ left: number;
158
+ of: number;
159
+ } | null;
160
+ dismissible: boolean;
161
+ atTheWall: boolean;
162
+ /** On `stalled`: the plan can only reach compute this person owns, so there
163
+ * is no elsewhere for a run to go while this machine is quiet. */
164
+ ownCompute: boolean;
165
+ }
166
+ export interface NudgeInput {
167
+ /** The plan they are on. Null while it loads, or when they are on none —
168
+ * either way there is nothing to claim. */
169
+ plan: Plan | null;
170
+ /** The allowance arithmetic, as `useSoba().plan` already derives it. */
171
+ allowance: {
172
+ remaining: number | null;
173
+ included: number | null;
174
+ };
175
+ /** Something paired. It may still be asleep. */
176
+ connected: boolean;
177
+ /** Something of theirs could serve a run right now. */
178
+ online: boolean;
179
+ /** The machine that has gone quiet, named. Null unless one has. */
180
+ quiet: string | null;
181
+ error?: unknown;
182
+ /** Whether to speak about what is already in the way, or only about a run
183
+ * that has actually been refused. */
184
+ predict: boolean;
185
+ /** The ceiling the run was sent under. `undefined` falls back to the plan. */
186
+ allow?: string[] | null;
187
+ /** How little has to be left before the `running` claim speaks. */
188
+ at: number;
189
+ /** The dismissible claim has already been waved off. */
190
+ waved: boolean;
191
+ }
192
+ export declare function nudgeSays(input: NudgeInput): NudgeSaid | null;
193
+ export declare function useConnectNudge(options?: UseConnectNudgeOptions): NudgeView;
194
+ /**
195
+ * A step that names a screen somebody does not have, or one they cannot reach
196
+ * from here, is an instruction to go and find something — which is where people
197
+ * stop. So the builds are links, the second step opens the flow it describes,
198
+ * and the third answers the question it raises.
199
+ *
200
+ * If the flow changes this list is wrong and nothing will fail to tell us — the
201
+ * one real cost of naming screens, and worth paying, because a list that could
202
+ * not be wrong was not saying anything.
203
+ */
204
+ export declare function stepsFor(app: string | undefined, origin: string): NudgeStep[];
@@ -0,0 +1,93 @@
1
+ import type { ModelGroup, ModelOption } from "./types.js";
2
+ export interface UseModelOptions {
3
+ /**
4
+ * CONTROLLED. Pass both, or neither — a `value` without an `onChange` is a
5
+ * picker that cannot be changed, which is a label.
6
+ *
7
+ * Passing them turns off the remembered pick entirely: the app owns the
8
+ * value, and two places writing one preference is how it ends up disagreeing
9
+ * with itself across two tabs.
10
+ */
11
+ value?: string;
12
+ onChange?: (id: string) => void;
13
+ /** What a first-time visitor gets. `auto` unless you have a reason. */
14
+ defaultValue?: string;
15
+ /**
16
+ * Remember the pick in this browser, per app and per person. Left on: the
17
+ * person chose a model for themselves rather than for a page, and a reload
18
+ * putting them back on Automatic is the app forgetting what they said.
19
+ * Ignored under `value`.
20
+ */
21
+ remember?: boolean;
22
+ /**
23
+ * WHO THEY ARE, when the app knows, so two people sharing a browser do not
24
+ * share a pick. Many apps resolve the person server-side from a cookie and
25
+ * never tell the browser — that is the better design, and then the pick is
26
+ * scoped to the app alone. It is folded before it is stored, never sent
27
+ * anywhere, and the worst case on a shared machine is that a stranger learns
28
+ * somebody here prefers Haiku.
29
+ */
30
+ user?: string;
31
+ }
32
+ export interface ModelView {
33
+ /** Every model this person could be answered by, Automatic first. */
34
+ options: ModelOption[];
35
+ /** The same list, ready for a menu: Automatic and the sizes, then one group
36
+ * per agent. */
37
+ groups: ModelGroup[];
38
+ /** The one in force. Never null — Automatic when nothing is picked, and when
39
+ * what was picked is not here. */
40
+ selected: ModelOption;
41
+ select: (id: string) => void;
42
+ /**
43
+ * SPREAD THIS INTO THE RUN, and spread it rather than reading `model` off it.
44
+ *
45
+ * It is `{ model }` for Automatic and a size, and `{ model, runtime }` for a
46
+ * named model — because the broker applies the person's preferred agent only
47
+ * to a run whose model is blank or a size, so a name sent without its agent
48
+ * is handed to whatever agent the broker finds first. An app that forwards
49
+ * only `model` works until somebody with two agents picks a model from the
50
+ * second one.
51
+ */
52
+ run: {
53
+ model: string;
54
+ runtime?: string;
55
+ };
56
+ /**
57
+ * WHAT THEY PICKED THAT IS NOT CURRENTLY ON OFFER — the laptop with Opus on
58
+ * it is shut, or they are on a phone.
59
+ *
60
+ * The pick is kept, not cleared: `selected` falls back to Automatic so the
61
+ * run still goes, and this comes back the moment the machine does. Say
62
+ * something with it if the surface has room; ignoring it is also correct,
63
+ * because the run will work either way.
64
+ */
65
+ unavailable: string | null;
66
+ /** More than Automatic to choose between. False with nothing connected,
67
+ * which is when `<ModelPicker />` draws nothing. */
68
+ pickable: boolean;
69
+ /** The first machines poll is still in flight. The list is Automatic only
70
+ * until it lands, so a picker drawn on this is a picker that pops in. */
71
+ loading: boolean;
72
+ }
73
+ /**
74
+ * WHICH MODEL ANSWERS, AS A HOOK — because the moment of choosing lands in the
75
+ * middle of somebody else's composer.
76
+ *
77
+ * `<ModelPicker />` is one rendering of exactly this, so a control built from
78
+ * these values and the one we draw cannot disagree about what is on offer.
79
+ *
80
+ * NOTHING HERE FETCHES. The options are derived from the machines the provider
81
+ * is already polling, so three pickers on a page is still one request — and the
82
+ * list changes on its own when somebody connects a second agent, with no event
83
+ * for an app to subscribe to.
84
+ *
85
+ * WHY THE LIST IS NOT A CONSTANT, which is the thing worth understanding before
86
+ * using this: the models on offer are read off the computer the person
87
+ * connected. Claude Code advertises haiku/sonnet/opus/fable because those are
88
+ * the words its CLI accepts; a local endpoint advertises what has been pulled;
89
+ * Codex advertises nothing and gets Automatic alone. An app cannot know any of
90
+ * that at build time, which is why a model picker on this product is derived
91
+ * rather than configured.
92
+ */
93
+ export declare function useModel(options?: UseModelOptions): ModelView;
@@ -1,4 +1,4 @@
1
- import { type Launch } from "@soba-so/connect-ui";
1
+ import type { Launch } from "./types.js";
2
2
  /**
3
3
  * THE PRESS THAT PUTS SOBA CONNECT IN FRONT OF SOMEBODY, and the one honest
4
4
  * thing a page can say about it afterwards.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soba-so/react",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "Connect the AI your users already pay for, from your own React app.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://soba.so/docs/components",
@@ -19,13 +19,18 @@
19
19
  "!dist/**/*.map",
20
20
  "README.md"
21
21
  ],
22
+ "scripts": {
23
+ "build": "node ../../scripts/build-react.mjs",
24
+ "typecheck": "tsc -p tsconfig.json",
25
+ "prepublishOnly": "node ../../scripts/build-react.mjs"
26
+ },
22
27
  "peerDependencies": {
23
28
  "react": ">=18"
24
29
  },
25
30
  "devDependencies": {
31
+ "@soba-so/connect-ui": "workspace:*",
26
32
  "@types/react": "^19.0.0",
27
- "typescript": "^7.0.2",
28
- "@soba-so/connect-ui": "0.1.0"
33
+ "typescript": "^7.0.2"
29
34
  },
30
35
  "engines": {
31
36
  "node": ">=22"
@@ -39,9 +44,5 @@
39
44
  ],
40
45
  "publishConfig": {
41
46
  "access": "public"
42
- },
43
- "scripts": {
44
- "build": "node ../../scripts/build-react.mjs",
45
- "typecheck": "tsc -p tsconfig.json"
46
47
  }
47
- }
48
+ }