@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,250 @@
1
+ import { useState } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import { BonsaiBoard, type BoardTile } from "./board.js";
4
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
5
+ import { arrangeBento } from "@fungi.computer/maosaico";
6
+ import { Button } from "@fungi.computer/caps/components/button";
7
+ import { Select } from "@fungi.computer/caps/components/select";
8
+ import {
9
+ defaultLayout,
10
+ insightLayout,
11
+ savedLayout,
12
+ tileId,
13
+ tileTitles,
14
+ type InsightLayout,
15
+ type TileId,
16
+ } from "../shared/insights.js";
17
+ import { callMethod } from "./transport.js";
18
+ import { Failure, Loading } from "./parts.js";
19
+
20
+ function Grid({
21
+ repo,
22
+ initial,
23
+ definitions,
24
+ controlsTarget,
25
+ }: {
26
+ repo: string;
27
+ initial: InsightLayout["placements"];
28
+ definitions: BoardTile[];
29
+ controlsTarget: HTMLElement | null;
30
+ }) {
31
+ const client = useQueryClient();
32
+ const [tiles, setTiles] = useState(initial);
33
+ const [editing, setEditing] = useState(false);
34
+ const [saving, setSaving] = useState(false);
35
+ const [saved, setSaved] = useState(initial);
36
+ const [error, setError] = useState<Error | null>(null);
37
+ const [add, setAdd] = useState<TileId>("contributors");
38
+ const missing = tileId.options.filter(
39
+ (id) =>
40
+ !tiles.some((tile) => tile.id === id) &&
41
+ definitions.some((tile) => tile.id === id),
42
+ );
43
+ const selectedAdd = missing.includes(add) ? add : missing[0];
44
+ async function save() {
45
+ setSaving(true);
46
+ setError(null);
47
+ try {
48
+ await callMethod(
49
+ "insights.layout.save",
50
+ { repo, tiles: { view: "overview", placements: tiles } },
51
+ savedLayout,
52
+ );
53
+ client.setQueryData(["insight-layout", repo], {
54
+ tiles: { view: "overview", placements: tiles },
55
+ });
56
+ setSaved(tiles);
57
+ setEditing(false);
58
+ } catch (cause) {
59
+ setError(
60
+ cause instanceof Error ? cause : new Error("Could not save layout"),
61
+ );
62
+ } finally {
63
+ setSaving(false);
64
+ }
65
+ }
66
+ return (
67
+ <section className="bonsai-insights" aria-label="Repository overview">
68
+ {controlsTarget
69
+ ? createPortal(
70
+ <div className="flex flex-wrap items-center gap-2 bonsai-insight-actions">
71
+ {editing ? (
72
+ <>
73
+ {selectedAdd ? (
74
+ <>
75
+ <Select
76
+ aria-label="Visualization to add"
77
+ value={selectedAdd}
78
+ onChange={(event) => {
79
+ setAdd(tileId.parse(event.target.value));
80
+ }}
81
+ >
82
+ {missing.map((id) => (
83
+ <option key={id} value={id}>
84
+ {tileTitles[id]}
85
+ </option>
86
+ ))}
87
+ </Select>
88
+ <Button
89
+ size="sm"
90
+ variant="outline"
91
+ disabled={saving}
92
+ onClick={() => {
93
+ setTiles(
94
+ insightLayout.shape.placements.parse(
95
+ arrangeBento(
96
+ [
97
+ ...tiles,
98
+ {
99
+ id: selectedAdd,
100
+ col: 0,
101
+ row: 48,
102
+ w: 2,
103
+ h: 1,
104
+ },
105
+ ],
106
+ 4,
107
+ ),
108
+ ),
109
+ );
110
+ }}
111
+ >
112
+ Add
113
+ </Button>
114
+ </>
115
+ ) : null}
116
+ <Button
117
+ size="sm"
118
+ variant="ghost"
119
+ disabled={saving}
120
+ onClick={() => {
121
+ setTiles(defaultLayout.placements);
122
+ }}
123
+ >
124
+ Reset
125
+ </Button>
126
+ <Button
127
+ size="sm"
128
+ variant="ghost"
129
+ disabled={saving}
130
+ onClick={() => {
131
+ setTiles(saved);
132
+ setEditing(false);
133
+ setError(null);
134
+ }}
135
+ >
136
+ Cancel
137
+ </Button>
138
+ <Button
139
+ size="sm"
140
+ loading={saving}
141
+ onClick={() => void save()}
142
+ >
143
+ Save layout
144
+ </Button>
145
+ </>
146
+ ) : (
147
+ <Button
148
+ size="sm"
149
+ variant="outline"
150
+ onClick={() => {
151
+ setEditing(true);
152
+ }}
153
+ >
154
+ Arrange tiles
155
+ </Button>
156
+ )}
157
+ </div>,
158
+ controlsTarget,
159
+ )
160
+ : null}
161
+ {editing ? (
162
+ <p
163
+ className="text-sm text-[var(--stipe-fg-muted)] bonsai-muted"
164
+ role="status"
165
+ >
166
+ Drag Move or Resize. With a handle focused, use arrow keys; Escape
167
+ cancels a drag. Save keeps this layout for your account.
168
+ </p>
169
+ ) : null}
170
+ {error ? <Failure error={error} /> : null}
171
+ {tiles.length === 0 ? (
172
+ <p className="text-sm text-[var(--stipe-fg-muted)] bonsai-muted">
173
+ No tiles. Arrange tiles to add a visualization.
174
+ </p>
175
+ ) : null}
176
+ <BonsaiBoard
177
+ placements={tiles}
178
+ tiles={definitions}
179
+ phoneOrder={
180
+ editing
181
+ ? []
182
+ : [
183
+ "network",
184
+ "live",
185
+ "changes",
186
+ "actors",
187
+ "next",
188
+ "readme",
189
+ "contributors",
190
+ "ages",
191
+ "churn",
192
+ "calendar",
193
+ ]
194
+ }
195
+ editable={editing && !saving}
196
+ onChange={(next) => {
197
+ setTiles(
198
+ insightLayout.shape.placements.parse(
199
+ next.map((tile) => ({
200
+ ...tile,
201
+ col: Math.min(3, tile.col),
202
+ row: Math.min(48, tile.row),
203
+ w: Math.min(4, tile.w),
204
+ h: Math.min(4, tile.h),
205
+ })),
206
+ ),
207
+ );
208
+ }}
209
+ onRemove={(id) => {
210
+ setTiles(tiles.filter((tile) => tile.id !== id));
211
+ }}
212
+ />
213
+ </section>
214
+ );
215
+ }
216
+
217
+ export function OverviewBoard({
218
+ repo,
219
+ definitions,
220
+ controlsTarget,
221
+ }: {
222
+ repo: string;
223
+ definitions: BoardTile[];
224
+ controlsTarget: HTMLElement | null;
225
+ }) {
226
+ const layout = useQuery({
227
+ queryKey: ["insight-layout", repo],
228
+ queryFn: () => callMethod("insights.layout.get", { repo }, savedLayout),
229
+ staleTime: Infinity,
230
+ });
231
+ if (layout.isPending) return <Loading label="Loading your layout" />;
232
+ if (layout.isError)
233
+ return (
234
+ <Failure
235
+ error={layout.error}
236
+ retry={() => {
237
+ void layout.refetch();
238
+ }}
239
+ />
240
+ );
241
+ return (
242
+ <Grid
243
+ key={repo}
244
+ repo={repo}
245
+ initial={(layout.data.tiles ?? defaultLayout).placements}
246
+ definitions={definitions}
247
+ controlsTarget={controlsTarget}
248
+ />
249
+ );
250
+ }
@@ -0,0 +1,305 @@
1
+ import { BonsaiBoard } from "./board.js";
2
+ import { CardTitle } from "@fungi.computer/caps/components/card";
3
+ /**
4
+ * Issues as files: Markdown tickets under issues/ in the repository, checked
5
+ * by the shared issue validator, with the unblocked frontier first.
6
+ */
7
+ import { Badge } from "@fungi.computer/caps/components/badge";
8
+ import {
9
+ DataList,
10
+ DataListHeading,
11
+ DataRow,
12
+ } from "@fungi.computer/caps/components/data-list";
13
+ import {
14
+ Status,
15
+ type StatusTone,
16
+ } from "@fungi.computer/caps/components/status";
17
+ import { EmptyState } from "@fungi.computer/caps/blocks/empty-state";
18
+ import {
19
+ inspectIssueTree,
20
+ type IssueIndexRecord,
21
+ type IssueStatus,
22
+ } from "@fungi.computer/issues";
23
+ import { useMemo } from "react";
24
+ import { useIssueFiles } from "./api.js";
25
+ import { Failure, Loading } from "./parts.js";
26
+ import { href, Link } from "./router.js";
27
+ import { Markdown } from "./views.js";
28
+
29
+ const statusTone = {
30
+ "needs-triage": "neutral",
31
+ "needs-info": "warning",
32
+ "ready-for-agent": "info",
33
+ "ready-for-human": "info",
34
+ "in-progress": "primary",
35
+ resolved: "success",
36
+ wontfix: "neutral",
37
+ } satisfies Record<IssueStatus, StatusTone>;
38
+
39
+ const statusLabel = {
40
+ "needs-triage": "triage",
41
+ "needs-info": "needs info",
42
+ "ready-for-agent": "ready · agent",
43
+ "ready-for-human": "ready · person",
44
+ "in-progress": "in progress",
45
+ resolved: "resolved",
46
+ wontfix: "won't fix",
47
+ } satisfies Record<IssueStatus, string>;
48
+
49
+ function IssueRow({
50
+ repo,
51
+ refName,
52
+ issue,
53
+ records,
54
+ }: {
55
+ repo: string;
56
+ refName: string;
57
+ issue: IssueIndexRecord;
58
+ records: readonly IssueIndexRecord[];
59
+ }) {
60
+ return (
61
+ <DataRow className="grid-cols-[minmax(0,1fr)_auto]">
62
+ <div className="bonsai-commit-text">
63
+ <Link
64
+ to={href({
65
+ page: "repo",
66
+ repo,
67
+ tab: "issues",
68
+ ref: refName,
69
+ path: "",
70
+ file: issue.path,
71
+ })}
72
+ >
73
+ {issue.title}
74
+ </Link>
75
+ <p className="text-sm text-[var(--stipe-fg-muted)] bonsai-muted">
76
+ <code>{issue.id}</code>
77
+ {issue.assignee === null ? "" : ` · ${issue.assignee}`}
78
+ </p>
79
+ </div>
80
+ <div className="flex flex-wrap items-center justify-end gap-2">
81
+ <Status
82
+ size="compact"
83
+ tone={issue.blocked ? "warning" : statusTone[issue.status]}
84
+ glyph={
85
+ issue.status === "in-progress" && !issue.blocked ? "arc" : "dot"
86
+ }
87
+ label={
88
+ issue.blocked ? (
89
+ <>
90
+ blocked by{" "}
91
+ {issue.blocked_by.map((id, index) => {
92
+ const dependency = records.find((record) => record.id === id);
93
+ return (
94
+ <span key={id}>
95
+ {index > 0 ? ", " : ""}
96
+ {dependency ? (
97
+ <Link
98
+ to={href({
99
+ page: "repo",
100
+ repo,
101
+ tab: "issues",
102
+ ref: refName,
103
+ path: "",
104
+ file: dependency.path,
105
+ })}
106
+ >
107
+ {id}
108
+ </Link>
109
+ ) : (
110
+ id
111
+ )}
112
+ </span>
113
+ );
114
+ })}
115
+ </>
116
+ ) : (
117
+ statusLabel[issue.status]
118
+ )
119
+ }
120
+ />
121
+ <Badge
122
+ size="sm"
123
+ variant="quiet"
124
+ tone={
125
+ issue.priority === "p0" || issue.priority === "p1"
126
+ ? "warning"
127
+ : "neutral"
128
+ }
129
+ >
130
+ {issue.priority}
131
+ </Badge>
132
+ </div>
133
+ </DataRow>
134
+ );
135
+ }
136
+
137
+ export function IssuesPanel({
138
+ repo,
139
+ refName,
140
+ open,
141
+ }: {
142
+ repo: string;
143
+ refName: string;
144
+ open: string | null;
145
+ }) {
146
+ const files = useIssueFiles(repo, refName);
147
+ const report = useMemo(
148
+ () => (files.data ? inspectIssueTree(files.data.files) : null),
149
+ [files.data],
150
+ );
151
+ if (files.isPending) return <Loading label="Loading issues" />;
152
+ if (files.isError)
153
+ return (
154
+ <Failure
155
+ error={files.error}
156
+ retry={() => {
157
+ void files.refetch();
158
+ }}
159
+ />
160
+ );
161
+ if (report === null || Object.keys(files.data.files).length === 0)
162
+ return (
163
+ <EmptyState
164
+ variant="panel"
165
+ headingLevel={3}
166
+ title="No issues yet"
167
+ description="Issues live in the repository as Markdown under issues/<effort>/<NN>-<slug>.md, so agents read and update them with git."
168
+ />
169
+ );
170
+ const opened = open === null ? undefined : files.data.files[open];
171
+ if (open !== null && opened !== undefined)
172
+ return (
173
+ <div className="bonsai-issue">
174
+ <nav className="bonsai-crumbs" aria-label="Issue">
175
+ <Link
176
+ to={href({
177
+ page: "repo",
178
+ repo,
179
+ tab: "issues",
180
+ ref: refName,
181
+ path: "",
182
+ file: null,
183
+ })}
184
+ >
185
+ Issues
186
+ </Link>
187
+ <span>/ {open}</span>
188
+ </nav>
189
+ <div className="caps-card caps-card-surface bonsai-file">
190
+ <Markdown text={opened} />
191
+ </div>
192
+ </div>
193
+ );
194
+ const done = (issue: IssueIndexRecord) =>
195
+ issue.status === "resolved" || issue.status === "wontfix";
196
+ const groups = [
197
+ {
198
+ title: "Unblocked",
199
+ records: report.records.filter((issue) => !done(issue) && !issue.blocked),
200
+ },
201
+ {
202
+ title: "Blocked",
203
+ records: report.records.filter((issue) => !done(issue) && issue.blocked),
204
+ },
205
+ { title: "Done", records: report.records.filter(done) },
206
+ ];
207
+ return (
208
+ <BonsaiBoard
209
+ rowHeight={0}
210
+ placements={[
211
+ { id: "issues", col: 0, row: 0, w: 3, h: 1 },
212
+ { id: "issue-help", col: 3, row: 0, w: 1, h: 1 },
213
+ ]}
214
+ phoneOrder={["issues", "issue-help"]}
215
+ tiles={[
216
+ {
217
+ id: "issues",
218
+ title: "Issues",
219
+ autoHeight: true,
220
+ body: (
221
+ <div className="space-y-4">
222
+ {report.status === "fail" ? (
223
+ <section
224
+ className="caps-card caps-card-surface p-4 bonsai-issue-problems"
225
+ aria-labelledby="issue-problems"
226
+ >
227
+ <header className="bonsai-section-head">
228
+ <CardTitle level={2} id="issue-problems">
229
+ Issue files need fixing
230
+ </CardTitle>
231
+ <Badge size="sm" tone="error" variant="quiet">
232
+ {report.diagnostics.length}
233
+ </Badge>
234
+ </header>
235
+ <ul>
236
+ {report.diagnostics
237
+ .slice(0, 12)
238
+ .map((diagnostic, index) => (
239
+ <li key={`${diagnostic.path}:${String(index)}`}>
240
+ <code>{diagnostic.path}</code>{" "}
241
+ <span className="text-sm text-[var(--stipe-fg-muted)] bonsai-muted">
242
+ {diagnostic.detail}
243
+ </span>
244
+ </li>
245
+ ))}
246
+ </ul>
247
+ </section>
248
+ ) : null}
249
+ {report.records.length === 0 ? (
250
+ <DataList className="bonsai-change-list" density="compact">
251
+ {Object.keys(files.data.files).map((path) => (
252
+ <DataRow key={path}>
253
+ <Link
254
+ to={href({
255
+ page: "repo",
256
+ repo,
257
+ tab: "issues",
258
+ ref: refName,
259
+ path: "",
260
+ file: path,
261
+ })}
262
+ >
263
+ {path}
264
+ </Link>
265
+ </DataRow>
266
+ ))}
267
+ </DataList>
268
+ ) : null}
269
+ {groups
270
+ .filter((group) => group.records.length > 0)
271
+ .map((group) => (
272
+ <section key={group.title}>
273
+ <DataListHeading level={3}>{group.title}</DataListHeading>
274
+ <DataList>
275
+ {group.records.map((issue) => (
276
+ <IssueRow
277
+ key={issue.id}
278
+ repo={repo}
279
+ refName={refName}
280
+ issue={issue}
281
+ records={report.records}
282
+ />
283
+ ))}
284
+ </DataList>
285
+ </section>
286
+ ))}
287
+ </div>
288
+ ),
289
+ },
290
+ {
291
+ id: "issue-help",
292
+ title: "How issues work",
293
+ autoHeight: true,
294
+ body: (
295
+ <p>
296
+ Issues are Markdown files under <code>issues/</code>. Agents and
297
+ people edit them with git. Dependencies link each issue to the
298
+ work that must finish first.
299
+ </p>
300
+ ),
301
+ },
302
+ ]}
303
+ />
304
+ );
305
+ }
package/web/main.tsx ADDED
@@ -0,0 +1,72 @@
1
+ import { HomePage } from "./home-page.js";
2
+ import { ChangePage } from "./change-page.js";
3
+ import { mountApp, useWindowStatus } from "@fungi.computer/agar/react";
4
+ import type { AppRoot } from "@fungi.computer/agar";
5
+ import { QueryClient } from "@tanstack/react-query";
6
+ import { StrictMode } from "react";
7
+ import "@fungi.computer/caps/fonts.css";
8
+ import "./bonsai.css";
9
+ import { productName } from "../shared/contract.js";
10
+ import { useLive } from "./api.js";
11
+ import { CommitPage, RepoPage } from "./pages.js";
12
+ import { connect } from "./transport.js";
13
+ import { RouterProvider, useRouter } from "./router.js";
14
+
15
+ function Shell({ app }: { app: AppRoot | null }) {
16
+ const { route } = useRouter();
17
+ const live = useLive();
18
+ // Bonsai's one status bar is the window footer: the Live/Connecting
19
+ // readout goes there, the same way Shed and Inkcap report their state.
20
+ useWindowStatus(app, [
21
+ {
22
+ kind: "text",
23
+ id: "live",
24
+ text: live.error ?? (live.connected ? "Live" : "Connecting"),
25
+ tone:
26
+ live.error !== null ? "error" : live.connected ? "success" : "neutral",
27
+ },
28
+ ]);
29
+ return (
30
+ <div className="caps-app-canvas bonsai-shell">
31
+ <main className="bonsai-page">
32
+ {route.page === "home" ? (
33
+ <HomePage live={live} />
34
+ ) : route.page === "repo" ? (
35
+ <RepoPage key={route.repo} route={route} live={live} />
36
+ ) : route.page === "change" ? (
37
+ <ChangePage
38
+ key={`${route.repo}:${String(route.number)}`}
39
+ repo={route.repo}
40
+ number={route.number}
41
+ />
42
+ ) : (
43
+ <CommitPage repo={route.repo} hash={route.hash} />
44
+ )}
45
+ </main>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ const client = new QueryClient({
51
+ defaultOptions: {
52
+ queries: { staleTime: 10_000, refetchOnWindowFocus: false, retry: 1 },
53
+ },
54
+ });
55
+
56
+ const root = document.getElementById("bonsai-root");
57
+ if (!root) throw new Error("Bonsai's root element is missing");
58
+ mountApp(
59
+ root,
60
+ { name: productName, queryClient: client },
61
+ ({ app, status }) => {
62
+ if (status === "connecting") return null;
63
+ connect(app);
64
+ return (
65
+ <StrictMode>
66
+ <RouterProvider>
67
+ <Shell app={app} />
68
+ </RouterProvider>
69
+ </StrictMode>
70
+ );
71
+ },
72
+ );