@hanzo/rooms 0.1.38 → 0.1.39

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/Column.js CHANGED
@@ -4,17 +4,20 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // New, Dev's places, the Recents of the mode it is in, and at the foot the
5
5
  // account (components/workspace/Me.tsx).
6
6
  //
7
+ // Dev's places are @hanzo/build's `nav`, the one list every rail draws, each a
8
+ // row of its own under a quiet group label: nothing waits behind a More.
9
+ //
7
10
  // It is a pane on the frame's ground (app/_web.tsx), sized by the edge in the
8
11
  // gutter to its right: dragged, keyed or double-clicked back (@hanzo/build's
9
12
  // `Grip`), kept per browser. Pulled in past its floor it collapses to a rail of
10
13
  // marks, kept under the key the builder's rail kept it under, so a reader's
11
14
  // choice outlives the move.
12
15
  import { Text, XStack, YStack } from '@hanzo/gui';
13
- import { ChevronDown, ChevronUp, PanelLeft, Plus, Search } from 'lucide-react';
14
- import { Grip, nav, route } from '@hanzo/build';
16
+ import { PanelLeft, Plus, Search } from 'lucide-react';
17
+ import { Grip, nav, NEW } from '@hanzo/build';
15
18
  import { Sidebar, SidebarIconButton, SidebarItem } from '@hanzo/ui/chat';
16
19
  import { HanzoMark } from '@hanzogui/shell';
17
- import { useState } from 'react';
20
+ import { useId, useState } from 'react';
18
21
  import { ModeMark } from './ModeMark.js';
19
22
  import { useKept } from './kept.js';
20
23
  import { Made, Me } from './Me.js';
@@ -49,22 +52,20 @@ const keys = (press) => ({
49
52
  press();
50
53
  },
51
54
  });
52
- /** A row of the column: a pill, lit where the reader is. */
53
- const ROW = { rounded: '$4', minH: 34 };
55
+ /** A row of the column: a pill, lit where the reader is. Compact, so every place fits a laptop's height with Recents under them. */
56
+ const ROW = { rounded: '$4', minH: 30, py: 0 };
54
57
  export function Column({ host, mode, thread, onChat, onDev, go, drawer = false, }) {
55
58
  const [kept, setCollapsed] = useKept('hanzo.build.rail', false);
56
59
  const collapsed = kept && !drawer;
57
- const [unfolded, setUnfolded] = useState(false);
58
60
  const [words, setWords] = useState('');
61
+ // The desktop column and a phone's drawer can both be in the page: each names its own group labels.
62
+ const uid = useId();
59
63
  const span = useSpan(SPAN, WIDTH, FLOOR, CEIL);
60
64
  const dev = (p) => host.go(p);
61
- const r = route(host.path);
62
- const fresh = mode === 'chat' ? !thread : r.kind === 'new';
63
- const { links, more } = nav(host, dev);
65
+ const groups = nav(host, dev);
64
66
  const open = (row) => (row.kind === 'chat' ? go(talk(row.id)) : dev(row.id));
67
+ // Chat's New; Dev's is the first of its places.
65
68
  const create = () => {
66
- if (mode === 'dev')
67
- return dev('');
68
69
  empty();
69
70
  go(talk());
70
71
  };
@@ -74,15 +75,15 @@ export function Column({ host, mode, thread, onChat, onDev, go, drawer = false,
74
75
  window.location.assign(mode === 'dev' ? app('') : talk());
75
76
  };
76
77
  if (collapsed)
77
- return (_jsx(YStack, { "data-slot": "app-column", "data-collapsed": "", position: "relative", shrink: 0, width: RAIL, "$max-md": { display: 'none' }, children: _jsxs(Sidebar, { "data-slot": "app-rail", "data-collapsed": "true", role: "navigation", "aria-label": brand(), ...pane(), ...FILL, px: "$1.5", py: "$2.5", gap: "$1.5", items: "center", children: [_jsx(SidebarIconButton, { label: "Expand sidebar", onPress: () => setCollapsed(false), width: 36, height: 36, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }), _jsx(ModeMark, { mode: mode, onChat: onChat, onDev: onDev, stacked: true }), _jsx(SidebarIconButton, { label: mode === 'chat' ? 'New chat' : 'New run', onPress: create, width: 36, height: 36, rounded: 999, children: _jsx(Plus, { size: 16 }) }), _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, onOrg: org })] }) }));
78
+ return (_jsx(YStack, { "data-slot": "app-column", "data-collapsed": "", position: "relative", shrink: 0, width: RAIL, "$max-md": { display: 'none' }, children: _jsxs(Sidebar, { "data-slot": "app-rail", "data-collapsed": "true", role: "navigation", "aria-label": brand(), ...pane(), ...FILL, px: "$1.5", py: "$2.5", gap: "$1.5", items: "center", children: [_jsx(SidebarIconButton, { label: "Expand sidebar", onPress: () => setCollapsed(false), width: 36, height: 36, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }), _jsx(ModeMark, { mode: mode, onChat: onChat, onDev: onDev, stacked: true }), mode === 'chat' ? (_jsx(SidebarIconButton, { label: "New chat", onPress: create, width: 36, height: 36, rounded: 999, children: _jsx(Plus, { size: 16 }) })) : (
79
+ // Every place, as its mark: collapsed is narrower, never shorter of a destination.
80
+ groups.map((g) => (_jsx(YStack, { role: "group", "aria-label": g.label, items: "center", gap: 2, pt: "$1", children: g.links.map((link) => (_jsx(SidebarIconButton, { label: link.label, onPress: () => link.onPress?.(), width: 36, height: 32, rounded: 999, "aria-current": link.active ? 'page' : undefined, children: link.icon }, link.id))) }, g.id)))), _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, onOrg: org })] }) }));
78
81
  return (_jsxs(YStack, { "data-slot": "app-column", ref: drawer ? undefined : span.ref,
79
82
  // inline-style: `--span`, the width a drag rewrites on the element itself (span.ts); no prop sets a variable.
80
83
  style: drawer ? undefined : span.style, position: "relative", shrink: 0, height: "100%", width: drawer ? '100%' : 'var(--span)', ...(drawer ? {} : { '$max-md': { display: 'none' } }), children: [_jsxs(Sidebar, { "data-slot": "app-rail", "data-collapsed": "false", role: "navigation", "aria-label": brand(), ...pane(), ...FILL, px: "$2.5", pt: "$3", pb: "$2", gap: "$0", children: [_jsxs(XStack, { "data-slot": "mode-switch", items: "center", gap: "$2", px: "$1.5", pb: "$3", minW: 0, children: [_jsx(HanzoMark, { size: 18 }), _jsx(ModeMark, { mode: mode, onChat: onChat, onDev: onDev })] }), _jsxs(XStack, { "data-slot": "app-filter", items: "center", gap: "$2", px: "$3", height: 36, rounded: 999, borderWidth: 1, borderColor: "$borderColor", bg: "$edge", shrink: 0, children: [_jsx(Search, { size: 15, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }), _jsx(Text, { render: _jsx("input", { value: words, onChange: (e) => setWords(e.target.value), onKeyDown: (e) => {
81
84
  if (e.key === 'Escape')
82
85
  setWords('');
83
- }, placeholder: "Filter", "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }), _jsxs(YStack, { role: "list", mt: "$2.5", gap: 2, shrink: 0, children: [_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: fresh, icon: _jsx(Plus, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: mode === 'chat' ? 'New chat' : 'New run' }) }), mode === 'dev'
84
- ? links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id)))
85
- : null, mode === 'dev' ? (_jsxs(YStack, { role: "listitem", children: [_jsx(SidebarItem, { ...ROW, "aria-expanded": unfolded, icon: unfolded ? _jsx(ChevronUp, { size: 16, "aria-hidden": true }) : _jsx(ChevronDown, { size: 16, "aria-hidden": true }), onPress: () => setUnfolded((v) => !v), ...keys(() => setUnfolded((v) => !v)), children: "More" }), unfolded ? (_jsx(YStack, { role: "list", gap: 2, children: more.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id))) })) : null] })) : null] }), _jsx(XStack, { height: 1, bg: "$borderColor", mx: "$1.5", mt: "$3", shrink: 0 }), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open, onNew: create }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] }), _jsxs(XStack, { "data-slot": "app-foot", items: "center", gap: "$1", pt: "$2", shrink: 0, children: [_jsx(YStack, { flex: 1, minW: 0, children: _jsx(Me, { host: host, onOrg: org }) }), drawer ? null : (_jsx(SidebarIconButton, { label: "Collapse sidebar", onPress: () => setCollapsed(true), width: 32, height: 32, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }))] })] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: () => setCollapsed(true), label: "Resize sidebar",
86
+ }, placeholder: mode === 'chat' ? 'Filter chats' : 'Filter runs', "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }), mode === 'chat' ? (_jsx(YStack, { role: "list", mt: "$2.5", gap: 2, shrink: 0, children: _jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: !thread, icon: _jsx(Plus, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: "New chat" }) }) })) : (_jsx(YStack, { "data-slot": "app-places", mt: "$1.5", shrink: 0, children: groups.map((g) => (_jsxs(YStack, { pt: "$1.5", children: [_jsx(Text, { id: `${uid}-${g.id}`, pl: "$2", pb: 2, fontSize: "$1", color: "$soft", fontWeight: "600", textTransform: "uppercase", letterSpacing: 0.6, children: g.label }), _jsx(YStack, { role: "list", "aria-labelledby": `${uid}-${g.id}`, gap: 1, children: g.links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, ...(link.id === NEW ? { 'data-slot': 'app-new' } : {}), active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id))) })] }, g.id))) })), _jsx(XStack, { height: 1, bg: "$borderColor", mx: "$1.5", mt: "$3", shrink: 0 }), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open, onNew: create }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] }), _jsxs(XStack, { "data-slot": "app-foot", items: "center", gap: "$1", pt: "$2", shrink: 0, children: [_jsx(YStack, { flex: 1, minW: 0, children: _jsx(Me, { host: host, onOrg: org }) }), drawer ? null : (_jsx(SidebarIconButton, { label: "Collapse sidebar", onPress: () => setCollapsed(true), width: 32, height: 32, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }))] })] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: () => setCollapsed(true), label: "Resize sidebar",
86
87
  // In the gutter between the column and the pane, not over the column's own rows.
