@hanzo/rooms 0.1.81 → 0.1.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/App.js CHANGED
@@ -241,5 +241,5 @@ function Window({ bar }) {
241
241
  shut();
242
242
  setFinding(true);
243
243
  };
244
- return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(HostProvider, { host: host, children: _jsx(PrefsProvider, { children: _jsxs(YStack, { ref: view, "data-slot": "window", ...(bar ? floor : null), flex: 1, minW: 0, height: "100dvh", overflow: "hidden", "$max-md": { position: 'fixed', t: TOP, l: 0, r: 0, height: HEIGHT }, children: [bar ? (_jsxs(XStack, { "data-slot": "title-bar", height: BAR, shrink: 0, items: "center", gap: 2, pl: (bar.inset ?? 0) + 6, pr: "$2", ...bar.drag, children: [_jsx(XStack, { "$max-md": { display: 'none' }, children: _jsx(Toggle, { open: !closed, onPress: flip }) }), _jsx(XStack, { $md: { display: 'none' }, children: _jsx(Toggle, { open: drawer, onPress: () => setDrawer(true), label: "Open sidebar" }) }), _jsx(Mark, { label: "Back", "data-slot": "back", onPress: () => router.back(), children: _jsx(ArrowLeft, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Forward", "data-slot": "forward", onPress: () => router.forward(), children: _jsx(ArrowRight, { size: 18, "aria-hidden": true }) }), _jsx(XStack, { flex: 1, self: "stretch", ...bar.drag })] })) : null, _jsxs(XStack, { "data-room": "", "data-app": mode, "data-slot": "frame", ...(bar ? null : floor), flex: 1, minW: 0, minH: 0, pr: "var(--pane-gap)", pb: "var(--pane-gap)", pt: bar ? 0 : 'var(--pane-gap)', "$max-md": { p: 0 }, children: [_jsx(YStack, { height: "100%", "$max-md": { display: 'none' }, children: _jsx(Rail, { host: host, mode: mode, at: where, open: !closed, toggle: bar ? undefined : flip, onChat: toChat, onDev: toDev, onStudio: toStudio, go: move, onOrg: org }) }), closed ? null : (_jsx(YStack, { "$max-md": { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: () => setClosed(true), onSeek: seek, places: sidebar }) })), _jsxs(YStack, { "data-slot": "stage", flex: 1, minW: 0, minH: 0, ml: "var(--pane-gap)", "$max-md": { ml: 0, pb: FOOT }, children: [bar ? null : _jsx(Top, { host: host, mode: mode, places: sidebar, go: move, onMenu: () => setDrawer(true), onChat: toChat, onDev: toDev }), _jsx(XStack, { "data-slot": "panes", flex: 1, minW: 0, minH: 0, children: mode === 'chat' ? (_jsx(Paned, { framed: true, children: _jsx(Chat, {}) })) : mode === 'studio' ? (_jsx(YStack, { render: "main", "data-slot": "pane", ...pane(), overflow: "hidden", flex: 1, minW: 0, minH: 0, children: _jsx(YStack, { flex: 1, minH: 0, height: "100%", children: _jsx(Suspense, { fallback: null, children: _jsx(Studio, { host: maker, rail: false }) }) }) })) : (_jsx(YStack, { render: "main", "data-slot": "pane", ...(run ? null : { ...pane(), overflow: 'hidden' }), flex: 1, minW: 0, minH: 0, children: plans ? null : _jsx(Builder, { host: host, rail: false }) })) })] })] }), drawer ? (_jsxs(_Fragment, { children: [_jsx(YStack, { render: "button", "aria-label": "Close sidebar", onPress: () => setDrawer(false), bg: DIM, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: "var(--z-overlay)", $md: { display: 'none' } }), _jsx(YStack, { "data-slot": "drawer", position: "absolute", t: INSET.top, b: INSET.bottom, l: INSET.left, width: "min(320px, calc(100% - 2 * var(--pane-gap) - 48px))", z: "var(--z-modal)", $md: { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: shut, onSeek: seek, drawer: true, places: sidebar, foot: _jsx(Rail, { host: host, mode: mode, at: where, open: true, flat: true, onChat: toChat, onDev: toDev, onStudio: toStudio, go: move, onOrg: org }) }) })] })) : null, finding ? _jsx(Palette, { go: move, onClose: found }) : null] }) }) }) }));
244
+ return (_jsx(AiProvider, { client: client ?? undefined, children: _jsx(HostProvider, { host: host, children: _jsx(PrefsProvider, { children: _jsxs(YStack, { ref: view, "data-slot": "window", ...(bar ? floor : null), flex: 1, minW: 0, height: "100dvh", overflow: "hidden", "$max-md": { position: 'fixed', t: TOP, l: 0, r: 0, height: HEIGHT }, children: [bar ? (_jsxs(XStack, { "data-slot": "title-bar", height: BAR, shrink: 0, items: "center", gap: 2, pl: (bar.inset ?? 0) + 6, pr: "$2", ...bar.drag, children: [_jsx(XStack, { "$max-md": { display: 'none' }, children: _jsx(Toggle, { open: !closed, onPress: flip }) }), _jsx(XStack, { $md: { display: 'none' }, children: _jsx(Toggle, { open: drawer, onPress: () => setDrawer(true), label: "Open sidebar" }) }), _jsx(Mark, { label: "Back", "data-slot": "back", onPress: () => router.back(), children: _jsx(ArrowLeft, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Forward", "data-slot": "forward", onPress: () => router.forward(), children: _jsx(ArrowRight, { size: 18, "aria-hidden": true }) }), _jsx(XStack, { flex: 1, self: "stretch", ...bar.drag })] })) : null, _jsxs(XStack, { "data-room": "", "data-app": mode, "data-slot": "frame", ...(bar ? null : floor), flex: 1, minW: 0, minH: 0, pr: "var(--pane-gap)", pb: "var(--pane-gap)", pt: bar ? 0 : 'var(--pane-gap)', "$max-md": { p: 0 }, children: [_jsx(YStack, { height: "100%", "$max-md": { display: 'none' }, children: _jsx(Rail, { host: host, mode: mode, open: !closed, toggle: bar ? undefined : flip, onChat: toChat, onDev: toDev, onStudio: toStudio, onOrg: org }) }), closed ? null : (_jsx(YStack, { "$max-md": { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: () => setClosed(true), onSeek: seek, places: sidebar }) })), _jsxs(YStack, { "data-slot": "stage", flex: 1, minW: 0, minH: 0, ml: "var(--pane-gap)", "$max-md": { ml: 0, pb: FOOT }, children: [bar ? null : _jsx(Top, { host: host, mode: mode, places: sidebar, go: move, onMenu: () => setDrawer(true), onChat: toChat, onDev: toDev }), _jsx(XStack, { "data-slot": "panes", flex: 1, minW: 0, minH: 0, children: mode === 'chat' ? (_jsx(Paned, { framed: true, children: _jsx(Chat, {}) })) : mode === 'studio' ? (_jsx(YStack, { render: "main", "data-slot": "pane", ...pane(), overflow: "hidden", flex: 1, minW: 0, minH: 0, children: _jsx(YStack, { flex: 1, minH: 0, height: "100%", children: _jsx(Suspense, { fallback: null, children: _jsx(Studio, { host: maker, rail: false }) }) }) })) : (_jsx(YStack, { render: "main", "data-slot": "pane", ...(run ? null : { ...pane(), overflow: 'hidden' }), flex: 1, minW: 0, minH: 0, children: plans ? null : _jsx(Builder, { host: host, rail: false }) })) })] })] }), drawer ? (_jsxs(_Fragment, { children: [_jsx(YStack, { render: "button", "aria-label": "Close sidebar", onPress: () => setDrawer(false), bg: DIM, position: "absolute", t: 0, r: 0, b: 0, l: 0, z: "var(--z-overlay)", $md: { display: 'none' } }), _jsx(YStack, { "data-slot": "drawer", position: "absolute", t: INSET.top, b: INSET.bottom, l: INSET.left, width: "min(320px, calc(100% - 2 * var(--pane-gap) - 48px))", z: "var(--z-modal)", $md: { display: 'none' }, children: _jsx(Column, { host: host, mode: mode, thread: thread, go: move, onShut: shut, onSeek: seek, drawer: true, places: sidebar, foot: _jsx(Rail, { host: host, mode: mode, open: true, flat: true, onChat: toChat, onDev: toDev, onStudio: toStudio, onOrg: org }) }) })] })) : null, finding ? _jsx(Palette, { go: move, onClose: found, mode: mode, host: host }) : null] }) }) }) }));
245
245
  }
package/dist/Chat.js CHANGED
@@ -42,6 +42,7 @@ import { AgentMaker, HOUSE, useAgentShelf, useCrew } from "./crew.js";
42
42
  import { called } from "./team.js";
43
43
  import { Anchor, Box, Button, Text, View, XStack, YStack } from "@hanzo/ui";
44
44
  import { useMedia } from "@hanzo/gui";
45
+ import { tidy } from "./lib/title.js";
45
46
  import { useIam, useIamToken } from "@hanzo/iam/react";
46
47
  import { bearer, hasSession, org } from "./lib/session.js";
47
48
  import { api } from "./lib/api.js";
@@ -540,12 +541,13 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
540
541
  const at = channel(opened);
541
542
  // The first of them: what a surface that addresses one person reads.
542
543
  const withWhom = room[0] ?? null;
543
- // THE SIDE PANEL (@hanzo/build `Panel`), the one Dev's runs open too: open or
544
- // shut as Chat was left on a laptop, a sheet asked for on a phone, its tabs
545
- // kept per conversation. The header's Panel button and ⌘. move it.
544
+ // THE SIDE PANEL (@hanzo/build `Panel`), the one Dev's runs open too: shut
545
+ // until the reader opens it or a preview lands (`see`, a link that frames),
546
+ // kept per conversation with its tabs, so a chat opens as it was left and
547
+ // never as the last chat was. The header's Panel button and ⌘. move it.
546
548
  const media = useMedia();
547
549
  const wide = media.lg;
548
- const [panel, setPanel] = usePanel("chat", false, wide);
550
+ const [panel, setPanel] = usePanel(`chat:${at}`, false, wide);
549
551
  const deck = useDeck(`chat:${at}`, SEED);
550
552
  // The org's sites, which an empty tab suggests.
551
553
  const sites = useSites();
@@ -1392,6 +1394,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1392
1394
  upload: () => take(at, via),
1393
1395
  agents: agentShelf,
1394
1396
  skills: carry.skills,
1397
+ design: carry.design,
1395
1398
  plugins: carry.plugins,
1396
1399
  research: { on: carry.gear.gear.research, set: (on) => carry.gear.set({ research: on }) },
1397
1400
  memory: { on: carry.gear.gear.memory, set: (on) => carry.gear.set({ memory: on }) },
@@ -1463,7 +1466,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1463
1466
  }, children: [_jsx(Schedule, { open: plan !== null, onOpenChange: (open) => {
1464
1467
  if (!open)
1465
1468
  setPlan(null);
1466
- }, prompt: plan ?? "", model: model, models: models.map((m) => ({ id: m.id, name: nameOf(m.id, listed) })) }), 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, { "data-slot": "conversation-head", height: 52, items: "center", justify: "space-between", gap: "$3", pl: "$4", pr: "$2", shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, flex: 1, children: [mine ? (_jsx(Text, { fontSize: 15, lineHeight: 20, color: "$ink", numberOfLines: 1, shrink: 1, minW: 0, children: mine.title || "Untitled" })) : null, 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, { "data-slot": "conversation-controls", items: "center", gap: 2, shrink: 0, children: [open ? _jsx(Share, { thread: open }) : null, mine ? (_jsx(ConversationMenu, { threads: alone, active: open, onSchedule: anonymous ? undefined : () => setPlan(lastAsked()), children: _jsx(XStack, { children: _jsx(ThreadMore, { t: mine }) }) })) : null, _jsx(Summary, { sections: sections, onOpen: () => setPanel(true) }), _jsx(PanelToggle, { open: panel, onToggle: () => setPanel(!panel) })] })] }), wide && aside !== null ? (_jsx(Beside, { children: _jsx(Panel, { side: deck, kinds: NONE, onHide: () => setPanel(false), label: "Tabs", ...browsing }) })) : null, !wide && panel ? _jsx(Panel, { side: deck, kinds: NONE, sheet: true, onHide: () => setPanel(false), label: "Tabs", ...browsing }) : null, isFree(model) ? (_jsxs(Text, { fontSize: "$2", color: "$soft", text: "center", maxW: MEASURE, mx: "auto", px: "$4", py: "$2", children: [freeCopy.notice, " ", _jsx(Anchor, { href: site("/legal/privacy"), children: freeCopy.noticeCta })] })) : null, _jsx(ChatSurface, { ...chat, messages: shown, send: guarded,
1469
+ }, prompt: plan ?? "", model: model, models: models.map((m) => ({ id: m.id, name: nameOf(m.id, listed) })) }), 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, { "data-slot": "conversation-head", height: 52, items: "center", justify: "space-between", gap: "$3", pl: "$4", pr: "$2", shrink: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", minW: 0, flex: 1, children: [mine ? (_jsx(Text, { fontSize: 15, lineHeight: 20, color: "$ink", numberOfLines: 1, shrink: 1, minW: 0, children: tidy(mine.title) })) : null, 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, { "data-slot": "conversation-controls", items: "center", gap: 2, shrink: 0, children: [open ? _jsx(Share, { thread: open }) : null, mine ? (_jsx(ConversationMenu, { threads: alone, active: open, onSchedule: anonymous ? undefined : () => setPlan(lastAsked()), children: _jsx(XStack, { children: _jsx(ThreadMore, { t: mine }) }) })) : null, _jsx(Summary, { sections: sections, onOpen: () => setPanel(true) }), _jsx(PanelToggle, { open: panel, onToggle: () => setPanel(!panel) })] })] }), wide && aside !== null ? (_jsx(Beside, { children: _jsx(Panel, { side: deck, kinds: NONE, onHide: () => setPanel(false), label: "Tabs", ...browsing }) })) : null, !wide && panel ? _jsx(Panel, { side: deck, kinds: NONE, sheet: true, onHide: () => setPanel(false), label: "Tabs", ...browsing }) : null, _jsx(ChatSurface, { ...chat, messages: shown, send: guarded,
1467
1470
  // AN EMPTY ROOM CENTRES ITS COMPOSER on a laptop. While nothing has been
1468
1471
  // said the opening (the heading) and the composer sit as one group in the
1469
1472
  // middle of the height; the moment a turn exists the transcript takes the
@@ -1530,7 +1533,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
1530
1533
  empty: heading === null ? undefined : (_jsxs(YStack, { "data-slot": "opening", items: "center", width: "100%", py: "$6", ...(media.sm ? null : { flex: 1, justify: "center" }), children: [room.length > 1 ? (_jsx(XStack, { items: "center", justify: "center", mb: "$4", children: room.map((name, i) => (_jsx(Box, { ml: i ? -16 : 0, rounded: 40, borderWidth: 2, borderColor: "$background", z: room.length - i, children: _jsx(Face, { src: faces[name]?.avatar, emoji: faces[name]?.emoji, name: name, size: 72 }) }, name))) })) : portrait ? (_jsx(Box, { width: 160, height: 160, rounded: 24, overflow: "hidden", borderWidth: 2, borderColor: "var(--white-15)", boxShadow: "0 10px 24px color-mix(in srgb, var(--pure-black) 70%, transparent)", mb: "$4", bg: "var(--neutral-900)", children: _jsx(View, { render: _jsx("img", { src: portrait, alt: withWhom }), width: "100%", height: "100%", objectFit: "cover" }) })) : null, room.length === 1 && !portrait ? (_jsx(Box, { mb: "$3", children: _jsx(Face, { emoji: mate?.emoji ?? persona?.emoji, name: withWhom, size: 72 }) })) : null, _jsx(Text, { render: "h1", fontSize: "$7", lineHeight: "$7", fontWeight: "700", color: "$ink", text: "center", mb: "$1", children: join(room) || heading || "What can I help with?" }), room.length === 1 && (mate?.description ?? persona?.description) ? (_jsx(Text, { fontSize: "$3", color: "$soft", text: "center", maxW: 500, mb: "$4", lineHeight: "$4", children: mate?.description ?? persona?.description })) : null] })),
1531
1534
  // The transcript reads at the same measure. `Thread` defaults to 768
1532
1535
  // itself; handing it the column here is what lets Width reach it.
1533
- column: { maxW: MEASURE }, composer: prompt({ ...composing, field: mention.field }, mention.state) }), _jsx(AgentMaker, {}), carry.dialog, isFree(model) ? (_jsx(XStack, { items: "center", justify: "center", py: "$1", px: "$3", borderTopWidth: 1, borderColor: "$borderColor", bg: "$background", children: _jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Free chat is data-shared to train models. ", _jsx(Anchor, { href: site("/privacy"), fontSize: "$1", color: "$ink", children: "Privacy" })] }) })) : null, consent, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
1536
+ column: { maxW: MEASURE }, composer: prompt({ ...composing, field: mention.field }, mention.state) }), _jsx(AgentMaker, {}), carry.dialog, _jsxs(XStack, { items: "center", justify: "center", py: "$1", px: "$3", gap: "$2", children: [_jsx(Anchor, { href: site("/legal/terms"), fontSize: "$1", color: "$soft", children: "Terms" }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "\u00B7" }), _jsx(Anchor, { href: site("/legal/privacy"), fontSize: "$1", color: "$soft", children: "Privacy" })] }), consent, _jsx(Upgrade, { ask: ask, onClose: () => setAsk(null) })] }));
1534
1537
  }
1535
1538
  /** The agreement free asks for, in @hanzo/ai's words so every surface says one thing. */
1536
1539
  function Consent({ question, onAgree, onCancel, }) {
package/dist/Column.js CHANGED
@@ -21,6 +21,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
21
21
  import { Text, XStack, YStack } from '@hanzo/gui';
22
22
  import { Bell, Search, Settings2, SquarePen } from 'lucide-react';
23
23
  import { DOTS, Grip, nav, NEW, path as written, useSessions } from '@hanzo/build';
24
+ import { tidy } from './lib/title.js';
24
25
  import { DropdownMenu } from '@hanzo/ui';
25
26
  import { Sidebar, SidebarIconButton, SidebarItem, StatusDot } from '@hanzo/ui/chat';
26
27
  import { useId } from 'react';
@@ -78,7 +79,7 @@ function Activity({ host }) {
78
79
  ...(latest.length
79
80
  ? latest.map((s) => ({
80
81
  key: s.id,
81
- label: s.title || 'Untitled run',
82
+ label: tidy(s.title, 56, 'Untitled run'),
82
83
  description: [s.status, ago(s.updatedAt || s.createdAt)].filter(Boolean).join(' · '),
83
84
  icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }),
84
85
  onSelect: () => host.go(s.id),
package/dist/Palette.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
+ import { type Host } from '@hanzo/build';
2
3
  import { type ShortcutKeys } from '@hanzo/ui';
4
+ import { type Place } from './lib/host.js';
3
5
  import { type Hit } from './seek.js';
4
6
  /** A row: what it is called, its glyph, and what pressing it does. */
5
7
  export interface Action extends Hit {
@@ -9,10 +11,14 @@ export interface Action extends Hit {
9
11
  /** Pressing it keeps the palette open. */
10
12
  stay?: boolean;
11
13
  }
12
- export declare function Palette({ onClose, go, actions, }: {
14
+ export declare function Palette({ onClose, go, actions, mode, host, }: {
13
15
  onClose: () => void;
14
16
  /** Moves the app to one of its addresses (lib/host `talk`, `app`). */
15
17
  go: (address: string) => void;
16
18
  /** The frame's own actions, after the palette's; one with a palette action's id stands in for it. */
17
19
  actions?: Action[];
20
+ /** The product the reader is in: what the empty field lists and what a query ranks first. */
21
+ mode?: Place['mode'];
22
+ /** Dev's host, whose runs the palette lists in Dev. */
23
+ host?: Host;
18
24
  }): import("react").JSX.Element;
package/dist/Palette.js CHANGED
@@ -1,13 +1,17 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // The one search. ⌘K / Ctrl+K from anywhere and the sidebar's search open it:
4
- // a dialog with the field on top, listing Recent chats while the field is empty
5
- // and, for a query, Chats, Projects, Agents, Skills and Actions, each cut to its
6
- // best five (seek.ts `find`). A row opens where it lives: a chat at its
7
- // address, a project in Dev, an agent in a new chat with it, a skill in Dev's
8
- // Customize, an action runs. Arrows move, Enter opens, Escape closes
9
- // (@hanzo/ui's CommandMenu); Keyboard shortcuts lists the app's keys, and
10
- // Backspace on an empty field comes back.
4
+ // a dialog with the field on top, listing the product's own recents while the
5
+ // field is empty — Chat's Recent chats, Dev's Recent runs — and, for a query,
6
+ // the product's own things first: in Chat, Chats, Projects, Agents, Skills and
7
+ // Actions; in Dev, Runs, Dev's places (Templates, Projects, Issues, Machines,
8
+ // Environments, Library, Scheduled tasks, Customize), Projects, Skills, Actions
9
+ // and Chats last; each cut to its best five (seek.ts `findIn`). A row opens
10
+ // where it lives: a chat at its address, a run or a place in Dev, a project in
11
+ // Dev, an agent in a new chat with it, a skill in Dev's Customize, an action
12
+ // runs. Arrows move, Enter opens, Escape closes (@hanzo/ui's CommandMenu);
13
+ // Keyboard shortcuts lists the app's keys, and Backspace on an empty field
14
+ // comes back.
11
15
  //
12
16
  // MOUNTED ONLY WHILE OPEN, and every list is read once as it opens, then
13
17
  // searched here as the reader types: the conversations store (hanzoai/agent)
@@ -18,9 +22,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
22
  // and the agents (crew.tsx) arrive while the reader types, and a read the
19
23
  // palette closes on is aborted.
20
24
  import { useEffect, useMemo, useState } from 'react';
21
- import { BookOpen, Bot, Code2, FolderGit2, Keyboard, LogOut, MessageCircle, Puzzle, Settings, SquarePen } from 'lucide-react';
25
+ import { BookOpen, Bot, Boxes, Clock, Code2, FolderGit2, Keyboard, LayoutTemplate, Library, ListTodo, LogOut, MessageCircle, Puzzle, Server, Settings, SquarePen } from 'lucide-react';
22
26
  import { useAi } from '@hanzo/ai/react';
23
- import { path as written } from '@hanzo/build';
27
+ import { path as written, useSessions } from '@hanzo/build';
24
28
  import { Dialog, DialogContent, DialogTitle } from '@hanzo/ui';
25
29
  import { CommandMenu } from '@hanzo/ui/product/CommandMenu';
26
30
  import { ordered } from './agents.js';
@@ -30,7 +34,8 @@ import { empty, openThread, withAgents } from './open.js';
30
34
  import { useThreadList } from './thread.js';
31
35
  import { useSignOut } from './lib/signout.js';
32
36
  import { app, talk } from './lib/host.js';
33
- import { find } from './seek.js';
37
+ import { tidy } from './lib/title.js';
38
+ import { find, findIn } from './seek.js';
34
39
  const ICON = { size: 16, 'aria-hidden': true };
35
40
  /** The app's keys, as Keyboard shortcuts lists them. */
36
41
  const KEYS = [
@@ -43,6 +48,17 @@ const KEYS = [
43
48
  { id: 'forward', label: 'Forward, in the desktop app', shortcut: ['Alt', 'ArrowRight'] },
44
49
  ];
45
50
  const SKILLS = written({ kind: 'customize', tab: 'skills' });
51
+ /** Dev's places, as its sidebar lists them, by the builder address each opens. */
52
+ const PLACES = [
53
+ { id: 'templates', label: 'Templates', at: '-/templates', icon: _jsx(LayoutTemplate, { ...ICON }) },
54
+ { id: 'projects', label: 'Projects', at: '-/projects', icon: _jsx(FolderGit2, { ...ICON }) },
55
+ { id: 'issues', label: 'Issues', at: '-/issues', icon: _jsx(ListTodo, { ...ICON }) },
56
+ { id: 'machines', label: 'Machines', at: '-/settings/machines', icon: _jsx(Server, { ...ICON }) },
57
+ { id: 'environments', label: 'Environments', at: '-/settings/environments', icon: _jsx(Boxes, { ...ICON }) },
58
+ { id: 'library', label: 'Library', at: '-/artifacts', icon: _jsx(Library, { ...ICON }) },
59
+ { id: 'scheduled', label: 'Scheduled tasks', at: '-/automations', icon: _jsx(Clock, { ...ICON }) },
60
+ { id: 'customize', label: 'Customize', at: '-/customize', icon: _jsx(Puzzle, { ...ICON }) },
61
+ ];
46
62
  /** A list read once as the palette opens; a refusal leaves it empty and the rest still answer. */
47
63
  function useRead(path, shape) {
48
64
  const ai = useAi();
@@ -69,7 +85,7 @@ const skills = (body) => rows(obj(body).tools)
69
85
  .map(obj)
70
86
  .filter((t) => str(t.name))
71
87
  .map((t) => ({ id: str(t.name), label: str(t.name).replace(/^skill_/, ''), keywords: [str(t.description)] }));
72
- export function Palette({ onClose, go, actions = [], }) {
88
+ export function Palette({ onClose, go, actions = [], mode = 'chat', host, }) {
73
89
  const [query, setQuery] = useState('');
74
90
  const [keys, setKeys] = useState(false);
75
91
  const { threads } = useThreadList();
@@ -77,6 +93,7 @@ export function Palette({ onClose, go, actions = [], }) {
77
93
  const made = useRead('/v1/projects', projects);
78
94
  const known = useRead('/v1/tool/skills?activated=true', skills);
79
95
  const signOut = useSignOut();
96
+ const runs = useSessions(host ?? null);
80
97
  const groups = useMemo(() => {
81
98
  const own = [
82
99
  { id: 'new-chat', label: 'New chat', icon: _jsx(SquarePen, { ...ICON }), run: () => (empty(), go(talk())) },
@@ -89,24 +106,46 @@ export function Palette({ onClose, go, actions = [], }) {
89
106
  const doing = [...own.map((a) => actions.find((b) => b.id === a.id) ?? a), ...actions.filter((b) => !own.some((a) => a.id === b.id))];
90
107
  const chats = threads.map((t) => ({
91
108
  id: `chat-${t.id}`,
92
- label: t.title || 'Untitled',
109
+ label: tidy(t.title),
93
110
  at: t.updatedAt,
94
111
  icon: _jsx(MessageCircle, { ...ICON }),
95
112
  run: () => (openThread(t.id), go(talk(t.id))),
96
113
  }));
97
- const shown = find(query, chats, [
98
- { id: 'projects', label: 'Projects', items: made.map((p) => ({ ...p, id: `project-${p.id}`, icon: _jsx(FolderGit2, { ...ICON }), run: () => go(app(p.id)) })) },
99
- {
100
- id: 'agents',
101
- label: 'Agents',
102
- items: ordered(agents).map((one) => {
103
- const name = called(one.name);
104
- return { id: `agent-${one.id || one.name}`, label: name, icon: _jsx(Bot, { ...ICON }), run: () => (withAgents([name]), go(talk())) };
105
- }),
106
- },
107
- { id: 'skills', label: 'Skills', items: known.map((s) => ({ ...s, id: `skill-${s.id}`, icon: _jsx(BookOpen, { ...ICON }), run: () => go(app(SKILLS)) })) },
108
- { id: 'actions', label: 'Actions', items: doing },
109
- ]);
114
+ const projectRows = { id: 'projects', label: 'Projects', items: made.map((p) => ({ ...p, id: `project-${p.id}`, icon: _jsx(FolderGit2, { ...ICON }), run: () => go(app(p.id)) })) };
115
+ const skillRows = { id: 'skills', label: 'Skills', items: known.map((s) => ({ ...s, id: `skill-${s.id}`, icon: _jsx(BookOpen, { ...ICON }), run: () => go(app(SKILLS)) })) };
116
+ const actionRows = { id: 'actions', label: 'Actions', items: doing };
117
+ const shown = mode === 'dev'
118
+ ? findIn(query, {
119
+ label: 'Recent runs',
120
+ items: [...(runs?.value ?? [])].map((s) => ({
121
+ id: `run-${s.id}`,
122
+ label: tidy(s.title, 80, 'Untitled run'),
123
+ at: s.updatedAt || s.createdAt,
124
+ keywords: [s.status ?? ''],
125
+ icon: _jsx(Code2, { ...ICON }),
126
+ run: () => go(app(s.id)),
127
+ })),
128
+ }, [
129
+ { id: 'runs', label: 'Runs', items: [...(runs?.value ?? [])].map((s) => ({ id: `run-${s.id}`, label: tidy(s.title, 80, 'Untitled run'), at: s.updatedAt || s.createdAt, keywords: [s.status ?? ''], icon: _jsx(Code2, { ...ICON }), run: () => go(app(s.id)) })) },
130
+ { id: 'places', label: 'Dev', items: PLACES.map((p) => ({ id: `place-${p.id}`, label: p.label, icon: p.icon, run: () => go(app(p.at)) })) },
131
+ projectRows,
132
+ skillRows,
133
+ actionRows,
134
+ { id: 'chats', label: 'Chats', items: chats },
135
+ ])
136
+ : find(query, chats, [
137
+ projectRows,
138
+ {
139
+ id: 'agents',
140
+ label: 'Agents',
141
+ items: ordered(agents).map((one) => {
142
+ const name = called(one.name);
143
+ return { id: `agent-${one.id || one.name}`, label: name, icon: _jsx(Bot, { ...ICON }), run: () => (withAgents([name]), go(talk())) };
144
+ }),
145
+ },
146
+ skillRows,
147
+ actionRows,
148
+ ]);
110
149
  return shown.map((g) => ({
111
150
  ...g,
112
151
  items: g.items.map((a) => ({
@@ -123,9 +162,9 @@ export function Palette({ onClose, go, actions = [], }) {
123
162
  },
124
163
  })),
125
164
  }));
126
- }, [query, threads, agents, made, known, actions, go, onClose, signOut]);
165
+ }, [query, threads, agents, made, known, actions, go, onClose, signOut, mode, runs]);
127
166
  const listed = keys
128
167
  ? [{ id: 'keys', label: 'Keyboard shortcuts', items: KEYS.map((k) => ({ ...k, id: `key-${k.id}`, onSelect: () => setKeys(false) })) }]
129
168
  : groups;
130
- return (_jsx(Dialog, { open: true, onOpenChange: (open) => (open ? null : onClose()), children: _jsxs(DialogContent, { "data-slot": "palette", p: 0, gap: 0, width: "100%", maxW: 680, height: "min(520px, calc(100dvh - 32px))", rounded: "$6", overflow: "hidden", "$max-md": { width: '100vw', maxW: '100vw', height: '100dvh', rounded: 0, borderWidth: 0 }, children: [_jsx(DialogTitle, { position: "absolute", width: 1, height: 1, overflow: "hidden", opacity: 0, children: "Search" }), _jsx(CommandMenu, { groups: listed, query: query, onQueryChange: setQuery, placeholder: "Search\u2026", label: "Search", empty: keys ? 'No such key.' : query.trim() ? `Nothing matches “${query.trim()}”.` : 'No chats yet.', onBack: keys ? () => setKeys(false) : undefined, onClose: onClose })] }) }));
169
+ return (_jsx(Dialog, { open: true, onOpenChange: (open) => (open ? null : onClose()), children: _jsxs(DialogContent, { "data-slot": "palette", p: 0, gap: 0, width: "100%", maxW: 680, height: "min(520px, calc(100dvh - 32px))", rounded: "$6", overflow: "hidden", "$max-md": { width: '100vw', maxW: '100vw', height: '100dvh', rounded: 0, borderWidth: 0 }, children: [_jsx(DialogTitle, { position: "absolute", width: 1, height: 1, overflow: "hidden", opacity: 0, children: "Search" }), _jsx(CommandMenu, { groups: listed, query: query, onQueryChange: setQuery, placeholder: "Search\u2026", label: "Search", empty: keys ? 'No such key.' : query.trim() ? `Nothing matches “${query.trim()}”.` : mode === 'dev' ? 'No runs yet.' : 'No chats yet.', onBack: keys ? () => setKeys(false) : undefined, onClose: onClose })] }) }));
131
170
  }
package/dist/Rail.d.ts CHANGED
@@ -18,11 +18,9 @@ export declare function Toggle({ open, onPress, label }: {
18
18
  onPress: () => void;
19
19
  label?: string;
20
20
  }): import("react").JSX.Element;
21
- export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev, onStudio, go, onOrg, }: {
21
+ export declare function Rail({ host, mode, open, toggle, flat, onChat, onDev, onStudio, onOrg, }: {
22
22
  host: Host;
23
23
  mode: Place['mode'];
24
- /** Dev's builder address, or null in Chat. */
25
- at: string | null;
26
24
  /** The sidebar beside the rail is open. */
27
25
  open: boolean;
28
26
  /** Lead with the sidebar's toggle; absent where the host's bar holds it. */
@@ -33,8 +31,6 @@ export declare function Rail({ host, mode, at, open, toggle, flat, onChat, onDev
33
31
  onChat: () => void;
34
32
  onDev: () => void;
35
33
  onStudio: () => void;
36
- /** Moves the app to an address of this page. */
37
- go: (address: string) => void;
38
34
  /** Work in another organization. */
39
35
  onOrg: (org: string) => void;
40
36
  }): import("react").JSX.Element;
package/dist/Rail.js CHANGED
@@ -1,17 +1,19 @@
1
1
  'use client';
2
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // The app's rail: its products as marks down the window's left edge, in one
4
- // order — Chat, Dev, Studio, Library, Scheduled tasks, Customize — then More, a
5
- // menu of the rest, and the account at the foot. It is how the app moves
6
- // between its products; a phone, which has no rail, switches Chat and Dev in
7
- // its bar (Top.tsx).
4
+ // order — Chat, Dev, Studio — then More, a menu of the products on hosts of
5
+ // their own, and the account at the foot. It is how the app moves between its
6
+ // products; a phone, which has no rail, switches Chat and Dev in its bar
7
+ // (Top.tsx).
8
8
  //
9
9
  // Studio is the app's third mode (@hanzo/studio at /studio), each of the three
10
10
  // opened where the reader left it.
11
11
  //
12
- // Library, Scheduled tasks and Customize are Dev's own places (@hanzo/build's
13
- // Artifacts, Automations and Customize), raised to the rail; each is lit while
14
- // the reader stands on it, and Dev is lit for the rest of Dev.
12
+ // A mark is a product, and a product owns the sidebar beside the rail: Chat its
13
+ // conversations, Dev its places (@hanzo/build's New, Projects, Issues, Library,
14
+ // Templates, Scheduled tasks, Machines, Environments, Customize, Docs) and its
15
+ // runs, Studio its own. Nothing of Dev's is raised to the rail, and nothing of
16
+ // the rail's is listed inside Dev: one level of the app a place.
15
17
  //
16
18
  // It stands on the frame's floor, never cut as a pane (ground.ts): the rail and
17
19
  // the sidebar beside it are the window, and the content is the panel inset in
@@ -20,35 +22,17 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
20
22
  // The sidebar's toggle leads it (`toggle`) where the host has no bar of its own:
21
23
  // a browser tab. A host that is a window draws the toggle in its bar beside Back
22
24
  // and Forward (App.tsx `bar`), and its rail starts with Chat. On a phone the
23
- // rail lies along the foot of the drawer (`flat`), the account at its end; the
24
- // drawer is too narrow for every mark, so Library, Scheduled tasks and
25
- // Customize lead More there.
26
- import { Boxes, Clock, Code2, Ellipsis, FolderGit2, LayoutTemplate, Library, ListTodo, MessageSquare, PanelLeft, Puzzle, Server, SquareTerminal, Users, WandSparkles } from 'lucide-react';
25
+ // rail lies along the foot of the drawer (`flat`), the account at its end.
26
+ import { Code2, Ellipsis, MessageSquare, PanelLeft, SquareTerminal, Users, WandSparkles } from 'lucide-react';
27
27
  import { XStack, YStack } from '@hanzo/gui';
28
- import { route } from '@hanzo/build';
29
28
  import { DropdownMenu } from '@hanzo/ui';
30
29
  import { out } from './help.js';
31
30
  import { Me } from './Me.js';
32
- import { app, TABS, WORKSPACE } from './lib/host.js';
31
+ import { TABS, WORKSPACE } from './lib/host.js';
33
32
  /** The rail's width, px. */
34
33
  export const RAIL = 48;
35
34
  /** A mark, px: a 36px square on a 44px pitch. */
36
35
  const MARK = 36;
37
- /** The places the rail raises out of Dev, by the builder address each opens. */
38
- const RAISED = { library: '-/artifacts', scheduled: '-/automations', customize: '-/customize' };
39
- /** Which raised place a builder address stands on, if any. */
40
- function raised(at) {
41
- if (at === null)
42
- return null;
43
- const r = route(at);
44
- if (r.kind === 'screen' && r.screen === 'artifacts')
45
- return 'library';
46
- if ((r.kind === 'screen' && r.screen === 'automations') || r.kind === 'automation')
47
- return 'scheduled';
48
- if (r.kind === 'customize')
49
- return 'customize';
50
- return null;
51
- }
52
36
  /** One mark: an icon button named for where it goes, its name its tooltip, lit where the reader is. */
53
37
  export function Mark({ label, on = false, onPress, children, ...rest }) {
54
38
  return (_jsx(XStack, { render: "button", "aria-label": label, ...{ title: label }, "aria-current": on ? 'page' : undefined, onPress: onPress, width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", shrink: 0, cursor: "pointer", opacity: on ? 1 : 0.62, bg: on ? '$edge' : 'transparent', hoverStyle: { bg: on ? '$edge' : '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' },
@@ -59,28 +43,12 @@ export function Mark({ label, on = false, onPress, children, ...rest }) {
59
43
  export function Toggle({ open, onPress, label }) {
60
44
  return (_jsx(Mark, { label: label ?? (open ? 'Close sidebar' : 'Open sidebar'), "data-slot": "sidebar-toggle", "aria-expanded": open, onPress: onPress, children: _jsx(PanelLeft, { size: 18, "aria-hidden": true }) }));
61
45
  }
62
- export function Rail({ host, mode, at, open, toggle, flat = false, onChat, onDev, onStudio, go, onOrg, }) {
63
- const here = mode === 'dev' ? raised(at) : null;
64
- const to = (p) => () => go(app(p));
65
- // Everything else the app reaches, behind More: on a phone the raised places first, then Dev's other places, then the products on hosts of their own.
46
+ export function Rail({ host, mode, open, toggle, flat = false, onChat, onDev, onStudio, onOrg, }) {
47
+ // The products on hosts of their own, behind More.
66
48
  const rest = [
67
- ...(flat
68
- ? [
69
- { key: 'library', label: 'Library', icon: _jsx(Library, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.library) },
70
- { key: 'scheduled', label: 'Scheduled tasks', icon: _jsx(Clock, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.scheduled) },
71
- { key: 'customize', label: 'Customize', icon: _jsx(Puzzle, { size: 16, "aria-hidden": true }), onSelect: to(RAISED.customize) },
72
- { type: 'separator', key: 'raised' },
73
- ]
74
- : []),
75
- { key: 'projects', label: 'Projects', icon: _jsx(FolderGit2, { size: 16, "aria-hidden": true }), onSelect: to('-/projects') },
76
- { key: 'templates', label: 'Templates', icon: _jsx(LayoutTemplate, { size: 16, "aria-hidden": true }), onSelect: to('-/templates') },
77
- { key: 'issues', label: 'Issues', icon: _jsx(ListTodo, { size: 16, "aria-hidden": true }), onSelect: to('-/issues') },
78
- { key: 'machines', label: 'Machines', icon: _jsx(Server, { size: 16, "aria-hidden": true }), onSelect: to('-/settings/machines') },
79
- { key: 'environments', label: 'Environments', icon: _jsx(Boxes, { size: 16, "aria-hidden": true }), onSelect: to('-/settings/environments') },
80
- { type: 'separator', key: 'away' },
81
49
  { key: 'tabs', label: 'Tabs', icon: _jsx(SquareTerminal, { size: 16, "aria-hidden": true }), onSelect: () => out(TABS) },
82
50
  { key: 'team', label: 'Team', icon: _jsx(Users, { size: 16, "aria-hidden": true }), onSelect: () => out(WORKSPACE) },
83
51
  ];
84
52
  const Stack = flat ? XStack : YStack;
85
- return (_jsxs(Stack, { "data-slot": "rail", render: "nav", "aria-label": "Products", ...(flat ? { pt: '$2', mt: '$2', borderTopWidth: 1, borderColor: '$borderColor', gap: '$1' } : { width: RAIL, flex: 1, pt: '$2', pb: '$2', gap: 8 }), shrink: 0, items: "center", children: [toggle ? (_jsx(YStack, { pb: 8, children: _jsx(Toggle, { open: open, onPress: toggle }) })) : null, _jsx(Mark, { label: "Chat", on: mode === 'chat', onPress: onChat, "data-slot": "rail-chat", children: _jsx(MessageSquare, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Dev", on: mode === 'dev' && !here, onPress: onDev, "data-slot": "rail-dev", children: _jsx(Code2, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Studio", on: mode === 'studio', onPress: onStudio, "data-slot": "rail-studio", children: _jsx(WandSparkles, { size: 18, "aria-hidden": true }) }), flat ? null : (_jsxs(_Fragment, { children: [_jsx(Mark, { label: "Library", on: here === 'library', onPress: to(RAISED.library), "data-slot": "rail-library", children: _jsx(Library, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Scheduled tasks", on: here === 'scheduled', onPress: to(RAISED.scheduled), "data-slot": "rail-scheduled", children: _jsx(Clock, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Customize", on: here === 'customize', onPress: to(RAISED.customize), "data-slot": "rail-customize", children: _jsx(Puzzle, { size: 18, "aria-hidden": true }) })] })), _jsx(DropdownMenu, { placement: flat ? 'top-start' : 'right-start', minWidth: 220, trigger: _jsx(XStack, { render: "button", "aria-label": "More", ...{ title: 'More' }, "data-slot": "rail-more", width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", cursor: "pointer", opacity: 0.62, hoverStyle: { bg: '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, $touchable: { width: 44, height: 44 }, children: _jsx(Ellipsis, { size: 18, "aria-hidden": true }) }), items: rest }), _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, above: flat, onOrg: onOrg })] }));
53
+ return (_jsxs(Stack, { "data-slot": "rail", render: "nav", "aria-label": "Products", ...(flat ? { pt: '$2', mt: '$2', borderTopWidth: 1, borderColor: '$borderColor', gap: '$1' } : { width: RAIL, flex: 1, pt: '$2', pb: '$2', gap: 8 }), shrink: 0, items: "center", children: [toggle ? (_jsx(YStack, { pb: 8, children: _jsx(Toggle, { open: open, onPress: toggle }) })) : null, _jsx(Mark, { label: "Chat", on: mode === 'chat', onPress: onChat, "data-slot": "rail-chat", children: _jsx(MessageSquare, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Dev", on: mode === 'dev', onPress: onDev, "data-slot": "rail-dev", children: _jsx(Code2, { size: 18, "aria-hidden": true }) }), _jsx(Mark, { label: "Studio", on: mode === 'studio', onPress: onStudio, "data-slot": "rail-studio", children: _jsx(WandSparkles, { size: 18, "aria-hidden": true }) }), _jsx(DropdownMenu, { placement: flat ? 'top-start' : 'right-start', minWidth: 220, trigger: _jsx(XStack, { render: "button", "aria-label": "More", ...{ title: 'More' }, "data-slot": "rail-more", width: MARK, height: MARK, rounded: "$4", items: "center", justify: "center", cursor: "pointer", opacity: 0.62, hoverStyle: { bg: '$hover', opacity: 1 }, pressStyle: { bg: '$raised' }, focusVisibleStyle: { outlineWidth: 2, outlineStyle: 'solid', outlineColor: '$outlineColor' }, $touchable: { width: 44, height: 44 }, children: _jsx(Ellipsis, { size: 18, "aria-hidden": true }) }), items: rest }), _jsx(YStack, { flex: 1 }), _jsx(Me, { host: host, narrow: true, above: flat, onOrg: onOrg })] }));
86
54
  }
package/dist/Shell.js CHANGED
@@ -77,6 +77,7 @@ import { netKey } from "./conversations.js";
77
77
  import { FIRST, SettingsList, } from "./Settings.js";
78
78
  import { Me } from "./Me.js";
79
79
  import { Palette } from "./Palette.js";
80
+ import { tidy } from "./lib/title.js";
80
81
  import { summons } from "./seek.js";
81
82
  import { called } from "./team.js";
82
83
  import { checkoutUrl } from './lib/pay.js';
@@ -393,7 +394,7 @@ function Chats({ filter = "", onPick, }) {
393
394
  const mergedThreads = useMemo(() => {
394
395
  const list = threads.map((t) => ({
395
396
  id: t.id,
396
- title: t.title || "Untitled",
397
+ title: tidy(t.title),
397
398
  updatedAt: t.updatedAt,
398
399
  thread: t,
399
400
  }));
@@ -409,7 +410,7 @@ function Chats({ filter = "", onPick, }) {
409
410
  }
410
411
  return list;
411
412
  }, [threads, customThreads]);
412
- const found = mergedThreads.filter((t) => matches(t.title || "Untitled", filter));
413
+ const found = mergedThreads.filter((t) => matches(tidy(t.title), filter));
413
414
  const byId = useMemo(() => new Map(threads.map((t) => [t.id, t])), [threads]);
414
415
  // PINNED FIRST, by the server's pin (thread.tsx), then the days.
415
416
  const shown = found.filter((t) => !t.thread?.pinned);
@@ -433,7 +434,7 @@ function Chats({ filter = "", onPick, }) {
433
434
  const item = (_jsx(SidebarItem, { active: t.id === thread, ...(t.thread ? { pr: "$8", $touchable: { pr: 52 } } : null), onPress: () => {
434
435
  open(t.id);
435
436
  onPick?.();
436
- }, children: _jsxs(YStack, { minW: 0, children: [_jsxs(XStack, { items: "center", gap: "$1.5", minW: 0, children: [t.kind === "call" ? _jsx(Video, { size: 13, color: "var(--muted-foreground)", "aria-label": "Call" }) : null, t.kind === "group" ? _jsx(Users, { size: 13, color: "var(--muted-foreground)", "aria-label": "Group" }) : null, _jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: t.title || "Untitled" })] }), _jsx(Span, { id: t.id, updatedAt: t.updatedAt })] }) }, t.id));
437
+ }, children: _jsxs(YStack, { minW: 0, children: [_jsxs(XStack, { items: "center", gap: "$1.5", minW: 0, children: [t.kind === "call" ? _jsx(Video, { size: 13, color: "var(--muted-foreground)", "aria-label": "Call" }) : null, t.kind === "group" ? _jsx(Users, { size: 13, color: "var(--muted-foreground)", "aria-label": "Group" }) : null, _jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: tidy(t.title) })] }), _jsx(Span, { id: t.id, updatedAt: t.updatedAt })] }) }, t.id));
437
438
  // A stored conversation carries its menu; a call or a group is no conversation of the store's.
438
439
  return t.thread ? (_jsx(ThreadRow, { t: t.thread, active: t.id === thread, listed: false, children: item }, t.id)) : (item);
439
440
  }) }, band.label)))] }) }), _jsx(SharedWithYou, { filter: filter, onPick: onPick })] }));
@@ -455,13 +456,13 @@ function SharedWithYou({ filter, onPick }) {
455
456
  on = false;
456
457
  };
457
458
  }, []);
458
- const shown = (items ?? []).filter((one) => matches(one.title || "Untitled", filter));
459
+ const shown = (items ?? []).filter((one) => matches(tidy(one.title), filter));
459
460
  if (shown.length === 0)
460
461
  return null;
461
462
  return (_jsx(SidebarSection, { label: "Shared with you", children: shown.map((one) => (_jsx(SidebarItem, { onPress: () => {
462
463
  onPick?.();
463
464
  window.location.assign(address(one.share));
464
- }, children: _jsxs(YStack, { minW: 0, children: [_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: one.title || "Untitled" }), one.by ? (_jsxs(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: ["From ", one.by] })) : null] }) }, one.share))) }));
465
+ }, children: _jsxs(YStack, { minW: 0, children: [_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: tidy(one.title) }), one.by ? (_jsxs(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: ["From ", one.by] })) : null] }) }, one.share))) }));
465
466
  }
466
467
  /**
467
468
  * The projects. A run is something that happened, a project is the thing it
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A title fit to show: a conversation's or a run's title as the server kept it,
3
+ * less the markup a first message can carry (`<workspace-files>{…}</workspace-files>`,
4
+ * a lone tag, a leading JSON object), its whitespace one space, cut to `max`
5
+ * characters with an ellipsis; `fallback` when nothing readable is left.
6
+ */
7
+ export declare function tidy(title: string | null | undefined, max?: number, fallback?: string): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * A title fit to show: a conversation's or a run's title as the server kept it,
3
+ * less the markup a first message can carry (`<workspace-files>{…}</workspace-files>`,
4
+ * a lone tag, a leading JSON object), its whitespace one space, cut to `max`
5
+ * characters with an ellipsis; `fallback` when nothing readable is left.
6
+ */
7
+ export function tidy(title, max = 80, fallback = 'Untitled') {
8
+ let t = (title ?? '').replace(/<([a-z][\w-]*)[^>]*>[\s\S]*?<\/\1>/gi, ' ').replace(/<\/?[a-z][\w-]*[^>]*>/gi, ' ');
9
+ // A JSON object the message opened with, balanced, goes whole.
10
+ const m = t.match(/^\s*\{/);
11
+ if (m) {
12
+ let depth = 0;
13
+ let quoted = false;
14
+ for (let i = m[0].length - 1; i < t.length; i++) {
15
+ const c = t[i];
16
+ if (quoted) {
17
+ if (c === '\\')
18
+ i++;
19
+ else if (c === '"')
20
+ quoted = false;
21
+ }
22
+ else if (c === '"')
23
+ quoted = true;
24
+ else if (c === '{')
25
+ depth++;
26
+ else if (c === '}' && --depth === 0) {
27
+ t = t.slice(i + 1);
28
+ break;
29
+ }
30
+ }
31
+ }
32
+ t = t.replace(/\s+/g, ' ').trim();
33
+ if (!t)
34
+ return fallback;
35
+ return t.length > max ? `${t.slice(0, max - 1).trimEnd()}…` : t;
36
+ }
package/dist/seek.d.ts CHANGED
@@ -33,5 +33,14 @@ export declare function rank<T extends Hit>(rows: T[], query: string, cap?: numb
33
33
  * `CAP`, a group with nothing left out.
34
34
  */
35
35
  export declare function find<T extends Hit>(query: string, chats: T[], groups: Group<T>[]): Group<T>[];
36
+ /**
37
+ * The same search for any product: an empty field lists the newest `RECENT` of
38
+ * `recent` under its label (a product's own recents: Chat's conversations, Dev's
39
+ * runs), and a query ranks every group in the order given.
40
+ */
41
+ export declare function findIn<T extends Hit>(query: string, recent: {
42
+ label: string;
43
+ items: T[];
44
+ }, groups: Group<T>[]): Group<T>[];
36
45
  /** Whether a key press opens the palette: ⌘K or Ctrl+K, once a press, from anywhere, a field included. */
37
46
  export declare const summons: (e: Press) => boolean;
package/dist/seek.js CHANGED
@@ -43,13 +43,19 @@ export function rank(rows, query, cap = CAP) {
43
43
  * `CAP`, a group with nothing left out.
44
44
  */
45
45
  export function find(query, chats, groups) {
46
+ return findIn(query, { label: 'Recent chats', items: chats }, [{ id: 'chats', label: 'Chats', items: chats }, ...groups]);
47
+ }
48
+ /**
49
+ * The same search for any product: an empty field lists the newest `RECENT` of
50
+ * `recent` under its label (a product's own recents: Chat's conversations, Dev's
51
+ * runs), and a query ranks every group in the order given.
52
+ */
53
+ export function findIn(query, recent, groups) {
46
54
  if (!query.trim()) {
47
- const newest = [...chats].sort((a, b) => (b.at ?? '').localeCompare(a.at ?? '')).slice(0, RECENT);
48
- return newest.length ? [{ id: 'recent', label: 'Recent chats', items: newest }] : [];
55
+ const newest = [...recent.items].sort((a, b) => (b.at ?? '').localeCompare(a.at ?? '')).slice(0, RECENT);
56
+ return newest.length ? [{ id: 'recent', label: recent.label, items: newest }] : [];
49
57
  }
50
- return [{ id: 'chats', label: 'Chats', items: chats }, ...groups]
51
- .map((g) => ({ ...g, items: rank(g.items, query) }))
52
- .filter((g) => g.items.length);
58
+ return groups.map((g) => ({ ...g, items: rank(g.items, query) })).filter((g) => g.items.length);
53
59
  }
54
60
  /** Whether a key press opens the palette: ⌘K or Ctrl+K, once a press, from anywhere, a field included. */
55
61
  export const summons = (e) => (e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && !e.repeat && !e.isComposing && (e.key ?? '').toLowerCase() === 'k';
package/dist/thread.js CHANGED
@@ -20,6 +20,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
20
20
  // flight over what it last read, and reads the store again once it answers.
21
21
  // A pin is the server's, never this browser's: it follows the person to every
22
22
  // device they sign in on.
23
+ import { tidy } from './lib/title.js';
23
24
  import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
24
25
  import { useAi } from '@hanzo/ai/react';
25
26
  import { Archive, ArchiveRestore, Clock, ExternalLink, Link2, MoreHorizontal, Pencil, Pin, PinOff, Share2, Trash2 } from 'lucide-react';
@@ -128,7 +129,7 @@ export function useThreadActions() {
128
129
  };
129
130
  }, [ai]);
130
131
  }
131
- const titleOf = (t) => t.title || 'Untitled';
132
+ const titleOf = (t) => tidy(t.title);
132
133
  /** A conversation's own address, whole: the app at `/?chat=<id>` (lib/host `talk`), wherever the rooms are drawn. */
133
134
  export const href = (id) => new URL(site(talk(id)), window.location.href).href;
134
135
  /** Opens a conversation in a tab of its own. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/rooms",
3
- "version": "0.1.81",
3
+ "version": "0.1.83",
4
4
  "description": "The workspace rooms — Home, Chat, Inbox, Contacts, Meet, Cal, Drive, Board, Work, Bots — and the frame that holds them, once, for every Hanzo surface.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,7 +49,7 @@
49
49
  "peerDependencies": {
50
50
  "@hanzo/ai": ">=0.6.23",
51
51
  "@hanzo/appearance": ">=0.2.6",
52
- "@hanzo/build": ">=0.2.68",
52
+ "@hanzo/build": ">=0.2.69",
53
53
  "@hanzo/composer": ">=0.2.0",
54
54
  "@hanzo/event": ">=0.3.56",
55
55
  "@hanzo/gui": "^8.3.5",
@@ -67,7 +67,7 @@
67
67
  "devDependencies": {
68
68
  "@hanzo/ai": "0.6.23",
69
69
  "@hanzo/appearance": "0.2.8",
70
- "@hanzo/build": "0.2.68",
70
+ "@hanzo/build": "0.2.69",
71
71
  "@hanzo/composer": "0.2.0",
72
72
  "@hanzo/design": "0.5.34",
73
73
  "@hanzo/event": "0.3.56",