@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/App.js +1 -1
- package/dist/Chat.js +8 -6
- package/dist/Column.js +2 -1
- package/dist/Me.js +3 -5
- package/dist/Orgs.js +266 -8
- package/dist/Palette.d.ts +7 -1
- package/dist/Palette.js +66 -27
- package/dist/Rail.d.ts +1 -5
- package/dist/Rail.js +16 -48
- package/dist/Shell.js +6 -5
- package/dist/Support.d.ts +0 -7
- package/dist/Support.js +11 -88
- package/dist/lib/orgname.d.ts +29 -0
- package/dist/lib/orgname.js +51 -0
- package/dist/lib/session.d.ts +4 -0
- package/dist/lib/session.js +6 -0
- package/dist/lib/title.d.ts +7 -0
- package/dist/lib/title.js +36 -0
- package/dist/seek.d.ts +9 -0
- package/dist/seek.js +11 -5
- package/dist/thread.js +2 -1
- package/package.json +1 -1
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
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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 {
|
|
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
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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,
|
|
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 {
|
|
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
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
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
|
|
24
|
-
|
|
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 {
|
|
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,
|
|
63
|
-
|
|
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'
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
import {
|
|
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 {
|
|
17
|
-
|
|
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
|
+
}
|
package/dist/lib/session.d.ts
CHANGED
|
@@ -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
|
package/dist/lib/session.js
CHANGED
|
@@ -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;
|