87
88
  r: GUTTER_END, width: "var(--pane-gap)" }))] }));
88
89
  }
@@ -1,14 +1,9 @@
1
1
  /**
2
- * The four ways this experience is customized, and the whole taxonomy.
3
- *
4
- * APPS are connected services — the thing that used to be called Connectors,
5
- * which named the wire rather than what a reader installs. CHANNELS are
6
- * transports: where a conversation reaches you. PLUGINS extend the client.
7
- * SKILLS extend what an agent knows how to do.
8
- *
9
- * Order is the ruling's: Apps, Channels, Plugins, Skills.
2
+ * What this directory lists. APPS are what Hanzo works with — the public
3
+ * catalog, each row a page. CHANNELS are transports: where a conversation
4
+ * reaches you. Skills and plugins are Customize's (Settings links there).
10
5
  */
11
- export type Tab = 'apps' | 'channels' | 'plugins' | 'skills';
6
+ export type Tab = 'apps' | 'channels';
12
7
  export declare function Directory({ open, onClose, tab: first, }: {
13
8
  open: boolean;
14
9
  onClose: () => void;
package/dist/Directory.js CHANGED
@@ -20,11 +20,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
20
20
  // row shown as a small badge, never a section boundary and never a second
21
21
  // install flow — one `+` everywhere, branching internally.
22
22
  //
23
- // FOUR TABS, and the channels are tiles. A channel is a provider the org can
23
+ // TWO TABS, and the channels are tiles. A channel is a provider the org can
24
24
  // hold an account with, read from `/v1/integrations` — which says whether this
25
25
  // deployment can open a connection and whether this org has one — and every
26
26
  // tile's button is a call to that registry. The Inbox's "Connect an app" opens
27
- // this same directory on this tab; there is no second grid.
27
+ // this same directory on this tab; there is no second grid. Skills, plugins and
28
+ // connectors are what an agent brings to a run, and they have one home, Dev's
29
+ // Customize (@hanzo/build); Settings links there rather than drawing a copy.
28
30
  //
29
31
  // COUNTS ARE DERIVED. The catalog is counted, never typed. None of these
30
32
  // registries serves a download figure, so no card draws one: the rows carried
@@ -33,12 +35,12 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
33
35
  // deleted a "600+ integrations" claim for the same reason.
34
36
  //
35
37
  // SIGNED IN, EACH TAB READS ITS OWN REGISTRY. Apps is this site's public catalog
36
- // AND the org's connected accounts; Channels, Plugins and Skills are the org's
37
- // own lists at `/v1/channels`, `/v1/tools/plugins`, `/v1/tools/skills`. Those
38
- // three need the bearer, so signed-out asks the reader to sign in rather than
39
- // drawing a catalogue nobody can install from, and a registry that cannot be
40
- // reached says so and points at the console rather than claiming the list is
41
- // empty. `null` is unknown; it never means zero.
38
+ // AND the org's connected accounts; Channels is the org's own list at
39
+ // `/v1/integrations`. That one needs the bearer, so signed-out asks the reader to
40
+ // sign in rather than drawing a catalogue nobody can connect from, and a
41
+ // registry that cannot be reached says so rather than claiming the list is
42
+ // empty. Nothing stands in for an answer: no row is drawn that the registry did
43
+ // not send, and none is marked Connected that it did not mark.
42
44
  import { useEffect, useMemo, useState } from 'react';
43
45
  import { ArrowUpRight, Check, Plus, Search, X } from 'lucide-react';
44
46
  import { Box, Dialog, DialogContent, DialogTitle, Input, Text, XStack, YStack, } from '@hanzo/ui';
@@ -53,31 +55,12 @@ import { Action } from '@hanzo/ui/marketing';
53
55
  import { tap } from './lib/tap.js';
54
56
  import { GOOD } from './lib/mix.js';
55
57
  import { site } from './where.js';
56
- /**
57
- * The console, which is a PRODUCT ADDRESS rather than an environment.
58
- *
59
- * The gateway is `api()` because it moves — a local cloud, the dev server's own
60
- * proxy — and nine modules got that wrong by freezing it into a const. This one
61
- * does not move: `platform.hanzo.ai` is written as a literal in
62
- * `lib/constants/products-metadata.ts`, `lib/data/oss-catalog.ts` and
63
- * `pricing.json`, no env names it, and `e2e/directory.spec.ts` asserts this
64
- * exact origin on the links below.
65
- */
66
- const CONSOLE = 'https://platform.hanzo.ai';
67
58
  const TABS = [
68
59
  { id: 'apps', label: 'Works with' },
69
60
  { id: 'channels', label: 'Channels' },
70
- { id: 'plugins', label: 'Plugins' },
71
- { id: 'skills', label: 'Skills' },
72
61
  ];
73
- /** The org registry each non-apps tab reads, once there is a bearer to read it with. */
74
- const ENDPOINT = {
75
- // The org-plane catalogue: every provider the platform can connect an account
76
- // to, with THIS org's connection and whether the deployment can open one.
77
- channels: '/v1/integrations',
78
- plugins: '/v1/tools/plugins',
79
- skills: '/v1/tools/skills',
80
- };
62
+ /** The org-plane catalogue: every provider the platform can connect an account to, with THIS org's connection and whether the deployment can open one. */
63
+ const CHANNELS = '/v1/integrations';
81
64
  /**
82
65
  * The channels a reader reaches for first, in the order they were asked for.
83
66
  * The rest of the registry follows in its own order. A rank, not a filter: a
@@ -100,99 +83,33 @@ const apps = () => (listing ??= import('./lib/integrations.js')
100
83
  kind: 'native',
101
84
  })))
102
85
  .catch(() => []));
103
- const CHANNELS_CATALOG = [
104
- { id: 'slack', name: 'Slack', publisher: 'Hanzo Auto', description: 'Bidirectional sync with FoundationDAO & workspace Slack channels and threads.', kind: 'native', available: true, connected: true },
105
- { id: 'discord', name: 'Discord', publisher: 'Hanzo Auto', description: 'Bot and webhook integration for community server announcements and chat.', kind: 'native', available: true, connected: true },
106
- { id: 'telegram', name: 'Telegram', publisher: 'Hanzo Auto', description: 'Direct messaging bot with instant agent responses and notifications.', kind: 'native', available: true, connected: true },
107
- { id: 'teams', name: 'Microsoft Teams', publisher: 'Hanzo Auto', description: 'Enterprise collaboration connector for channels, meetings, and team chats.', kind: 'native', available: true, connected: false },
108
- { id: 'whatsapp', name: 'WhatsApp Business', publisher: 'Hanzo Auto', description: 'Conversational agent support over official WhatsApp Business API.', kind: 'native', available: true, connected: false },
109
- { id: 'email', name: 'Email (SMTP/IMAP)', publisher: 'Hanzo Auto', description: 'Inbound conversation routing and outbound transactional dispatch.', kind: 'native', available: true, connected: false },
110
- { id: 'webhooks', name: 'Webhooks', publisher: 'Hanzo Core', description: 'Custom HTTP payload delivery for real-time external event ingest.', kind: 'native', available: true, connected: true },
111
- { id: 'sms', name: 'SMS (Twilio)', publisher: 'Hanzo Auto', description: 'Two-way text message delivery and conversational verification.', kind: 'native', available: true, connected: false },
112
- { id: 'x', name: 'X / Twitter', publisher: 'Hanzo Auto', description: 'Direct messages, mentions, and timeline thread monitoring.', kind: 'native', available: true, connected: false },
113
- { id: 'linkedin', name: 'LinkedIn', publisher: 'Hanzo Auto', description: 'Company page updates, comments, and direct message handling.', kind: 'native', available: true, connected: false },
114
- ];
115
- const PLUGINS_CATALOG = [
116
- { id: 'mcp-filesystem', name: 'Filesystem MCP', publisher: 'Model Context Protocol', description: 'Secure local and virtual file access, directory reading, and atomic edits.', kind: 'MCP', connected: true },
117
- { id: 'mcp-git', name: 'Git MCP', publisher: 'Model Context Protocol', description: 'Repository inspection, branch management, commit diffing, and git blame.', kind: 'MCP', connected: true },
118
- { id: 'mcp-code', name: 'Code & LSP MCP', publisher: 'Hanzo AI', description: 'Language server protocol diagnostics, symbol jump, and semantic search.', kind: 'MCP', connected: true },
119
- { id: 'mcp-memory', name: 'Memory & Knowledge Graph MCP', publisher: 'Hanzo AI', description: 'Persistent entity memory, semantic vector store, and recall graph.', kind: 'MCP', connected: true },
120
- { id: 'mcp-zen', name: 'Zen Architecture & Review MCP', publisher: 'Hanzo AI', description: 'Frontier reasoning engine, architecture planning, and rigorous code reviews.', kind: 'MCP', connected: true },
121
- { id: 'mcp-browser', name: 'Browser & CDP MCP', publisher: 'Hanzo AI', description: 'Headless Chrome DevTools Protocol automation and interactive web tasks.', kind: 'MCP', connected: true },
122
- { id: 'mcp-terminal', name: 'Terminal & PTY MCP', publisher: 'Hanzo AI', description: 'Safe shell execution, sandbox command runner, and real-time streaming.', kind: 'MCP', connected: true },
123
- { id: 'mcp-playwright', name: 'Playwright MCP', publisher: 'Microsoft', description: 'End-to-end browser automation, screenshots, and visual regression testing.', kind: 'MCP', connected: true },
124
- { id: 'mcp-fetch', name: 'Fetch & Curl MCP', publisher: 'Hanzo AI', description: 'Fast HTTP/HTTPS content extraction and markdown web page synthesis.', kind: 'MCP', connected: true },
125
- { id: 'mcp-datastore', name: 'Datastore MCP', publisher: 'Hanzo Datastore', description: 'ClickHouse high-performance analytical queries and telemetry exploration.', kind: 'MCP', connected: true },
126
- ];
127
- const SKILLS_CATALOG = [
128
- { id: 'skill-code-search', name: 'Codebase Search & Navigation', publisher: 'Hanzo Core', description: 'Deep semantic search, rip-grep, and symbol indexing across all project repositories.', kind: 'native', connected: true },
129
- { id: 'skill-data-analysis', name: 'Data Science & Statistical Analysis', publisher: 'Hanzo Core', description: 'Automated data exploration, tabular summaries, charting, and statistical tests.', kind: 'native', connected: true },
130
- { id: 'skill-git-ops', name: 'Git Workflow & Release Management', publisher: 'Hanzo Core', description: 'Branch creation, conventional commits, PR generation, and release drafting.', kind: 'native', connected: true },
131
- { id: 'skill-swarm', name: 'Autonomous Agent Swarm Orchestration', publisher: 'Hanzo Core', description: 'Divide complex tasks across specialized subagents with asynchronous coordination.', kind: 'native', connected: true },
132
- { id: 'skill-web-research', name: 'Web Research & Academic Synthesis', publisher: 'Hanzo Core', description: 'Multi-source fact-checking, literature review, and technical paper extraction.', kind: 'native', connected: true },
133
- { id: 'skill-sandbox', name: 'Python Sandbox Execution', publisher: 'Hanzo Core', description: 'Isolated execution of Python scripts with visualization and data artifact export.', kind: 'native', connected: true },
134
- { id: 'skill-media', name: 'Image & Media Generation', publisher: 'Hanzo Core', description: 'Diffusion generation, UI mockup creation, and media asset transformations.', kind: 'native', connected: true },
135
- { id: 'skill-web3', name: 'Smart Contract & EVM Interaction', publisher: 'Hanzo', description: 'Solidity compilation, ABI inspection, EVM deployment, and chain reads.', kind: 'native', connected: true },
136
- ];
137
- function catalogFor(tab) {
138
- switch (tab) {
139
- case 'apps':
140
- return [];
141
- case 'channels':
142
- return CHANNELS_CATALOG;
143
- case 'plugins':
144
- return PLUGINS_CATALOG;
145
- case 'skills':
146
- return SKILLS_CATALOG;
147
- }
148
- }
149
- /** One title-cased word from a channel/plugin id. */
86
+ /** One title-cased word from a channel id. */
150
87
  const title = (s) => s.charAt(0).toUpperCase() + s.slice(1);
151
- /** Map each org registry's own shape onto the one Row the card renders. */
152
- function rowsFor(tab, j) {
88
+ /** The channel registry's own shape, as the one Row the card renders. */
89
+ function channelsOf(j) {
153
90
  const o = (j ?? {});
154
- if (tab === 'channels') {
155
- const rank = (id) => {
156
- const i = LEAD.indexOf(id);
157
- return i < 0 ? LEAD.length : i;
158
- };
159
- return (o.providers ?? [])
160
- .map((p) => {
161
- const link = (p.connection ?? {});
162
- const account = String(link.account ?? link.label ?? '');
163
- return {
164
- id: String(p.id),
165
- name: String(p.name ?? title(String(p.id))),
166
- // What the registry knows, in the reader's order of interest: who is
167
- // connected, else whether a connection can be opened here at all.
168
- publisher: p.connected ? account || 'Connected' : p.available ? String(p.category ?? '') : 'Needs app credentials',
169
- description: String(p.description ?? ''),
170
- kind: 'native',
171
- connected: Boolean(p.connected),
172
- available: Boolean(p.available),
173
- account,
174
- };
175
- })
176
- .sort((a, b) => rank(a.id) - rank(b.id));
177
- }
178
- if (tab === 'plugins') {
179
- return (o.plugins ?? []).map((p) => ({
180
- id: String(p.name),
181
- name: title(String(p.name)),
182
- publisher: 'Hanzo',
183
- description: `Serves ${(p.prefixes ?? []).join(', ') || 'this workspace'}.`,
91
+ const rank = (id) => {
92
+ const i = LEAD.indexOf(id);
93
+ return i < 0 ? LEAD.length : i;
94
+ };
95
+ return (o.providers ?? [])
96
+ .map((p) => {
97
+ const link = (p.connection ?? {});
98
+ const account = String(link.account ?? link.label ?? '');
99
+ return {
100
+ id: String(p.id),
101
+ name: String(p.name ?? title(String(p.id))),
102
+ // What the registry knows, in the reader's order of interest: who is
103
+ // connected, else whether a connection can be opened here at all.
104
+ publisher: p.connected ? account || 'Connected' : p.available ? String(p.category ?? '') : 'Needs app credentials',
105
+ description: String(p.description ?? ''),
184
106
  kind: 'native',
185
- connected: p.enabled !== false,
186
- }));
187
- }
188
- // skills: the tools list, whichever key the source used.
189
- return (o.tools ?? o.skills ?? []).map((t) => ({
190
- id: String(t.id ?? t.name),
191
- name: String(t.name ?? t.id),
192
- publisher: String(t.publisher ?? t.author ?? 'You'),
193
- description: String(t.description ?? t.summary ?? 'A skill your agents can reach for.'),
194
- kind: (t.kind === 'MCP' ? 'MCP' : 'native'),
195
- }));
107
+ connected: Boolean(p.connected),
108
+ available: Boolean(p.available),
109
+ account,
110
+ };
111
+ })
112
+ .sort((a, b) => rank(a.id) - rank(b.id));
196
113
  }
197
114
  /**
198
115
  * A rejection `say()` can read.
@@ -218,18 +135,21 @@ export function Directory({ open, onClose, tab: first = 'apps', }) {
218
135
  * why it is exported and takes the tab as a prop.
219
136
  */
220
137
  export function Catalog({ tab }) {
221
- const { isAuthenticated, login, accessToken } = useIam();
138
+ const { isAuthenticated, accessToken } = useIam();
222
139
  const [query, setQuery] = useState('');
223
- const [rows, setRows] = useState(() => catalogFor(tab));
224
- // Bumped when a card changed a connection, so the registry is read again and
225
- // the card shows what the registry now holds rather than what the press hoped.
140
+ // null until the list is read: unknown is never drawn as empty.
141
+ const [rows, setRows] = useState(null);
142
+ const [wrong, setWrong] = useState(null);
143
+ // Bumped when a card changed a connection, or a failed read is asked again,
144
+ // so the registry is read again and the card shows what it now holds.
226
145
  const [turn, setTurn] = useState(0);
227
146
  const label = TABS.find((t) => t.id === tab)?.label ?? tab;
147
+ const signed = isAuthenticated && Boolean(accessToken);
228
148
  useEffect(() => {
229
149
  let live = true;
230
- const fallback = catalogFor(tab);
231
- setRows(fallback);
232
- const auth = isAuthenticated && accessToken ? scope() : null;
150
+ setRows(null);
151
+ setWrong(null);
152
+ const auth = signed ? scope() : null;
233
153
  // Apps is this site's own public catalog — real for everyone. Signed in, we
234
154
  // also mark which providers the org has actually connected, from the one
235
155
  // read that names them.
@@ -257,28 +177,18 @@ export function Catalog({ tab }) {
257
177
  }
258
178
  if (!auth)
259
179
  return;
260
- fetch(`${api()}${ENDPOINT[tab]}`, { headers: auth })
261
- .then((r) => (r.ok ? r.json() : null))
262
- .then((j) => {
263
- if (!live || !j)
264
- return;
265
- const liveRows = rowsFor(tab, j);
266
- if (liveRows && liveRows.length > 0) {
267
- const map = new Map();
268
- for (const r of fallback)
269
- map.set(r.id, r);
270
- for (const r of liveRows)
271
- map.set(r.id, { ...(map.get(r.id) || {}), ...r });
272
- setRows(Array.from(map.values()));
273
- }
180
+ fetch(`${api()}${CHANNELS}`, { headers: auth })
181
+ .then((r) => {
182
+ if (!r.ok)
183
+ throw refusal(r);
184
+ return r.json();
274
185
  })
275
- .catch(() => {
276
- // Degrades gracefully to fallback catalog
277
- });
186
+ .then((j) => live && setRows(channelsOf(j)))
187
+ .catch((e) => live && setWrong(e));
278
188
  return () => {
279
189
  live = false;
280
190
  };
281
- }, [tab, isAuthenticated, accessToken, turn]);
191
+ }, [tab, signed, turn]); // eslint-disable-line react-hooks/exhaustive-deps -- scope() reads the same token
282
192
  // A connection finishes in the provider's own tab. Coming back to this one is
283
193
  // the moment to ask the registry again.
284
194
  useEffect(() => {
@@ -293,14 +203,10 @@ export function Catalog({ tab }) {
293
203
  }, [tab]);
294
204
  const shown = useMemo(() => {
295
205
  const q = query.trim().toLowerCase();
296
- const list = Array.isArray(rows) ? rows : [];
297
- return list.filter((r) => {
298
- if (!q)
299
- return true;
300
- return r.name.toLowerCase().includes(q) || r.description.toLowerCase().includes(q);
301
- });
206
+ return (rows ?? []).filter((r) => !q || r.name.toLowerCase().includes(q) || r.description.toLowerCase().includes(q));
302
207
  }, [rows, query]);
303
- return (_jsxs(YStack, { gap: "$4", children: [_jsx(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: _jsxs(XStack, { items: "center", gap: "$2", flex: 1, minW: 180, children: [_jsx(Search, { size: 14, "aria-hidden": true }), _jsx(Input, { value: query, onChangeText: setQuery, placeholder: `Search ${tab}`, "aria-label": `Search ${tab}`, flex: 1 })] }) }), shown.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs(Text, { fontSize: "$3", color: "$soft", children: [shown.length, " ", shown.length === 1 ? 'result' : 'results'] }), _jsx(YStack, { maxH: 380, overflowY: "auto", overflowX: "hidden", pr: "$1", width: "100%", children: _jsx(Grid, { columns: tab === 'channels' ? { min: 168, max: 4 } : { min: 240, max: 2 }, gap: 12, children: shown.map((r) => tab === 'channels' ? (_jsx(Channel, { row: r, onChange: () => setTurn((n) => n + 1) }, r.id)) : (_jsx(RowCard, { row: r, tab: tab }, r.id))) }) })] })) : (_jsxs(YStack, { gap: "$3", py: "$4", children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: query ? `Nothing matches “${query}”.` : `No ${label.toLowerCase()} here yet.` }), !query ? (_jsx(Action, { href: `${CONSOLE}/${tab}`, target: "_blank", rel: "noreferrer", $touchable: { minH: 44, minW: 44 }, children: "Add in console" })) : null] }))] }));
208
+ const quiet = (text, action) => (_jsxs(YStack, { gap: "$3", py: "$4", items: "flex-start", children: [_jsx(Text, { fontSize: "$3", color: "$soft", children: text }), action] }));
209
+ return (_jsxs(YStack, { gap: "$4", children: [_jsx(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: _jsxs(XStack, { items: "center", gap: "$2", flex: 1, minW: 180, children: [_jsx(Search, { size: 14, "aria-hidden": true }), _jsx(Input, { value: query, onChangeText: setQuery, placeholder: `Search ${label.toLowerCase()}`, "aria-label": `Search ${label.toLowerCase()}`, flex: 1 })] }) }), tab === 'channels' && !signed ? (quiet('Sign in to see the channels your organization can connect.', _jsx(Action, { render: "button", onPress: () => enter(), $touchable: { minH: 44, minW: 44 }, children: "Sign in" }))) : wrong ? (quiet(say(wrong, 'the channels'), _jsx(Action, { render: "button", onPress: () => setTurn((n) => n + 1), $touchable: { minH: 44, minW: 44 }, children: "Try again" }))) : rows === null ? (quiet(`Loading ${label.toLowerCase()}…`)) : shown.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs(Text, { fontSize: "$3", color: "$soft", children: [shown.length, " ", shown.length === 1 ? 'result' : 'results'] }), _jsx(YStack, { maxH: 380, overflowY: "auto", overflowX: "hidden", pr: "$1", width: "100%", children: _jsx(Grid, { columns: tab === 'channels' ? { min: 168, max: 4 } : { min: 240, max: 2 }, gap: 12, children: shown.map((r) => tab === 'channels' ? (_jsx(Channel, { row: r, onChange: () => setTurn((n) => n + 1) }, r.id)) : (_jsx(RowCard, { row: r, tab: tab }, r.id))) }) })] })) : (quiet(query ? `Nothing matches “${query}”.` : tab === 'channels' ? 'No channel can be connected here yet.' : 'Nothing is listed yet.'))] }));
304
210
  }
