@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.
Files changed (235) hide show
  1. package/LICENSE-APACHE +205 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +23 -0
  4. package/dist/Account.d.ts +8 -0
  5. package/dist/Account.js +58 -0
  6. package/dist/Beluga.d.ts +12 -0
  7. package/dist/Beluga.js +152 -0
  8. package/dist/Billing.d.ts +1 -0
  9. package/dist/Billing.js +31 -0
  10. package/dist/Board.d.ts +9 -0
  11. package/dist/Board.js +447 -0
  12. package/dist/Boxes.d.ts +1 -0
  13. package/dist/Boxes.js +173 -0
  14. package/dist/CalScheduler.d.ts +15 -0
  15. package/dist/CalScheduler.js +196 -0
  16. package/dist/Chat.d.ts +35 -0
  17. package/dist/Chat.js +1049 -0
  18. package/dist/Column.d.ts +15 -0
  19. package/dist/Column.js +88 -0
  20. package/dist/Contacts.d.ts +1 -0
  21. package/dist/Contacts.js +301 -0
  22. package/dist/Directory.d.ts +24 -0
  23. package/dist/Directory.js +356 -0
  24. package/dist/Drive.d.ts +2 -0
  25. package/dist/Drive.js +466 -0
  26. package/dist/Find.d.ts +7 -0
  27. package/dist/Find.js +82 -0
  28. package/dist/Guide.d.ts +6 -0
  29. package/dist/Guide.js +272 -0
  30. package/dist/Home.d.ts +39 -0
  31. package/dist/Home.js +306 -0
  32. package/dist/Inbox.d.ts +9 -0
  33. package/dist/Inbox.js +193 -0
  34. package/dist/Market.d.ts +3 -0
  35. package/dist/Market.js +149 -0
  36. package/dist/Me.d.ts +27 -0
  37. package/dist/Me.js +251 -0
  38. package/dist/Meet.d.ts +9 -0
  39. package/dist/Meet.js +370 -0
  40. package/dist/ModeMark.d.ts +8 -0
  41. package/dist/ModeMark.js +18 -0
  42. package/dist/OrgSwitcher.d.ts +31 -0
  43. package/dist/OrgSwitcher.js +120 -0
  44. package/dist/Orgs.d.ts +10 -0
  45. package/dist/Orgs.js +582 -0
  46. package/dist/Profile.d.ts +4 -0
  47. package/dist/Profile.js +67 -0
  48. package/dist/Prose.d.ts +5 -0
  49. package/dist/Prose.js +111 -0
  50. package/dist/ProviderMark.d.ts +17 -0
  51. package/dist/ProviderMark.js +277 -0
  52. package/dist/Recents.d.ts +10 -0
  53. package/dist/Recents.js +91 -0
  54. package/dist/RightPane.d.ts +13 -0
  55. package/dist/RightPane.js +426 -0
  56. package/dist/Room.d.ts +8 -0
  57. package/dist/Room.js +50 -0
  58. package/dist/Scene.d.ts +12 -0
  59. package/dist/Scene.js +105 -0
  60. package/dist/Settings.d.ts +46 -0
  61. package/dist/Settings.js +321 -0
  62. package/dist/Setup.d.ts +1 -0
  63. package/dist/Setup.js +36 -0
  64. package/dist/Share.d.ts +3 -0
  65. package/dist/Share.js +130 -0
  66. package/dist/Shared.d.ts +1 -0
  67. package/dist/Shared.js +113 -0
  68. package/dist/Shell.d.ts +63 -0
  69. package/dist/Shell.js +1225 -0
  70. package/dist/Start.d.ts +1 -0
  71. package/dist/Start.js +96 -0
  72. package/dist/Support.d.ts +14 -0
  73. package/dist/Support.js +122 -0
  74. package/dist/Talk.d.ts +4 -0
  75. package/dist/Talk.js +107 -0
  76. package/dist/Upgrade.d.ts +11 -0
  77. package/dist/Upgrade.js +75 -0
  78. package/dist/Usage.d.ts +1 -0
  79. package/dist/Usage.js +21 -0
  80. package/dist/Vibe.d.ts +2 -0
  81. package/dist/Vibe.js +188 -0
  82. package/dist/Work.d.ts +12 -0
  83. package/dist/Work.js +256 -0
  84. package/dist/bots/Bots.d.ts +2 -0
  85. package/dist/bots/Bots.js +456 -0
  86. package/dist/bots/Lights.d.ts +8 -0
  87. package/dist/bots/Lights.js +13 -0
  88. package/dist/bots/api.d.ts +120 -0
  89. package/dist/bots/api.js +75 -0
  90. package/dist/bots/face.d.ts +5 -0
  91. package/dist/bots/face.js +13 -0
  92. package/dist/bots/ink.d.ts +7 -0
  93. package/dist/bots/ink.js +29 -0
  94. package/dist/bots/kit.d.ts +113 -0
  95. package/dist/bots/kit.js +43 -0
  96. package/dist/bots/runtime.d.ts +4 -0
  97. package/dist/bots/runtime.js +326 -0
  98. package/dist/card.d.ts +52 -0
  99. package/dist/card.js +52 -0
  100. package/dist/cast.d.ts +143 -0
  101. package/dist/cast.js +251 -0
  102. package/dist/connect.d.ts +29 -0
  103. package/dist/connect.js +64 -0
  104. package/dist/contacts-store.d.ts +111 -0
  105. package/dist/contacts-store.js +276 -0
  106. package/dist/conversations.d.ts +54 -0
  107. package/dist/conversations.js +98 -0
  108. package/dist/copy.d.ts +23 -0
  109. package/dist/copy.js +39 -0
  110. package/dist/crew.d.ts +36 -0
  111. package/dist/crew.js +304 -0
  112. package/dist/dev.d.ts +3 -0
  113. package/dist/dev.js +102 -0
  114. package/dist/enso.d.ts +70 -0
  115. package/dist/enso.js +112 -0
  116. package/dist/failure.d.ts +57 -0
  117. package/dist/failure.js +143 -0
  118. package/dist/ground.d.ts +32 -0
  119. package/dist/ground.js +32 -0
  120. package/dist/host.d.ts +76 -0
  121. package/dist/host.js +76 -0
  122. package/dist/index.d.ts +14 -0
  123. package/dist/index.js +12 -0
  124. package/dist/kept.d.ts +5 -0
  125. package/dist/kept.js +27 -0
  126. package/dist/lib/account.d.ts +49 -0
  127. package/dist/lib/account.js +229 -0
  128. package/dist/lib/ai.d.ts +46 -0
  129. package/dist/lib/ai.js +140 -0
  130. package/dist/lib/api.d.ts +43 -0
  131. package/dist/lib/api.js +62 -0
  132. package/dist/lib/bare.d.ts +9 -0
  133. package/dist/lib/bare.js +7 -0
  134. package/dist/lib/coding.d.ts +105 -0
  135. package/dist/lib/coding.js +87 -0
  136. package/dist/lib/destination.d.ts +102 -0
  137. package/dist/lib/destination.js +260 -0
  138. package/dist/lib/durable.d.ts +66 -0
  139. package/dist/lib/durable.js +107 -0
  140. package/dist/lib/first.d.ts +11 -0
  141. package/dist/lib/first.js +23 -0
  142. package/dist/lib/home.d.ts +61 -0
  143. package/dist/lib/home.js +54 -0
  144. package/dist/lib/host.d.ts +104 -0
  145. package/dist/lib/host.js +132 -0
  146. package/dist/lib/hostname.d.ts +1 -0
  147. package/dist/lib/hostname.js +14 -0
  148. package/dist/lib/hydrated.d.ts +1 -0
  149. package/dist/lib/hydrated.js +20 -0
  150. package/dist/lib/integrations.d.ts +21 -0
  151. package/dist/lib/integrations.js +1 -0
  152. package/dist/lib/intent.d.ts +6 -0
  153. package/dist/lib/intent.js +71 -0
  154. package/dist/lib/invite.d.ts +155 -0
  155. package/dist/lib/invite.js +245 -0
  156. package/dist/lib/limits.d.ts +55 -0
  157. package/dist/lib/limits.js +119 -0
  158. package/dist/lib/list.d.ts +9 -0
  159. package/dist/lib/list.js +11 -0
  160. package/dist/lib/mark.d.ts +13 -0
  161. package/dist/lib/mark.js +85 -0
  162. package/dist/lib/mix.d.ts +22 -0
  163. package/dist/lib/mix.js +22 -0
  164. package/dist/lib/models.d.ts +27 -0
  165. package/dist/lib/models.js +106 -0
  166. package/dist/lib/org.d.ts +109 -0
  167. package/dist/lib/org.js +140 -0
  168. package/dist/lib/path.d.ts +9 -0
  169. package/dist/lib/path.js +29 -0
  170. package/dist/lib/plans.d.ts +193 -0
  171. package/dist/lib/plans.js +310 -0
  172. package/dist/lib/prose.d.ts +13 -0
  173. package/dist/lib/prose.js +62 -0
  174. package/dist/lib/reach.d.ts +17 -0
  175. package/dist/lib/reach.js +70 -0
  176. package/dist/lib/rooms.json +23 -0
  177. package/dist/lib/served.d.ts +20 -0
  178. package/dist/lib/served.js +92 -0
  179. package/dist/lib/session.d.ts +211 -0
  180. package/dist/lib/session.js +493 -0
  181. package/dist/lib/share.d.ts +131 -0
  182. package/dist/lib/share.js +216 -0
  183. package/dist/lib/signout.d.ts +14 -0
  184. package/dist/lib/signout.js +40 -0
  185. package/dist/lib/tags.d.ts +27 -0
  186. package/dist/lib/tags.js +46 -0
  187. package/dist/lib/tap.d.ts +17 -0
  188. package/dist/lib/tap.js +9 -0
  189. package/dist/lib/tier.d.ts +115 -0
  190. package/dist/lib/tier.js +207 -0
  191. package/dist/lib/tiers.d.ts +9 -0
  192. package/dist/lib/tiers.js +141 -0
  193. package/dist/lib/todo.d.ts +125 -0
  194. package/dist/lib/todo.js +379 -0
  195. package/dist/look.d.ts +1 -0
  196. package/dist/look.js +21 -0
  197. package/dist/meters.d.ts +8 -0
  198. package/dist/meters.js +31 -0
  199. package/dist/model.d.ts +4 -0
  200. package/dist/model.js +44 -0
  201. package/dist/network.d.ts +64 -0
  202. package/dist/network.js +216 -0
  203. package/dist/open.d.ts +81 -0
  204. package/dist/open.js +149 -0
  205. package/dist/pane.d.ts +92 -0
  206. package/dist/pane.js +231 -0
  207. package/dist/progress.d.ts +46 -0
  208. package/dist/progress.js +51 -0
  209. package/dist/recent.d.ts +67 -0
  210. package/dist/recent.js +161 -0
  211. package/dist/rooms.d.ts +131 -0
  212. package/dist/rooms.js +195 -0
  213. package/dist/roster.d.ts +28 -0
  214. package/dist/roster.js +70 -0
  215. package/dist/span.d.ts +14 -0
  216. package/dist/span.js +50 -0
  217. package/dist/speech.d.ts +44 -0
  218. package/dist/speech.js +229 -0
  219. package/dist/stars.d.ts +16 -0
  220. package/dist/stars.js +75 -0
  221. package/dist/tabs.d.ts +26 -0
  222. package/dist/tabs.js +25 -0
  223. package/dist/team.d.ts +11 -0
  224. package/dist/team.gen.d.ts +10 -0
  225. package/dist/team.gen.js +11 -0
  226. package/dist/team.js +30 -0
  227. package/dist/tools.d.ts +35 -0
  228. package/dist/tools.js +143 -0
  229. package/dist/tours.d.ts +24 -0
  230. package/dist/tours.js +311 -0
  231. package/dist/waiting.d.ts +15 -0
  232. package/dist/waiting.js +17 -0
  233. package/dist/where.d.ts +54 -0
  234. package/dist/where.js +14 -0
  235. 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;
@@ -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
+ }
@@ -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
+ }
@@ -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
+ };