@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/Board.js ADDED
@@ -0,0 +1,447 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // Board: the org's work, and the day beside it.
4
+ //
5
+ // NAMED BOARD, NOT TASKS, and the rename is the point rather than a tidy-up.
6
+ // `/v1/tasks` is Hanzo Tasks — the durable execution engine, ten thousand
7
+ // workflow runs deep — and this room is a board of forge issues. Two different
8
+ // things wearing one word is how somebody comes to believe the board IS the
9
+ // engine, and asks why their cards are not replayable. Tasks keeps the word it
10
+ // earned; this is the Board.
11
+ //
12
+ // A CARD IS A FORGE ISSUE. `/v1/todo` says so itself — "the column is a LABEL on
13
+ // the forge, so the board and the forge web UI are the same object seen twice:
14
+ // relabelling in either moves the card in both" — so this surface keeps no work
15
+ // of its own. Everything drawn here is a row somebody could also have moved from
16
+ // the forge, and everything written here is a row they will find there.
17
+ //
18
+ // TWO READINGS OF ONE SET OF CARDS, and one of them is up at a time: the board
19
+ // by column, the planner by date. The room opens on the board, which is what it
20
+ // is called. There is no inbox pane: the workspace already has one, and a second
21
+ // would be two places for the same arriving thing.
22
+ //
23
+ // WHAT A CARD CAN CARRY is what the store holds — a title, a description, a
24
+ // column, a priority, an assignee, labels, a start and a due date. Checklists
25
+ // ride in the description as markdown task items, which is not a workaround but
26
+ // the way a forge issue has always spelled one, so a step ticked here is ticked
27
+ // in the forge and a step written there arrives here. Covers and file
28
+ // attachments have nowhere to live yet, so they are not drawn.
29
+ import { useCallback, useEffect, useMemo, useState } from 'react';
30
+ import { useAgents, usePeople } from '@hanzo/ai/react';
31
+ import { Box, Text, View, XStack, YStack } from '@hanzo/ui';
32
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@hanzo/ui';
33
+ import { useIam, useOrganizations } from '@hanzo/iam/react';
34
+ import { Calendar, CalendarClock, Check, ChevronLeft, ChevronRight, Columns3, ListChecks, ListFilter, SquareTerminal, Plus, Tag, UserPlus, X, } from 'lucide-react';
35
+ import { useOpen } from './open.js';
36
+ import { useAi } from './lib/ai.js';
37
+ import { Face } from './cast.js';
38
+ import { say } from './failure.js';
39
+ import { AGENT_QUEUE, AGENT_WORKFLOW, reads, runOf, start as startRun, } from './lib/durable.js';
40
+ import { COLUMNS, PRIORITIES, addStep, board as readBoard, claim, edit, open as openCard, projects as readProjects, sift, steps, tick, } from './lib/todo.js';
41
+ import { BAD } from './lib/mix.js';
42
+ const DAY = 86400;
43
+ /** A pane with nothing in it yet, centred, saying which nothing it is. */
44
+ function Empty({ children }) {
45
+ return (_jsxs(YStack, { flex: 1, items: "center", justify: "center", p: "$6", gap: "$2", children: [_jsx(Text, { render: "h1", fontSize: "$6", fontWeight: "600", color: "var(--foreground)", children: "Board" }), _jsx(Text, { fontSize: "$3", color: "$soft", text: "center", children: children })] }));
46
+ }
47
+ /** A quiet control that says its verb. */
48
+ function Act({ icon: Icon, label, on, onPress, }) {
49
+ return (_jsx(Box, { render: "button", onClick: onPress, "aria-pressed": on, px: "$2.5", py: "$1.5", rounded: "$2", bg: on ? '$raised' : 'transparent', borderWidth: 1, borderColor: on ? '$borderColor' : 'transparent', hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$1.5", children: [Icon ? _jsx(Icon, { size: 13, "aria-hidden": true }) : null, _jsx(Text, { fontSize: "$1", color: "$ink", children: label })] }) }));
50
+ }
51
+ /** A label: a neutral badge, so a board reads by its words, not a hue per name. */
52
+ function Label({ name }) {
53
+ return (_jsx(XStack, { px: "$2", py: "$0.5", rounded: "$2", bg: "$hover", borderWidth: 1, borderColor: "$borderColor", children: _jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: name }) }));
54
+ }
55
+ /** A date, as a person says it. */
56
+ function when(unix) {
57
+ if (!unix)
58
+ return '';
59
+ const at = new Date(unix * 1000);
60
+ const now = new Date();
61
+ const same = at.getFullYear() === now.getFullYear();
62
+ return at.toLocaleDateString(undefined, {
63
+ month: 'short',
64
+ day: 'numeric',
65
+ ...(same ? {} : { year: 'numeric' }),
66
+ });
67
+ }
68
+ /**
69
+ * WHAT A DAY IS CALLED, from where it sits relative to now.
70
+ *
71
+ * "Saturday" is the right name for a day three weeks out and the wrong one for
72
+ * the day you are standing in — a reader looking at the planner already knows
73
+ * what today is, and being told the weekday makes them work out whether that IS
74
+ * today. The three days a person has a word for get that word; the rest get the
75
+ * weekday, and one outside this week gets its date too, because "Tuesday" stops
76
+ * being an answer once there is more than one of them in view.
77
+ */
78
+ function dayName(day, now = new Date()) {
79
+ const at = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
80
+ const days = Math.round((at(day) - at(now)) / 86400000);
81
+ if (days === 0)
82
+ return 'Today';
83
+ if (days === 1)
84
+ return 'Tomorrow';
85
+ if (days === -1)
86
+ return 'Yesterday';
87
+ const weekday = day.toLocaleDateString(undefined, { weekday: 'long' });
88
+ if (days > 1 && days < 7)
89
+ return weekday;
90
+ return `${weekday} ${day.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}`;
91
+ }
92
+ /** The value a date input wants, from unix seconds. */
93
+ const asInput = (unix) => unix ? new Date(unix * 1000).toISOString().slice(0, 10) : '';
94
+ // ── the card ────────────────────────────────────────────────────────────────
95
+ /**
96
+ * ONE CARD on the board.
97
+ *
98
+ * Everything on it is a field the row already carries, so a board of fifty
99
+ * costs the one read that drew it. The checklist count is the description's own
100
+ * task items — the same ones the forge renders.
101
+ */
102
+ function Tile({ card, who, onOpen, }) {
103
+ const list = steps(card.description);
104
+ const done = list.filter((s) => s.done).length;
105
+ const late = card.dueAt ? card.dueAt * 1000 < Date.now() : false;
106
+ return (_jsx(View, { render: _jsx("div", { draggable: true }), onDragStart: (e) => e.dataTransfer.setData('text/plain', `${card.projectKey}#${card.number}`), display: "block", cursor: "grab", children: _jsx(Box, { render: "button", onClick: onOpen, width: "100%", p: "$3", gap: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", hoverStyle: { borderColor: '$ink' }, children: _jsxs(YStack, { gap: "$2", width: "100%", children: [card.labels.length > 0 ? (_jsx(XStack, { gap: "$1", flexWrap: "wrap", children: card.labels.slice(0, 4).map((l) => (_jsx(Label, { name: l }, l))) })) : null, _jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 3, text: "left", children: card.title }), _jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [card.dueAt ? (_jsxs(XStack, { items: "center", gap: "$1", children: [_jsx(CalendarClock, { size: 12, "aria-hidden": true }), _jsx(Text, { fontSize: "$1", color: late ? BAD : '$soft', children: when(card.dueAt) })] })) : null, list.length > 0 ? (_jsxs(XStack, { items: "center", gap: "$1", children: [_jsx(ListChecks, { size: 12, "aria-hidden": true }), _jsxs(Text, { fontSize: "$1", color: done === list.length ? '$ink' : '$soft', children: [done, "/", list.length] })] })) : null, card.priority && card.priority !== 'none' ? (_jsx(Text, { fontSize: "$1", color: "$soft", children: card.priority })) : null, _jsx(YStack, { flex: 1 }), card.assignee ? (_jsx(Face, { src: who?.avatar, emoji: who?.emoji, name: who?.name ?? card.assignee, size: 20 })) : null] })] }) }) }));
107
+ }
108
+ // ── the card, opened ────────────────────────────────────────────────────────
109
+ /**
110
+ * THE CARD IN FULL, over the board.
111
+ *
112
+ * Each panel writes one thing and re-reads, so nothing here holds a second copy
113
+ * of the card that could disagree with the store. The controls that have no
114
+ * field behind them — a cover, a file — are absent rather than drawn and inert.
115
+ */
116
+ function Detail({ card, people, agents, onClose, onSaved, }) {
117
+ const { client } = useAi();
118
+ const [panel, setPanel] = useState(null);
119
+ const [title, setTitle] = useState(card.title);
120
+ const [body, setBody] = useState(card.description ?? '');
121
+ const [busy, setBusy] = useState(false);
122
+ const [wrong, setWrong] = useState(null);
123
+ const [step, setStep] = useState('');
124
+ /**
125
+ * THE DURABLE RUN this card's work is doing, if it was ever handed over.
126
+ *
127
+ * The board and the engine stay two stores; the workflow's NAME is the join,
128
+ * so this asks the engine directly rather than reading a column the card
129
+ * would have to keep in step. A card that was never started answers null,
130
+ * which the engine spells as an empty chain.
131
+ */
132
+ const [run, setRun] = useState(null);
133
+ const [starting, setStarting] = useState(false);
134
+ useEffect(() => {
135
+ if (!client)
136
+ return;
137
+ let live = true;
138
+ const stop = new AbortController();
139
+ void runOf(client, card.projectKey, card.number, stop.signal)
140
+ .then((r) => live && setRun(r))
141
+ // The engine being unreachable is not this card's failure to report; the
142
+ // run simply is not known, which is what null already means.
143
+ .catch(() => live && setRun(null));
144
+ return () => {
145
+ live = false;
146
+ stop.abort();
147
+ };
148
+ }, [client, card.projectKey, card.number]);
149
+ // WHAT THE STORE CALLS FINISHED. The column IS the status, so a card in the
150
+ // done column and a card marked done are one fact — reading it here rather
151
+ // than keeping a second flag is what keeps the circle and the column from
152
+ // ever disagreeing.
153
+ const finished = (card.status ?? '').toLowerCase() === 'done';
154
+ const list = steps(body);
155
+ const done = list.filter((s) => s.done).length;
156
+ const pct = list.length ? Math.round((done / list.length) * 100) : 0;
157
+ const who = people.find((p) => p.name === card.assignee);
158
+ const save = useCallback(async (patch) => {
159
+ if (!client)
160
+ return;
161
+ setBusy(true);
162
+ setWrong(null);
163
+ try {
164
+ await edit(client, card.projectKey, card.number, patch);
165
+ onSaved();
166
+ }
167
+ catch (e) {
168
+ setWrong(e instanceof Error ? e.message : 'The change was refused');
169
+ }
170
+ finally {
171
+ setBusy(false);
172
+ }
173
+ }, [client, card.projectKey, card.number, onSaved]);
174
+ // TICKING A STEP REWRITES ITS LINE, and only its line — the description is
175
+ // the reader's prose as well as the checklist, so a save that reformatted it
176
+ // would eat what they wrote around the boxes.
177
+ const flip = (line, next) => {
178
+ const written = tick(body, line, next);
179
+ setBody(written);
180
+ void save({ description: written });
181
+ };
182
+ return (_jsxs(YStack, { position: "absolute", t: 0, r: 0, b: 0, width: 560, maxW: "100%", bg: "$panel", borderLeftWidth: 1, borderColor: "$borderColor", z: "var(--z-sheet)", children: [_jsxs(XStack, { items: "center", gap: "$2", px: "$4", py: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$1", color: "$soft", flex: 1, numberOfLines: 1, children: card.identifier }), _jsx(Box, { render: "button", onClick: onClose, p: "$1", rounded: "$2", "aria-label": "Close", children: _jsx(X, { size: 16, "aria-hidden": true }) })] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", p: "$4", gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Box, { render: "button", onClick: () => void save({ status: finished ? 'todo' : 'done' }), "aria-label": finished ? 'Reopen this card' : 'Mark this card done', "aria-pressed": finished, width: 22, height: 22, shrink: 0, rounded: 9999, borderWidth: finished ? 0 : 1.5, borderColor: "$soft", bg: finished ? '$ink' : 'transparent', items: "center", justify: "center", hoverStyle: { borderColor: '$ink' }, children: finished ? _jsx(Check, { size: 13, color: "var(--background)", "aria-hidden": true }) : null }), _jsx(Text, { render: _jsx("input", { value: title, onChange: (e) => setTitle(e.target.value), onBlur: () => title.trim() && title !== card.title && void save({ title: title.trim() }), "aria-label": "Card title" }), flex: 1, minW: 0, bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "inherit", fontSize: "$7", fontWeight: "600", textDecorationLine: finished ? 'line-through' : 'none' })] }), _jsxs(XStack, { gap: "$1.5", flexWrap: "wrap", children: [_jsx(Act, { icon: Tag, label: "Labels", on: panel === 'labels', onPress: () => setPanel(panel === 'labels' ? null : 'labels') }), _jsx(Act, { icon: Calendar, label: "Dates", on: panel === 'dates', onPress: () => setPanel(panel === 'dates' ? null : 'dates') }), _jsx(Act, { icon: UserPlus, label: "Members", on: panel === 'members', onPress: () => setPanel(panel === 'members' ? null : 'members') })] }), panel === 'labels' ? (_jsxs(YStack, { gap: "$2", p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Labels" }), card.labels.length > 0 ? (_jsx(XStack, { gap: "$1", flexWrap: "wrap", children: card.labels.map((l) => (_jsx(Label, { name: l }, l))) })) : (_jsx(Text, { fontSize: "$1", color: "$soft", children: "None yet." })), _jsx(Text, { fontSize: "$1", color: "$soft", children: "A label is a word on the forge issue, and its colour comes from the word \u2014 so it reads the same here and there. Labels are added on the forge; this board does not write them yet." })] })) : null, panel === 'dates' ? (_jsxs(YStack, { gap: "$2", p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Dates" }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Start" }), _jsx(Text, { render: _jsx("input", { type: "date", value: asInput(card.startAt), readOnly: true }), ...DATE })] }), _jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Due" }), _jsx(Text, { render: _jsx("input", { type: "date", value: asInput(card.dueAt), readOnly: true }), ...DATE })] })] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "A card's dates come from its forge milestone, which is why they read here and cannot yet be set here." })] })) : null, panel === 'members' ? (_jsxs(YStack, { gap: "$2", p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Members" }), people.length === 0 ? (_jsx(Text, { fontSize: "$1", color: "$soft", children: "Nobody to assign yet." })) : (people.map((p) => (_jsx(Box, { render: "button", onClick: () => void save({ assignee: card.assignee === p.name ? '' : p.name }), width: "100%", p: "$2", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Face, { src: p.avatar, emoji: p.emoji, name: p.label, size: 22 }), _jsx(Text, { fontSize: "$2", color: "$ink", flex: 1, numberOfLines: 1, children: p.label }), p.agent ? (_jsx(Text, { fontSize: "$1", color: "$soft", children: "agent" })) : null, card.assignee === p.name ? _jsx(Check, { size: 14, "aria-hidden": true }) : null] }) }, p.id)))), _jsx(Box, { render: "button", onClick: () => {
183
+ if (!client || busy)
184
+ return;
185
+ setBusy(true);
186
+ void claim(client, card.projectKey, card.number)
187
+ .then(onSaved)
188
+ .catch((e) => setWrong(e.message))
189
+ .finally(() => setBusy(false));
190
+ }, p: "$2", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$1", color: "$ink", children: "Take it myself" }) })] })) : null, _jsxs(YStack, { gap: "$2", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Description" }), _jsx(Text, { render: _jsx("textarea", { value: body, onChange: (e) => setBody(e.target.value), onBlur: () => body !== (card.description ?? '') && void save({ description: body }), placeholder: "What is this card about?", rows: 5 }), width: "100%", bg: "transparent", borderWidth: 1, borderColor: "var(--border)", rounded: 8, p: 10, outlineStyle: "none", color: "inherit", fontSize: "$2" })] }), _jsxs(YStack, { gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", flex: 1, children: "Checklist" }), list.length > 0 ? (_jsxs(Text, { fontSize: "$1", color: "$soft", children: [pct, "%"] })) : null] }), list.length > 0 ? (_jsx(YStack, { height: 4, rounded: 9999, bg: "$hover", overflow: "hidden", children: _jsx(YStack, { width: `${pct}%`, height: 4, bg: "$ink" }) })) : null, list.map((s) => (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx("input", { type: "checkbox", checked: s.done, onChange: (e) => flip(s.line, e.target.checked), "aria-label": s.text }), _jsx(Text, { fontSize: "$2", color: s.done ? '$soft' : '$ink', textDecorationLine: s.done ? 'line-through' : 'none', flex: 1, children: s.text })] }, s.line))), _jsx(XStack, { gap: "$2", children: _jsx(Text, { render: _jsx("input", { value: step, onChange: (e) => setStep(e.target.value), onKeyDown: (e) => {
191
+ if (e.key !== 'Enter' || !step.trim())
192
+ return;
193
+ const written = addStep(body, step);
194
+ setBody(written);
195
+ setStep('');
196
+ void save({ description: written });
197
+ }, placeholder: "Add an item" }), flex: 1, bg: "transparent", borderWidth: 1, borderColor: "var(--border)", rounded: 6, px: 8, py: 6, outlineStyle: "none", color: "inherit", fontSize: "$2" }) })] }), _jsxs(XStack, { gap: "$3", flexWrap: "wrap", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Column" }), _jsxs(Select, { value: card.status, onValueChange: (v) => void save({ status: v }), children: [_jsx(SelectTrigger, { "aria-label": "Column", width: 160, children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: COLUMNS.map((c) => (_jsx(SelectItem, { value: c.id, children: c.label }, c.id))) })] })] }), _jsxs(YStack, { gap: "$1", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Priority" }), _jsxs(Select, { value: card.priority || 'none', onValueChange: (v) => void save({ priority: v }), children: [_jsx(SelectTrigger, { "aria-label": "Priority", width: 160, children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: PRIORITIES.map((p) => (_jsx(SelectItem, { value: p, children: p }, p))) })] })] })] }), _jsxs(YStack, { gap: "$2", pt: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Durable run" }), run ? (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(Text, { fontSize: "$2", color: "$ink", children: reads(run.status) }), _jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, children: run.workflowId }), run.historyLength ? (_jsxs(Text, { fontSize: "$1", color: "$soft", children: [run.historyLength, " events"] })) : null] })) : (_jsxs(XStack, { items: "center", gap: "$2", flexWrap: "wrap", children: [_jsx(Text, { fontSize: "$1", color: "$soft", flex: 1, children: card.assignee
198
+ ? `${card.assignee} holds this. Hand the work to the engine and it survives a crash.`
199
+ : 'Nobody holds this yet — it can still be run, and the run is durable either way.' }), _jsx(Box, { render: "button", onClick: () => {
200
+ if (!client || starting)
201
+ return;
202
+ setStarting(true);
203
+ setWrong(null);
204
+ void startRun(client, card.projectKey, card.number, {
205
+ title: card.title,
206
+ queue: AGENT_QUEUE,
207
+ type: AGENT_WORKFLOW,
208
+ })
209
+ .then(setRun)
210
+ .catch((e) => setWrong(e.message))
211
+ .finally(() => setStarting(false));
212
+ }, px: "$2.5", py: "$1.5", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$1", color: "$ink", children: starting ? 'Handing over…' : 'Run it' }) })] }))] }), who ? (_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Face, { src: who.avatar, emoji: who.emoji, name: who.label, size: 22 }), _jsxs(Text, { fontSize: "$2", color: "$soft", children: [who.label, " has this one."] })] })) : null, wrong ? (_jsx(Text, { fontSize: "$1", color: BAD, numberOfLines: 3, children: wrong })) : null] })] }));
213
+ }
214
+ /** A date the sheet shows and does not yet write. */
215
+ const DATE = {
216
+ bg: 'transparent',
217
+ color: 'inherit',
218
+ fontSize: '$2',
219
+ borderWidth: 1,
220
+ borderColor: 'var(--border)',
221
+ rounded: 6,
222
+ px: 8,
223
+ py: 6,
224
+ minW: 150,
225
+ };
226
+ // ── the planner ─────────────────────────────────────────────────────────────
227
+ /**
228
+ * THE DAY, beside the board.
229
+ *
230
+ * It draws the cards that are DUE on the day shown, against the hours — so the
231
+ * question it answers is "what is landing today", which is the one a board of
232
+ * columns cannot answer. A card with a due date but no hour sits at the top of
233
+ * the day rather than being placed at an hour nobody chose.
234
+ */
235
+ function Planner({ cards, onOpen }) {
236
+ const [offset, setOffset] = useState(0);
237
+ const day = useMemo(() => {
238
+ const d = new Date();
239
+ d.setDate(d.getDate() + offset);
240
+ d.setHours(0, 0, 0, 0);
241
+ return d;
242
+ }, [offset]);
243
+ const start = Math.floor(day.getTime() / 1000);
244
+ const due = cards.filter((c) => c.dueAt && c.dueAt >= start && c.dueAt < start + DAY);
245
+ return (_jsxs(YStack, { flex: 1, minW: 0, minH: 0, children: [_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Calendar, { size: 14, "aria-hidden": true }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", flex: 1, children: day.toLocaleDateString(undefined, { month: 'long' }) }), _jsx(Box, { render: "button", onClick: () => setOffset((n) => n - 1), p: "$1", rounded: "$2", "aria-label": "Previous day", children: _jsx(ChevronLeft, { size: 14, "aria-hidden": true }) }), _jsx(Box, { render: "button", onClick: () => setOffset(0), px: "$2", py: "$1", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", children: _jsx(Text, { fontSize: "$1", color: "$ink", children: "Today" }) }), _jsx(Box, { render: "button", onClick: () => setOffset((n) => n + 1), p: "$1", rounded: "$2", "aria-label": "Next day", children: _jsx(ChevronRight, { size: 14, "aria-hidden": true }) })] }), _jsxs(YStack, { items: "center", py: "$2", gap: "$1", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: dayName(day) }), _jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", children: day.getDate() })] }), _jsxs(YStack, { flex: 1, minH: 0, overflow: "scroll", children: [due.length > 0 ? (_jsxs(YStack, { p: "$3", gap: "$2", children: [_jsx(Text, { fontSize: "$1", color: "$soft", children: "Due today" }), due.map((c) => (_jsx(Box, { render: "button", onClick: () => onOpen(c), width: "100%", p: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", hoverStyle: { borderColor: '$ink' }, children: _jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 2, text: "left", children: c.title }) }, c.id)))] })) : null, Array.from({ length: 12 }, (_, i) => i + 8).map((hour) => (_jsx(XStack, { height: 56, borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(YStack, { width: 54, pt: "$1", pl: "$3", children: _jsxs(Text, { fontSize: "$1", color: "$soft", children: [hour % 12 === 0 ? 12 : hour % 12, " ", hour < 12 ? 'am' : 'pm'] }) }) }, hour)))] })] }));
246
+ }
247
+ // ── the board ───────────────────────────────────────────────────────────────
248
+ function Column({ column, cards, people, onDrop, onOpen, onAdd, loading, }) {
249
+ const [over, setOver] = useState(false);
250
+ const [adding, setAdding] = useState(false);
251
+ const [title, setTitle] = useState('');
252
+ return (_jsx(XStack, { onDragOver: (e) => {
253
+ e.preventDefault();
254
+ setOver(true);
255
+ }, onDragLeave: () => setOver(false), onDrop: (e) => {
256
+ e.preventDefault();
257
+ setOver(false);
258
+ const ref = e.dataTransfer.getData('text/plain');
259
+ if (ref)
260
+ onDrop(ref, column.id);
261
+ }, children: _jsxs(YStack, { width: 300, gap: "$2", p: "$2", rounded: "$3", borderWidth: 1, borderColor: over ? '$ink' : '$borderColor', bg: "$hover", maxH: "100%", children: [_jsxs(XStack, { items: "baseline", gap: "$2", px: "$2", pt: "$1", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", flex: 1, children: column.label }), _jsx(Text, { fontSize: "$1", color: "$soft", children: loading ? '' : cards.length })] }), _jsx(YStack, { gap: "$2", overflow: "scroll", shrink: 1, children: cards.map((c) => (_jsx(Tile, { card: c, who: people.find((p) => p.name === c.assignee), onOpen: () => onOpen(c) }, c.id))) }), adding ? (_jsx(YStack, { gap: "$2", children: _jsx(Text, { render: _jsx("textarea", { value: title, autoFocus: true, onChange: (e) => setTitle(e.target.value), onKeyDown: (e) => {
262
+ if (e.key === 'Escape') {
263
+ setAdding(false);
264
+ setTitle('');
265
+ }
266
+ if (e.key === 'Enter' && !e.shiftKey) {
267
+ e.preventDefault();
268
+ const said = title.trim();
269
+ if (!said)
270
+ return;
271
+ // THE TEXT SURVIVES A REFUSAL. Clearing before the answer is
272
+ // how a failed save costs somebody the sentence they wrote.
273
+ void onAdd(column.id, said).then((landed) => {
274
+ if (!landed)
275
+ return;
276
+ setTitle('');
277
+ setAdding(false);
278
+ });
279
+ }
280
+ }, placeholder: "What needs doing?", rows: 2 }), "$platform-web": { resize: 'none' }, width: "100%", bg: "var(--surface-card)", borderWidth: 1, borderColor: "var(--border)", rounded: 8, p: 8, outlineStyle: "none", color: "inherit", fontSize: "$2" }) })) : (_jsx(Box, { render: "button", onClick: () => setAdding(true), width: "100%", p: "$2", rounded: "$2",
281
+ // THE WHOLE ROW LIGHTS, not the words — it is a full-width target and
282
+ // a fill is what says so. The tint sits above the column's own
283
+ // ground so the row reads as raised rather than merely tinted.
284
+ hoverStyle: { bg: '$raised' }, children: _jsxs(XStack, { items: "center", gap: "$1.5", children: [_jsx(Plus, { size: 13, "aria-hidden": true }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Add a card" })] }) }))] }) }));
285
+ }
286
+ /** One narrowing, as a box a reader ticks. */
287
+ function Tick({ on, label, onPress, }) {
288
+ return (_jsx(Box, { render: "button", onClick: onPress, width: "100%", px: "$2", py: "$1.5", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(YStack, { width: 14, height: 14, rounded: 3, borderWidth: 1, borderColor: on ? '$ink' : '$borderColor', bg: on ? '$ink' : 'transparent', items: "center", justify: "center", shrink: 0, children: on ? _jsx(Check, { size: 10, color: "var(--background)", "aria-hidden": true }) : null }), typeof label === 'string' ? (_jsx(Text, { fontSize: "$2", color: "$ink", numberOfLines: 1, children: label })) : (label)] }) }));
289
+ }
290
+ /**
291
+ * THE FILTER, and it is what makes this board auditable.
292
+ *
293
+ * "Who did this" is the question a board of cards cannot answer by looking, and
294
+ * it is the one an org actually asks once agents work alongside people. The
295
+ * store answers it two ways at once — a row carries the `source` it was filed
296
+ * from and the `assignee` holding it — so "by an agent" means either, and this
297
+ * panel says so rather than picking one and being quietly wrong about the other.
298
+ *
299
+ * Every facet here is settled from a card the board already read. There is no
300
+ * narrowing that needs a second request, and none that needs a field the store
301
+ * does not keep.
302
+ */
303
+ function Filter({ want, labels, onWant, onClose, }) {
304
+ const set = (patch) => onWant({ ...want, ...patch });
305
+ const flipLabel = (l) => {
306
+ const held = want.labels ?? [];
307
+ set({ labels: held.includes(l) ? held.filter((x) => x !== l) : [...held, l] });
308
+ };
309
+ const Head = ({ children }) => (_jsx(Text, { fontSize: "$1", color: "$soft", px: "$2", pt: "$2", children: children }));
310
+ return (_jsxs(YStack, { position: "absolute", t: 52, r: 12, width: 300, maxH: 560, overflow: "scroll", p: "$2", gap: "$0.5", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", z: "var(--z-dock)", children: [_jsxs(XStack, { items: "center", px: "$2", py: "$1", children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", flex: 1, children: "Filter" }), _jsx(Box, { render: "button", onClick: onClose, p: "$1", rounded: "$2", "aria-label": "Close filter", children: _jsx(X, { size: 14, "aria-hidden": true }) })] }), _jsx(Tick, { on: want.holder === 'none', label: "Nobody has it", onPress: () => set({ holder: want.holder === 'none' ? undefined : 'none' }) }), _jsx(Tick, { on: want.holder === 'me', label: "Mine", onPress: () => set({ holder: want.holder === 'me' ? undefined : 'me' }) }), _jsx(Head, { children: "Who did the work" }), _jsx(Tick, { on: want.by === 'person', label: "A person", onPress: () => set({ by: want.by === 'person' ? undefined : 'person' }) }), _jsx(Tick, { on: want.by === 'agent', label: "An agent", onPress: () => set({ by: want.by === 'agent' ? undefined : 'agent' }) }), _jsx(Head, { children: "Card status" }), _jsx(Tick, { on: want.state === 'done', label: "Finished", onPress: () => set({ state: want.state === 'done' ? undefined : 'done' }) }), _jsx(Tick, { on: want.state === 'open', label: "Still open", onPress: () => set({ state: want.state === 'open' ? undefined : 'open' }) }), _jsx(Head, { children: "Due date" }), [['none', 'No dates'], ['late', 'Overdue'], ['day', 'Due in the next day'], ['week', 'Due in the next week'], ['month', 'Due in the next month']].map(([id, label]) => (_jsx(Tick, { on: want.due === id, label: label, onPress: () => set({ due: want.due === id ? undefined : id }) }, id))), labels.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Head, { children: "Labels" }), labels.map((l) => (_jsx(Tick, { on: (want.labels ?? []).includes(l), label: _jsx(Label, { name: l }), onPress: () => flipLabel(l) }, l)))] })) : null, _jsx(Head, { children: "Activity" }), [[7, 'Moved in the last week'], [14, 'Moved in the last two weeks'], [28, 'Moved in the last four weeks']].map(([days, label]) => (_jsx(Tick, { on: want.active === days, label: label, onPress: () => set({ active: want.active === days ? undefined : days }) }, days))), _jsx(Box, { render: "button", onClick: () => onWant({}), width: "100%", mt: "$2", p: "$2", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$1", color: "$ink", children: "Clear" }) })] }));
311
+ }
312
+ // ── the room ────────────────────────────────────────────────────────────────
313
+ /** Said when a write has no board to write to. It is the same event whichever
314
+ * write asked, so it is one sentence rather than two wordings of one. */
315
+ const NOWHERE = 'No project answered, so there is nowhere to file this card.';
316
+ function Cards() {
317
+ const { client } = useAi();
318
+ const { user } = useIam();
319
+ const { currentOrgId } = useOrganizations();
320
+ // WHO I AM on the forge, which is the name a card's assignee is spelled with.
321
+ const me = user?.name;
322
+ const { agents } = useAgents();
323
+ const { people: humans } = usePeople({ owner: currentOrgId ?? '', service: false });
324
+ const [cards, setCards] = useState(null);
325
+ const [key, setKey] = useState('');
326
+ const [keys, setKeys] = useState([]);
327
+ const [refused, setRefused] = useState(null);
328
+ // ONE STATE FOR TWO READINGS OF THE SAME CARDS. Two booleans — one per view
329
+ // — is how a room ends up showing neither or both, and the foot ended up
330
+ // lighting Board while the planner was the thing on screen.
331
+ const { aside, showAside } = useOpen();
332
+ const [view, setView] = useState('board');
333
+ const [opened, setOpened] = useState(null);
334
+ const [want, setWant] = useState({});
335
+ const [filtering, setFiltering] = useState(false);
336
+ const [nonce, bump] = useState(0);
337
+ // WHO CAN HAVE A CARD: the org's people and its agents, in one list, because
338
+ // the store keeps one assignee and does not ask which kind of worker it is.
339
+ const roster = useMemo(() => [
340
+ ...humans.map((p) => ({
341
+ id: p.id,
342
+ name: p.name,
343
+ label: p.displayName || p.name,
344
+ agent: false,
345
+ })),
346
+ ...agents.map((a) => ({
347
+ id: a.id,
348
+ name: a.name,
349
+ label: a.name,
350
+ avatar: a.avatar,
351
+ emoji: a.emoji,
352
+ agent: true,
353
+ })),
354
+ ], [humans, agents]);
355
+ useEffect(() => {
356
+ let live = true;
357
+ const stop = new AbortController();
358
+ void readProjects(client, stop.signal)
359
+ .then((ps) => {
360
+ if (!live)
361
+ return;
362
+ const ks = ps.map((p) => p.key).filter(Boolean);
363
+ setKeys(ks);
364
+ setKey((k) => k || ks[0] || '');
365
+ })
366
+ .catch(() => { });
367
+ void readBoard(client, key ? { key } : {}, stop.signal)
368
+ .then((rows) => live && (setCards(rows), setRefused(null)))
369
+ .catch((e) => live && (setCards([]), setRefused(say(e, 'this board'))));
370
+ return () => {
371
+ live = false;
372
+ stop.abort();
373
+ };
374
+ }, [client, key, nonce]);
375
+ // THE AGENT NAMES, so "an agent did this" can be answered about a card whose
376
+ // source says `team` but whose holder is a bot — which is what a person filing
377
+ // work and handing it over actually looks like.
378
+ const bots = useMemo(() => new Set(agents.map((a) => a.name)), [agents]);
379
+ const labels = useMemo(() => [...new Set((cards ?? []).flatMap((c) => c.labels))].sort(), [cards]);
380
+ const shown = useMemo(() => sift(cards ?? [], want, me, bots), [cards, want, me, bots]);
381
+ const narrowed = Object.values(want).some((v) => (Array.isArray(v) ? v.length : v !== undefined));
382
+ const filed = useMemo(() => {
383
+ const out = new Map();
384
+ for (const c of shown) {
385
+ const held = out.get(c.status);
386
+ if (held)
387
+ held.push(c);
388
+ else
389
+ out.set(c.status, [c]);
390
+ }
391
+ return out;
392
+ }, [shown]);
393
+ const move = useCallback((ref, status) => {
394
+ const [k, n] = ref.split('#');
395
+ // THE CARD'S OWN REF FIRST, then the board on screen, then the first one
396
+ // the store named — and nothing after that. The chain used to end
397
+ // `|| currentOrgId || 'hanzo'`, which files a write against a board whose
398
+ // owner nobody established: an org id is not a project key, and `hanzo` is
399
+ // somebody's real board. A write with no name to write under is refused.
400
+ const targetKey = k || key || keys[0];
401
+ if (!n)
402
+ return;
403
+ if (!targetKey)
404
+ return setRefused(NOWHERE);
405
+ void edit(client, targetKey, Number(n), { status })
406
+ .then(() => bump((x) => x + 1))
407
+ .catch((e) => setRefused(say(e, 'this card', 'save')));
408
+ }, [client, key, keys]);
409
+ /**
410
+ * Files a card, and ANSWERS WHETHER IT LANDED.
411
+ */
412
+ const add = useCallback(async (status, title) => {
413
+ const targetKey = key || keys[0];
414
+ if (!targetKey) {
415
+ setRefused(NOWHERE);
416
+ return false;
417
+ }
418
+ try {
419
+ await openCard(client, targetKey, { title, status });
420
+ setRefused(null);
421
+ if (!key) {
422
+ setKey(targetKey);
423
+ setKeys((prev) => (prev.includes(targetKey) ? prev : [...prev, targetKey]));
424
+ }
425
+ bump((x) => x + 1);
426
+ return true;
427
+ }
428
+ catch (e) {
429
+ setRefused(say(e, 'this card', 'save'));
430
+ return false;
431
+ }
432
+ }, [client, key, keys]);
433
+ return (_jsxs(YStack, { flex: 1, minH: 0, position: "relative", children: [_jsxs(XStack, { height: 52, shrink: 0, items: "center", gap: "$3", px: "$3", borderBottomWidth: 1, borderColor: "$borderColor", children: [_jsx(Text, { render: "h1", fontSize: "$4", fontWeight: "600", color: "$ink", numberOfLines: 1, children: "Board" }), _jsxs(Select, { value: key || 'all', onValueChange: setKey, children: [_jsx(SelectTrigger, { "aria-label": "Board", width: 140, $sm: { width: 100 }, children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All Boards" }, "all"), keys.map((k) => (_jsx(SelectItem, { value: k, children: k.charAt(0).toUpperCase() + k.slice(1) }, k)))] })] }), _jsx(YStack, { flex: 1 }), cards ? (_jsx(Text, { fontSize: "$1", color: "$soft", display: "none", $md: { display: 'flex' }, children: narrowed ? `${shown.length} of ${cards.length}` : `${cards.length} cards` })) : null, _jsx(Act, { icon: ListFilter, label: "Filter", on: filtering || narrowed, onPress: () => setFiltering((v) => !v) }), _jsx(XStack, { display: "none", $md: { display: 'flex' }, children: _jsx(Act, { icon: SquareTerminal, label: "Tabs & Shell", on: Boolean(aside), onPress: () => showAside(aside ? null : "tasks") }) })] }), _jsxs(XStack, { flex: 1, minH: 0, children: [view === 'planner' ? _jsx(Planner, { cards: cards ?? [], onOpen: setOpened }) : null, _jsxs(YStack, { flex: 1, minW: 0, minH: 0,
434
+ // One view at a time: the planner is the other reading of these cards.
435
+ display: view === 'planner' ? "none" : "flex", children: [refused ? (_jsxs(XStack, { items: "center", gap: "$2", mx: "$4", mt: "$3", px: "$3", py: "$2", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$hover", children: [_jsxs(Text, { fontSize: "$1", color: "$soft", flex: 1, minW: 0, children: [refused, " Cards will appear here when it answers."] }), _jsx(Box, { render: "button", onClick: () => bump((x) => x + 1), shrink: 0, px: "$2", py: "$1", rounded: "$2", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(Text, { fontSize: "$1", color: "$ink", children: "Try again" }) })] })) : null, _jsx(XStack, { flex: 1, minH: 0, overflow: "scroll", p: "$4", gap: "$3", items: "flex-start", children: COLUMNS.map((c) => (_jsx(Column, { column: c, cards: filed.get(c.id) ?? [], people: roster, onDrop: move, onOpen: setOpened, onAdd: add, loading: cards === null && !refused }, c.id))) })] })] }), _jsxs(XStack, { shrink: 0, items: "center", gap: "$2", px: "$3", py: "$2", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(Act, { icon: Columns3, label: "Board", on: view === 'board', onPress: () => setView('board') }), _jsx(Act, { icon: Calendar, label: "Planner", on: view === 'planner', onPress: () => setView('planner') }), _jsx(YStack, { flex: 1 }), _jsx(Text, { fontSize: "$1", color: "$soft", numberOfLines: 1, display: "none", $md: { display: 'flex' }, children: "A card is a forge issue \u2014 moving it here moves it there." })] }), filtering ? (_jsx(Filter, { want: want, labels: labels, onWant: setWant, onClose: () => setFiltering(false) })) : null, opened ? (_jsx(Detail, { card: cards?.find((c) => c.id === opened.id) ?? opened, people: roster, agents: agents, onClose: () => setOpened(null), onSaved: () => bump((x) => x + 1) })) : null] }));
436
+ }
437
+ /**
438
+ * The pane, and the session its reads need.
439
+ *
440
+ * SPLIT so the hooks below never run for a reader with none: every route this
441
+ * pane reads is org-scoped and answers 403 without one, and a hook cannot be
442
+ * called conditionally — so the condition is a component boundary rather than
443
+ * an `if` the rules forbid.
444
+ */
445
+ export function Board() {
446
+ return _jsx(Cards, {});
447
+ }
@@ -0,0 +1 @@
1
+ export declare function Boxes(): import("react").JSX.Element;