@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,667 @@
1
+ import { HomePage } from "../../web/home-page.js";
2
+ import { useEffect, useState } from "react";
3
+ import { BonsaiBoard } from "../../web/board.js";
4
+ /** Presentation fixture: real Bonsai pages, deterministic Git/host responses. */
5
+ import * as z from "zod";
6
+ import { savedLayout, insightLayout } from "../../shared/insights.js";
7
+ import { createRoot } from "react-dom/client";
8
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
9
+ import { createCapabilityRegistry } from "@fungi.computer/agar";
10
+ import { appsOpen } from "@fungi.computer/agar/apps";
11
+ import { appMethods } from "@fungi.computer/agar/backend";
12
+ import { connect } from "../../web/transport.js";
13
+
14
+ import type { Graph, RepoDetail } from "../../shared/contract.js";
15
+ import "@fungi.computer/caps/fonts.css";
16
+ import { RepoPage, CommitPage } from "../../web/pages.js";
17
+ import { ChangePage } from "../../web/change-page.js";
18
+ import { changePage, changeDetail } from "../../shared/contract.js";
19
+ import { RouterProvider, useRouter } from "../../web/router.js";
20
+ import "../../web/bonsai.css";
21
+
22
+ const hash = (n: number) =>
23
+ (Math.imul(n, 2654435761) >>> 0).toString(16).padStart(8, "0") +
24
+ n.toString(16).padStart(32, "0");
25
+ const now = Date.UTC(2026, 9, 12) / 1000;
26
+ const agents = ["Levi", "Luna", "Astra", "Sol"].map((name, i) => ({
27
+ id: name,
28
+ name,
29
+ kind: i === 0 ? "human" : "agent",
30
+ color: `c${String(i)}`,
31
+ createdAt: 0,
32
+ }));
33
+ const graph: Graph = {
34
+ lanes: agents.map((agent, i) => ({
35
+ id: `${agent.name}:main`,
36
+ repo: i ? `koi-pond--${agent.name}` : "koi-pond",
37
+ ref: "main",
38
+ owner: i ? agent.name : null,
39
+ head: hash(37 + i),
40
+ canonical: i === 0,
41
+ })),
42
+ nodes: Array.from({ length: 40 }, (_, i) => {
43
+ const lane = i % 4;
44
+ const actor = agents[lane];
45
+ return {
46
+ hash: hash(i + 1),
47
+ parents:
48
+ i === 0
49
+ ? []
50
+ : [hash(Math.max(1, i - 3)), ...(i % 8 === 0 ? [hash(i)] : [])],
51
+ message:
52
+ [
53
+ "Let the pond breathe",
54
+ "Add ripples around the koi",
55
+ "Keep fish inside the reeds",
56
+ "Balance the evening light",
57
+ ][lane] ?? "Pond",
58
+ author: actor?.name ?? "Levi",
59
+ agent: actor?.name ?? null,
60
+ origin: lane ? `koi-pond--${actor?.name ?? "Luna"}` : "koi-pond",
61
+ committedAt: now - (40 - i) * 86400,
62
+ lanes: [`${actor?.name ?? "Levi"}:main`],
63
+ };
64
+ }).reverse(),
65
+ };
66
+ const detail: RepoDetail = {
67
+ repo: {
68
+ name: "koi-pond",
69
+ description: "A small living world, made together.",
70
+ parent: null,
71
+ owner: null,
72
+ createdAt: 0,
73
+ lastPushAt: now * 1000,
74
+ forks: agents.slice(1).map((a) => `koi-pond--${a.name}`),
75
+ openChanges: 3,
76
+ activity: [1, 3, 2, 5],
77
+ },
78
+ defaultBranch: "main",
79
+ refs: [{ name: "refs/heads/main", hash: hash(40) }],
80
+ cloneUrl: "https://example.test/git/koi-pond",
81
+ empty: false,
82
+ };
83
+ const scene5 = new URLSearchParams(location.search).has("scene5");
84
+ if (scene5)
85
+ detail.cloneUrl = "https://oyster.shiit.app/git/bonsai/koi-pond.git";
86
+ const changeScenario = new URLSearchParams(location.search).get("change");
87
+ const empty = new URLSearchParams(location.search).has("empty");
88
+ const noChanges =
89
+ empty || new URLSearchParams(location.search).has("no-changes");
90
+ if (empty) {
91
+ graph.nodes = [];
92
+ graph.lanes = [];
93
+ detail.empty = true;
94
+ }
95
+ const client = new QueryClient({
96
+ defaultOptions: { queries: { staleTime: Infinity, retry: false } },
97
+ });
98
+ client.setQueryData(["repo", "koi-pond"], detail);
99
+ client.setQueryData(["graph", "koi-pond"], graph);
100
+ client.setQueryData(["code-insights", "koi-pond", "main"], {
101
+ head: hash(40),
102
+ commits: 40,
103
+ partial: true,
104
+ omittedFiles: 2,
105
+ files: [
106
+ { path: "src/pond.ts", lines: 420, changes: 18 },
107
+ { path: "src/fish.ts", lines: 230, changes: 12 },
108
+ { path: "src/ripples.ts", lines: 180, changes: 9 },
109
+ { path: "src/light.ts", lines: 125, changes: 4 },
110
+ { path: "README.md", lines: 70, changes: 2 },
111
+ { path: "index.html", lines: 40, changes: 1 },
112
+ ],
113
+ ageHistory: Array.from({ length: 16 }, (_, index) => ({
114
+ at: now - (15 - index) * 7 * 86400,
115
+ lines: [
116
+ Math.round(80 + index * 12),
117
+ Math.round(90 + index * 16),
118
+ Math.round(35 + index * 14),
119
+ Math.round(10 + index * 4),
120
+ 160,
121
+ ],
122
+ })),
123
+ });
124
+ if (empty)
125
+ client.setQueryData(["code-insights", "koi-pond", "main"], {
126
+ head: null,
127
+ commits: 0,
128
+ files: [],
129
+ ageHistory: [],
130
+ omittedFiles: 0,
131
+ partial: false,
132
+ });
133
+ const fixtureCommit = {
134
+ hash: hash(40),
135
+ parents: [hash(36)],
136
+ treeHash: hash(100),
137
+ message: "Balance the evening light",
138
+ author: { name: "Sol", email: "fixture@example.test" },
139
+ committer: { name: "Sol", email: "fixture@example.test" },
140
+ authoredAt: now,
141
+ committedAt: now,
142
+ };
143
+ const issue = `---
144
+ id: pond-01
145
+ title: Add a lily pad
146
+ type: task
147
+ status: ready-for-agent
148
+ priority: p1
149
+ created: 2026-10-12
150
+ assignee: null
151
+ areas: [pond]
152
+ blocked_by: []
153
+ ---
154
+ # Add a lily pad
155
+ ## Outcome
156
+ The koi have shade.
157
+ ## Context
158
+ The pond.
159
+ ## Acceptance criteria
160
+ - [ ] Add shade.
161
+ ## Verification
162
+ - [ ] Observe the pond.
163
+ `;
164
+ client.setQueryData(["issues", "koi-pond", "main"], {
165
+ ref: "main",
166
+ commit: fixtureCommit,
167
+ truncated: false,
168
+ files: empty
169
+ ? {}
170
+ : {
171
+ "pond/01-lily-pad.md": issue,
172
+ "pond/02-reeds.md": issue
173
+ .replaceAll("pond-01", "pond-02")
174
+ .replaceAll("Add a lily pad", "Plant reeds")
175
+ .replace("blocked_by: []", "blocked_by: [pond-01]"),
176
+ "pond/03-water.md":
177
+ issue
178
+ .replaceAll("pond-01", "pond-03")
179
+ .replaceAll("Add a lily pad", "Fill the pond")
180
+ .replace("status: ready-for-agent", "status: resolved") +
181
+ "\n## Resolution\nThe pond is full.\n",
182
+ },
183
+ });
184
+ client.setQueryData(["files", "koi-pond", "main"], {
185
+ ref: "main",
186
+ commit: fixtureCommit,
187
+ paths: ["README.md", "src/pond.ts"],
188
+ truncated: false,
189
+ });
190
+ const readme = {
191
+ commit: fixtureCommit.hash,
192
+ blob: hash(101),
193
+ ref: "main",
194
+ path: "README.md",
195
+ size: 160,
196
+ binary: false,
197
+ version: null,
198
+ text: "# Koi pond\n\nA small living world, made together. Agents tend the water, reeds and ripples through their own branches.\n\nWatch the pond grow with every change.",
199
+ };
200
+ client.setQueryData(
201
+ ["blob", "koi-pond", "main", "README.md"],
202
+ new URLSearchParams(location.search).has("pending")
203
+ ? {
204
+ ...readme,
205
+ version: 3,
206
+ pending: {
207
+ text: "# Koi pond\n\nA small living world, made together. Agents tend the water, reeds and ripples through their own branches.\n\nThree new lilies float by the reeds.",
208
+ revision: 3,
209
+ state: "pending",
210
+ },
211
+ }
212
+ : readme,
213
+ );
214
+ client.setQueryData(["blob", "koi-pond", "main", "src/pond.ts"], {
215
+ commit: fixtureCommit.hash,
216
+ blob: hash(102),
217
+ ref: "main",
218
+ path: "src/pond.ts",
219
+ size: 57,
220
+ binary: false,
221
+ version: null,
222
+ text: "export const ripples = 5;\nexport const reeds = true;\n",
223
+ });
224
+ client.setQueryData(["agents"], { agents });
225
+ client.setQueryData(["events", "koi-pond"], {
226
+ events: empty
227
+ ? []
228
+ : [
229
+ {
230
+ id: 2,
231
+ at: Date.now() - (scene5 ? 480000 : 120000),
232
+ type: "push",
233
+ repo: "koi-pond--Luna",
234
+ actor: "Luna",
235
+ data: {
236
+ updates: [
237
+ { ref: "refs/heads/main", before: hash(35), after: hash(38) },
238
+ ],
239
+ commits: [
240
+ { hash: hash(39), message: "Keep fish inside the reeds" },
241
+ ],
242
+ paths: ["src/fish.ts"],
243
+ },
244
+ },
245
+ {
246
+ id: 1,
247
+ at: Date.now() - (scene5 ? 720000 : 240000),
248
+ type: "push",
249
+ repo: "koi-pond--Sol",
250
+ actor: "Sol",
251
+ data: {
252
+ updates: [
253
+ { ref: "refs/heads/main", before: hash(33), after: hash(40) },
254
+ ],
255
+ commits: [{ hash: hash(37), message: "Let the pond breathe" }],
256
+ paths: ["src/pond.ts"],
257
+ },
258
+ },
259
+ ...(scene5
260
+ ? [
261
+ {
262
+ id: 3,
263
+ at: Date.now() - 840000,
264
+ type: "push",
265
+ repo: "koi-pond--Astra",
266
+ actor: "Astra",
267
+ data: {
268
+ updates: [
269
+ {
270
+ ref: "refs/heads/main",
271
+ before: hash(32),
272
+ after: hash(37),
273
+ },
274
+ ],
275
+ commits: [],
276
+ paths: ["src/light.ts"],
277
+ },
278
+ },
279
+ ]
280
+ : []),
281
+ ],
282
+ });
283
+ client.setQueryData(["repos"], { repos: [detail.repo] });
284
+ client.setQueryData(
285
+ ["events", null],
286
+ client.getQueryData(["events", "koi-pond"]),
287
+ );
288
+ const fixtureChanges = changePage.parse({
289
+ changes: noChanges
290
+ ? []
291
+ : agents.slice(1).map((actor, index) => ({
292
+ repo: "koi-pond",
293
+ number: index + 1,
294
+ sourceRepo: `koi-pond--${actor.name}`,
295
+ sourceRef: "main",
296
+ targetRef: "main",
297
+ title: [
298
+ "Add ripples around the koi",
299
+ "Keep fish inside the reeds",
300
+ "Balance the evening light",
301
+ ][index],
302
+ body: "Keep the pond calm while the agents add new life.",
303
+ openedBy: actor.name,
304
+ openedKind: "agent",
305
+ createdAt: Date.now() - 300000,
306
+ updatedAt: Date.now() - 120000,
307
+ state: "open",
308
+ head: hash(38 + index),
309
+ target: hash(37),
310
+ revision: 0,
311
+ base: hash(20),
312
+ ahead: 3 + index,
313
+ behind: index,
314
+ conflict:
315
+ index === 1 || (index === 0 && changeScenario === "conflict")
316
+ ? {
317
+ head: hash(38 + index),
318
+ target: hash(37),
319
+ paths: ["src/pond.ts"],
320
+ }
321
+ : null,
322
+ status:
323
+ index === 1 || (index === 0 && changeScenario === "conflict")
324
+ ? "conflict"
325
+ : "ready",
326
+ mergeable:
327
+ index === 1 || (index === 0 && changeScenario === "conflict")
328
+ ? { allowed: false, reason: "conflict" }
329
+ : { allowed: true },
330
+ lease: null,
331
+ mergedCommit: null,
332
+ mergedBy: null,
333
+ closedBy: null,
334
+ })),
335
+ });
336
+ client.setQueryData(["changes", "koi-pond"], fixtureChanges);
337
+ client.setQueryData(["commits", "koi-pond", "main"], {
338
+ ref: "main",
339
+ commits: graph.nodes.map((node) => ({
340
+ agent: node.agent,
341
+ commit: {
342
+ ...node,
343
+ treeHash: hash(100),
344
+ author: { name: node.author, email: "fixture@example.test" },
345
+ committer: { name: node.author, email: "fixture@example.test" },
346
+ authoredAt: node.committedAt,
347
+ },
348
+ })),
349
+ });
350
+ const fixtureDiff = {
351
+ files: [
352
+ {
353
+ path: "src/pond.ts",
354
+ oldPath: null,
355
+ status: "modified",
356
+ additions: 2,
357
+ deletions: 1,
358
+ binary: false,
359
+ truncated: false,
360
+ oldText: "export const ripples = 3;\nexport const reeds = true;\n",
361
+ newText:
362
+ "export const ripples = 5;\nexport const reeds = true;\nexport const lilies = 2;\n",
363
+ },
364
+ ],
365
+ additions: 2,
366
+ deletions: 1,
367
+ omittedFiles: 0,
368
+ };
369
+ for (const node of graph.nodes) {
370
+ client.setQueryData(["commit", node.origin ?? "koi-pond", node.hash], {
371
+ commit: {
372
+ ...fixtureCommit,
373
+ hash: node.hash,
374
+ message: node.message,
375
+ parents: node.parents,
376
+ author: { name: node.author, email: "fixture@example.test" },
377
+ committedAt: node.committedAt,
378
+ },
379
+ agent: node.agent,
380
+ diff: fixtureDiff,
381
+ });
382
+ }
383
+ const fixtureDetails = new Map(
384
+ fixtureChanges.changes.map((change) => {
385
+ const value = changeDetail.parse({
386
+ change,
387
+ commits: [
388
+ {
389
+ commit: {
390
+ ...fixtureCommit,
391
+ hash: change.head,
392
+ message: change.title,
393
+ parents: [hash(33 + change.number)],
394
+ author: { name: change.openedBy, email: "fixture@example.test" },
395
+ committer: { name: change.openedBy, email: "fixture@example.test" },
396
+ },
397
+ agent: change.openedBy,
398
+ },
399
+ ],
400
+ diff: fixtureDiff,
401
+ actions: { approve: { allowed: true }, close: { allowed: true } },
402
+ review: {
403
+ approvals: [
404
+ {
405
+ actor: "Levi",
406
+ kind: "human",
407
+ head: change.head,
408
+ at: Date.now() - 60000,
409
+ },
410
+ {
411
+ actor: "Levi",
412
+ kind: "human",
413
+ head: hash(2),
414
+ at: Date.now() - 240000,
415
+ },
416
+ ],
417
+ checks: [
418
+ {
419
+ head: change.head,
420
+ name: "pond tests",
421
+ status: "passed",
422
+ reporter: "workflow",
423
+ attemptId: "demo-check",
424
+ runUrl: null,
425
+ summary: "12 tests passed",
426
+ updatedAt: Date.now() - 60000,
427
+ },
428
+ ],
429
+ },
430
+ });
431
+ client.setQueryData(["change", change.repo, change.number], value);
432
+ return [change.number, value];
433
+ }),
434
+ );
435
+ const loadingScenario = new URLSearchParams(location.search).get("load");
436
+ if (loadingScenario) {
437
+ client.removeQueries({ queryKey: ["graph"] });
438
+ client.removeQueries({ queryKey: ["agents"] });
439
+ }
440
+ const capabilities = createCapabilityRegistry();
441
+ capabilities.provide(appMethods, {
442
+ call: (method, inputJson) => {
443
+ if (method === "changes.get") {
444
+ const input = z
445
+ .object({ repo: z.literal("koi-pond"), number: z.number() })
446
+ .parse(JSON.parse(inputJson));
447
+ const value = fixtureDetails.get(input.number);
448
+ if (!value) throw new Error("Unknown fixture Change");
449
+ return Promise.resolve(z.json().parse(value));
450
+ }
451
+ if (method === "changes.list")
452
+ return Promise.resolve(z.json().parse(fixtureChanges));
453
+ if (method === "graph.get" || method === "actors.list") {
454
+ if (loadingScenario === "pending") return new Promise(() => undefined);
455
+ if (loadingScenario === "error")
456
+ return Promise.reject(
457
+ new Error("Bonsai could not finish that request"),
458
+ );
459
+ return Promise.resolve(method === "graph.get" ? graph : { agents });
460
+ }
461
+ if (method === "activity.list") return Promise.resolve({ events: [] });
462
+ if (method === "repos.get") return Promise.resolve(detail);
463
+ if (method === "changes.approve") {
464
+ const input = z
465
+ .strictObject({
466
+ repo: z.literal("koi-pond"),
467
+ number: z.number().int().positive(),
468
+ head: z.string(),
469
+ })
470
+ .parse(JSON.parse(inputJson));
471
+ const value = fixtureDetails.get(input.number);
472
+ if (value?.change.head !== input.head)
473
+ throw new Error("Approval did not pin the displayed head");
474
+ localStorage.setItem("fixture-approval", JSON.stringify(input));
475
+ return Promise.resolve({ kind: "approved" });
476
+ }
477
+ if (method === "changes.close") {
478
+ const input = z
479
+ .strictObject({
480
+ repo: z.literal("koi-pond"),
481
+ number: z.number().int().positive(),
482
+ })
483
+ .parse(JSON.parse(inputJson));
484
+ const value = fixtureDetails.get(input.number);
485
+ if (!value) throw new Error("Unknown fixture Change");
486
+ value.change.state = "closed";
487
+ value.change.status = "closed";
488
+ value.change.mergeable = { allowed: false, reason: "not-open" };
489
+ value.actions = {
490
+ approve: { allowed: false, reason: "not-open" },
491
+ close: { allowed: false, reason: "not-open" },
492
+ };
493
+ localStorage.setItem("fixture-close", JSON.stringify(input));
494
+ return Promise.resolve({ allowed: true });
495
+ }
496
+ if (method === "changes.merge") {
497
+ const input = z
498
+ .strictObject({
499
+ repo: z.literal("koi-pond"),
500
+ number: z.number().int().positive(),
501
+ head: z.string(),
502
+ })
503
+ .parse(JSON.parse(inputJson));
504
+ const value = fixtureDetails.get(input.number);
505
+ if (value?.change.head !== input.head)
506
+ throw new Error("Merge did not pin the displayed head");
507
+ localStorage.setItem("fixture-merge", JSON.stringify(input));
508
+ const result = new URLSearchParams(location.search).get("result");
509
+ if (result === "error") throw new Error("Fixture merge unavailable");
510
+ if (result === "refused")
511
+ return Promise.resolve({ kind: "refused", reason: "checks-failed" });
512
+ if (result === "moved") {
513
+ value.change.head = hash(99);
514
+ return Promise.resolve({ kind: "moved", head: hash(99) });
515
+ }
516
+ if (result === "conflict") {
517
+ value.change.status = "conflict";
518
+ value.change.mergeable = { allowed: false, reason: "conflict" };
519
+ value.change.conflict = {
520
+ head: input.head,
521
+ target: hash(37),
522
+ paths: ["src/pond.ts"],
523
+ };
524
+ return Promise.resolve({
525
+ kind: "conflict",
526
+ paths: ["src/pond.ts"],
527
+ head: input.head,
528
+ });
529
+ }
530
+ value.actions = {
531
+ approve: { allowed: false, reason: "not-open" },
532
+ close: { allowed: false, reason: "not-open" },
533
+ };
534
+ value.change.state = "merged";
535
+ value.change.status = "merged";
536
+ value.change.mergeable = { allowed: false, reason: "not-open" };
537
+ return Promise.resolve({
538
+ kind: "merged",
539
+ before: hash(37),
540
+ after: hash(99),
541
+ });
542
+ }
543
+ if (method === "insights.layout.get") {
544
+ const parsed = savedLayout.safeParse(
545
+ JSON.parse(localStorage.getItem("fixture-layout") ?? '{"tiles":null}'),
546
+ );
547
+ return Promise.resolve(parsed.success ? parsed.data : { tiles: null });
548
+ }
549
+ if (method === "insights.layout.save") {
550
+ if (new URLSearchParams(location.search).has("save-error"))
551
+ return Promise.reject(new Error("Fixture save unavailable"));
552
+ const input = z
553
+ .object({ tiles: insightLayout })
554
+ .parse(JSON.parse(inputJson));
555
+ const result = { tiles: input.tiles };
556
+ localStorage.setItem("fixture-layout", JSON.stringify(result));
557
+ return Promise.resolve(result);
558
+ }
559
+ return Promise.reject(new Error(`Unseeded fixture method: ${method}`));
560
+ },
561
+ });
562
+ capabilities.provide(appsOpen, {
563
+ open: (input) => {
564
+ document.documentElement.dataset.openApp = JSON.stringify(input);
565
+ return Promise.resolve();
566
+ },
567
+ // The shell's role routing names the Team's files App.
568
+ role: (role) =>
569
+ Promise.resolve(role === "files" ? { displayName: "Inkcap" } : null),
570
+ });
571
+ connect({ capabilities });
572
+ function BoardFixture() {
573
+ const [expanded, setExpanded] = useState(false);
574
+ return (
575
+ <>
576
+ <button
577
+ onClick={() => {
578
+ setExpanded(!expanded);
579
+ }}
580
+ >
581
+ Expand document
582
+ </button>
583
+ <BonsaiBoard
584
+ placements={[
585
+ { id: "document", col: 0, row: 0, w: 3, h: 1 },
586
+ { id: "facts", col: 3, row: 0, w: 1, h: 1 },
587
+ { id: "below", col: 0, row: 1, w: 4, h: 1 },
588
+ ]}
589
+ phoneOrder={["facts", "document", "below"]}
590
+ tiles={[
591
+ {
592
+ id: "document",
593
+ title: "Document",
594
+ autoHeight: true,
595
+ body: (
596
+ <div>
597
+ {Array.from({ length: expanded ? 30 : 3 }, (_, i) => (
598
+ <p key={i}>A real row of document content.</p>
599
+ ))}
600
+ </div>
601
+ ),
602
+ },
603
+ { id: "facts", title: "Facts", body: "Short readout" },
604
+ { id: "below", title: "Below", body: "Follows the document" },
605
+ ]}
606
+ />
607
+ </>
608
+ );
609
+ }
610
+
611
+ function Fixture() {
612
+ const { route, go } = useRouter();
613
+ useEffect(() => {
614
+ if (changeScenario && route.page === "home") {
615
+ go("#change/koi-pond/1");
616
+ return;
617
+ }
618
+ const tab = new URLSearchParams(location.search).get("tab");
619
+ if (tab && tab !== "home" && route.page === "home")
620
+ go(`/koi-pond?tab=${encodeURIComponent(tab)}`);
621
+ }, [route.page, go]);
622
+ if (new URLSearchParams(location.search).has("board"))
623
+ return <BoardFixture />;
624
+ return (
625
+ <div className="caps-app-canvas">
626
+ <main className="bonsai-page">
627
+ {route.page === "home" &&
628
+ new URLSearchParams(location.search).get("tab") === "home" ? (
629
+ <HomePage live={{ connected: true, error: null, fresh: new Set() }} />
630
+ ) : route.page === "change" ? (
631
+ <ChangePage
632
+ key={`${route.repo}:${String(route.number)}`}
633
+ repo={route.repo}
634
+ number={route.number}
635
+ />
636
+ ) : route.page === "commit" ? (
637
+ <CommitPage repo={route.repo} hash={route.hash} />
638
+ ) : (
639
+ <RepoPage
640
+ route={
641
+ route.page === "repo"
642
+ ? route
643
+ : {
644
+ page: "repo",
645
+ repo: "koi-pond",
646
+ tab: "commits",
647
+ ref: new URLSearchParams(location.search).get("ref"),
648
+ path: "",
649
+ file: null,
650
+ }
651
+ }
652
+ live={{ connected: true, error: null, fresh: new Set() }}
653
+ />
654
+ )}
655
+ </main>
656
+ </div>
657
+ );
658
+ }
659
+ const root = document.getElementById("root");
660
+ if (!root) throw new Error("Missing fixture root");
661
+ createRoot(root).render(
662
+ <QueryClientProvider client={client}>
663
+ <RouterProvider>
664
+ <Fixture />
665
+ </RouterProvider>
666
+ </QueryClientProvider>,
667
+ );
@@ -0,0 +1,10 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5
+ </head>
6
+ <body>
7
+ <div id="root"></div>
8
+ <script type="module" src="./visual-fixture.tsx"></script>
9
+ </body>
10
+ </html>
@@ -0,0 +1,9 @@
1
+ import { URL, fileURLToPath } from "node:url";
2
+ import react from "@vitejs/plugin-react";
3
+ import tailwindcss from "@tailwindcss/vite";
4
+ export default {
5
+ root: fileURLToPath(new URL("../../", import.meta.url)),
6
+ envDir: false,
7
+ plugins: [react(), tailwindcss()],
8
+ server: { host: "127.0.0.1", port: 5188 },
9
+ };