@hanzo/rooms 0.1.0
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/LICENSE-APACHE +205 -0
- package/LICENSE-MIT +21 -0
- package/README.md +23 -0
- package/dist/Account.d.ts +8 -0
- package/dist/Account.js +58 -0
- package/dist/Beluga.d.ts +12 -0
- package/dist/Beluga.js +152 -0
- package/dist/Billing.d.ts +1 -0
- package/dist/Billing.js +31 -0
- package/dist/Board.d.ts +9 -0
- package/dist/Board.js +447 -0
- package/dist/Boxes.d.ts +1 -0
- package/dist/Boxes.js +173 -0
- package/dist/CalScheduler.d.ts +15 -0
- package/dist/CalScheduler.js +196 -0
- package/dist/Chat.d.ts +35 -0
- package/dist/Chat.js +1049 -0
- package/dist/Column.d.ts +15 -0
- package/dist/Column.js +88 -0
- package/dist/Contacts.d.ts +1 -0
- package/dist/Contacts.js +301 -0
- package/dist/Directory.d.ts +24 -0
- package/dist/Directory.js +356 -0
- package/dist/Drive.d.ts +2 -0
- package/dist/Drive.js +466 -0
- package/dist/Find.d.ts +7 -0
- package/dist/Find.js +82 -0
- package/dist/Guide.d.ts +6 -0
- package/dist/Guide.js +272 -0
- package/dist/Home.d.ts +39 -0
- package/dist/Home.js +306 -0
- package/dist/Inbox.d.ts +9 -0
- package/dist/Inbox.js +193 -0
- package/dist/Market.d.ts +3 -0
- package/dist/Market.js +149 -0
- package/dist/Me.d.ts +27 -0
- package/dist/Me.js +251 -0
- package/dist/Meet.d.ts +9 -0
- package/dist/Meet.js +370 -0
- package/dist/ModeMark.d.ts +8 -0
- package/dist/ModeMark.js +18 -0
- package/dist/OrgSwitcher.d.ts +31 -0
- package/dist/OrgSwitcher.js +120 -0
- package/dist/Orgs.d.ts +10 -0
- package/dist/Orgs.js +582 -0
- package/dist/Profile.d.ts +4 -0
- package/dist/Profile.js +67 -0
- package/dist/Prose.d.ts +5 -0
- package/dist/Prose.js +111 -0
- package/dist/ProviderMark.d.ts +17 -0
- package/dist/ProviderMark.js +277 -0
- package/dist/Recents.d.ts +10 -0
- package/dist/Recents.js +91 -0
- package/dist/RightPane.d.ts +13 -0
- package/dist/RightPane.js +426 -0
- package/dist/Room.d.ts +8 -0
- package/dist/Room.js +50 -0
- package/dist/Scene.d.ts +12 -0
- package/dist/Scene.js +105 -0
- package/dist/Settings.d.ts +46 -0
- package/dist/Settings.js +321 -0
- package/dist/Setup.d.ts +1 -0
- package/dist/Setup.js +36 -0
- package/dist/Share.d.ts +3 -0
- package/dist/Share.js +130 -0
- package/dist/Shared.d.ts +1 -0
- package/dist/Shared.js +113 -0
- package/dist/Shell.d.ts +63 -0
- package/dist/Shell.js +1225 -0
- package/dist/Start.d.ts +1 -0
- package/dist/Start.js +96 -0
- package/dist/Support.d.ts +14 -0
- package/dist/Support.js +122 -0
- package/dist/Talk.d.ts +4 -0
- package/dist/Talk.js +107 -0
- package/dist/Upgrade.d.ts +11 -0
- package/dist/Upgrade.js +75 -0
- package/dist/Usage.d.ts +1 -0
- package/dist/Usage.js +21 -0
- package/dist/Vibe.d.ts +2 -0
- package/dist/Vibe.js +188 -0
- package/dist/Work.d.ts +12 -0
- package/dist/Work.js +256 -0
- package/dist/bots/Bots.d.ts +2 -0
- package/dist/bots/Bots.js +456 -0
- package/dist/bots/Lights.d.ts +8 -0
- package/dist/bots/Lights.js +13 -0
- package/dist/bots/api.d.ts +120 -0
- package/dist/bots/api.js +75 -0
- package/dist/bots/face.d.ts +5 -0
- package/dist/bots/face.js +13 -0
- package/dist/bots/ink.d.ts +7 -0
- package/dist/bots/ink.js +29 -0
- package/dist/bots/kit.d.ts +113 -0
- package/dist/bots/kit.js +43 -0
- package/dist/bots/runtime.d.ts +4 -0
- package/dist/bots/runtime.js +326 -0
- package/dist/card.d.ts +52 -0
- package/dist/card.js +52 -0
- package/dist/cast.d.ts +143 -0
- package/dist/cast.js +251 -0
- package/dist/connect.d.ts +29 -0
- package/dist/connect.js +64 -0
- package/dist/contacts-store.d.ts +111 -0
- package/dist/contacts-store.js +276 -0
- package/dist/conversations.d.ts +54 -0
- package/dist/conversations.js +98 -0
- package/dist/copy.d.ts +23 -0
- package/dist/copy.js +39 -0
- package/dist/crew.d.ts +36 -0
- package/dist/crew.js +304 -0
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +102 -0
- package/dist/enso.d.ts +70 -0
- package/dist/enso.js +112 -0
- package/dist/failure.d.ts +57 -0
- package/dist/failure.js +143 -0
- package/dist/ground.d.ts +32 -0
- package/dist/ground.js +32 -0
- package/dist/host.d.ts +76 -0
- package/dist/host.js +76 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/kept.d.ts +5 -0
- package/dist/kept.js +27 -0
- package/dist/lib/account.d.ts +49 -0
- package/dist/lib/account.js +229 -0
- package/dist/lib/ai.d.ts +46 -0
- package/dist/lib/ai.js +140 -0
- package/dist/lib/api.d.ts +43 -0
- package/dist/lib/api.js +62 -0
- package/dist/lib/bare.d.ts +9 -0
- package/dist/lib/bare.js +7 -0
- package/dist/lib/coding.d.ts +105 -0
- package/dist/lib/coding.js +87 -0
- package/dist/lib/destination.d.ts +102 -0
- package/dist/lib/destination.js +260 -0
- package/dist/lib/durable.d.ts +66 -0
- package/dist/lib/durable.js +107 -0
- package/dist/lib/first.d.ts +11 -0
- package/dist/lib/first.js +23 -0
- package/dist/lib/home.d.ts +61 -0
- package/dist/lib/home.js +54 -0
- package/dist/lib/host.d.ts +104 -0
- package/dist/lib/host.js +132 -0
- package/dist/lib/hostname.d.ts +1 -0
- package/dist/lib/hostname.js +14 -0
- package/dist/lib/hydrated.d.ts +1 -0
- package/dist/lib/hydrated.js +20 -0
- package/dist/lib/integrations.d.ts +21 -0
- package/dist/lib/integrations.js +1 -0
- package/dist/lib/intent.d.ts +6 -0
- package/dist/lib/intent.js +71 -0
- package/dist/lib/invite.d.ts +155 -0
- package/dist/lib/invite.js +245 -0
- package/dist/lib/limits.d.ts +55 -0
- package/dist/lib/limits.js +119 -0
- package/dist/lib/list.d.ts +9 -0
- package/dist/lib/list.js +11 -0
- package/dist/lib/mark.d.ts +13 -0
- package/dist/lib/mark.js +85 -0
- package/dist/lib/mix.d.ts +22 -0
- package/dist/lib/mix.js +22 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +106 -0
- package/dist/lib/org.d.ts +109 -0
- package/dist/lib/org.js +140 -0
- package/dist/lib/path.d.ts +9 -0
- package/dist/lib/path.js +29 -0
- package/dist/lib/plans.d.ts +193 -0
- package/dist/lib/plans.js +310 -0
- package/dist/lib/prose.d.ts +13 -0
- package/dist/lib/prose.js +62 -0
- package/dist/lib/reach.d.ts +17 -0
- package/dist/lib/reach.js +70 -0
- package/dist/lib/rooms.json +23 -0
- package/dist/lib/served.d.ts +20 -0
- package/dist/lib/served.js +92 -0
- package/dist/lib/session.d.ts +211 -0
- package/dist/lib/session.js +493 -0
- package/dist/lib/share.d.ts +131 -0
- package/dist/lib/share.js +216 -0
- package/dist/lib/signout.d.ts +14 -0
- package/dist/lib/signout.js +40 -0
- package/dist/lib/tags.d.ts +27 -0
- package/dist/lib/tags.js +46 -0
- package/dist/lib/tap.d.ts +17 -0
- package/dist/lib/tap.js +9 -0
- package/dist/lib/tier.d.ts +115 -0
- package/dist/lib/tier.js +207 -0
- package/dist/lib/tiers.d.ts +9 -0
- package/dist/lib/tiers.js +141 -0
- package/dist/lib/todo.d.ts +125 -0
- package/dist/lib/todo.js +379 -0
- package/dist/look.d.ts +1 -0
- package/dist/look.js +21 -0
- package/dist/meters.d.ts +8 -0
- package/dist/meters.js +31 -0
- package/dist/model.d.ts +4 -0
- package/dist/model.js +44 -0
- package/dist/network.d.ts +64 -0
- package/dist/network.js +216 -0
- package/dist/open.d.ts +81 -0
- package/dist/open.js +149 -0
- package/dist/pane.d.ts +92 -0
- package/dist/pane.js +231 -0
- package/dist/progress.d.ts +46 -0
- package/dist/progress.js +51 -0
- package/dist/recent.d.ts +67 -0
- package/dist/recent.js +161 -0
- package/dist/rooms.d.ts +131 -0
- package/dist/rooms.js +195 -0
- package/dist/roster.d.ts +28 -0
- package/dist/roster.js +70 -0
- package/dist/span.d.ts +14 -0
- package/dist/span.js +50 -0
- package/dist/speech.d.ts +44 -0
- package/dist/speech.js +229 -0
- package/dist/stars.d.ts +16 -0
- package/dist/stars.js +75 -0
- package/dist/tabs.d.ts +26 -0
- package/dist/tabs.js +25 -0
- package/dist/team.d.ts +11 -0
- package/dist/team.gen.d.ts +10 -0
- package/dist/team.gen.js +11 -0
- package/dist/team.js +30 -0
- package/dist/tools.d.ts +35 -0
- package/dist/tools.js +143 -0
- package/dist/tours.d.ts +24 -0
- package/dist/tours.js +311 -0
- package/dist/waiting.d.ts +15 -0
- package/dist/waiting.js +17 -0
- package/dist/where.d.ts +54 -0
- package/dist/where.js +14 -0
- package/package.json +108 -0
package/dist/pane.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
// What the column beside a conversation is holding, PER CONVERSATION.
|
|
3
|
+
//
|
|
4
|
+
// The store lives outside React for the reason `open.ts` gives: the router
|
|
5
|
+
// mounts the rooms, so anything kept inside one is destroyed by a route change.
|
|
6
|
+
// This differs from `open.ts` in the one way that matters — IT PERSISTS. A
|
|
7
|
+
// selection is WHERE YOU ARE and a fresh document is rightly a fresh room; a
|
|
8
|
+
// pane is WHAT YOU HAD OPEN, and losing four tabs to a reload is the reader
|
|
9
|
+
// losing work rather than losing their place.
|
|
10
|
+
//
|
|
11
|
+
// ONE ENTRY PER CONVERSATION, not one blob holding them all. A blob is read in
|
|
12
|
+
// full at boot and rewritten in full on every change, so ten conversations make
|
|
13
|
+
// nine channels' tabs the cost of moving one tab in the tenth. Keyed
|
|
14
|
+
// separately, both costs are proportional to the conversation you are actually
|
|
15
|
+
// in, and a corrupt entry loses one pane rather than all of them.
|
|
16
|
+
//
|
|
17
|
+
// WHAT IS KEPT AND WHAT IS NOT. Tabs, the chosen tab and the pin survive a
|
|
18
|
+
// reload because they are addresses and flags. Files picked off the reader's
|
|
19
|
+
// own disk DO NOT: a file handle is an object URL, which the browser revokes
|
|
20
|
+
// with the document that minted it, so a persisted row would come back as a
|
|
21
|
+
// name pointing at nothing. A row that looks like a file and opens nothing is
|
|
22
|
+
// worse than no row, so they are held for the session and no longer.
|
|
23
|
+
import { useSyncExternalStore } from 'react';
|
|
24
|
+
/** A pane nobody has touched. Shared, and never mutated — see `EMPTY` in
|
|
25
|
+
* `open.ts` for why a fresh object per read is an infinite render. */
|
|
26
|
+
const BARE = { tabs: [], at: null, pinned: true, held: [] };
|
|
27
|
+
const where = (key) => `hanzo.pane.${key}`;
|
|
28
|
+
/**
|
|
29
|
+
* The conversation a pane belongs to.
|
|
30
|
+
*
|
|
31
|
+
* THREE FIELDS AND NOT ONE, because `open.ts` already refuses to conflate them:
|
|
32
|
+
* an inbox room arrives over a transport and is keyed by it, a thread is a
|
|
33
|
+
* conversation the org has recorded, and an agent is who you are talking to
|
|
34
|
+
* before a thread exists. Prefixed so a room called `x` and a thread called `x`
|
|
35
|
+
* cannot land on one entry.
|
|
36
|
+
*
|
|
37
|
+
* `new` is a real answer and not a fallback: a room nobody has spoken in yet is
|
|
38
|
+
* somewhere a reader can open tabs, and they should still be there when the
|
|
39
|
+
* first turn mints a thread. It is the ONE key that several conversations pass
|
|
40
|
+
* through, which is the honest cost of not having an id to key on yet.
|
|
41
|
+
*/
|
|
42
|
+
export function channel(open) {
|
|
43
|
+
if (open.room)
|
|
44
|
+
return `room:${open.room}`;
|
|
45
|
+
if (open.thread)
|
|
46
|
+
return `thread:${open.thread}`;
|
|
47
|
+
if (open.agent)
|
|
48
|
+
return `agent:${open.agent}`;
|
|
49
|
+
return 'new';
|
|
50
|
+
}
|
|
51
|
+
// The panes this document has actually touched. A switch back reads from here
|
|
52
|
+
// rather than re-parsing JSON, so moving between two conversations costs one
|
|
53
|
+
// read each and nothing after that.
|
|
54
|
+
const live = new Map();
|
|
55
|
+
const listeners = new Set();
|
|
56
|
+
function announce() {
|
|
57
|
+
for (const l of listeners)
|
|
58
|
+
l();
|
|
59
|
+
}
|
|
60
|
+
function subscribe(l) {
|
|
61
|
+
listeners.add(l);
|
|
62
|
+
return () => {
|
|
63
|
+
listeners.delete(l);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The pane for a conversation, reading it in the first time it is asked for.
|
|
68
|
+
*
|
|
69
|
+
* A BAD ENTRY IS DROPPED, NOT THROWN. Storage holds whatever an older build
|
|
70
|
+
* wrote, and one unparseable pane must not take the room down with it — the
|
|
71
|
+
* reader loses some tabs, which is what they would have had anyway.
|
|
72
|
+
*/
|
|
73
|
+
function read(key) {
|
|
74
|
+
const found = live.get(key);
|
|
75
|
+
if (found)
|
|
76
|
+
return found;
|
|
77
|
+
let pane = BARE;
|
|
78
|
+
try {
|
|
79
|
+
const raw = typeof window === 'undefined' ? null : window.localStorage.getItem(where(key));
|
|
80
|
+
if (raw) {
|
|
81
|
+
const kept = JSON.parse(raw);
|
|
82
|
+
const tabs = Array.isArray(kept.tabs)
|
|
83
|
+
? kept.tabs.filter((t) => !!t && typeof t.id === 'string' && typeof t.url === 'string' && typeof t.title === 'string')
|
|
84
|
+
: [];
|
|
85
|
+
pane = {
|
|
86
|
+
tabs,
|
|
87
|
+
// A chosen tab that is no longer open is not a choice. Restoring one
|
|
88
|
+
// leaves the strip with nothing marked and the panel drawing a tab that
|
|
89
|
+
// is not in it.
|
|
90
|
+
at: tabs.some((t) => t.id === kept.at) ? kept.at : (tabs[0]?.id ?? null),
|
|
91
|
+
pinned: kept.pinned !== false,
|
|
92
|
+
held: [],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
pane = BARE;
|
|
98
|
+
}
|
|
99
|
+
live.set(key, pane);
|
|
100
|
+
return pane;
|
|
101
|
+
}
|
|
102
|
+
/** The server renders no storage, so every pane starts bare there. Returning
|
|
103
|
+
* the shared object keeps the snapshot stable across reads. */
|
|
104
|
+
const onServer = () => BARE;
|
|
105
|
+
function write(key, next) {
|
|
106
|
+
live.set(key, next);
|
|
107
|
+
try {
|
|
108
|
+
if (typeof window === 'undefined')
|
|
109
|
+
return;
|
|
110
|
+
const kept = { tabs: next.tabs, at: next.at, pinned: next.pinned };
|
|
111
|
+
window.localStorage.setItem(where(key), JSON.stringify(kept));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// A browser refusing to store — private mode, a full quota — is not a
|
|
115
|
+
// reason to refuse the change on screen. The pane stands for this document.
|
|
116
|
+
}
|
|
117
|
+
announce();
|
|
118
|
+
}
|
|
119
|
+
/** Applies a change to one conversation's pane. */
|
|
120
|
+
function edit(key, change) {
|
|
121
|
+
write(key, change(read(key)));
|
|
122
|
+
}
|
|
123
|
+
/** The host and path of an address, which is as much as a title can honestly be. */
|
|
124
|
+
export function name(url) {
|
|
125
|
+
try {
|
|
126
|
+
const u = new URL(url);
|
|
127
|
+
return u.host + (u.pathname === '/' ? '' : u.pathname);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return url;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* An address as typed, made into one that can be opened.
|
|
135
|
+
*
|
|
136
|
+
* A bare host is `https`, because that is the web now; a loopback name is
|
|
137
|
+
* `http`, because nothing is listening on 443 of a laptop and offering a
|
|
138
|
+
* reader a URL that cannot connect is worse than guessing.
|
|
139
|
+
*/
|
|
140
|
+
export function address(typed) {
|
|
141
|
+
const t = typed.trim();
|
|
142
|
+
if (!t)
|
|
143
|
+
return '';
|
|
144
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(t))
|
|
145
|
+
return t;
|
|
146
|
+
return /^(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/i.test(t) ? `http://${t}` : `https://${t}`;
|
|
147
|
+
}
|
|
148
|
+
let minted = 0;
|
|
149
|
+
const mint = (prefix) => `${prefix}${Date.now().toString(36)}${(minted++).toString(36)}`;
|
|
150
|
+
/**
|
|
151
|
+
* Opens an address in this conversation, and chooses it.
|
|
152
|
+
*
|
|
153
|
+
* `called` is for the things that HAVE a name of their own — an answer's
|
|
154
|
+
* `index.html`, a file off the reader's disk — because their address is a blob
|
|
155
|
+
* and `name()` can read nothing useful out of one. Without it the host and path
|
|
156
|
+
* are the honest title.
|
|
157
|
+
*/
|
|
158
|
+
export function openTab(key, url, called) {
|
|
159
|
+
const at = address(url);
|
|
160
|
+
if (!at)
|
|
161
|
+
return;
|
|
162
|
+
edit(key, (was) => {
|
|
163
|
+
const leaf = { id: mint('t'), url: at, title: called || name(at) };
|
|
164
|
+
return { ...was, tabs: [...was.tabs, leaf], at: leaf.id };
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** Points an already-open tab somewhere else. */
|
|
168
|
+
export function goTab(key, id, url) {
|
|
169
|
+
const at = address(url);
|
|
170
|
+
if (!at)
|
|
171
|
+
return;
|
|
172
|
+
edit(key, (was) => ({
|
|
173
|
+
...was,
|
|
174
|
+
tabs: was.tabs.map((t) => (t.id === id ? { ...t, url: at, title: name(at) } : t)),
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
/** Chooses an open tab. */
|
|
178
|
+
export function pickTab(key, id) {
|
|
179
|
+
edit(key, (was) => (was.at === id ? was : { ...was, at: id }));
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Shuts a tab.
|
|
183
|
+
*
|
|
184
|
+
* THE NEIGHBOUR IS THE ONE TO ITS RIGHT, falling back to its left — what every
|
|
185
|
+
* browser does, and the only choice that does not send the reader somewhere
|
|
186
|
+
* they were not looking. Closing the last tab leaves none chosen rather than
|
|
187
|
+
* refusing, because a pane with no tabs is a state this panel draws.
|
|
188
|
+
*/
|
|
189
|
+
export function shutTab(key, id) {
|
|
190
|
+
edit(key, (was) => {
|
|
191
|
+
const gone = was.tabs.findIndex((t) => t.id === id);
|
|
192
|
+
if (gone < 0)
|
|
193
|
+
return was;
|
|
194
|
+
const tabs = was.tabs.filter((t) => t.id !== id);
|
|
195
|
+
if (was.at !== id)
|
|
196
|
+
return { ...was, tabs };
|
|
197
|
+
return { ...was, tabs, at: (tabs[gone] ?? tabs[gone - 1])?.id ?? null };
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/** Shows or hides the summary column beside this conversation's browser. */
|
|
201
|
+
export function pin(key, on) {
|
|
202
|
+
edit(key, (was) => (was.pinned === on ? was : { ...was, pinned: on }));
|
|
203
|
+
}
|
|
204
|
+
/** Takes files from the reader's disk into this conversation. */
|
|
205
|
+
export function hold(key, files) {
|
|
206
|
+
if (!files.length)
|
|
207
|
+
return;
|
|
208
|
+
const taken = files.map((f) => ({
|
|
209
|
+
id: mint('h'),
|
|
210
|
+
name: f.name,
|
|
211
|
+
size: f.size,
|
|
212
|
+
type: f.type,
|
|
213
|
+
href: URL.createObjectURL(f),
|
|
214
|
+
}));
|
|
215
|
+
edit(key, (was) => ({ ...was, held: [...was.held, ...taken] }));
|
|
216
|
+
}
|
|
217
|
+
/** Lets go of a file, and of the handle the browser minted for it. */
|
|
218
|
+
export function drop(key, id) {
|
|
219
|
+
edit(key, (was) => {
|
|
220
|
+
const going = was.held.find((h) => h.id === id);
|
|
221
|
+
if (!going)
|
|
222
|
+
return was;
|
|
223
|
+
URL.revokeObjectURL(going.href);
|
|
224
|
+
return { ...was, held: was.held.filter((h) => h.id !== id) };
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/** This conversation's pane, and it alone — a change to another redraws nothing
|
|
228
|
+
* here, because the snapshot returned is that key's own object. */
|
|
229
|
+
export function usePane(key) {
|
|
230
|
+
return useSyncExternalStore(subscribe, () => read(key), onServer);
|
|
231
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Session, SessionDetail } from '@hanzo/ai';
|
|
3
|
+
/**
|
|
4
|
+
* What a run says about how far it has got.
|
|
5
|
+
*
|
|
6
|
+
* `pct` is ABSENT when unknown — never 0 — and `estimated` says whether the
|
|
7
|
+
* number was derived rather than reported by the run itself, which is a fact a
|
|
8
|
+
* reader is owed before they plan around it.
|
|
9
|
+
*/
|
|
10
|
+
export interface Progress {
|
|
11
|
+
/** 0–100. Absent means unknown. */
|
|
12
|
+
pct?: number;
|
|
13
|
+
/** The stage it is in — the coarse answer. */
|
|
14
|
+
phase?: string;
|
|
15
|
+
/** What it is doing inside that stage — the fine one. */
|
|
16
|
+
activity?: string;
|
|
17
|
+
/** When the run last said so, RFC3339. */
|
|
18
|
+
at?: string;
|
|
19
|
+
/** The percentage was derived, not self-reported. */
|
|
20
|
+
estimated?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Read it off a session, whatever the SDK's type currently declares.
|
|
24
|
+
*
|
|
25
|
+
* The narrowing lives HERE and nowhere else — one cast at the boundary, in the
|
|
26
|
+
* one file that knows the shape — so the day `@hanzo/ai` declares the field,
|
|
27
|
+
* this function loses its cast and nothing else in the app changes.
|
|
28
|
+
*
|
|
29
|
+
* Every field is checked rather than trusted: this is a network response, and
|
|
30
|
+
* `pct` in particular is refused unless it is a finite number in range. A NaN
|
|
31
|
+
* reaching a width would render as a bar of no width, which is a zero again.
|
|
32
|
+
*/
|
|
33
|
+
export declare function progressOf(run: Session | SessionDetail | null | undefined): Progress | null;
|
|
34
|
+
/**
|
|
35
|
+
* The bar.
|
|
36
|
+
*
|
|
37
|
+
* `role="progressbar"` with NO `aria-valuenow` is the standard way to say
|
|
38
|
+
* indeterminate, and `aria-busy` says the work is still going. Setting
|
|
39
|
+
* `aria-valuenow={0}` for an unknown percentage is the same lie in the
|
|
40
|
+
* accessibility tree that an empty fill would be on screen.
|
|
41
|
+
*/
|
|
42
|
+
export declare function Bar({ run, label, }: {
|
|
43
|
+
run: Session | SessionDetail | null | undefined;
|
|
44
|
+
/** Draw the phase and the percentage under the track. */
|
|
45
|
+
label?: boolean;
|
|
46
|
+
}): ReactNode;
|
package/dist/progress.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Text, XStack, YStack } from '@hanzo/ui';
|
|
4
|
+
/**
|
|
5
|
+
* Read it off a session, whatever the SDK's type currently declares.
|
|
6
|
+
*
|
|
7
|
+
* The narrowing lives HERE and nowhere else — one cast at the boundary, in the
|
|
8
|
+
* one file that knows the shape — so the day `@hanzo/ai` declares the field,
|
|
9
|
+
* this function loses its cast and nothing else in the app changes.
|
|
10
|
+
*
|
|
11
|
+
* Every field is checked rather than trusted: this is a network response, and
|
|
12
|
+
* `pct` in particular is refused unless it is a finite number in range. A NaN
|
|
13
|
+
* reaching a width would render as a bar of no width, which is a zero again.
|
|
14
|
+
*/
|
|
15
|
+
export function progressOf(run) {
|
|
16
|
+
const held = run?.progress;
|
|
17
|
+
if (!held || typeof held !== 'object')
|
|
18
|
+
return null;
|
|
19
|
+
const { pct, phase, activity, at, estimated } = held;
|
|
20
|
+
const out = {};
|
|
21
|
+
if (typeof pct === 'number' && Number.isFinite(pct) && pct >= 0 && pct <= 100)
|
|
22
|
+
out.pct = pct;
|
|
23
|
+
if (typeof phase === 'string' && phase)
|
|
24
|
+
out.phase = phase;
|
|
25
|
+
if (typeof activity === 'string' && activity)
|
|
26
|
+
out.activity = activity;
|
|
27
|
+
if (typeof at === 'string' && at)
|
|
28
|
+
out.at = at;
|
|
29
|
+
if (estimated === true)
|
|
30
|
+
out.estimated = true;
|
|
31
|
+
// A progress object saying nothing is nothing.
|
|
32
|
+
return out.pct === undefined && !out.phase && !out.activity ? null : out;
|
|
33
|
+
}
|
|
34
|
+
/** The words under the bar: the stage, then what it is doing in it. */
|
|
35
|
+
const words = (p) => [p.phase, p.activity].filter(Boolean).join(' — ');
|
|
36
|
+
/**
|
|
37
|
+
* The bar.
|
|
38
|
+
*
|
|
39
|
+
* `role="progressbar"` with NO `aria-valuenow` is the standard way to say
|
|
40
|
+
* indeterminate, and `aria-busy` says the work is still going. Setting
|
|
41
|
+
* `aria-valuenow={0}` for an unknown percentage is the same lie in the
|
|
42
|
+
* accessibility tree that an empty fill would be on screen.
|
|
43
|
+
*/
|
|
44
|
+
export function Bar({ run, label = true, }) {
|
|
45
|
+
const p = progressOf(run);
|
|
46
|
+
if (!p)
|
|
47
|
+
return null;
|
|
48
|
+
const said = words(p);
|
|
49
|
+
const known = p.pct !== undefined;
|
|
50
|
+
return (_jsxs(YStack, { gap: "$1", width: "100%", role: "progressbar", "aria-busy": !known, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": p.pct, "aria-label": said || 'Progress', children: [_jsx(XStack, { height: 4, width: "100%", rounded: "$10", bg: "$edge", overflow: "hidden", children: known ? (_jsx(YStack, { height: 4, width: `${p.pct}%`, rounded: "$10", bg: "$ink" })) : null }), label && (said || known) ? (_jsxs(XStack, { gap: "$2", items: "baseline", minW: 0, children: [said ? (_jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, shrink: 1, children: said })) : null, known ? (_jsxs(Text, { fontSize: "$1", color: "$soft", children: [p.estimated ? '~' : '', Math.round(p.pct), "%"] })) : null] })) : null] }));
|
|
51
|
+
}
|
package/dist/recent.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type Mode = 'chat' | 'dev';
|
|
2
|
+
export interface Recent {
|
|
3
|
+
kind: Mode;
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
/** When it was last spoken in, as an ISO time. */
|
|
7
|
+
at: string;
|
|
8
|
+
/** When it began, as an ISO time. */
|
|
9
|
+
began: string;
|
|
10
|
+
status?: string;
|
|
11
|
+
project?: string;
|
|
12
|
+
}
|
|
13
|
+
export type Status = 'all' | 'running' | 'paused' | 'done' | 'error' | 'stopped';
|
|
14
|
+
export type Since = 'any' | 'day' | 'week' | 'month';
|
|
15
|
+
export type Group = 'date' | 'state' | 'project' | 'starred' | 'none';
|
|
16
|
+
export type Sort = 'activity' | 'created' | 'title';
|
|
17
|
+
/** How the list is narrowed, grouped and sorted. */
|
|
18
|
+
export interface View {
|
|
19
|
+
status: Status;
|
|
20
|
+
since: Since;
|
|
21
|
+
group: Group;
|
|
22
|
+
sort: Sort;
|
|
23
|
+
}
|
|
24
|
+
export declare const PLAIN: View;
|
|
25
|
+
export type Option<T> = {
|
|
26
|
+
id: T;
|
|
27
|
+
label: string;
|
|
28
|
+
};
|
|
29
|
+
export declare const SINCE: Option<Since>[];
|
|
30
|
+
/** What each mode's menu offers, section by section, as Claude's Recents menu does. */
|
|
31
|
+
export declare const MENUS: Record<Mode, {
|
|
32
|
+
status: Option<Status>[];
|
|
33
|
+
group: Option<Group>[];
|
|
34
|
+
sort: Option<Sort>[];
|
|
35
|
+
}>;
|
|
36
|
+
/** A kept view as this mode may hold it: anything a menu does not offer reads as the plain view's. */
|
|
37
|
+
export declare function viewOf(mode: Mode, kept: unknown): View;
|
|
38
|
+
/** Chat's conversations as rows. */
|
|
39
|
+
export declare function chats(threads: {
|
|
40
|
+
id: string;
|
|
41
|
+
title?: string;
|
|
42
|
+
updatedAt?: string;
|
|
43
|
+
}[]): Recent[];
|
|
44
|
+
/** Dev's runs as rows. */
|
|
45
|
+
export declare function runs(sessions: {
|
|
46
|
+
id: string;
|
|
47
|
+
title: string;
|
|
48
|
+
status: string;
|
|
49
|
+
project?: string;
|
|
50
|
+
repo?: string;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
}[]): Recent[];
|
|
54
|
+
/**
|
|
55
|
+
* The list as drawn: the rows the view and the words admit, sorted, and cut into
|
|
56
|
+
* headed groups in the order they read. A view grouped by None is one group with
|
|
57
|
+
* no heading.
|
|
58
|
+
*/
|
|
59
|
+
export declare function arrange(rows: Recent[], view: View, words: string, starred: Set<string>, now?: Date): {
|
|
60
|
+
title: string;
|
|
61
|
+
rows: Recent[];
|
|
62
|
+
}[];
|
|
63
|
+
/**
|
|
64
|
+
* How long ago a row was last active, as a list reads it at a glance: `now`,
|
|
65
|
+
* `12m`, `3h`, `2d`, `5w`, `4mo`, `2y`. '' for an instant that is not one.
|
|
66
|
+
*/
|
|
67
|
+
export declare function ago(at: string, now?: Date): string;
|
package/dist/recent.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// The Recents list as data: each mode's rows, the views a menu offers, and the
|
|
2
|
+
// list a view makes of them. No React, so a spec reads it as it is.
|
|
3
|
+
export const PLAIN = { status: 'all', since: 'any', group: 'date', sort: 'activity' };
|
|
4
|
+
const STATUSES = [
|
|
5
|
+
{ id: 'all', label: 'All' },
|
|
6
|
+
{ id: 'running', label: 'Running' },
|
|
7
|
+
{ id: 'paused', label: 'Paused' },
|
|
8
|
+
{ id: 'done', label: 'Done' },
|
|
9
|
+
{ id: 'error', label: 'Failed' },
|
|
10
|
+
{ id: 'stopped', label: 'Stopped' },
|
|
11
|
+
];
|
|
12
|
+
export const SINCE = [
|
|
13
|
+
{ id: 'any', label: 'Any time' },
|
|
14
|
+
{ id: 'day', label: 'Today' },
|
|
15
|
+
{ id: 'week', label: 'Past 7 days' },
|
|
16
|
+
{ id: 'month', label: 'Past 30 days' },
|
|
17
|
+
];
|
|
18
|
+
/** What each mode's menu offers, section by section, as Claude's Recents menu does. */
|
|
19
|
+
export const MENUS = {
|
|
20
|
+
chat: {
|
|
21
|
+
status: [],
|
|
22
|
+
group: [
|
|
23
|
+
{ id: 'date', label: 'Date' },
|
|
24
|
+
{ id: 'starred', label: 'Starred' },
|
|
25
|
+
{ id: 'none', label: 'None' },
|
|
26
|
+
],
|
|
27
|
+
sort: [
|
|
28
|
+
{ id: 'activity', label: 'Activity' },
|
|
29
|
+
{ id: 'title', label: 'Title' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
dev: {
|
|
33
|
+
status: STATUSES,
|
|
34
|
+
group: [
|
|
35
|
+
{ id: 'date', label: 'Date' },
|
|
36
|
+
{ id: 'state', label: 'State' },
|
|
37
|
+
{ id: 'project', label: 'Project' },
|
|
38
|
+
{ id: 'none', label: 'None' },
|
|
39
|
+
],
|
|
40
|
+
sort: [
|
|
41
|
+
{ id: 'activity', label: 'Activity' },
|
|
42
|
+
{ id: 'created', label: 'Created' },
|
|
43
|
+
{ id: 'title', label: 'Title' },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
/** A kept view as this mode may hold it: anything a menu does not offer reads as the plain view's. */
|
|
48
|
+
export function viewOf(mode, kept) {
|
|
49
|
+
const k = (kept && typeof kept === 'object' ? kept : {});
|
|
50
|
+
const m = MENUS[mode];
|
|
51
|
+
const pick = (v, from, fallback) => from.find((o) => o.id === v)?.id ?? fallback;
|
|
52
|
+
return {
|
|
53
|
+
status: pick(k.status, m.status, PLAIN.status),
|
|
54
|
+
since: pick(k.since, SINCE, PLAIN.since),
|
|
55
|
+
group: pick(k.group, m.group, PLAIN.group),
|
|
56
|
+
sort: pick(k.sort, m.sort, PLAIN.sort),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Chat's conversations as rows. */
|
|
60
|
+
export function chats(threads) {
|
|
61
|
+
return threads.map((t) => ({ kind: 'chat', id: t.id, title: t.title || 'Untitled', at: t.updatedAt ?? '', began: t.updatedAt ?? '' }));
|
|
62
|
+
}
|
|
63
|
+
/** Dev's runs as rows. */
|
|
64
|
+
export function runs(sessions) {
|
|
65
|
+
return sessions.map((r) => ({
|
|
66
|
+
kind: 'dev',
|
|
67
|
+
id: r.id,
|
|
68
|
+
title: r.title || 'Untitled run',
|
|
69
|
+
at: r.updatedAt || r.createdAt,
|
|
70
|
+
began: r.createdAt,
|
|
71
|
+
status: r.status,
|
|
72
|
+
project: r.project || r.repo?.split('/').pop() || '',
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
const DAY = 86_400_000;
|
|
76
|
+
const SPAN = { any: Infinity, day: DAY, week: 7 * DAY, month: 30 * DAY };
|
|
77
|
+
const STATE = { running: 'Running', paused: 'Paused', done: 'Done', error: 'Failed', stopped: 'Stopped' };
|
|
78
|
+
/** The date headings, in the order they read. */
|
|
79
|
+
const DAYS = ['Today', 'Yesterday', 'Past 7 days', 'Past 30 days', 'Older'];
|
|
80
|
+
/** Which date heading an instant falls under, counted in the reader's own days. */
|
|
81
|
+
function day(at, now) {
|
|
82
|
+
const t = new Date(at);
|
|
83
|
+
if (Number.isNaN(t.getTime()))
|
|
84
|
+
return 'Older';
|
|
85
|
+
const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
86
|
+
if (t.getTime() >= midnight)
|
|
87
|
+
return 'Today';
|
|
88
|
+
if (t.getTime() >= midnight - DAY)
|
|
89
|
+
return 'Yesterday';
|
|
90
|
+
if (t.getTime() >= midnight - 7 * DAY)
|
|
91
|
+
return 'Past 7 days';
|
|
92
|
+
if (t.getTime() >= midnight - 30 * DAY)
|
|
93
|
+
return 'Past 30 days';
|
|
94
|
+
return 'Older';
|
|
95
|
+
}
|
|
96
|
+
const newest = (a, b) => (a < b ? 1 : a > b ? -1 : 0);
|
|
97
|
+
/**
|
|
98
|
+
* The list as drawn: the rows the view and the words admit, sorted, and cut into
|
|
99
|
+
* headed groups in the order they read. A view grouped by None is one group with
|
|
100
|
+
* no heading.
|
|
101
|
+
*/
|
|
102
|
+
export function arrange(rows, view, words, starred, now = new Date()) {
|
|
103
|
+
const w = words.trim().toLowerCase();
|
|
104
|
+
const kept = rows.filter((r) => (!w || r.title.toLowerCase().includes(w)) &&
|
|
105
|
+
(view.status === 'all' || r.status === view.status) &&
|
|
106
|
+
(view.since === 'any' || now.getTime() - new Date(r.at).getTime() <= SPAN[view.since]));
|
|
107
|
+
kept.sort((a, b) => view.sort === 'title' ? a.title.localeCompare(b.title) : view.sort === 'created' ? newest(a.began, b.began) : newest(a.at, b.at));
|
|
108
|
+
const headOf = (r) => {
|
|
109
|
+
switch (view.group) {
|
|
110
|
+
case 'date':
|
|
111
|
+
return day(view.sort === 'created' ? r.began : r.at, now);
|
|
112
|
+
case 'state':
|
|
113
|
+
return STATE[r.status ?? ''] ?? 'Other';
|
|
114
|
+
case 'project':
|
|
115
|
+
return r.project || 'No project';
|
|
116
|
+
case 'starred':
|
|
117
|
+
return starred.has(r.id) ? 'Starred' : 'Others';
|
|
118
|
+
default:
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const groups = [];
|
|
123
|
+
for (const r of kept) {
|
|
124
|
+
const title = headOf(r);
|
|
125
|
+
const g = groups.find((one) => one.title === title);
|
|
126
|
+
if (g)
|
|
127
|
+
g.rows.push(r);
|
|
128
|
+
else
|
|
129
|
+
groups.push({ title, rows: [r] });
|
|
130
|
+
}
|
|
131
|
+
if (view.group === 'starred')
|
|
132
|
+
groups.sort((a, b) => (a.title === 'Starred' ? -1 : b.title === 'Starred' ? 1 : 0));
|
|
133
|
+
if (view.group === 'date')
|
|
134
|
+
groups.sort((a, b) => DAYS.indexOf(a.title) - DAYS.indexOf(b.title));
|
|
135
|
+
return groups;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* How long ago a row was last active, as a list reads it at a glance: `now`,
|
|
139
|
+
* `12m`, `3h`, `2d`, `5w`, `4mo`, `2y`. '' for an instant that is not one.
|
|
140
|
+
*/
|
|
141
|
+
export function ago(at, now = new Date()) {
|
|
142
|
+
const t = new Date(at).getTime();
|
|
143
|
+
if (Number.isNaN(t))
|
|
144
|
+
return '';
|
|
145
|
+
const m = Math.max(0, Math.floor((now.getTime() - t) / 60_000));
|
|
146
|
+
if (m < 1)
|
|
147
|
+
return 'now';
|
|
148
|
+
if (m < 60)
|
|
149
|
+
return `${m}m`;
|
|
150
|
+
const h = Math.floor(m / 60);
|
|
151
|
+
if (h < 24)
|
|
152
|
+
return `${h}h`;
|
|
153
|
+
const d = Math.floor(h / 24);
|
|
154
|
+
if (d < 7)
|
|
155
|
+
return `${d}d`;
|
|
156
|
+
if (d < 30)
|
|
157
|
+
return `${Math.floor(d / 7)}w`;
|
|
158
|
+
if (d < 365)
|
|
159
|
+
return `${Math.floor(d / 30)}mo`;
|
|
160
|
+
return `${Math.floor(d / 365)}y`;
|
|
161
|
+
}
|
package/dist/rooms.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A room as Team keeps it: a channel, a direct message, or a room bound to work.
|
|
3
|
+
*
|
|
4
|
+
* `direct` is the distinction the sidebar draws — a channel anyone in the org
|
|
5
|
+
* can read against a conversation between two people — so it decides the sigil
|
|
6
|
+
* and the section rather than a guess from the name.
|
|
7
|
+
*
|
|
8
|
+
* `space` is half of a room's address: two spaces of one org may each hold a
|
|
9
|
+
* room called #general, so the pair identifies one and the id alone does not.
|
|
10
|
+
* It is also the box a file goes in — `/v1/team/files/{space}` is addressed by
|
|
11
|
+
* space, never by room. The field is the server's own name for it
|
|
12
|
+
* (apps/team/room.go, teamRoom.Space).
|
|
13
|
+
*/
|
|
14
|
+
export interface TeamRoom {
|
|
15
|
+
id: string;
|
|
16
|
+
space: string;
|
|
17
|
+
name: string;
|
|
18
|
+
topic?: string;
|
|
19
|
+
desc?: string;
|
|
20
|
+
direct: boolean;
|
|
21
|
+
private: boolean;
|
|
22
|
+
visibility?: 'public' | 'org' | 'private';
|
|
23
|
+
orgId?: string;
|
|
24
|
+
archived: boolean;
|
|
25
|
+
members: string[];
|
|
26
|
+
count?: number;
|
|
27
|
+
/**
|
|
28
|
+
* What the room is FOR — "standing" for one meant to outlive any task,
|
|
29
|
+
* "bound" for one opened for a single piece of work. Absent reads standing.
|
|
30
|
+
* It is intent, not state: whether the room is open now is `archived`.
|
|
31
|
+
*/
|
|
32
|
+
life?: string;
|
|
33
|
+
/**
|
|
34
|
+
* What the room is ABOUT, each a "<kind>:<ref>" — "repo:hanzoai/cloud",
|
|
35
|
+
* "issue:1010". References, never copies: the app that owns a project is the
|
|
36
|
+
* one that can resolve it.
|
|
37
|
+
*/
|
|
38
|
+
bindings?: string[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The key the workspace selects a room by, and puts in the address:
|
|
42
|
+
* `team:<space>/<id>`. Both halves, because a room id is unique within a space
|
|
43
|
+
* and not across the org. Prefixed rather than hashed, because a key a person
|
|
44
|
+
* can read in a URL is one they can report in a bug.
|
|
45
|
+
*/
|
|
46
|
+
export declare const teamKey: (room: Pick<TeamRoom, 'space' | 'id'>) => string;
|
|
47
|
+
/** The address a key names, or null for a key that is not a room's. */
|
|
48
|
+
export declare function roomKey(key: string | null): {
|
|
49
|
+
space: string;
|
|
50
|
+
id: string;
|
|
51
|
+
} | null;
|
|
52
|
+
/** The page a key opens on: Home, carrying it as the `?room=` that
|
|
53
|
+
* `app/(app)/home/_workspace.tsx` reads on arrival. */
|
|
54
|
+
export declare const roomHref: (key: string) => string;
|
|
55
|
+
/** Asks the platform again. The list held stays on screen until it answers. */
|
|
56
|
+
export declare function reloadRooms(): void;
|
|
57
|
+
/**
|
|
58
|
+
* The org's real rooms, from Team, and the way to add one.
|
|
59
|
+
*
|
|
60
|
+
* `/v1/team/rooms` is the org's own list and answers only a validated
|
|
61
|
+
* principal. An archived room is not drawn: it is a room the org closed, and a
|
|
62
|
+
* sidebar that lists them makes leaving one meaningless.
|
|
63
|
+
*
|
|
64
|
+
* NULL IS UNKNOWN AND IT IS NOT EMPTY. Three states, and the caller owes a
|
|
65
|
+
* sentence for each: `null` while nothing is known, `[]` for an org that
|
|
66
|
+
* genuinely has no rooms, and `wrong` for a service that answered no. A list is
|
|
67
|
+
* never fabricated to cover any of them.
|
|
68
|
+
*
|
|
69
|
+
* `open` posts a room and HOLDS the row the platform answered with: a 201 is
|
|
70
|
+
* the stored room, so every view carries it from that moment and the reader can
|
|
71
|
+
* step into it without a second read. The caller is not added to a room by the
|
|
72
|
+
* server, so it names its own members.
|
|
73
|
+
*/
|
|
74
|
+
export declare function useTeamRooms(): {
|
|
75
|
+
rooms: TeamRoom[] | null;
|
|
76
|
+
wrong: unknown;
|
|
77
|
+
reload: () => void;
|
|
78
|
+
open: (room: {
|
|
79
|
+
name: string;
|
|
80
|
+
space?: string;
|
|
81
|
+
members: string[];
|
|
82
|
+
}) => Promise<TeamRoom>;
|
|
83
|
+
};
|
|
84
|
+
/** The room with that id, or undefined for none open. */
|
|
85
|
+
export declare function roomOf(rooms: TeamRoom[] | null, id: string | null): TeamRoom | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* One thing somebody said in a room.
|
|
88
|
+
*
|
|
89
|
+
* `author` is the team ACCOUNT that wrote it, not a name: what to call somebody
|
|
90
|
+
* is the roster's answer (`roster.ts`), and copying it onto every message is
|
|
91
|
+
* how the two come to disagree. `text` is plain — the platform stores the
|
|
92
|
+
* client's markup and reduces it on the way out, so nothing here parses HTML.
|
|
93
|
+
*/
|
|
94
|
+
export interface RoomMessage {
|
|
95
|
+
id: string;
|
|
96
|
+
room: string;
|
|
97
|
+
author: string;
|
|
98
|
+
text: string;
|
|
99
|
+
/** Unix MILLIseconds, which is what the platform stamps. */
|
|
100
|
+
createdOn: number;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A room's conversation, and the way to add to it.
|
|
104
|
+
*
|
|
105
|
+
* READ AND WRITE IN ONE HOOK, because they are one question — a pane that can
|
|
106
|
+
* show a room and not answer in it is a directory — and because the poller and
|
|
107
|
+
* the sender then cannot disagree about what the room holds.
|
|
108
|
+
*
|
|
109
|
+
* THE ROOM IS POLLED, every two seconds while it is open and the tab is
|
|
110
|
+
* showing. The team plane has no push a browser can read but its transactor
|
|
111
|
+
* socket, which is a binary protocol rather than a line; a read costs a few
|
|
112
|
+
* hundred milliseconds and nothing on the server past the space's own
|
|
113
|
+
* messages, so half a request a second per open room is the honest price of
|
|
114
|
+
* seeing an answer land. Each read carries its own AbortController, and the
|
|
115
|
+
* cleanup aborts the one in flight and stops the clock — on unmount, and when
|
|
116
|
+
* the room or its space changes.
|
|
117
|
+
*
|
|
118
|
+
* `send` posts and answers the row the platform stored: a 201 is the message,
|
|
119
|
+
* with its id and stamp, so the hook holds it at once and the next poll finds
|
|
120
|
+
* the same id and keeps one. Nothing the platform did not accept is ever held.
|
|
121
|
+
*
|
|
122
|
+
* `space` is half a room's address (two spaces of one org may each hold a room
|
|
123
|
+
* with one id), so both calls carry it and a room with neither reads empty
|
|
124
|
+
* rather than guessing.
|
|
125
|
+
*/
|
|
126
|
+
export declare function useRoomMessages(room: string | null, space: string | undefined): {
|
|
127
|
+
messages: RoomMessage[];
|
|
128
|
+
failed: unknown;
|
|
129
|
+
send: (text: string) => Promise<RoomMessage>;
|
|
130
|
+
reload: () => void;
|
|
131
|
+
};
|