305
211
  function RowCard({ row, tab }) {
306
212
  const { isAuthenticated } = useIam();
@@ -315,10 +221,9 @@ function RowCard({ row, tab }) {
315
221
  const back = encodeURIComponent(`${window.location.origin}/inbox`);
316
222
  window.open(`${api()}/v1/channels/${encodeURIComponent(row.id)}/oauth/authorize?redirect_uri=${back}`, '_blank', 'noopener,noreferrer');
317
223
  };
318
- // Apps open the page this site serves; a plugin or a skill is installed in
319
- // the console, which is where its registry is. Only a channel has a flow of
320
- // its own, and only that one is a button.
321
- const href = tab === 'apps' ? site(`/integrations/${row.id}`) : `${CONSOLE}/${tab}`;
224
+ // Apps open the page this site serves. Only a channel has a flow of its own,
225
+ // and only that one is a button.
226
+ const href = site(`/integrations/${row.id}`);
322
227
  return (_jsxs(YStack, { gap: "$2", p: "$3", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", height: "100%", bg: "$raised", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(ProviderMark, { org: row.id.replace(/-sdk$/, ''), size: 20 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(Text, { fontSize: "$3", fontWeight: "500", color: "$ink", numberOfLines: 1, children: row.name }), _jsx(Text, { fontSize: "$2", color: "$soft", numberOfLines: 1, children: row.publisher })] }), row.connected ? (_jsxs(XStack, { items: "center", gap: "$1", px: "$2", py: "$1", rounded: "$2", bg: "$hover", children: [_jsx(Check, { size: 12, color: GOOD, "aria-hidden": true }), _jsx(Text, { fontSize: "$1", color: GOOD, fontWeight: "600", children: "Connected" })] })) : tab === 'channels' ? (_jsx(Box, { render: "button", onClick: connect, "aria-label": `Connect ${row.name}`, px: "$2", py: "$1", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", bg: "var(--white-05)", hoverStyle: { bg: '$hover' }, cursor: "pointer", children: _jsx(Text, { fontSize: "$1", color: "$color", fontWeight: "600", children: "Connect" }) })) : (_jsx(Box, { render: _jsx("a", { href: href, target: tab === 'apps' ? undefined : '_blank', rel: "noreferrer", "aria-label": `Open ${row.name}` }), ...tap, children: _jsxs(XStack, { items: "center", gap: "$1", px: "$2", py: "$1", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", bg: "var(--white-05)", hoverStyle: { bg: '$hover' }, children: [_jsx(Text, { fontSize: "$1", color: "$color", fontWeight: "600", children: tab === 'apps' ? 'Docs' : 'Install' }), _jsx(ArrowUpRight, { size: 11, "aria-hidden": true })] }) }))] }), _jsx(Text, { fontSize: "$2", color: "$soft", numberOfLines: 2, flex: 1, children: row.description }), _jsx(XStack, { items: "center", pt: "$1", children: _jsx(Text, { fontFamily: "$mono", fontVariant: ['tabular-nums'], fontSize: "$1", color: "$soft", children: row.kind }) })] }));
323
228
  }
324
229
  /**
package/dist/Me.js CHANGED
@@ -6,31 +6,34 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
6
6
  // name, and their role in the organization they work in. The menu lists every
7
7
  // organization the token names — the personal one first, with its plan — and
8
8
  // switches with `pick()` (lib/auth/session.ts), the one switch the app has.
9
+ //
10
+ // THE PLAN LEADS: @hanzo/build's `Meter`, the block every account menu opens
11
+ // with — the plan by its family (`Max` with `20x` beside it) and, on a plan, its
12
+ // windows as shares; the free allowance left today on Free; the balance only
13
+ // for an account with no plan, whose meter it is. Credits are Billing's, so a
14
+ // plan never sits beside a balance here.
15
+ //
9
16
  // Under the list: Create organization, Organization settings, and Hanzo Team —
10
17
  // opened in the same organization for a team, offered as "Create a team" for a
11
- // personal one. Then the balance with Add funds, Settings, Usage, API keys, the
12
- // plans, help and Log out. A SuperAdmin also gets All organizations (support mode).
18
+ // personal one. Then Settings, Usage, Billing, API keys, the plans, help and Log
19
+ // out. A SuperAdmin also gets All organizations (support mode).
13
20
  //
14
21
  // IAM is the source of every fact here: names and roles from the token's `orgs`
15
22
  // claim, display names and which org is personal from `GET /v1/iam/organizations`,
16
- // the plan and balance from billing. An organization is created by the same
23
+ // the plan from billing and the limits. An organization is created by the same
17
24
  // `POST /v1/account/orgs` the no-organization gate uses (components/workspace/
18
25
  // Orgs.tsx); the next load mints a token that carries it.
19
- import { useEffect, useRef, useState } from 'react';
26
+ import { useEffect, useMemo, useRef, useState } from 'react';
20
27
  import { Popover, SizableText, View, XStack, YStack } from '@hanzo/gui';
21
- import { Building2, ChevronsUpDown, ExternalLink, Gauge, KeyRound, LifeBuoy, LogOut, Plus, Settings, Sparkles, Users, X } from 'lucide-react';
22
- import { path, useWho } from '@hanzo/build';
28
+ import { Building2, ChevronsUpDown, CreditCard, ExternalLink, Gauge, KeyRound, LifeBuoy, LogOut, Plus, Settings, Sparkles, Users, X } from 'lucide-react';
29
+ import { label, Meter, path, said as spoken, useStanding, useWho } from '@hanzo/build';
23
30
  import { useOrganizations } from '@hanzo/iam/react';
24
31
  import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
25
32
  import { glass } from '@hanzo/ui/glass';
26
- import { formatCents } from '@hanzo/usage';
27
33
  import { api } from './lib/api.js';
28
34
  import { createOrg, say } from './lib/org.js';
29
- import { useTier } from './lib/tier.js';
30
35
  import { list } from './lib/list.js';
31
36
  import { org as current, orgs, pick, renew, scope, superAdmin } from './lib/session.js';
32
- import { payPage } from './lib/pay.js';
33
- import { planName } from './lib/plans.js';
34
37
  import { ENTRY } from './lib/host.js';
35
38
  import { where } from './where.js';
36
39
  import { SupportPicker } from './Support.js';
@@ -52,8 +55,8 @@ export const initials = (name) => name
52
55
  .slice(0, 2)
53
56
  .map((w) => w.charAt(0).toUpperCase())
54
57
  .join('') || '?';
55
- /** The plan's name as billing sells it. */
56
- const planOf = (tier) => (tier ? planName(tier.plan) || tier.tier?.displayName || tier.tier?.name || '' : '');
58
+ /** The plan by its family, `Max 20x`, from a tier answer; '' with no plan. */
59
+ const planOf = (tier) => (tier ? spoken(label(tier.plan, tier.tier?.name)) : '');
57
60
  /**
58
61
  * The token's organizations with IAM's rows beside them: display name, and which
59
62
  * one is the person's own. The personal organization leads, then the token's order.
@@ -139,9 +142,10 @@ export function Me({ host, narrow = false, onOrg, }) {
139
142
  const all = useOrgs(current);
140
143
  const here = all.find((o) => o.name === current) ?? null;
141
144
  const personal = all.find((o) => o.personal) ?? null;
142
- const { tier } = useTier(menu.open, current);
145
+ const target = useMemo(() => ({ api: host.api, token: host.token, org: host.org }), [host.api, host.token, host.org]);
146
+ const read = useStanding(target, menu.open);
143
147
  const other = usePlan(personal && personal.name !== current ? personal.name : null, menu.open);
144
- const planFor = (o) => (o.name === current ? planOf(tier) : o.personal ? other : '');
148
+ const planFor = (o) => (o.name === current ? spoken(read.value?.label ?? null) : o.personal ? other : '');
145
149
  const who = host.person?.name || host.person?.email || '';
146
150
  const standing = [said(here?.role ?? ''), here?.display ?? current].filter(Boolean).join(' · ');
147
151
  // The organization in use takes focus once the menu is drawn — after the
@@ -164,11 +168,11 @@ export function Me({ host, narrow = false, onOrg, }) {
164
168
  return null;
165
169
  return (_jsxs(_Fragment, { children: [_jsxs(Popover, { open: menu.open, onOpenChange: menu.onOpenChange, placement: narrow ? 'right-end' : 'top-start', allowFlip: true, stayInFrame: true, offset: 8, children: [_jsx(Popover.Anchor, { width: "100%", children: _jsxs(XStack, { ref: (el) => {
166
170
  row.current = el instanceof HTMLElement ? el : null;
167
- }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, "aria-haspopup": "menu", "aria-expanded": menu.open, onPress: menu.toggle, width: narrow ? 40 : '100%', minH: narrow ? 40 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 30 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), standing ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: standing })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsx(Popover.Content, { ...glass(2), items: "stretch", borderWidth: 1, rounded: "$6", p: "$1.5", width: 288, maxW: "calc(100vw - 16px)", maxH: "calc(100dvh - 32px)", overflowY: "auto", overflowX: "hidden", children: _jsx(View, { render: _jsx("div", { ref: land, role: "menu", "aria-label": "Account", onKeyDown: (e) => rove(e, close) }), self: "stretch", minW: 0, children: _jsxs(YStack, { gap: 2, children: [_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", py: "$1.5", numberOfLines: 1, children: host.person.email || who }), _jsx(Rule, {}), _jsx(Label, { children: "Organizations" }), _jsx(YStack, { role: "group", "aria-label": "Organizations", gap: 2, children: all.map((o) => (_jsx(Item, { icon: _jsx(Mark, { org: o }), label: o.display, sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), checked: o.name === current, onPress: () => {
171
+ }, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, "aria-haspopup": "menu", "aria-expanded": menu.open, onPress: menu.toggle, width: narrow ? 40 : '100%', minH: narrow ? 40 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 30 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), standing ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: standing })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsx(Popover.Content, { ...glass(2), items: "stretch", borderWidth: 1, rounded: "$6", p: "$1.5", width: 288, maxW: "calc(100vw - 16px)", maxH: "calc(100dvh - 32px)", overflowY: "auto", overflowX: "hidden", children: _jsx(View, { render: _jsx("div", { ref: land, role: "menu", "aria-label": "Account", onKeyDown: (e) => rove(e, close) }), self: "stretch", minW: 0, children: _jsxs(YStack, { gap: 2, children: [_jsx(SizableText, { size: "$2", color: "$soft", px: "$2", py: "$1.5", numberOfLines: 1, children: host.person.email || who }), _jsx(Rule, {}), _jsx(Meter, { read: read, onPlans: () => go(path({ kind: 'screen', screen: 'plans' })), onBilling: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Rule, {}), _jsx(Label, { children: "Organizations" }), _jsx(YStack, { role: "group", "aria-label": "Organizations", gap: 2, children: all.map((o) => (_jsx(Item, { icon: _jsx(Mark, { org: o }), label: o.display, sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), checked: o.name === current, onPress: () => {
168
172
  menu.onOpenChange(false);
169
173
  if (o.name !== current)
170
174
  onOrg(o.name);
171
- } }, o.name))) }), _jsx(Item, { icon: _jsx(Plus, { size: 15, "aria-hidden": true }), label: "Create organization", onPress: () => (menu.onOpenChange(false), setCreating(true)) }), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Organization settings", onPress: () => (menu.onOpenChange(false), window.location.assign(ORG_SETTINGS)) }), where().team ? null : here && !here.personal ? (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Open in Hanzo Team", trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }), onPress: () => (menu.onOpenChange(false), window.location.assign(`${ENTRY}&org=${encodeURIComponent(here.name)}`)) })) : (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Create a team", onPress: () => (menu.onOpenChange(false), setCreating(true)) })), superAdmin() ? (_jsx(Item, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), label: "All organizations", onPress: () => (menu.onOpenChange(false), setSupporting(true)) })) : null, _jsx(Rule, {}), _jsx(Item, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), label: "Add funds", sub: typeof tier?.balance?.effectiveAvailable === 'number' ? `Credits ${formatCents(tier.balance.effectiveAvailable)}` : 'Credits', onPress: () => (menu.onOpenChange(false), window.open(payPage(), '_blank', 'noopener')), trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }) }), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Settings", onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(Item, { icon: _jsx(Gauge, { size: 15, "aria-hidden": true }), label: "Usage", onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(Item, { icon: _jsx(KeyRound, { size: 15, "aria-hidden": true }), label: "API keys", onPress: () => (menu.onOpenChange(false), window.location.assign(KEYS)) }), _jsx(Item, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), label: "View all plans", onPress: () => go(path({ kind: 'screen', screen: 'plans' })) }), _jsx(Item, { icon: _jsx(LifeBuoy, { size: 15, "aria-hidden": true }), label: "Get help", onPress: () => (menu.onOpenChange(false), window.open(DOCS, '_blank', 'noopener,noreferrer')) }), host.signOut ? (_jsxs(_Fragment, { children: [_jsx(Rule, {}), _jsx(Item, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), label: "Log out", onPress: () => (menu.onOpenChange(false), host.signOut?.()) })] })) : null] }) }) })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), supporting ? _jsx(SupportPicker, { onClose: () => setSupporting(false) }) : null] }));
175
+ } }, o.name))) }), _jsx(Item, { icon: _jsx(Plus, { size: 15, "aria-hidden": true }), label: "Create organization", onPress: () => (menu.onOpenChange(false), setCreating(true)) }), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Organization settings", onPress: () => (menu.onOpenChange(false), window.location.assign(ORG_SETTINGS)) }), where().team ? null : here && !here.personal ? (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Open in Hanzo Team", trail: _jsx(ExternalLink, { size: 13, "aria-hidden": true, opacity: 0.6 }), onPress: () => (menu.onOpenChange(false), window.location.assign(`${ENTRY}&org=${encodeURIComponent(here.name)}`)) })) : (_jsx(Item, { icon: _jsx(Users, { size: 15, "aria-hidden": true }), label: "Create a team", onPress: () => (menu.onOpenChange(false), setCreating(true)) })), superAdmin() ? (_jsx(Item, { icon: _jsx(Building2, { size: 15, "aria-hidden": true }), label: "All organizations", onPress: () => (menu.onOpenChange(false), setSupporting(true)) })) : null, _jsx(Rule, {}), _jsx(Item, { icon: _jsx(Settings, { size: 15, "aria-hidden": true }), label: "Settings", onPress: () => go(path({ kind: 'settings', section: 'general' })) }), _jsx(Item, { icon: _jsx(Gauge, { size: 15, "aria-hidden": true }), label: "Usage", onPress: () => go(path({ kind: 'settings', section: 'usage' })) }), _jsx(Item, { icon: _jsx(CreditCard, { size: 15, "aria-hidden": true }), label: "Billing", onPress: () => go(path({ kind: 'settings', section: 'billing' })) }), _jsx(Item, { icon: _jsx(KeyRound, { size: 15, "aria-hidden": true }), label: "API keys", onPress: () => (menu.onOpenChange(false), window.location.assign(KEYS)) }), _jsx(Item, { icon: _jsx(Sparkles, { size: 15, "aria-hidden": true }), label: "View all plans", onPress: () => go(path({ kind: 'screen', screen: 'plans' })) }), _jsx(Item, { icon: _jsx(LifeBuoy, { size: 15, "aria-hidden": true }), label: "Get help", onPress: () => (menu.onOpenChange(false), window.open(DOCS, '_blank', 'noopener,noreferrer')) }), host.signOut ? (_jsxs(_Fragment, { children: [_jsx(Rule, {}), _jsx(Item, { icon: _jsx(LogOut, { size: 15, "aria-hidden": true }), label: "Log out", onPress: () => (menu.onOpenChange(false), host.signOut?.()) })] })) : null] }) }) })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), supporting ? _jsx(SupportPicker, { onClose: () => setSupporting(false) }) : null] }));
172
176
  }
173
177
  /**
174
178
  * The sheet that creates an organization: a name, then `POST /v1/account/orgs`,
package/dist/Prose.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Anchor, Text, View, XStack, YStack } from '@hanzo/gui';
4
+ import { Code } from '@hanzo/ui/chat';
4
5
  import { destination, read } from './lib/prose.js';
5
6
  const INK = 'var(--foreground)';
6
7
  const QUIET = 'var(--muted-foreground)';
@@ -86,11 +87,11 @@ function blocks(tokens, tone) {
86
87
  return _jsx(List, { list: t, tone: tone }, i);
87
88
  case 'table':
88
89
  return _jsx(Table, { table: t, tone: tone }, i);
89
- // A fenced block scrolls SIDEWAYS on its own: wrapping code changes what
90
- // it says, and letting it push the message wider drags the whole
91
- // transcript into a horizontal scroll.
90
+ // A fenced block is @hanzo/ui's `Code`, the frame Dev's answers draw:
91
+ // its language, a copy control, and a body that scrolls SIDEWAYS on its
92
+ // own, because wrapping code changes what it says.
92
93
  case 'code':
93
- return (_jsx(View, { render: "pre", m: 0, overflowX: "auto", px: "$3.5", py: "$3", rounded: "$3", bg: GROUND, children: _jsx(Text, { render: "code", color: tone, fontFamily: "$mono", fontSize: "$1", lineHeight: "$2", whiteSpace: "pre", children: t.text }) }, i));
94
+ return (_jsx(Code, { language: t.lang || 'text', value: t.text, children: t.text }, i));
94
95
  case 'blockquote':
95
96
  return (_jsx(YStack, { render: "blockquote", m: 0, pl: "$3", gap: "$2", borderWidth: 0, borderLeftWidth: 2, borderStyle: "solid", borderColor: EDGE, children: blocks(t.tokens, QUIET) }, i));
96
97
  case 'hr':