@hanzo/rooms 0.1.82 → 0.1.84

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/Palette.js CHANGED
@@ -1,13 +1,17 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // The one search. ⌘K / Ctrl+K from anywhere and the sidebar's search open it:
4
- // a dialog with the field on top, listing Recent chats while the field is empty
5
- // and, for a query, Chats, Projects, Agents, Skills and Actions, each cut to its
6
- // best five (seek.ts `find`). A row opens where it lives: a chat at its
7
- // address, a project in Dev, an agent in a new chat with it, a skill in Dev's
8
- // Customize, an action runs. Arrows move, Enter opens, Escape closes
9
- // (@hanzo/ui's CommandMenu); Keyboard shortcuts lists the app's keys, and
10
- // Backspace on an empty field comes back.
4
+ // a dialog with the field on top, listing the product's own recents while the
5
+ // field is empty — Chat's Recent chats, Dev's Recent runs — and, for a query,
6
+ // the product's own things first: in Chat, Chats, Projects, Agents, Skills and
7
+ // Actions; in Dev, Runs, Dev's places (Templates, Projects, Issues, Machines,
8
+ // Environments, Library, Scheduled tasks, Customize), Projects, Skills, Actions
9
+ // and Chats last; each cut to its best five (seek.ts `findIn`). A row opens
10
+ // where it lives: a chat at its address, a run or a place in Dev, a project in
11
+ // Dev, an agent in a new chat with it, a skill in Dev's Customize, an action
12
+ // runs. Arrows move, Enter opens, Escape closes (@hanzo/ui's CommandMenu);
13
+ // Keyboard shortcuts lists the app's keys, and Backspace on an empty field
14
+ // comes back.
11
15
  //
12
16
  // MOUNTED ONLY WHILE OPEN, and every list is read once as it opens, then
13
17
  // searched here as the reader types: the conversations store (hanzoai/agent)
@@ -18,9 +22,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
22
  // and the agents (crew.tsx) arrive while the reader types, and a read the
19
23
  // palette closes on is aborted.
20
24
  import { useEffect, useMemo, useState } from 'react';
21
- import { BookOpen, Bot, Code2, FolderGit2, Keyboard, LogOut, MessageCircle, Puzzle, Settings, SquarePen } from 'lucide-react';
25
+ import { BookOpen, Bot, Boxes, Clock, Code2, FolderGit2, Keyboard, LayoutTemplate, Library, ListTodo, LogOut, MessageCircle, Puzzle, Server, Settings, SquarePen } from 'lucide-react';
22
26
  import { useAi } from '@hanzo/ai/react';
