@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,83 @@
1
+ // The Code tab's Inkcap entry and unsynced chip at both widths and themes.
2
+ import assert from "node:assert/strict";
3
+ import console from "node:console";
4
+ import process from "node:process";
5
+ import { mkdir } from "node:fs/promises";
6
+ import { chromium } from "playwright";
7
+
8
+ const origin = process.argv[2];
9
+ const output = process.argv[3];
10
+ assert.ok(origin && output, "Usage: edit-in-inkcap.browser.mjs <origin> <dir>");
11
+ await mkdir(output, { recursive: true });
12
+ const browser = await chromium.launch({ headless: true });
13
+ try {
14
+ for (const scenario of ["code", "pending"])
15
+ for (const width of [1440, 390])
16
+ for (const theme of ["light", "dark"]) {
17
+ const page = await browser.newPage({
18
+ viewport: { width, height: width === 390 ? 844 : 900 },
19
+ colorScheme: theme,
20
+ reducedMotion: "reduce",
21
+ });
22
+ const errors = [];
23
+ page.on("pageerror", (error) => errors.push(error.message));
24
+ await page.goto(
25
+ `${origin}/test/browser/visual.html?tab=code${scenario === "pending" ? "&pending" : ""}`,
26
+ );
27
+ await page.evaluate((value) => {
28
+ globalThis.document.documentElement.setAttribute("data-theme", value);
29
+ }, theme);
30
+ const edit = page.getByRole("button", {
31
+ name: "Edit in Inkcap",
32
+ exact: true,
33
+ });
34
+ await edit.waitFor();
35
+ if (scenario === "pending") {
36
+ await page.getByText("Unsynced edit", { exact: true }).waitFor();
37
+ await page
38
+ .getByRole("button", { name: "Sync", exact: true })
39
+ .waitFor();
40
+ await page
41
+ .getByText("Three new lilies float by the reeds.")
42
+ .waitFor();
43
+ }
44
+ await page.evaluate(() => globalThis.document.fonts.ready);
45
+ assert.deepEqual(errors, []);
46
+ await page.screenshot({
47
+ path: `${output}/${scenario}-${String(width)}-${theme}.png`,
48
+ fullPage: true,
49
+ });
50
+ await edit.click();
51
+ assert.deepEqual(
52
+ JSON.parse(
53
+ await page.evaluate(
54
+ () => globalThis.document.documentElement.dataset.openApp,
55
+ ),
56
+ ),
57
+ {
58
+ role: "files",
59
+ intent: {
60
+ kind: "navigate",
61
+ params: [
62
+ { key: "repository", value: "koi-pond" },
63
+ { key: "branch", value: "main" },
64
+ { key: "path", value: "README.md" },
65
+ ],
66
+ },
67
+ },
68
+ );
69
+ assert.ok(
70
+ (await page.evaluate(
71
+ () => globalThis.document.documentElement.scrollWidth,
72
+ )) <= width,
73
+ "no horizontal page scroll",
74
+ );
75
+ assert.deepEqual(errors, []);
76
+ await page.close();
77
+ }
78
+ console.log(
79
+ "PASS Edit in Inkcap entry and unsynced chip 1440/390 light/dark",
80
+ );
81
+ } finally {
82
+ await browser.close();
83
+ }
@@ -0,0 +1,108 @@
1
+ /** Real repository pages with controlled App-method replies, no provider writes. */
2
+ import { useEffect, useRef } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+ import { createCapabilityRegistry } from "@fungi.computer/agar";
6
+ import { appMethods } from "@fungi.computer/agar/backend";
7
+ import * as z from "zod";
8
+ import { repoDetail } from "../../shared/contract.js";
9
+ import { connect } from "../../web/transport.js";
10
+ import { RouterProvider, useRouter } from "../../web/router.js";
11
+ import { RepoPage } from "../../web/pages.js";
12
+ import "@fungi.computer/caps/fonts.css";
13
+ import "../../web/bonsai.css";
14
+
15
+ const name = "disposable-merge-1791840651624";
16
+ const live = { connected: true, error: null, fresh: new Set<number>() };
17
+ const detail = repoDetail.parse({
18
+ repo: {
19
+ name,
20
+ description: "Contest probe repository",
21
+ parent: null,
22
+ owner: null,
23
+ createdAt: 0,
24
+ lastPushAt: null,
25
+ forks: [],
26
+ openChanges: 0,
27
+ activity: [],
28
+ },
29
+ defaultBranch: "main",
30
+ refs: [],
31
+ cloneUrl: `https://example.test/git/${name}.git`,
32
+ empty: true,
33
+ });
34
+ const client = new QueryClient({
35
+ defaultOptions: { queries: { staleTime: Infinity, retry: false } },
36
+ });
37
+ client.setQueryData(["repo", name], detail);
38
+ client.setQueryData(["events", name], { events: [] });
39
+ client.setQueryData(["changes", name], { changes: [] });
40
+ client.setQueryData(["agents"], { agents: [] });
41
+ let attempts = 0;
42
+ let withdrawn = false;
43
+ const capabilities = createCapabilityRegistry();
44
+ capabilities.provide(appMethods, {
45
+ call: (method, inputJson) => {
46
+ if (method === "repos.delete") {
47
+ z.strictObject({ repo: z.literal(name) }).parse(JSON.parse(inputJson));
48
+ attempts += 1;
49
+ const counter = document.getElementById("attempts");
50
+ if (counter) counter.textContent = String(attempts);
51
+ if (new URLSearchParams(location.search).get("scenario") === "refused")
52
+ return Promise.resolve({
53
+ error: "Only the owner can delete repositories",
54
+ });
55
+ withdrawn = true;
56
+ return Promise.resolve(
57
+ attempts === 1
58
+ ? { error: "Provider temporarily unavailable; retry deletion" }
59
+ : { deleted: true },
60
+ );
61
+ }
62
+ if (method === "repos.get")
63
+ return Promise.resolve(withdrawn ? { error: "No repository" } : detail);
64
+ if (method === "issues.read")
65
+ return Promise.resolve({ error: "Repository has no commits" });
66
+ if (method === "repos.list")
67
+ return Promise.resolve({ repos: withdrawn ? [] : [detail.repo] });
68
+ if (method === "activity.list") return Promise.resolve({ events: [] });
69
+ if (method === "changes.list") return Promise.resolve({ changes: [] });
70
+ if (method === "actors.list") return Promise.resolve({ agents: [] });
71
+ return Promise.reject(new Error(`Unexpected fixture method: ${method}`));
72
+ },
73
+ });
74
+ connect({ capabilities });
75
+
76
+ function Shell() {
77
+ const { route, go } = useRouter();
78
+ const opened = useRef(false);
79
+ useEffect(() => {
80
+ if (!opened.current) {
81
+ opened.current = true;
82
+ go(`/${name}`);
83
+ }
84
+ }, [go]);
85
+ return (
86
+ <div className="caps-app-canvas bonsai-shell">
87
+ <main className="bonsai-page">
88
+ {route.page === "repo" ? (
89
+ <RepoPage route={route} live={live} />
90
+ ) : (
91
+ <h1>Repositories</h1>
92
+ )}
93
+ </main>
94
+ <output id="attempts" hidden>
95
+ 0
96
+ </output>
97
+ </div>
98
+ );
99
+ }
100
+ const root = document.getElementById("root");
101
+ if (!root) throw new Error("Settings fixture root missing");
102
+ createRoot(root).render(
103
+ <QueryClientProvider client={client}>
104
+ <RouterProvider>
105
+ <Shell />
106
+ </RouterProvider>
107
+ </QueryClientProvider>,
108
+ );
@@ -0,0 +1,136 @@
1
+ import assert from "node:assert/strict";
2
+ import console from "node:console";
3
+ import process from "node:process";
4
+ import { mkdir } from "node:fs/promises";
5
+ import { fileURLToPath, URL } from "node:url";
6
+ import { createServer } from "vite";
7
+ import { chromium } from "playwright";
8
+ import viteConfig from "./vite.config.mjs";
9
+
10
+ const output = new URL(
11
+ "../../../../.botanical/repos-delete/shots/",
12
+ import.meta.url,
13
+ );
14
+ await mkdir(output, { recursive: true });
15
+ const server = await createServer({
16
+ ...viteConfig,
17
+ configFile: false,
18
+ server: { ...viteConfig.server, port: 0 },
19
+ });
20
+ await server.listen();
21
+ const origin = server.resolvedUrls.local[0];
22
+ let browser;
23
+ const name = "disposable-merge-1791840651624";
24
+ const before = process.argv.includes("--before");
25
+ try {
26
+ browser = await chromium.launch({ headless: true });
27
+ for (const width of [1440, 390])
28
+ for (const theme of ["light", "dark"]) {
29
+ const page = await browser.newPage({
30
+ viewport: { width, height: 1000 },
31
+ colorScheme: theme,
32
+ });
33
+ page.setDefaultTimeout(10000);
34
+ const errors = [];
35
+ page.on("pageerror", (cause) => errors.push(cause.message));
36
+ await page.goto(`${origin}test/browser/repository-settings.html`);
37
+ await page.evaluate(
38
+ (value) =>
39
+ globalThis.document.documentElement.setAttribute("data-theme", value),
40
+ theme,
41
+ );
42
+ await page.getByRole("heading", { name, exact: true }).waitFor();
43
+ const settings = page.getByRole("link", {
44
+ name: "Settings",
45
+ exact: true,
46
+ });
47
+ if (before) {
48
+ assert.equal(await settings.count(), 0);
49
+ await page.screenshot({
50
+ path: fileURLToPath(new URL(`${width}-${theme}-before.png`, output)),
51
+ });
52
+ await page.close();
53
+ continue;
54
+ }
55
+ await settings.click();
56
+ await page
57
+ .getByRole("heading", { name: "Repository settings", exact: true })
58
+ .waitFor();
59
+ assert.equal(await page.locator("#attempts").textContent(), "0");
60
+ await page.screenshot({
61
+ path: fileURLToPath(new URL(`${width}-${theme}-settings.png`, output)),
62
+ });
63
+ await page
64
+ .getByRole("button", { name: "Delete repository", exact: true })
65
+ .click();
66
+ await page
67
+ .getByRole("textbox", { name: "Repository name" })
68
+ .fill("wrong");
69
+ assert.equal(
70
+ await page
71
+ .getByRole("button", { name: "Confirm deletion" })
72
+ .isDisabled(),
73
+ true,
74
+ );
75
+ await page.getByRole("button", { name: "Cancel", exact: true }).click();
76
+ assert.equal(await page.locator("#attempts").textContent(), "0");
77
+ await page
78
+ .getByRole("button", { name: "Delete repository", exact: true })
79
+ .click();
80
+ await page.getByRole("textbox", { name: "Repository name" }).fill(name);
81
+ await page.screenshot({
82
+ path: fileURLToPath(new URL(`${width}-${theme}-confirm.png`, output)),
83
+ });
84
+ await page.getByRole("button", { name: "Confirm deletion" }).click();
85
+ await page
86
+ .getByRole("alert")
87
+ .filter({
88
+ hasText: "Deletion didn't finish because storage was unavailable.",
89
+ })
90
+ .waitFor();
91
+ assert.equal(await page.getByRole("alert").locator("p").count(), 1);
92
+ assert.equal(await page.locator("#attempts").textContent(), "1");
93
+ await page.screenshot({
94
+ path: fileURLToPath(new URL(`${width}-${theme}-retry.png`, output)),
95
+ });
96
+ await page.getByRole("button", { name: "Confirm deletion" }).click();
97
+ await page
98
+ .getByRole("heading", { name: "Repositories", exact: true })
99
+ .waitFor();
100
+ assert.equal(await page.locator("#attempts").textContent(), "2");
101
+ await page.goto(
102
+ `${origin}test/browser/repository-settings.html?scenario=refused`,
103
+ );
104
+ await page.getByRole("link", { name: "Settings", exact: true }).click();
105
+ await page
106
+ .getByRole("button", { name: "Delete repository", exact: true })
107
+ .click();
108
+ await page.getByRole("textbox", { name: "Repository name" }).fill(name);
109
+ await page.getByRole("button", { name: "Confirm deletion" }).click();
110
+ await page
111
+ .getByRole("alert")
112
+ .filter({ hasText: "Only the owner" })
113
+ .waitFor();
114
+ assert.deepEqual(errors, []);
115
+ assert.equal(
116
+ await page.evaluate(
117
+ () =>
118
+ globalThis.document.documentElement.scrollWidth <=
119
+ globalThis.innerWidth,
120
+ ),
121
+ true,
122
+ "no horizontal overflow",
123
+ );
124
+ await page.close();
125
+ }
126
+ console.log(
127
+ before
128
+ ? "PASS baseline 1440/390 light/dark: repository view before Settings"
129
+ : "PASS Settings 1440/390 light/dark: launcher entry, explicit confirmation, cancel, owner refusal, failed provider retry and home navigation",
130
+ );
131
+ } finally {
132
+ await browser?.close();
133
+ await server.close();
134
+ console.log("CLOSED browser and Vite server");
135
+ }
136
+ process.exitCode = 0;
@@ -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="./repository-settings-fixture.tsx"></script>
9
+ </body>
10
+ </html>