@hanzo/rooms 0.1.33 → 0.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/Chat.js +125 -15
- package/dist/RightPane.d.ts +35 -0
- package/dist/RightPane.js +14 -8
- package/dist/dev.js +9 -9
- package/dist/host.d.ts +9 -5
- package/dist/host.js +5 -4
- package/dist/lib/host.d.ts +20 -15
- package/dist/lib/host.js +41 -26
- package/dist/lib/session.js +7 -6
- package/dist/pane.d.ts +2 -0
- package/dist/pane.js +12 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,13 +11,13 @@ import { CalScheduler } from '@hanzo/rooms/CalScheduler'
|
|
|
11
11
|
|
|
12
12
|
configure({ site: '', home: '/home', signUp: '/signup' })
|
|
13
13
|
|
|
14
|
-
<Rooms router={router} search={search} route={route} Link={Link}>
|
|
14
|
+
<Rooms router={router} search={search} path={pathname} route={route} Link={Link}>
|
|
15
15
|
<Room mode="cal"><CalScheduler /></Room>
|
|
16
16
|
</Rooms>
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
`configure()` names the host's addresses (where its own pages live, the
|
|
20
20
|
gateway and IAM, the plan catalogue to paint first). `<Rooms>` carries what
|
|
21
|
-
moves: the router, the query, the route and the link component. The rooms
|
|
21
|
+
moves: the router, the query, the path, the route and the link component. The rooms
|
|
22
22
|
import nothing from a host, and their look comes from `@hanzo/appearance` and
|
|
23
23
|
`@hanzo/design` through `@hanzo/ui`.
|
package/dist/Chat.js
CHANGED
|
@@ -34,8 +34,8 @@ import { useAgent, useAi, useChat, useModels, } from "@hanzo/ai/react";
|
|
|
34
34
|
import { freeCopy, grantConsent, hasConsent, isFree, } from "@hanzo/ai";
|
|
35
35
|
import { ASK, Chat as ChatSurface, Code, Failure, Parts, words } from "@hanzo/ui/chat";
|
|
36
36
|
import { carriedIn, compose, partsOf, said as told, weigh } from "./lib/attach.js";
|
|
37
|
-
import { dropped, pct, readable, refOf, retrieve } from "./lib/files.js";
|
|
38
|
-
import { channel, drop, hold, landed, spend, take, usePane, useWorkFile } from "./pane.js";
|
|
37
|
+
import { download, dropped, fileOf, pct, readable, refOf, retrieve } from "./lib/files.js";
|
|
38
|
+
import { channel, drop, hold, landed, localOf, spend, take, usePane, useWorkFile } from "./pane.js";
|
|
39
39
|
import { Face, brief as roomBrief, join, roleOf, roomTurn, roster as rosterLine, rosterOf, speakers, speaksOf, voiceOf } from "./cast.js";
|
|
40
40
|
import { SpeakButton, cleanForSpeech } from "./speech.js";
|
|
41
41
|
import { Crew, HOUSE, useCrew } from "./crew.js";
|
|
@@ -52,12 +52,13 @@ 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, Image as ImageMark, Mic, Paperclip, Square, Star, PanelRight, X } from "lucide-react";
|
|
55
|
+
import { ArrowUp, AudioLines, FileText, Image as ImageMark, Mic, Paperclip, Square, Star, 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";
|
|
59
59
|
import { pane } from "./ground.js";
|
|
60
|
-
import { RightPane, usePinned } from "./RightPane.js";
|
|
60
|
+
import { RightPane, addressOf, look, usePinned } from "./RightPane.js";
|
|
61
|
+
import { useMedia } from "@hanzo/gui";
|
|
61
62
|
import { Take } from "./copy.js";
|
|
62
63
|
import { Share } from "./Share.js";
|
|
63
64
|
import { useStarred } from "./stars.js";
|
|
@@ -351,27 +352,113 @@ const STAGE = {
|
|
|
351
352
|
graph: "linking",
|
|
352
353
|
embed: "vectors",
|
|
353
354
|
};
|
|
354
|
-
/**
|
|
355
|
-
function
|
|
355
|
+
/** How a file is drawn: a picture, a PDF's tile, or a chip. Read off its type, else its name, since a store may answer with neither. */
|
|
356
|
+
function kindOf(name, type) {
|
|
357
|
+
if (type.startsWith("image/") || /\.(png|jpe?g|gif|webp|avif|svg)$/i.test(name))
|
|
358
|
+
return "image";
|
|
359
|
+
if (type === "application/pdf" || /\.pdf$/i.test(name))
|
|
360
|
+
return "pdf";
|
|
361
|
+
return "file";
|
|
362
|
+
}
|
|
363
|
+
// SIGNED ADDRESSES FOR PICTURES, by workspace file: a list of turns mounts and
|
|
364
|
+
// remounts its chips, and asking the store each time would be a request per
|
|
365
|
+
// picture per redraw. Kept for less than a signature lives (the store's are
|
|
366
|
+
// five minutes), so a chip mounted later reads a fresh one; a picture already
|
|
367
|
+
// drawn keeps its pixels whatever becomes of its address.
|
|
368
|
+
const signed = new Map();
|
|
369
|
+
const SIGNED_FOR = 4 * 60_000;
|
|
370
|
+
function signedOf(api, id, file) {
|
|
371
|
+
const kept = signed.get(id);
|
|
372
|
+
if (kept && Date.now() - kept.at < SIGNED_FOR)
|
|
373
|
+
return kept.got;
|
|
374
|
+
const got = (file?.bucket && file.key ? Promise.resolve(file) : fileOf(api, id)).then((f) => download(api, f.bucket, f.key));
|
|
375
|
+
signed.set(id, { at: Date.now(), got });
|
|
376
|
+
got.catch(() => signed.delete(id));
|
|
377
|
+
return got;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Where a picture's thumbnail reads from: the bytes on this machine where this
|
|
381
|
+
* document holds them (the held file's object URL, which outlives its send),
|
|
382
|
+
* else a signed address for the workspace's copy — a file taken from Drive, or
|
|
383
|
+
* a turn read back after a reload. '' until there is one.
|
|
384
|
+
*/
|
|
385
|
+
function usePicture(on, href, id, file) {
|
|
386
|
+
const api = useAi();
|
|
387
|
+
const near = on ? href || (id ? localOf(id) : "") : "";
|
|
388
|
+
const [far, setFar] = useState("");
|
|
389
|
+
const bucket = file?.bucket;
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (!on || near || !id)
|
|
392
|
+
return;
|
|
393
|
+
let live = true;
|
|
394
|
+
signedOf(api, id, file).then((url) => live && setFar(url), () => { });
|
|
395
|
+
return () => {
|
|
396
|
+
live = false;
|
|
397
|
+
};
|
|
398
|
+
// The file's record moves with every stage of its ingest; its address does not.
|
|
399
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
400
|
+
}, [on, near, id, bucket, api]);
|
|
401
|
+
return near || far;
|
|
402
|
+
}
|
|
403
|
+
/** Edge of a picture's or a PDF's thumbnail, in px. */
|
|
404
|
+
const THUMB = 48;
|
|
405
|
+
/**
|
|
406
|
+
* One file put into a message.
|
|
407
|
+
*
|
|
408
|
+
* A PICTURE SHOWS ITSELF and a PDF shows a tile, each with its name and size
|
|
409
|
+
* beside it; any other kind is a chip of its name. Nothing is filled behind
|
|
410
|
+
* them: a hairline on whatever they sit on, so a file reads alike over the
|
|
411
|
+
* composer and inside a turn's bubble, in a light room and a dark one. A second
|
|
412
|
+
* fill on the bubble's was the darker grey that read as a stain. The word for
|
|
413
|
+
* how far a file has got shows only while it is getting there — a picture of
|
|
414
|
+
* the file already says it is attached — and `data-state` keeps the index's own
|
|
415
|
+
* word for whoever reads it.
|
|
416
|
+
*
|
|
417
|
+
* Pressing one opens it beside the conversation (`onOpen`).
|
|
418
|
+
*/
|
|
419
|
+
function Chip({ name, type, size, state, sent, file, picture, onOpen, onRemove, }) {
|
|
420
|
+
const kind = kindOf(name, type);
|
|
356
421
|
const bad = state === "failed";
|
|
357
422
|
const why = state === "failed" || state === "stored" ? file?.error : file?.note;
|
|
358
|
-
|
|
423
|
+
// A picture the browser cannot draw (HEIC, a lapsed signature) falls back to its glyph.
|
|
424
|
+
const [broken, setBroken] = useState("");
|
|
425
|
+
const word = state === "ready" ? (file?.clipped ? "in part" : "") : progress(state, sent, file);
|
|
426
|
+
const meta = [kind === "pdf" ? "PDF" : "", size > 0 ? weigh(size) : "", word].filter(Boolean).join(" \u00b7 ");
|
|
427
|
+
const remove = onRemove ? (_jsx(Box, { render: "button", "aria-label": `Remove ${name}`, onClick: onRemove, p: "$1", rounded: "$10", shrink: 0, hoverStyle: { bg: "$hover" }, children: _jsx(X, { size: 12, "aria-hidden": true }) })) : null;
|
|
428
|
+
const marks = {
|
|
429
|
+
role: "listitem",
|
|
430
|
+
"data-slot": "file-chip",
|
|
431
|
+
"data-state": state,
|
|
432
|
+
"data-kind": kind,
|
|
433
|
+
"data-stage": file?.stage ?? "",
|
|
434
|
+
...(why ? { title: why } : null),
|
|
435
|
+
};
|
|
436
|
+
if (kind === "file")
|
|
437
|
+
return (_jsxs(XStack, { ...marks, self: "center", items: "center", gap: "$1", pr: onRemove ? "$1" : "$2.5", rounded: "$10", borderWidth: 1, borderColor: "$borderColor", maxW: 320, children: [_jsxs(XStack, { render: "button", "aria-label": `Open ${name}`, onClick: onOpen, items: "center", gap: "$1.5", pl: "$2.5", py: "$1", minW: 0, cursor: "pointer", children: [_jsx(Paperclip, { size: 12, "aria-hidden": true, color: "var(--muted-foreground)" }), _jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: name }), meta ? (_jsx(Text, { fontSize: "$1", color: bad ? BAD : "$soft", numberOfLines: 1, shrink: 0, children: meta })) : null] }), remove] }));
|
|
438
|
+
const drawn = kind === "image" && picture && broken !== picture;
|
|
439
|
+
const Glyph = kind === "pdf" ? FileText : ImageMark;
|
|
440
|
+
return (_jsxs(XStack, { ...marks, items: "center", gap: "$1", p: "$1", pr: onRemove ? "$1" : "$2.5", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", maxW: 280, children: [_jsxs(XStack, { render: "button", "aria-label": `Open ${name}`, onClick: onOpen, items: "center", gap: "$2", minW: 0, cursor: "pointer", children: [_jsx(YStack, { "data-slot": "file-thumb", width: THUMB, height: THUMB, shrink: 0, rounded: "$3", overflow: "hidden", items: "center", justify: "center", gap: "$0.5", borderWidth: drawn ? 0 : 1, borderColor: "$borderColor", children: drawn ? (_jsx(View, { render: _jsx("img", { src: picture, alt: "", loading: "lazy", decoding: "async", onError: () => setBroken(picture) }), width: "100%", height: "100%", objectFit: "cover", display: "block" })) : (_jsxs(_Fragment, { children: [_jsx(Glyph, { size: 18, "aria-hidden": true, color: "var(--muted-foreground)" }), kind === "pdf" ? (_jsx(Text, { fontSize: 9, fontWeight: "600", letterSpacing: 0.5, color: "$soft", children: "PDF" })) : null] })) }), _jsxs(YStack, { minW: 0, gap: "$0.5", children: [_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, text: "left", children: name }), meta ? (_jsx(Text, { fontSize: "$1", color: bad ? BAD : "$soft", numberOfLines: 1, text: "left", children: meta })) : null] })] }), remove] }));
|
|
359
441
|
}
|
|
360
442
|
/** A file held for the next message: uploading, then indexing, then ready. */
|
|
361
|
-
function HeldChip({ one, onRemove }) {
|
|
443
|
+
function HeldChip({ one, onOpen, onRemove }) {
|
|
362
444
|
const live = useWorkFile(one.file?.id);
|
|
363
445
|
const file = live ?? one.file;
|
|
364
446
|
const state = one.state === "uploading" ? one.state : (file?.status ?? one.state);
|
|
365
|
-
|
|
447
|
+
const picture = usePicture(kindOf(one.name, one.type) === "image", one.href, file?.id, file);
|
|
448
|
+
// The mark the column's Sources row opens for the same file, so both open it alike.
|
|
449
|
+
const mark = { id: one.id, name: one.name, kind: kindOf(one.name, one.type) === "image" ? "image" : "file", href: one.href || undefined, file: file?.id, mime: one.type };
|
|
450
|
+
return (_jsx(Chip, { name: one.name, type: one.type, size: one.size, state: state, sent: one.sent, file: file ?? (one.error ? { error: one.error } : undefined), picture: picture, onOpen: () => onOpen(mark), onRemove: onRemove }));
|
|
366
451
|
}
|
|
367
452
|
/** A file a sent turn carried, as the index says of it now. */
|
|
368
|
-
function TurnChip({ f }) {
|
|
453
|
+
function TurnChip({ f, onOpen }) {
|
|
369
454
|
const live = useWorkFile(f.id);
|
|
370
|
-
|
|
455
|
+
const picture = usePicture(kindOf(f.name, f.type) === "image", "", f.id, live);
|
|
456
|
+
const mark = { id: f.id, name: f.name, kind: kindOf(f.name, f.type) === "image" ? "image" : "file", file: f.id, mime: f.type };
|
|
457
|
+
return _jsx(Chip, { name: f.name, type: f.type, size: f.size, state: live?.status ?? "ready", sent: 1, file: live, picture: picture, onOpen: () => onOpen(mark) });
|
|
371
458
|
}
|
|
372
459
|
/** A question that carried files: its words, the files put into it, and those read again. */
|
|
373
|
-
function Carried({ text, attached, reused }) {
|
|
374
|
-
return (_jsxs(YStack, { gap: "$
|
|
460
|
+
function Carried({ text, attached, reused, onOpen }) {
|
|
461
|
+
return (_jsxs(YStack, { gap: "$2", "data-slot": "turn-files", children: [text ? _jsx(Prose, { text: text }) : null, attached.length ? (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", role: "list", "aria-label": "Files in this message", children: attached.map((f) => (_jsx(TurnChip, { f: f, onOpen: onOpen }, f.id))) })) : null, reused.length ? (_jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Read again: ", reused.map((f) => `${f.name} (${weigh(f.size)})`).join(", ")] })) : null] }));
|
|
375
462
|
}
|
|
376
463
|
/**
|
|
377
464
|
* The conversation. One component for a visitor and for an account.
|
|
@@ -420,6 +507,29 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
420
507
|
// switch for it. Kept per conversation by `pane.ts`, so the header's control
|
|
421
508
|
// reflects THIS room rather than the last one that was open.
|
|
422
509
|
const [pinned, flipPin] = usePinned();
|
|
510
|
+
// The frame draws the column only from `$lg` (Shell.tsx `Paned`).
|
|
511
|
+
const wide = useMedia().lg;
|
|
512
|
+
/**
|
|
513
|
+
* OPENS A FILE A MESSAGE CARRIES, by the road the column's Sources row takes
|
|
514
|
+
* (`look`): a tab in the browser beside the conversation, the column opened
|
|
515
|
+
* first when it is shut. A phone has no column — the frame spends the width
|
|
516
|
+
* on the room — so there the file opens in a tab of the browser's own, minted
|
|
517
|
+
* before the signed address is asked for, since a window opened after an
|
|
518
|
+
* await is a popup the browser blocks.
|
|
519
|
+
*/
|
|
520
|
+
const openFile = useCallback((m) => {
|
|
521
|
+
if (wide) {
|
|
522
|
+
if (aside === null)
|
|
523
|
+
showAside("tasks");
|
|
524
|
+
void look(at, m, ai);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const tab = window.open("", "_blank");
|
|
528
|
+
if (!tab)
|
|
529
|
+
return;
|
|
530
|
+
tab.opener = null;
|
|
531
|
+
void addressOf(m, ai).then((href) => (href ? tab.location.replace(href) : tab.close()));
|
|
532
|
+
}, [wide, aside, showAside, at, ai]);
|
|
423
533
|
/**
|
|
424
534
|
* WHO YOU ARE TALKING TO, when it is somebody rather than the plain model.
|
|
425
535
|
*
|
|
@@ -1132,7 +1242,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1132
1242
|
if (turn.role === "user" && said) {
|
|
1133
1243
|
const read = told(said);
|
|
1134
1244
|
if (read.carried)
|
|
1135
|
-
return _jsx(Carried, { text: read.text, attached: read.carried.attached, reused: read.carried.reused });
|
|
1245
|
+
return _jsx(Carried, { text: read.text, attached: read.carried.attached, reused: read.carried.reused, onOpen: openFile });
|
|
1136
1246
|
}
|
|
1137
1247
|
// WHILE THE ANSWER HAS NO WORDS YET: who is answering (the agents the
|
|
1138
1248
|
// question addresses, or the whole room when it names nobody), and the
|
|
@@ -1238,7 +1348,7 @@ function Thread({ placeholder = message(), initial, anonymous = false, heading,
|
|
|
1238
1348
|
},
|
|
1239
1349
|
// WHAT GOES WITH THE MESSAGE, above the frame: one chip a file, each
|
|
1240
1350
|
// with its own remove, and the reason any file was refused.
|
|
1241
|
-
head: files.length || turnedAway || preparing || talk.open || talk.refusal || pause ? (_jsxs(YStack, { width: "calc(100% - 32px)", maxW: `calc(${MEASURE} - 1.5rem)`, mx: "auto", gap: "$1.5", children: [pause ? (_jsx(LimitedBanner, { message: pause.message, actions: pause.actions, onAction: act, onUsage: () => showSettings("usage") })) : null, actWrong ? (_jsx(Text, { role: "alert", "data-slot": "limited-banner-wrong", fontSize: "$2", color: BAD, children: actWrong })) : null, files.length ? (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", role: "list", "aria-label": "Attached files", "data-slot": "composer-attached", children: files.map((one) => (_jsx(HeldChip, { one: one, onRemove: () => drop(at, one.id) }, one.id))) })) : null, preparing ? (_jsxs(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-preparing", fontSize: "$2", color: "$soft", children: [preparing, "\u2026"] })) : null, turnedAway ? (_jsx(Text, { role: "alert", fontSize: "$2", color: BAD, children: turnedAway })) : null, talk.open || talk.refusal ? (_jsx(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-talk-reply", fontSize: "$2", color: talk.refusal ? BAD : "$soft", children: talk.refusal
|
|
1351
|
+
head: files.length || turnedAway || preparing || talk.open || talk.refusal || pause ? (_jsxs(YStack, { width: "calc(100% - 32px)", maxW: `calc(${MEASURE} - 1.5rem)`, mx: "auto", gap: "$1.5", children: [pause ? (_jsx(LimitedBanner, { message: pause.message, actions: pause.actions, onAction: act, onUsage: () => showSettings("usage") })) : null, actWrong ? (_jsx(Text, { role: "alert", "data-slot": "limited-banner-wrong", fontSize: "$2", color: BAD, children: actWrong })) : null, files.length ? (_jsx(XStack, { gap: "$1.5", flexWrap: "wrap", role: "list", "aria-label": "Attached files", "data-slot": "composer-attached", children: files.map((one) => (_jsx(HeldChip, { one: one, onOpen: openFile, onRemove: () => drop(at, one.id) }, one.id))) })) : null, preparing ? (_jsxs(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-preparing", fontSize: "$2", color: "$soft", children: [preparing, "\u2026"] })) : null, turnedAway ? (_jsx(Text, { role: "alert", fontSize: "$2", color: BAD, children: turnedAway })) : null, talk.open || talk.refusal ? (_jsx(Text, { role: "status", "aria-live": "polite", "data-slot": "composer-talk-reply", fontSize: "$2", color: talk.refusal ? BAD : "$soft", children: talk.refusal
|
|
1242
1352
|
? worded(talk.refusal)
|
|
1243
1353
|
: talk.reply
|
|
1244
1354
|
? `Hanzo: ${talk.reply}`
|
package/dist/RightPane.d.ts
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
import { type ChatMessage } from '@hanzo/ai/react';
|
|
2
|
+
import { type Api } from './lib/files.js';
|
|
3
|
+
/**
|
|
4
|
+
* One thing the conversation carries.
|
|
5
|
+
*
|
|
6
|
+
* `href` is where it can be opened and `body` is the text of something that was
|
|
7
|
+
* never a file — a fenced block exists only in the transcript until somebody
|
|
8
|
+
* asks to look at it, at which point `body` becomes a blob and gets an address.
|
|
9
|
+
*/
|
|
10
|
+
export interface Mark {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
kind: 'image' | 'code' | 'file';
|
|
14
|
+
href?: string;
|
|
15
|
+
body?: string;
|
|
16
|
+
/** The type a blob should carry, so a browser renders it rather than downloads it. */
|
|
17
|
+
mime?: string;
|
|
18
|
+
/** A workspace file's id: it opens through a signed address minted when it is looked at. */
|
|
19
|
+
file?: string;
|
|
20
|
+
}
|
|
2
21
|
/**
|
|
3
22
|
* The column. `said` is the conversation it stands beside; without one it draws
|
|
4
23
|
* the same shape with nothing in it, which is what a room nobody has spoken in
|
|
@@ -8,6 +27,22 @@ export declare function RightPane({ said, onClose, }: {
|
|
|
8
27
|
said?: ChatMessage[];
|
|
9
28
|
onClose?: () => void;
|
|
10
29
|
}): import("react").JSX.Element;
|
|
30
|
+
/**
|
|
31
|
+
* Opens a mark in this conversation's browser.
|
|
32
|
+
*
|
|
33
|
+
* A BLOCK BECOMES A FILE THE MOMENT SOMEBODY LOOKS AT IT. It has no address
|
|
34
|
+
* until then — it is text inside a turn — so one is minted here from its own
|
|
35
|
+
* bytes, carrying the type its fence declared. That is what makes an answer's
|
|
36
|
+
* `index.html` something you can actually see rather than something you can
|
|
37
|
+
* only read the source of.
|
|
38
|
+
*/
|
|
39
|
+
export declare function look(at: string, m: Mark, api: Api): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Where a mark can be opened: its own address, a signed one minted for its
|
|
42
|
+
* workspace file, or one minted from its text. Null when there is none — a file
|
|
43
|
+
* forgotten or a store unreachable opens nothing, and the row stays.
|
|
44
|
+
*/
|
|
45
|
+
export declare function addressOf(m: Mark, api: Api): Promise<string | null>;
|
|
11
46
|
/** Shows or hides the summary above this conversation's browser. Exported for
|
|
12
47
|
* the conversation header, which is where the control belongs. */
|
|
13
48
|
export declare function usePinned(): [boolean, () => void];
|
package/dist/RightPane.js
CHANGED
|
@@ -210,21 +210,27 @@ function Marks({ at, kind, title, icon, marks, children, }) {
|
|
|
210
210
|
* `index.html` something you can actually see rather than something you can
|
|
211
211
|
* only read the source of.
|
|
212
212
|
*/
|
|
213
|
-
async function look(at, m, api) {
|
|
213
|
+
export async function look(at, m, api) {
|
|
214
|
+
const href = await addressOf(m, api);
|
|
215
|
+
if (href)
|
|
216
|
+
openTab(at, href, m.name);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Where a mark can be opened: its own address, a signed one minted for its
|
|
220
|
+
* workspace file, or one minted from its text. Null when there is none — a file
|
|
221
|
+
* forgotten or a store unreachable opens nothing, and the row stays.
|
|
222
|
+
*/
|
|
223
|
+
export async function addressOf(m, api) {
|
|
214
224
|
if (!m.href && m.file) {
|
|
215
225
|
try {
|
|
216
226
|
const f = await fileOf(api, m.file);
|
|
217
|
-
|
|
227
|
+
return await download(api, f.bucket, f.key);
|
|
218
228
|
}
|
|
219
229
|
catch {
|
|
220
|
-
|
|
230
|
+
return null;
|
|
221
231
|
}
|
|
222
|
-
return;
|
|
223
232
|
}
|
|
224
|
-
|
|
225
|
-
if (!href)
|
|
226
|
-
return;
|
|
227
|
-
openTab(at, href, m.name);
|
|
233
|
+
return m.href ?? (m.body === undefined ? null : URL.createObjectURL(new Blob([m.body], { type: m.mime })));
|
|
228
234
|
}
|
|
229
235
|
/**
|
|
230
236
|
* A menu row's words.
|
package/dist/dev.js
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
// to leave. The name drawn at the top left is Hanzo Dev: this host is the
|
|
8
8
|
// dev agent. Chat, on the rooms shell, is Hanzo AI.
|
|
9
9
|
//
|
|
10
|
-
// THE ADDRESS IS THE
|
|
11
|
-
//
|
|
10
|
+
// THE ADDRESS IS THE PATH. Dev is `/dev`, and what it shows is the path under
|
|
11
|
+
// it in the builder's own grammar (lib/host.ts `app`, `under`), so a run, a
|
|
12
12
|
// screen, a Settings section, a Customize tab, the plans and a project all
|
|
13
13
|
// survive a reload, and Back steps through them. A move is a native history
|
|
14
|
-
// entry, which the Next router folds into `
|
|
15
|
-
//
|
|
14
|
+
// entry, which the Next router folds into `usePathname`, so reading the address
|
|
15
|
+
// and moving it are one path.
|
|
16
16
|
import { useMemo } from 'react';
|
|
17
17
|
import { useLook, useRooms } from './host.js';
|
|
18
18
|
import { useIam } from '@hanzo/iam/react';
|
|
@@ -21,7 +21,7 @@ import { administers, path, route } from '@hanzo/build';
|
|
|
21
21
|
import { bearer, named, org, orgs, pick } from './lib/session.js';
|
|
22
22
|
import { enter } from './lib/destination.js';
|
|
23
23
|
import { api } from './lib/api.js';
|
|
24
|
-
import { app,
|
|
24
|
+
import { app, under } from './lib/host.js';
|
|
25
25
|
import { link } from './lib/tags.js';
|
|
26
26
|
/** Where the builder links out: every one of them an address of this site. */
|
|
27
27
|
const LINKS = {
|
|
@@ -32,14 +32,14 @@ const LINKS = {
|
|
|
32
32
|
};
|
|
33
33
|
/** The person's theme, when they have chosen one the builder can name. */
|
|
34
34
|
const theme = (t) => (t === 'light' || t === 'dark' || t === 'system' ? t : undefined);
|
|
35
|
-
/** The app address a
|
|
35
|
+
/** The app address a builder path names, canonical; '' for anything that is not one. */
|
|
36
36
|
const canonical = (at) => path(route(at ?? ''));
|
|
37
37
|
/** The host for a signed-in reader. */
|
|
38
38
|
export function useDevHost() {
|
|
39
39
|
const { user, accessToken } = useIam();
|
|
40
40
|
const logout = useSignOut();
|
|
41
|
-
const { router,
|
|
42
|
-
const here = canonical(
|
|
41
|
+
const { router, path: pathname } = useRooms();
|
|
42
|
+
const here = canonical(under(pathname));
|
|
43
43
|
const look = useLook();
|
|
44
44
|
// The token's claims change only when the token does: a sign-in, a refresh.
|
|
45
45
|
const memberships = useMemo(() => orgs(), [accessToken]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
@@ -70,7 +70,7 @@ export function useDevHost() {
|
|
|
70
70
|
path: here,
|
|
71
71
|
go: (to, how) => {
|
|
72
72
|
const next = app(canonical(to));
|
|
73
|
-
if (
|
|
73
|
+
if (window.location.pathname === next && !window.location.search)
|
|
74
74
|
return;
|
|
75
75
|
if (how?.replace)
|
|
76
76
|
window.history.replaceState(null, '', next);
|
package/dist/host.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* configure({ site: 'https://hanzo.ai', home: '/', signUp: '/start' })
|
|
9
9
|
*
|
|
10
|
-
* <Rooms router={useRouter()} search={useSearchParams()} route={route} Link={Link}>
|
|
10
|
+
* <Rooms router={useRouter()} search={useSearchParams()} path={usePathname()} route={route} Link={Link}>
|
|
11
11
|
* <Room mode="cal" />
|
|
12
12
|
* </Rooms>
|
|
13
13
|
*
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* any component and on pages the rooms never draw. So the host states them once
|
|
18
18
|
* with `configure()`, in a module its root imports, before anything renders.
|
|
19
19
|
*
|
|
20
|
-
* The ROUTER, the query and the route change as the page moves, so they ride
|
|
20
|
+
* The ROUTER, the query, the path and the route change as the page moves, so they ride
|
|
21
21
|
* React context, and a room reads them with `useRooms()`.
|
|
22
22
|
*/
|
|
23
23
|
import { type AnchorHTMLAttributes, type ComponentType, type ReactNode } from 'react';
|
|
@@ -34,14 +34,18 @@ export interface Router {
|
|
|
34
34
|
}
|
|
35
35
|
interface Moving {
|
|
36
36
|
router: Router;
|
|
37
|
-
/** The address's query, for the rooms that read one (
|
|
37
|
+
/** The address's query, for the rooms that read one (Chat's `?chat=`). */
|
|
38
38
|
search: URLSearchParams | null;
|
|
39
|
+
/** The address's path, for the rooms that read one (Dev's, under `/dev`). */
|
|
40
|
+
path: string | null;
|
|
39
41
|
route: string;
|
|
40
42
|
Link: Link;
|
|
41
43
|
}
|
|
42
|
-
export interface RoomsProps extends Omit<Moving, 'search'> {
|
|
44
|
+
export interface RoomsProps extends Omit<Moving, 'search' | 'path'> {
|
|
43
45
|
/** Absent where no mounted room reads the query, so a static export needs no Suspense for it. */
|
|
44
46
|
search?: URLSearchParams | null;
|
|
47
|
+
/** Absent where no mounted room reads the path. */
|
|
48
|
+
path?: string | null;
|
|
45
49
|
children?: ReactNode;
|
|
46
50
|
}
|
|
47
51
|
/** The router, the query, the route and the link the host mounted the rooms with. */
|
|
@@ -72,5 +76,5 @@ export declare function useLook(): {
|
|
|
72
76
|
* keeps their choice in step with IAM.
|
|
73
77
|
*/
|
|
74
78
|
export declare function Look(): any;
|
|
75
|
-
export declare function Rooms({ router, search, route, Link, children }: RoomsProps): import("react").JSX.Element;
|
|
79
|
+
export declare function Rooms({ router, search, path, route, Link, children }: RoomsProps): import("react").JSX.Element;
|
|
76
80
|
export {};
|
package/dist/host.js
CHANGED
|
@@ -9,7 +9,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
9
9
|
*
|
|
10
10
|
* configure({ site: 'https://hanzo.ai', home: '/', signUp: '/start' })
|
|
11
11
|
*
|
|
12
|
-
* <Rooms router={useRouter()} search={useSearchParams()} route={route} Link={Link}>
|
|
12
|
+
* <Rooms router={useRouter()} search={useSearchParams()} path={usePathname()} route={route} Link={Link}>
|
|
13
13
|
* <Room mode="cal" />
|
|
14
14
|
* </Rooms>
|
|
15
15
|
*
|
|
@@ -19,7 +19,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
19
19
|
* any component and on pages the rooms never draw. So the host states them once
|
|
20
20
|
* with `configure()`, in a module its root imports, before anything renders.
|
|
21
21
|
*
|
|
22
|
-
* The ROUTER, the query and the route change as the page moves, so they ride
|
|
22
|
+
* The ROUTER, the query, the path and the route change as the page moves, so they ride
|
|
23
23
|
* React context, and a room reads them with `useRooms()`.
|
|
24
24
|
*/
|
|
25
25
|
import { createContext, useContext } from 'react';
|
|
@@ -35,6 +35,7 @@ const Context = createContext({
|
|
|
35
35
|
forward: () => window.history.forward(),
|
|
36
36
|
},
|
|
37
37
|
search: null,
|
|
38
|
+
path: null,
|
|
38
39
|
route: '/',
|
|
39
40
|
Link: Plain,
|
|
40
41
|
});
|
|
@@ -71,6 +72,6 @@ export function Look() {
|
|
|
71
72
|
useLook();
|
|
72
73
|
return null;
|
|
73
74
|
}
|
|
74
|
-
export function Rooms({ router, search = null, route, Link, children }) {
|
|
75
|
-
return _jsx(Context.Provider, { value: { router, search, route, Link }, children: children });
|
|
75
|
+
export function Rooms({ router, search = null, path = null, route, Link, children }) {
|
|
76
|
+
return _jsx(Context.Provider, { value: { router, search, path, route, Link }, children: children });
|
|
76
77
|
}
|
package/dist/lib/host.d.ts
CHANGED
|
@@ -30,30 +30,35 @@ export declare const ENTRY = "https://hanzo.team/?signin";
|
|
|
30
30
|
*/
|
|
31
31
|
export declare const START = "https://hanzo.team/start";
|
|
32
32
|
/**
|
|
33
|
-
* The web app,
|
|
34
|
-
* Dev is @hanzo/build's `<Builder
|
|
35
|
-
* in the builder's own grammar (`route` in @hanzo/build):
|
|
36
|
-
* run,
|
|
37
|
-
*
|
|
38
|
-
*
|
|
33
|
+
* The web app, for a signed-in reader, in two modes. Chat is the default, at
|
|
34
|
+
* `/`. Dev is @hanzo/build's `<Builder>` at `/dev`, and what it shows is the
|
|
35
|
+
* PATH under it, in the builder's own grammar (`route` in @hanzo/build): `/dev`
|
|
36
|
+
* New, `/dev/sess_<id>` a run, `/dev/<org>/<repo>` a project,
|
|
37
|
+
* `/dev/-/<screen>`, `/dev/-/settings[/<section>]`, `/dev/-/customize[/<tab>]`,
|
|
38
|
+
* `/dev/<slug>` a deployed site. The export holds one page for all of them,
|
|
39
|
+
* `/dev`, and the edge serves it for every path under it (hanzo.ai's
|
|
40
|
+
* lib/edge.ts `APP`).
|
|
39
41
|
*/
|
|
40
|
-
export declare const
|
|
41
|
-
/**
|
|
42
|
+
export declare const DEV = "/dev";
|
|
43
|
+
/** Dev at an app address; '' is Dev's New, `/dev` itself. */
|
|
42
44
|
export declare const app: (path: string) => string;
|
|
45
|
+
/** The app address a pathname names under `/dev`, or null for a pathname that is not Dev's. */
|
|
46
|
+
export declare const under: (pathname: string | null | undefined) => string | null;
|
|
43
47
|
/**
|
|
44
48
|
* Chat, the app's default mode: `/` is a new conversation and `?chat=<id>` one
|
|
45
|
-
* of the reader's. So the address says which mode the app is in —
|
|
46
|
-
* anything else Chat — and a reload keeps it.
|
|
49
|
+
* of the reader's. So the address says which mode the app is in — a path under
|
|
50
|
+
* `/dev` for Dev, anything else Chat — and a reload keeps it.
|
|
47
51
|
*/
|
|
48
52
|
export declare const TALK = "chat";
|
|
49
53
|
/** `/` in Chat, on a conversation, or `/` itself for a new one. */
|
|
50
54
|
export declare const talk: (id?: string | null) => string;
|
|
51
55
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
56
|
+
* Dev opened on one run, project or site, from anywhere: every "Try Hanzo Dev"
|
|
57
|
+
* link, a room's Dev, the console's "open in the builder". It signs a stranger
|
|
58
|
+
* in on the way (app/(app)/dev/_door.tsx), and is reached through `site()`:
|
|
59
|
+
* here on hanzo.ai, across to hanzo.ai from hanzo.team. Only a value that is
|
|
60
|
+
* exactly a session id, a repository or a site's slug is carried; anything else
|
|
61
|
+
* opens /dev itself, so no caller can make this name another address.
|
|
57
62
|
*/
|
|
58
63
|
export declare const dev: (ref?: string | null) => string;
|
|
59
64
|
/**
|
package/dist/lib/host.js
CHANGED
|
@@ -32,44 +32,59 @@ export const ENTRY = `${WORKSPACE}/?signin`;
|
|
|
32
32
|
*/
|
|
33
33
|
export const START = `${WORKSPACE}/start`;
|
|
34
34
|
/**
|
|
35
|
-
* The web app,
|
|
36
|
-
* Dev is @hanzo/build's `<Builder
|
|
37
|
-
* in the builder's own grammar (`route` in @hanzo/build):
|
|
38
|
-
* run,
|
|
39
|
-
*
|
|
40
|
-
*
|
|
35
|
+
* The web app, for a signed-in reader, in two modes. Chat is the default, at
|
|
36
|
+
* `/`. Dev is @hanzo/build's `<Builder>` at `/dev`, and what it shows is the
|
|
37
|
+
* PATH under it, in the builder's own grammar (`route` in @hanzo/build): `/dev`
|
|
38
|
+
* New, `/dev/sess_<id>` a run, `/dev/<org>/<repo>` a project,
|
|
39
|
+
* `/dev/-/<screen>`, `/dev/-/settings[/<section>]`, `/dev/-/customize[/<tab>]`,
|
|
40
|
+
* `/dev/<slug>` a deployed site. The export holds one page for all of them,
|
|
41
|
+
* `/dev`, and the edge serves it for every path under it (hanzo.ai's
|
|
42
|
+
* lib/edge.ts `APP`).
|
|
41
43
|
*/
|
|
42
|
-
export const
|
|
43
|
-
/**
|
|
44
|
-
export const app = (path) =>
|
|
44
|
+
export const DEV = '/dev';
|
|
45
|
+
/** Dev at an app address; '' is Dev's New, `/dev` itself. */
|
|
46
|
+
export const app = (path) => {
|
|
47
|
+
const p = path.replace(/^\/+|\/+$/g, '');
|
|
48
|
+
return p ? `${DEV}/${p}` : DEV;
|
|
49
|
+
};
|
|
50
|
+
/** The app address a pathname names under `/dev`, or null for a pathname that is not Dev's. */
|
|
51
|
+
export const under = (pathname) => {
|
|
52
|
+
if (!pathname)
|
|
53
|
+
return null;
|
|
54
|
+
if (pathname === DEV || pathname === `${DEV}/`)
|
|
55
|
+
return '';
|
|
56
|
+
if (!pathname.startsWith(`${DEV}/`))
|
|
57
|
+
return null;
|
|
58
|
+
try {
|
|
59
|
+
return decodeURIComponent(pathname.slice(DEV.length + 1));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
};
|
|
45
65
|
/**
|
|
46
66
|
* Chat, the app's default mode: `/` is a new conversation and `?chat=<id>` one
|
|
47
|
-
* of the reader's. So the address says which mode the app is in —
|
|
48
|
-
* anything else Chat — and a reload keeps it.
|
|
67
|
+
* of the reader's. So the address says which mode the app is in — a path under
|
|
68
|
+
* `/dev` for Dev, anything else Chat — and a reload keeps it.
|
|
49
69
|
*/
|
|
50
70
|
export const TALK = 'chat';
|
|
51
71
|
/** `/` in Chat, on a conversation, or `/` itself for a new one. */
|
|
52
72
|
export const talk = (id) => (id ? `/?${TALK}=${encodeURIComponent(id)}` : '/');
|
|
53
|
-
/**
|
|
54
|
-
* The door into the app: every "Try Hanzo Dev" link, the rail's Dev, the
|
|
55
|
-
* console's "open in the builder", dev.hanzo.ai. It signs a stranger in and
|
|
56
|
-
* forwards a member to Dev in the app, `/?at=` (app/(app)/dev/_door.tsx). It is
|
|
57
|
-
* the app's page, so it is reached through `site()`: here on hanzo.ai, across
|
|
58
|
-
* to hanzo.ai from hanzo.team.
|
|
59
|
-
*/
|
|
60
|
-
const DEV = '/dev';
|
|
61
73
|
/** A run, as the platform mints its id. */
|
|
62
74
|
const SESSION = /^sess_[0-9a-f]{32}$/;
|
|
63
|
-
/** A
|
|
75
|
+
/** A deployed site, as the Sites plane slugs it. */
|
|
64
76
|
const SLUG = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
|
|
77
|
+
/** A repository on the forge, `<org>/<name>`. */
|
|
78
|
+
const REPO = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}\/[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
65
79
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
80
|
+
* Dev opened on one run, project or site, from anywhere: every "Try Hanzo Dev"
|
|
81
|
+
* link, a room's Dev, the console's "open in the builder". It signs a stranger
|
|
82
|
+
* in on the way (app/(app)/dev/_door.tsx), and is reached through `site()`:
|
|
83
|
+
* here on hanzo.ai, across to hanzo.ai from hanzo.team. Only a value that is
|
|
84
|
+
* exactly a session id, a repository or a site's slug is carried; anything else
|
|
85
|
+
* opens /dev itself, so no caller can make this name another address.
|
|
71
86
|
*/
|
|
72
|
-
export const dev = (ref) => site(
|
|
87
|
+
export const dev = (ref) => site(ref && (SESSION.test(ref) || REPO.test(ref) || SLUG.test(ref)) ? app(ref) : DEV);
|
|
73
88
|
/** The routes under app/(app) the apex keeps: the app — /chat, a chat shared by link, and /dev. */
|
|
74
89
|
const KEPT = new Set(['/chat', '/chat/shared', '/dev']);
|
|
75
90
|
/** Settings, and every page under it: the person's and the organization's, served wherever the app is. */
|
package/dist/lib/session.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* who was signed in, which reads as a permissions problem rather than a typo.
|
|
15
15
|
* Two derivations of one name never agree; this is the one derivation.
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
17
|
+
import { TALK } from './host.js';
|
|
18
18
|
import { api } from './api.js';
|
|
19
19
|
/** The SDK's own keys, `hanzo_iam_`-prefixed. The far side of a boundary. */
|
|
20
20
|
const PREFIX = 'hanzo_iam_';
|
|
@@ -59,12 +59,13 @@ export function live() {
|
|
|
59
59
|
/** The attribute on `<html>` that says `/` is showing the app, not the landing. */
|
|
60
60
|
export const APP = 'data-app';
|
|
61
61
|
/**
|
|
62
|
-
* An address on `/` that names a place in the app — `?
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
62
|
+
* An address on `/` that names a place in the app — `?chat` in Chat
|
|
63
|
+
* (lib/host.ts `TALK`; Dev's places are paths under `/dev`, whose own page
|
|
64
|
+
* signs a stranger in). The app is signed-in only, so a reader nobody has
|
|
65
|
+
* signed in is sent to hanzo.id with it rather than shown the landing, and it
|
|
66
|
+
* comes back with them.
|
|
66
67
|
*/
|
|
67
|
-
const PLACE = new RegExp(`[?&]
|
|
68
|
+
const PLACE = new RegExp(`[?&]${TALK}(?:[=&]|$)`);
|
|
68
69
|
/** Whether `search` names a place in the app. */
|
|
69
70
|
export const place = (search) => PLACE.test(search);
|
|
70
71
|
/**
|
package/dist/pane.d.ts
CHANGED
|
@@ -103,6 +103,8 @@ export declare function shutTab(key: string, id: string): void;
|
|
|
103
103
|
export declare function pin(key: string, on: boolean): void;
|
|
104
104
|
/** The workspace file `id` as the index last said, or undefined until it has. */
|
|
105
105
|
export declare function useWorkFile(id: string | undefined): WorkFile | undefined;
|
|
106
|
+
/** The object URL over workspace file `id`'s bytes on this machine, or '' where this document never held them. */
|
|
107
|
+
export declare const localOf: (id: string) => string;
|
|
106
108
|
/**
|
|
107
109
|
* Takes files into this conversation: each goes straight to the org's
|
|
108
110
|
* workspace — in parts when it is large, several at once — is registered with
|
package/dist/pane.js
CHANGED
|
@@ -229,6 +229,13 @@ export function useWorkFile(id) {
|
|
|
229
229
|
};
|
|
230
230
|
}, () => (id ? files.get(id) : undefined), () => undefined);
|
|
231
231
|
}
|
|
232
|
+
// THE BYTES ON THIS MACHINE, by the workspace file they became. `spend` leaves a
|
|
233
|
+
// held file's object URL standing, so a turn that carried a picture draws it
|
|
234
|
+
// from here with no request, for as long as this document lives. A reload ends
|
|
235
|
+
// it, and the turn reads the workspace's copy instead.
|
|
236
|
+
const local = new Map();
|
|
237
|
+
/** The object URL over workspace file `id`'s bytes on this machine, or '' where this document never held them. */
|
|
238
|
+
export const localOf = (id) => local.get(id) ?? '';
|
|
232
239
|
// Each held file's upload, by held id: what stops it, and what it settles to.
|
|
233
240
|
const carrying = new Map();
|
|
234
241
|
/** How many files go up at once; each large one also sends several parts at once. */
|
|
@@ -294,6 +301,7 @@ export async function hold(key, list, via) {
|
|
|
294
301
|
const objectKey = chatKey(file);
|
|
295
302
|
await upload(via.api, bucket, objectKey, file, (sent) => patch(key, h.id, { sent }), stop.signal);
|
|
296
303
|
const got = await register(via.api, bucket, objectKey);
|
|
304
|
+
local.set(got.id, h.href);
|
|
297
305
|
patch(key, h.id, fromFile(got));
|
|
298
306
|
watch(via.api, got, key, h.id);
|
|
299
307
|
land(got);
|
|
@@ -363,8 +371,11 @@ export function drop(key, id) {
|
|
|
363
371
|
return was;
|
|
364
372
|
carrying.get(id)?.stop.abort();
|
|
365
373
|
carrying.delete(id);
|
|
366
|
-
if (going.href)
|
|
374
|
+
if (going.href) {
|
|
375
|
+
if (going.file)
|
|
376
|
+
local.delete(going.file.id);
|
|
367
377
|
URL.revokeObjectURL(going.href);
|
|
378
|
+
}
|
|
368
379
|
return { ...was, held: was.held.filter((h) => h.id !== id) };
|
|
369
380
|
});
|
|
370
381
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/rooms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.35",
|
|
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",
|