23
- import { path as written } from '@hanzo/build';
27
+ import { path as written, useSessions } from '@hanzo/build';
24
28
  import { Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
25
29
  import { CommandMenu } from '@hanzo/ui/product/CommandMenu';
26
30
  import { ordered } from './agents.js';
@@ -30,7 +34,8 @@ import { empty, openThread, withAgents } from './open.js';
30
34
  import { useThreadList } from './thread.js';
31
35
  import { useSignOut } from './lib/signout.js';
32
36
  import { app, talk } from './lib/host.js';
33
- import { find } from './seek.js';
37
+ import { tidy } from './lib/title.js';
38
+ import { find, findIn } from './seek.js';
34
39
  const ICON = { size: 16, 'aria-hidden': true };
35
40
  /** The app's keys, as Keyboard shortcuts lists them. */
36
41
  const KEYS = [
@@ -43,6 +48,17 @@ const KEYS = [
43
48
  { id: 'forward', label: 'Forward, in the desktop app', shortcut: ['Alt', 'ArrowRight'] },
44
49
  ];
45
50
  const SKILLS = written({ kind: 'customize', tab: 'skills' });
51
+ /** Dev's places, as its sidebar lists them, by the builder address each opens. */
52
+ const PLACES = [
53
+ { id: 'templates', label: 'Templates', at: '-/templates', icon: _jsx(LayoutTemplate, { ...ICON }) },
54
+ { id: 'projects', label: 'Projects', at: '-/projects', icon: _jsx(FolderGit2, { ...ICON }) },
55
+ { id: 'issues', label: 'Issues', at: '-/issues', icon: _jsx(ListTodo, { ...ICON }) },
56
+ { id: 'machines', label: 'Machines', at: '-/settings/machines', icon: _jsx(Server, { ...ICON }) },
57
+ { id: 'environments', label: 'Environments', at: '-/settings/environments', icon: _jsx(Boxes, { ...ICON }) },
58
+ { id: 'library', label: 'Library', at: '-/artifacts', icon: _jsx(Library, { ...ICON }) },
59
+ { id: 'scheduled', label: 'Scheduled tasks', at: '-/automations', icon: _jsx(Clock, { ...ICON }) },
60
+ { id: 'customize', label: 'Customize', at: '-/customize', icon: _jsx(Puzzle, { ...ICON }) },
61
+ ];
46
62
  /** A list read once as the palette opens; a refusal leaves it empty and the rest still answer. */
47
63
  function useRead(path, shape) {
48
64
  const ai = useAi();
@@ -69,7 +85,7 @@ const skills = (body) => rows(obj(body).tools)
69
85
  .map(obj)
70
86
  .filter((t) => str(t.name))
71
87
  .map((t) => ({ id: str(t.name), label: str(t.name).replace(/^skill_/, ''), keywords: [str(t.description)] }));
72
- export function Palette({ onClose, go, actions = [], }) {
88
+ export function Palette({ onClose, go, actions = [], mode = 'chat', host, }) {
73
89
  const [query, setQuery] = useState('');
74
90
  const [keys, setKeys] = useState(false);
75
91
  const { threads } = useThreadList();
@@ -77,6 +93,7 @@ export function Palette({ onClose, go, actions = [], }) {
77
93
  const made = useRead('/v1/projects', projects);
78
94
  const known = useRead('/v1/tool/skills?activated=true', skills);
79
95
  const signOut = useSignOut();
96
+ const runs = useSessions(host ?? null);
80
97
  const groups = useMemo(() => {
81
98
  const own = [
82
99
  { id: 'new-chat', label: 'New chat', icon: _jsx(SquarePen, { ...ICON }), run: () => (empty(), go(talk())) },
@@ -89,24 +106,46 @@ export function Palette({ onClose, go, actions = [], }) {
89
106
  const doing = [...own.map((a) => actions.find((b) => b.id === a.id) ?? a), ...actions.filter((b) => !own.some((a) => a.id === b.id))];
90
107
  const chats = threads.map((t) => ({
91
108
  id: `chat-${t.id}`,
92
- label: t.title || 'Untitled',
109
+ label: tidy(t.title),
93
110
  at: t.updatedAt,
94
111
  icon: _jsx(MessageCircle, { ...ICON }),
95
112
  run: () => (openThread(t.id), go(talk(t.id))),
96
113
  }));
97
- const shown = find(query, chats, [
98
- { id: 'projects', label: 'Projects', items: made.map((p) => ({ ...p, id: `project-${p.id}`, icon: _jsx(FolderGit2, { ...ICON }), run: () => go(app(p.id)) })) },
99
- {
100
- id: 'agents',
101
- label: 'Agents',
102
- items: ordered(agents).map((one) => {
103
- const name = called(one.name);
104
- return { id: `agent-${one.id || one.name}`, label: name, icon: _jsx(Bot, { ...ICON }), run: () => (withAgents([name]), go(talk())) };
105
- }),
106
- },
107
- { id: 'skills', label: 'Skills', items: known.map((s) => ({ ...s, id: `skill-${s.id}`, icon: _jsx(BookOpen, { ...ICON }), run: () => go(app(SKILLS)) })) },
108
- { id: 'actions', label: 'Actions', items: doing },
109
- ]);
114
+ const projectRows = { id: 'projects', label: 'Projects', items: made.map((p) => ({ ...p, id: `project-${p.id}`, icon: _jsx(FolderGit2, { ...ICON }), run: () => go(app(p.id)) })) };
115
+ const skillRows = { id: 'skills', label: 'Skills', items: known.map((s) => ({ ...s, id: `skill-${s.id}`, icon: _jsx(BookOpen, { ...ICON }), run: () => go(app(SKILLS)) })) };
116
+ const actionRows = { id: 'actions', label: 'Actions', items: doing };
117
+ const shown = mode === 'dev'
118
+ ? findIn(query, {
119
+ label: 'Recent runs',
120
+ items: [...(runs?.value ?? [])].map((s) => ({
121
+ id: `run-${s.id}`,
122
+ label: tidy(s.title, 80, 'Untitled run'),
123
+ at: s.updatedAt || s.createdAt,
124
+ keywords: [s.status ?? ''],
125
+ icon: _jsx(Code2, { ...ICON }),
126
+ run: () => go(app(s.id)),
127
+ })),
128
+ }, [
129
+ { id: 'runs', label: 'Runs', items: [...(runs?.value ?? [])].map((s) => ({ id: `run-${s.id}`, label: tidy(s.title, 80, 'Untitled run'), at: s.updatedAt || s.createdAt, keywords: [s.status ?? ''], icon: _jsx(Code2, { ...ICON }), run: () => go(app(s.id)) })) },
130
+ { id: 'places', label: 'Dev', items: PLACES.map((p) => ({ id: `place-${p.id}`, label: p.label, icon: p.icon, run: () => go(app(p.at)) })) },
131
+ projectRows,
132
+ skillRows,
133
+ actionRows,
134
+ { id: 'chats', label: 'Chats', items: chats },
135
+ ])
136
+ : find(query, chats, [
137
+ projectRows,
138
+ {
139
+ id: 'agents',
140
+ label: 'Agents',
141
+ items: ordered(agents).map((one) => {
142
+ const name = called(one.name);
143
+ return { id: `agent-${one.id || one.name}`, label: name, icon: _jsx(Bot, { ...ICON }), run: () => (withAgents([name]), go(talk())) };
144
+ }),
145
+ },
146
+ skillRows,
147
+ actionRows,
148
+ ]);
110
149
  return shown.map((g) => ({
111
150
  ...g,
112
151
  items: g.items.map((a) => ({
@@ -123,9 +162,9 @@ export function Palette({ onClose, go, actions = [], }) {
123
162
  },
124
163
  })),
125
164
  }));
126
- }, [query, threads, agents, made, known, actions, go, onClose, signOut]);
165
+ }, [query, threads, agents, made, known, actions, go, onClose, signOut, mode, runs]);
127
166
  const listed = keys
128
167
  ? [{ id: 'keys', label: 'Keyboard shortcuts', items: KEYS.map((k) => ({ ...k, id: `key-${k.id}`, onSelect: () => setKeys(false) })) }]
129
168
  : groups;
130
- return (_jsx(Dialog, { open: true, onOpenChange: (open) => (open ? null : onClose()), children: _jsxs(DialogContent, { "data-slot": "palette", p: 0, gap: 0, width: "100%", maxW: 680, height: "min(520px, calc(100dvh - 32px))", rounded: "$6", overflow: "hidden", "$max-md": { width: '100vw', maxW: '100vw', height: '100dvh', rounded: 0, borderWidth: 0 }, children: [_jsx(DialogTitle, { position: "absolute", width: 1, height: 1, overflow: "hidden", opacity: 0, children: "Search" }), _jsx(CommandMenu, { groups: listed, query: query, onQueryChange: setQuery, placeholder: "Search\u2026", label: "Search", empty: keys ? 'No such key.' : query.trim() ? `Nothing matches “${query.trim()}”.` : 'No chats yet.', onBack: keys ? () => setKeys(false) : undefined, onClose: onClose })] }) }));
169
+ return (_jsx(Dialog, { open: true, onOpenChange: (open) => (open ? null : onClose()), children: _jsxs(DialogContent, { "data-slot": "palette", p: 0, gap: 0, width: "100%", maxW: 680, height: "min(520px, calc(100dvh - 32px))", rounded: "$6", overflow: "hidden", "$max-md": { width: '100vw', maxW: '100vw', height: '100dvh', rounded: 0, borderWidth: 0 }, children: [_jsx(DialogTitle, { position: "absolute", width: 1, height: 1, overflow: "hidden", opacity: 0, children: "Search" }), _jsx(CommandMenu, { groups: listed, query: query, onQueryChange: setQuery, placeholder: "Search\u2026", label: "Search", empty: keys ? 'No such key.' : query.trim() ? `Nothing matches “${query.trim()}”.` : mode === 'dev' ? 'No runs yet.' : 'No chats yet.', onBack: keys ? () => setKeys(false) : undefined, onClose: onClose })] }) }));
131
170
  }
package/dist/Rail.d.ts CHANGED
@@ -18,11 +18,9 @@ export declare function Toggle({ open, onPress, label }: {
18
18
  onPress: () => void;
19
19
  label?: string;
20
20
  }): import("react").JSX.Element;
21
- export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev, onStudio, go, onOrg, }: {
21
+ export declare function Rail({ host, mode, open, toggle, flat, onChat, onDev, onStudio, onOrg, }: {
22
22
  host: Host;
23
23
  mode: Place['mode'];
24
- /** Dev's builder address, or null in Chat. */
25
- at: string | null;
26
24
  /** The sidebar beside the rail is open. */
27
25
  open: boolean;
28
26
  /** Lead with the sidebar's toggle; absent where the host's bar holds it. */
@@ -33,8 +31,6 @@ export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev
33
31
  onChat: () => void;
34
32
  onDev: () => void;
35
33
  onStudio: () => void;
36
- /** Moves the app to an address of this page. */
37
- go: (address: string) => void;
38
34
  /** Work in another organization. */
39
35
  onOrg: (org: string) => void;
40
36
  }): import("react").JSX.Element;
package/dist/Rail.js CHANGED
@@ -1,17 +1,19 @@
1
1
  'use client';
2
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // The app's rail: its products as marks down the window's left edge, in one
4
- // order — Chat, Dev, Studio, Library, Scheduled tasks, Customize — then More, a
5
- // menu of the rest, and the account at the foot. It is how the app moves
6
- // between its products; a phone, which has no rail, switches Chat and Dev in
7
- // its bar (Top.tsx).
4
+ // order — Chat, Dev, Studio — then More, a menu of the products on hosts of
5
+ // their own, and the account at the foot. It is how the app moves between its
6
+ // products; a phone, which has no rail, switches Chat and Dev in its bar
7
+ // (Top.tsx).
8
8
  //
9
9
  // Studio is the app's third mode (@hanzo/studio at /studio), each of the three
10
10
  // opened where the reader left it.
11
11
  //
12
- // Library, Scheduled tasks and Customize are Dev's own places (@hanzo/build's
13
- // Artifacts, Automations and Customize), raised to the rail; each is lit while
14
- // the reader stands on it, and Dev is lit for the rest of Dev.
12
+ // A mark is a product, and a product owns the sidebar beside the rail: Chat its
13
+ // conversations, Dev its places (@hanzo/build's New, Projects, Issues, Library,
14
+ // Templates, Scheduled tasks, Machines, Environments, Customize, Docs) and its
15
+ // runs, Studio its own. Nothing of Dev's is raised to the rail, and nothing of
16
+ // the rail's is listed inside Dev: one level of the app a place.
15
17
  //
16
18
  // It stands on the frame's floor, never cut as a pane (ground.ts): the rail and
17
19
  // the sidebar beside it are the window, and the content is the panel inset in
@@ -20,35 +22,17 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
20
22
  // The sidebar's toggle leads it (`toggle`) where the host has no bar of its own:
21
23
  // a browser tab. A host that is a window draws the toggle in its bar beside Back
22
24
  // and Forward (App.tsx `bar`), and its rail starts with Chat. On a phone the
23
- // rail lies along the foot of the drawer (`flat`), the account at its end; the
24
- // drawer is too narrow for every mark, so Library, Scheduled tasks and
25
- // Customize lead More there.
26
- import { Boxes, Clock, Code2, Ellipsis, FolderGit2, LayoutTemplate, Library, ListTodo, MessageSquare, PanelLeft, Puzzle, Server, SquareTerminal, Users, WandSparkles } from 'lucide-react';
25
+ // rail lies along the foot of the drawer (`flat`), the account at its end.
26
+ import { Code2, Ellipsis, MessageSquare, PanelLeft, SquareTerminal, Users, WandSparkles } from 'lucide-react';
27
27
  import { XStack, YStack } from '@hanzo/gui';
28
- import { route } from '@hanzo/build';
29
28
  import { DropdownMenu } from '@hanzo/ui';
30
29
  import { out } from './help.js';
31
30
  import { Me } from './Me.js';
32
- import { app, TABS, WORKSPACE } from './lib/host.js';
31
+ import { TABS, WORKSPACE } from './lib/host.js';
33
32
  /** The rail's width, px. */
34
33
  export const RAIL = 48;
35
34
  /** A mark, px: a 36px square on a 44px pitch. */
36
35
  const MARK = 36;
37
- /** The places the rail raises out of Dev, by the builder address each opens. */
38
- const RAISED = { library: '-/artifacts', scheduled: '-/automations', customize: '-/customize' };
39
- /** Which raised place a builder address stands on, if any. */
40
- function raised(at) {
41
- if (at === null)
42
- return null;
43
- const r = route(at);
44
- if (r.kind === 'screen' && r.screen === 'artifacts')
45
- return 'library';
46
- if ((r.kind === 'screen' && r.screen === 'automations') || r.kind === 'automation')
47
- return 'scheduled';
48
- if (r.kind === 'customize')
49
- return 'customize';
50
- return null;
51
- }
52
36
  /** One mark: an icon button named for where it goes, its name its tooltip, lit where the reader is. */
53
37
  export function Mark({ label, on = false, onPress, children, ...rest }) {
54
38
  return (_jsx(XStack, { render: "button", "aria-label": label, ...{ title: label }, "aria-current": on ? 'page' : undefined, onPress: onPress, width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", shrink: 0, cursor: "pointer", opacity: on ? 1 : 0.62, bg: on ? '$edge' : 'transparent', hoverStyle: { bg: on ? '$edge' : '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' },
@@ -59,28 +43,12 @@ export function Mark({ label, on = false, onPress, children, ...rest }) {
59
43
  export function Toggle({ open, onPress, label }) {
60
44
  return (_jsx(Mark, { label: label ?? (open ? 'Close sidebar' : 'Open sidebar'), "data-slot": "sidebar-toggle", "aria-expanded": open, onPress: onPress, children: _jsx(PanelLeft, { size: 18, "aria-hidden": true }) }));
61
45
  }
62
- export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev, onStudio, go, onOrg, }) {
63
- const here = mode === 'dev' ? raised(at) : null;
64
- const to = (p) => () => go(app(p));
65
- // Everything else the app reaches, behind More: on a phone the raised places first, then Dev's other places, then the products on hosts of their own.
46
+ export function Rail({ host, mode, open, toggle, flat = false, onChat, onDev, onStudio, onOrg, }) {
47
+ // The products on hosts of their own, behind More.
66
48
  const rest = [
67
- ...(flat
68
- ? [
69
- { key: 'library', label: 'Library', icon: _jsx(Library, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.library) },
70
- { key: 'scheduled', label: 'Scheduled tasks', icon: _jsx(Clock, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.scheduled) },
71
- { key: 'customize', label: 'Customize', icon: _jsx(Puzzle, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.customize) },
72
- { type: 'separator', key: 'raised' },
73
- ]
74
- : []),
75
- { key: 'projects', label: 'Projects', icon: _jsx(FolderGit2, { size: 16, "aria-hidden": true }), onSelect: to('-/projects') },
76
- { key: 'templates', label: 'Templates', icon: _jsx(LayoutTemplate, { size: 16, "aria-hidden": true }), onSelect: to('-/templates') },
77
- { key: 'issues', label: 'Issues', icon: _jsx(ListTodo, { size: 16, "aria-hidden": true }), onSelect: to('-/issues') },
78
- { key: 'machines', label: 'Machines', icon: _jsx(Server, { size: 16, "aria-hidden": true }), onSelect: to('-/settings/machines') },
79
- { key: 'environments', label: 'Environments', icon: _jsx(Boxes, { size: 16, "aria-hidden": true }), onSelect: to('-/settings/environments') },
80
- { type: 'separator', key: 'away' },
81
49
  { key: 'tabs', label: 'Tabs', icon: _jsx(SquareTerminal, { size: 16, "aria-hidden": true }), onSelect: () => out(TABS) },
82
50
  { key: 'team', label: 'Team', icon: _jsx(Users, { size: 16, "aria-hidden": true }), onSelect: () => out(WORKSPACE) },
83
51
  ];
84
52
  const Stack = flat ? XStack : YStack;
85
- return (_jsxs(Stack, { "data-slot": "rail", render: "nav", "aria-label": "Products", ...(flat ? { pt: '$2', mt: '$2', borderTopWidth: 1, borderColor: '$borderColor', gap: '$1' } : { width: RAIL, flex: 1, pt: '$2', pb: '$2', gap: 8 }), shrink: 0, items: "center", children: [toggle ? (_jsx(YStack, { pb: 8, children: _jsx(Toggle, { open: open, onPress: toggle }) })) : null, _jsx(Mark, { label: "Chat", on: mode === 'chat', onPress: onChat, "data-slot": "rail-chat", children: _jsx(MessageSquare, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Dev", on: mode === 'dev' && !here, onPress: onDev, "data-slot": "rail-dev", children: _jsx(Code2, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Studio", on: mode === 'studio', onPress: onStudio, "data-slot": "rail-studio", children: _jsx(WandSparkles, { size: 18, "aria-hidden": true }) }), flat ? null : (_jsxs(_Fragment, { children: [_jsx(Mark, { label: "Library", on: here === 'library', onPress: to(RAISED.library), "data-slot": "rail-library", children: _jsx(Library, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Scheduled tasks", on: here === 'scheduled', onPress: to(RAISED.scheduled), "data-slot": "rail-scheduled", children: _jsx(Clock, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Customize", on: here === 'customize', onPress: to(RAISED.customize), "data-slot": "rail-customize", children: _jsx(Puzzle, { size: 18, "aria-hidden": true }) })] })), _jsx(DropdownMenu, { placement: flat ? 'top-start' : 'right-start', minWidth: 220, trigger: _jsx(XStack, { render: "button", "aria-label": "More", ...{ title: 'More' }, "data-slot": "rail-more", width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", cursor: "pointer", opacity: 0.62, hoverStyle: { bg: '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, $touchable: { width: 44, height: 44 }, children: _jsx(Ellipsis, { size: 18, "aria-hidden": true }) }), items: rest }), _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, above: flat, onOrg: onOrg })] }));
53
+ return (_jsxs(Stack, { "data-slot": "rail", render: "nav", "aria-label": "Products", ...(flat ? { pt: '$2', mt: '$2', borderTopWidth: 1, borderColor: '$borderColor', gap: '$1' } : { width: RAIL, flex: 1, pt: '$2', pb: '$2', gap: 8 }), shrink: 0, items: "center", children: [toggle ? (_jsx(YStack, { pb: 8, children: _jsx(Toggle, { open: open, onPress: toggle }) })) : null, _jsx(Mark, { label: "Chat", on: mode === 'chat', onPress: onChat, "data-slot": "rail-chat", children: _jsx(MessageSquare, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Dev", on: mode === 'dev', onPress: onDev, "data-slot": "rail-dev", children: _jsx(Code2, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Studio", on: mode === 'studio', onPress: onStudio, "data-slot": "rail-studio", children: _jsx(WandSparkles, { size: 18, "aria-hidden": true }) }), _jsx(DropdownMenu, { placement: flat ? 'top-start' : 'right-start', minWidth: 220, trigger: _jsx(XStack, { render: "button", "aria-label": "More", ...{ title: 'More' }, "data-slot": "rail-more", width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", cursor: "pointer", opacity: 0.62, hoverStyle: { bg: '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, $touchable: { width: 44, height: 44 }, children: _jsx(Ellipsis, { size: 18, "aria-hidden": true }) }), items: rest }), _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, above: flat, onOrg: onOrg })] }));
86
54
  }
package/dist/Shell.js CHANGED
@@ -77,6 +77,7 @@ import { netKey } from "./conversations.js";
77
77
  import { FIRST, SettingsList, } from "./Settings.js";
78
78
  import { Me } from "./Me.js";
79
79
  import { Palette } from "./Palette.js";
80
+ import { tidy } from "./lib/title.js";
80
81
  import { summons } from "./seek.js";
81
82
  import { called } from "./team.js";
82
83
  import { checkoutUrl } from './lib/pay.js';
@@ -393,7 +394,7 @@ function Chats({ filter = "", onPick, }) {
393
394
  const mergedThreads = useMemo(() => {
394
395
  const list = threads.map((t) => ({
395
396
  id: t.id,
396
- title: t.title || "Untitled",
397
+ title: tidy(t.title),
397
398
  updatedAt: t.updatedAt,
398
399
  thread: t,
399
400
  }));
@@ -409,7 +410,7 @@ function Chats({ filter = "", onPick, }) {
409
410
  }
410
411
  return list;
411
412
  }, [threads, customThreads]);
412
- const found = mergedThreads.filter((t) => matches(t.title || "Untitled", filter));
413
+ const found = mergedThreads.filter((t) => matches(tidy(t.title), filter));
413
414
  const byId = useMemo(() => new Map(threads.map((t) => [t.id, t])), [threads]);
414
415
  // PINNED FIRST, by the server's pin (thread.tsx), then the days.
415
416
  const shown = found.filter((t) => !t.thread?.pinned);
@@ -433,7 +434,7 @@ function Chats({ filter = "", onPick, }) {
433
434
  const item = (_jsx(SidebarItem, { active: t.id === thread, ...(t.thread ? { pr: "$8", $touchable: { pr: 52 } } : null), onPress: () => {
434
435
  open(t.id);
435
436
  onPick?.();
436
- }, children: _jsxs(YStack, { minW: 0, children: [_jsxs(XStack, { items: "center", gap: "$1.5", minW: 0, children: [t.kind === "call" ? _jsx(Video, { size: 13, color: "var(--muted-foreground)", "aria-label": "Call" }) : null, t.kind === "group" ? _jsx(Users, { size: 13, color: "var(--muted-foreground)", "aria-label": "Group" }) : null, _jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: t.title || "Untitled" })] }), _jsx(Span, { id: t.id, updatedAt: t.updatedAt })] }) }, t.id));
437
+ }, children: _jsxs(YStack, { minW: 0, children: [_jsxs(XStack, { items: "center", gap: "$1.5", minW: 0, children: [t.kind === "call" ? _jsx(Video, { size: 13, color: "var(--muted-foreground)", "aria-label": "Call" }) : null, t.kind === "group" ? _jsx(Users, { size: 13, color: "var(--muted-foreground)", "aria-label": "Group" }) : null, _jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: tidy(t.title) })] }), _jsx(Span, { id: t.id, updatedAt: t.updatedAt })] }) }, t.id));
437
438
  // A stored conversation carries its menu; a call or a group is no conversation of the store's.
438
439
  return t.thread ? (_jsx(ThreadRow, { t: t.thread, active: t.id === thread, listed: false, children: item }, t.id)) : (item);
439
440
  }) }, band.label)))] }) }), _jsx(SharedWithYou, { filter: filter, onPick: onPick })] }));
@@ -455,13 +456,13 @@ function SharedWithYou({ filter, onPick }) {
455
456
  on = false;
456
457
  };
457
458
  }, []);
458
- const shown = (items ?? []).filter((one) => matches(one.title || "Untitled", filter));
459
+ const shown = (items ?? []).filter((one) => matches(tidy(one.title), filter));
459
460
  if (shown.length === 0)
460
461
  return null;
461
462
  return (_jsx(SidebarSection, { label: "Shared with you", children: shown.map((one) => (_jsx(SidebarItem, { onPress: () => {
462
463
  onPick?.();
463
464
  window.location.assign(address(one.share));
464
- }, children: _jsxs(YStack, { minW: 0, children: [_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: one.title || "Untitled" }), one.by ? (_jsxs(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: ["From ", one.by] })) : null] }) }, one.share))) }));
465
+ }, children: _jsxs(YStack, { minW: 0, children: [_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: tidy(one.title) }), one.by ? (_jsxs(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: ["From ", one.by] })) : null] }) }, one.share))) }));
465
466
  }
