@hanzo/rooms 0.1.90 → 0.1.92
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 +9 -3
- package/dist/Betas.d.ts +21 -0
- package/dist/Betas.js +127 -0
- package/dist/Chat.js +45 -8
- 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 +23 -5
- 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 +4 -3
- 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 +5 -5
package/dist/App.js
CHANGED
|
@@ -119,7 +119,7 @@ function Window({ bar }) {
|
|
|
119
119
|
const shut = useCallback(() => setDrawer(false), []);
|
|
120
120
|
// The sidebar shut, kept per browser under the key the builder's rail kept it under.
|
|
121
121
|
const [closed, setClosed] = useKept('hanzo.build.rail', false);
|
|
122
|
-
// The palette is open (
|
|
122
|
+
// The palette is open (the sidebar's search; ⌘K toggles it).
|
|
123
123
|
const [finding, setFinding] = useState(false);
|
|
124
124
|
const found = useCallback(() => setFinding(false), []);
|
|
125
125
|
const dev = useDevHost();
|
|
@@ -156,7 +156,13 @@ function Window({ bar }) {
|
|
|
156
156
|
// A run floats two panes of its own, so the pane it stands in is not cut.
|
|
157
157
|
const run = where !== null && route(where).kind === 'run';
|
|
158
158
|
const asked = at.mode === 'chat' ? at.thread : null;
|
|
159
|
-
const { thread } = useOpen();
|
|
159
|
+
const { thread, settings } = useOpen();
|
|
160
|
+
// A row of the account menu asks for Settings: the drawer goes away even when
|
|
161
|
+
// the address does not move (the reader was on that page already).
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (settings)
|
|
164
|
+
setDrawer(false);
|
|
165
|
+
}, [settings]);
|
|
160
166
|
const plans = where !== null && isPlans(where);
|
|
161
167
|
useEffect(() => {
|
|
162
168
|
if (plans)
|
|
@@ -211,7 +217,7 @@ function Window({ bar }) {
|
|
|
211
217
|
const key = (e) => {
|
|
212
218
|
if (summons(e)) {
|
|
213
219
|
e.preventDefault();
|
|
214
|
-
setFinding(
|
|
220
|
+
setFinding((open) => !open);
|
|
215
221
|
return;
|
|
216
222
|
}
|
|
217
223
|
if (typing(e))
|
package/dist/Betas.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The apps in beta as this reader stands: which open, which they asked for, and the ask. */
|
|
2
|
+
export declare function useBetas(): {
|
|
3
|
+
/** The one predicate: whether this reader may open the app `id`. */
|
|
4
|
+
open: (id: string) => boolean;
|
|
5
|
+
/** Whether they asked for it. */
|
|
6
|
+
asked: (id: string) => boolean;
|
|
7
|
+
/** Ask for these apps. */
|
|
8
|
+
request: (ids: string[]) => Promise<void>;
|
|
9
|
+
/** The cloud has answered for this reader. */
|
|
10
|
+
ready: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** Every app in beta, by group, each with its action. */
|
|
13
|
+
export declare function BetasList({ go }: {
|
|
14
|
+
go?: (href: string) => void;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
16
|
+
/** The Betas list in a dialog, from the rail's More. */
|
|
17
|
+
export declare function BetasDialog({ open, onOpenChange, go }: {
|
|
18
|
+
open: boolean;
|
|
19
|
+
onOpenChange: (open: boolean) => void;
|
|
20
|
+
go?: (href: string) => void;
|
|
21
|
+
}): import("react").JSX.Element;
|
package/dist/Betas.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// The apps in beta: which this reader may open, and the list they ask from.
|
|
4
|
+
//
|
|
5
|
+
// WHO MAY OPEN ONE is the cloud's answer, never this page's. Each app in beta is a
|
|
6
|
+
// principal switch, `beta.<id>`, decided per caller from the validated org and
|
|
7
|
+
// person (cloud apps/flags), and GET /v1/flag/platform answers every one for the
|
|
8
|
+
// signed-in caller. A SuperAdmin grants one to a person from their CRM record.
|
|
9
|
+
// `opens` (lib/betas) reads that answer, the one predicate every caller asks.
|
|
10
|
+
//
|
|
11
|
+
// WHAT THEY ASKED FOR is their CRM record's, apps/marketing's beta record:
|
|
12
|
+
// GET /v1/marketing/beta reads it and POST /v1/marketing/beta/request adds to it,
|
|
13
|
+
// stamped with the time and stated on the event plane under the person and the
|
|
14
|
+
// org, so the request reaches the team where it works the person.
|
|
15
|
+
//
|
|
16
|
+
// Signed out, still loading, or on any error nothing is open: a flags outage
|
|
17
|
+
// cannot leak an unfinished app.
|
|
18
|
+
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
|
|
19
|
+
import { useIam } from '@hanzo/iam/react';
|
|
20
|
+
import { Button, Dialog, DialogContent, DialogTitle, Text, XStack, YStack } from '@hanzo/ui';
|
|
21
|
+
import { api } from './lib/api.js';
|
|
22
|
+
import { BETAS, GROUPS, opens } from './lib/betas.js';
|
|
23
|
+
import { bearer, scope, superAdmin } from './lib/session.js';
|
|
24
|
+
const NONE = { flags: {}, asked: [] };
|
|
25
|
+
/** One read per token, shared by every caller on the page. */
|
|
26
|
+
let held = null;
|
|
27
|
+
let reading = null;
|
|
28
|
+
const listeners = new Set();
|
|
29
|
+
const tell = () => listeners.forEach((f) => f());
|
|
30
|
+
/** A switch read this long ago is read again on the next mount. */
|
|
31
|
+
const FRESH = 30_000;
|
|
32
|
+
async function get(path) {
|
|
33
|
+
const r = await fetch(`${api()}${path}`, { headers: scope() });
|
|
34
|
+
return r.ok ? (await r.json()) : null;
|
|
35
|
+
}
|
|
36
|
+
function read(token) {
|
|
37
|
+
if (reading)
|
|
38
|
+
return reading;
|
|
39
|
+
reading = Promise.all([
|
|
40
|
+
get('/v1/flag/platform').catch(() => null),
|
|
41
|
+
get('/v1/marketing/beta').catch(() => null),
|
|
42
|
+
])
|
|
43
|
+
.then(([f, b]) => {
|
|
44
|
+
held = { token, said: { flags: f?.flags ?? {}, asked: b?.betas ?? [] }, at: Date.now() };
|
|
45
|
+
})
|
|
46
|
+
.finally(() => {
|
|
47
|
+
reading = null;
|
|
48
|
+
tell();
|
|
49
|
+
});
|
|
50
|
+
return reading;
|
|
51
|
+
}
|
|
52
|
+
const subscribe = (f) => {
|
|
53
|
+
listeners.add(f);
|
|
54
|
+
return () => listeners.delete(f);
|
|
55
|
+
};
|
|
56
|
+
/** The apps in beta as this reader stands: which open, which they asked for, and the ask. */
|
|
57
|
+
export function useBetas() {
|
|
58
|
+
const { accessToken } = useIam();
|
|
59
|
+
const [sudo, setSudo] = useState(false);
|
|
60
|
+
const [local, setLocal] = useState(false);
|
|
61
|
+
// Facts only the browser holds arrive after mount, so the first render
|
|
62
|
+
// matches the HTML it hydrates.
|
|
63
|
+
useEffect(() => setLocal(here()), []);
|
|
64
|
+
useEffect(() => setSudo(superAdmin()), [accessToken]);
|
|
65
|
+
const token = accessToken ? bearer() ?? '' : '';
|
|
66
|
+
const now = useSyncExternalStore(subscribe, () => held, () => null);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (token && (!held || held.token !== token || Date.now() - held.at > FRESH))
|
|
69
|
+
void read(token);
|
|
70
|
+
}, [token]);
|
|
71
|
+
const said = token && now?.token === token ? now.said : NONE;
|
|
72
|
+
const open = useCallback((id) => opens(id, said.flags, sudo || local), [said, sudo, local]);
|
|
73
|
+
const asked = useCallback((id) => said.asked.includes(id), [said]);
|
|
74
|
+
const request = useCallback(async (ids) => {
|
|
75
|
+
const r = await fetch(`${api()}/v1/marketing/beta/request`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'content-type': 'application/json', ...scope() },
|
|
78
|
+
body: JSON.stringify({ betas: ids }),
|
|
79
|
+
});
|
|
80
|
+
if (!r.ok)
|
|
81
|
+
throw new Error(r.status === 401 ? 'Sign in to ask for access.' : `The request was not taken (${r.status}).`);
|
|
82
|
+
const record = (await r.json());
|
|
83
|
+
if (held && held.token === token) {
|
|
84
|
+
held = { ...held, said: { ...held.said, asked: record.betas ?? [...held.said.asked, ...ids] } };
|
|
85
|
+
tell();
|
|
86
|
+
}
|
|
87
|
+
}, [token]);
|
|
88
|
+
return { open, asked, request, ready: Boolean(token) && now?.token === token };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Whether this page is served from a developer's own machine, where every app
|
|
92
|
+
* opens: a room nobody can open is a room nobody can work on. The ORIGIN, which
|
|
93
|
+
* no deployment can acquire by configuration.
|
|
94
|
+
*/
|
|
95
|
+
function here() {
|
|
96
|
+
if (typeof window === 'undefined')
|
|
97
|
+
return false;
|
|
98
|
+
const host = window.location.hostname;
|
|
99
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '[::1]' || host.endsWith('.local') || /^10\./.test(host) || /^192\.168\./.test(host);
|
|
100
|
+
}
|
|
101
|
+
/** One app's action: Open where it may, Requested once asked, else Request access. */
|
|
102
|
+
function Ask({ beta, go }) {
|
|
103
|
+
const { open, asked, request } = useBetas();
|
|
104
|
+
const [busy, setBusy] = useState(false);
|
|
105
|
+
const [wrong, setWrong] = useState(null);
|
|
106
|
+
if (open(beta.id)) {
|
|
107
|
+
return beta.href && go ? (_jsx(Button, { size: "sm", rounded: 999, onPress: () => go(beta.href), "data-slot": "beta-open", children: "Open" })) : (_jsx(Text, { fontSize: "$2", color: "$color11", "data-slot": "beta-on", children: "You\u2019re in" }));
|
|
108
|
+
}
|
|
109
|
+
if (asked(beta.id)) {
|
|
110
|
+
return (_jsx(Text, { fontSize: "$2", color: "$color11", "data-slot": "beta-requested", children: "Requested" }));
|
|
111
|
+
}
|
|
112
|
+
return (_jsxs(YStack, { items: "flex-end", gap: "$1", children: [_jsx(Button, { size: "sm", rounded: 999, variant: "outline", disabled: busy, "data-slot": "beta-request", onPress: () => {
|
|
113
|
+
setBusy(true);
|
|
114
|
+
setWrong(null);
|
|
115
|
+
request([beta.id])
|
|
116
|
+
.catch((e) => setWrong(e instanceof Error ? e.message : String(e)))
|
|
117
|
+
.finally(() => setBusy(false));
|
|
118
|
+
}, children: busy ? 'Requesting…' : 'Request access' }), wrong ? (_jsx(Text, { fontSize: "$1", color: "$red10", children: wrong })) : null] }));
|
|
119
|
+
}
|
|
120
|
+
/** Every app in beta, by group, each with its action. */
|
|
121
|
+
export function BetasList({ go }) {
|
|
122
|
+
return (_jsx(YStack, { gap: "$4", "data-slot": "betas", children: GROUPS.map((group) => (_jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$1", color: "$color10", textTransform: "uppercase", letterSpacing: 0.6, children: group }), BETAS.filter((b) => b.group === group).map((b) => (_jsxs(XStack, { items: "center", gap: "$3", "data-beta": b.id, children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(Text, { fontSize: "$3", fontWeight: "500", children: b.name }), _jsx(Text, { fontSize: "$2", color: "$color11", children: b.line })] }), _jsx(Ask, { beta: b, go: go })] }, b.id)))] }, group))) }));
|
|
123
|
+
}
|
|
124
|
+
/** The Betas list in a dialog, from the rail's More. */
|
|
125
|
+
export function BetasDialog({ open, onOpenChange, go }) {
|
|
126
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 520, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: "Betas" }), _jsx(Text, { fontSize: "$2", color: "$color11", children: "Apps we have not opened to everyone yet. Ask for the ones you want and we will let you in." }), _jsx(YStack, { maxH: "60vh", overflowY: "auto", overflowX: "hidden", pr: "$2", children: _jsx(BetasList, { go: go }) })] }) }));
|
|
127
|
+
}
|
package/dist/Chat.js
CHANGED
|
@@ -42,7 +42,7 @@ import { AgentMaker, HOUSE, useAgentShelf, useCrew } from "./crew.js";
|
|
|
42
42
|
import { called } from "./team.js";
|
|
43
43
|
import { Anchor, Box, Button, Text, View, XStack, YStack } from "@hanzo/ui";
|
|
44
44
|
import { useMedia } from "@hanzo/gui";
|
|
45
|
-
import { tidy } from "./lib/title.js";
|
|
45
|
+
import { given, tidy } from "./lib/title.js";
|
|
46
46
|
import { useIam, useIamToken } from "@hanzo/iam/react";
|
|
47
47
|
import { bearer, hasSession, named, org } from "./lib/session.js";
|
|
48
48
|
import { api } from "./lib/api.js";
|
|
@@ -64,7 +64,7 @@ import { Share } from "./Share.js";
|
|
|
64
64
|
import { ConversationMenu, ThreadMore, useThreadList } from "./thread.js";
|
|
65
65
|
import { Schedule } from "./Schedule.js";
|
|
66
66
|
import { keepFile, useAttach, useKeep } from "./keep.js";
|
|
67
|
-
import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, Summary, useDeck, useKey, Suggestions, useCarry, useLanguage, useMention, useMind, useSuggestions, CHAT_MODES, usePanel, frames, name as tabName, } from "@hanzo/build";
|
|
67
|
+
import { artifacts, lead, nameOf, PAGE, Panel, PanelToggle, prompt, renders, Reply, Served, Summary, useDeck, useKey, Suggestions, useCarry, useLanguage, useMention, useMind, useSuggestions, book, hand, taken, want, path as devPath, CHAT_MODES, usePanel, frames, name as tabName, } from "@hanzo/build";
|
|
68
68
|
import { checkoutUrl } from './lib/pay.js';
|
|
69
69
|
import { Upgrade, useFree } from './Upgrade.js';
|
|
70
70
|
import { enter, LOGIN } from './lib/destination.js';
|
|
@@ -527,8 +527,9 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
527
527
|
go: (href) => (/^https?:/.test(href) ? void window.open(href, "_blank", "noopener,noreferrer") : go(href)),
|
|
528
528
|
}, CHAT_MODES);
|
|
529
529
|
const agentShelf = useAgentShelf();
|
|
530
|
-
// The opening greets the person by
|
|
531
|
-
|
|
530
|
+
// The opening greets the person by the first word of their display name, capitalised ("What’s up, Z?").
|
|
531
|
+
// A username is a handle, not a name: without a display name the greeting has none.
|
|
532
|
+
const firstName = useMemo(() => (anonymous ? "" : given(named()?.display ?? "")), [anonymous]);
|
|
532
533
|
// Up to three things worth doing next, from the person's own activity (@hanzo/build).
|
|
533
534
|
const hints = useSuggestions(target);
|
|
534
535
|
// THE HEADER'S ⋯ IS THE ROW'S MENU (thread.tsx): rename, pin, share, its
|
|
@@ -1179,8 +1180,8 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1179
1180
|
.then((id) => (!open && id ? reopen(id) : undefined))
|
|
1180
1181
|
.catch(() => { });
|
|
1181
1182
|
};
|
|
1182
|
-
const submit = () => {
|
|
1183
|
-
const text =
|
|
1183
|
+
const submit = (ask = draft) => {
|
|
1184
|
+
const text = ask.trim();
|
|
1184
1185
|
if (preparing || (!text && !files.length))
|
|
1185
1186
|
return;
|
|
1186
1187
|
if (!carry.preparing)
|
|
@@ -1356,10 +1357,46 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1356
1357
|
};
|
|
1357
1358
|
// THE COMPOSER'S OPTIONS, held apart so its keys (the `/` and `@` lists' arrows,
|
|
1358
1359
|
// Ctrl+U) hook in through `useMention` before the frame is drawn.
|
|
1360
|
+
// What pressing a suggestion does, at once: open what it names, send its prompt
|
|
1361
|
+
// here (the room is a new chat while one shows), hand its run to Dev, or make
|
|
1362
|
+
// its scheduled task and show it in Scheduled tasks.
|
|
1363
|
+
const hint = (a) => {
|
|
1364
|
+
switch (a.kind) {
|
|
1365
|
+
case "open":
|
|
1366
|
+
if (a.item)
|
|
1367
|
+
want(a.item);
|
|
1368
|
+
if (/^https:\/\//.test(a.to))
|
|
1369
|
+
window.open(a.to, "_blank", "noopener,noreferrer");
|
|
1370
|
+
else
|
|
1371
|
+
go(a.to.startsWith("/") ? a.to : app(a.to));
|
|
1372
|
+
return;
|
|
1373
|
+
case "send":
|
|
1374
|
+
submit(a.prompt);
|
|
1375
|
+
return;
|
|
1376
|
+
case "task":
|
|
1377
|
+
hand("task", a.prompt);
|
|
1378
|
+
go(app(""));
|
|
1379
|
+
return;
|
|
1380
|
+
case "schedule":
|
|
1381
|
+
void book(target, a, "chat").then((made) => go(app(devPath({ kind: "automation", id: made.id }))), (e) => setPrepWrong(e instanceof Error && e.message ? e.message : "The scheduled task could not be made."));
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
// A chat a suggestion in Dev left for this room is sent once the room is a new
|
|
1386
|
+
// chat: the address's own conversation let go of, nothing said yet.
|
|
1387
|
+
const [handed] = useState(() => ({ prompt: anonymous ? "" : taken("send") }));
|
|
1388
|
+
useEffect(() => {
|
|
1389
|
+
if (!handed.prompt || open || said.length)
|
|
1390
|
+
return;
|
|
1391
|
+
const p = handed.prompt;
|
|
1392
|
+
handed.prompt = "";
|
|
1393
|
+
submit(p);
|
|
1394
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1395
|
+
}, [open, said.length]);
|
|
1359
1396
|
const composing = {
|
|
1360
1397
|
value: draft,
|
|
1361
1398
|
onChange: setDraft,
|
|
1362
|
-
onSend: submit,
|
|
1399
|
+
onSend: () => submit(),
|
|
1363
1400
|
onStop: chat.stop,
|
|
1364
1401
|
busy: chat.streaming,
|
|
1365
1402
|
// Files alone are a message: what is held goes with an empty draft too.
|
|
@@ -1424,7 +1461,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1424
1461
|
onLanguage: speakIn,
|
|
1425
1462
|
talk,
|
|
1426
1463
|
// While nothing has been said: what is worth doing next.
|
|
1427
|
-
below: said.length === 0 && !anonymous ? _jsx(Suggestions, { rows: hints.rows,
|
|
1464
|
+
below: said.length === 0 && !anonymous ? _jsx(Suggestions, { rows: hints.rows, onAct: hint, onDismiss: hints.dismiss }) : undefined,
|
|
1428
1465
|
};
|
|
1429
1466
|
const mention = useMention(composing);
|
|
1430
1467
|
return (_jsxs(YStack, { flex: 1, minH: 0, width: "100%", position: "relative", "data-slot": "chat-room",
|
package/dist/Me.js
CHANGED
|
@@ -5,7 +5,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
5
5
|
// other app (Shell.tsx).
|
|
6
6
|
//
|
|
7
7
|
// Top to bottom: who, with their plan, a row whose › opens the account itself;
|
|
8
|
-
// then Upgrade plan, Personalization, Profile and Settings (⌘, / Ctrl+,);
|
|
8
|
+
// then Upgrade plan, Personalization, Profile and Settings (⌘, / Ctrl+,); Beta,
|
|
9
|
+
// the person's place on the beta waitlist (the site's /beta/status); then
|
|
9
10
|
// Help ›, the site's own pages and the keyboard shortcuts (⌘/ / Ctrl+/ from
|
|
10
11
|
// anywhere); and Log out. Every row opens a real destination: Settings'
|
|
11
12
|
// sections through `showSettings` (open.ts), which the app answers with the
|
|
@@ -29,7 +30,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
29
30
|
// mints a token that carries it.
|
|
30
31
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
31
32
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
32
|
-
import { ArrowUpRight, Building2, Check, ChevronsUpDown, CircleHelp, CircleUserRound, Download, FileText, Info, Keyboard, LifeBuoy, LogOut, Palette, Plus, ScrollText, Settings, ShieldCheck, Sparkles, Users, X, } from 'lucide-react';
|
|
33
|
+
import { ArrowUpRight, Building2, Check, ChevronsUpDown, CircleHelp, CircleUserRound, Download, FileText, FlaskConical, Info, Keyboard, LifeBuoy, LogOut, Palette, Plus, ScrollText, Settings, ShieldCheck, Sparkles, Users, X, } from 'lucide-react';
|
|
33
34
|
import { label, Meter, said as spoken, useStanding } from '@hanzo/build';
|
|
34
35
|
import { useOrganizations } from '@hanzo/iam/react';
|
|
35
36
|
import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Input, Kbd, } from '@hanzo/ui';
|
|
@@ -196,7 +197,7 @@ export function Me({ host, narrow = false, above = false, onOrg, }) {
|
|
|
196
197
|
// Clear of the rail's mark (or the row) it opens from, and flush with its foot.
|
|
197
198
|
offset: 8, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(XStack, { ref: (el) => {
|
|
198
199
|
row.current = el instanceof HTMLElement ? el : null;
|
|
199
|
-
}, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 26 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), working ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: working })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": "Account", onCloseAutoFocus: back, children: [_jsxs(DropdownMenuSub, { open: account, onOpenChange: setAccount, children: [_jsxs(DropdownMenuSubTrigger, { minH: 48, gap: "$2.5", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: host.person.email || working })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: "Organizations" }), all.map((o) => (_jsx(DropdownMenuItem, { onSelect: () => o.name !== current && onOrg(o.name), children: _jsx(Line, { icon: _jsx(Mark, { org: o }), sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), trail: o.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: "Create organization" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: "Organization settings" }) }), where().team ? null : here && !here.personal ? (_jsx(DropdownMenuItem, { onSelect: () => out(`${ENTRY}&org=${encodeURIComponent(here.name)}`), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), trail: _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }), children: "Open in Hanzo Team" }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: "Create a team" }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: "All organizations" }) })) : null] })) : null] }), team ? (_jsx(DropdownMenuItem, { "data-slot": "me-team", onSelect: () => team.name !== current && onOrg(team.name), children: _jsx(Line, { icon: _jsx(Mark, { org: team }), trail: team.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: `${team.display} · ${teamPlan || 'No plan'}` }) })) : null, personal ? (_jsx(DropdownMenuItem, { "data-slot": "me-personal", onSelect: () => personal.name !== current && onOrg(personal.name), actionsVisibility: "always", actions: ownPlan ? undefined : (_jsx(Button, { size: "xs", variant: "outline", onPress: getPro, children: "Get Pro" })), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), trail: personal.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: `Personal · ${ownPlan || 'No plan'}` }) })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { onSelect: () => leave('here'), children: _jsx(Line, { icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), children: upgrade(read.value?.label?.name) }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Palette, { size: 16, "aria-hidden": true }), children: "Personalization" }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: "Profile" }) }), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+, Meta+,", onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘,' : 'Ctrl+,' }), children: "Settings" }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: "Help" }) }), help ? (_jsxs(DropdownMenuSubContent, { minW: 240, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [pages().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], trail: p.key === 'help' ? _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }) : null, children: p.label }) }, p.key))), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+/ Meta+/", onSelect: () => setKeys(true), children: _jsx(Line, { icon: _jsx(Keyboard, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘/' : 'Ctrl+/' }), children: "Keyboard shortcuts" }) }), _jsx(DropdownMenuSeparator, {}), legal().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], children: p.label }) }, p.key)))] })) : null] }), host.signOut ? (_jsx(DropdownMenuItem, { onSelect: () => host.signOut?.(), children: _jsx(Line, { icon: _jsx(LogOut, { size: 16, "aria-hidden": true }), children: "Log out" }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple })] }));
|
|
200
|
+
}, render: "button", "data-slot": "me", "aria-label": `Account: ${[who, current].filter(Boolean).join(' · ')}`, ...{ title: who }, width: narrow ? 36 : '100%', minH: narrow ? 36 : 48, px: narrow ? 0 : '$2', gap: "$2.5", rounded: narrow ? 999 : '$5', items: "center", justify: narrow ? 'center' : 'flex-start', hoverStyle: { bg: '$hover' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, cursor: "pointer", children: [_jsx(Face, { host: host, size: narrow ? 26 : 32 }), narrow ? null : (_jsxs(_Fragment, { children: [_jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$3", fontWeight: "600", color: "$ink", numberOfLines: 1, text: "left", children: who }), working ? (_jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, text: "left", children: working })) : null] }), _jsx(ChevronsUpDown, { size: 14, "aria-hidden": true, opacity: 0.6 })] }))] }) }), _jsxs(DropdownMenuContent, { minW: 260, maxW: "calc(100vw - 16px)", "aria-labelledby": undefined, "aria-label": "Account", onCloseAutoFocus: back, children: [_jsxs(DropdownMenuSub, { open: account, onOpenChange: setAccount, children: [_jsxs(DropdownMenuSubTrigger, { minH: 48, gap: "$2.5", children: [_jsx(Face, { host: host, size: 28 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$2", fontWeight: "600", color: "$ink", numberOfLines: 1, children: who }), _jsx(SizableText, { size: "$1", color: "$soft", numberOfLines: 1, children: host.person.email || working })] })] }), account ? (_jsxs(DropdownMenuSubContent, { minW: 260, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [_jsx(Meter, { read: read, onPlans: () => leave('here'), onBilling: () => (setOpen(false), showSettings('billing')) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { children: "Organizations" }), all.map((o) => (_jsx(DropdownMenuItem, { onSelect: () => o.name !== current && onOrg(o.name), children: _jsx(Line, { icon: _jsx(Mark, { org: o }), sub: [o.personal ? 'Personal' : said(o.role), planFor(o)].filter(Boolean).join(' · '), trail: o.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: o.display }) }, o.name))), _jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Plus, { size: 16, "aria-hidden": true }), children: "Create organization" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site(ORG_SETTINGS)), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), children: "Organization settings" }) }), where().team ? null : here && !here.personal ? (_jsx(DropdownMenuItem, { onSelect: () => out(`${ENTRY}&org=${encodeURIComponent(here.name)}`), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), trail: _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }), children: "Open in Hanzo Team" }) })) : (_jsx(DropdownMenuItem, { onSelect: () => setCreating(true), children: _jsx(Line, { icon: _jsx(Users, { size: 16, "aria-hidden": true }), children: "Create a team" }) })), superAdmin() ? (_jsx(DropdownMenuItem, { onSelect: choose, children: _jsx(Line, { icon: _jsx(Building2, { size: 16, "aria-hidden": true }), children: "All organizations" }) })) : null] })) : null] }), team ? (_jsx(DropdownMenuItem, { "data-slot": "me-team", onSelect: () => team.name !== current && onOrg(team.name), children: _jsx(Line, { icon: _jsx(Mark, { org: team }), trail: team.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: `${team.display} · ${teamPlan || 'No plan'}` }) })) : null, personal ? (_jsx(DropdownMenuItem, { "data-slot": "me-personal", onSelect: () => personal.name !== current && onOrg(personal.name), actionsVisibility: "always", actions: ownPlan ? undefined : (_jsx(Button, { size: "xs", variant: "outline", onPress: getPro, children: "Get Pro" })), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), trail: personal.name === current ? _jsx(Check, { size: 16, "aria-label": "Current" }) : null, children: `Personal · ${ownPlan || 'No plan'}` }) })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { onSelect: () => leave('here'), children: _jsx(Line, { icon: _jsx(Sparkles, { size: 16, "aria-hidden": true }), children: upgrade(read.value?.label?.name) }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Palette, { size: 16, "aria-hidden": true }), children: "Personalization" }) }), _jsx(DropdownMenuItem, { onSelect: () => showSettings('account'), children: _jsx(Line, { icon: _jsx(CircleUserRound, { size: 16, "aria-hidden": true }), children: "Profile" }) }), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+, Meta+,", onSelect: () => showSettings('general'), children: _jsx(Line, { icon: _jsx(Settings, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘,' : 'Ctrl+,' }), children: "Settings" }) }), _jsx(DropdownMenuItem, { onSelect: () => window.location.assign(site('/beta/status')), children: _jsx(Line, { icon: _jsx(FlaskConical, { size: 16, "aria-hidden": true }), children: "Beta" }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuSub, { open: help, onOpenChange: setHelp, children: [_jsx(DropdownMenuSubTrigger, { children: _jsx(Line, { icon: _jsx(LifeBuoy, { size: 16, "aria-hidden": true }), children: "Help" }) }), help ? (_jsxs(DropdownMenuSubContent, { minW: 240, maxW: "calc(100vw - 16px)", pointerEvents: "auto", children: [pages().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], trail: p.key === 'help' ? _jsx(ArrowUpRight, { size: 14, "aria-hidden": true, opacity: 0.6 }) : null, children: p.label }) }, p.key))), _jsx(DropdownMenuItem, { "aria-keyshortcuts": "Control+/ Meta+/", onSelect: () => setKeys(true), children: _jsx(Line, { icon: _jsx(Keyboard, { size: 16, "aria-hidden": true }), trail: _jsx(DropdownMenuShortcut, { "aria-hidden": true, children: apple ? '⌘/' : 'Ctrl+/' }), children: "Keyboard shortcuts" }) }), _jsx(DropdownMenuSeparator, {}), legal().map((p) => (_jsx(DropdownMenuItem, { onSelect: () => out(p.href), children: _jsx(Line, { icon: ICONS[p.key], children: p.label }) }, p.key)))] })) : null] }), host.signOut ? (_jsx(DropdownMenuItem, { onSelect: () => host.signOut?.(), children: _jsx(Line, { icon: _jsx(LogOut, { size: 16, "aria-hidden": true }), children: "Log out" }) })) : null] })] }), _jsx(Create, { open: creating, onOpenChange: setCreating }), _jsx(Shortcuts, { open: keys, onOpenChange: setKeys, apple: apple })] }));
|
|
200
201
|
}
|
|
201
202
|
/** The app's keys (help.ts `KEYS`), one row each, spelled for this platform. */
|
|
202
203
|
function Shortcuts({ open, onOpenChange, apple }) {
|
package/dist/Palette.d.ts
CHANGED
|
@@ -3,22 +3,36 @@ import { type Host } from '@hanzo/build';
|
|
|
3
3
|
import { type ShortcutKeys } from '@hanzo/ui';
|
|
4
4
|
import { type Place } from './lib/host.js';
|
|
5
5
|
import { type Hit } from './seek.js';
|
|
6
|
-
/** A row: what it is called, its glyph, and what pressing it does. */
|
|
6
|
+
/** A row: what it is called, its glyph, where it lives, and what pressing it does. */
|
|
7
7
|
export interface Action extends Hit {
|
|
8
8
|
icon?: ReactNode;
|
|
9
9
|
shortcut?: ShortcutKeys;
|
|
10
|
+
/** Where it lives, dimmed after its name: Settings, Customize, Help. */
|
|
11
|
+
where?: string;
|
|
10
12
|
run: () => void;
|
|
11
13
|
/** Pressing it keeps the palette open. */
|
|
12
14
|
stay?: boolean;
|
|
13
15
|
}
|
|
14
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Every place the reader can be taken: `top` is what an empty field lists, the
|
|
18
|
+
* products and the places of the app; `deep` is what only a query reaches.
|
|
19
|
+
* Without a host (hanzo.team's frame) the builder's registries are not there,
|
|
20
|
+
* and the products and the site's pages are.
|
|
21
|
+
*/
|
|
22
|
+
export declare function places(host: Host | undefined, go: (address: string) => void): {
|
|
23
|
+
top: Action[];
|
|
24
|
+
deep: Action[];
|
|
25
|
+
};
|
|
26
|
+
/** The palette's actions; a host that cannot do one (no theme to change, no org to pick) is not offered it. */
|
|
27
|
+
export declare function actions(host: Host | undefined, go: (address: string) => void, signOut: () => void, keys: () => void): Action[];
|
|
28
|
+
export declare function Palette({ onClose, go, actions: frame, mode, host, }: {
|
|
15
29
|
onClose: () => void;
|
|
16
|
-
/** Moves the app to one of its addresses (lib/host `talk`, `app`). */
|
|
30
|
+
/** Moves the app to one of its addresses (lib/host `talk`, `app`, `studio`). */
|
|
17
31
|
go: (address: string) => void;
|
|
18
|
-
/** The frame's own
|
|
32
|
+
/** The frame's own places; one with a palette row's id stands in for it, the rest follow under Go to. */
|
|
19
33
|
actions?: Action[];
|
|
20
|
-
/** The product the reader is in:
|
|
34
|
+
/** The product the reader is in: its own things lead a tie. */
|
|
21
35
|
mode?: Place['mode'];
|
|
22
|
-
/**
|
|
36
|
+
/** The builder's host: its registries, its runs, its theme and organization. */
|
|
23
37
|
host?: Host;
|
|
24
38
|
}): import("react").JSX.Element;
|
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. */
|
|
@@ -68,17 +73,30 @@ export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev
|
|
|
68
73
|
const here = mode === 'dev' ? spot(at) : null;
|
|
69
74
|
const to = (p) => () => go(app(p));
|
|
70
75
|
// The products on hosts of their own, behind More; on a phone the raised places lead it.
|
|
76
|
+
// Studio offers no Team, so on a laptop it draws no More.
|
|
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));
|
|
71
82
|
const rest = [
|
|
72
83
|
...(flat
|
|
73
84
|
? [
|
|
74
85
|
{ key: 'library', label: 'Library', icon: _jsx(Library, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.library) },
|
|
75
86
|
{ key: 'scheduled', label: 'Scheduled tasks', icon: _jsx(Clock, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.scheduled) },
|
|
76
87
|
{ key: 'customize', label: 'Customize', icon: _jsx(Puzzle, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.customize) },
|
|
77
|
-
{ type: 'separator', key: 'raised' },
|
|
88
|
+
...(team ? [{ type: 'separator', key: 'raised' }] : []),
|
|
89
|
+
]
|
|
90
|
+
: []),
|
|
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) },
|
|
78
97
|
]
|
|
79
98
|
: []),
|
|
80
|
-
{ key: 'team', label: 'Team', icon: _jsx(Users, { size: 16, "aria-hidden": true }), onSelect: () => out(WORKSPACE) },
|
|
81
99
|
];
|
|
82
100
|
const Stack = flat ? XStack : YStack;
|
|
83
|
-
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(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 })] }));
|
|
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 })] }));
|
|
84
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
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* who was signed in, which reads as a permissions problem rather than a typo.
|
|
15
15
|
* Two derivations of one name never agree; this is the one derivation.
|
|
16
16
|
*/
|
|
17
|
-
import { superAdmin as sudoOf } from '@hanzo/iam/
|
|
17
|
+
import { superAdmin as sudoOf } from '@hanzo/iam/identity';
|
|
18
18
|
import { api } from './api.js';
|
|
19
19
|
import { where } from '../where.js';
|
|
20
20
|
/** The SDK's own keys, `hanzo_iam_`-prefixed. The far side of a boundary. */
|
|
@@ -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;
|
package/dist/lib/title.js
CHANGED
|
@@ -34,3 +34,8 @@ export function tidy(title, max = 80, fallback = 'Untitled') {
|
|
|
34
34
|
return fallback;
|
|
35
35
|
return t.length > max ? `${t.slice(0, max - 1).trimEnd()}…` : t;
|
|
36
36
|
}
|
|
37
|
+
/** A name's first word, its first letter capitalised ("z" → "Z", "ada lovelace" → "Ada"), or '' for none. */
|
|
38
|
+
export function given(name) {
|
|
39
|
+
const w = name.trim().split(/\s+/)[0] ?? '';
|
|
40
|
+
return w ? w[0].toUpperCase() + w.slice(1) : '';
|
|
41
|
+
}
|
package/dist/seek.d.ts
CHANGED
|
@@ -15,32 +15,25 @@ export interface Group<T extends Hit = Hit> {
|
|
|
15
15
|
}
|
|
16
16
|
/** The most rows one group shows for a query. */
|
|
17
17
|
export declare const CAP = 5;
|
|
18
|
-
/** The
|
|
19
|
-
export declare const RECENT =
|
|
18
|
+
/** The rows Recent lists on an empty field. */
|
|
19
|
+
export declare const RECENT = 6;
|
|
20
20
|
/**
|
|
21
21
|
* How well a row answers a query: 0 drops it, more ranks it higher. Every word
|
|
22
22
|
* of the query must be found, in the label or the row's other words; each counts
|
|
23
23
|
* most where the label starts with it, then where a word of the label does, then
|
|
24
|
-
* anywhere in the label, then in the other words
|
|
25
|
-
*
|
|
24
|
+
* anywhere in the label, then in the other words, then spelled out in order
|
|
25
|
+
* within a word of the label (a typo's fuzz). The label read whole outranks
|
|
26
|
+
* everything.
|
|
26
27
|
*/
|
|
27
28
|
export declare function fit(row: Hit, query: string): number;
|
|
28
29
|
/** The rows a query keeps, best first, ties in the order they came, at most `cap`. */
|
|
29
30
|
export declare function rank<T extends Hit>(rows: T[], query: string, cap?: number): T[];
|
|
30
31
|
/**
|
|
31
|
-
* What the palette lists. An empty field: the newest
|
|
32
|
-
*
|
|
33
|
-
* `CAP`, a group with nothing left out
|
|
32
|
+
* What the palette lists. An empty field: the newest `RECENT` of `recent` under
|
|
33
|
+
* Recent, then `idle` as it is. A query: every group of `all` cut to its best
|
|
34
|
+
* `CAP`, a group with nothing left out, and the group with the best answer
|
|
35
|
+
* first, ties in the order given.
|
|
34
36
|
*/
|
|
35
|
-
export declare function find<T extends Hit>(query: string,
|
|
36
|
-
/**
|
|
37
|
-
* The same search for any product: an empty field lists the newest `RECENT` of
|
|
38
|
-
* `recent` under its label (a product's own recents: Chat's conversations, Dev's
|
|
39
|
-
* runs), and a query ranks every group in the order given.
|
|
40
|
-
*/
|
|
41
|
-
export declare function findIn<T extends Hit>(query: string, recent: {
|
|
42
|
-
label: string;
|
|
43
|
-
items: T[];
|
|
44
|
-
}, groups: Group<T>[]): Group<T>[];
|
|
37
|
+
export declare function find<T extends Hit>(query: string, recent: T[], idle: Group<T>[], all: Group<T>[]): Group<T>[];
|
|
45
38
|
/** Whether a key press opens the palette: ⌘K or Ctrl+K, once a press, from anywhere, a field included. */
|
|
46
39
|
export declare const summons: (e: Press) => boolean;
|
package/dist/seek.js
CHANGED
|
@@ -3,14 +3,23 @@
|
|
|
3
3
|
// so a spec reads them as they are.
|
|
4
4
|
/** The most rows one group shows for a query. */
|
|
5
5
|
export const CAP = 5;
|
|
6
|
-
/** The
|
|
7
|
-
export const RECENT =
|
|
6
|
+
/** The rows Recent lists on an empty field. */
|
|
7
|
+
export const RECENT = 6;
|
|
8
|
+
/** Whether `term`'s letters stand in `word` in order: "sttngs" in "settings". */
|
|
9
|
+
const spelled = (word, term) => {
|
|
10
|
+
let i = 0;
|
|
11
|
+
for (const c of word)
|
|
12
|
+
if (c === term[i])
|
|
13
|
+
i++;
|
|
14
|
+
return i === term.length;
|
|
15
|
+
};
|
|
8
16
|
/**
|
|
9
17
|
* How well a row answers a query: 0 drops it, more ranks it higher. Every word
|
|
10
18
|
* of the query must be found, in the label or the row's other words; each counts
|
|
11
19
|
* most where the label starts with it, then where a word of the label does, then
|
|
12
|
-
* anywhere in the label, then in the other words
|
|
13
|
-
*
|
|
20
|
+
* anywhere in the label, then in the other words, then spelled out in order
|
|
21
|
+
* within a word of the label (a typo's fuzz). The label read whole outranks
|
|
22
|
+
* everything.
|
|
14
23
|
*/
|
|
15
24
|
export function fit(row, query) {
|
|
16
25
|
const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
@@ -21,7 +30,17 @@ export function fit(row, query) {
|
|
|
21
30
|
const more = (row.keywords ?? []).join(' ').toLowerCase();
|
|
22
31
|
let total = label === terms.join(' ') ? 10 : 0;
|
|
23
32
|
for (const t of terms) {
|
|
24
|
-
const s = label.startsWith(t)
|
|
33
|
+
const s = label.startsWith(t)
|
|
34
|
+
? 4
|
|
35
|
+
: words.some((w) => w.startsWith(t))
|
|
36
|
+
? 3
|
|
37
|
+
: label.includes(t)
|
|
38
|
+
? 2
|
|
39
|
+
: more.includes(t)
|
|
40
|
+
? 1
|
|
41
|
+
: t.length > 2 && words.some((w) => w[0] === t[0] && spelled(w, t))
|
|
42
|
+
? 0.5
|
|
43
|
+
: 0;
|
|
25
44
|
if (!s)
|
|
26
45
|
return 0;
|
|
27
46
|
total += s;
|
|
@@ -38,24 +57,22 @@ export function rank(rows, query, cap = CAP) {
|
|
|
38
57
|
.map((x) => x.row);
|
|
39
58
|
}
|
|
40
59
|
/**
|
|
41
|
-
* What the palette lists. An empty field: the newest
|
|
42
|
-
*
|
|
43
|
-
* `CAP`, a group with nothing left out
|
|
60
|
+
* What the palette lists. An empty field: the newest `RECENT` of `recent` under
|
|
61
|
+
* Recent, then `idle` as it is. A query: every group of `all` cut to its best
|
|
62
|
+
* `CAP`, a group with nothing left out, and the group with the best answer
|
|
63
|
+
* first, ties in the order given.
|
|
44
64
|
*/
|
|
45
|
-
export function find(query,
|
|
46
|
-
return findIn(query, { label: 'Recent chats', items: chats }, [{ id: 'chats', label: 'Chats', items: chats }, ...groups]);
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* The same search for any product: an empty field lists the newest `RECENT` of
|
|
50
|
-
* `recent` under its label (a product's own recents: Chat's conversations, Dev's
|
|
51
|
-
* runs), and a query ranks every group in the order given.
|
|
52
|
-
*/
|
|
53
|
-
export function findIn(query, recent, groups) {
|
|
65
|
+
export function find(query, recent, idle, all) {
|
|
54
66
|
if (!query.trim()) {
|
|
55
|
-
const newest = [...recent
|
|
56
|
-
return
|
|
67
|
+
const newest = [...recent].sort((a, b) => (b.at ?? '').localeCompare(a.at ?? '')).slice(0, RECENT);
|
|
68
|
+
return [{ id: 'recent', label: 'Recent', items: newest }, ...idle].filter((g) => g.items.length);
|
|
57
69
|
}
|
|
58
|
-
return
|
|
70
|
+
return all
|
|
71
|
+
.map((g) => ({ ...g, items: rank(g.items, query) }))
|
|
72
|
+
.filter((g) => g.items.length)
|
|
73
|
+
.map((g, i) => ({ g, i, best: fit(g.items[0], query) }))
|
|
74
|
+
.sort((a, b) => b.best - a.best || a.i - b.i)
|
|
75
|
+
.map((x) => x.g);
|
|
59
76
|
}
|
|
60
77
|
/** Whether a key press opens the palette: ⌘K or Ctrl+K, once a press, from anywhere, a field included. */
|
|
61
78
|
export const summons = (e) => (e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && !e.repeat && !e.isComposing && (e.key ?? '').toLowerCase() === 'k';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/rooms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.92",
|
|
4
4
|
"description": "The workspace rooms — Home, Chat, Inbox, Contacts, Meet, Cal, Drive, Board, Work, Bots — and the frame that holds them, once, for every Hanzo surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@hanzo/ai": ">=0.6.23",
|
|
51
51
|
"@hanzo/appearance": ">=0.2.6",
|
|
52
|
-
"@hanzo/build": ">=0.2.
|
|
52
|
+
"@hanzo/build": ">=0.2.79",
|
|
53
53
|
"@hanzo/composer": ">=0.2.0",
|
|
54
54
|
"@hanzo/event": ">=0.3.56",
|
|
55
55
|
"@hanzo/gui": "^8.3.5",
|
|
56
|
-
"@hanzo/iam": ">=0.21.
|
|
56
|
+
"@hanzo/iam": ">=0.21.35",
|
|
57
57
|
"@hanzo/logo": ">=1.0.26",
|
|
58
58
|
"@hanzo/studio": ">=0.1.0",
|
|
59
59
|
"@hanzo/tabs": ">=0.1.8",
|
|
@@ -68,12 +68,12 @@
|
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@hanzo/ai": "0.6.23",
|
|
70
70
|
"@hanzo/appearance": "0.2.8",
|
|
71
|
-
"@hanzo/build": "0.2.
|
|
71
|
+
"@hanzo/build": "0.2.79",
|
|
72
72
|
"@hanzo/composer": "0.2.0",
|
|
73
73
|
"@hanzo/design": "0.5.34",
|
|
74
74
|
"@hanzo/event": "0.3.56",
|
|
75
75
|
"@hanzo/gui": "8.3.5",
|
|
76
|
-
"@hanzo/iam": "0.21.
|
|
76
|
+
"@hanzo/iam": "0.21.35",
|
|
77
77
|
"@hanzo/logo": "1.0.26",
|
|
78
78
|
"@hanzo/studio": "0.1.0",
|
|
79
79
|
"@hanzo/tabs": "github:hanzo-apps/tabs#af6f1e4c02a3ef7faa192a8310e193219fa5c1c5",
|