@hanzo/rooms 0.1.91 → 0.1.93
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 +2 -2
- package/dist/Betas.d.ts +21 -0
- package/dist/Betas.js +127 -0
- package/dist/Chat.d.ts +3 -5
- package/dist/Chat.js +65 -82
- package/dist/Me.js +4 -3
- package/dist/Palette.d.ts +20 -6
- package/dist/Palette.js +203 -99
- package/dist/Rail.d.ts +1 -1
- package/dist/Rail.js +20 -4
- package/dist/Shell.js +1 -1
- package/dist/lib/betas.d.ts +38 -0
- package/dist/lib/betas.js +66 -0
- package/dist/lib/session.d.ts +3 -1
- package/dist/lib/session.js +3 -2
- package/dist/lib/title.d.ts +2 -0
- package/dist/lib/title.js +5 -0
- package/dist/seek.d.ts +10 -17
- package/dist/seek.js +37 -20
- package/package.json +3 -3
package/dist/Palette.js
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
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.
|
|
3
|
+
// The palette: ⌘K / Ctrl+K from anywhere toggles it, the sidebar's search opens
|
|
4
|
+
// it. Every place in the app can be jumped to from it and every action run.
|
|
15
5
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
6
|
+
// An empty field lists Recent (the reader's chats and Dev tasks, newest first,
|
|
7
|
+
// each with its kind's icon), Actions, and Go to: the products, the places of
|
|
8
|
+
// the app and the apps in beta the reader may open. A query searches all of it, deeper places too (Settings' sections,
|
|
9
|
+
// Customize's tabs, the Library's kinds, Studio's places, Help), and the
|
|
10
|
+
// reader's chats, Dev tasks, projects, agents, skills and connectors by name;
|
|
11
|
+
// each group keeps its best five and the group with the best answer leads
|
|
12
|
+
// (seek.ts `find`). A row opens where it lives; an action runs.
|
|
13
|
+
//
|
|
14
|
+
// GO TO IS DERIVED, never listed here: the places are @hanzo/build's own
|
|
15
|
+
// registries (`nav` for Dev's places, `sections` for Settings, `tabs` for
|
|
16
|
+
// Customize, `library` for the Library's kinds), @hanzo/studio's `nav`, and
|
|
17
|
+
// help.ts's pages, lib/betas.ts's granted apps: the same lists the sidebars and
|
|
18
|
+
// menus draw. A page added to
|
|
19
|
+
// any of them is in the palette with nothing written here.
|
|
20
|
+
//
|
|
21
|
+
// MOUNTED ONLY WHILE OPEN, and every list is read once as it opens and searched
|
|
22
|
+
// here as the reader types: the conversations store (hanzoai/agent) answers its
|
|
23
|
+
// whole list and searches nothing, titles or words, so a title is all there is
|
|
24
|
+
// to find a chat by. The chats are the sidebar's last read (thread.tsx
|
|
25
|
+
// `useThreadList`), drawn at once; the projects (`/v1/projects`), the active
|
|
26
|
+
// skills (`/v1/tool/skills?activated=true`), the connectors
|
|
27
|
+
// (`/v1/tool/mcp/servers`) and the agents (crew.tsx) arrive while the reader
|
|
28
|
+
// types, and a read the palette closes on is aborted.
|
|
24
29
|
import { useEffect, useMemo, useState } from 'react';
|
|
25
|
-
import { BookOpen, Bot,
|
|
30
|
+
import { BookOpen, Bot, Building2, CircleHelp, Clock, Code2, FlaskConical, FolderGit2, Gift, Keyboard, LogOut, MessageCircle, MessageSquare, Plug, Settings, Sparkles, SquarePen, SquareTerminal, SunMoon, Users, WandSparkles, } from 'lucide-react';
|
|
26
31
|
import { useAi } from '@hanzo/ai/react';
|
|
27
|
-
import { path as written, useSessions } from '@hanzo/build';
|
|
32
|
+
import { library, nav, NEW, path as written, sections, tabs, useSessions } from '@hanzo/build';
|
|
33
|
+
import { nav as made } from '@hanzo/studio';
|
|
28
34
|
import { Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
|
|
29
35
|
import { CommandMenu } from '@hanzo/ui/product/CommandMenu';
|
|
30
36
|
import { ordered } from './agents.js';
|
|
@@ -33,77 +39,170 @@ import { called } from './team.js';
|
|
|
33
39
|
import { empty, openThread, withAgents } from './open.js';
|
|
34
40
|
import { useThreadList } from './thread.js';
|
|
35
41
|
import { useSignOut } from './lib/signout.js';
|
|
36
|
-
import {
|
|
42
|
+
import { choose } from './lib/session.js';
|
|
43
|
+
import { app, studio, talk, TERMINALS, WORKSPACE } from './lib/host.js';
|
|
44
|
+
import { BETAS } from './lib/betas.js';
|
|
45
|
+
import { useBetas } from './Betas.js';
|
|
37
46
|
import { tidy } from './lib/title.js';
|
|
38
|
-
import {
|
|
47
|
+
import { KEYS, legal, out, pages } from './help.js';
|
|
48
|
+
import { site } from './where.js';
|
|
49
|
+
import { find } from './seek.js';
|
|
39
50
|
const ICON = { size: 16, 'aria-hidden': true };
|
|
40
|
-
/** The
|
|
41
|
-
const
|
|
42
|
-
{ id: 'search', label: 'Search', shortcut: ['Mod', 'K'] },
|
|
43
|
-
{ id: 'settings', label: 'Settings', shortcut: ['Mod', ','] },
|
|
44
|
-
{ id: 'panel', label: 'Side panel', shortcut: ['Mod', '.'] },
|
|
45
|
-
{ id: 'rename', label: 'Rename chat', shortcut: ['Ctrl', 'Alt', 'R'] },
|
|
46
|
-
{ id: 'pin', label: 'Pin or unpin chat', shortcut: ['Ctrl', 'Alt', 'P'] },
|
|
47
|
-
{ id: 'back', label: 'Back, in the desktop app', shortcut: ['Alt', 'ArrowLeft'] },
|
|
48
|
-
{ id: 'forward', label: 'Forward, in the desktop app', shortcut: ['Alt', 'ArrowRight'] },
|
|
49
|
-
];
|
|
51
|
+
/** The builder's Settings, its first section. */
|
|
52
|
+
const SETTINGS = written({ kind: 'settings', section: 'general' });
|
|
50
53
|
const SKILLS = written({ kind: 'customize', tab: 'skills' });
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
const CONNECTORS = written({ kind: 'customize', tab: 'connectors' });
|
|
55
|
+
/** The words a Settings section is also asked for by: the account menu's names for two of them, and the obvious ones. */
|
|
56
|
+
const ALSO = {
|
|
57
|
+
general: ['personalization', 'appearance', 'theme'],
|
|
58
|
+
account: ['profile'],
|
|
59
|
+
privacy: ['security'],
|
|
60
|
+
billing: ['plans', 'subscription'],
|
|
61
|
+
members: ['organization', 'team'],
|
|
62
|
+
};
|
|
63
|
+
/** A registry's groups as rows, each named for where it lives. */
|
|
64
|
+
const rows = (groups, where, prefix) => groups
|
|
65
|
+
.flatMap((g) => g.links)
|
|
66
|
+
.map((l) => ({ id: `${prefix}-${l.id}`, label: l.label, icon: l.icon, where, keywords: [where, ...(ALSO[l.id] ?? [])].filter(Boolean), run: () => l.onPress?.() }));
|
|
67
|
+
/**
|
|
68
|
+
* Every place the reader can be taken: `top` is what an empty field lists, the
|
|
69
|
+
* products and the places of the app; `deep` is what only a query reaches.
|
|
70
|
+
* Without a host (hanzo.team's frame) the builder's registries are not there,
|
|
71
|
+
* and the products and the site's pages are.
|
|
72
|
+
*/
|
|
73
|
+
export function places(host, go) {
|
|
74
|
+
const builder = (p) => go(app(p));
|
|
75
|
+
const top = [
|
|
76
|
+
{ id: 'chat', label: 'Chat', icon: _jsx(MessageSquare, { ...ICON }), run: () => go(talk()) },
|
|
77
|
+
{ id: 'dev', label: 'Dev', icon: _jsx(Code2, { ...ICON }), run: () => go(app('')) },
|
|
78
|
+
{ id: 'studio', label: 'Studio', icon: _jsx(WandSparkles, { ...ICON }), run: () => go(studio('')) },
|
|
79
|
+
{ id: 'tabs', label: 'Tabs', icon: _jsx(SquareTerminal, { ...ICON }), run: () => go(TERMINALS) },
|
|
80
|
+
...(host ? [{ id: 'team', label: 'Team', icon: _jsx(Users, { ...ICON }), keywords: ['workspace'], run: () => out(WORKSPACE) }] : []),
|
|
81
|
+
...(host ? rows(nav(host, builder).map((g) => ({ links: g.links.filter((l) => l.id !== NEW) })), '', 'dev') : []),
|
|
82
|
+
{ id: 'settings', label: 'Settings', icon: _jsx(Settings, { ...ICON }), shortcut: ['Mod', ','], run: () => go(app(SETTINGS)) },
|
|
83
|
+
// The site's own pages, outside the app: the browser loads them.
|
|
84
|
+
{ id: 'referral', label: 'Referral', icon: _jsx(Gift, { ...ICON }), keywords: ['invite', 'share'], run: () => window.location.assign(site('/referral')) },
|
|
85
|
+
{ id: 'beta', label: 'Beta status', icon: _jsx(FlaskConical, { ...ICON }), keywords: ['waitlist'], run: () => window.location.assign(site('/beta/status')) },
|
|
86
|
+
...pages()
|
|
87
|
+
.filter((p) => p.key === 'help')
|
|
88
|
+
.map((p) => ({ id: `help-${p.key}`, label: p.label, icon: _jsx(CircleHelp, { ...ICON }), keywords: ['support', 'docs'], run: () => out(p.href) })),
|
|
89
|
+
];
|
|
90
|
+
const deep = [
|
|
91
|
+
...(host
|
|
92
|
+
? [
|
|
93
|
+
...rows(sections(host, builder), 'Settings', 'settings'),
|
|
94
|
+
...rows(tabs(host, builder), 'Customize', 'customize'),
|
|
95
|
+
...rows(library(host, builder).slice(1), 'Library', 'library'),
|
|
96
|
+
...rows(made(host, (p) => go(studio(p))), 'Studio', 'studio').filter((a) => a.id !== `studio-${NEW}`),
|
|
97
|
+
]
|
|
98
|
+
: []),
|
|
99
|
+
...[...pages(), ...legal()]
|
|
100
|
+
.filter((p) => p.key !== 'help')
|
|
101
|
+
.map((p) => ({ id: `help-${p.key}`, label: p.label, where: 'Help', keywords: ['Help'], run: () => out(p.href) })),
|
|
102
|
+
];
|
|
103
|
+
// A place two registries hold (Machines is Dev's and a Settings section) is listed once, as Dev's.
|
|
104
|
+
return { top, deep: deep.filter((a) => !top.some((t) => t.id === a.id.replace(/^settings-/, 'dev-'))) };
|
|
105
|
+
}
|
|
106
|
+
/** The composer's model menu, opened as a press would: a page without a composer opens New chat's. */
|
|
107
|
+
function tune(go) {
|
|
108
|
+
const open = () => {
|
|
109
|
+
const chip = document.querySelector('[data-slot="mind"][aria-haspopup="menu"]');
|
|
110
|
+
chip?.focus();
|
|
111
|
+
chip?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
|
112
|
+
return !!chip;
|
|
113
|
+
};
|
|
114
|
+
let tries = 0;
|
|
115
|
+
const wait = () => {
|
|
116
|
+
if (!open() && ++tries < 120)
|
|
117
|
+
requestAnimationFrame(wait);
|
|
118
|
+
};
|
|
119
|
+
// After the palette has closed and handed focus back, so the menu keeps it.
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
if (!open()) {
|
|
122
|
+
go(talk());
|
|
123
|
+
requestAnimationFrame(wait);
|
|
124
|
+
}
|
|
125
|
+
}, 0);
|
|
126
|
+
}
|
|
127
|
+
/** The palette's actions; a host that cannot do one (no theme to change, no org to pick) is not offered it. */
|
|
128
|
+
export function actions(host, go, signOut, keys) {
|
|
129
|
+
const upload = host ? library(host, (p) => go(app(p)))[0]?.links.find((l) => l.id === 'upload') : undefined;
|
|
130
|
+
const list = [
|
|
131
|
+
{ id: 'new-chat', label: 'New chat', icon: _jsx(SquarePen, { ...ICON }), run: () => (empty(), go(talk())) },
|
|
132
|
+
{ id: 'new-task', label: 'New Dev task', icon: _jsx(Code2, { ...ICON }), keywords: ['run'], run: () => go(app('')) },
|
|
133
|
+
{ id: 'new-creation', label: 'New Studio creation', icon: _jsx(WandSparkles, { ...ICON }), keywords: ['image', 'video'], run: () => go(studio('')) },
|
|
134
|
+
{ id: 'new-scheduled', label: 'New scheduled task', icon: _jsx(Clock, { ...ICON }), keywords: ['automation'], run: () => go(app(written({ kind: 'automation', id: 'new' }))) },
|
|
135
|
+
!!upload && { id: 'upload', label: 'Upload', icon: upload.icon, keywords: ['file', 'library'], run: () => upload.onPress?.() },
|
|
136
|
+
{ id: 'model', label: 'Switch model', icon: _jsx(Sparkles, { ...ICON }), keywords: ['model'], run: () => tune(go) },
|
|
137
|
+
!!host?.chooseTheme && {
|
|
138
|
+
id: 'theme',
|
|
139
|
+
label: 'Toggle theme',
|
|
140
|
+
icon: _jsx(SunMoon, { ...ICON }),
|
|
141
|
+
keywords: ['dark', 'light', 'appearance'],
|
|
142
|
+
run: () => host.chooseTheme?.(document.documentElement.classList.contains('dark') ? 'light' : 'dark'),
|
|
143
|
+
},
|
|
144
|
+
!!host && { id: 'org', label: 'Switch organization', icon: _jsx(Building2, { ...ICON }), keywords: ['org', 'team', 'workspace'], run: choose },
|
|
145
|
+
{ id: 'keys', label: 'Keyboard shortcuts', icon: _jsx(Keyboard, { ...ICON }), stay: true, run: keys },
|
|
146
|
+
{ id: 'logout', label: 'Log out', icon: _jsx(LogOut, { ...ICON }), keywords: ['sign out'], run: signOut },
|
|
147
|
+
];
|
|
148
|
+
return list.filter((a) => !!a);
|
|
149
|
+
}
|
|
62
150
|
/** A list read once as the palette opens; a refusal leaves it empty and the rest still answer. */
|
|
63
151
|
function useRead(path, shape) {
|
|
64
152
|
const ai = useAi();
|
|
65
|
-
const [
|
|
153
|
+
const [found, setFound] = useState([]);
|
|
66
154
|
useEffect(() => {
|
|
67
155
|
const stop = new AbortController();
|
|
68
|
-
ai.http.json({ path, signal: stop.signal }).then((body) =>
|
|
156
|
+
ai.http.json({ path, signal: stop.signal }).then((body) => setFound(shape(body)), () => { });
|
|
69
157
|
return () => stop.abort();
|
|
70
158
|
// `shape` is a module constant at each call.
|
|
71
159
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
72
160
|
}, [ai, path]);
|
|
73
|
-
return
|
|
161
|
+
return found;
|
|
74
162
|
}
|
|
75
163
|
const obj = (v) => (v && typeof v === 'object' ? v : {});
|
|
76
164
|
const str = (v) => (typeof v === 'string' ? v : '');
|
|
77
|
-
const
|
|
165
|
+
const list = (v) => (Array.isArray(v) ? v : []);
|
|
78
166
|
/** `/v1/projects`: a project by its slug, found by its name, slug and repository. */
|
|
79
|
-
const projects = (body) => (Array.isArray(body) ? body :
|
|
167
|
+
const projects = (body) => (Array.isArray(body) ? body : list(obj(body).data))
|
|
80
168
|
.map(obj)
|
|
81
169
|
.filter((p) => str(p.slug))
|
|
82
170
|
.map((p) => ({ id: str(p.slug), label: str(p.name) || str(p.slug), keywords: [str(p.slug), str(obj(p.repo).url)] }));
|
|
83
171
|
/** `/v1/tool/skills?activated=true`: a skill by its name, its tool's less `skill_`. */
|
|
84
|
-
const skills = (body) =>
|
|
172
|
+
const skills = (body) => list(obj(body).tools)
|
|
85
173
|
.map(obj)
|
|
86
174
|
.filter((t) => str(t.name))
|
|
87
175
|
.map((t) => ({ id: str(t.name), label: str(t.name).replace(/^skill_/, ''), keywords: [str(t.description)] }));
|
|
88
|
-
|
|
176
|
+
/** `/v1/tool/mcp/servers`: a connector by its name. */
|
|
177
|
+
const connectors = (body) => list(obj(body).servers)
|
|
178
|
+
.map(obj)
|
|
179
|
+
.filter((s) => str(s.id))
|
|
180
|
+
.map((s) => ({ id: str(s.id), label: str(s.name) || str(s.id), keywords: [str(s.url)] }));
|
|
181
|
+
export function Palette({ onClose, go, actions: frame = [], mode = 'chat', host, }) {
|
|
89
182
|
const [query, setQuery] = useState('');
|
|
90
183
|
const [keys, setKeys] = useState(false);
|
|
91
184
|
const { threads } = useThreadList();
|
|
92
185
|
const { agents } = useCrew();
|
|
93
|
-
const
|
|
186
|
+
const repos = useRead('/v1/projects', projects);
|
|
94
187
|
const known = useRead('/v1/tool/skills?activated=true', skills);
|
|
188
|
+
const plugged = useRead('/v1/tool/mcp/servers', connectors);
|
|
95
189
|
const signOut = useSignOut();
|
|
96
190
|
const runs = useSessions(host ?? null);
|
|
191
|
+
const { open: opened } = useBetas();
|
|
97
192
|
const groups = useMemo(() => {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
193
|
+
const { top, deep } = places(host, go);
|
|
194
|
+
const swap = (a) => frame.find((b) => b.id === a.id) ?? a;
|
|
195
|
+
// The apps in beta this reader may open, each where it lives (the rail's More).
|
|
196
|
+
const granted = BETAS.filter((b) => b.href && opened(b.id)).map((b) => ({
|
|
197
|
+
id: `beta-${b.id}`,
|
|
198
|
+
label: b.name,
|
|
199
|
+
icon: _jsx(Sparkles, { ...ICON }),
|
|
200
|
+
where: 'Beta',
|
|
201
|
+
keywords: ['beta', b.line],
|
|
202
|
+
run: () => out(b.href),
|
|
203
|
+
}));
|
|
204
|
+
const here = [...top.map(swap), ...frame.filter((b) => !top.some((a) => a.id === b.id)), ...granted];
|
|
205
|
+
const doing = actions(host, go, () => void signOut(), () => (setQuery(''), setKeys(true)));
|
|
107
206
|
const chats = threads.map((t) => ({
|
|
108
207
|
id: `chat-${t.id}`,
|
|
109
208
|
label: tidy(t.title),
|
|
@@ -111,47 +210,49 @@ export function Palette({ onClose, go, actions = [], mode = 'chat', host, }) {
|
|
|
111
210
|
icon: _jsx(MessageCircle, { ...ICON }),
|
|
112
211
|
run: () => (openThread(t.id), go(talk(t.id))),
|
|
113
212
|
}));
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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,
|
|
213
|
+
const tasks = (runs?.value ?? []).map((s) => ({
|
|
214
|
+
id: `run-${s.id}`,
|
|
215
|
+
label: tidy(s.title, 80, 'Untitled run'),
|
|
216
|
+
at: s.updatedAt || s.createdAt,
|
|
217
|
+
keywords: [s.status ?? ''],
|
|
218
|
+
icon: _jsx(Code2, { ...ICON }),
|
|
219
|
+
run: () => go(app(s.id)),
|
|
220
|
+
}));
|
|
221
|
+
const mine = mode === 'dev'
|
|
222
|
+
? [
|
|
223
|
+
{ id: 'runs', label: 'Dev tasks', items: tasks },
|
|
134
224
|
{ id: 'chats', label: 'Chats', items: chats },
|
|
135
|
-
]
|
|
136
|
-
:
|
|
137
|
-
|
|
138
|
-
{
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
225
|
+
]
|
|
226
|
+
: [
|
|
227
|
+
{ id: 'chats', label: 'Chats', items: chats },
|
|
228
|
+
{ id: 'runs', label: 'Dev tasks', items: tasks },
|
|
229
|
+
];
|
|
230
|
+
const shown = find(query, [...chats, ...tasks], [
|
|
231
|
+
{ id: 'actions', label: 'Actions', items: doing },
|
|
232
|
+
{ id: 'go', label: 'Go to', items: here },
|
|
233
|
+
], [
|
|
234
|
+
{ id: 'go', label: 'Go to', items: [...here, ...deep] },
|
|
235
|
+
{ id: 'actions', label: 'Actions', items: doing },
|
|
236
|
+
...mine,
|
|
237
|
+
{ id: 'projects', label: 'Projects', items: repos.map((p) => ({ ...p, id: `project-${p.id}`, icon: _jsx(FolderGit2, { ...ICON }), run: () => go(app(p.id)) })) },
|
|
238
|
+
{
|
|
239
|
+
id: 'agents',
|
|
240
|
+
label: 'Agents',
|
|
241
|
+
items: ordered(agents).map((one) => {
|
|
242
|
+
const name = called(one.name);
|
|
243
|
+
return { id: `agent-${one.id || one.name}`, label: name, icon: _jsx(Bot, { ...ICON }), run: () => (withAgents([name]), go(talk())) };
|
|
244
|
+
}),
|
|
245
|
+
},
|
|
246
|
+
{ id: 'skills', label: 'Skills', items: known.map((s) => ({ ...s, id: `skill-${s.id}`, icon: _jsx(BookOpen, { ...ICON }), run: () => go(app(SKILLS)) })) },
|
|
247
|
+
{ id: 'connectors', label: 'Connectors', items: plugged.map((c) => ({ ...c, id: `connector-${c.id}`, icon: _jsx(Plug, { ...ICON }), run: () => go(app(CONNECTORS)) })) },
|
|
248
|
+
]);
|
|
149
249
|
return shown.map((g) => ({
|
|
150
250
|
...g,
|
|
151
251
|
items: g.items.map((a) => ({
|
|
152
252
|
id: a.id,
|
|
153
253
|
label: a.label,
|
|
154
254
|
icon: a.icon,
|
|
255
|
+
contextual: a.where || undefined,
|
|
155
256
|
shortcut: a.shortcut,
|
|
156
257
|
// The palette ranked these against the query; the menu's own filter passes what it kept.
|
|
157
258
|
keywords: [query],
|
|
@@ -162,9 +263,12 @@ export function Palette({ onClose, go, actions = [], mode = 'chat', host, }) {
|
|
|
162
263
|
},
|
|
163
264
|
})),
|
|
164
265
|
}));
|
|
165
|
-
}, [query, threads, agents,
|
|
266
|
+
}, [query, threads, agents, repos, known, plugged, frame, go, onClose, signOut, mode, runs, host, opened]);
|
|
267
|
+
const apple = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent);
|
|
166
268
|
const listed = keys
|
|
167
|
-
? [{ id: 'keys', label: 'Keyboard shortcuts', items: KEYS.map((k) => ({
|
|
269
|
+
? [{ id: 'keys', label: 'Keyboard shortcuts', items: KEYS.map((k) => ({ id: `key-${k.does}`, label: k.does, contextual: apple ? k.mac : k.pc, onSelect: () => setKeys(false) })) }]
|
|
168
270
|
: groups;
|
|
169
|
-
return (_jsx(Dialog, { open: true, onOpenChange: (open) => (open ? null : onClose()), children: _jsxs(DialogContent, { "data-slot": "palette", p: 0, gap: 0, width: "100%", maxW:
|
|
271
|
+
return (_jsx(Dialog, { open: true, onOpenChange: (open) => (open ? null : onClose()), children: _jsxs(DialogContent, { "data-slot": "palette", showCloseButton: false, p: 0, gap: 0, width: "100%", maxW: 640,
|
|
272
|
+
// As tall as its rows, under a fixed top, so typing never moves the field.
|
|
273
|
+
maxH: "min(520px, 76dvh)", mt: "12dvh", mb: "auto", rounded: "$6", overflow: "hidden", "$max-md": { width: '100vw', maxW: '100vw', height: '100dvh', maxH: '100dvh', mt: 0, mb: 0, 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 or jump to\u2026", label: "Search", empty: keys ? 'No such key.' : `Nothing matches “${query.trim()}”.`, onBack: keys ? () => setKeys(false) : undefined, onClose: onClose })] }) }));
|
|
170
274
|
}
|
package/dist/Rail.d.ts
CHANGED
package/dist/Rail.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { Fragment as _Fragment, 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
4
|
// order — Chat, Dev, Studio, Tabs, Library, Scheduled tasks, Customize — then
|
|
5
|
-
// More (Team, on a host of its own,
|
|
5
|
+
// More (Team, on a host of its own, then the apps in beta this reader may open
|
|
6
|
+
// and Betas, the list of every app in beta; none of it in Studio), and the
|
|
7
|
+
// account at the foot. It
|
|
6
8
|
// is how the app moves between its products; a phone, which has no rail,
|
|
7
9
|
// switches Chat and Dev in its bar (Top.tsx).
|
|
8
10
|
//
|
|
@@ -26,11 +28,14 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
26
28
|
// rail lies along the foot of the drawer (`flat`), the account at its end; the
|
|
27
29
|
// drawer is too narrow for every mark, so Library, Scheduled tasks and
|
|
28
30
|
// Customize lead More there.
|
|
29
|
-
import { Clock, Code2, Ellipsis, Library, MessageSquare, PanelLeft, Puzzle, SquareTerminal, Users, WandSparkles } from 'lucide-react';
|
|
31
|
+
import { Clock, Code2, Ellipsis, FlaskConical, Library, MessageSquare, PanelLeft, Puzzle, Sparkles, SquareTerminal, Users, WandSparkles } from 'lucide-react';
|
|
32
|
+
import { useState } from 'react';
|
|
30
33
|
import { XStack, YStack } from '@hanzo/gui';
|
|
31
34
|
import { route } from '@hanzo/build';
|
|
32
35
|
import { DropdownMenu } from '@hanzo/ui';
|
|
36
|
+
import { BetasDialog, useBetas } from './Betas.js';
|
|
33
37
|
import { out } from './help.js';
|
|
38
|
+
import { BETAS } from './lib/betas.js';
|
|
34
39
|
import { Me } from './Me.js';
|
|
35
40
|
import { app, WORKSPACE } from './lib/host.js';
|
|
36
41
|
/** The rail's width, px. */
|
|
@@ -70,6 +75,10 @@ export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev
|
|
|
70
75
|
// The products on hosts of their own, behind More; on a phone the raised places lead it.
|
|
71
76
|
// Studio offers no Team, so on a laptop it draws no More.
|
|
72
77
|
const team = mode !== 'studio';
|
|
78
|
+
const { open: opened } = useBetas();
|
|
79
|
+
const [betas, setBetas] = useState(false);
|
|
80
|
+
// The apps in beta this reader may open, each where it lives.
|
|
81
|
+
const granted = BETAS.filter((b) => b.href && opened(b.id));
|
|
73
82
|
const rest = [
|
|
74
83
|
...(flat
|
|
75
84
|
? [
|
|
@@ -79,8 +88,15 @@ export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev
|
|
|
79
88
|
...(team ? [{ type: 'separator', key: 'raised' }] : []),
|
|
80
89
|
]
|
|
81
90
|
: []),
|
|
82
|
-
...(team
|
|
91
|
+
...(team
|
|
92
|
+
? [
|
|
93
|
+
{ key: 'team', label: 'Team', icon: _jsx(Users, { size: 16, "aria-hidden": true }), onSelect: () => out(WORKSPACE) },
|
|
94
|
+
...granted.map((b) => ({ key: `beta-${b.id}`, label: b.name, icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), onSelect: () => out(b.href) })),
|
|
95
|
+
{ type: 'separator', key: 'betas-rule' },
|
|
96
|
+
{ key: 'betas', label: 'Betas', icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), onSelect: () => setBetas(true) },
|
|
97
|
+
]
|
|
98
|
+
: []),
|
|
83
99
|
];
|
|
84
100
|
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 }) }), _jsx(Mark, { label: "Tabs", on: mode === 'tabs', onPress: onTabs, "data-slot": "rail-tabs", children: _jsx(SquareTerminal, { 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 }) })] })), rest.length ? (_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 })) : null, _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, above: flat, onOrg: onOrg })] }));
|
|
101
|
+
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 }) }), _jsx(Mark, { label: "Tabs", on: mode === 'tabs', onPress: onTabs, "data-slot": "rail-tabs", children: _jsx(SquareTerminal, { 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(BetasDialog, { open: betas, onOpenChange: setBetas, go: out }), rest.length ? (_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 })) : null, _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, above: flat, onOrg: onOrg })] }));
|
|
86
102
|
}
|
package/dist/Shell.js
CHANGED
|
@@ -1069,7 +1069,7 @@ function Frame({ mode, children, navigate, back, forward, }) {
|
|
|
1069
1069
|
const shortcut = (event) => {
|
|
1070
1070
|
if (summons(event)) {
|
|
1071
1071
|
event.preventDefault();
|
|
1072
|
-
setFinding(
|
|
1072
|
+
setFinding((open) => !open);
|
|
1073
1073
|
}
|
|
1074
1074
|
};
|
|
1075
1075
|
window.addEventListener("keydown", shortcut);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The apps in beta: every Hanzo app that exists or is announced and that the
|
|
3
|
+
* Hanzo app does not offer everyone yet, once, for every list that names them —
|
|
4
|
+
* the app's Betas list (Betas.tsx) and hanzo.ai/beta's.
|
|
5
|
+
*
|
|
6
|
+
* Each is a principal switch on the cloud, `beta.<id>` (hanzo-inc/cloud
|
|
7
|
+
* apps/flags betas.go carries the same ids), decided per caller: a SuperAdmin
|
|
8
|
+
* grants one to a person from their CRM record. `opens` is the one predicate a
|
|
9
|
+
* caller asks of that answer.
|
|
10
|
+
*
|
|
11
|
+
* `href` is where the app opens once granted: its own host, or its page in the
|
|
12
|
+
* page on the developer platform. An app with none is not built yet as an app; asking for it says you
|
|
13
|
+
* want it.
|
|
14
|
+
*
|
|
15
|
+
* Only what no one has today: nothing hanzo.ai or platform.hanzo.ai already
|
|
16
|
+
* offers every user is listed here, so no switch hides anything a person has.
|
|
17
|
+
*/
|
|
18
|
+
export declare const GROUPS: readonly ['Business', 'Data', 'Agents', 'Build & run', 'Security', 'Creative'];
|
|
19
|
+
export type Group = (typeof GROUPS)[number];
|
|
20
|
+
export interface Beta {
|
|
21
|
+
/** Stable, lowercase, one word: the switch is `beta.<id>`. */
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
/** What it is, in one line. */
|
|
25
|
+
line: string;
|
|
26
|
+
group: Group;
|
|
27
|
+
/** Where it opens once granted; absent while it is not built as an app. */
|
|
28
|
+
href?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare const BETAS: readonly Beta[];
|
|
31
|
+
/** The switch of one app in beta. */
|
|
32
|
+
export declare const key: (id: string) => string;
|
|
33
|
+
/**
|
|
34
|
+
* THE predicate: whether a reader may open the app `id`. `flags` is what
|
|
35
|
+
* GET /v1/flag/platform answered for them; `sudo` is a SuperAdmin (who sees every
|
|
36
|
+
* app) or a page served off a developer's own machine. Nothing else opens one.
|
|
37
|
+
*/
|
|
38
|
+
export declare const opens: (id: string, flags: Record<string, boolean>, sudo: boolean) => boolean;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The apps in beta: every Hanzo app that exists or is announced and that the
|
|
3
|
+
* Hanzo app does not offer everyone yet, once, for every list that names them —
|
|
4
|
+
* the app's Betas list (Betas.tsx) and hanzo.ai/beta's.
|
|
5
|
+
*
|
|
6
|
+
* Each is a principal switch on the cloud, `beta.<id>` (hanzo-inc/cloud
|
|
7
|
+
* apps/flags betas.go carries the same ids), decided per caller: a SuperAdmin
|
|
8
|
+
* grants one to a person from their CRM record. `opens` is the one predicate a
|
|
9
|
+
* caller asks of that answer.
|
|
10
|
+
*
|
|
11
|
+
* `href` is where the app opens once granted: its own host, or its page in the
|
|
12
|
+
* page on the developer platform. An app with none is not built yet as an app; asking for it says you
|
|
13
|
+
* want it.
|
|
14
|
+
*
|
|
15
|
+
* Only what no one has today: nothing hanzo.ai or platform.hanzo.ai already
|
|
16
|
+
* offers every user is listed here, so no switch hides anything a person has.
|
|
17
|
+
*/
|
|
18
|
+
export const GROUPS = ['Business', 'Data', 'Agents', 'Build & run', 'Security', 'Creative'];
|
|
19
|
+
/** The developer platform, where a module in beta opens at its own address for anyone who has it. */
|
|
20
|
+
const PLATFORM = 'https://platform.hanzo.ai';
|
|
21
|
+
export const BETAS = [
|
|
22
|
+
{ id: 'crm', name: 'CRM', line: 'Companies, contacts and deals, per org', group: 'Business', href: `${PLATFORM}/crm` },
|
|
23
|
+
{ id: 'erp', name: 'ERP', line: 'Items, sales, purchasing, accounting and HR', group: 'Business', href: `${PLATFORM}/erp` },
|
|
24
|
+
{ id: 'commerce', name: 'Commerce', line: 'Catalog, cart, checkout and orders', group: 'Business', href: 'https://commerce-admin.hanzo.ai/admin' },
|
|
25
|
+
{ id: 'books', name: 'Books', line: 'Chart of accounts, ledger and bank reconciliation', group: 'Business' },
|
|
26
|
+
{ id: 'finance', name: 'Finance', line: 'One ledger for your money operations', group: 'Business', href: 'https://finance.hanzo.ai' },
|
|
27
|
+
{ id: 'treasury', name: 'Treasury & Ledger', line: 'Reserves, payouts and a double-entry journal', group: 'Business' },
|
|
28
|
+
{ id: 'dataroom', name: 'Data Room', line: 'Share documents and see who opened them', group: 'Business', href: 'https://dataroom.hanzo.ai' },
|
|
29
|
+
{ id: 'sign', name: 'Sign', line: 'Send a PDF, get it signed, keep the record', group: 'Business', href: 'https://esign.hanzo.ai' },
|
|
30
|
+
{ id: 'company', name: 'Company', line: 'Formation, documents and filings', group: 'Business', href: `${PLATFORM}/company` },
|
|
31
|
+
{ id: 'cms', name: 'CMS', line: 'Pages, posts, media and navigation', group: 'Business', href: 'https://cms.hanzo.ai/admin' },
|
|
32
|
+
{ id: 'helpdesk', name: 'Help Center', line: 'Tickets, teams and SLAs', group: 'Business', href: 'https://help.hanzo.ai' },
|
|
33
|
+
{ id: 'marketing', name: 'Marketing', line: 'Campaigns, audiences and email sequences', group: 'Business', href: 'https://marketing.hanzo.ai' },
|
|
34
|
+
{ id: 'ads', name: 'Ads', line: 'Paid campaigns, launched and paused in one place', group: 'Business', href: 'https://ads.hanzo.ai' },
|
|
35
|
+
{ id: 'social', name: 'Social', line: 'Schedule posts across your social accounts', group: 'Business', href: 'https://social.hanzo.ai' },
|
|
36
|
+
{ id: 'tel', name: 'Telecom', line: 'Phone numbers, calls and messages', group: 'Business', href: `${PLATFORM}/tel` },
|
|
37
|
+
{ id: 'risk', name: 'Risk', line: 'AML monitoring and fraud on one engine', group: 'Business', href: 'https://aml.hanzo.ai' },
|
|
38
|
+
{ id: 'idv', name: 'Identity Verification', line: 'KYC, KYB and AML vendors behind one interface', group: 'Business' },
|
|
39
|
+
{ id: 'base', name: 'Base', line: 'Data, users, files and live updates per org', group: 'Data', href: 'https://base.hanzo.ai' },
|
|
40
|
+
{ id: 'analytics', name: 'Analytics', line: 'Privacy-first product analytics', group: 'Data', href: 'https://analytics.hanzo.ai' },
|
|
41
|
+
{ id: 'insights', name: 'Insights', line: 'Funnels, session replay, flags and experiments', group: 'Data', href: 'https://insights.hanzo.ai' },
|
|
42
|
+
{ id: 'search', name: 'Search', line: 'Keyword and meaning in one query', group: 'Data', href: `${PLATFORM}/search` },
|
|
43
|
+
{ id: 'world', name: 'World', line: 'A live map of every model and AI workload', group: 'Data', href: 'https://world.hanzo.ai' },
|
|
44
|
+
{ id: 'crawl', name: 'Crawl', line: 'Turn a website into clean Markdown', group: 'Data', href: `${PLATFORM}/crawl` },
|
|
45
|
+
{ id: 'operative', name: 'Operative', line: 'A desktop in a container, driven by a model', group: 'Agents' },
|
|
46
|
+
{ id: 'tasks', name: 'Tasks', line: 'Durable execution for long-running work', group: 'Agents', href: `${PLATFORM}/tasks` },
|
|
47
|
+
{ id: 'functions', name: 'Functions', line: 'Serverless functions, no servers to run', group: 'Build & run', href: `${PLATFORM}/functions` },
|
|
48
|
+
{ id: 'visor', name: 'Visor', line: 'Provision and watch VMs and bare metal', group: 'Build & run', href: 'https://visor.hanzo.ai' },
|
|
49
|
+
{ id: 'edge', name: 'Edge', line: 'Compute close to your users', group: 'Build & run', href: `${PLATFORM}/edge` },
|
|
50
|
+
{ id: 'registry', name: 'Registry', line: 'A container registry signed in with Hanzo', group: 'Build & run', href: `${PLATFORM}/registry` },
|
|
51
|
+
{ id: 'ci', name: 'CI', line: 'From source to a signed image', group: 'Build & run', href: 'https://ci.hanzo.ai' },
|
|
52
|
+
{ id: 'kms', name: 'KMS', line: 'Secrets held per org, never in code', group: 'Security', href: 'https://kms.hanzo.ai' },
|
|
53
|
+
{ id: 'guard', name: 'Guard', line: 'Stops prompt injection and leaks in model calls', group: 'Security' },
|
|
54
|
+
{ id: 'sentinel', name: 'Sentinel', line: 'Errors and slow requests, caught early', group: 'Security', href: 'https://sentry.hanzo.ai' },
|
|
55
|
+
{ id: 'authz', name: 'Authz', line: 'Fine-grained authorization, Zanzibar style', group: 'Security', href: `${PLATFORM}/authz` },
|
|
56
|
+
{ id: 'hsm', name: 'HSM', line: 'Hardware-backed keys across cloud KMSs', group: 'Security', href: `${PLATFORM}/hsm` },
|
|
57
|
+
{ id: 'oto', name: 'Oto', line: 'Sound effects and Foley for film and games', group: 'Creative' },
|
|
58
|
+
];
|
|
59
|
+
/** The switch of one app in beta. */
|
|
60
|
+
export const key = (id) => `beta.${id}`;
|
|
61
|
+
/**
|
|
62
|
+
* THE predicate: whether a reader may open the app `id`. `flags` is what
|
|
63
|
+
* GET /v1/flag/platform answered for them; `sudo` is a SuperAdmin (who sees every
|
|
64
|
+
* app) or a page served off a developer's own machine. Nothing else opens one.
|
|
65
|
+
*/
|
|
66
|
+
export const opens = (id, flags, sudo) => sudo || flags[key(id)] === true;
|
package/dist/lib/session.d.ts
CHANGED
|
@@ -153,12 +153,14 @@ export declare function home(): string | null;
|
|
|
153
153
|
*/
|
|
154
154
|
export declare function role(o: string | null): string | null;
|
|
155
155
|
/**
|
|
156
|
-
* Who the token names — its display name
|
|
156
|
+
* Who the token names — its display name (the username when it has none; `display`
|
|
157
|
+
* is the display name alone) and email — or null. The IAM user says
|
|
157
158
|
* the same once userinfo answers; this is the answer before it, so a signed-in
|
|
158
159
|
* reader is never drawn as a stranger while it is asked.
|
|
159
160
|
*/
|
|
160
161
|
export declare function named(): {
|
|
161
162
|
name: string;
|
|
163
|
+
display: string;
|
|
162
164
|
email: string;
|
|
163
165
|
} | null;
|
|
164
166
|
/**
|
package/dist/lib/session.js
CHANGED
|
@@ -385,7 +385,8 @@ export function role(o) {
|
|
|
385
385
|
return null;
|
|
386
386
|
}
|
|
387
387
|
/**
|
|
388
|
-
* Who the token names — its display name
|
|
388
|
+
* Who the token names — its display name (the username when it has none; `display`
|
|
389
|
+
* is the display name alone) and email — or null. The IAM user says
|
|
389
390
|
* the same once userinfo answers; this is the answer before it, so a signed-in
|
|
390
391
|
* reader is never drawn as a stranger while it is asked.
|
|
391
392
|
*/
|
|
@@ -394,7 +395,7 @@ export function named() {
|
|
|
394
395
|
if (!c)
|
|
395
396
|
return null;
|
|
396
397
|
const text = (k) => (typeof c[k] === 'string' ? c[k] : '');
|
|
397
|
-
return { name: text('displayName') || text('name'), email: text('email') };
|
|
398
|
+
return { name: text('displayName') || text('name'), display: text('displayName'), email: text('email') };
|
|
398
399
|
}
|
|
399
400
|
/**
|
|
400
401
|
* The reader's account as IAM keys a membership: `<home org>/<username>`, or null.
|
package/dist/lib/title.d.ts
CHANGED
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
* characters with an ellipsis; `fallback` when nothing readable is left.
|
|
6
6
|
*/
|
|
7
7
|
export declare function tidy(title: string | null | undefined, max?: number, fallback?: string): string;
|
|
8
|
+
/** A name's first word, its first letter capitalised ("z" → "Z", "ada lovelace" → "Ada"), or '' for none. */
|
|
9
|
+
export declare function given(name: string): string;
|