@soba-so/react 0.0.16 → 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.
- package/dist/ModelPicker.d.ts +31 -0
- package/dist/SobaProvider.d.ts +11 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.mjs +360 -6
- package/dist/session.d.ts +12 -0
- package/dist/styles.css +352 -1
- package/dist/tier.d.ts +35 -0
- package/dist/types.d.ts +113 -0
- package/dist/useCheckout.d.ts +41 -0
- package/dist/useCompute.d.ts +91 -0
- package/dist/useConnectButton.d.ts +75 -0
- package/dist/useConnectNudge.d.ts +204 -0
- package/dist/useModel.d.ts +93 -0
- package/dist/useOpenSobaConnect.d.ts +1 -1
- package/dist/watch.d.ts +48 -0
- package/package.json +9 -8
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Machine, Runtime } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* WHAT THIS PERSON HAS CONNECTED, IN WORDS — the headless half of
|
|
4
|
+
* `<ComputeStatus />` and `<ConnectCompute />`.
|
|
5
|
+
*
|
|
6
|
+
* `useSoba().compute` is already the raw list, and it is not enough to draw
|
|
7
|
+
* from. Every surface that tried re-derived the same five judgements, and the
|
|
8
|
+
* two below are ones this repo has got wrong in production:
|
|
9
|
+
*
|
|
10
|
+
* **"Nothing connected yet" is a claim about somebody's ACCOUNT**, and it is
|
|
11
|
+
* only true once the account has been read. While the first poll is in flight,
|
|
12
|
+
* or after it has failed, the honest answer is that we do not know — and saying
|
|
13
|
+
* the empty thing instead tells a person with a paired laptop that it has
|
|
14
|
+
* vanished, during an outage they cannot see and did not cause. `view` is that
|
|
15
|
+
* decision, made once. Two of our own components drew this list and only one of
|
|
16
|
+
* them knew the rule.
|
|
17
|
+
*
|
|
18
|
+
* **Machines in hand beat both flags.** The provider polls, so `loading` and
|
|
19
|
+
* `error` come back around; a poll that fails on its fourth round must not
|
|
20
|
+
* blank a list somebody is already reading.
|
|
21
|
+
*
|
|
22
|
+
* The rest is naming: a hostname is not what anybody calls the laptop on their
|
|
23
|
+
* desk, `darwin · arm64` is what a kernel calls itself, and a runtime's
|
|
24
|
+
* `authenticated` is tri-state all the way to the screen — "could not tell" is
|
|
25
|
+
* not "signed out", and rendering it as one tells somebody their working setup
|
|
26
|
+
* is broken.
|
|
27
|
+
*/
|
|
28
|
+
export interface ComputeRuntime {
|
|
29
|
+
runtime: Runtime;
|
|
30
|
+
/** "Claude Code". */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Signed in, signed out, or we could not tell — with the tone to draw it
|
|
33
|
+
* in. Never collapse `unknown` into `warn`. */
|
|
34
|
+
auth: {
|
|
35
|
+
tone: "ok" | "warn" | "unknown";
|
|
36
|
+
text: string;
|
|
37
|
+
};
|
|
38
|
+
/** It arrived on a machine that was already paired, so it waits to be let in
|
|
39
|
+
* and serves nothing until somebody says so. */
|
|
40
|
+
quarantined: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface ComputeMachine {
|
|
43
|
+
machine: Machine;
|
|
44
|
+
/** What to call it: the name they gave it, else a tidied hostname. */
|
|
45
|
+
name: string;
|
|
46
|
+
/** One word plus a tone, for a dot. Agrees with the panel's own badge. */
|
|
47
|
+
state: {
|
|
48
|
+
tone: "ok" | "busy" | "off";
|
|
49
|
+
word: string;
|
|
50
|
+
};
|
|
51
|
+
/** The sentence version — "serving 2 of 4" — for where there is room. */
|
|
52
|
+
status: {
|
|
53
|
+
tone: "ok" | "busy" | "off";
|
|
54
|
+
text: string;
|
|
55
|
+
};
|
|
56
|
+
/** "MacBook · Apple silicon", or null when the worker reported nothing. */
|
|
57
|
+
hardware: string | null;
|
|
58
|
+
/** "3 days ago", or null while it is here. */
|
|
59
|
+
lastSeen: string | null;
|
|
60
|
+
/** The agents a person may send runs to: let in, and never the shell. */
|
|
61
|
+
runtimes: ComputeRuntime[];
|
|
62
|
+
/** Which agent this app's next run on it goes to, and whose decision that
|
|
63
|
+
* was — the app's, the person's, or the machine's own first-found default. */
|
|
64
|
+
runsOn: {
|
|
65
|
+
by: "app" | "you" | "default";
|
|
66
|
+
name: string | null;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export interface ComputeView {
|
|
70
|
+
/**
|
|
71
|
+
* WHAT TO DRAW, decided once instead of by each surface. `empty` is a claim
|
|
72
|
+
* about the account and is only reached once one has actually been read.
|
|
73
|
+
*/
|
|
74
|
+
view: "loading" | "error" | "empty" | "machines";
|
|
75
|
+
/** Every machine, named and worded, best-present first. */
|
|
76
|
+
machines: ComputeMachine[];
|
|
77
|
+
/** The one the panel is about: serving beats awake beats asleep, and within a
|
|
78
|
+
* state the one heard from most recently. Null with nothing connected. */
|
|
79
|
+
primary: ComputeMachine | null;
|
|
80
|
+
/** Something paired. It may still be asleep — see `online`. */
|
|
81
|
+
connected: boolean;
|
|
82
|
+
/** Something of theirs could serve a run right now. */
|
|
83
|
+
online: boolean;
|
|
84
|
+
/** Paired and not reachable: the state an app goes silent in unless it says
|
|
85
|
+
* something. `<ConnectNudge />`'s `stalled` reason is this one. */
|
|
86
|
+
stalled: ComputeMachine | null;
|
|
87
|
+
loading: boolean;
|
|
88
|
+
error: Error | null;
|
|
89
|
+
refresh: () => Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
export declare function useCompute(): ComputeView;
|
|
@@ -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;
|
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AgentEvent } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* READ A RUN STRAIGHT FROM SOBA, in the browser.
|
|
4
|
+
*
|
|
5
|
+
* Your server starts the run (`soba.runs.start`) and hands the page the run's
|
|
6
|
+
* `stream`: a URL and a token good for that one run. The page reads the run
|
|
7
|
+
* here instead of through your server, so your server answers in milliseconds
|
|
8
|
+
* rather than holding a function open for the whole run, and is never cut off
|
|
9
|
+
* by its host's time limit.
|
|
10
|
+
*
|
|
11
|
+
* for await (const event of watchRun(stream)) { … }
|
|
12
|
+
*
|
|
13
|
+
* Read-only: the token cannot start, steer or cancel anything, and tool calls
|
|
14
|
+
* arrive as their names only (your server answers them). If the connection
|
|
15
|
+
* drops before the run ends — a deploy on Soba's side, a network blip — it
|
|
16
|
+
* reconnects and reads on from the last frame it saw.
|
|
17
|
+
*
|
|
18
|
+
* With `local`, a page on the computer running the run reads it from that
|
|
19
|
+
* computer instead (the Soba app's local server on 127.0.0.1), live for as long
|
|
20
|
+
* as the run goes and without passing through Soba at all. The browser asks
|
|
21
|
+
* the person once whether this site may talk to apps on their device; if they
|
|
22
|
+
* say no, or the page is on another computer, it reads through Soba as above.
|
|
23
|
+
*
|
|
24
|
+
* watchRun(stream, { local: { session: useSoba().session } })
|
|
25
|
+
*/
|
|
26
|
+
export interface RunStream {
|
|
27
|
+
url: string;
|
|
28
|
+
token: string;
|
|
29
|
+
}
|
|
30
|
+
export interface WatchOptions {
|
|
31
|
+
/** Read on from after this frame, e.g. one you kept from an earlier visit. */
|
|
32
|
+
after?: number;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
fetchImpl?: typeof fetch;
|
|
35
|
+
/** Reconnects in a row without a frame before giving up. */
|
|
36
|
+
maxRetries?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Try the person's own machine first: their Soba session, which the machine
|
|
39
|
+
* checks with Soba before showing anything. Ignored with `after`, whose
|
|
40
|
+
* numbers are Soba's.
|
|
41
|
+
*/
|
|
42
|
+
local?: {
|
|
43
|
+
session: string | null;
|
|
44
|
+
port?: number;
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export declare function watchRun(stream: RunStream, options?: WatchOptions): AsyncGenerator<AgentEvent>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soba-so/react",
|
|
3
|
-
"version": "0.0.
|
|
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
|
+
}
|