@hanzo/rooms 0.1.23 → 0.1.25

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/Chat.js CHANGED
@@ -44,13 +44,14 @@ import { Anchor, Box, Button, Text, Tooltip, TooltipContent, TooltipTrigger, Vie
44
44
  import { Control } from "@hanzo/composer";
45
45
  import { useIam, useIamToken } from "@hanzo/iam/react";
46
46
  import { hasSession, org } from "./lib/session.js";
47
- import { chats, sku, useLimits } from "./lib/limits.js";
47
+ import { chats, house, useLimits } from "./lib/limits.js";
48
+ import { parseModels } from "@hanzo/ui/models/catalog";
48
49
  import { LimitedBanner } from "@hanzo/ui/product/LimitedBanner";
49
50
  import { WEB } from "./lib/web.js";
50
51
  import { useHydrated } from "./lib/hydrated.js";
51
52
  import { base, served } from "./lib/ai.js";
52
53
  import { onServed } from "./lib/served.js";
53
- import { REFUSED, speech, useDictation, useTalk, useVoice, Voice } from "@hanzo/voice";
54
+ import { refused as worded, speech, useDictation, useTalk, useVoice, Voice } from "@hanzo/voice";
54
55
  import { ArrowUp, AudioLines, Image as ImageMark, Mic, Paperclip, Square, Star, PanelRight, X } from "lucide-react";
55
56
  import { ENSO, FREE } from "./lib/ai.js";
56
57
  import { openThread, useOpen } from "./open.js";
@@ -63,7 +64,6 @@ import { useStarred } from "./stars.js";
63
64
  import { useModel } from "./model.js";
64
65
  import { Enso, nameOf, useEffort } from "./enso.js";
65
66
  import { checkoutUrl } from './lib/pay.js';
66
- import { onFree } from './lib/plans.js';
67
67
  import { Upgrade, useFree } from './Upgrade.js';
68
68
  import { enter, LOGIN } from './lib/destination.js';
69
69
  import { first } from './lib/first.js';
@@ -518,15 +518,22 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
518
518
  // outputs and names no text (`chats`) is neither offered nor fallen back to.
519
519
  const { models: catalog } = useModels();
520
520
  const models = useMemo(() => catalog.filter(chats), [catalog]);
521
- // HANZO MODELS ALONE are offered here, to everyone: an Enso or Zen id
522
- // (`sku`). A preference kept from before for any other model is not one this
523
- // room offers, so the house router is asked for instead, and `served` keeps
524
- // to the house models wherever the catalog carries one. An agent's own model
525
- // is sent as it is named, never swapped for another the catalog lists; the
526
- // gateway says which model answered (`servedBy`).
527
- const offered = useMemo(() => models.filter((m) => sku(m.id)), [models]);
528
- const wanted = sku(preferred) ? preferred : ENSO;
529
- const model = persona?.model || served(models, anonymous ? FREE : wanted);
521
+ // EVERY MODEL THE GATEWAY LISTS is offered here, through the one picker, and
522
+ // read from the catalog's own fields (`parseModels`): its class, its family.
523
+ // A premium model is picked for THIS conversation and never remembered as
524
+ // the default, so the next chat starts on Enso again; any other pick is the
525
+ // reader's default. A preference kept from before for a premium model is
526
+ // not a default either, so the house router is asked for instead. An agent's
527
+ // own model is sent as it is named; the gateway says which model answered
528
+ // (`servedBy`).
529
+ const listed = useMemo(() => parseModels(catalog), [catalog]);
530
+ const rowOf = (id) => listed.find((m) => m.id === id);
531
+ const [picked, setPicked] = useState(null);
532
+ const wanted = picked ?? (preferred && rowOf(preferred)?.class !== "premium" ? preferred : ENSO);
533
+ // A model the catalog lists is sent as picked, whatever it answers with; only
534
+ // a name the catalog does not list falls to one that converses (`served`).
535
+ const sent = anonymous ? FREE : wanted;
536
+ const model = persona?.model || (rowOf(sent) ? sent : served(models, sent));
530
537
  // THE PLAN'S USAGE (lib/limits.ts), kept current by every chat answer's
531
538
  // headers and read again whenever a send settles, below. The room draws no
532
539
  // bars and no near note — those live on Settings → Usage. It says something
@@ -535,15 +542,19 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
535
542
  const { limits, notice, reload: reread } = useLimits(!anonymous, org(), (id) => nameOf(catalog, id));
536
543
  const held = Boolean(limits?.plan);
537
544
  const pause = notice && (notice.fallback || notice.refused) ? notice : null;
538
- // A MODEL THE FREE PLAN DOES NOT INCLUDE IS ASKED FOR, NOT CHOSEN. On Free,
539
- // picking a model the plan's row does not run (`onFree`) leaves the choice
540
- // where it was and opens the upgrade, titled with the model; so does a turn
541
- // the gateway refuses `plan_required`. A plan the limits name is not Free,
542
- // whatever the tier says, so every Hanzo model is simply chosen on it.
543
- // `isFree` still drives the free-lane notice.
545
+ // EVERY MODEL IS CHOSEN, never asked for: one the plan does not cover still
546
+ // picks, and the gateway answers it with the policy's refusal or from Enso,
547
+ // which the room then shows. A turn the gateway refuses `plan_required`
548
+ // opens the upgrade, titled with the model. `isFree` still drives the
549
+ // free-lane notice.
544
550
  const free = useFree(!anonymous) && !held;
545
551
  const [ask, setAsk] = useState(null);
546
- const choose = (id) => (free && !onFree(id) ? setAsk({ model: nameOf(models, id) }) : setPreferred(id));
552
+ const choose = (id) => {
553
+ if (rowOf(id)?.class === "premium")
554
+ return setPicked(id);
555
+ setPicked(null);
556
+ setPreferred(id);
557
+ };
547
558
  // KEEPING THE CONVERSATION is the reader's account, not this pane's choice:
548
559
  // the store is per-org, so a signed-in reader has somewhere for it to go and
549
560
  // an anonymous one does not. `thread` names the conversation being continued
@@ -560,7 +571,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
560
571
  // lookups the platform's assistant uses, run inside the completion by the
561
572
  // client's fetch (lib/web.ts), so a question about the weather or the news
562
573
  // is answered from what the web says now.
563
- params: { reasoning_effort: effort, ...(!anonymous && sku(model) ? { tools: WEB } : {}) },
574
+ params: { reasoning_effort: effort, ...(!anonymous && house(rowOf(model)?.family) ? { tools: WEB } : {}) },
564
575
  ...(open ? { thread: open } : {}),
565
576
  onError: (e) => {
566
577
  const needs = planRequired(e);
@@ -1198,13 +1209,13 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1198
1209
  // WHAT GOES WITH THE MESSAGE, above the frame: one chip a file, each
1199
1210
  // with its own remove, and the reason any file was refused.
1200
1211
  head: files.length || turnedAway || preparing || talk.open || talk.refusal || pause ? (_jsxs(YStack, { width: "calc(100% - 32px)", maxW: `calc(${MEASURE} - 3rem)`, mx: "auto", gap: "$1.5", children: [pause ? (_jsx(LimitedBanner, { message: pause.message, actions: pause.actions, onUsage: () => showSettings("usage") })) : null, files.length ? (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", role: "list", "aria-label": "Attached files", "data-slot": "composer-attached", children: files.map((one) => (_jsx(HeldChip, { one: one, onRemove: () => drop(at, one.id) }, one.id))) })) : null, preparing ? (_jsxs(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-preparing", fontSize: "$2", color: "$soft", children: [preparing, "\u2026"] })) : null, turnedAway ? (_jsx(Text, { role: "alert", fontSize: "$2", color: BAD, children: turnedAway })) : null, talk.open || talk.refusal ? (_jsx(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-talk-reply", fontSize: "$2", color: talk.refusal ? BAD : "$soft", children: talk.refusal
1201
- ? REFUSED.lost
1212
+ ? worded(talk.refusal)
1202
1213
  : talk.reply
1203
1214
  ? `Hanzo: ${talk.reply}`
1204
1215
  : talk.state === "listening"
1205
1216
  ? "Listening — speak, and Hanzo answers aloud."
1206
1217
  : "Connecting…" })) : null] })) : undefined,
