@hanzo/rooms 0.1.34 → 0.1.36
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/README.md +2 -2
- package/dist/Chat.js +10 -7
- package/dist/Column.js +1 -1
- package/dist/Recents.d.ts +3 -1
- package/dist/Recents.js +31 -33
- package/dist/Shell.js +18 -11
- package/dist/dev.js +9 -9
- package/dist/host.d.ts +9 -5
- package/dist/host.js +5 -4
- package/dist/lib/host.d.ts +20 -15
- package/dist/lib/host.js +41 -26
- package/dist/lib/session.js +7 -6
- package/dist/recent.d.ts +12 -5
- package/dist/recent.js +24 -11
- package/dist/thread.d.ts +35 -0
- package/dist/thread.js +147 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -11,13 +11,13 @@ import { CalScheduler } from '@hanzo/rooms/CalScheduler'
|
|
|
11
11
|
|
|
12
12
|
configure({ site: '', home: '/home', signUp: '/signup' })
|
|
13
13
|
|
|
14
|
-
<Rooms router={router} search={search} route={route} Link={Link}>
|
|
14
|
+
<Rooms router={router} search={search} path={pathname} route={route} Link={Link}>
|
|
15
15
|
<Room mode="cal"><CalScheduler /></Room>
|
|
16
16
|
</Rooms>
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
`configure()` names the host's addresses (where its own pages live, the
|
|
20
20
|
gateway and IAM, the plan catalogue to paint first). `<Rooms>` carries what
|
|
21
|
-
moves: the router, the query, the route and the link component. The rooms
|
|
21
|
+
moves: the router, the query, the path, the route and the link component. The rooms
|
|
22
22
|
import nothing from a host, and their look comes from `@hanzo/appearance` and
|
|
23
23
|
`@hanzo/design` through `@hanzo/ui`.
|
package/dist/Chat.js
CHANGED
|
@@ -52,7 +52,7 @@ import { useHydrated } from "./lib/hydrated.js";
|
|
|
52
52
|
import { base, served } from "./lib/ai.js";
|
|
53
53
|
import { onServed } from "./lib/served.js";
|
|
54
54
|
import { refused as worded, speech, useDictation, useTalk, useVoice, Voice } from "@hanzo/voice";
|
|
55
|
-
import { ArrowUp, AudioLines, FileText, Image as ImageMark, Mic, Paperclip,
|
|
55
|
+
import { ArrowUp, AudioLines, FileText, Image as ImageMark, Mic, Paperclip, Pin, Square, PanelRight, X } from "lucide-react";
|
|
56
56
|
import { ENSO, FREE } from "./lib/ai.js";
|
|
57
57
|
import { openThread, useOpen } from "./open.js";
|
|
58
58
|
import { Beside, Framed } from "./Shell.js";
|
|
@@ -61,7 +61,7 @@ import { RightPane, addressOf, look, usePinned } from "./RightPane.js";
|
|
|
61
61
|
import { useMedia } from "@hanzo/gui";
|
|
62
62
|
import { Take } from "./copy.js";
|
|
63
63
|
import { Share } from "./Share.js";
|
|
64
|
-
import {
|
|
64
|
+
import { useThreadActions, useThreadList } from "./thread.js";
|
|
65
65
|
import { useModel } from "./model.js";
|
|
66
66
|
import { Enso, nameOf, useEffort } from "./enso.js";
|
|
67
67
|
import { checkoutUrl } from './lib/pay.js';
|
|
@@ -486,10 +486,6 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
486
486
|
* that settles after mount. Picking from the menu still wins, permanently.
|
|
487
487
|
*/
|
|
488
488
|
const [asking, setAsking] = useState(null);
|
|
489
|
-
// STARRED HERE, READ IN THE COLUMN — the shape a room and a project already
|
|
490
|
-
// have. hanzo.chat called it a bookmark; the word this site uses is starred,
|
|
491
|
-
// and one word for one act is what keeps the three lists reading alike.
|
|
492
|
-
const [starred, star] = useStarred("threads");
|
|
493
489
|
// The setter is taken now, not just the value: the override moved from
|
|
494
490
|
// Settings into the composer's own panel, so this is where it is written.
|
|
495
491
|
const [preferred, setPreferred] = useModel();
|
|
@@ -499,6 +495,13 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
499
495
|
const framed = useContext(Framed);
|
|
500
496
|
const opened = useOpen();
|
|
501
497
|
const { thread: open, open: reopen, agents: room, openAgent, seat, emptied, aside, showAside, showSettings } = opened;
|
|
498
|
+
// PINNED HERE, READ IN THE COLUMN, and kept by the server (thread.tsx): the
|
|
499
|
+
// pin this header sets is the one the row's menu sets, on every device the
|
|
500
|
+
// reader signs in on. A conversation not in the list yet (just begun, or
|
|
501
|
+
// archived) reads unpinned.
|
|
502
|
+
const listing = useThreadList(false, open);
|
|
503
|
+
const acts = useThreadActions();
|
|
504
|
+
const mine = open ? listing.threads.find((t) => t.id === open) : undefined;
|
|
502
505
|
// The conversation's key in `pane.ts`, the one the column beside it reads.
|
|
503
506
|
const at = channel(opened);
|
|
504
507
|
// The first of them: what a surface that addresses one person reads.
|
|
@@ -1200,7 +1203,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1200
1203
|
e.preventDefault();
|
|
1201
1204
|
// A dropped folder arrives as entries; each file under it goes up with its path.
|
|
1202
1205
|
void dropped(e.dataTransfer).then((list) => hold(at, list, via));
|
|
1203
|
-
}, children: [dragging ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: 10, items: "center", justify: "center", bg: "var(--surface-scrim)", borderWidth: 2, borderStyle: "dashed", borderColor: "$borderColor", rounded: "var(--pane-round)", pointerEvents: "none", children: _jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$2.5", rounded: "$10", bg: "$background", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Paperclip, { size: 15, "aria-hidden": true }), _jsx(Text, { fontSize: "$4", color: "$ink", children: "Drop files to attach" })] }) })) : null, _jsxs(XStack, { height: 48, items: "center", justify: "space-between", px: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, children: [_jsx(Box, { render: "button", onClick: () =>
|
|
1206
|
+
}, children: [dragging ? (_jsx(YStack, { "aria-hidden": true, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: 10, items: "center", justify: "center", bg: "var(--surface-scrim)", borderWidth: 2, borderStyle: "dashed", borderColor: "$borderColor", rounded: "var(--pane-round)", pointerEvents: "none", children: _jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$2.5", rounded: "$10", bg: "$background", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Paperclip, { size: 15, "aria-hidden": true }), _jsx(Text, { fontSize: "$4", color: "$ink", children: "Drop files to attach" })] }) })) : null, _jsxs(XStack, { height: 48, items: "center", justify: "space-between", px: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, children: [_jsx(Box, { render: "button", onClick: () => mine && void acts.pin(mine), "aria-disabled": !mine, "aria-label": mine?.pinned ? "Unpin this conversation" : "Pin this conversation", "aria-pressed": !!mine?.pinned, p: "$1.5", rounded: "$2", opacity: mine ? 1 : 0.5, hoverStyle: { bg: '$hover' }, children: _jsx(Pin, { size: 15, "aria-hidden": true, color: mine?.pinned ? "var(--foreground)" : "var(--muted-foreground)", fill: mine?.pinned ? "currentColor" : "none" }) }), room.length ? (_jsx(XStack, { role: "list", "aria-label": "Agents in this chat", items: "center", gap: "$1.5", minW: 0, overflowX: "auto", "$platform-web": { scrollbarWidth: "none" }, children: room.map((name) => (_jsxs(XStack, { role: "listitem", items: "center", gap: "$1.5", pl: "$1", pr: "$0.5", py: "$0.5", rounded: 999, borderWidth: 1, borderColor: "$borderColor", shrink: 0, children: [_jsx(Face, { src: faces[name]?.avatar ?? (name === withWhom ? portrait : undefined), emoji: faces[name]?.emoji ?? (name === withWhom ? persona?.emoji : undefined), name: name, size: 22 }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$color", numberOfLines: 1, display: "none", $sm: { display: "flex" }, children: name }), _jsx(Box, { render: "button", onClick: () => openAgent(name), "aria-label": `Remove ${name} from this chat`, title: `Remove ${name}`, width: 24, height: 24, rounded: 999, items: "center", justify: "center", hoverStyle: { bg: "$hover" }, children: _jsx(X, { size: 13, "aria-hidden": true, color: "var(--soft, var(--muted-foreground))" }) })] }, name))) })) : null] }), _jsxs(XStack, { items: "center", gap: "$1", children: [_jsx(Box, { render: "button", onClick: () => {
|
|
1204
1207
|
if (aside === null) {
|
|
1205
1208
|
showAside("tasks");
|
|
1206
1209
|
if (!pinned)
|
package/dist/Column.js
CHANGED
|
@@ -82,7 +82,7 @@ export function Column({ host, mode, thread, onChat, onDev, go, drawer = false,
|
|
|
82
82
|
setWords('');
|
|
83
83
|
}, placeholder: "Filter", "aria-label": "Filter recents" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }), _jsxs(YStack, { role: "list", mt: "$2.5", gap: 2, shrink: 0, children: [_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { "data-slot": "app-new", ...ROW, active: fresh, icon: _jsx(Plus, { size: 16, "aria-hidden": true }), onPress: create, ...keys(create), children: mode === 'chat' ? 'New chat' : 'New run' }) }), mode === 'dev'
|
|
84
84
|
? links.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id)))
|
|
85
|
-
: null, mode === 'dev' ? (_jsxs(YStack, { role: "listitem", children: [_jsx(SidebarItem, { ...ROW, "aria-expanded": unfolded, icon: unfolded ? _jsx(ChevronUp, { size: 16, "aria-hidden": true }) : _jsx(ChevronDown, { size: 16, "aria-hidden": true }), onPress: () => setUnfolded((v) => !v), ...keys(() => setUnfolded((v) => !v)), children: "More" }), unfolded ? (_jsx(YStack, { role: "list", gap: 2, children: more.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id))) })) : null] })) : null] }), _jsx(XStack, { height: 1, bg: "$borderColor", mx: "$1.5", mt: "$3", shrink: 0 }), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] }), _jsxs(XStack, { "data-slot": "app-foot", items: "center", gap: "$1", pt: "$2", shrink: 0, children: [_jsx(YStack, { flex: 1, minW: 0, children: _jsx(Me, { host: host, onOrg: org }) }), drawer ? null : (_jsx(SidebarIconButton, { label: "Collapse sidebar", onPress: () => setCollapsed(true), width: 32, height: 32, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }))] })] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: () => setCollapsed(true), label: "Resize sidebar",
|
|
85
|
+
: null, mode === 'dev' ? (_jsxs(YStack, { role: "listitem", children: [_jsx(SidebarItem, { ...ROW, "aria-expanded": unfolded, icon: unfolded ? _jsx(ChevronUp, { size: 16, "aria-hidden": true }) : _jsx(ChevronDown, { size: 16, "aria-hidden": true }), onPress: () => setUnfolded((v) => !v), ...keys(() => setUnfolded((v) => !v)), children: "More" }), unfolded ? (_jsx(YStack, { role: "list", gap: 2, children: more.map((link) => (_jsx(YStack, { role: "listitem", children: _jsx(SidebarItem, { ...ROW, active: link.active, icon: link.icon, onPress: link.onPress, ...keys(() => link.onPress?.()), children: link.label }) }, link.id))) })) : null] })) : null] }), _jsx(XStack, { height: 1, bg: "$borderColor", mx: "$1.5", mt: "$3", shrink: 0 }), _jsx(Recents, { host: host, mode: mode, thread: thread, words: words, onOpen: open, onNew: create }), _jsxs(YStack, { gap: "$2", pt: "$2", shrink: 0, children: [_jsx(Made, {}), _jsx(Setup, {})] }), _jsxs(XStack, { "data-slot": "app-foot", items: "center", gap: "$1", pt: "$2", shrink: 0, children: [_jsx(YStack, { flex: 1, minW: 0, children: _jsx(Me, { host: host, onOrg: org }) }), drawer ? null : (_jsx(SidebarIconButton, { label: "Collapse sidebar", onPress: () => setCollapsed(true), width: 32, height: 32, rounded: 999, children: _jsx(PanelLeft, { size: 16 }) }))] })] }), drawer ? null : (_jsx(Grip, { side: "left", span: span.span, floor: FLOOR, ceil: CEIL, reset: WIDTH, onSpan: span.move, onKeep: span.keep, onShut: () => setCollapsed(true), label: "Resize sidebar",
|
|
86
86
|
// In the gutter between the column and the pane, not over the column's own rows.
|
|
87
87
|
r: GUTTER_END, width: "var(--pane-gap)" }))] }));
|
|
88
88
|
}
|
package/dist/Recents.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type Host } from '@hanzo/build';
|
|
2
2
|
import { type Mode, type Recent } from './recent.js';
|
|
3
|
-
export declare function Recents({ host, mode, thread, words, onOpen, }: {
|
|
3
|
+
export declare function Recents({ host, mode, thread, words, onOpen, onNew, }: {
|
|
4
4
|
host: Host;
|
|
5
5
|
mode: Mode;
|
|
6
6
|
/** The Chat conversation the pane holds, or null. */
|
|
7
7
|
thread: string | null;
|
|
8
8
|
words: string;
|
|
9
9
|
onOpen: (r: Recent) => void;
|
|
10
|
+
/** Moves the pane to a new conversation: where it goes when the one it holds is deleted. */
|
|
11
|
+
onNew: () => void;
|
|
10
12
|
}): import("react").JSX.Element;
|
package/dist/Recents.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
// The reader's Recents in the column's mode, and only that mode's: Chat lists its
|
|
4
|
-
// conversations (`/v1/agents/chat/conversations`,
|
|
4
|
+
// conversations (`/v1/agents/chat/conversations`, `useThreadList`), Dev its
|
|
5
5
|
// coding runs (`/v1/agent/sessions?kind=coding`, @hanzo/build `useSessions`). Each
|
|
6
6
|
// mode reads its own store. The menu beside the heading narrows, groups and sorts
|
|
7
|
-
// the list; the choice is kept per mode in this browser.
|
|
7
|
+
// the list; the choice is kept per mode in this browser. A conversation's row
|
|
8
|
+
// carries its own menu (thread.tsx): pinned ones list first, under Pinned, and
|
|
9
|
+
// Chat's Status shows the archived ones apart.
|
|
8
10
|
import { useEffect, useId, useMemo, useRef } from 'react';
|
|
9
11
|
import { ScrollView, SizableText, XStack, YStack } from '@hanzo/gui';
|
|
10
12
|
import { ListFilter, MessageSquare } from 'lucide-react';
|
|
11
|
-
import { useThreads } from '@hanzo/ai/react';
|
|
12
13
|
import { DOTS, route, useSessions } from '@hanzo/build';
|
|
13
14
|
import { DropdownMenu } from '@hanzo/ui';
|
|
14
15
|
import { SidebarItem, StatusDot } from '@hanzo/ui/chat';
|
|
15
16
|
import { useKept } from './kept.js';
|
|
16
17
|
import { ago, arrange, chats, MENUS, PLAIN, runs, SINCE, viewOf } from './recent.js';
|
|
17
|
-
import {
|
|
18
|
+
import { ThreadRow, useThreadList } from './thread.js';
|
|
18
19
|
/** The menu's items: each section a heading and its options, the one in force checked. */
|
|
19
20
|
function items(mode, view, set) {
|
|
20
21
|
const m = MENUS[mode];
|
|
@@ -34,26 +35,20 @@ function items(mode, view, set) {
|
|
|
34
35
|
return parts.flatMap((p, i) => (i ? [{ type: 'separator', key: `sep-${i}` }, ...p] : p));
|
|
35
36
|
}
|
|
36
37
|
const Note = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", py: "$1.5", children: children }));
|
|
37
|
-
export function Recents({ host, mode, thread, words, onOpen, }) {
|
|
38
|
-
|
|
38
|
+
export function Recents({ host, mode, thread, words, onOpen, onNew, }) {
|
|
39
|
+
const [kept, keep] = useKept(`hanzo.recents.${mode}`, PLAIN);
|
|
40
|
+
const view = viewOf(mode, kept);
|
|
41
|
+
return mode === 'chat' ? (_jsx(Chats, { thread: thread, words: words, view: view, keep: keep, onOpen: onOpen, onNew: onNew })) : (_jsx(Runs, { host: host, words: words, view: view, keep: keep, onOpen: onOpen }));
|
|
39
42
|
}
|
|
40
|
-
/** Chat's Recents: the conversation store alone. */
|
|
41
|
-
function Chats({ thread, words, onOpen }) {
|
|
42
|
-
const store =
|
|
43
|
-
// A conversation this list has not seen is one just begun: read the store again, once for it.
|
|
44
|
-
const asked = useRef(new Set());
|
|
45
|
-
const reload = store.reload;
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (!thread || store.loading || asked.current.has(thread) || store.threads.some((t) => t.id === thread))
|
|
48
|
-
return;
|
|
49
|
-
asked.current.add(thread);
|
|
50
|
-
reload();
|
|
51
|
-
}, [thread, store.loading, store.threads, reload]);
|
|
43
|
+
/** Chat's Recents: the conversation store alone, the archived ones when the view asks for them. */
|
|
44
|
+
function Chats({ thread, words, view, keep, onOpen, onNew, }) {
|
|
45
|
+
const store = useThreadList(view.status === 'archived', thread);
|
|
52
46
|
const rows = useMemo(() => chats(store.threads), [store.threads]);
|
|
53
|
-
|
|
47
|
+
const byId = useMemo(() => new Map(store.threads.map((t) => [t.id, t])), [store.threads]);
|
|
48
|
+
return (_jsx(List, { mode: "chat", rows: rows, words: words, view: view, keep: keep, open: (r) => r.id === thread, reading: store.loading && !store.threads.length, error: store.error, onOpen: onOpen, threads: byId, onNew: onNew }));
|
|
54
49
|
}
|
|
55
50
|
/** Dev's Recents: the org's coding runs alone. */
|
|
56
|
-
function Runs({ host, words, onOpen }) {
|
|
51
|
+
function Runs({ host, words, view, keep, onOpen }) {
|
|
57
52
|
const store = useSessions(host);
|
|
58
53
|
const r = route(host.path);
|
|
59
54
|
const run = r.kind === 'run' ? r.id : null;
|
|
@@ -67,25 +62,28 @@ function Runs({ host, words, onOpen }) {
|
|
|
67
62
|
reload();
|
|
68
63
|
}, [run, store.loading, store.value, reload]);
|
|
69
64
|
const rows = useMemo(() => runs(store.value), [store.value]);
|
|
70
|
-
return (_jsx(List, { mode: "dev", rows: rows, words: words, open: (row) => row.id === run, reading: store.loading && !store.value.length, error: store.error, onOpen: onOpen }));
|
|
65
|
+
return (_jsx(List, { mode: "dev", rows: rows, words: words, view: view, keep: keep, open: (row) => row.id === run, reading: store.loading && !store.value.length, error: store.error, onOpen: onOpen }));
|
|
71
66
|
}
|
|
72
|
-
function List({ mode, rows, words, open, reading, error, onOpen, }) {
|
|
73
|
-
const [kept, keep] = useKept(`hanzo.recents.${mode}`, PLAIN);
|
|
74
|
-
const view = viewOf(mode, kept);
|
|
75
|
-
const [starred] = useStarred('threads');
|
|
67
|
+
function List({ mode, rows, words, view, keep, open, reading, error, onOpen, threads, onNew, }) {
|
|
76
68
|
const label = useId();
|
|
77
|
-
const groups = arrange(rows, view, words
|
|
69
|
+
const groups = arrange(rows, view, words);
|
|
78
70
|
const narrowed = view.status !== PLAIN.status || view.since !== PLAIN.since;
|
|
79
71
|
const now = new Date();
|
|
80
72
|
const running = mode === 'dev' ? rows.filter((r) => r.status === 'running').length : 0;
|
|
81
73
|
const row = (r) => {
|
|
82
74
|
const when = ago(r.at, now);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
75
|
+
const item = (_jsx(SidebarItem, { "data-kind": r.kind, active: open(r), rounded: "$4", minH: 32, pr: when ? '$8' : '$2', onKeyDown: (e) => {
|
|
76
|
+
if (e.key !== 'Enter' && e.key !== ' ')
|
|
77
|
+
return;
|
|
78
|
+
e.preventDefault?.();
|
|
79
|
+
onOpen(r);
|
|
80
|
+
}, icon: r.kind === 'chat' ? (_jsx(MessageSquare, { size: 14, "aria-hidden": true })) : (_jsx(XStack, { width: 14, justify: "center", children: _jsx(StatusDot, { status: DOTS[r.status] ?? 'idle' }) })), onPress: () => onOpen(r), children: r.title }));
|
|
81
|
+
// How long ago, at the row's end; the full time is its title.
|
|
82
|
+
const age = when ? (_jsx(SizableText, { size: "$1", color: "$soft", pointerEvents: "none", display: "flex", items: "center", fontVariant: ['tabular-nums'], children: _jsx("time", { dateTime: r.at, title: new Date(r.at).toLocaleString(), children: when }) })) : null;
|
|
83
|
+
const t = threads?.get(r.id);
|
|
84
|
+
if (t)
|
|
85
|
+
return (_jsx(ThreadRow, { t: t, active: open(r), aside: age, onGone: onNew, children: item }, r.id));
|
|
86
|
+
return (_jsxs(YStack, { role: "listitem", position: "relative", children: [item, age ? (_jsx(XStack, { position: "absolute", r: "$2", t: 0, b: 0, items: "center", pointerEvents: "none", children: age })) : null] }, r.id));
|
|
89
87
|
};
|
|
90
|
-
return (_jsxs(YStack, { "data-slot": "recents", flex: 1, minH: 0, mt: "$2", children: [_jsxs(XStack, { items: "center", justify: "space-between", height: 30, pl: "$2", pr: "$1", shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { id: label, size: "$1", color: "$soft", fontWeight: "600", textTransform: "uppercase", letterSpacing: 0.6, children: "Recents" }), running ? (_jsx(XStack, { "aria-label": `${running} running`, minW: 18, height: 18, px: "$1.5", rounded: 999, items: "center", justify: "center", bg: "$edge", borderWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$1", fontWeight: "600", color: "$ink", "aria-hidden": true, children: String(running) }) })) : null] }), _jsx(DropdownMenu, { minWidth: 200, trigger: _jsx(XStack, { render: "button", "aria-label": "Recents view", width: 26, height: 26, rounded: "$2", items: "center", justify: "center", cursor: "pointer", hoverStyle: { bg: '$hover' }, bg: narrowed ? '$hover' : 'transparent', children: _jsx(ListFilter, { size: 14, "aria-hidden": true }) }), items: items(mode, view, keep) })] }), _jsxs(ScrollView, { flex: 1, showsVerticalScrollIndicator: false, children: [error ? _jsx(Note, { children: error.message }) : null, groups.length ? (_jsx(YStack, { role: "list", "aria-labelledby": label, gap: 1, children: groups.map((g) => g.title ? (_jsxs(YStack, { role: "listitem", gap: 1, pt: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: g.title }), _jsx(YStack, { role: "list", "aria-label": g.title, gap: 1, children: g.rows.map(row) })] }, g.title)) : (g.rows.map(row))) })) : reading ? (_jsx(Note, { children: mode === 'chat' ? 'Reading your chats…' : 'Reading your runs…' })) : error ? null : words.trim() ? (_jsx(Note, { children: `Nothing matches “${words.trim()}”.` })) : narrowed ? (_jsx(Note, { children: "Nothing in this view." })) : (_jsx(Note, { children: mode === 'chat' ? 'No chats yet. Say something and it lands here.' : 'No runs yet.' }))] })] }));
|
|
88
|
+
return (_jsxs(YStack, { "data-slot": "recents", flex: 1, minH: 0, mt: "$2", children: [_jsxs(XStack, { items: "center", justify: "space-between", height: 30, pl: "$2", pr: "$1", shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { id: label, size: "$1", color: "$soft", fontWeight: "600", textTransform: "uppercase", letterSpacing: 0.6, children: "Recents" }), running ? (_jsx(XStack, { "aria-label": `${running} running`, minW: 18, height: 18, px: "$1.5", rounded: 999, items: "center", justify: "center", bg: "$edge", borderWidth: 1, borderColor: "$borderColor", children: _jsx(SizableText, { size: "$1", fontWeight: "600", color: "$ink", "aria-hidden": true, children: String(running) }) })) : null] }), _jsx(DropdownMenu, { minWidth: 200, trigger: _jsx(XStack, { render: "button", "aria-label": "Recents view", width: 26, height: 26, rounded: "$2", items: "center", justify: "center", cursor: "pointer", hoverStyle: { bg: '$hover' }, bg: narrowed ? '$hover' : 'transparent', children: _jsx(ListFilter, { size: 14, "aria-hidden": true }) }), items: items(mode, view, keep) })] }), _jsxs(ScrollView, { flex: 1, showsVerticalScrollIndicator: false, children: [error ? _jsx(Note, { children: error.message }) : null, groups.length ? (_jsx(YStack, { role: "list", "aria-labelledby": label, gap: 1, children: groups.map((g) => g.title ? (_jsxs(YStack, { role: "listitem", gap: 1, pt: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", pb: "$1", children: g.title }), _jsx(YStack, { role: "list", "aria-label": g.title, gap: 1, children: g.rows.map(row) })] }, g.title)) : (g.rows.map(row))) })) : reading ? (_jsx(Note, { children: mode === 'chat' ? 'Reading your chats…' : 'Reading your runs…' })) : error ? null : words.trim() ? (_jsx(Note, { children: `Nothing matches “${words.trim()}”.` })) : view.status === 'archived' ? (_jsx(Note, { children: "No archived chats." })) : narrowed ? (_jsx(Note, { children: "Nothing in this view." })) : (_jsx(Note, { children: mode === 'chat' ? 'No chats yet. Say something and it lands here.' : 'No runs yet.' }))] })] }));
|
|
91
89
|
}
|
package/dist/Shell.js
CHANGED
|
@@ -16,7 +16,9 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
16
16
|
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
17
17
|
import { createPortal } from "react-dom";
|
|
18
18
|
import { MessageSquare, Code2, Inbox, Bot, ListChecks, HardDrive, Settings2, UserPlus, Building2, Search, Hash, AtSign, Lock, Workflow, Plus, ArrowLeft, ArrowRight, Send, Minus, Blocks, Star, FolderOpen, Activity, BookOpen, SquareTerminal, Share2, Sparkles, PanelLeft, PanelRight, Users, Globe, Cpu, Boxes, Wallet, Video, Calendar, LayoutGrid, ArrowUpLeft, } from "lucide-react";
|
|
19
|
-
import { AiProvider, useAgents, useInbox, useModels, usePeople,
|
|
19
|
+
import { AiProvider, useAgents, useInbox, useModels, usePeople, } from "@hanzo/ai/react";
|
|
20
|
+
import { PINNED } from "./recent.js";
|
|
21
|
+
import { ThreadRow, useThreadList } from "./thread.js";
|
|
20
22
|
import { Box, XStack, YStack, Text } from "@hanzo/ui";
|
|
21
23
|
// THE PAPER LADDER. `sheet(n)` is a fill AND the light on it, published as a
|
|
22
24
|
// pair by @hanzo/design, so a surface asks for a HEIGHT instead of composing a
|
|
@@ -373,7 +375,7 @@ function Chats({ filter = "", onPick, }) {
|
|
|
373
375
|
// lease-agreement file search" — banded under Today and Yesterday like a
|
|
374
376
|
// history somebody had. A visitor's first sight of the app was ten chats they
|
|
375
377
|
// had never had, and every one of them opened nothing.
|
|
376
|
-
const { threads, loading, error: wrong } =
|
|
378
|
+
const { threads, loading, error: wrong } = useThreadList(false, thread);
|
|
377
379
|
const [customThreads, setCustomThreads] = useState([]);
|
|
378
380
|
useEffect(() => {
|
|
379
381
|
setCustomThreads(getCustomThreads());
|
|
@@ -384,12 +386,12 @@ function Chats({ filter = "", onPick, }) {
|
|
|
384
386
|
const { isAuthenticated, user } = useIam();
|
|
385
387
|
const { user: account } = useAccount();
|
|
386
388
|
const signedIn = isAuthenticated || Boolean(user) || Boolean(account) || hasSession();
|
|
387
|
-
const [starred] = useStarred("threads");
|
|
388
389
|
const mergedThreads = useMemo(() => {
|
|
389
390
|
const list = threads.map((t) => ({
|
|
390
391
|
id: t.id,
|
|
391
392
|
title: t.title || "Untitled",
|
|
392
393
|
updatedAt: t.updatedAt,
|
|
394
|
+
thread: t,
|
|
393
395
|
}));
|
|
394
396
|
for (const ct of customThreads) {
|
|
395
397
|
if (!list.some((t) => t.id === ct.id)) {
|
|
@@ -404,16 +406,17 @@ function Chats({ filter = "", onPick, }) {
|
|
|
404
406
|
return list;
|
|
405
407
|
}, [threads, customThreads]);
|
|
406
408
|
const found = mergedThreads.filter((t) => matches(t.title || "Untitled", filter));
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
+
// PINNED FIRST, by the server's pin (thread.tsx), then the days.
|
|
410
|
+
const shown = found.filter((t) => !t.thread?.pinned);
|
|
411
|
+
const kept = found.filter((t) => t.thread?.pinned);
|
|
409
412
|
const bands = [];
|
|
410
413
|
if (kept.length)
|
|
411
|
-
bands.push({ label:
|
|
414
|
+
bands.push({ label: PINNED, rows: kept });
|
|
412
415
|
const now = new Date();
|
|
413
416
|
for (const t of shown) {
|
|
414
417
|
const label = when(t.updatedAt, now);
|
|
415
418
|
const last = bands[bands.length - 1];
|
|
416
|
-
if (last && last.label === label && last.label !==
|
|
419
|
+
if (last && last.label === label && last.label !== PINNED)
|
|
417
420
|
last.rows.push(t);
|
|
418
421
|
else
|
|
419
422
|
bands.push({ label, rows: [t] });
|
|
@@ -421,10 +424,14 @@ function Chats({ filter = "", onPick, }) {
|
|
|
421
424
|
if (!signedIn)
|
|
422
425
|
return _jsx(Note, { children: "Sign in to see your recent chats." });
|
|
423
426
|
const mine = wrong ? (_jsx(Note, { children: say(wrong, "your chats") })) : loading && threads.length === 0 ? (_jsx(Note, { children: "Reading your chats." })) : found.length === 0 && filter ? (_jsxs(Note, { children: ["No chat matches \u201C", filter, "\u201D."] })) : threads.length === 0 ? (_jsx(Note, { children: "No chats yet. Say something and it lands here." })) : null;
|
|
424
|
-
return (_jsxs(_Fragment, { children: [mine, mine ? null : bands.map((band) => (_jsx(SidebarSection, { label: band.label, children: band.rows.map((t) =>
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
427
|
+
return (_jsxs(_Fragment, { children: [mine, mine ? null : bands.map((band) => (_jsx(SidebarSection, { label: band.label, children: band.rows.map((t) => {
|
|
428
|
+
const item = (_jsx(SidebarItem, { active: t.id === thread, onPress: () => {
|
|
429
|
+
open(t.id);
|
|
430
|
+
onPick?.();
|
|
431
|
+
}, children: _jsxs(YStack, { minW: 0, children: [_jsxs(XStack, { items: "center", gap: "$1.5", minW: 0, children: [t.kind === "call" ? _jsx(Video, { size: 13, color: "var(--muted-foreground)", "aria-label": "Call" }) : null, t.kind === "group" ? _jsx(Users, { size: 13, color: "var(--muted-foreground)", "aria-label": "Group" }) : null, _jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: t.title || "Untitled" })] }), _jsx(Span, { id: t.id, updatedAt: t.updatedAt })] }) }, t.id));
|
|
432
|
+
// A stored conversation carries its menu; a call or a group is no conversation of the store's.
|
|
433
|
+
return t.thread ? (_jsx(ThreadRow, { t: t.thread, active: t.id === thread, listed: false, children: item }, t.id)) : (item);
|
|
434
|
+
}) }, band.label))), _jsx(SharedWithYou, { filter: filter, onPick: onPick })] }));
|
|
428
435
|
}
|
|
429
436
|
/**
|
|
430
437
|
* The chats other people shared with this reader, and who shared each: every
|
package/dist/dev.js
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
// to leave. The name drawn at the top left is Hanzo Dev: this host is the
|
|
8
8
|
// dev agent. Chat, on the rooms shell, is Hanzo AI.
|
|
9
9
|
//
|
|
10
|
-
// THE ADDRESS IS THE
|
|
11
|
-
//
|
|
10
|
+
// THE ADDRESS IS THE PATH. Dev is `/dev`, and what it shows is the path under
|
|
11
|
+
// it in the builder's own grammar (lib/host.ts `app`, `under`), so a run, a
|
|
12
12
|
// screen, a Settings section, a Customize tab, the plans and a project all
|
|
13
13
|
// survive a reload, and Back steps through them. A move is a native history
|
|
14
|
-
// entry, which the Next router folds into `
|
|
15
|
-
//
|
|
14
|
+
// entry, which the Next router folds into `usePathname`, so reading the address
|
|
15
|
+
// and moving it are one path.
|
|
16
16
|
import { useMemo } from 'react';
|
|
17
17
|
import { useLook, useRooms } from './host.js';
|
|
18
18
|
import { useIam } from '@hanzo/iam/react';
|
|
@@ -21,7 +21,7 @@ import { administers, path, route } from '@hanzo/build';
|
|
|
21
21
|
import { bearer, named, org, orgs, pick } from './lib/session.js';
|
|
22
22
|
import { enter } from './lib/destination.js';
|
|
23
23
|
import { api } from './lib/api.js';
|
|
24
|
-
import { app,
|
|
24
|
+
import { app, under } from './lib/host.js';
|
|
25
25
|
import { link } from './lib/tags.js';
|
|
26
26
|
/** Where the builder links out: every one of them an address of this site. */
|
|
27
27
|
const LINKS = {
|
|
@@ -32,14 +32,14 @@ const LINKS = {
|
|
|
32
32
|
};
|
|
33
33
|
/** The person's theme, when they have chosen one the builder can name. */
|
|
34
34
|
const theme = (t) => (t === 'light' || t === 'dark' || t === 'system' ? t : undefined);
|
|
35
|
-
/** The app address a
|
|
35
|
+
/** The app address a builder path names, canonical; '' for anything that is not one. */
|
|
36
36
|
const canonical = (at) => path(route(at ?? ''));
|
|
37
37
|
/** The host for a signed-in reader. */
|
|
38
38
|
export function useDevHost() {
|
|
39
39
|
const { user, accessToken } = useIam();
|
|
40
40
|
const logout = useSignOut();
|
|
41
|
-
const { router,
|
|
42
|
-
const here = canonical(
|
|
41
|
+
const { router, path: pathname } = useRooms();
|
|
42
|
+
const here = canonical(under(pathname));
|
|
43
43
|
const look = useLook();
|
|
44
44
|
// The token's claims change only when the token does: a sign-in, a refresh.
|
|
45
45
|
const memberships = useMemo(() => orgs(), [accessToken]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
@@ -70,7 +70,7 @@ export function useDevHost() {
|
|
|
70
70
|
path: here,
|
|
71
71
|
go: (to, how) => {
|
|
72
72
|
const next = app(canonical(to));
|
|
73
|
-
if (
|
|
73
|
+
if (window.location.pathname === next && !window.location.search)
|
|
74
74
|
return;
|
|
75
75
|
if (how?.replace)
|
|
76
76
|
window.history.replaceState(null, '', next);
|
package/dist/host.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* configure({ site: 'https://hanzo.ai', home: '/', signUp: '/start' })
|
|
9
9
|
*
|
|
10
|
-
* <Rooms router={useRouter()} search={useSearchParams()} route={route} Link={Link}>
|
|
10
|
+
* <Rooms router={useRouter()} search={useSearchParams()} path={usePathname()} route={route} Link={Link}>
|
|
11
11
|
* <Room mode="cal" />
|
|
12
12
|
* </Rooms>
|
|
13
13
|
*
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* any component and on pages the rooms never draw. So the host states them once
|
|
18
18
|
* with `configure()`, in a module its root imports, before anything renders.
|
|
19
19
|
*
|
|
20
|
-
* The ROUTER, the query and the route change as the page moves, so they ride
|
|
20
|
+
* The ROUTER, the query, the path and the route change as the page moves, so they ride
|
|
21
21
|
* React context, and a room reads them with `useRooms()`.
|
|
22
22
|
*/
|
|
23
23
|
import { type AnchorHTMLAttributes, type ComponentType, type ReactNode } from 'react';
|
|
@@ -34,14 +34,18 @@ export interface Router {
|
|
|
34
34
|
}
|
|
35
35
|
interface Moving {
|
|
36
36
|
router: Router;
|
|
37
|
-
/** The address's query, for the rooms that read one (
|
|
37
|
+
/** The address's query, for the rooms that read one (Chat's `?chat=`). */
|
|
38
38
|
search: URLSearchParams | null;
|
|
39
|
+
/** The address's path, for the rooms that read one (Dev's, under `/dev`). */
|
|
40
|
+
path: string | null;
|
|
39
41
|
route: string;
|
|
40
42
|
Link: Link;
|
|
41
43
|
}
|
|
42
|
-
export interface RoomsProps extends Omit<Moving, 'search'> {
|
|
44
|
+
export interface RoomsProps extends Omit<Moving, 'search' | 'path'> {
|
|
43
45
|
/** Absent where no mounted room reads the query, so a static export needs no Suspense for it. */
|
|
44
46
|
search?: URLSearchParams | null;
|
|
47
|
+
/** Absent where no mounted room reads the path. */
|
|
48
|
+
path?: string | null;
|
|
45
49
|
children?: ReactNode;
|
|
46
50
|
}
|
|
47
51
|
/** The router, the query, the route and the link the host mounted the rooms with. */
|
|
@@ -72,5 +76,5 @@ export declare function useLook(): {
|
|
|
72
76
|
* keeps their choice in step with IAM.
|
|
73
77
|
*/
|
|
74
78
|
export declare function Look(): any;
|
|
75
|
-
export declare function Rooms({ router, search, route, Link, children }: RoomsProps): import("react").JSX.Element;
|
|
79
|
+
export declare function Rooms({ router, search, path, route, Link, children }: RoomsProps): import("react").JSX.Element;
|
|
76
80
|
export {};
|
package/dist/host.js
CHANGED
|
@@ -9,7 +9,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
9
9
|
*
|
|
10
10
|
* configure({ site: 'https://hanzo.ai', home: '/', signUp: '/start' })
|
|
11
11
|
*
|
|
12
|
-
* <Rooms router={useRouter()} search={useSearchParams()} route={route} Link={Link}>
|
|
12
|
+
* <Rooms router={useRouter()} search={useSearchParams()} path={usePathname()} route={route} Link={Link}>
|
|
13
13
|
* <Room mode="cal" />
|
|
14
14
|
* </Rooms>
|
|
15
15
|
*
|
|
@@ -19,7 +19,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
19
19
|
* any component and on pages the rooms never draw. So the host states them once
|
|
20
20
|
* with `configure()`, in a module its root imports, before anything renders.
|
|
21
21
|
*
|
|
22
|
-
* The ROUTER, the query and the route change as the page moves, so they ride
|
|
22
|
+
* The ROUTER, the query, the path and the route change as the page moves, so they ride
|
|
23
23
|
* React context, and a room reads them with `useRooms()`.
|
|
24
24
|
*/
|
|
25
25
|
import { createContext, useContext } from 'react';
|
|
@@ -35,6 +35,7 @@ const Context = createContext({
|
|
|
35
35
|
forward: () => window.history.forward(),
|
|
36
36
|
},
|
|
37
37
|
search: null,
|
|
38
|
+
path: null,
|
|
38
39
|
route: '/',
|
|
39
40
|
Link: Plain,
|
|
40
41
|
});
|
|
@@ -71,6 +72,6 @@ export function Look() {
|
|
|
71
72
|
useLook();
|
|
72
73
|
return null;
|
|
73
74
|
}
|
|
74
|
-
export function Rooms({ router, search = null, route, Link, children }) {
|
|
75
|
-
return _jsx(Context.Provider, { value: { router, search, route, Link }, children: children });
|
|
75
|
+
export function Rooms({ router, search = null, path = null, route, Link, children }) {
|
|
76
|
+
return _jsx(Context.Provider, { value: { router, search, path, route, Link }, children: children });
|
|
76
77
|
}
|
package/dist/lib/host.d.ts
CHANGED
|
@@ -30,30 +30,35 @@ export declare const ENTRY = "https://hanzo.team/?signin";
|
|
|
30
30
|
*/
|
|
31
31
|
export declare const START = "https://hanzo.team/start";
|
|
32
32
|
/**
|
|
33
|
-
* The web app,
|
|
34
|
-
* Dev is @hanzo/build's `<Builder
|
|
35
|
-
* in the builder's own grammar (`route` in @hanzo/build):
|
|
36
|
-
* run,
|
|
37
|
-
*
|
|
38
|
-
*
|
|
33
|
+
* The web app, for a signed-in reader, in two modes. Chat is the default, at
|
|
34
|
+
* `/`. Dev is @hanzo/build's `<Builder>` at `/dev`, and what it shows is the
|
|
35
|
+
* PATH under it, in the builder's own grammar (`route` in @hanzo/build): `/dev`
|
|
36
|
+
* New, `/dev/sess_<id>` a run, `/dev/<org>/<repo>` a project,
|
|
37
|
+
* `/dev/-/<screen>`, `/dev/-/settings[/<section>]`, `/dev/-/customize[/<tab>]`,
|
|
38
|
+
* `/dev/<slug>` a deployed site. The export holds one page for all of them,
|
|
39
|
+
* `/dev`, and the edge serves it for every path under it (hanzo.ai's
|
|
40
|
+
* lib/edge.ts `APP`).
|
|
39
41
|
*/
|
|
40
|
-
export declare const
|
|
41
|
-
/**
|
|
42
|
+
export declare const DEV = "/dev";
|
|
43
|
+
/** Dev at an app address; '' is Dev's New, `/dev` itself. */
|
|
42
44
|
export declare const app: (path: string) => string;
|
|
45
|
+
/** The app address a pathname names under `/dev`, or null for a pathname that is not Dev's. */
|
|
46
|
+
export declare const under: (pathname: string | null | undefined) => string | null;
|
|
43
47
|
/**
|
|
44
48
|
* Chat, the app's default mode: `/` is a new conversation and `?chat=<id>` one
|
|
45
|
-
* of the reader's. So the address says which mode the app is in —
|
|
46
|
-
* anything else Chat — and a reload keeps it.
|
|
49
|
+
* of the reader's. So the address says which mode the app is in — a path under
|
|
50
|
+
* `/dev` for Dev, anything else Chat — and a reload keeps it.
|
|
47
51
|
*/
|
|
48
52
|
export declare const TALK = "chat";
|
|
49
53
|
/** `/` in Chat, on a conversation, or `/` itself for a new one. */
|
|
50
54
|
export declare const talk: (id?: string | null) => string;
|
|
51
55
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
56
|
+
* Dev opened on one run, project or site, from anywhere: every "Try Hanzo Dev"
|
|
57
|
+
* link, a room's Dev, the console's "open in the builder". It signs a stranger
|
|
58
|
+
* in on the way (app/(app)/dev/_door.tsx), and is reached through `site()`:
|
|
59
|
+
* here on hanzo.ai, across to hanzo.ai from hanzo.team. Only a value that is
|
|
60
|
+
* exactly a session id, a repository or a site's slug is carried; anything else
|
|
61
|
+
* opens /dev itself, so no caller can make this name another address.
|
|
57
62
|
*/
|
|
58
63
|
export declare const dev: (ref?: string | null) => string;
|
|
59
64
|
/**
|
package/dist/lib/host.js
CHANGED
|
@@ -32,44 +32,59 @@ export const ENTRY = `${WORKSPACE}/?signin`;
|
|
|
32
32
|
*/
|
|
33
33
|
export const START = `${WORKSPACE}/start`;
|
|
34
34
|
/**
|
|
35
|
-
* The web app,
|
|
36
|
-
* Dev is @hanzo/build's `<Builder
|
|
37
|
-
* in the builder's own grammar (`route` in @hanzo/build):
|
|
38
|
-
* run,
|
|
39
|
-
*
|
|
40
|
-
*
|
|
35
|
+
* The web app, for a signed-in reader, in two modes. Chat is the default, at
|
|
36
|
+
* `/`. Dev is @hanzo/build's `<Builder>` at `/dev`, and what it shows is the
|
|
37
|
+
* PATH under it, in the builder's own grammar (`route` in @hanzo/build): `/dev`
|
|
38
|
+
* New, `/dev/sess_<id>` a run, `/dev/<org>/<repo>` a project,
|
|
39
|
+
* `/dev/-/<screen>`, `/dev/-/settings[/<section>]`, `/dev/-/customize[/<tab>]`,
|
|
40
|
+
* `/dev/<slug>` a deployed site. The export holds one page for all of them,
|
|
41
|
+
* `/dev`, and the edge serves it for every path under it (hanzo.ai's
|
|
42
|
+
* lib/edge.ts `APP`).
|
|
41
43
|
*/
|
|
42
|
-
export const
|
|
43
|
-
/**
|
|
44
|
-
export const app = (path) =>
|
|
44
|
+
export const DEV = '/dev';
|
|
45
|
+
/** Dev at an app address; '' is Dev's New, `/dev` itself. */
|
|
46
|
+
export const app = (path) => {
|
|
47
|
+
const p = path.replace(/^\/+|\/+$/g, '');
|
|
48
|
+
return p ? `${DEV}/${p}` : DEV;
|
|
49
|
+
};
|
|
50
|
+
/** The app address a pathname names under `/dev`, or null for a pathname that is not Dev's. */
|
|
51
|
+
export const under = (pathname) => {
|
|
52
|
+
if (!pathname)
|
|
53
|
+
return null;
|
|
54
|
+
if (pathname === DEV || pathname === `${DEV}/`)
|
|
55
|
+
return '';
|
|
56
|
+
if (!pathname.startsWith(`${DEV}/`))
|
|
57
|
+
return null;
|
|
58
|
+
try {
|
|
59
|
+
return decodeURIComponent(pathname.slice(DEV.length + 1));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
};
|
|
45
65
|
/**
|
|
46
66
|
* Chat, the app's default mode: `/` is a new conversation and `?chat=<id>` one
|
|
47
|
-
* of the reader's. So the address says which mode the app is in —
|
|
48
|
-
* anything else Chat — and a reload keeps it.
|
|
67
|
+
* of the reader's. So the address says which mode the app is in — a path under
|
|
68
|
+
* `/dev` for Dev, anything else Chat — and a reload keeps it.
|
|
49
69
|
*/
|
|
50
70
|
export const TALK = 'chat';
|
|
51
71
|
/** `/` in Chat, on a conversation, or `/` itself for a new one. */
|
|
52
72
|
export const talk = (id) => (id ? `/?${TALK}=${encodeURIComponent(id)}` : '/');
|
|
53
|
-
/**
|
|
54
|
-
* The door into the app: every "Try Hanzo Dev" link, the rail's Dev, the
|
|
55
|
-
* console's "open in the builder", dev.hanzo.ai. It signs a stranger in and
|
|
56
|
-
* forwards a member to Dev in the app, `/?at=` (app/(app)/dev/_door.tsx). It is
|
|
57
|
-
* the app's page, so it is reached through `site()`: here on hanzo.ai, across
|
|
58
|
-
* to hanzo.ai from hanzo.team.
|
|
59
|
-
*/
|
|
60
|
-
const DEV = '/dev';
|
|
61
73
|
/** A run, as the platform mints its id. */
|
|
62
74
|
const SESSION = /^sess_[0-9a-f]{32}$/;
|
|
63
|
-
/** A
|
|
75
|
+
/** A deployed site, as the Sites plane slugs it. */
|
|
64
76
|
const SLUG = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
|
|
77
|
+
/** A repository on the forge, `<org>/<name>`. */
|
|
78
|
+
const REPO = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}\/[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
65
79
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
80
|
+
* Dev opened on one run, project or site, from anywhere: every "Try Hanzo Dev"
|
|
81
|
+
* link, a room's Dev, the console's "open in the builder". It signs a stranger
|
|
82
|
+
* in on the way (app/(app)/dev/_door.tsx), and is reached through `site()`:
|
|
83
|
+
* here on hanzo.ai, across to hanzo.ai from hanzo.team. Only a value that is
|
|
84
|
+
* exactly a session id, a repository or a site's slug is carried; anything else
|
|
85
|
+
* opens /dev itself, so no caller can make this name another address.
|
|
71
86
|
*/
|
|
72
|
-
export const dev = (ref) => site(
|
|
87
|
+
export const dev = (ref) => site(ref && (SESSION.test(ref) || REPO.test(ref) || SLUG.test(ref)) ? app(ref) : DEV);
|
|
73
88
|
/** The routes under app/(app) the apex keeps: the app — /chat, a chat shared by link, and /dev. */
|
|
74
89
|
const KEPT = new Set(['/chat', '/chat/shared', '/dev']);
|
|
75
90
|
/** Settings, and every page under it: the person's and the organization's, served wherever the app is. */
|
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 {
|
|
17
|
+
import { TALK } from './host.js';
|
|
18
18
|
import { api } from './api.js';
|
|
19
19
|
/** The SDK's own keys, `hanzo_iam_`-prefixed. The far side of a boundary. */
|
|
20
20
|
const PREFIX = 'hanzo_iam_';
|
|
@@ -59,12 +59,13 @@ export function live() {
|
|
|
59
59
|
/** The attribute on `<html>` that says `/` is showing the app, not the landing. */
|
|
60
60
|
export const APP = 'data-app';
|
|
61
61
|
/**
|
|
62
|
-
* An address on `/` that names a place in the app — `?
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
62
|
+
* An address on `/` that names a place in the app — `?chat` in Chat
|
|
63
|
+
* (lib/host.ts `TALK`; Dev's places are paths under `/dev`, whose own page
|
|
64
|
+
* signs a stranger in). The app is signed-in only, so a reader nobody has
|
|
65
|
+
* signed in is sent to hanzo.id with it rather than shown the landing, and it
|
|
66
|
+
* comes back with them.
|
|
66
67
|
*/
|
|
67
|
-
const PLACE = new RegExp(`[?&]
|
|
68
|
+
const PLACE = new RegExp(`[?&]${TALK}(?:[=&]|$)`);
|
|
68
69
|
/** Whether `search` names a place in the app. */
|
|
69
70
|
export const place = (search) => PLACE.test(search);
|
|
70
71
|
/**
|
package/dist/recent.d.ts
CHANGED
|
@@ -9,10 +9,12 @@ export interface Recent {
|
|
|
9
9
|
began: string;
|
|
10
10
|
status?: string;
|
|
11
11
|
project?: string;
|
|
12
|
+
/** A conversation its person pinned: listed first, under Pinned. */
|
|
13
|
+
pinned?: boolean;
|
|
12
14
|
}
|
|
13
|
-
export type Status = 'all' | 'running' | 'paused' | 'done' | 'error' | 'stopped';
|
|
15
|
+
export type Status = 'all' | 'running' | 'paused' | 'done' | 'error' | 'stopped' | 'archived';
|
|
14
16
|
export type Since = 'any' | 'day' | 'week' | 'month';
|
|
15
|
-
export type Group = 'date' | 'state' | 'project' | '
|
|
17
|
+
export type Group = 'date' | 'state' | 'project' | 'none';
|
|
16
18
|
export type Sort = 'activity' | 'created' | 'title';
|
|
17
19
|
/** How the list is narrowed, grouped and sorted. */
|
|
18
20
|
export interface View {
|
|
@@ -35,11 +37,13 @@ export declare const MENUS: Record<Mode, {
|
|
|
35
37
|
}>;
|
|
36
38
|
/** A kept view as this mode may hold it: anything a menu does not offer reads as the plain view's. */
|
|
37
39
|
export declare function viewOf(mode: Mode, kept: unknown): View;
|
|
38
|
-
/** Chat's conversations as rows. */
|
|
40
|
+
/** Chat's conversations as rows: an archived one's status says so. */
|
|
39
41
|
export declare function chats(threads: {
|
|
40
42
|
id: string;
|
|
41
43
|
title?: string;
|
|
42
44
|
updatedAt?: string;
|
|
45
|
+
pinned?: boolean;
|
|
46
|
+
archived?: boolean;
|
|
43
47
|
}[]): Recent[];
|
|
44
48
|
/** Dev's runs as rows. */
|
|
45
49
|
export declare function runs(sessions: {
|
|
@@ -53,13 +57,16 @@ export declare function runs(sessions: {
|
|
|
53
57
|
}[]): Recent[];
|
|
54
58
|
/**
|
|
55
59
|
* The list as drawn: the rows the view and the words admit, sorted, and cut into
|
|
56
|
-
* headed groups in the order they read.
|
|
60
|
+
* headed groups in the order they read. Pinned rows come first, under Pinned,
|
|
61
|
+
* whatever the grouping; a view grouped by None holds the rest in one group with
|
|
57
62
|
* no heading.
|
|
58
63
|
*/
|
|
59
|
-
export declare function arrange(rows: Recent[], view: View, words: string,
|
|
64
|
+
export declare function arrange(rows: Recent[], view: View, words: string, now?: Date): {
|
|
60
65
|
title: string;
|
|
61
66
|
rows: Recent[];
|
|
62
67
|
}[];
|
|
68
|
+
/** The heading pinned rows read under, first in every view. */
|
|
69
|
+
export declare const PINNED = "Pinned";
|
|
63
70
|
/**
|
|
64
71
|
* How long ago a row was last active, as a list reads it at a glance: `now`,
|
|
65
72
|
* `12m`, `3h`, `2d`, `5w`, `4mo`, `2y`. '' for an instant that is not one.
|
package/dist/recent.js
CHANGED
|
@@ -17,11 +17,14 @@ export const SINCE = [
|
|
|
17
17
|
];
|
|
18
18
|
/** What each mode's menu offers, section by section, as Claude's Recents menu does. */
|
|
19
19
|
export const MENUS = {
|
|
20
|
+
// A chat's status is whether it is archived: the store lists the two apart.
|
|
20
21
|
chat: {
|
|
21
|
-
status: [
|
|
22
|
+
status: [
|
|
23
|
+
{ id: 'all', label: 'Active' },
|
|
24
|
+
{ id: 'archived', label: 'Archived' },
|
|
25
|
+
],
|
|
22
26
|
group: [
|
|
23
27
|
{ id: 'date', label: 'Date' },
|
|
24
|
-
{ id: 'starred', label: 'Starred' },
|
|
25
28
|
{ id: 'none', label: 'None' },
|
|
26
29
|
],
|
|
27
30
|
sort: [
|
|
@@ -56,9 +59,17 @@ export function viewOf(mode, kept) {
|
|
|
56
59
|
sort: pick(k.sort, m.sort, PLAIN.sort),
|
|
57
60
|
};
|
|
58
61
|
}
|
|
59
|
-
/** Chat's conversations as rows. */
|
|
62
|
+
/** Chat's conversations as rows: an archived one's status says so. */
|
|
60
63
|
export function chats(threads) {
|
|
61
|
-
return threads.map((t) => ({
|
|
64
|
+
return threads.map((t) => ({
|
|
65
|
+
kind: 'chat',
|
|
66
|
+
id: t.id,
|
|
67
|
+
title: t.title || 'Untitled',
|
|
68
|
+
at: t.updatedAt ?? '',
|
|
69
|
+
began: t.updatedAt ?? '',
|
|
70
|
+
...(t.archived ? { status: 'archived' } : {}),
|
|
71
|
+
...(t.pinned ? { pinned: true } : {}),
|
|
72
|
+
}));
|
|
62
73
|
}
|
|
63
74
|
/** Dev's runs as rows. */
|
|
64
75
|
export function runs(sessions) {
|
|
@@ -96,10 +107,11 @@ function day(at, now) {
|
|
|
96
107
|
const newest = (a, b) => (a < b ? 1 : a > b ? -1 : 0);
|
|
97
108
|
/**
|
|
98
109
|
* The list as drawn: the rows the view and the words admit, sorted, and cut into
|
|
99
|
-
* headed groups in the order they read.
|
|
110
|
+
* headed groups in the order they read. Pinned rows come first, under Pinned,
|
|
111
|
+
* whatever the grouping; a view grouped by None holds the rest in one group with
|
|
100
112
|
* no heading.
|
|
101
113
|
*/
|
|
102
|
-
export function arrange(rows, view, words,
|
|
114
|
+
export function arrange(rows, view, words, now = new Date()) {
|
|
103
115
|
const w = words.trim().toLowerCase();
|
|
104
116
|
const kept = rows.filter((r) => (!w || r.title.toLowerCase().includes(w)) &&
|
|
105
117
|
(view.status === 'all' || r.status === view.status) &&
|
|
@@ -113,14 +125,14 @@ export function arrange(rows, view, words, starred, now = new Date()) {
|
|
|
113
125
|
return STATE[r.status ?? ''] ?? 'Other';
|
|
114
126
|
case 'project':
|
|
115
127
|
return r.project || 'No project';
|
|
116
|
-
case 'starred':
|
|
117
|
-
return starred.has(r.id) ? 'Starred' : 'Others';
|
|
118
128
|
default:
|
|
119
129
|
return '';
|
|
120
130
|
}
|
|
121
131
|
};
|
|
122
132
|
const groups = [];
|
|
123
133
|
for (const r of kept) {
|
|
134
|
+
if (r.pinned)
|
|
135
|
+
continue;
|
|
124
136
|
const title = headOf(r);
|
|
125
137
|
const g = groups.find((one) => one.title === title);
|
|
126
138
|
if (g)
|
|
@@ -128,12 +140,13 @@ export function arrange(rows, view, words, starred, now = new Date()) {
|
|
|
128
140
|
else
|
|
129
141
|
groups.push({ title, rows: [r] });
|
|
130
142
|
}
|
|
131
|
-
if (view.group === 'starred')
|
|
132
|
-
groups.sort((a, b) => (a.title === 'Starred' ? -1 : b.title === 'Starred' ? 1 : 0));
|
|
133
143
|
if (view.group === 'date')
|
|
134
144
|
groups.sort((a, b) => DAYS.indexOf(a.title) - DAYS.indexOf(b.title));
|
|
135
|
-
|
|
145
|
+
const pinned = kept.filter((r) => r.pinned);
|
|
146
|
+
return pinned.length ? [{ title: PINNED, rows: pinned }, ...groups] : groups;
|
|
136
147
|
}
|
|
148
|
+
/** The heading pinned rows read under, first in every view. */
|
|
149
|
+
export const PINNED = 'Pinned';
|
|
137
150
|
/**
|
|
138
151
|
* How long ago a row was last active, as a list reads it at a glance: `now`,
|
|
139
152
|
* `12m`, `3h`, `2d`, `5w`, `4mo`, `2y`. '' for an instant that is not one.
|
package/dist/thread.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
import type { Thread } from '@hanzo/ai';
|
|
3
|
+
import { useThreads } from '@hanzo/ai/react';
|
|
4
|
+
/**
|
|
5
|
+
* The member's conversations: pinned first, then most recently spoken in, the
|
|
6
|
+
* archived ones left out — or, with `archived`, those alone. It reads again
|
|
7
|
+
* whenever a conversation changes here, and once for `seek` when it does not
|
|
8
|
+
* hold it: a conversation it has not seen is one just begun.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useThreadList(archived?: boolean, seek?: string | null): ReturnType<typeof useThreads>;
|
|
11
|
+
/** What a conversation can be asked to do. Each reports a failure in a toast, in its own words. */
|
|
12
|
+
export declare function useThreadActions(): {
|
|
13
|
+
pin: (t: Thread) => Promise<boolean>;
|
|
14
|
+
archive: (t: Thread) => Promise<boolean>;
|
|
15
|
+
rename: (t: Thread, title: string) => Promise<boolean>;
|
|
16
|
+
remove: (t: Thread) => Promise<boolean>;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* One conversation's row with its menu: a right-click anywhere on it, or its ⋯.
|
|
20
|
+
*
|
|
21
|
+
* The ⋯ stands where `aside` (the row's age) stands, and takes its place while
|
|
22
|
+
* the row is hovered, focused, open in the pane or has its menu open; the rest
|
|
23
|
+
* of the time the age reads there. `onGone` runs after the conversation the
|
|
24
|
+
* pane holds is deleted — the column moves to a new chat — and defaults to
|
|
25
|
+
* emptying the room.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ThreadRow({ t, active, aside, onGone, listed, children, }: {
|
|
28
|
+
t: Thread;
|
|
29
|
+
active?: boolean;
|
|
30
|
+
aside?: ReactNode;
|
|
31
|
+
onGone?: () => void;
|
|
32
|
+
/** A row of a list (`role="listitem"`); false where it stands among other rows. */
|
|
33
|
+
listed?: boolean;
|
|
34
|
+
children: ReactElement;
|
|
35
|
+
}): import("react").JSX.Element;
|
package/dist/thread.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// A conversation's own actions — pin, rename, archive, delete — kept where the
|
|
4
|
+
// conversation is: PATCH and DELETE /v1/agent/chat/conversations/{id}
|
|
5
|
+
// (hanzoai/agent v1.0.12), the member's own only. One menu, two ways in: a
|
|
6
|
+
// right-click on the row opens it at the cursor (gui's ContextMenu), and the
|
|
7
|
+
// row's ⋯ hangs it from itself (gui's DropdownMenu), which a keyboard reaches.
|
|
8
|
+
// One list of items feeds both. Not @hanzo/ui/product/menu's ContextMenu: it
|
|
9
|
+
// portals its panel under gui's `pointer-events: none` host, so it drew and took
|
|
10
|
+
// no click (measured: every ancestor of its panel reads `none`).
|
|
11
|
+
//
|
|
12
|
+
// EVERY LIST ON THE PAGE READS AGAIN WHEN ONE CONVERSATION CHANGES, so the
|
|
13
|
+
// column's Recents, the rooms' shell and the conversation's own header agree
|
|
14
|
+
// without a second copy of anyone's pins. A pin is the server's, never this
|
|
15
|
+
// browser's: it follows the person to every device they sign in on.
|
|
16
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
17
|
+
import { useAi, useThreads } from '@hanzo/ai/react';
|
|
18
|
+
import { Archive, ArchiveRestore, MoreHorizontal, Pencil, Pin, PinOff, Trash2 } from 'lucide-react';
|
|
19
|
+
import { Button, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, Dialog, DialogContent, DialogTitle, DropdownMenu, Input, SizableText, XStack, YStack, toast, } from '@hanzo/ui';
|
|
20
|
+
import { empty, useOpen } from './open.js';
|
|
21
|
+
import { say } from './lib/org.js';
|
|
22
|
+
const lists = new Set();
|
|
23
|
+
/** Every list of conversations on the page reads again. */
|
|
24
|
+
const changed = () => lists.forEach((reload) => reload());
|
|
25
|
+
/**
|
|
26
|
+
* The member's conversations: pinned first, then most recently spoken in, the
|
|
27
|
+
* archived ones left out — or, with `archived`, those alone. It reads again
|
|
28
|
+
* whenever a conversation changes here, and once for `seek` when it does not
|
|
29
|
+
* hold it: a conversation it has not seen is one just begun.
|
|
30
|
+
*/
|
|
31
|
+
export function useThreadList(archived = false, seek = null) {
|
|
32
|
+
const store = useThreads({ archived });
|
|
33
|
+
const { reload, loading, threads } = store;
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
lists.add(reload);
|
|
36
|
+
return () => {
|
|
37
|
+
lists.delete(reload);
|
|
38
|
+
};
|
|
39
|
+
}, [reload]);
|
|
40
|
+
const asked = useRef(new Set());
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (archived || !seek || loading || asked.current.has(seek) || threads.some((t) => t.id === seek))
|
|
43
|
+
return;
|
|
44
|
+
asked.current.add(seek);
|
|
45
|
+
reload();
|
|
46
|
+
}, [archived, seek, loading, threads, reload]);
|
|
47
|
+
return store;
|
|
48
|
+
}
|
|
49
|
+
/** What a conversation can be asked to do. Each reports a failure in a toast, in its own words. */
|
|
50
|
+
export function useThreadActions() {
|
|
51
|
+
const ai = useAi();
|
|
52
|
+
const run = useCallback(async (work) => {
|
|
53
|
+
try {
|
|
54
|
+
await work();
|
|
55
|
+
changed();
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
toast.error(say(e));
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}, []);
|
|
63
|
+
return {
|
|
64
|
+
pin: (t) => run(() => ai.threads.update(t.id, { pinned: !t.pinned })),
|
|
65
|
+
archive: (t) => run(() => ai.threads.update(t.id, { archived: !t.archived })),
|
|
66
|
+
rename: (t, title) => run(() => ai.threads.update(t.id, { title })),
|
|
67
|
+
remove: (t) => run(() => ai.threads.remove(t.id)),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const titleOf = (t) => t.title || 'Untitled';
|
|
71
|
+
/**
|
|
72
|
+
* One conversation's row with its menu: a right-click anywhere on it, or its ⋯.
|
|
73
|
+
*
|
|
74
|
+
* The ⋯ stands where `aside` (the row's age) stands, and takes its place while
|
|
75
|
+
* the row is hovered, focused, open in the pane or has its menu open; the rest
|
|
76
|
+
* of the time the age reads there. `onGone` runs after the conversation the
|
|
77
|
+
* pane holds is deleted — the column moves to a new chat — and defaults to
|
|
78
|
+
* emptying the room.
|
|
79
|
+
*/
|
|
80
|
+
export function ThreadRow({ t, active = false, aside, onGone = empty, listed = true, children, }) {
|
|
81
|
+
const act = useThreadActions();
|
|
82
|
+
const { thread } = useOpen();
|
|
83
|
+
const [menu, setMenu] = useState(false);
|
|
84
|
+
const [focused, setFocused] = useState(false);
|
|
85
|
+
const [renaming, setRenaming] = useState(false);
|
|
86
|
+
const [deleting, setDeleting] = useState(false);
|
|
87
|
+
const items = [
|
|
88
|
+
{
|
|
89
|
+
key: 'pin',
|
|
90
|
+
label: t.pinned ? 'Unpin' : 'Pin',
|
|
91
|
+
icon: t.pinned ? _jsx(PinOff, { size: 16, "aria-hidden": true }) : _jsx(Pin, { size: 16, "aria-hidden": true }),
|
|
92
|
+
onSelect: () => void act.pin(t),
|
|
93
|
+
},
|
|
94
|
+
{ key: 'rename', label: 'Rename', icon: _jsx(Pencil, { size: 16, "aria-hidden": true }), onSelect: () => setRenaming(true) },
|
|
95
|
+
{
|
|
96
|
+
key: 'archive',
|
|
97
|
+
label: t.archived ? 'Unarchive' : 'Archive',
|
|
98
|
+
icon: t.archived ? _jsx(ArchiveRestore, { size: 16, "aria-hidden": true }) : _jsx(Archive, { size: 16, "aria-hidden": true }),
|
|
99
|
+
onSelect: () => void act.archive(t),
|
|
100
|
+
},
|
|
101
|
+
{ type: 'separator', key: 'gone' },
|
|
102
|
+
{ key: 'delete', label: 'Delete', icon: _jsx(Trash2, { size: 16, "aria-hidden": true }), destructive: true, onSelect: () => setDeleting(true) },
|
|
103
|
+
];
|
|
104
|
+
// The ⋯ and the age share one place: whichever shows, the other gives way.
|
|
105
|
+
const shown = active || menu || focused;
|
|
106
|
+
return (_jsxs(_Fragment, { children: [_jsxs(ContextMenu, { children: [_jsx(ContextMenuTrigger, { asChild: true, children: _jsxs(YStack, { ...(listed ? { role: 'listitem' } : null), position: "relative", group: true, "data-thread": t.id, "data-pinned": t.pinned ? '' : undefined, children: [children, aside ? (_jsx(XStack, { position: "absolute", r: "$2", t: 0, b: 0, items: "center", pointerEvents: "none", opacity: shown ? 0 : 1, "$group-hover": { opacity: 0 }, children: aside })) : null, _jsx(XStack, { position: "absolute", r: "$1", t: 0, b: 0, items: "center", children: _jsx(DropdownMenu, { open: menu, onOpenChange: setMenu, minWidth: 180, items: items, trigger: _jsx(XStack, { render: "button", "aria-label": "Chat options", "aria-haspopup": "menu", "data-slot": "thread-options", width: 26, height: 26, rounded: "$2", items: "center", justify: "center", cursor: "pointer", bg: "transparent", opacity: shown ? 1 : 0, "$group-hover": { opacity: 1 }, hoverStyle: { bg: '$hover' }, onFocus: () => setFocused(true), onBlur: () => setFocused(false), children: _jsx(MoreHorizontal, { size: 15, "aria-hidden": true }) }) }) })] }) }), _jsx(ContextMenuContent, { children: items.map((one, i) => one.type === 'separator' ? (_jsx(ContextMenuSeparator, {}, one.key ?? i)) : one.type === 'label' ? null : (_jsxs(ContextMenuItem, { variant: one.destructive ? 'destructive' : 'default', onSelect: one.onSelect, children: [one.icon, one.label] }, one.key))) })] }), _jsx(Rename, { t: t, open: renaming, onOpenChange: setRenaming, save: (words) => act.rename(t, words) }), _jsx(Delete, { t: t, open: deleting, onOpenChange: setDeleting, remove: async () => {
|
|
107
|
+
const ok = await act.remove(t);
|
|
108
|
+
if (ok && thread === t.id)
|
|
109
|
+
onGone();
|
|
110
|
+
return ok;
|
|
111
|
+
} })] }));
|
|
112
|
+
}
|
|
113
|
+
/** A conversation's title, written again: one line, never empty. */
|
|
114
|
+
function Rename({ t, open, onOpenChange, save, }) {
|
|
115
|
+
const [words, setWords] = useState(titleOf(t));
|
|
116
|
+
const [busy, setBusy] = useState(false);
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (open)
|
|
119
|
+
setWords(titleOf(t));
|
|
120
|
+
}, [open, t]);
|
|
121
|
+
const submit = async () => {
|
|
122
|
+
const title = words.trim();
|
|
123
|
+
if (!title || busy)
|
|
124
|
+
return;
|
|
125
|
+
setBusy(true);
|
|
126
|
+
if (await save(title))
|
|
127
|
+
onOpenChange(false);
|
|
128
|
+
setBusy(false);
|
|
129
|
+
};
|
|
130
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: "Rename chat" }), _jsx(Input, { autoFocus: true, value: words, onChangeText: setWords, "aria-label": "Chat title", maxLength: 80, onKeyDown: (e) => {
|
|
131
|
+
if (e.key === 'Enter')
|
|
132
|
+
void submit();
|
|
133
|
+
} }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", rounded: 999, onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", rounded: 999, disabled: busy || !words.trim(), onPress: () => void submit(), children: busy ? 'Saving…' : 'Save' })] })] }) }));
|
|
134
|
+
}
|
|
135
|
+
/** Deleting asks first, and says what goes with it: every turn, and every link it was shared by. */
|
|
136
|
+
function Delete({ t, open, onOpenChange, remove, }) {
|
|
137
|
+
const [busy, setBusy] = useState(false);
|
|
138
|
+
const submit = async () => {
|
|
139
|
+
if (busy)
|
|
140
|
+
return;
|
|
141
|
+
setBusy(true);
|
|
142
|
+
if (await remove())
|
|
143
|
+
onOpenChange(false);
|
|
144
|
+
setBusy(false);
|
|
145
|
+
};
|
|
146
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 440, rounded: "$6", gap: "$3", children: [_jsx(DialogTitle, { children: "Delete chat?" }), _jsx(SizableText, { size: "$2", color: "$soft", children: `“${titleOf(t)}” is deleted for good, with every link it was shared by. To keep it out of the list instead, archive it.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "outline", rounded: 999, onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", variant: "destructive", rounded: 999, disabled: busy, onPress: () => void submit(), children: busy ? 'Deleting…' : 'Delete' })] })] }) }));
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/rooms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
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",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"simple-icons": "16.30.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@hanzo/ai": ">=0.6.
|
|
50
|
+
"@hanzo/ai": ">=0.6.23",
|
|
51
51
|
"@hanzo/appearance": ">=0.2.6",
|
|
52
52
|
"@hanzo/build": ">=0.2.34",
|
|
53
53
|
"@hanzo/composer": ">=0.2.0",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"react-dom": ">=19.2.8"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@hanzo/ai": "0.6.
|
|
68
|
+
"@hanzo/ai": "0.6.23",
|
|
69
69
|
"@hanzo/appearance": "0.2.8",
|
|
70
70
|
"@hanzo/build": "0.2.34",
|
|
71
71
|
"@hanzo/composer": "0.2.0",
|