466
467
  /**
467
468
  * The projects. A run is something that happened, a project is the thing it
package/dist/Support.d.ts CHANGED
@@ -5,10 +5,3 @@
5
5
  * token is.
6
6
  */
7
7
  export declare function SupportBanner(): import("react").JSX.Element;
8
- /**
9
- * Every organization, for a SuperAdmin to step into. The list and the search are
10
- * IAM's, a page at a time; picking one asks IAM to step in.
11
- */
12
- export declare function SupportPicker({ onClose }: {
13
- onClose: () => void;
14
- }): import("react").JSX.Element;
package/dist/Support.js CHANGED
@@ -2,40 +2,20 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // Support mode: a SuperAdmin steps into any organization to see what it sees.
4
4
  //
5
- // Everything here is IAM's. The list is IAM's organization list, which answers a
6
- // SuperAdmin every organization and anyone else only their own; the step in and
7
- // the step out are IAM's `assume` and `release`, which answer the operator's own
8
- // token re-scoped and record each step in the organization's audit log
9
- // (lib/auth/session.ts `support`). This file only draws them, and only for the
10
- // person IAM signs as a SuperAdmin (`superAdmin`); nothing here decides who may.
11
- import { useCallback, useEffect, useRef, useState } from 'react';
12
- import { Text, View, XStack, YStack } from '@hanzo/gui';
5
+ // Everything here is IAM's. The list of every organization is the chooser's
6
+ // (Orgs.tsx `Everyone`), IAM's organization list, which answers a SuperAdmin
7
+ // every organization and anyone else only their own; the step in and the step
8
+ // out are IAM's `assume` and `release`, which answer the operator's own token
9
+ // re-scoped and record each step in the organization's audit log
10
+ // (lib/session.ts `support`). This file draws the strip that says so while it
11
+ // lasts; nothing here decides who may.
12
+ import { useEffect, useState } from 'react';
13
+ import { Text, XStack } from '@hanzo/gui';
13
14
  import { Button } from '@hanzo/ui';
