@fungi.computer/bonsai 0.0.0-stage → 0.0.1

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 (132) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +196 -2
  4. package/backend/background.ts +259 -0
  5. package/backend/change-copy.ts +99 -0
  6. package/backend/changes.ts +530 -0
  7. package/backend/diff.ts +228 -0
  8. package/backend/file-edits.ts +850 -0
  9. package/backend/files-copy.ts +361 -0
  10. package/backend/gills.ts +125 -0
  11. package/backend/git.ts +123 -0
  12. package/backend/graph-commit-copies.ts +321 -0
  13. package/backend/history.ts +178 -0
  14. package/backend/index.ts +144 -0
  15. package/backend/insights.ts +256 -0
  16. package/backend/merge.ts +999 -0
  17. package/backend/methods.ts +835 -0
  18. package/backend/policy.ts +160 -0
  19. package/backend/read-copy-page.ts +77 -0
  20. package/backend/repository-copy.ts +70 -0
  21. package/backend/reviews.ts +317 -0
  22. package/backend/service.ts +892 -0
  23. package/backend/state.ts +914 -0
  24. package/demo/koi-pond/README.md +17 -0
  25. package/demo/koi-pond/index.html +11 -0
  26. package/demo/koi-pond/package.json +13 -0
  27. package/demo/koi-pond/pnpm-lock.yaml +450 -0
  28. package/demo/koi-pond/src/fish.ts +32 -0
  29. package/demo/koi-pond/src/main.ts +24 -0
  30. package/demo/koi-pond/src/pond.ts +27 -0
  31. package/docs/media/bonsai-insights.png +0 -0
  32. package/docs/media/overview-phone.png +0 -0
  33. package/docs/media/overview.png +0 -0
  34. package/package.json +152 -3
  35. package/scripts/agent.sh +55 -0
  36. package/scripts/demo.sh +233 -0
  37. package/scripts/rebuild-copies.mjs +34 -0
  38. package/scripts/release.mjs +52 -0
  39. package/scripts/runtime-proof.mjs +299 -0
  40. package/scripts/seed-demo.mjs +724 -0
  41. package/scripts/seed-step.mjs +37 -0
  42. package/scripts/shed-terminal-proof.mjs +774 -0
  43. package/shared/contract.ts +404 -0
  44. package/shared/hooks.ts +208 -0
  45. package/shared/insights.ts +90 -0
  46. package/shared/picker-options.ts +23 -0
  47. package/shared/presentation.ts +21 -0
  48. package/shared/repository-deletion.ts +4 -0
  49. package/test/apps-build-install.test.ts +302 -0
  50. package/test/background-runtime.test.ts +206 -0
  51. package/test/browser/board.browser.mjs +52 -0
  52. package/test/browser/capture.browser.mjs +1264 -0
  53. package/test/browser/edit-in-inkcap.browser.mjs +83 -0
  54. package/test/browser/repository-settings-fixture.tsx +108 -0
  55. package/test/browser/repository-settings.browser.mjs +136 -0
  56. package/test/browser/repository-settings.html +10 -0
  57. package/test/browser/visual-fixture.tsx +667 -0
  58. package/test/browser/visual.html +10 -0
  59. package/test/browser/vite.config.mjs +9 -0
  60. package/test/change-page.test.ts +916 -0
  61. package/test/changes.test.ts +1063 -0
  62. package/test/checks-workflow-template.test.mjs +234 -0
  63. package/test/diff.test.ts +70 -0
  64. package/test/edit-in-inkcap.test.ts +227 -0
  65. package/test/file-edits.test.ts +2714 -0
  66. package/test/files-read-copy-calls.test.ts +374 -0
  67. package/test/fork-adoption.test.ts +111 -0
  68. package/test/gills.test.ts +125 -0
  69. package/test/git-binding.fixture.ts +30 -0
  70. package/test/git-push.config.jsonc +9 -0
  71. package/test/git-push.native.mjs +413 -0
  72. package/test/git-push.worker.ts +280 -0
  73. package/test/graph-commit-read-copy-calls.test.ts +401 -0
  74. package/test/home-page.test.ts +170 -0
  75. package/test/hooks.test.ts +250 -0
  76. package/test/insight-data.test.ts +51 -0
  77. package/test/insight-layout.test.mjs +230 -0
  78. package/test/insights.test.ts +154 -0
  79. package/test/merge-background.test.ts +756 -0
  80. package/test/merge-conflict.test.ts +337 -0
  81. package/test/merge-declaration.test.ts +46 -0
  82. package/test/merge-repro.test.ts +561 -0
  83. package/test/merge-template-build.test.mjs +234 -0
  84. package/test/network-layout.test.ts +200 -0
  85. package/test/outbox.test.ts +424 -0
  86. package/test/parts.test.ts +86 -0
  87. package/test/pkt-line.test.ts +54 -0
  88. package/test/policy.test.ts +172 -0
  89. package/test/read-copy-calls.test.ts +691 -0
  90. package/test/read-copy-composer.test.ts +388 -0
  91. package/test/read-copy-lifetimes.test.ts +214 -0
  92. package/test/read-copy-rpc.config.jsonc +12 -0
  93. package/test/read-copy-rpc.native.mjs +47 -0
  94. package/test/read-copy-rpc.worker.ts +92 -0
  95. package/test/recent-activity.test.ts +62 -0
  96. package/test/repository-deletion.test.ts +500 -0
  97. package/test/seed-resume.test.mjs +91 -0
  98. package/test/storyboard-save-https-git.fixture.mjs +119 -0
  99. package/test/storyboard-save-template.test.mjs +545 -0
  100. package/test/ui-errors.test.ts +976 -0
  101. package/test/workflow-store.test.ts +115 -0
  102. package/tsconfig.json +36 -0
  103. package/turbo.jsonc +25 -0
  104. package/vite.config.ts +19 -0
  105. package/vitest.config.ts +5 -0
  106. package/web/api.ts +224 -0
  107. package/web/app-build-panel.tsx +171 -0
  108. package/web/board.tsx +259 -0
  109. package/web/bonsai.css +642 -0
  110. package/web/change-page.tsx +715 -0
  111. package/web/change-rows.tsx +135 -0
  112. package/web/code-tab.tsx +267 -0
  113. package/web/commit-tree.tsx +152 -0
  114. package/web/format.ts +54 -0
  115. package/web/home-page.tsx +274 -0
  116. package/web/index.html +17 -0
  117. package/web/insight-data.ts +76 -0
  118. package/web/insight-tiles.tsx +534 -0
  119. package/web/insights.tsx +250 -0
  120. package/web/issues-panel.tsx +305 -0
  121. package/web/main.tsx +72 -0
  122. package/web/network-graph.tsx +647 -0
  123. package/web/overview.tsx +339 -0
  124. package/web/pages.tsx +469 -0
  125. package/web/parts.tsx +381 -0
  126. package/web/public/favicon.svg +1 -0
  127. package/web/repo-navigation.tsx +256 -0
  128. package/web/repo-settings.tsx +145 -0
  129. package/web/router.tsx +151 -0
  130. package/web/transport.ts +91 -0
  131. package/web/views.tsx +151 -0
  132. package/web/working-now.tsx +98 -0