1207
- send: (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Enso, { effort: effort, onEffort: setEffort, model: wanted, onModel: choose, models: offered }), _jsx(Control, { asChild: true, size: ROUND, children: _jsx(Voice, { voice: voice, says: { idle: "Dictate", listening: "Dictating — click to stop" }, children: (state) => (_jsx(Mic, { size: 15, fill: state === "idle" ? "none" : "currentColor" })) }) }), _jsx(Control, { asChild: true, size: ROUND, children: _jsx(Voice, { voice: talk, "data-slot": "composer-talk", says: {
1218
+ send: (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Enso, { effort: effort, onEffort: setEffort, model: wanted, onModel: choose, models: listed, limits: limits }), _jsx(Control, { asChild: true, size: ROUND, children: _jsx(Voice, { voice: voice, says: { idle: "Dictate", listening: "Dictating — click to stop" }, children: (state) => (_jsx(Mic, { size: 15, fill: state === "idle" ? "none" : "currentColor" })) }) }), _jsx(Control, { asChild: true, size: ROUND, children: _jsx(Voice, { voice: talk, "data-slot": "composer-talk", says: {
1208
1219
  idle: "Talk with Hanzo",
1209
1220
  listening: "In conversation — click to hang up",
1210
1221
  speaking: "Hanzo is speaking — talk to interrupt",
package/dist/Meet.js CHANGED
@@ -5,7 +5,7 @@ import { Video, VideoOff, Mic, MicOff, PhoneOff, Copy, Check, Plus, Calendar, Ex
5
5
  import { Text, View, XStack, YStack } from '@hanzo/gui';
6
6
  import { Button } from '@hanzo/ui';
7
7
  import { useIamToken } from '@hanzo/iam/react';
8
- import { REFUSED, speech, useTranscript, useVoice } from '@hanzo/voice';
8
+ import { refused, speech, useTranscript, useVoice } from '@hanzo/voice';
9
9
  import { Face, voiceOf } from './cast.js';
10
10
  import { member } from './team.js';
11
11
  import { cleanForSpeech, speakAgent, stopAgentSpeech, AudioWave, useAgentSpeech } from './speech.js';
@@ -231,7 +231,7 @@ export function Meet() {
231
231
  onPartial: setCaption,
232
232
  onSettled: (text) => setLiveNotes((prev) => [...prev, `${clock()}: You: ${text}`]),
233
233
  });
234
- const transcriberNote = transcriber.reason ?? (transcriber.refusal ? REFUSED.lost : null);
234
+ const transcriberNote = transcriber.reason ?? (transcriber.refusal ? refused(transcriber.refusal) : null);
235
235
  const handleSpokenInput = useCallback(async (transcript) => {
236
236
  const time = clock();
237
237
  setLiveNotes((prev) => [...prev, `${time}: You (voice): ${transcript}`]);
@@ -269,7 +269,7 @@ export function Meet() {
269
269
  };
270
270
  // What the hands-free control says: why it cannot run, or that Hanzo's speech
271
271
  // refused and the browser stood in (or could not) — never silence.
272
- const handsFreeNote = handsFree.reason ?? (handsFree.refusal ? REFUSED[handsFree.refusal.covered ? 'covered' : 'lost'] : null);
272
+ const handsFreeNote = handsFree.reason ?? (handsFree.refusal ? refused(handsFree.refusal) : null);
273
273
  return (_jsxs(YStack, { width: "100%", height: "100%", bg: "$background", overflowX: "auto", overflowY: "auto", children: [_jsxs(XStack, { minH: 56, px: 20, py: 8, gap: 10, flexWrap: "wrap", bg: "$panel", borderBottomWidth: 1, borderBottomColor: "$borderColor", items: "center", justify: "space-between", children: [_jsxs(XStack, { items: "center", gap: 12, shrink: 1, minW: "auto", children: [_jsx(XStack, { width: 32, height: 32, rounded: 8, bg: "$raised", items: "center", justify: "center", shrink: 1, minW: "auto", children: _jsx(Video, { size: 18, color: "var(--foreground)" }) }), _jsxs(YStack, { shrink: 1, minW: "auto", children: [_jsxs(XStack, { items: "center", gap: 8, children: [_jsx(Text, { fontSize: "$4", fontWeight: "700", color: "$ink", children: "Hanzo Meet" }), _jsx(Text, { fontSize: FLOOR, fontWeight: "700", bg: "$hover", color: "$soft", borderWidth: 1, borderColor: "$borderColor", px: 6, py: 1, rounded: 4, children: "LIVE \u00B7 meet.hanzo.ai" })] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Native WebRTC video conferencing with AI meeting bot transcription & voice" })] })] }), activeMeetingRoom ? (_jsxs(XStack, { items: "center", gap: 8, shrink: 1, minW: "auto", flexWrap: "wrap", children: [_jsx(XStack, { bg: "$hover", rounded: 8, p: 2, gap: 2, borderWidth: 1, borderColor: "$borderColor", shrink: 1, minW: "auto", children: [
274
274
  ['iframe', 'SFU Stage', Tv],
275
275
  ['grid', 'Agent Grid', LayoutGrid],
package/dist/Settings.js CHANGED
@@ -30,7 +30,8 @@ import { tap } from './lib/tap.js';
30
30
  import { useIam, useIamIdentity, useOrganizations } from '@hanzo/iam/react';
31
31
  import { useSignOut } from './lib/signout.js';
32
32
  import { useModels } from '@hanzo/ai/react';
33
- import { ModelSelector, RESEARCH } from '@hanzo/ui/models';
33
+ import { ModelPicker } from '@hanzo/ui/models';
34
+ import { parseModels } from '@hanzo/ui/models/catalog';
34
35
  import { UsagePanel } from '@hanzo/usage/panel';
35
36
  import { Catalog } from './Directory.js';
36
37
  import { useModel } from './model.js';
@@ -38,7 +39,7 @@ import { payPage } from './lib/pay.js';
38
39
  import { planName } from './lib/plans.js';
39
40
  import { api } from './lib/api.js';
40
41
  import { renewal, useSubscription, useTier } from './lib/tier.js';
41
- import { sku, spendShown, useLimits } from './lib/limits.js';
42
+ import { spendShown, useLimits } from './lib/limits.js';
42
43
  import { PlanUsage } from '@hanzo/ui/product/PlanUsage';
43
44
  import { Look } from './look.js';
44
45
  import { ProviderMark } from './ProviderMark.js';
@@ -169,20 +170,16 @@ function Pane({ id, note }) {
169
170
  return _jsx(MemorySettings, {});
170
171
  return (_jsx(Text, { fontSize: "$3", color: "$soft", children: note ?? 'Nothing to configure here yet.' }));
171
172
  }
172
- /** One default in one place; agents may still bring their own model. */
173
+ /**
174
+ * One default in one place; agents may still bring their own model. A premium
175
+ * model is never a default: it is picked per conversation, in the composer, so
176
+ * this picker offers the rest of the catalog.
177
+ */
173
178
  function Model() {
174
179
  const { models: served } = useModels();
175
- // Hanzo models alone, as the composer's picker offers them (`sku`).
176
- const models = useMemo(() => served.filter((m) => sku(m.id)), [served]);
180
+ const models = useMemo(() => parseModels(served).filter((m) => m.class !== 'premium'), [served]);
177
181
  const [model, choose] = useModel();
178
- return (_jsxs(YStack, { gap: "$3", maxW: 420, children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: "Used for new chats. An agent with its own model keeps that choice." }), _jsx(XStack, { self: "flex-start", children: _jsx(ModelSelector, { models: [...models, ...RESEARCH], value: model, onChange: choose, size: "sm" }) }), _jsx(YStack, { gap: "$2", mt: "$2", children: models
179
- .filter((m) => !m.id.includes('openrouter'))
180
- .slice(0, 8)
181
- .map((m) => {
182
- const isSel = model === m.id;
183
- const p = m.id.startsWith('zen') ? 'zen' : 'enso';
184
- return (_jsx(Box, { render: "button", onClick: () => choose(m.id), px: "$3", py: "$2", rounded: "$2", borderWidth: 1, borderColor: isSel ? '$ink' : '$borderColor', bg: isSel ? '$raised' : 'transparent', hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsxs(XStack, { items: "center", gap: "$2.5", children: [_jsx(ProviderMark, { provider: p, size: 16 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", flex: 1, children: m.id }), isSel ? (_jsx(Text, { fontSize: "$1", color: "$soft", children: "Active" })) : null] }) }, m.id));
185
- }) })] }));
182
+ return (_jsxs(YStack, { gap: "$3", maxW: 420, children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: "Used for new chats. An agent with its own model keeps that choice. Premium models are picked per conversation in the composer." }), _jsx(XStack, { self: "flex-start", children: _jsx(ModelPicker, { models: models, scope: "chat", value: model, onChange: choose, size: "sm" }) })] }));
186
183
  }
187
184
  /**
188
185
  * WHAT THE GATEWAY SERVES, read from the same `/v1/models` the rooms read.
package/dist/Shell.js CHANGED
@@ -36,7 +36,8 @@ import { hasSession, superAdmin } from "./lib/session.js";
36
36
  import { enter, LOGIN } from "./lib/destination.js";
37
37
  import { address, sharedWithMe } from "./lib/share.js";
38
38
  import { list } from "./lib/list.js";
39
- import { sku } from "./lib/limits.js";
39
+ import { ModelPicker } from "@hanzo/ui/models";
40
+ import { parseModels } from "@hanzo/ui/models/catalog";
40
41
  import { mark } from "./lib/mark.js";
41
42
  import { useOpen } from "./open.js";
42
43
  import { useSpan } from "./span.js";
@@ -667,6 +668,7 @@ function Bots({ show, owner, filter = "", onPick, navigate, }) {
667
668
  const [wrong, setWrong] = useState(null);
668
669
  const picking = useRef(null);
669
670
  const { models } = useModels();
671
+ const listed = useMemo(() => parseModels(models), [models]);
670
672
  const allPeopleMap = new Map();
671
673
  for (const p of (Array.isArray(storedPeople) ? storedPeople : []))
672
674
  if (p?.id)
@@ -776,7 +778,7 @@ function Bots({ show, owner, filter = "", onPick, navigate, }) {
776
778
  setEmoji(e.target.value);
777
779
  if (e.target.value)
778
780
  setAvatar("");
779
- }, placeholder: "\uD83D\uDE42", "aria-label": "Emoji" }), ...FIELD, width: 44, text: "center", px: 4 }), _jsxs(Text, { render: _jsx("select", { value: model, onChange: (e) => setModel(e.target.value), "aria-label": "Model" }), ...FIELD, outlineStyle: undefined, flex: 1, minW: 0, px: 8, children: [_jsx("option", { value: "enso", children: "enso" }), models.filter((one) => sku(one.id)).map((one) => (_jsx("option", { value: one.id, children: one.id }, one.id)))] }), _jsx(Box, { render: "button", onClick: () => {
781
+ }, placeholder: "\uD83D\uDE42", "aria-label": "Emoji" }), ...FIELD, width: 44, text: "center", px: 4 }), _jsx(YStack, { flex: 1, minW: 0, children: _jsx(ModelPicker, { models: listed, scope: "chat", value: model, onChange: setModel, size: "sm" }) }), _jsx(Box, { render: "button", onClick: () => {
780
782
  if (!busy)
781
783
  void add();
782
784
  }, "aria-disabled": busy || !name.trim(), opacity: busy || !name.trim() ? 0.5 : 1, px: "$2.5", py: "$1.5", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: "$raised" }, children: _jsx(Text, { fontSize: "$1", color: "$ink", children: busy ? "Adding" : "Add" }) })] }), wrong ? (_jsx(Text, { fontSize: "$1", color: BAD, numberOfLines: 2, children: wrong })) : null] })) : null, open ? (_jsxs(YStack, { pl: "$4", gap: "$0.5", children: [broke ? (_jsx(Note, { children: say(broke, "this org's roster") })) : waiting && empty ? (_jsx(Note, { children: "Reading the roster." })) : empty ? (_jsx(Note, { children: show === "agents" ? "No agents yet. Add one with +." : "No one here yet. Invite a teammate." })) : null, folk.map((person) => {
package/dist/Vibe.js CHANGED
@@ -26,9 +26,10 @@ import { useEffect, useMemo, useRef, useState } from 'react';
26
26
  import { useRooms } from './host.js';
27
27
  import { ArrowUp, Globe, Monitor, RefreshCw, Smartphone, Sparkles, Tablet, UserPlus, Users, } from 'lucide-react';
28
28
  import { View, XStack, YStack, Text } from '@hanzo/gui';
29
- import { Button, Picker } from '@hanzo/ui';
29
+ import { Button } from '@hanzo/ui';
30
30
  import { AiProvider, useAgents, useModels, usePeople } from '@hanzo/ai/react';
31
- import { sku } from './lib/limits.js';
31
+ import { ModelPicker } from '@hanzo/ui/models';
32
+ import { parseModels } from '@hanzo/ui/models/catalog';
32
33
  import { useIam, useOrganizations } from '@hanzo/iam/react';
33
34
  import { hasSession } from './lib/session.js';
34
35
  import { useHydrated } from './lib/hydrated.js';
@@ -122,9 +123,9 @@ function VibeWorkspaceInner() {
122
123
  function Vibe() {
123
124
  const { Link } = useRooms();
124
125
  const { user } = useIam();
125
- // Hanzo models alone, as every picker here offers them (`sku`).
126
- const { models: listed } = useModels();
127
- const models = useMemo(() => listed.filter((m) => sku(m.id)), [listed]);
126
+ // Every model the gateway lists, through the one picker.
127
+ const { models: catalog } = useModels();
128
+ const models = useMemo(() => parseModels(catalog), [catalog]);
128
129
  const [activeTabMobile, setActiveTabMobile] = useState('stream');
129
130
  // The frame shows what the bar says. It starts empty because a run publishes
130
131
  // no URL — the 202 names a session, not an address — so this room has nothing
@@ -136,7 +137,7 @@ function Vibe() {
136
137
  // The house name for the lane, resolved against what the gateway serves —
137
138
  // `served` falls to a model that answers rather than sending one nobody has.
138
139
  const [wanted, setWanted] = useState(user ? ENSO : FREE);
139
- const model = served(models, wanted);
140
+ const model = models.some((m) => m.id === wanted) ? wanted : served(models, wanted);
140
141
  const [busy, setBusy] = useState(false);
141
142
  const [turns, setTurns] = useState([]);
142
143
  const bottom = useRef(null);
@@ -168,7 +169,7 @@ function Vibe() {
168
169
  };
169
170
  return (_jsxs(YStack, { "data-vibe": true, flex: 1, minH: 0, height: "100vh", width: "100%", maxW: "100%", overflowX: "auto", overflowY: "hidden", bg: "$background", pb: 56, children: [_jsxs(XStack, { height: 52, items: "center", justify: "space-between", px: "$3.5", borderBottomWidth: 1, borderColor: "$borderColor", bg: "$background", z: 30, shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2.5", children: [_jsx(TopOrgSelector, {}), _jsxs(XStack, { display: "none", $md: { display: 'flex' }, items: "center", gap: "$2", py: 4, px: 10, rounded: 20, bg: "$panel", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(View, { width: 7, height: 7, rounded: 4, bg: busy ? 'var(--state-online)' : '$faint' }), _jsx(Text, { fontSize: 11, color: "$quiet", fontWeight: "500", children: busy ? 'Starting a run' : 'Idle' })] })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(XStack, { $md: { display: 'none' }, bg: "$panel", rounded: 8, p: 2, gap: 2, borderWidth: 1, borderColor: "$borderColor", children: ['stream', 'preview', 'team'].map((tab) => (_jsx(Button, { size: "sm", px: "$2", variant: activeTabMobile === tab ? 'secondary' : 'ghost', color: activeTabMobile === tab ? '$ink' : '$soft', "aria-pressed": activeTabMobile === tab, onClick: () => setActiveTabMobile(tab), children: tab === 'stream' ? 'Stream' : tab === 'preview' ? 'Preview' : 'Team' }, tab))) }), _jsx(Button, { asChild: true, variant: "primary", size: "sm", children: _jsxs(Link, { href: invite(), children: [_jsx(UserPlus, { size: 13 }), "Invite"] }) })] })] }), _jsxs(View, { flex: 1, minH: 0, width: "100%", height: "100%", overflow: "hidden", display: "grid", gridTemplateRows: "100%", gridTemplateColumns: "100%", $md: { gridTemplateColumns: 'minmax(320px, 380px) minmax(0, 1fr)' }, $lg: { gridTemplateColumns: 'minmax(340px, 400px) minmax(360px, 1fr) minmax(300px, 340px)' }, $xl: { gridTemplateColumns: 'minmax(380px, 440px) minmax(480px, 1fr) minmax(340px, 380px)' }, children: [_jsxs(YStack, { height: "100%", borderRightWidth: 1, borderColor: "$borderColor", bg: "$background", minW: 0, display: activeTabMobile === 'stream' ? 'flex' : 'none', $md: { display: 'flex' }, children: [_jsxs(XStack, { height: 42, items: "center", gap: "$2", px: "$3.5", borderBottomWidth: 1, borderColor: "$borderColor", bg: "$background", children: [_jsx(Sparkles, { size: 14, color: "var(--foreground)" }), _jsx(Text, { fontSize: 12, fontWeight: "600", color: "$ink", children: "Runs" }), turns.length > 0 && (_jsx(View, { px: 6, py: 2, rounded: 4, bg: "$hover", children: _jsx(Text, { fontSize: 10, color: "$soft", children: turns.length }) }))] }), _jsxs(YStack, { flex: 1, overflowY: "auto", overflowX: "hidden", p: "$3.5", gap: "$4", children: [turns.length === 0 ? (_jsx(Quiet, { children: "Nothing has been sent from this room yet. Describe a task below and the run it opens appears here." })) : null, turns.map((turn) => (_jsxs(YStack, { gap: "$2.5", children: [_jsxs(XStack, { gap: "$2.5", children: [_jsx(View, { width: 24, height: 24, rounded: 12, bg: "$raised", items: "center", justify: "center", shrink: 0, children: _jsx(Text, { fontSize: 10, fontWeight: "700", color: "$ink", children: turn.who.charAt(0).toUpperCase() }) }), _jsxs(YStack, { flex: 1, gap: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Text, { fontSize: 12, fontWeight: "700", color: "$ink", children: turn.who }), _jsx(Text, { fontSize: 11, color: "$soft", children: turn.time }), _jsx(View, { px: 5, py: 1, rounded: 4, bg: "$hover", children: _jsx(Text, { fontSize: 10, color: "$soft", fontFamily: "$mono", children: turn.model }) })] }), _jsx(Text, { fontSize: 12, color: "$quiet", lineHeight: 18, children: turn.prompt })] })] }), _jsx(YStack, { ml: "$4", gap: "$1.5", children: turn.refused ? (_jsx(Text, { fontSize: 11, color: "$ink", lineHeight: 16, children: turn.refused })) : !turn.run ? (_jsx(Quiet, { children: "Starting." })) : (_jsxs(YStack, { p: "$2", rounded: 8, bg: "$panel", borderWidth: 1, borderColor: "$borderColor", gap: "$1", children: [_jsx(Text, { fontSize: 11, color: "$quiet", fontFamily: "$mono", children: turn.run.sessionId }), _jsxs(Text, { fontSize: 11, color: "$soft", children: [turn.run.routed
170
171
  ? `Running on ${turn.run.targetId}`
171
- : 'Running in a sandbox', turn.run.branch ? ` · ${turn.run.branch}` : ''] }), _jsx(Text, { render: _jsx(Link, { href: dev(turn.run.sessionId) }), color: "$ink", fontSize: "$1", children: "Watch it work" })] })) })] }, turn.id))), _jsx("div", { ref: bottom })] }), _jsxs(YStack, { p: "$3", borderTopWidth: 1, borderColor: "$borderColor", bg: "$background", gap: "$2", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsxs(Picker, { value: model, onChange: (e) => setWanted(e.target.value), "aria-label": "Model", maxW: 200, children: [models.length === 0 ? _jsx("option", { value: model, children: model }) : null, models.map((m) => (_jsx("option", { value: m.id, children: m.id }, m.id)))] }), _jsx(Text, { fontSize: 10, color: "$faint", children: "Press Enter to send" })] }), _jsxs(YStack, { bg: "$panel", borderWidth: 1, borderColor: "var(--border-control)", rounded: 12, overflow: "hidden", children: [_jsx(Text, { render: _jsx("textarea", { value: agentInput, onChange: (e) => setAgentInput(e.target.value), onKeyDown: (e) => {
172
+ : 'Running in a sandbox', turn.run.branch ? ` · ${turn.run.branch}` : ''] }), _jsx(Text, { render: _jsx(Link, { href: dev(turn.run.sessionId) }), color: "$ink", fontSize: "$1", children: "Watch it work" })] })) })] }, turn.id))), _jsx("div", { ref: bottom })] }), _jsxs(YStack, { p: "$3", borderTopWidth: 1, borderColor: "$borderColor", bg: "$background", gap: "$2", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(ModelPicker, { models: models, scope: "chat", value: model, onChange: setWanted, size: "sm", quiet: true }), _jsx(Text, { fontSize: 10, color: "$faint", children: "Press Enter to send" })] }), _jsxs(YStack, { bg: "$panel", borderWidth: 1, borderColor: "var(--border-control)", rounded: 12, overflow: "hidden", children: [_jsx(Text, { render: _jsx("textarea", { value: agentInput, onChange: (e) => setAgentInput(e.target.value), onKeyDown: (e) => {
172
173
  if (e.key === 'Enter' && !e.shiftKey) {
173
174
  e.preventDefault();
174
175
  send();
package/dist/enso.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type ModelCatalogEntry, type PauseSource } from '@hanzo/ui/models';
1
2
  /**
2
3
  * How hard to think — and, one press inside, what thinks.
3
4
  *
@@ -45,7 +46,7 @@ export type EffortId = (typeof EFFORTS)[number]['id'];
45
46
  */
46
47
  export declare function useEffort(): readonly [EffortId, (next: EffortId) => void];
47
48
  /** The name a served model goes by: its row's label, its name, or its id made readable. */
48
- export declare function nameOf(models: {
49
+ export declare function nameOf(models: readonly {
49
50
  id: string;
50
51
  }[], id: string): string;
51
52
  export interface EnsoProps {
@@ -55,9 +56,10 @@ export interface EnsoProps {
55
56
  /** The override. `models` empty hides it rather than offering an empty menu. */
56
57
  model?: string;
57
58
  onModel?: (id: string) => void;
58
- models?: {
59
- id: string;
60
- }[];
59
+ /** The catalog, as `parseModels` reads `GET /v1/models`: every model is offered. */
60
+ models?: readonly ModelCatalogEntry[];
61
+ /** The payer's plan usage: a paused model says so and still picks. */
62
+ limits?: PauseSource | null;
61
63
  disabled?: boolean;
62
64
  }
63
65
  /**
@@ -67,4 +69,4 @@ export interface EnsoProps {
67
69
  * (`ENSO`, or `FREE` on the free route), and the model itself once one is
68
70
  * picked. Beside the name it states the effort where the surface sends one.
69
71
  */
70
- export declare function Enso({ effort, onEffort, model, onModel, models, disabled }: EnsoProps): import("react").JSX.Element;
72
+ export declare function Enso({ effort, onEffort, model, onModel, models, limits, disabled }: EnsoProps): import("react").JSX.Element;
package/dist/enso.js CHANGED
@@ -3,7 +3,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
3
3
  import { useEffect, useState } from 'react';
4
4
  import { Check, ChevronDown } from 'lucide-react';
5
5
  import { Box, Popover, PopoverContent, PopoverTrigger, Separator, Text, XStack, YStack, } from '@hanzo/ui';
6
- import { ModelSelector, RESEARCH } from '@hanzo/ui/models';
6
+ import { ModelPicker } from '@hanzo/ui/models';
7
7
  import { ProviderMark } from './ProviderMark.js';
8
8
  import { ENSO, FREE } from './lib/ai.js';
9
9
  import { modelName } from './lib/models.js';
@@ -90,7 +90,7 @@ export function nameOf(models, id) {
90
90
  * (`ENSO`, or `FREE` on the free route), and the model itself once one is
91
91
  * picked. Beside the name it states the effort where the surface sends one.
92
92
  */
93
- export function Enso({ effort, onEffort, model, onModel, models = [], disabled }) {
93
+ export function Enso({ effort, onEffort, model, onModel, models = [], limits, disabled }) {
94
94
  const [open, setOpen] = useState(false);
95
95
  const chosen = EFFORTS.find((e) => e.id === effort);
96
96
  const pace = onEffort && chosen ? chosen.label : null;
@@ -105,7 +105,7 @@ export function Enso({ effort, onEffort, model, onModel, models = [], disabled }
105
105
  onEffort(e.id);
106
106
  setOpen(false);
107
107
  }, px: "$2", py: "$2", rounded: "$3", width: "100%", hoverStyle: { bg: '$hover' }, "aria-pressed": e.id === effort, children: _jsxs(XStack, { items: "center", gap: "$2", width: "100%", children: [_jsxs(YStack, { flex: 1, minW: 0, items: "flex-start", children: [_jsx(Text, { fontSize: "$2", color: "$ink", text: "left", children: e.label }), _jsx(Text, { fontSize: "$1", color: "$soft", text: "left", children: e.note })] }), e.id === effort ? _jsx(Check, { size: 14, "aria-hidden": true }) : null] }) }, e.id)))
108
- : null, onModel && models.length ? (_jsxs(_Fragment, { children: [onEffort ? _jsx(Separator, { my: "$1" }) : null, _jsxs(YStack, { px: "$2", pt: "$1", pb: "$2", gap: "$2", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Enso picks the model. Override it for this account:" }), _jsx(ModelSelector, { models: [...models, ...RESEARCH], value: model, onChange: (id) => {
108
+ : null, onModel && models.length ? (_jsxs(_Fragment, { children: [onEffort ? _jsx(Separator, { my: "$1" }) : null, _jsxs(YStack, { px: "$2", pt: "$1", pb: "$2", gap: "$2", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Enso picks the model. Override it for this account:" }), _jsx(ModelPicker, { models: models, limits: limits, value: model, onChange: (id) => {
109
109
  onModel(id);
110
110
  setOpen(false);
111
111
  }, size: "sm" })] })] })) : null] }) })] }));
@@ -20,8 +20,8 @@ export declare const useLimits: (enabled?: boolean, org?: string | null, name?:
20
20
  * plan, or could not be read. A plan holder sees usage as shares, never money.
21
21
  */
22
22
  export declare const spendShown: ({ limits, answered }: Pick<UseLimits, 'limits' | 'answered'>) => boolean;
23
- /** Whether a model is a Hanzo SKU — an Enso or Zen id — the only models a consumer surface offers. */
24
- export declare const sku: (id: string) => boolean;
23
+ /** Whether a catalog family is one of Hanzo's chat families, Enso or Zen, whose turns carry the live web. */
24
+ export declare const house: (family: string | undefined) => boolean;
25
25
  /**
26
26
  * Whether a model can answer a chat turn: it names no outputs, or names text
27
27
  * among them. `zen-scribe` (a transcript) and `zen-voice-mini` (audio) are
@@ -28,8 +28,8 @@ export const useLimits = (enabled = true, org = null, name) => useHeld(enabled ?
28
28
  * plan, or could not be read. A plan holder sees usage as shares, never money.
29
29
  */
30
30
  export const spendShown = ({ limits, answered }) => answered && !limits?.plan;
31
- /** Whether a model is a Hanzo SKU — an Enso or Zen id — the only models a consumer surface offers. */
32
- export const sku = (id) => /^(hanzo\/)?(enso|zen)/i.test(id);
31
+ /** Whether a catalog family is one of Hanzo's chat families, Enso or Zen, whose turns carry the live web. */
32
+ export const house = (family) => family === 'enso' || family === 'zen';
33
33
  /**
34
34
  * Whether a model can answer a chat turn: it names no outputs, or names text
35
35
  * among them. `zen-scribe` (a transcript) and `zen-voice-mini` (audio) are
@@ -150,8 +150,6 @@ export declare function charge(plan: SubscriptionPlan, interval: Interval): numb
150
150
  export declare function quote(plan: SubscriptionPlan, interval: Interval): string | null;
151
151
  /** What a year saves against twelve months, as a whole percent: 1 − year ÷ (12 × month). Null where it saves nothing. */
152
152
  export declare function saving(plan: SubscriptionPlan): number | null;
153
- /** Whether the Free plan runs a model: one its row names, or any `:free` route. */
154
- export declare function onFree(model: string): boolean;
155
153
  /**
156
154
  * The catalog's own name for a plan slug — "max-20x" reads as "Max 20x" —
157
155
  * or null where the slug is empty or the catalog does not carry it.
package/dist/lib/plans.js CHANGED
@@ -222,17 +222,6 @@ export function saving(plan) {
222
222
  return null;
223
223
  return Math.round((1 - year / (12 * month)) * 100);
224
224
  }
225
- /**
226
- * The models the Free plan runs, as @hanzo/plans states them: `ai.models` on
227
- * the `free` row. Read on first ask and not at import, so a page that only
228
- * formats money or names the pay page carries none of the catalogue.
229
- */
230
- let freeModels = null;
231
- /** Whether the Free plan runs a model: one its row names, or any `:free` route. */
232
- export function onFree(model) {
233
- freeModels ??= new Set(subscriptionPlans.find((p) => p.id === "free")?.entitlements?.["ai.models"] ?? []);
234
- return freeModels.has(model) || model.endsWith(":free");
235
- }
236
225
  /**
237
226
  * The catalog's own name for a plan slug — "max-20x" reads as "Max 20x" —
238
227
  * or null where the slug is empty or the catalog does not carry it.
package/dist/speech.js CHANGED
@@ -18,7 +18,7 @@ import { Volume2, Square } from 'lucide-react';
18
18
  import { View, XStack } from '@hanzo/gui';
19
19
  import { Button } from '@hanzo/ui';
20
20
  import { useIamToken } from '@hanzo/iam/react';
21
- import { mouth, speech, REFUSED } from '@hanzo/voice';
21
+ import { mouth, refused, speech } from '@hanzo/voice';
22
22
  import { GOOD } from './lib/mix.js';
23
23
  import { base } from './lib/ai.js';
24
24
  import { voiceOf, voiceProfileOf } from './cast.js';
@@ -211,7 +211,7 @@ export function SpeakButton({ text, agentName, compact = false, }) {
211
211
  const voice = voiceOf(agentName);
212
212
  // A browser voice standing in for a refused platform sounds like success;
213
213
  // the label is the one place that says otherwise.
214
- const stood = refusal ? ` ${REFUSED[refusal.covered ? 'covered' : 'lost']}` : '';
214
+ const stood = refusal ? ` ${refused(refusal)}` : '';
215
215
  // An @hanzo/ui Button: ghost at rest, the quiet pressed ground while it
216
216
  // speaks. The word for speaking is the one status here, so it alone takes
217
217
  // the success ink (GOOD holds 4.5:1 on either theme); the icons follow the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
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",
@@ -56,9 +56,9 @@
56
56
  "@hanzo/iam": ">=0.21.28",
57
57
  "@hanzo/logo": ">=1.0.26",
58
58
  "@hanzo/plans": ">=1.8.9",
59
- "@hanzo/ui": ">=8.27.57",
59
+ "@hanzo/ui": ">=8.27.67",
60
60
  "@hanzo/usage": ">=0.1.18",
61
- "@hanzo/voice": ">=0.1.12",
61
+ "@hanzo/voice": ">=0.1.16",
62
62
  "@hanzogui/shell": "^8.3.20",
63
63
  "lucide-react": ">=1.42.0",
64
64
  "react": ">=19.2.8",
@@ -66,18 +66,18 @@
66
66
  },
67
67
  "devDependencies": {
68
68
  "@hanzo/ai": "0.6.20",
69
- "@hanzo/appearance": "0.2.6",
69
+ "@hanzo/appearance": "0.2.8",
70
70
  "@hanzo/build": "0.2.34",
71
71
  "@hanzo/composer": "0.2.0",
72
- "@hanzo/design": "0.5.33",
72
+ "@hanzo/design": "0.5.34",
73
73
  "@hanzo/event": "0.3.56",
74
74
  "@hanzo/gui": "8.3.5",
75
75
  "@hanzo/iam": "0.21.28",
76
76
  "@hanzo/logo": "1.0.26",
77
77
  "@hanzo/plans": "1.8.9",
78
- "@hanzo/ui": "8.27.57",
78
+ "@hanzo/ui": "8.27.67",
79
79
  "@hanzo/usage": "0.1.18",
80
- "@hanzo/voice": "0.1.12",
80
+ "@hanzo/voice": "0.1.16",
81
81
  "@hanzogui/config": "8.3.5",
82
82
  "@hanzogui/core": "8.3.5",
83
83
  "@hanzogui/lucide-icons-2": "8.3.5",