@hanzo/rooms 0.1.35 → 0.1.37
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/Chat.js +10 -7
- package/dist/Column.js +1 -1
- package/dist/Recents.d.ts +3 -1
- package/dist/Recents.js +32 -33
- package/dist/Share.d.ts +10 -0
- package/dist/Share.js +37 -22
- package/dist/Shell.js +19 -11
- package/dist/chord.d.ts +21 -0
- package/dist/chord.js +51 -0
- package/dist/pending.d.ts +41 -0
- package/dist/pending.js +181 -0
- package/dist/recent.d.ts +12 -5
- package/dist/recent.js +24 -11
- package/dist/stars.d.ts +8 -0
- package/dist/stars.js +32 -0
- package/dist/thread.d.ts +57 -0
- package/dist/thread.js +408 -0
- package/package.json +3 -3
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. Chat's rows share one
|
|
8
|
+
// menu (thread.tsx `ConversationMenu`): pinned ones list first, under Pinned,
|
|
9
|
+
// and 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 { ConversationMenu, 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, held: thread, 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,29 @@ 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, held = null, 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 t = threads?.get(r.id);
|
|
76
|
+
const item = (_jsx(SidebarItem, { "data-kind": r.kind, active: open(r), rounded: "$4", minH: 32, pr: when || t ? '$8' : '$2', ...(t ? { $touchable: { pr: 52 } } : null), onKeyDown: (e) => {
|
|
77
|
+
if (e.key !== 'Enter' && e.key !== ' ')
|
|
78
|
+
return;
|
|
79
|
+
e.preventDefault?.();
|
|
80
|
+
onOpen(r);
|
|
81
|
+
}, 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 }));
|
|
82
|
+
// How long ago, at the row's end; the full time is its title.
|
|
83
|
+
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;
|
|
84
|
+
if (t)
|
|
85
|
+
return (_jsx(ThreadRow, { t: t, active: open(r), aside: age, 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
|
-
|
|
88
|
+
const body = 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.' }));
|
|
89
|
+
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, threads ? (_jsx(ConversationMenu, { threads: threads, active: held, onGone: onNew, children: _jsx(YStack, { children: body }) })) : (body)] })] }));
|
|
91
90
|
}
|
package/dist/Share.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
export declare function Share({ thread }: {
|
|
2
2
|
thread: string;
|
|
3
3
|
}): import("react").JSX.Element;
|
|
4
|
+
/**
|
|
5
|
+
* The same panel as a dialog, for a conversation the pane need not hold: a row
|
|
6
|
+
* menu's Share…. `onCloseAutoFocus` says where focus goes when it shuts.
|
|
7
|
+
*/
|
|
8
|
+
export declare function ShareDialog({ thread, open, onOpenChange, onCloseAutoFocus, }: {
|
|
9
|
+
thread: string | null;
|
|
10
|
+
open: boolean;
|
|
11
|
+
onOpenChange: (open: boolean) => void;
|
|
12
|
+
onCloseAutoFocus?: (e: Event) => void;
|
|
13
|
+
}): import("react").JSX.Element;
|
package/dist/Share.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
// The conversation header's Share control and the panel it opens
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// The conversation header's Share control and the panel it opens, and the same
|
|
4
|
+
// panel as a dialog for a conversation's row menu (thread.tsx): one `Sharing`,
|
|
5
|
+
// placed two ways.
|
|
4
6
|
//
|
|
5
7
|
// A link opens this ONE conversation, read only, as it stands when the link is
|
|
6
8
|
// made (lib/share.ts), to the people who open it signed in. The panel copies
|
|
@@ -9,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
11
|
// do not reply: a conversation belongs to the member who started it.
|
|
10
12
|
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
11
13
|
import { Check, Link2, Share2 } from 'lucide-react';
|
|
12
|
-
import { Box, Button, Text, XStack, YStack } from '@hanzo/ui';
|
|
14
|
+
import { Box, Button, Dialog, DialogContent, DialogTitle, Text, XStack, YStack } from '@hanzo/ui';
|
|
13
15
|
import { hold, heldToken, link, Refused, shares } from './lib/share.js';
|
|
14
16
|
import { BAD } from './lib/mix.js';
|
|
15
17
|
/** How long "Link copied" stays. The same 1400 ms `Take` confirms for. */
|
|
@@ -29,9 +31,40 @@ export function Share({ thread }) {
|
|
|
29
31
|
}, []);
|
|
30
32
|
return (_jsxs(Box, { position: "relative", children: [_jsx(Box, { ref: button, render: "button", onClick: () => setOpen((o) => !o), "aria-haspopup": "dialog", "aria-expanded": open, "aria-label": "Share this conversation", borderWidth: 0, bg: "transparent", p: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$1", children: [_jsx(Share2, { size: 13, "aria-hidden": true }), _jsx(Text, { fontSize: "$2", color: "$ink", children: "Share" })] }) }), open ? _jsx(Panel, { thread: thread, onClose: close }) : null] }));
|
|
31
33
|
}
|
|
34
|
+
/** The header's panel: hung under its button, shut by Escape or a press outside, focus starting inside. */
|
|
32
35
|
function Panel({ thread, onClose }) {
|
|
33
36
|
const heading = useId();
|
|
34
37
|
const box = useRef(null);
|
|
38
|
+
// Escape closes; a press outside closes; focus starts inside.
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
box.current?.querySelector('button')?.focus();
|
|
41
|
+
const key = (e) => {
|
|
42
|
+
if (e.key === 'Escape')
|
|
43
|
+
onClose();
|
|
44
|
+
};
|
|
45
|
+
const press = (e) => {
|
|
46
|
+
if (box.current && !box.current.contains(e.target) && !e.target?.closest?.('[aria-haspopup="dialog"]'))
|
|
47
|
+
onClose();
|
|
48
|
+
};
|
|
49
|
+
document.addEventListener('keydown', key);
|
|
50
|
+
document.addEventListener('mousedown', press);
|
|
51
|
+
return () => {
|
|
52
|
+
document.removeEventListener('keydown', key);
|
|
53
|
+
document.removeEventListener('mousedown', press);
|
|
54
|
+
};
|
|
55
|
+
}, [onClose]);
|
|
56
|
+
return (_jsx(YStack, { ref: box, role: "dialog", "aria-labelledby": heading, position: "absolute", r: 0, t: "100%", mt: "$2", z: "var(--z-dropdown)", width: 360, maxW: "calc(100vw - 32px)", p: "$4", gap: "$3", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$background", boxShadow: "0 16px 36px color-mix(in srgb, var(--pure-black) 30%, transparent)", children: _jsx(Sharing, { thread: thread, note: `${heading}-said`, title: _jsx(Text, { id: heading, render: "h2", fontSize: "$4", fontWeight: "600", color: "$ink", children: "Share this chat" }) }) }));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The same panel as a dialog, for a conversation the pane need not hold: a row
|
|
60
|
+
* menu's Share…. `onCloseAutoFocus` says where focus goes when it shuts.
|
|
61
|
+
*/
|
|
62
|
+
export function ShareDialog({ thread, open, onOpenChange, onCloseAutoFocus, }) {
|
|
63
|
+
const note = useId();
|
|
64
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsx(DialogContent, { maxW: 400, rounded: "$6", gap: "$3", onCloseAutoFocus: onCloseAutoFocus, children: thread ? _jsx(Sharing, { thread: thread, note: note, title: _jsx(DialogTitle, { children: "Share this chat" }) }) : null }) }));
|
|
65
|
+
}
|
|
66
|
+
/** What a link to one conversation is, who opened each, and the controls over them. */
|
|
67
|
+
function Sharing({ thread, title, note }) {
|
|
35
68
|
const [live, setLive] = useState(null);
|
|
36
69
|
const [busy, setBusy] = useState(null);
|
|
37
70
|
const [wrong, setWrong] = useState(null);
|
|
@@ -51,24 +84,6 @@ function Panel({ thread, onClose }) {
|
|
|
51
84
|
on = false;
|
|
52
85
|
};
|
|
53
86
|
}, [thread]);
|
|
54
|
-
// Escape closes; a press outside closes; focus starts inside.
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
box.current?.querySelector('button')?.focus();
|
|
57
|
-
const key = (e) => {
|
|
58
|
-
if (e.key === 'Escape')
|
|
59
|
-
onClose();
|
|
60
|
-
};
|
|
61
|
-
const press = (e) => {
|
|
62
|
-
if (box.current && !box.current.contains(e.target) && !e.target?.closest?.('[aria-haspopup="dialog"]'))
|
|
63
|
-
onClose();
|
|
64
|
-
};
|
|
65
|
-
document.addEventListener('keydown', key);
|
|
66
|
-
document.addEventListener('mousedown', press);
|
|
67
|
-
return () => {
|
|
68
|
-
document.removeEventListener('keydown', key);
|
|
69
|
-
document.removeEventListener('mousedown', press);
|
|
70
|
-
};
|
|
71
|
-
}, [onClose]);
|
|
72
87
|
useEffect(() => {
|
|
73
88
|
if (!copied)
|
|
74
89
|
return;
|
|
@@ -126,5 +141,5 @@ function Panel({ thread, onClose }) {
|
|
|
126
141
|
setBusy(null);
|
|
127
142
|
}
|
|
128
143
|
};
|
|
129
|
-
return (_jsxs(
|
|
144
|
+
return (_jsxs(_Fragment, { children: [title, _jsx(Text, { fontSize: "$2", color: "$soft", lineHeight: "$3", children: "People who open the link signed in can read this chat as it is now, and you see who they are below. They cannot reply, and messages you send later are not shared." }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Button, { size: "sm", onClick: () => void copy(), disabled: busy !== null || live === null, "aria-describedby": note, children: _jsxs(XStack, { items: "center", gap: "$1.5", children: [copied ? _jsx(Check, { size: 14, "aria-hidden": true }) : _jsx(Link2, { size: 14, "aria-hidden": true }), _jsx(Text, { fontSize: "$2", color: "inherit", children: copied ? 'Link copied' : busy === 'copy' ? 'Making a link…' : 'Copy link' })] }) }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Read only" })] }), _jsx(Text, { id: note, "aria-live": "polite", fontSize: "$1", color: "$soft", minH: 16, children: copied ? 'The link is on your clipboard.' : '' }), _jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$1", color: "$soft", textTransform: "uppercase", children: "Who has access" }), _jsxs(XStack, { items: "center", justify: "space-between", children: [_jsx(Text, { fontSize: "$2", color: "$ink", children: "You" }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Owner" })] }), live === null ? (_jsx(Text, { fontSize: "$2", color: "$soft", children: "Reading links." })) : live.length === 0 ? (_jsx(Text, { fontSize: "$2", color: "$soft", children: "No links yet. Copy link makes one." })) : (_jsx(YStack, { render: "ul", gap: "$2", m: 0, p: 0, "aria-label": "Live links", children: live.map((row) => (_jsxs(YStack, { render: "li", gap: "$1.5", children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsxs(YStack, { minW: 0, children: [_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: "Link" }), _jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Read only", day(row.createdAt) ? ` · made ${day(row.createdAt)}` : ''] })] }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => void revoke(row.id), disabled: busy !== null, "aria-label": `Revoke the link made ${day(row.createdAt) || 'earlier'}`, children: busy === row.id ? 'Revoking…' : 'Revoke' })] }), (row.viewers ?? []).length === 0 ? (_jsx(Text, { fontSize: "$1", color: "$soft", pl: "$3", children: "Nobody has opened it yet." })) : (_jsx(YStack, { render: "ul", gap: "$1", m: 0, pl: "$3", "aria-label": "Opened by", children: row.viewers.map((v) => (_jsxs(XStack, { render: "li", items: "center", justify: "space-between", gap: "$2", children: [_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: v.name || 'Someone signed in' }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => void unview(row, v), disabled: busy !== null, "aria-label": `Remove ${v.name || 'this reader'}`, children: busy === v.id ? 'Removing…' : 'Remove' })] }, v.id))) }))] }, row.id))) }))] }), wrong ? (_jsx(Text, { role: "alert", fontSize: "$2", color: BAD, children: wrong })) : null] }));
|
|
130
145
|
}
|
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 { ConversationMenu, 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,18 @@ 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
|
-
const
|
|
408
|
-
|
|
409
|
+
const byId = useMemo(() => new Map(threads.map((t) => [t.id, t])), [threads]);
|
|
410
|
+
// PINNED FIRST, by the server's pin (thread.tsx), then the days.
|
|
411
|
+
const shown = found.filter((t) => !t.thread?.pinned);
|
|
412
|
+
const kept = found.filter((t) => t.thread?.pinned);
|
|
409
413
|
const bands = [];
|
|
410
414
|
if (kept.length)
|
|
411
|
-
bands.push({ label:
|
|
415
|
+
bands.push({ label: PINNED, rows: kept });
|
|
412
416
|
const now = new Date();
|
|
413
417
|
for (const t of shown) {
|
|
414
418
|
const label = when(t.updatedAt, now);
|
|
415
419
|
const last = bands[bands.length - 1];
|
|
416
|
-
if (last && last.label === label && last.label !==
|
|
420
|
+
if (last && last.label === label && last.label !== PINNED)
|
|
417
421
|
last.rows.push(t);
|
|
418
422
|
else
|
|
419
423
|
bands.push({ label, rows: [t] });
|
|
@@ -421,10 +425,14 @@ function Chats({ filter = "", onPick, }) {
|
|
|
421
425
|
if (!signedIn)
|
|
422
426
|
return _jsx(Note, { children: "Sign in to see your recent chats." });
|
|
423
427
|
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
|
-
|
|
428
|
+
return (_jsxs(_Fragment, { children: [_jsx(ConversationMenu, { threads: byId, active: thread, children: _jsxs(YStack, { children: [mine, mine ? null : bands.map((band) => (_jsx(SidebarSection, { label: band.label, children: band.rows.map((t) => {
|
|
429
|
+
const item = (_jsx(SidebarItem, { active: t.id === thread, ...(t.thread ? { pr: "$8", $touchable: { pr: 52 } } : null), onPress: () => {
|
|
430
|
+
open(t.id);
|
|
431
|
+
onPick?.();
|
|
432
|
+
}, 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));
|
|
433
|
+
// A stored conversation carries its menu; a call or a group is no conversation of the store's.
|
|
434
|
+
return t.thread ? (_jsx(ThreadRow, { t: t.thread, active: t.id === thread, listed: false, children: item }, t.id)) : (item);
|
|
435
|
+
}) }, band.label)))] }) }), _jsx(SharedWithYou, { filter: filter, onPick: onPick })] }));
|
|
428
436
|
}
|
|
429
437
|
/**
|
|
430
438
|
* The chats other people shared with this reader, and who shared each: every
|
package/dist/chord.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type Act = 'rename' | 'pin';
|
|
2
|
+
/** The part of a keydown this reads. */
|
|
3
|
+
export interface Press {
|
|
4
|
+
key?: string;
|
|
5
|
+
code?: string;
|
|
6
|
+
ctrlKey: boolean;
|
|
7
|
+
altKey: boolean;
|
|
8
|
+
metaKey: boolean;
|
|
9
|
+
shiftKey: boolean;
|
|
10
|
+
repeat?: boolean;
|
|
11
|
+
isComposing?: boolean;
|
|
12
|
+
target: EventTarget | null;
|
|
13
|
+
}
|
|
14
|
+
/** Whether a key pressed on `target` is typing: a field, a picker, or text that edits itself. */
|
|
15
|
+
export declare function typing(target: EventTarget | null): boolean;
|
|
16
|
+
/** What a keydown asks of a conversation, or null when it asks nothing of one. */
|
|
17
|
+
export declare function chord(e: Press): Act | null;
|
|
18
|
+
/** A chord as a menu prints it: ⌃⌥R on a Mac, Ctrl+Alt+R elsewhere. */
|
|
19
|
+
export declare function said(act: Act, mac: boolean): string;
|
|
20
|
+
/** A chord as `aria-keyshortcuts` says it, the same on every platform. */
|
|
21
|
+
export declare const named: (act: Act) => string;
|
package/dist/chord.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The keys that act on a conversation: Ctrl+Alt+R renames it, Ctrl+Alt+P pins
|
|
2
|
+
// or unpins it — the one whose menu is open, or else the one the pane holds.
|
|
3
|
+
// Never while the reader is typing or a dialog holds the keys, and once a press:
|
|
4
|
+
// a held key repeats, and a held Ctrl+Alt+P would toggle the pin thirty times a
|
|
5
|
+
// second. No React, so a spec reads it as it is.
|
|
6
|
+
const LETTERS = { r: 'rename', p: 'pin' };
|
|
7
|
+
/** Whether `target` sits inside something matching `selector`. */
|
|
8
|
+
const within = (target, selector) => {
|
|
9
|
+
const el = target;
|
|
10
|
+
return Boolean(el && typeof el === 'object' && typeof el.closest === 'function' && el.closest(selector) !== null);
|
|
11
|
+
};
|
|
12
|
+
/** Whether a key pressed on `target` is typing: a field, a picker, or text that edits itself. */
|
|
13
|
+
export function typing(target) {
|
|
14
|
+
const el = target;
|
|
15
|
+
if (!el || typeof el !== 'object')
|
|
16
|
+
return false;
|
|
17
|
+
if (el.isContentEditable)
|
|
18
|
+
return true;
|
|
19
|
+
const tag = (el.tagName ?? '').toUpperCase();
|
|
20
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT')
|
|
21
|
+
return true;
|
|
22
|
+
return within(target, '[contenteditable=""],[contenteditable="true"],[role="textbox"]');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The letter a key press names: the one on the key, whatever the layout (the
|
|
26
|
+
* labelled P is where QWERTY's R is on Dvorak), or — where Option or AltGr made
|
|
27
|
+
* it another character, ⌥R being "®" on a Mac — the key's place.
|
|
28
|
+
*/
|
|
29
|
+
function letter(e) {
|
|
30
|
+
if (e.key && /^[a-z]$/i.test(e.key))
|
|
31
|
+
return e.key.toLowerCase();
|
|
32
|
+
const place = /^Key([A-Z])$/.exec(e.code ?? '');
|
|
33
|
+
return place ? place[1].toLowerCase() : null;
|
|
34
|
+
}
|
|
35
|
+
/** What a keydown asks of a conversation, or null when it asks nothing of one. */
|
|
36
|
+
export function chord(e) {
|
|
37
|
+
if (!e.ctrlKey || !e.altKey || e.metaKey || e.shiftKey || e.repeat || e.isComposing)
|
|
38
|
+
return null;
|
|
39
|
+
const l = letter(e);
|
|
40
|
+
const act = l ? LETTERS[l] : undefined;
|
|
41
|
+
if (!act || typing(e.target) || within(e.target, '[role="dialog"],[role="alertdialog"]'))
|
|
42
|
+
return null;
|
|
43
|
+
return act;
|
|
44
|
+
}
|
|
45
|
+
/** A chord as a menu prints it: ⌃⌥R on a Mac, Ctrl+Alt+R elsewhere. */
|
|
46
|
+
export function said(act, mac) {
|
|
47
|
+
const key = act === 'rename' ? 'R' : 'P';
|
|
48
|
+
return mac ? `⌃⌥${key}` : `Ctrl+Alt+${key}`;
|
|
49
|
+
}
|
|
50
|
+
/** A chord as `aria-keyshortcuts` says it, the same on every platform. */
|
|
51
|
+
export const named = (act) => `Control+Alt+${act === 'rename' ? 'R' : 'P'}`;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Thread } from '@hanzo/ai';
|
|
2
|
+
/** What a change sets: the fields it names. */
|
|
3
|
+
export type Patch = Partial<Pick<Thread, 'title' | 'pinned' | 'archived'>>;
|
|
4
|
+
/** A list of conversations on the page: the moment its rows were asked for, and how to ask again. */
|
|
5
|
+
export interface Reader {
|
|
6
|
+
at: number;
|
|
7
|
+
reload: () => void;
|
|
8
|
+
}
|
|
9
|
+
/** The next moment. A read takes one as it is asked for, an answer as it lands. */
|
|
10
|
+
export declare const tick: () => number;
|
|
11
|
+
/** For `useSyncExternalStore`: told whenever a change is made, settled, taken back or dropped. */
|
|
12
|
+
export declare function subscribe(listener: () => void): () => void;
|
|
13
|
+
/** For `useSyncExternalStore`: a number that moves whenever the changes do. */
|
|
14
|
+
export declare const snapshot: () => number;
|
|
15
|
+
/**
|
|
16
|
+
* Whose conversations these are: the client the lists read through. Another
|
|
17
|
+
* client is another reader or organization, and nothing asked under the last
|
|
18
|
+
* one is drawn over its rows.
|
|
19
|
+
*/
|
|
20
|
+
export declare function bind(next: unknown): void;
|
|
21
|
+
/** A list joins the page; the returned function is its leaving. */
|
|
22
|
+
export declare function enlist(reader: Reader): () => void;
|
|
23
|
+
/** Every list on the page reads the store again. */
|
|
24
|
+
export declare function reread(): void;
|
|
25
|
+
/** Folds each settled change every list on the page has read after into the thread it was laid over. */
|
|
26
|
+
export declare function sweep(): void;
|
|
27
|
+
/**
|
|
28
|
+
* A list as it reads with every change it has not read yet: `threads` read at
|
|
29
|
+
* `at`, from the archived shelf or the active one. A deleted conversation
|
|
30
|
+
* leaves; one archived or brought back leaves the shelf it is no longer on and
|
|
31
|
+
* joins the other; any other change shows in place.
|
|
32
|
+
*/
|
|
33
|
+
export declare function view(threads: Thread[], at: number, archived: boolean): Thread[];
|
|
34
|
+
/**
|
|
35
|
+
* Asks the store for one change to `t`, shown at once: `patch` the fields it
|
|
36
|
+
* sets, or null for a delete, and `send` the request, made once every earlier
|
|
37
|
+
* change to the same conversation has been answered. What the store answers
|
|
38
|
+
* for those fields is what shows. On a refusal this change alone is taken back
|
|
39
|
+
* and the refusal thrown.
|
|
40
|
+
*/
|
|
41
|
+
export declare function attempt(t: Thread, patch: Patch | null, send: () => Promise<Thread | void>): Promise<void>;
|