14
- import { X } from 'lucide-react';
15
15
  import { useHydrated } from './lib/hydrated.js';
16
- import { api } from './lib/api.js';
17
- import { abandon, assumed, bearer, named, support } from './lib/session.js';
18
- import { BAD, mix } from './lib/mix.js';
19
- /** The banner rides above the room's sheets, the picker between them. gui carries a `calc()` to the page unchanged;
20
- * its types only name custom properties. */
16
+ import { abandon, assumed, named, support } from './lib/session.js';
17
+ /** The banner rides above the room's sheets. gui carries a `calc()` to the page unchanged; its types only name custom properties. */
21
18
  const BANNER = 'calc(var(--z-modal) + 30)';
22
- const DIALOG = 'calc(var(--z-modal) + 20)';
23
- const SCRIM = mix('var(--pure-black)', 28, 'srgb');
24
- const muted = 'var(--muted-foreground)';
25
- /** A page of IAM's organization list: `q` narrows it, `cursor` continues it. */
26
- async function page(q, cursor) {
27
- const query = new URLSearchParams({ limit: '20' });
28
- if (q.trim())
29
- query.set('q', q.trim());
30
- if (cursor)
31
- query.set('cursor', cursor);
32
- const res = await fetch(`${api()}/v1/iam/organizations?${query}`, {
33
- headers: { Authorization: `Bearer ${bearer() ?? ''}` },
34
- });
35
- if (!res.ok)
36
- throw new Error(`IAM answered ${res.status}.`);
37
- return (await res.json());
38
- }
39
19
  /**
40
20
  * The strip across the top of every page while a SuperAdmin is inside an
41
21
  * organization that is not theirs: which one, as whom, and the way out. It reads
@@ -63,60 +43,3 @@ export function SupportBanner() {
63
43
  };
64
44
  return (_jsxs(XStack, { role: "status", "aria-label": "Support mode", position: "fixed", t: 52, l: "50%", x: "-50%", maxW: "calc(100vw - 32px)", z: BANNER, items: "center", justify: "center", gap: 12, flexWrap: "wrap", pt: 6, pr: 8, pb: 6, pl: 16, rounded: 999, bg: "var(--foreground)", children: [_jsxs(Text, { fontSize: "$2", color: "var(--background)", children: ["Support mode: you are in ", _jsx("strong", { children: org }), who ? ` as ${who}` : '', ". IAM records it under your name."] }), _jsx(Button, { size: "sm", onClick: leave, disabled: busy, children: busy ? 'Leaving' : 'Leave' })] }));
65
45
  }
66
- /**
67
- * Every organization, for a SuperAdmin to step into. The list and the search are
68
- * IAM's, a page at a time; picking one asks IAM to step in.
69
- */
70
- export function SupportPicker({ onClose }) {
71
- const [q, setQ] = useState('');
72
- const [rows, setRows] = useState([]);
73
- const [cursor, setCursor] = useState('');
74
- const [loading, setLoading] = useState(false);
75
- const [refused, setRefused] = useState(null);
76
- const [stepping, setStepping] = useState(null);
77
- // Only the newest read lands: a search typed while a page is in flight
78
- // supersedes it, so a slow answer to an old query never overwrites the new one.
79
- const asked = useRef(0);
80
- const load = useCallback(async (query, from) => {
81
- const mine = ++asked.current;
82
- setLoading(true);
83
- setRefused(null);
84
- try {
85
- const next = await page(query, from);
86
- if (mine !== asked.current)
87
- return;
88
- setRows((had) => {
89
- // IAM's cursor is a position, so an org created mid-walk can shift a row
90
- // across a page edge; each org is kept once.
91
- const all = from ? [...had, ...(next.organizations ?? [])] : next.organizations ?? [];
92
- return all.filter((o, i) => all.findIndex((x) => x.name === o.name) === i);
93
- });
94
- setCursor(next.cursor ?? '');
95
- }
96
- catch (e) {
97
- if (mine === asked.current)
98
- setRefused(e instanceof Error ? e.message : 'The list did not load.');
99
- }
100
- finally {
101
- if (mine === asked.current)
102
- setLoading(false);
103
- }
104
- }, []);
105
- useEffect(() => {
106
- const id = setTimeout(() => void load(q, ''), 250);
107
- return () => clearTimeout(id);
108
- }, [q, load]);
109
- const enter = (org) => {
110
- setStepping(org);
111
- setRefused(null);
112
- support(org).catch((e) => {
113
- setRefused(e instanceof Error ? e.message : 'IAM did not step in.');
114
- setStepping(null);
115
- });
116
- };
117
- return (_jsx(YStack, { role: "dialog", "aria-modal": true, "aria-label": "All organizations", onKeyDown: (e) => {
118
- // gui types the native event; on the web it is the DOM's, which carries the key.
119
- if ('key' in e && e.key === 'Escape')
120
- onClose();
121
- }, position: "fixed", inset: 0, z: DIALOG, bg: SCRIM, overflowY: "auto", py: "6vh", px: 16, onClick: onClose, children: _jsxs(YStack, { onClick: (e) => e.stopPropagation(), width: "100%", maxW: 560, mx: "auto", bg: "var(--background)", rounded: 16, pt: 24, px: 24, pb: 16, children: [_jsxs(XStack, { items: "flex-start", justify: "space-between", gap: 12, children: [_jsxs(YStack, { shrink: 1, minW: "auto", children: [_jsx(Text, { render: "h2", fontSize: "$7", fontWeight: "500", color: "var(--text-primary)", children: "All organizations" }), _jsx(Text, { render: "p", mt: 6, fontSize: "$3", color: muted, lineHeight: 21, children: "Support mode. Step into any organization to see its chats, agents and billing as it does. IAM records every step under your name." })] }), _jsx(Button, { variant: "ghost", size: "icon-sm", mr: -8, onClick: onClose, "aria-label": "Close", children: _jsx(X, { size: 16, "aria-hidden": true }) })] }), _jsx(Text, { render: _jsx("input", { value: q, onChange: (e) => setQ(e.target.value), placeholder: "Find an organization", "aria-label": "Find an organization", autoFocus: true }), width: "100%", mt: 16, fontSize: "$3", px: 12, py: 10, rounded: 10, borderWidth: 1, borderColor: "var(--border)", bg: "var(--background)", color: "var(--foreground)" }), _jsxs(View, { role: "list", "aria-label": "Organizations", mt: 10, display: "grid", gap: 2, children: [rows.map((o) => (_jsxs(XStack, { render: "button", role: "listitem", onClick: () => enter(o.name), disabled: stepping !== null, items: "baseline", justify: "space-between", gap: 12, width: "100%", px: 12, py: 10, rounded: 10, borderWidth: 0, bg: "transparent", cursor: "pointer", children: [_jsx(Text, { fontSize: "$3", fontWeight: "500", color: "var(--foreground)", text: "left", children: o.displayName || o.name }), _jsx(Text, { fontSize: "$2", color: muted, text: "left", children: stepping === o.name ? 'Stepping in' : o.name })] }, o.name))), !loading && rows.length === 0 && !refused ? (_jsx(Text, { render: "p", mx: 12, my: 8, fontSize: "$3", color: muted, children: "No organization matches." })) : null] }), refused ? (_jsx(Text, { render: "p", mx: 12, my: 8, fontSize: "$3", color: BAD, children: refused })) : null, cursor ? (_jsx(Button, { self: "flex-start", mt: 8, onClick: () => void load(q, cursor), disabled: loading, children: loading ? 'Loading' : 'More' })) : null] }) }));
122
- }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * An organization's name as IAM keys it, said once for the chooser.
3
+ *
4
+ * IAM refuses a name that is not already canonical rather than rewriting it
5
+ * (hanzoai/iam pkg/schema/orgname.go `OrgName`: lowercasing "Admin" would make
6
+ * it the reserved org), so a slug typed here is checked and refused, never
7
+ * folded. What IS folded is a display name into the slug it suggests, the way
8
+ * IAM's own onboarding does it (internal/oidc/onboard.go `slugifyOrg`), so the
9
+ * slug shown is the slug IAM makes of the same name.
10
+ */
11
+ /** The longest organization name IAM takes. */
12
+ export declare const LONGEST = 55;
13
+ /**
14
+ * The slug IAM's onboarding makes of a name: lowercase ASCII letters and digits,
15
+ * every other run one hyphen, none at either end, at most `LONGEST` long.
16
+ */
17
+ export declare function slugify(name: string): string;
18
+ /**
19
+ * Why IAM would not take `slug` as an organization's name, or null when it
20
+ * would. A name onboarding keeps exactly as typed: IAM's shape with single inner
21
+ * hyphens, since onboarding folds a double one. The names IAM holds for the
22
+ * platform are IAM's to refuse, in its own words.
23
+ */
24
+ export declare function refusal(slug: string): string | null;
25
+ /** Whether an organization answers what was typed: its name or its display name, in any case (IAM's `matches`). */
26
+ export declare function matches(o: {
27
+ name: string;
28
+ display: string;
29
+ }, typed: string): boolean;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * An organization's name as IAM keys it, said once for the chooser.
3
+ *
4
+ * IAM refuses a name that is not already canonical rather than rewriting it
5
+ * (hanzoai/iam pkg/schema/orgname.go `OrgName`: lowercasing "Admin" would make
6
+ * it the reserved org), so a slug typed here is checked and refused, never
7
+ * folded. What IS folded is a display name into the slug it suggests, the way
8
+ * IAM's own onboarding does it (internal/oidc/onboard.go `slugifyOrg`), so the
9
+ * slug shown is the slug IAM makes of the same name.
10
+ */
11
+ /** The longest organization name IAM takes. */
12
+ export const LONGEST = 55;
13
+ /**
14
+ * The slug IAM's onboarding makes of a name: lowercase ASCII letters and digits,
15
+ * every other run one hyphen, none at either end, at most `LONGEST` long.
16
+ */
17
+ export function slugify(name) {
18
+ let out = '';
19
+ let dash = false;
20
+ for (const ch of name.toLowerCase()) {
21
+ if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) {
22
+ out += ch;
23
+ dash = false;
24
+ }
25
+ else if (!dash) {
26
+ out += '-';
27
+ dash = true;
28
+ }
29
+ }
30
+ return out.replace(/^-+|-+$/g, '').slice(0, LONGEST).replace(/-+$/, '');
31
+ }
32
+ /**
33
+ * Why IAM would not take `slug` as an organization's name, or null when it
34
+ * would. A name onboarding keeps exactly as typed: IAM's shape with single inner
35
+ * hyphens, since onboarding folds a double one. The names IAM holds for the
36
+ * platform are IAM's to refuse, in its own words.
37
+ */
38
+ export function refusal(slug) {
39
+ if (slug.length < 2)
40
+ return 'Use at least 2 letters or numbers.';
41
+ if (slug.length > LONGEST)
42
+ return `Use at most ${LONGEST} characters.`;
43
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug))
44
+ return 'Use lowercase letters, digits and single hyphens between them.';
45
+ return null;
46
+ }
47
+ /** Whether an organization answers what was typed: its name or its display name, in any case (IAM's `matches`). */
48
+ export function matches(o, typed) {
49
+ const q = typed.trim().toLowerCase();
50
+ return !q || o.name.toLowerCase().includes(q) || o.display.toLowerCase().includes(q);
51
+ }
@@ -180,6 +180,10 @@ export declare const PICKED = "hanzo:org:picked";
180
180
  * app's account menu — is one the gate does not ask for again.
