@hanzo/rooms 0.1.53 → 0.1.55

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/Orgs.js CHANGED
@@ -297,9 +297,8 @@ function Veil() {
297
297
  /**
298
298
  * The room, in the one place every branch that draws it puts it. Under the veil
299
299
  * it is inert and hidden from assistive technology, so what the veil covers
300
- * takes no focus, key or press: Dev's composer focuses itself as it mounts, and
301
- * a team on Free could otherwise type and send before its ask was drawn. It
302
- * lays out as its children do.
300
+ * takes no focus, key or press: a team on Free cannot type and send before its
301
+ * ask is drawn. It lays out as its children do.
303
302
  */
304
303
  function Held({ veiled, children }) {
305
304
  return (_jsx(View, { render: _jsx("div", { "data-slot": "held", inert: veiled, "aria-hidden": veiled || undefined }), display: "contents", children: children }));
package/dist/lib/ai.d.ts CHANGED
@@ -18,7 +18,10 @@ export declare const FREE = "enso-free";
18
18
  /** Enso routes a signed-in turn: enso-auto when logged in. */
19
19
  export declare const ENSO = "enso-auto";
20
20
  /**
21
- * The model that will actually ANSWER, given what the gateway serves.
21
+ * The model that will actually ANSWER, given what the gateway serves: the one
22
+ * asked for when the gateway serves it, else the free pool for a free turn,
23
+ * else a house router (Enso or Zen), else the first model served. An id is read
24
+ * by its own name; one the gateway does not serve is never relabelled.
22
25
  */
23
26
  export declare function served(models: {
24
27
  id: string;
package/dist/lib/ai.js CHANGED
@@ -46,31 +46,22 @@ export const FREE = 'enso-free';
46
46
  /** Enso routes a signed-in turn: enso-auto when logged in. */
47
47
  export const ENSO = 'enso-auto';
48
48
  /**
49
- * The model that will actually ANSWER, given what the gateway serves.
49
+ * The model that will actually ANSWER, given what the gateway serves: the one
50
+ * asked for when the gateway serves it, else the free pool for a free turn,
51
+ * else a house router (Enso or Zen), else the first model served. An id is read
52
+ * by its own name; one the gateway does not serve is never relabelled.
50
53
  */
51
54
  export function served(models, prefer) {
52
- const cleanPrefer = prefer === 'openrouter/auto' || prefer?.includes('openrouter') ? 'enso-auto' : prefer;
53
55
  if (models.length === 0)
54
- return cleanPrefer || 'enso-auto';
55
- if (cleanPrefer && models.some((m) => m.id === cleanPrefer))
56
- return cleanPrefer;
57
- if (cleanPrefer === 'enso-free' || cleanPrefer === 'free' || cleanPrefer.includes('free')) {
58
- const free = models.find((m) => m.id === 'enso-free' || m.id === 'zen-free' || (m.id.includes('free') && !m.id.includes('openrouter')));
59
- if (free)
60
- return free.id;
61
- return 'enso-free';
56
+ return prefer || ENSO;
57
+ if (prefer && models.some((m) => m.id === prefer))
58
+ return prefer;
59
+ if (prefer === FREE || prefer === 'free' || prefer?.includes('free')) {
60
+ return models.find((m) => m.id === FREE || m.id === 'zen-free')?.id ?? FREE;
62
61
  }
63
- // House routers: Enso or Zen
64
- const houseAuto = models.find((m) => m.id === 'enso-auto' || m.id === 'zen-auto' || m.id === 'enso' || m.id === 'zen');
65
- if (houseAuto)
66
- return houseAuto.id;
67
- const houseModel = models.find((m) => (m.id.startsWith('enso') || m.id.startsWith('zen')) && !m.id.includes('openrouter'));
68
- if (houseModel)
69
- return houseModel.id;
70
- const clean = models.find((m) => !m.id.includes('openrouter'));
71
- if (clean)
72
- return clean.id;
73
- return 'enso-auto';
62
+ const house = models.find((m) => m.id === ENSO || m.id === 'zen-auto' || m.id === 'enso' || m.id === 'zen') ??
63
+ models.find((m) => m.id.startsWith('enso') || m.id.startsWith('zen'));
64
+ return house?.id ?? models[0].id;
74
65
  }
75
66
  /**
76
67
  * The gateway's address in the shape `createAiClient` takes it.
@@ -93,10 +93,57 @@ export declare function useSubscription(enabled?: boolean, org?: string | null):
93
93
  * are asked.
94
94
  */
95
95
  export declare function paying(signal?: AbortSignal): Promise<boolean>;
96
+ /** The last tier an organization was read at. */
97
+ export type Read = {
98
+ org: string | null;
99
+ tier: Tier;
100
+ };
101
+ /** What a read left held, and for which organization. */
102
+ export type Held = {
103
+ org: string | null;
104
+ tier: Tier | null;
105
+ wrong: unknown;
106
+ };
107
+ /**
108
+ * Whether a failed read may answer otherwise on the next try: no answer at all
109
+ * (the network, a timeout), too many asks (429) or billing's own failure (a
110
+ * 5xx). A 401, 403 or 404 is billing's answer, and stands.
111
+ */
112
+ export declare function transient(e: unknown): boolean;
113
+ /**
114
+ * What a read of the tier leaves held, and the tier the next read falls back
115
+ * on. A read that answers is the tier. One that fails for a moment
116
+ * (`transient`) keeps the tier this organization was last read at, so a reader
117
+ * is not swapped out of a room they pay for by a re-read that did not answer
118
+ * (the gate reads again on focus and on return). Billing's refusal, or a
119
+ * failure with nothing read for it yet, holds nothing and says why, which keeps
120
+ * every gate shut; a refusal also forgets the tier read before it, so no later
121
+ * failure brings it back.
122
+ */
123
+ export declare function hold(last: Read | null, org: string | null, read: {
124
+ tier: Tier;
125
+ } | {
126
+ wrong: unknown;
127
+ }): {
128
+ tier: Tier | null;
129
+ wrong: unknown;
130
+ last: Read | null;
131
+ };
132
+ /**
133
+ * The tier and the refusal held for `org`, and nothing for any other: what was
134
+ * read for the organization a reader just left is never the one they are in,
135
+ * so a switch from a paid organization to a team on Free shows no room while
136
+ * the new one is read.
137
+ */
138
+ export declare function standing(held: Held | null, org: string | null): {
139
+ tier: Tier | null;
140
+ wrong: unknown;
141
+ };
96
142
  /**
97
143
  * The tier, held. `null` while unknown — which is BOTH "still asking" and
98
144
  * "asked and was refused", so `wrong` is kept beside it rather than swallowed:
99
- * a row cannot choose between saying nothing and saying why without it.
145
+ * a row cannot choose between saying nothing and saying why without it. A
146
+ * re-read that fails keeps the last tier read (`hold`).
100
147
  */
101
148
  export declare function useTier(enabled?: boolean, org?: string | null): {
102
149
  tier: Tier | null;
package/dist/lib/tier.js CHANGED
@@ -21,7 +21,7 @@
21
21
  * reads first, so a refusal here already speaks the vocabulary every room uses.
22
22
  * A second Refusal class would be a second vocabulary for the same event.
23
23
  */
24
- import { useCallback, useEffect, useState } from 'react';
24
+ import { useCallback, useEffect, useRef, useState } from 'react';
25
25
  import { api } from './api.js';
26
26
  import { bearer, org, orgs, scope } from './session.js';
27
27
  import { list } from './list.js';
@@ -147,34 +147,69 @@ export async function paying(signal) {
147
147
  }));
148
148
  return reads.some((r) => r.status === 'fulfilled' && paid(r.value));
149
149
  }
150
+ /**
151
+ * Whether a failed read may answer otherwise on the next try: no answer at all
152
+ * (the network, a timeout), too many asks (429) or billing's own failure (a
153
+ * 5xx). A 401, 403 or 404 is billing's answer, and stands.
154
+ */
155
+ export function transient(e) {
156
+ const status = e?.status;
157
+ return typeof status !== 'number' || status === 408 || status === 429 || status >= 500;
158
+ }
159
+ /**
160
+ * What a read of the tier leaves held, and the tier the next read falls back
161
+ * on. A read that answers is the tier. One that fails for a moment
162
+ * (`transient`) keeps the tier this organization was last read at, so a reader
163
+ * is not swapped out of a room they pay for by a re-read that did not answer
164
+ * (the gate reads again on focus and on return). Billing's refusal, or a
165
+ * failure with nothing read for it yet, holds nothing and says why, which keeps
166
+ * every gate shut; a refusal also forgets the tier read before it, so no later
167
+ * failure brings it back.
168
+ */
169
+ export function hold(last, org, read) {
170
+ if ('tier' in read)
171
+ return { tier: read.tier, wrong: null, last: { org, tier: read.tier } };
172
+ const mine = last && last.org === org ? last : null;
173
+ if (transient(read.wrong) && mine)
174
+ return { tier: mine.tier, wrong: null, last: mine };
175
+ return { tier: null, wrong: read.wrong, last: transient(read.wrong) ? last : null };
176
+ }
177
+ /**
178
+ * The tier and the refusal held for `org`, and nothing for any other: what was
179
+ * read for the organization a reader just left is never the one they are in,
180
+ * so a switch from a paid organization to a team on Free shows no room while
181
+ * the new one is read.
182
+ */
183
+ export function standing(held, org) {
184
+ return held && held.org === org ? { tier: held.tier, wrong: held.wrong } : { tier: null, wrong: null };
185
+ }
150
186
  /**
151
187
  * The tier, held. `null` while unknown — which is BOTH "still asking" and
152
188
  * "asked and was refused", so `wrong` is kept beside it rather than swallowed:
153
- * a row cannot choose between saying nothing and saying why without it.
189
+ * a row cannot choose between saying nothing and saying why without it. A
190
+ * re-read that fails keeps the last tier read (`hold`).
154
191
  */
155
192
  export function useTier(enabled = true, org = null) {
156
- const [tier, setTier] = useState(null);
157
- const [wrong, setWrong] = useState(null);
193
+ const [held, setHeld] = useState(null);
158
194
  const [nonce, setNonce] = useState(0);
195
+ const last = useRef(null);
159
196
  useEffect(() => {
160
197
  if (!enabled)
161
198
  return;
162
199
  const stop = new AbortController();
163
- setWrong(null);
164
- readTier(stop.signal)
165
- .then((got) => {
166
- if (!stop.signal.aborted)
167
- setTier(got);
168
- })
169
- .catch((e) => {
170
- if (!stop.signal.aborted) {
171
- setTier(null);
172
- setWrong(e);
173
- }
174
- });
200
+ // Asking again: the refusal the last read gave is no longer the answer.
201
+ setHeld((h) => (h && h.org === org ? { ...h, wrong: null } : h));
202
+ const settle = (read) => {
203
+ if (stop.signal.aborted)
204
+ return;
205
+ const next = hold(last.current, org, read);
206
+ last.current = next.last;
207
+ setHeld({ org, tier: next.tier, wrong: next.wrong });
208
+ };
209
+ readTier(stop.signal).then((got) => settle({ tier: got }), (e) => settle({ wrong: e }));
175
210
  return () => stop.abort();
176
211
  }, [enabled, nonce, org]);
177
- return { tier, wrong, reload: useCallback(() => setNonce((n) => n + 1), []) };
212
+ return { ...standing(held, org), reload: useCallback(() => setNonce((n) => n + 1), []) };
178
213
  }
179
214
  /**
180
215
  * Whether an organization is its owner's personal one, off IAM's own row.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "description": "The workspace rooms — Home, Chat, Inbox, Contacts, Meet, Cal, Drive, Board, Work, Bots — and the frame that holds them, once, for every Hanzo surface.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",