@@ -0,0 +1,339 @@
1
+ import { useMemo } from "react";
2
+ import { EmptyState } from "@fungi.computer/caps/blocks/empty-state";
3
+ import { CopyField } from "@fungi.computer/caps/components/copy-field";
4
+ import {
5
+ DataList,
6
+ DataListHeading,
7
+ DataRow,
8
+ } from "@fungi.computer/caps/components/data-list";
9
+ import { inspectIssueTree } from "@fungi.computer/issues";
10
+ import type { RepoDetail } from "../shared/contract.js";
11
+ import { tileTitles, type TileId } from "../shared/insights.js";
12
+ import {
13
+ useChanges,
14
+ useGraph,
15
+ useAgents,
16
+ useEvents,
17
+ useFiles,
18
+ useFile,
19
+ useIssueFiles,
20
+ type LiveState,
21
+ } from "./api.js";
22
+ import { OverviewBoard } from "./insights.js";
23
+ import { InsightTile } from "./insight-tiles.js";
24
+ import { NetworkGraph } from "./network-graph.js";
25
+ import { ActivityFeed, Failure, Loading, type AgentColors } from "./parts.js";
26
+ import { WorkingNow, workingNow } from "./working-now.js";
27
+ import { ChangeGroups } from "./change-rows.js";
28
+ import { Markdown } from "./views.js";
29
+ import { href, Link, useRouter, type Route } from "./router.js";
30
+ import type { BoardTile } from "./board.js";
31
+
32
+ export function Overview({
33
+ route,
34
+ detail,
35
+ colors,
36
+ live,
37
+ controlsTarget,
38
+ }: {
39
+ route: Extract<Route, { page: "repo" }>;
40
+ detail: RepoDetail;
41
+ colors: AgentColors;
42
+ live: LiveState;
43
+ controlsTarget: HTMLElement | null;
44
+ }) {
45
+ const { go } = useRouter();
46
+ const family = detail.repo.parent ?? route.repo;
47
+ const refName = route.ref ?? detail.defaultBranch;
48
+ const graph = useGraph(family, !detail.empty);
49
+ const actors = useAgents();
50
+ const events = useEvents(family);
51
+ const changes = useChanges(family);
52
+ const files = useFiles(route.repo, refName, !detail.empty);
53
+ const readmePath =
54
+ files.data?.paths.find((path) => /^readme(\.md)?$/iu.test(path)) ?? null;
55
+ const readme = useFile(route.repo, refName, readmePath);
56
+ const issueFiles = useIssueFiles(route.repo, refName);
57
+ const issues = useMemo(
58
+ () => (issueFiles.data ? inspectIssueTree(issueFiles.data.files) : null),
59
+ [issueFiles.data],
60
+ );
61
+ const next =
62
+ issues?.records
63
+ .filter((issue) => issues.frontier.includes(issue.id))
64
+ .sort((a, b) => a.priority.localeCompare(b.priority)) ?? [];
65
+ const activity = events.data?.events ?? [];
66
+ const working = workingNow(activity);
67
+ const recentHeads = new Set(
68
+ activity
69
+ .filter(
70
+ (event) =>
71
+ event.type === "push" && event.at >= Date.now() - 15 * 60_000,
72
+ )
73
+ .flatMap((event) =>
74
+ event.type === "push"
75
+ ? event.data.updates.map((update) => update.after)
76
+ : [],
77
+ ),
78
+ );
79
+ const repoLink = (
80
+ tab: Extract<Route, { page: "repo" }>["tab"],
81
+ file: string | null = null,
82
+ ) => href({ ...route, tab, file, path: "" });
83
+ if (detail.empty)
84
+ return (
85
+ <div className="space-y-3">
86
+ <p>
87
+ Push a first commit to start this repository. Add a README to
88
+ introduce the project.
89
+ </p>
90
+ <CopyField value={`git clone ${detail.cloneUrl}`} />
91
+ </div>
92
+ );
93
+ const tiles: BoardTile[] = [];
94
+ const add = (
95
+ id: TileId,
96
+ body: BoardTile["body"],
97
+ status?: BoardTile["status"],
98
+ ) => {
99
+ tiles.push({ id, title: tileTitles[id], body, status, autoHeight: true });
100
+ };
101
+ add(
102
+ "network",
103
+ graph.isPending ? (
104
+ <Loading label="Loading network" />
105
+ ) : graph.isError ? (
106
+ <Failure
107
+ error={graph.error}
108
+ retry={() => {
109
+ void graph.refetch();
110
+ }}
111
+ />
112
+ ) : (
113
+ <>
114
+ <NetworkGraph
115
+ graph={graph.data}
116
+ colors={colors}
117
+ fresh={recentHeads}
118
+ laneDetails={
119
+ new Map(
120
+ (changes.data?.changes ?? []).map((change) => [
121
+ change.sourceRepo,
122
+ `${String(change.ahead)} ahead · ${String(change.behind)} behind`,
123
+ ]),
124
+ )
125
+ }
126
+ onLaneSelect={(lane) => {
127
+ const change = changes.data?.changes.find(
128
+ (item) =>
129
+ item.sourceRepo === lane.repo &&
130
+ item.sourceRef === lane.ref &&
131
+ item.state === "open",
132
+ );
133
+ if (change)
134
+ go(
135
+ href({
136
+ page: "change",
137
+ repo: change.repo,
138
+ number: change.number,
139
+ }),
140
+ );
141
+ else
142
+ go(
143
+ href({
144
+ ...route,
145
+ repo: lane.repo,
146
+ tab: "commits",
147
+ ref: lane.ref,
148
+ file: null,
149
+ }),
150
+ );
151
+ }}
152
+ onSelect={(node) => {
153
+ go(
154
+ href({
155
+ ...route,
156
+ repo: family,
157
+ tab: "commits",
158
+ ref: null,
159
+ commit: node.hash,
160
+ }),
161
+ );
162
+ }}
163
+ />
164
+ <p className="text-xs text-[var(--stipe-fg-muted)]">
165
+ Branches and forks · hollow nodes are merges · select a lane for its
166
+ change, or a commit to read it
167
+ </p>
168
+ </>
169
+ ),
170
+ );
171
+ if (events.isPending) add("live", <Loading label="Loading activity" />);
172
+ else if (events.isError)
173
+ add(
174
+ "live",
175
+ <Failure
176
+ error={events.error}
177
+ retry={() => {
178
+ void events.refetch();
179
+ }}
180
+ />,
181
+ );
182
+ else if (activity.length > 0)
183
+ add(
184
+ "live",
185
+ <>
186
+ {working.length > 0 ? (
187
+ <>
188
+ <DataListHeading>Working now</DataListHeading>
189
+ <WorkingNow events={activity} colors={colors} />
190
+ </>
191
+ ) : null}
192
+ <ActivityFeed
193
+ events={activity.slice(0, 5)}
194
+ colors={colors}
195
+ fresh={live.fresh}
196
+ />
197
+ </>,
198
+ );
199
+ if (changes.isPending) add("changes", <Loading label="Loading changes" />);
200
+ else if (changes.isError)
201
+ add(
202
+ "changes",
203
+ <Failure
204
+ error={changes.error}
205
+ retry={() => {
206
+ void changes.refetch();
207
+ }}
208
+ />,
209
+ );
210
+ else if (changes.data.changes.length)
211
+ add(
212
+ "changes",
213
+ <ChangeGroups changes={changes.data.changes} colors={colors} limit={3} />,
214
+ <Link to={repoLink("changes")}>All {changes.data.changes.length} ›</Link>,
215
+ );
216
+ else
217
+ add(
218
+ "changes",
219
+ <EmptyState
220
+ variant="quiet"
221
+ headingLevel={3}
222
+ title="No changes yet"
223
+ description="Proposed updates will appear here."
224
+ />,
225
+ );
226
+ // Nothing real to show yet: render nothing rather than a stuck loader (a
227
+ // read a visitor can't complete, such as an anonymous one, never
228
+ // resolves). The tile appears once the read settles.
229
+ if (issueFiles.isError)
230
+ add(
231
+ "next",
232
+ <Failure
233
+ error={issueFiles.error}
234
+ retry={() => {
235
+ void issueFiles.refetch();
236
+ }}
237
+ />,
238
+ );
239
+ else if (next.length > 0)
240
+ add(
241
+ "next",
242
+ <>
243
+ <DataList>
244
+ {next.slice(0, 3).map((issue) => (
245
+ <DataRow key={issue.id}>
246
+ <Link to={repoLink("issues", issue.path)}>{issue.title}</Link>
247
+ </DataRow>
248
+ ))}
249
+ </DataList>
250
+ <p className="text-xs text-[var(--stipe-fg-muted)]">
251
+ {issues?.records.filter((issue) => issue.blocked).length ?? 0} blocked
252
+ </p>
253
+ </>,
254
+ <Link to={repoLink("issues")}>Issues ›</Link>,
255
+ );
256
+ else if (!issueFiles.isPending)
257
+ add(
258
+ "next",
259
+ <EmptyState
260
+ variant="quiet"
261
+ headingLevel={3}
262
+ title="Nothing next"
263
+ description="No open issues are waiting on anyone."
264
+ />,
265
+ );
266
+ if (files.isError)
267
+ add(
268
+ "readme",
269
+ <Failure
270
+ error={files.error}
271
+ retry={() => {
272
+ void files.refetch();
273
+ }}
274
+ />,
275
+ );
276
+ else if (files.isPending || readmePath !== null)
277
+ add(
278
+ "readme",
279
+ files.isPending || readme.isPending ? (
280
+ <Loading label="Loading README" />
281
+ ) : readme.isError ? (
282
+ <Failure
283
+ error={readme.error}
284
+ retry={() => {
285
+ void readme.refetch();
286
+ }}
287
+ />
288
+ ) : (
289
+ <div className="line-clamp-6">
290
+ <Markdown text={readme.data.text ?? "README is not a text file."} />
291
+ </div>
292
+ ),
293
+ <Link to={repoLink("code", readmePath)}>Open in Code ›</Link>,
294
+ );
295
+ for (const id of [
296
+ "actors",
297
+ "contributors",
298
+ "ages",
299
+ "churn",
300
+ "calendar",
301
+ ] as const)
302
+ add(
303
+ id,
304
+ <div className="bonsai-tile-content">
305
+ {graph.isPending || actors.isPending ? (
306
+ <Loading label={`Loading ${tileTitles[id].toLowerCase()}`} />
307
+ ) : graph.isError ? (
308
+ <Failure
309
+ error={graph.error}
310
+ retry={() => {
311
+ void graph.refetch();
312
+ }}
313
+ />
314
+ ) : actors.isError ? (
315
+ <Failure
316
+ error={actors.error}
317
+ retry={() => {
318
+ void actors.refetch();
319
+ }}
320
+ />
321
+ ) : (
322
+ <InsightTile
323
+ id={id}
324
+ repo={route.repo}
325
+ refName={refName}
326
+ graph={graph.data}
327
+ actors={actors.data.agents}
328
+ />
329
+ )}
330
+ </div>,
331
+ );
332
+ return (
333
+ <OverviewBoard
334
+ repo={route.repo}
335
+ definitions={tiles}
336
+ controlsTarget={controlsTarget}
337
+ />
338
+ );
339
+ }