181
181
  */
182
182
  export declare function pick(o: string): void;
183
+ /** Fired on `window` to open the organization chooser over the room. */
184
+ export declare const CHOOSE = "hanzo:org:choose";
185
+ /** Open the chooser (`Orgs`): every organization of the person's, and every one for a SuperAdmin. */
186
+ export declare function choose(): void;
183
187
  /**
184
188
  * Whether the signed-in person is a SUPERADMIN: @hanzo/iam's `superAdmin`, which is
185
189
  * hanzoai/authz `Claims.Sudo` exactly — a person (never a program) whose own row's
@@ -437,6 +437,12 @@ export function pick(o) {
437
437
  }
438
438
  window.dispatchEvent(new Event(ORG));
439
439
  }
440
+ /** Fired on `window` to open the organization chooser over the room. */
441
+ export const CHOOSE = 'hanzo:org:choose';
442
+ /** Open the chooser (`Orgs`): every organization of the person's, and every one for a SuperAdmin. */
443
+ export function choose() {
444
+ window.dispatchEvent(new Event(CHOOSE));
445
+ }
440
446
  /**
441
447
  * Whether the signed-in person is a SUPERADMIN: @hanzo/iam's `superAdmin`, which is
442
448
  * hanzoai/authz `Claims.Sudo` exactly — a person (never a program) whose own row's
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A title fit to show: a conversation's or a run's title as the server kept it,
3
+ * less the markup a first message can carry (`<workspace-files>{…}</workspace-files>`,
4
+ * a lone tag, a leading JSON object), its whitespace one space, cut to `max`
5
+ * characters with an ellipsis; `fallback` when nothing readable is left.
6
+ */
7
+ export declare function tidy(title: string | null | undefined, max?: number, fallback?: string): string;