wao 0.40.0 → 0.40.2

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 (37) hide show
  1. package/cjs/ao.js +4 -4
  2. package/cjs/create.js +32 -27
  3. package/cjs/workspace/.claude/agents/builder.md +4 -2
  4. package/cjs/workspace/.claude/skills/build/SKILL.md +34 -12
  5. package/cjs/workspace/.claude/skills/build-aos/SKILL.md +10 -1
  6. package/cjs/workspace/.claude/skills/build-device/SKILL.md +37 -23
  7. package/cjs/workspace/.claude/skills/build-frontend/SKILL.md +17 -14
  8. package/cjs/workspace/.claude/skills/build-module/SKILL.md +10 -1
  9. package/cjs/workspace/.claude/skills/plan/SKILL.md +12 -4
  10. package/cjs/workspace/.claude/skills/readme/SKILL.md +151 -45
  11. package/cjs/workspace/.claude/skills/report/SKILL.md +2 -1
  12. package/cjs/workspace/dashboard/index.html +154 -3
  13. package/cjs/workspace/dashboard/public/favicon.ico +0 -0
  14. package/cjs/workspace/dashboard/public/favicon.png +0 -0
  15. package/cjs/workspace/dashboard/server.js +93 -12
  16. package/cjs/workspace/dashboard/src/App.jsx +2297 -372
  17. package/cjs/workspace/docs/wao-sdk.md +1 -1
  18. package/cjs/workspace/package.json +1 -1
  19. package/esm/ao.js +3 -3
  20. package/esm/create.js +3 -0
  21. package/esm/workspace/.claude/agents/builder.md +4 -2
  22. package/esm/workspace/.claude/skills/build/SKILL.md +34 -12
  23. package/esm/workspace/.claude/skills/build-aos/SKILL.md +10 -1
  24. package/esm/workspace/.claude/skills/build-device/SKILL.md +37 -23
  25. package/esm/workspace/.claude/skills/build-frontend/SKILL.md +17 -14
  26. package/esm/workspace/.claude/skills/build-module/SKILL.md +10 -1
  27. package/esm/workspace/.claude/skills/plan/SKILL.md +12 -4
  28. package/esm/workspace/.claude/skills/readme/SKILL.md +151 -45
  29. package/esm/workspace/.claude/skills/report/SKILL.md +2 -1
  30. package/esm/workspace/dashboard/index.html +154 -3
  31. package/esm/workspace/dashboard/public/favicon.ico +0 -0
  32. package/esm/workspace/dashboard/public/favicon.png +0 -0
  33. package/esm/workspace/dashboard/server.js +93 -12
  34. package/esm/workspace/dashboard/src/App.jsx +2297 -372
  35. package/esm/workspace/docs/wao-sdk.md +1 -1
  36. package/esm/workspace/package.json +1 -1
  37. package/package.json +1 -1
@@ -1,425 +1,2350 @@
1
1
  import { useState, useEffect } from "react"
2
2
 
3
- const TRACKS = [
4
- {
5
- name: "AOS",
6
- description: "Lua handlers running on AOS processes",
7
- deployments: [
8
- {
9
- target: "AO Testnet",
10
- command: "yarn deploy src/app.lua",
11
- code: `import { AO } from "wao"
12
- const jwk = JSON.parse(fs.readFileSync(".wallet.json", "utf8"))
13
- const ao = await new AO().init(jwk)
14
- const { p, pid } = await ao.deploy({ src_data })`,
15
- notes: "Original AO testnet via aoconnect. MU can be flaky.",
16
- },
17
- {
18
- target: "Local HyperBEAM (genesis-wasm)",
19
- command: "yarn deploy --local-hb src/app.lua",
20
- code: `import { HyperBEAM } from "wao/test"
21
- import { AO } from "wao"
22
- const hbeam = await new HyperBEAM({ reset: true, genesis_wasm: true }).ready()
23
- const ao = await new AO({ hb: hbeam.url }).init(hbeam.jwk)
24
- const { p, pid } = await ao.deploy({ src_data })`,
25
- notes: "Full Erlang stack with genesis-wasm. Best for integration testing.",
26
- },
27
- {
28
- target: "Local HyperBEAM (Lua mode)",
29
- command: "yarn deploy --local-hb --lua src/app.lua",
30
- code: `import { HyperBEAM } from "wao/test"
3
+ // ═══════════════════════════════════════════════════════════════
4
+ // Demo data
5
+ // ═══════════════════════════════════════════════════════════════
6
+
7
+ const DEMO_DATA = {
8
+ feature: "Token Transfer App",
9
+ tasks: [
10
+ { id: 1, name: "Plan feature", type: "plan", status: "done", skill: "/plan", done_when: "plan.md and tasks.json created", files: ["plan.md", "tasks.json"], started_at: "2026-02-15T10:00:00Z", completed_at: "2026-02-15T10:02:14Z" },
11
+ { id: 2, name: "Enhance AOS token handlers", type: "aos", status: "done", skill: "/build-aos", done_when: "Mint, transfer, balance handlers pass all tests", files: ["src/token.lua", "src/registry.lua"], started_at: "2026-02-15T10:02:14Z", completed_at: "2026-02-15T10:08:47Z" },
12
+ { id: 3, name: "Write in-memory AOS token tests", type: "aos-test", status: "done", skill: "/build-aos", done_when: "All token unit tests pass", files: ["test/token.test.js", "test/registry.test.js"], started_at: "2026-02-15T10:08:47Z", completed_at: "2026-02-15T10:14:22Z" },
13
+ { id: 4, name: "Write AOS HyperBEAM integration tests for token", type: "aos-integration", status: "done", skill: "/test-hb", done_when: "Token operations work on HyperBEAM", files: ["test/hyperbeam-token.test.js"], started_at: "2026-02-15T10:14:22Z", completed_at: "2026-02-15T10:19:05Z" },
14
+ { id: 5, name: "Build HyperBEAM token device + eunit tests", type: "device", status: "done", skill: "/build-device", done_when: "Device compiles and eunit passes", files: ["HyperBEAM/src/dev_token.erl"], started_at: "2026-02-15T10:19:05Z", completed_at: "2026-02-15T10:28:33Z" },
15
+ { id: 6, name: "Write device JS integration tests", type: "device-integration", status: "done", skill: "/test-device", done_when: "JS SDK can call device via HTTP", files: ["test/token-device.test.js"], started_at: "2026-02-15T10:28:33Z", completed_at: "2026-02-15T10:33:41Z" },
16
+ { id: 7, name: "Build frontend components", type: "frontend", status: "done", skill: "/build-frontend", done_when: "All components render correctly", files: ["frontend/src/App.jsx", "frontend/src/components/TransferForm.jsx"], started_at: "2026-02-15T10:33:41Z", completed_at: "2026-02-15T10:41:09Z" },
17
+ { id: 8, name: "Write frontend unit tests — debugging 1 failure", type: "frontend-test", status: "in_progress", skill: "/build-frontend", done_when: "Vitest passes 100%", files: ["frontend/src/__tests__/App.test.jsx", "frontend/src/__tests__/TransferForm.test.jsx"], started_at: "2026-02-15T10:41:09Z" },
18
+ { id: 9, name: "Write frontend E2E integration tests", type: "frontend-integration", status: "pending", skill: "/test-e2e", done_when: "Playwright E2E passes with live HyperBEAM", files: ["frontend/e2e/token-transfer.spec.js"] },
19
+ { id: 10, name: "Generate README", type: "readme", status: "pending", skill: "/readme", done_when: "README.md covers setup, usage, and API" },
20
+ { id: 11, name: "Final validation", type: "validate", status: "pending", skill: "/validate", done_when: "All gates pass, no Lua pitfalls" },
21
+ ],
22
+ }
23
+
24
+ const DEMO_FILES = [
25
+ { path: "package.json", size: 687 }, { path: "plan.md", size: 2948 }, { path: "README.md", size: 1843 }, { path: "tasks.json", size: 6541 },
26
+ { path: "frontend/e2e/debug.spec.js", size: 0 }, { path: "frontend/e2e/token-transfer.spec.js", size: 9216 },
27
+ { path: "frontend/src/App.css", size: 3072 }, { path: "frontend/src/App.jsx", size: 2048 }, { path: "frontend/src/index.css", size: 417 },
28
+ { path: "frontend/src/main.jsx", size: 129 }, { path: "frontend/src/test-setup.js", size: 0 },
29
+ { path: "frontend/src/__tests__/App.test.jsx", size: 1621 }, { path: "frontend/src/__tests__/TransferForm.test.jsx", size: 4136 },
30
+ { path: "frontend/src/components/BalanceDisplay.jsx", size: 640 }, { path: "frontend/src/components/TokenInfo.jsx", size: 730 },
31
+ { path: "frontend/src/components/TransactionStatus.jsx", size: 159 }, { path: "frontend/src/components/TransferForm.jsx", size: 0 },
32
+ { path: "frontend/src/components/WalletConnect.jsx", size: 610 },
33
+ { path: "frontend/src/hooks/useToken.js", size: 3148 }, { path: "frontend/src/hooks/useWallet.js", size: 1440 },
34
+ { path: "HyperBEAM/src/dev_token.erl", size: 4800 },
35
+ { path: "scripts/deploy.js", size: 783 },
36
+ { path: "src/counter.lua", size: 318 }, { path: "src/registry.lua", size: 1900 }, { path: "src/token.lua", size: 2948 },
37
+ { path: "test/aos.test.js", size: 2700 }, { path: "test/hyperbeam-token.test.js", size: 2700 }, { path: "test/hyperbeam.test.js", size: 3534 },
38
+ { path: "test/registry.test.js", size: 4200 }, { path: "test/token-device.test.js", size: 1228 }, { path: "test/token.test.js", size: 1900 },
39
+ ]
40
+
41
+ const DEMO_PLAN = `# Token Transfer App
42
+
43
+ ## Overview
44
+ A decentralized token transfer application built on AOS and HyperBEAM.
45
+
46
+ ## AOS Scripts
47
+ - \`src/token.lua\` — Token handler with mint, transfer, balance
48
+ - \`src/registry.lua\` — Token registry for discoverability
49
+
50
+ ## Frontend
51
+ - React SPA with Vite
52
+ - WalletConnect component for ArConnect
53
+ - TransferForm with validation
54
+ - BalanceDisplay with real-time updates
55
+
56
+ ## Test Plan
57
+ - Unit tests: in-memory AOS token operations
58
+ - Integration: HyperBEAM token device
59
+ - E2E: Full browser flow with Playwright
60
+ `
61
+
62
+ const DEMO_CONTENT = {
63
+ "package.json": `{
64
+ "name": "token-transfer-app",
65
+ "version": "0.0.1",
66
+ "type": "module",
67
+ "scripts": {
68
+ "test": "node --experimental-wasm-memory64 --test --test-concurrency=1",
69
+ "deploy": "node scripts/deploy.js",
70
+ "start": "trap 'kill $(jobs -p)' EXIT; node dashboard/server.js & cd dashboard && npx vite",
71
+ "keygen": "node scripts/keygen.js"
72
+ },
73
+ "dependencies": {
74
+ "hbsig": "0.3.0",
75
+ "wao": "0.40.0"
76
+ }
77
+ }`,
78
+ "plan.md": DEMO_PLAN,
79
+ "README.md": `# Token Transfer App
80
+
81
+ A decentralized token transfer application built on AOS and HyperBEAM.
82
+
83
+ ## Quick Start
84
+
85
+ \`\`\`bash
86
+ yarn install
87
+ yarn keygen # generate wallet
88
+ yarn test # run all tests
89
+ yarn deploy src/token.lua # deploy to testnet
90
+ \`\`\`
91
+
92
+ ## Architecture
93
+
94
+ - **src/token.lua** — Token handler (mint, transfer, balance)
95
+ - **src/registry.lua** — Token registry for discoverability
96
+ - **src/counter.lua** — Simple counter example
97
+ - **frontend/** — React SPA with ArConnect wallet integration
98
+ - **test/** — Unit and integration tests
99
+
100
+ ## Testing
101
+
102
+ \`\`\`bash
103
+ yarn test # all unit tests
104
+ yarn test test/aos.test.js # AOS integration
105
+ yarn test test/hyperbeam.test.js # HyperBEAM integration
106
+ \`\`\`
107
+
108
+ ## Deployment
109
+
110
+ \`\`\`bash
111
+ yarn deploy src/token.lua # AO testnet
112
+ yarn deploy --local-hb src/token.lua # local HyperBEAM
113
+ yarn deploy --mainnet src/token.lua # production
114
+ \`\`\`
115
+ `,
116
+ "tasks.json": JSON.stringify(DEMO_DATA, null, 2),
117
+ "frontend/e2e/debug.spec.js": "",
118
+ "frontend/e2e/token-transfer.spec.js": `import { test, expect } from "@playwright/test"
119
+
120
+ test.describe("Token Transfer E2E", () => {
121
+ test.beforeEach(async ({ page }) => {
122
+ await page.goto("http://localhost:5173")
123
+ })
124
+
125
+ test("should display app title", async ({ page }) => {
126
+ await expect(page.locator("h1")).toHaveText("Token Transfer")
127
+ })
128
+
129
+ test("should show connect wallet prompt", async ({ page }) => {
130
+ await expect(page.locator("text=Connecting to ArConnect")).toBeVisible()
131
+ })
132
+
133
+ test("should display balance after connection", async ({ page }) => {
134
+ // Mock ArConnect wallet
135
+ await page.evaluate(() => {
136
+ window.arweaveWallet = {
137
+ connect: async () => {},
138
+ getActiveAddress: async () => "test-addr-123",
139
+ sign: async (tx) => tx,
140
+ }
141
+ })
142
+ await page.reload()
143
+ await expect(page.locator("[data-testid=balance]")).toBeVisible({ timeout: 10000 })
144
+ })
145
+
146
+ test("should submit transfer form", async ({ page }) => {
147
+ await page.evaluate(() => {
148
+ window.arweaveWallet = {
149
+ connect: async () => {},
150
+ getActiveAddress: async () => "test-addr-123",
151
+ sign: async (tx) => tx,
152
+ }
153
+ })
154
+ await page.reload()
155
+ await page.fill("[data-testid=recipient]", "recipient-addr-456")
156
+ await page.fill("[data-testid=amount]", "100")
157
+ await page.click("[data-testid=transfer-btn]")
158
+ await expect(page.locator("[data-testid=status]")).toContainText("Transfer")
159
+ })
160
+ })`,
161
+ "frontend/src/App.css": `* { margin: 0; padding: 0; box-sizing: border-box; }
162
+
163
+ .app {
164
+ max-width: 640px;
165
+ margin: 0 auto;
166
+ padding: 2rem;
167
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
168
+ }
169
+
170
+ h1 { font-size: 1.5rem; margin-bottom: 1.5rem; }
171
+
172
+ .card {
173
+ border: 1px solid #e1e4e8;
174
+ border-radius: 8px;
175
+ padding: 1rem;
176
+ margin-bottom: 1rem;
177
+ }
178
+
179
+ .balance { font-size: 2rem; font-weight: 600; }
180
+ .balance-label { color: #586069; font-size: 0.875rem; }
181
+
182
+ .form-group { margin-bottom: 1rem; }
183
+ .form-group label { display: block; margin-bottom: 0.25rem; font-size: 0.875rem; color: #586069; }
184
+ .form-group input { width: 100%; padding: 0.5rem; border: 1px solid #e1e4e8; border-radius: 6px; font-size: 1rem; }
185
+
186
+ .btn { padding: 0.5rem 1rem; border-radius: 6px; border: none; cursor: pointer; font-size: 1rem; font-weight: 500; }
187
+ .btn-primary { background: #2ea44f; color: white; }
188
+ .btn-primary:hover { background: #2c974b; }
189
+ .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
190
+
191
+ .status { padding: 0.75rem; border-radius: 6px; margin-top: 1rem; font-size: 0.875rem; }
192
+ .status-success { background: #dcffe4; color: #22863a; }
193
+ .status-error { background: #ffeef0; color: #cb2431; }`,
194
+ "frontend/src/App.jsx": `import { useState, useEffect } from "react"
195
+ import { AO } from "wao/web"
196
+ import TransferForm from "./components/TransferForm"
197
+ import BalanceDisplay from "./components/BalanceDisplay"
198
+
199
+ export default function App() {
200
+ const [ao, setAo] = useState(null)
201
+ const [connected, setConnected] = useState(false)
202
+
203
+ useEffect(() => {
204
+ const init = async () => {
205
+ const ao = new AO()
206
+ await ao.init()
207
+ setAo(ao)
208
+ setConnected(true)
209
+ }
210
+ init().catch(console.error)
211
+ }, [])
212
+
213
+ return (
214
+ <div className="app">
215
+ <h1>Token Transfer</h1>
216
+ {connected ? (
217
+ <>
218
+ <BalanceDisplay ao={ao} />
219
+ <TransferForm ao={ao} />
220
+ </>
221
+ ) : (
222
+ <p>Connecting to ArConnect...</p>
223
+ )}
224
+ </div>
225
+ )
226
+ }`,
227
+ "frontend/src/index.css": `body {
228
+ margin: 0;
229
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
230
+ Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
231
+ -webkit-font-smoothing: antialiased;
232
+ -moz-osx-font-smoothing: grayscale;
233
+ }
234
+
235
+ code {
236
+ font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
237
+ }`,
238
+ "frontend/src/main.jsx": `import React from "react"
239
+ import ReactDOM from "react-dom/client"
240
+ import App from "./App"
241
+ import "./index.css"
242
+
243
+ ReactDOM.createRoot(document.getElementById("root")).render(
244
+ <React.StrictMode>
245
+ <App />
246
+ </React.StrictMode>
247
+ )`,
248
+ "frontend/src/test-setup.js": "",
249
+ "frontend/src/__tests__/App.test.jsx": `import { describe, it, expect } from "vitest"
250
+ import { render, screen, waitFor } from "@testing-library/react"
251
+ import App from "../App"
252
+
253
+ describe("App", () => {
254
+ it("renders title", () => {
255
+ render(<App />)
256
+ expect(screen.getByText("Token Transfer")).toBeInTheDocument()
257
+ })
258
+
259
+ it("shows connecting message initially", () => {
260
+ render(<App />)
261
+ expect(screen.getByText("Connecting to ArConnect...")).toBeInTheDocument()
262
+ })
263
+
264
+ it("renders balance after connection", async () => {
265
+ window.arweaveWallet = {
266
+ connect: async () => {},
267
+ getActiveAddress: async () => "test-addr",
268
+ }
269
+ render(<App />)
270
+ await waitFor(() => {
271
+ expect(screen.queryByText("Connecting")).not.toBeInTheDocument()
272
+ })
273
+ })
274
+ })`,
275
+ "frontend/src/__tests__/TransferForm.test.jsx": `import { describe, it, expect, vi } from "vitest"
276
+ import { render, screen, fireEvent, waitFor } from "@testing-library/react"
277
+ import TransferForm from "../components/TransferForm"
278
+
279
+ const mockAo = {
280
+ p: (pid) => ({
281
+ m: vi.fn().mockResolvedValue({ out: "Transfer successful" })
282
+ })
283
+ }
284
+
285
+ describe("TransferForm", () => {
286
+ it("renders form fields", () => {
287
+ render(<TransferForm ao={mockAo} />)
288
+ expect(screen.getByLabelText(/recipient/i)).toBeInTheDocument()
289
+ expect(screen.getByLabelText(/amount/i)).toBeInTheDocument()
290
+ })
291
+
292
+ it("validates empty fields", async () => {
293
+ render(<TransferForm ao={mockAo} />)
294
+ fireEvent.click(screen.getByRole("button", { name: /transfer/i }))
295
+ await waitFor(() => {
296
+ expect(screen.getByText(/required/i)).toBeInTheDocument()
297
+ })
298
+ })
299
+
300
+ it("validates negative amounts", async () => {
301
+ render(<TransferForm ao={mockAo} />)
302
+ fireEvent.change(screen.getByLabelText(/amount/i), { target: { value: "-10" } })
303
+ fireEvent.click(screen.getByRole("button", { name: /transfer/i }))
304
+ await waitFor(() => {
305
+ expect(screen.getByText(/positive/i)).toBeInTheDocument()
306
+ })
307
+ })
308
+
309
+ it("submits transfer successfully", async () => {
310
+ render(<TransferForm ao={mockAo} />)
311
+ fireEvent.change(screen.getByLabelText(/recipient/i), { target: { value: "addr-123" } })
312
+ fireEvent.change(screen.getByLabelText(/amount/i), { target: { value: "50" } })
313
+ fireEvent.click(screen.getByRole("button", { name: /transfer/i }))
314
+ await waitFor(() => {
315
+ expect(screen.getByText(/successful/i)).toBeInTheDocument()
316
+ })
317
+ })
318
+ })`,
319
+ "frontend/src/components/BalanceDisplay.jsx": `import { useState, useEffect } from "react"
320
+
321
+ export default function BalanceDisplay({ ao }) {
322
+ const [balance, setBalance] = useState(null)
323
+
324
+ useEffect(() => {
325
+ if (!ao) return
326
+ const fetchBalance = async () => {
327
+ try {
328
+ const { out } = await ao.p(import.meta.env.VITE_PROCESS_ID).m("Balance")
329
+ setBalance(out)
330
+ } catch (err) {
331
+ console.error("Failed to fetch balance:", err)
332
+ }
333
+ }
334
+ fetchBalance()
335
+ const id = setInterval(fetchBalance, 10000)
336
+ return () => clearInterval(id)
337
+ }, [ao])
338
+
339
+ return (
340
+ <div className="card" data-testid="balance">
341
+ <div className="balance-label">Your Balance</div>
342
+ <div className="balance">{balance ?? "..."} TKN</div>
343
+ </div>
344
+ )
345
+ }`,
346
+ "frontend/src/components/TokenInfo.jsx": `import { useState, useEffect } from "react"
347
+
348
+ export default function TokenInfo({ ao }) {
349
+ const [info, setInfo] = useState(null)
350
+
351
+ useEffect(() => {
352
+ if (!ao) return
353
+ const fetch = async () => {
354
+ const { out } = await ao.p(import.meta.env.VITE_PROCESS_ID).m("Info")
355
+ setInfo(JSON.parse(out))
356
+ }
357
+ fetch().catch(console.error)
358
+ }, [ao])
359
+
360
+ if (!info) return <div className="card">Loading token info...</div>
361
+
362
+ return (
363
+ <div className="card">
364
+ <h3>{info.Name} ({info.Ticker})</h3>
365
+ <p>Denomination: {info.Denomination}</p>
366
+ </div>
367
+ )
368
+ }`,
369
+ "frontend/src/components/TransactionStatus.jsx": `export default function TransactionStatus({ status, error }) {
370
+ if (!status && !error) return null
371
+ return (
372
+ <div className={\`status \${error ? "status-error" : "status-success"}\`} data-testid="status">
373
+ {error || status}
374
+ </div>
375
+ )
376
+ }`,
377
+ "frontend/src/components/TransferForm.jsx": "",
378
+ "frontend/src/components/WalletConnect.jsx": `import { useState } from "react"
379
+
380
+ export default function WalletConnect({ onConnect }) {
381
+ const [connecting, setConnecting] = useState(false)
382
+
383
+ const handleConnect = async () => {
384
+ setConnecting(true)
385
+ try {
386
+ await window.arweaveWallet.connect(["ACCESS_ADDRESS", "SIGN_TRANSACTION"])
387
+ const addr = await window.arweaveWallet.getActiveAddress()
388
+ onConnect(addr)
389
+ } catch (err) {
390
+ console.error("Wallet connection failed:", err)
391
+ } finally {
392
+ setConnecting(false)
393
+ }
394
+ }
395
+
396
+ return (
397
+ <button className="btn btn-primary" onClick={handleConnect} disabled={connecting}>
398
+ {connecting ? "Connecting..." : "Connect Wallet"}
399
+ </button>
400
+ )
401
+ }`,
402
+ "frontend/src/hooks/useToken.js": `import { useState, useEffect, useCallback } from "react"
403
+
404
+ export function useToken(ao, processId) {
405
+ const [balance, setBalance] = useState(null)
406
+ const [info, setInfo] = useState(null)
407
+ const [loading, setLoading] = useState(false)
408
+
409
+ const fetchBalance = useCallback(async () => {
410
+ if (!ao || !processId) return
411
+ try {
412
+ const { out } = await ao.p(processId).m("Balance")
413
+ setBalance(out)
414
+ } catch (err) {
415
+ console.error("Balance fetch failed:", err)
416
+ }
417
+ }, [ao, processId])
418
+
419
+ const fetchInfo = useCallback(async () => {
420
+ if (!ao || !processId) return
421
+ try {
422
+ const { out } = await ao.p(processId).m("Info")
423
+ setInfo(JSON.parse(out))
424
+ } catch (err) {
425
+ console.error("Info fetch failed:", err)
426
+ }
427
+ }, [ao, processId])
428
+
429
+ const transfer = useCallback(async (recipient, quantity) => {
430
+ if (!ao || !processId) throw new Error("Not connected")
431
+ setLoading(true)
432
+ try {
433
+ const { out } = await ao.p(processId).m("Transfer", {
434
+ Recipient: recipient,
435
+ Quantity: String(quantity),
436
+ })
437
+ await fetchBalance()
438
+ return out
439
+ } finally {
440
+ setLoading(false)
441
+ }
442
+ }, [ao, processId, fetchBalance])
443
+
444
+ useEffect(() => {
445
+ fetchBalance()
446
+ fetchInfo()
447
+ }, [fetchBalance, fetchInfo])
448
+
449
+ return { balance, info, loading, transfer, refresh: fetchBalance }
450
+ }`,
451
+ "frontend/src/hooks/useWallet.js": `import { useState, useEffect } from "react"
452
+
453
+ export function useWallet() {
454
+ const [address, setAddress] = useState(null)
455
+ const [connected, setConnected] = useState(false)
456
+
457
+ useEffect(() => {
458
+ const checkConnection = async () => {
459
+ try {
460
+ if (window.arweaveWallet) {
461
+ const addr = await window.arweaveWallet.getActiveAddress()
462
+ setAddress(addr)
463
+ setConnected(true)
464
+ }
465
+ } catch {}
466
+ }
467
+ checkConnection()
468
+ window.addEventListener("arweaveWalletLoaded", checkConnection)
469
+ return () => window.removeEventListener("arweaveWalletLoaded", checkConnection)
470
+ }, [])
471
+
472
+ const connect = async () => {
473
+ await window.arweaveWallet.connect(["ACCESS_ADDRESS", "SIGN_TRANSACTION"])
474
+ const addr = await window.arweaveWallet.getActiveAddress()
475
+ setAddress(addr)
476
+ setConnected(true)
477
+ }
478
+
479
+ const disconnect = () => {
480
+ setAddress(null)
481
+ setConnected(false)
482
+ }
483
+
484
+ return { address, connected, connect, disconnect }
485
+ }`,
486
+ "HyperBEAM/src/dev_token.erl": `%%% @doc Token device for HyperBEAM.
487
+ %%% Manages token balances with mint, transfer, and balance queries.
488
+ %%% Tests are inline (HyperBEAM convention — device + eunit in same file).
489
+ -module(dev_token).
490
+ -export([info/0, init/3, execute/3]).
491
+ -include("include/hb.hrl").
492
+ -ifdef(TEST).
493
+ -include_lib("eunit/include/eunit.hrl").
494
+ -endif.
495
+
496
+ info() ->
497
+ #{
498
+ name => <<"Token">>,
499
+ description => <<"Token transfer device">>,
500
+ version => <<"1.0.0">>
501
+ }.
502
+
503
+ init(_ID, _Params, State) ->
504
+ {ok, State#{balances => #{}}}.
505
+
506
+ execute(<<"Mint">>, Msg, State = #{balances := Bals}) ->
507
+ Sender = hb_message:get(<<"From">>, Msg),
508
+ Qty = binary_to_integer(hb_message:get(<<"Quantity">>, Msg)),
509
+ Current = maps:get(Sender, Bals, 0),
510
+ NewBals = maps:put(Sender, Current + Qty, Bals),
511
+ {ok, #{data => <<"Minted">>}, State#{balances => NewBals}};
512
+
513
+ execute(<<"Transfer">>, Msg, State = #{balances := Bals}) ->
514
+ Sender = hb_message:get(<<"From">>, Msg),
515
+ Recipient = hb_message:get(<<"Recipient">>, Msg),
516
+ Qty = binary_to_integer(hb_message:get(<<"Quantity">>, Msg)),
517
+ SenderBal = maps:get(Sender, Bals, 0),
518
+ case SenderBal >= Qty of
519
+ true ->
520
+ NewBals = maps:put(Sender, SenderBal - Qty,
521
+ maps:put(Recipient, maps:get(Recipient, Bals, 0) + Qty, Bals)),
522
+ {ok, #{data => <<"Transferred">>}, State#{balances => NewBals}};
523
+ false ->
524
+ {error, <<"Insufficient balance">>}
525
+ end;
526
+
527
+ execute(<<"Balance">>, Msg, State = #{balances := Bals}) ->
528
+ Target = hb_message:get(<<"Target">>, Msg, hb_message:get(<<"From">>, Msg)),
529
+ Bal = maps:get(Target, Bals, 0),
530
+ {ok, #{data => integer_to_binary(Bal)}, State};
531
+
532
+ execute(_Action, _Msg, State) ->
533
+ {ok, #{data => <<"Unknown action">>}, State}.
534
+
535
+ %%%===================================================================
536
+ %%% EUnit Tests
537
+ %%%===================================================================
538
+ -ifdef(TEST).
539
+
540
+ mint_test() ->
541
+ {ok, State0} = init(<<"test">>, #{}, #{}),
542
+ Msg = #{<<"From">> => <<"alice">>, <<"Quantity">> => <<"1000">>},
543
+ {ok, #{data := <<"Minted">>}, State1} = execute(<<"Mint">>, Msg, State0),
544
+ BalMsg = #{<<"From">> => <<"alice">>},
545
+ {ok, #{data := <<"1000">>}, _} = execute(<<"Balance">>, BalMsg, State1).
546
+
547
+ transfer_test() ->
548
+ {ok, S0} = init(<<"test">>, #{}, #{}),
549
+ {ok, _, S1} = execute(<<"Mint">>,
550
+ #{<<"From">> => <<"alice">>, <<"Quantity">> => <<"500">>}, S0),
551
+ {ok, #{data := <<"Transferred">>}, S2} = execute(<<"Transfer">>,
552
+ #{<<"From">> => <<"alice">>, <<"Recipient">> => <<"bob">>,
553
+ <<"Quantity">> => <<"200">>}, S1),
554
+ {ok, #{data := <<"300">>}, _} = execute(<<"Balance">>,
555
+ #{<<"From">> => <<"alice">>}, S2),
556
+ {ok, #{data := <<"200">>}, _} = execute(<<"Balance">>,
557
+ #{<<"From">> => <<"bob">>}, S2).
558
+
559
+ insufficient_balance_test() ->
560
+ {ok, S0} = init(<<"test">>, #{}, #{}),
561
+ {ok, _, S1} = execute(<<"Mint">>,
562
+ #{<<"From">> => <<"alice">>, <<"Quantity">> => <<"100">>}, S0),
563
+ {error, <<"Insufficient balance">>} = execute(<<"Transfer">>,
564
+ #{<<"From">> => <<"alice">>, <<"Recipient">> => <<"bob">>,
565
+ <<"Quantity">> => <<"200">>}, S1).
566
+
567
+ -endif.`,
568
+
569
+ "scripts/deploy.js": `import { readFileSync } from "node:fs"
31
570
  import { AO } from "wao"
32
- const hbeam = await new HyperBEAM({ reset: true }).ready()
33
- const ao = await new AO({ hb: hbeam.url, mode: "lua" }).init(hbeam.jwk)
34
- const { p, pid } = await ao.deploy({ src_data })`,
35
- notes: "Fast Lua VM (no WASM). No receive() — use msg.reply() pattern.",
36
- },
37
- {
38
- target: "Remote HyperBEAM (genesis-wasm)",
39
- command: "yarn deploy --mainnet src/app.lua",
40
- code: `import { AO } from "wao"
41
- const jwk = JSON.parse(fs.readFileSync(".wallet.json", "utf8"))
42
- const ao = await new AO({
43
- hb: "https://push-1.forward.computer"
44
- }).init(jwk)
45
- const { p, pid } = await ao.deploy({ src_data })`,
46
- notes: "Production deployment. Full compute with genesis-wasm.",
47
- },
48
- {
49
- target: "Remote HyperBEAM (Lua mode)",
50
- command: "yarn deploy --mainnet --lua src/app.lua",
51
- code: `import { AO } from "wao"
52
- const jwk = JSON.parse(fs.readFileSync(".wallet.json", "utf8"))
53
- const ao = await new AO({
54
- hb: "https://push-1.forward.computer",
55
- mode: "lua"
56
- }).init(jwk)
57
- const { p, pid } = await ao.deploy({ src_data })`,
58
- notes: "Production deployment with fast Lua VM. No receive().",
59
- },
571
+
572
+ const src = process.argv[2]
573
+ if (!src) { console.error("Usage: yarn deploy <file.lua>"); process.exit(1) }
574
+
575
+ const jwk = JSON.parse(readFileSync(".wallet.json", "utf8"))
576
+ const ao = await new AO().init(jwk)
577
+ const src_data = readFileSync(src, "utf8")
578
+ const { pid } = await ao.deploy({ src_data })
579
+ console.log("Deployed:", pid)`,
580
+ "src/counter.lua": `-- Simple Counter for AOS
581
+ Count = Count or 0
582
+
583
+ Handlers.add("increment",
584
+ Handlers.utils.hasMatchingTag("Action", "Increment"),
585
+ function(msg)
586
+ Count = Count + 1
587
+ msg.reply({ Data = tostring(Count) })
588
+ end
589
+ )
590
+
591
+ Handlers.add("get",
592
+ Handlers.utils.hasMatchingTag("Action", "Get"),
593
+ function(msg)
594
+ msg.reply({ Data = tostring(Count) })
595
+ end
596
+ )`,
597
+ "src/registry.lua": `-- Token Registry for AOS
598
+ local json = require("json")
599
+
600
+ Registry = Registry or {}
601
+
602
+ Handlers.add("register",
603
+ Handlers.utils.hasMatchingTag("Action", "Register"),
604
+ function(msg)
605
+ local id = msg.Tags.ProcessId
606
+ local name = msg.Tags.Name or "Unknown"
607
+ Registry[id] = { name = name, owner = msg.From }
608
+ msg.reply({ Data = "Registered: " .. name })
609
+ end
610
+ )
611
+
612
+ Handlers.add("list",
613
+ Handlers.utils.hasMatchingTag("Action", "List"),
614
+ function(msg)
615
+ msg.reply({ Data = json.encode(Registry) })
616
+ end
617
+ )`,
618
+ "src/token.lua": `-- Token Handler for AOS
619
+ local json = require("json")
620
+
621
+ Balances = Balances or {}
622
+ Name = Name or "Token"
623
+ Ticker = Ticker or "TKN"
624
+ Denomination = Denomination or 12
625
+
626
+ Handlers.add("info",
627
+ Handlers.utils.hasMatchingTag("Action", "Info"),
628
+ function(msg)
629
+ msg.reply({
630
+ Data = json.encode({ Name = Name, Ticker = Ticker, Denomination = Denomination })
631
+ })
632
+ end
633
+ )
634
+
635
+ Handlers.add("balance",
636
+ Handlers.utils.hasMatchingTag("Action", "Balance"),
637
+ function(msg)
638
+ local target = msg.Tags.Target or msg.From
639
+ local bal = Balances[target] or "0"
640
+ msg.reply({ Data = bal, Tags = { Balance = bal, Target = target } })
641
+ end
642
+ )
643
+
644
+ Handlers.add("transfer",
645
+ Handlers.utils.hasMatchingTag("Action", "Transfer"),
646
+ function(msg)
647
+ local qty = tonumber(msg.Tags.Quantity)
648
+ local from = msg.From
649
+ local to = msg.Tags.Recipient
650
+ assert(qty > 0, "Quantity must be positive")
651
+ assert(tonumber(Balances[from] or "0") >= qty, "Insufficient balance")
652
+ Balances[from] = tostring(tonumber(Balances[from]) - qty)
653
+ Balances[to] = tostring(tonumber(Balances[to] or "0") + qty)
654
+ msg.reply({ Data = "Transfer successful" })
655
+ ao.send({ Target = to, Action = "Credit-Notice", Quantity = tostring(qty), Sender = from })
656
+ end
657
+ )`,
658
+ "test/aos.test.js": `import { describe, it } from "node:test"
659
+ import assert from "node:assert"
660
+ import { readFileSync } from "node:fs"
661
+ import { AO, acc } from "wao/test"
662
+
663
+ describe("AOS Integration", () => {
664
+ it("should deploy and interact with counter", async () => {
665
+ const ao = await new AO().init(acc[0])
666
+ const src = readFileSync("src/counter.lua", "utf8")
667
+ const { p } = await ao.deploy({ src_data: src })
668
+ await p.m("Increment")
669
+ await p.m("Increment")
670
+ const { out } = await p.m("Get")
671
+ assert.equal(out, "2")
672
+ })
673
+ })`,
674
+ "test/hyperbeam-token.test.js": `import { describe, it } from "node:test"
675
+ import assert from "node:assert"
676
+ import { readFileSync } from "node:fs"
677
+ import { AO, HyperBEAM, acc } from "wao/test"
678
+
679
+ describe("HyperBEAM Token Integration", () => {
680
+ let hbeam, ao, p
681
+
682
+ it("should start HyperBEAM", async () => {
683
+ hbeam = await new HyperBEAM({ reset: true }).ready()
684
+ ao = await new AO({ hb: hbeam.url }).init(acc[0])
685
+ })
686
+
687
+ it("should deploy token to HyperBEAM", async () => {
688
+ const src = readFileSync("src/token.lua", "utf8")
689
+ const result = await ao.deploy({ src_data: src })
690
+ p = result.p
691
+ assert.ok(p)
692
+ })
693
+
694
+ it("should mint tokens via HyperBEAM", async () => {
695
+ await p.m("Mint", { Quantity: "5000" })
696
+ const { out } = await p.m("Balance")
697
+ assert.equal(out, "5000")
698
+ })
699
+
700
+ it("should transfer via HyperBEAM", async () => {
701
+ await p.m("Transfer", { Recipient: acc[1].addr, Quantity: "200" })
702
+ const { out } = await p.m("Balance")
703
+ assert.equal(out, "4800")
704
+ })
705
+
706
+ it("should cleanup", () => hbeam?.kill())
707
+ })`,
708
+ "test/hyperbeam.test.js": `import { describe, it } from "node:test"
709
+ import assert from "node:assert"
710
+ import { readFileSync } from "node:fs"
711
+ import { AO, HB, HyperBEAM, acc } from "wao/test"
712
+
713
+ describe("HyperBEAM", () => {
714
+ let hbeam
715
+
716
+ it("should start HyperBEAM node", async () => {
717
+ hbeam = await new HyperBEAM({ reset: true }).ready()
718
+ assert.ok(hbeam.port)
719
+ assert.ok(hbeam.url)
720
+ })
721
+
722
+ it("should respond to info endpoint", async () => {
723
+ const hb = new HB({ url: hbeam.url })
724
+ const info = await hb.info()
725
+ assert.ok(info.address)
726
+ })
727
+
728
+ it("should deploy and run AOS process", async () => {
729
+ const ao = await new AO({ hb: hbeam.url }).init(acc[0])
730
+ const src = readFileSync("src/counter.lua", "utf8")
731
+ const { p } = await ao.deploy({ src_data: src })
732
+ await p.m("Increment")
733
+ const { out } = await p.m("Get")
734
+ assert.equal(out, "1")
735
+ })
736
+
737
+ it("should cleanup", () => hbeam?.kill())
738
+ })`,
739
+ "test/registry.test.js": `import { describe, it } from "node:test"
740
+ import assert from "node:assert"
741
+ import { readFileSync } from "node:fs"
742
+ import { AO, acc } from "wao/test"
743
+
744
+ describe("Registry", () => {
745
+ let ao, p
746
+
747
+ it("should deploy registry", async () => {
748
+ ao = await new AO().init(acc[0])
749
+ const src = readFileSync("src/registry.lua", "utf8")
750
+ const result = await ao.deploy({ src_data: src })
751
+ p = result.p
752
+ assert.ok(p)
753
+ })
754
+
755
+ it("should register a token", async () => {
756
+ const { out } = await p.m("Register", {
757
+ ProcessId: "test-process-123",
758
+ Name: "TestToken",
759
+ })
760
+ assert.match(out, /Registered/)
761
+ })
762
+
763
+ it("should list registered tokens", async () => {
764
+ const { out } = await p.m("List")
765
+ const registry = JSON.parse(out)
766
+ assert.ok(registry["test-process-123"])
767
+ assert.equal(registry["test-process-123"].name, "TestToken")
768
+ })
769
+
770
+ it("should register multiple tokens", async () => {
771
+ await p.m("Register", { ProcessId: "proc-a", Name: "Alpha" })
772
+ await p.m("Register", { ProcessId: "proc-b", Name: "Beta" })
773
+ const { out } = await p.m("List")
774
+ const registry = JSON.parse(out)
775
+ assert.equal(Object.keys(registry).length, 3)
776
+ })
777
+ })`,
778
+ "test/token-device.test.js": `import { describe, it } from "node:test"
779
+ import assert from "node:assert"
780
+ import { HB, HyperBEAM, acc } from "wao/test"
781
+
782
+ describe("Token Device", () => {
783
+ let hbeam, hb
784
+
785
+ it("should start HyperBEAM with token device", async () => {
786
+ hbeam = await new HyperBEAM({ reset: true }).ready()
787
+ hb = new HB({ url: hbeam.url })
788
+ assert.ok(hb)
789
+ })
790
+
791
+ it("should call token device info", async () => {
792
+ const res = await hb.get({ path: "/~token@1.0/info" })
793
+ assert.ok(res)
794
+ })
795
+
796
+ it("should cleanup", () => hbeam?.kill())
797
+ })`,
798
+ "test/token.test.js": `import { describe, it } from "node:test"
799
+ import assert from "node:assert"
800
+ import { readFileSync } from "node:fs"
801
+ import { AO, acc } from "wao/test"
802
+
803
+ describe("Token", () => {
804
+ let ao, p
805
+
806
+ it("should deploy token process", async () => {
807
+ ao = await new AO().init(acc[0])
808
+ const src = readFileSync("src/token.lua", "utf8")
809
+ const result = await ao.deploy({ src_data: src })
810
+ p = result.p
811
+ assert.ok(p)
812
+ })
813
+
814
+ it("should return token info", async () => {
815
+ const { out } = await p.m("Info")
816
+ const info = JSON.parse(out)
817
+ assert.equal(info.Name, "Token")
818
+ })
819
+
820
+ it("should transfer tokens", async () => {
821
+ await p.m("Mint", { Quantity: "1000" })
822
+ await p.m("Transfer", { Recipient: acc[1].addr, Quantity: "100" })
823
+ const { out } = await p.m("Balance")
824
+ assert.equal(out, "900")
825
+ })
826
+ })`,
827
+ }
828
+
829
+ // ═══════════════════════════════════════════════════════════════
830
+ // Constants
831
+ // ═══════════════════════════════════════════════════════════════
832
+
833
+ const TYPE_COLORS = {
834
+ plan: "#8b5cf6", aos: "#3b82f6", "aos-test": "#14b8a6", "aos-integration": "#06b6d4",
835
+ device: "#22c55e", "device-integration": "#059669",
836
+ frontend: "#f97316", "frontend-test": "#ea580c", "frontend-integration": "#eab308",
837
+ "module-lua": "#a855f7", "module-wasm": "#9333ea", "module-test": "#7c3aed",
838
+ readme: "#6b7280", validate: "#84cc16",
839
+ }
840
+
841
+ const BADGE_COLORS = {
842
+ orchestrator: "#16a34a", plan: "#8b5cf6", validate: "#ca8a04", generate: "#2563eb",
843
+ build: "#ea580c", test: "#16a34a", info: "#6b7280", deploy: "#2563eb",
844
+ scaffold: "#0d9488", debug: "#dc2626", dev: "#ea580c", team: "#7c3aed",
845
+ }
846
+
847
+ const FILE_BADGES = {
848
+ json: { label: "JSON", color: "#3b82f6" }, md: { label: "MD", color: "#14b8a6" },
849
+ css: { label: "CSS", color: "#8b5cf6" }, lua: { label: "Lua", color: "#3b82f6" },
850
+ js: { label: "JS", color: "#eab308" }, jsx: { label: "JSX", color: "#eab308" },
851
+ ts: { label: "TS", color: "#3b82f6" }, tsx: { label: "TSX", color: "#3b82f6" },
852
+ erl: { label: "Erl", color: "#ef4444" }, rs: { label: "Rust", color: "#f97316" },
853
+ toml: { label: "TOML", color: "#6b7280" }, html: { label: "HTML", color: "#f97316" },
854
+ sh: { label: "Shell", color: "#22c55e" },
855
+ }
856
+
857
+ const EXT_TO_LANG = {
858
+ js: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript",
859
+ lua: "lua", json: "json", md: "markdown", css: "css", html: "xml",
860
+ erl: "erlang", rs: "rust", sh: "bash", toml: "ini",
861
+ }
862
+
863
+ const SKILLS = {
864
+ "Build Workflow": [
865
+ { cmd: "/build", badge: "orchestrator", desc: "Full build workflow \u2014 plan, build, test, validate, README. Orchestrates all steps." },
866
+ { cmd: "/plan", badge: "plan", desc: "Plan a feature \u2014 writes plan.md + tasks.json for persistent workflow." },
867
+ { cmd: "/validate", badge: "validate", desc: "Post-build validation \u2014 tests, Lua pitfalls, handler coverage." },
868
+ { cmd: "/readme", badge: "generate", desc: "Generate comprehensive README.md from plan, code, and tests." },
869
+ ],
870
+ "Build Steps": [
871
+ { cmd: "/build-aos", badge: "build", desc: "Build AOS Lua scripts + in-memory tests, iterate until 100% pass." },
872
+ { cmd: "/build-module", badge: "build", desc: "Build custom WASM64 (Rust) or standalone Lua modules + HyperBEAM integration tests." },
873
+ { cmd: "/build-device", badge: "build", desc: "Build Erlang device + inline eunit tests, iterate until 100% pass." },
874
+ { cmd: "/build-frontend", badge: "build", desc: "Build Vite + React components + vitest tests, iterate until 100% pass." },
875
+ ],
876
+ "Test Steps": [
877
+ { cmd: "/test", badge: "test", desc: "Run in-memory AOS unit tests." },
878
+ { cmd: "/test-hb", badge: "test", desc: "Run HyperBEAM integration tests with real Erlang node." },
879
+ { cmd: "/test-device", badge: "test", desc: "WAO SDK integration tests for Erlang devices via HTTP." },
880
+ { cmd: "/test-e2e", badge: "test", desc: "Playwright E2E tests with live HyperBEAM backend." },
881
+ ],
882
+ Utilities: [
883
+ { cmd: "/report", badge: "info", desc: "Show progress on current plan \u2014 task status, test results." },
884
+ { cmd: "/deploy", badge: "deploy", desc: "Deploy Lua source to testnet, local HB, or remote HB." },
885
+ { cmd: "/create-aos", badge: "scaffold", desc: "Scaffold new AOS Lua script + test file." },
886
+ { cmd: "/create-module", badge: "scaffold", desc: "Scaffold custom module (WASM64 Rust or standalone Lua) + test." },
887
+ { cmd: "/create-device", badge: "scaffold", desc: "Scaffold new HyperBEAM Erlang device + test." },
888
+ { cmd: "/debug", badge: "debug", desc: "Troubleshoot issues \u2014 port conflicts, WASM errors, compilation." },
889
+ { cmd: "/dev", badge: "dev", desc: "Start Vite dev server for frontend development." },
890
+ { cmd: "/team", badge: "team", desc: "Set up an agent team for parallel development." },
891
+ ],
892
+ }
893
+
894
+ function deriveCommands(data) {
895
+ const tasks = data?.tasks || []
896
+ const luaFiles = []
897
+ const testFiles = []
898
+ const hbTestFiles = []
899
+ const moduleTestFiles = []
900
+ const frontendTestFiles = []
901
+ const e2eFiles = []
902
+ const hasDevice = tasks.some(t => t.type === "device")
903
+ for (const t of tasks) {
904
+ for (const f of (t.files || [])) {
905
+ if (f.startsWith("src/") && f.endsWith(".lua")) luaFiles.push(f)
906
+ if (f.startsWith("test/") && f.endsWith(".test.js")) {
907
+ if (t.type === "aos-test") testFiles.push(f)
908
+ else if (t.type === "aos-integration" || t.type === "device-integration") hbTestFiles.push(f)
909
+ else if (t.type === "module-test") moduleTestFiles.push(f)
910
+ }
911
+ if (f.startsWith("frontend/") && f.endsWith(".test.jsx")) frontendTestFiles.push(f)
912
+ if (f.startsWith("frontend/e2e/")) e2eFiles.push(f)
913
+ }
914
+ }
915
+ const cmds = {}
916
+ const testing = [{ cmd: "yarn test", desc: "Run all unit tests" }]
917
+ for (const f of testFiles) testing.push({ cmd: `yarn test ${f}`, desc: `AOS unit tests \u2014 ${f.split("/").pop()}` })
918
+ for (const f of moduleTestFiles) testing.push({ cmd: `yarn test ${f}`, desc: `Custom module tests \u2014 ${f.split("/").pop()}` })
919
+ for (const f of hbTestFiles) testing.push({ cmd: `yarn test ${f}`, desc: `HyperBEAM integration \u2014 ${f.split("/").pop()}` })
920
+ if (hasDevice) {
921
+ const devFiles = tasks.filter(t => t.type === "device").flatMap(t => (t.files || []).filter(f => f.endsWith(".erl")))
922
+ for (const f of devFiles) {
923
+ const mod = f.split("/").pop().replace(".erl", "")
924
+ testing.push({ cmd: `cd $HB_DIR && rebar3 eunit --module=${mod}`, desc: `Erlang eunit \u2014 ${mod}` })
925
+ }
926
+ }
927
+ cmds["Testing"] = testing
928
+ const deploy = []
929
+ const luaArg = luaFiles.length === 1 ? ` ${luaFiles[0]}` : ""
930
+ deploy.push({ cmd: `yarn deploy${luaArg}`, desc: `Deploy ${luaFiles.length > 1 ? "all src/*.lua" : luaArg.trim() || "scripts"} to AO testnet` })
931
+ deploy.push({ cmd: `yarn deploy --local-hb${luaArg}`, desc: "Deploy to local HyperBEAM (genesis-wasm)" })
932
+ deploy.push({ cmd: `yarn deploy --mainnet${luaArg}`, desc: "Deploy to remote HyperBEAM" })
933
+ cmds["Deployment"] = deploy
934
+ cmds["Development"] = [
935
+ { cmd: "yarn start", desc: "Start dashboard (API :3333 + Vite :5174)" },
936
+ { cmd: "yarn start:api", desc: "Start API server only (:3333)" },
937
+ { cmd: "yarn keygen", desc: "Generate Arweave wallet (.wallet.json)" },
938
+ ]
939
+ const hasFrontend = tasks.some(t => t.type.startsWith("frontend"))
940
+ if (hasFrontend) {
941
+ const fe = [
942
+ { cmd: "cd frontend && npm run dev", desc: "Start Vite dev server (port 5173)" },
943
+ ]
944
+ for (const f of frontendTestFiles) fe.push({ cmd: `cd frontend && npx vitest run ${f.replace("frontend/", "")}`, desc: `Vitest \u2014 ${f.split("/").pop()}` })
945
+ if (frontendTestFiles.length === 0) fe.push({ cmd: "cd frontend && npm run test:unit", desc: "Run vitest component tests" })
946
+ for (const f of e2eFiles) fe.push({ cmd: `cd frontend && npx playwright test ${f.replace("frontend/", "")}`, desc: `E2E \u2014 ${f.split("/").pop()}` })
947
+ if (e2eFiles.length === 0) fe.push({ cmd: "cd frontend && npm run test:e2e", desc: "Run Playwright E2E tests" })
948
+ fe.push({ cmd: "cd frontend && npm run build", desc: "Production build" })
949
+ cmds["Frontend"] = fe
950
+ }
951
+ return cmds
952
+ }
953
+
954
+ const ENV_VARS = [
955
+ { name: "PORT", desc: "HyperBEAM HTTP port", def: "10001" },
956
+ { name: "MESSENGER_URL", desc: "Messenger unit URL", def: "(AO default)" },
957
+ { name: "CU_URL", desc: "Compute unit URL", def: "(AO default)" },
958
+ { name: "HB_URL", desc: "HyperBEAM URL", def: "http://localhost:10001" },
959
+ { name: "WALLET_PATH", desc: "Arweave JWK path", def: ".wallet.json" },
960
+ ]
961
+
962
+ // ═══════════════════════════════════════════════════════════════
963
+ // Helpers
964
+ // ═══════════════════════════════════════════════════════════════
965
+
966
+ function getTrackForType(type) {
967
+ if (/^aos/.test(type)) return "AOS"
968
+ if (/^device/.test(type)) return "Device"
969
+ if (/^frontend/.test(type)) return "Frontend"
970
+ if (/^module/.test(type)) return "Modules"
971
+ if (/^(readme|validate)$/.test(type)) return "Validate"
972
+ return null
973
+ }
974
+
975
+ function deriveTrackCards(tasks) {
976
+ const tracks = {}
977
+ for (const t of tasks) {
978
+ const track = getTrackForType(t.type)
979
+ if (!track) continue
980
+ if (!tracks[track]) tracks[track] = { total: 0, done: 0, inProgress: 0 }
981
+ tracks[track].total++
982
+ if (t.status === "done") tracks[track].done++
983
+ if (t.status === "in_progress") tracks[track].inProgress++
984
+ }
985
+ const order = ["AOS", "Modules", "Device", "Frontend", "Validate"]
986
+ return order.filter(n => tracks[n]).map(name => {
987
+ const t = tracks[name]
988
+ const status = t.done === t.total ? "Done" : t.inProgress > 0 ? "In Progress" : "Pending"
989
+ return { name, status }
990
+ })
991
+ }
992
+
993
+ function formatDuration(ms) {
994
+ if (ms < 0) return "0s"
995
+ const s = Math.floor(ms / 1000) % 60
996
+ const m = Math.floor(ms / 60000) % 60
997
+ const h = Math.floor(ms / 3600000)
998
+ if (h > 0) return `${h}h ${m}m`
999
+ if (m > 0) return `${m}m ${s}s`
1000
+ return `${s}s`
1001
+ }
1002
+
1003
+ function useElapsed(startedAt) {
1004
+ const [now, setNow] = useState(Date.now())
1005
+ useEffect(() => {
1006
+ if (!startedAt) return
1007
+ const id = setInterval(() => setNow(Date.now()), 1000)
1008
+ return () => clearInterval(id)
1009
+ }, [startedAt])
1010
+ if (!startedAt) return null
1011
+ return formatDuration(now - new Date(startedAt).getTime())
1012
+ }
1013
+
1014
+ const formatSize = b => b < 1024 ? b + " B" : (b / 1024).toFixed(1) + " KB"
1015
+ const getFileExt = p => { const d = p.lastIndexOf("."); return d > -1 ? p.slice(d + 1).toLowerCase() : "" }
1016
+ const escapeHtml = t => t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
1017
+
1018
+ function groupFilesByDir(files) {
1019
+ const groups = {}
1020
+ for (const f of files) {
1021
+ const slash = f.path.lastIndexOf("/")
1022
+ const dir = slash > -1 ? f.path.slice(0, slash) + "/" : "{root}/"
1023
+ if (!groups[dir]) groups[dir] = []
1024
+ groups[dir].push(f)
1025
+ }
1026
+ return groups
1027
+ }
1028
+
1029
+ // Build tree entries for current directory level
1030
+ function getTreeEntries(files, currentPath) {
1031
+ const prefix = currentPath || ""
1032
+ const entries = []
1033
+ const seenDirs = new Set()
1034
+
1035
+ for (const f of files) {
1036
+ const rel = prefix ? (f.path.startsWith(prefix) ? f.path.slice(prefix.length) : null) : f.path
1037
+ if (rel == null) continue
1038
+ const slashIdx = rel.indexOf("/")
1039
+ if (slashIdx > -1) {
1040
+ const dirName = rel.slice(0, slashIdx)
1041
+ if (!seenDirs.has(dirName)) {
1042
+ seenDirs.add(dirName)
1043
+ const dirPath = prefix + dirName + "/"
1044
+ const count = files.filter(ff => ff.path.startsWith(dirPath)).length
1045
+ entries.push({ type: "dir", name: dirName, path: dirPath, count })
1046
+ }
1047
+ } else {
1048
+ entries.push({ type: "file", name: rel, file: f })
1049
+ }
1050
+ }
1051
+ // Sort: dirs first, then files, alphabetical within each
1052
+ entries.sort((a, b) => {
1053
+ if (a.type !== b.type) return a.type === "dir" ? -1 : 1
1054
+ return a.name.localeCompare(b.name)
1055
+ })
1056
+ return entries
1057
+ }
1058
+
1059
+ const FolderIcon = () => (
1060
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="#54aeff">
1061
+ <path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1Z" />
1062
+ </svg>
1063
+ )
1064
+
1065
+ function renderInline(text) {
1066
+ const re = /\*\*(.+?)\*\*|`([^`]+)`|\[(.+?)\]\((.+?)\)/g
1067
+ const parts = []; let last = 0, m, k = 0
1068
+ while ((m = re.exec(text))) {
1069
+ if (m.index > last) parts.push(text.slice(last, m.index))
1070
+ if (m[1] != null) parts.push(<strong key={k++}>{m[1]}</strong>)
1071
+ else if (m[2] != null) parts.push(<code key={k++}>{m[2]}</code>)
1072
+ else if (m[3] != null) parts.push(<a key={k++} href={m[4]}>{m[3]}</a>)
1073
+ last = m.index + m[0].length
1074
+ }
1075
+ if (last < text.length) parts.push(text.slice(last))
1076
+ return parts.length === 1 && typeof parts[0] === "string" ? parts[0] : parts
1077
+ }
1078
+
1079
+ // ═══════════════════════════════════════════════════════════════
1080
+ // Icons (SVG)
1081
+ // ═══════════════════════════════════════════════════════════════
1082
+
1083
+ const MoonIcon = () => (
1084
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
1085
+ <path d="M14.53 10.53a7 7 0 0 1-9.058-9.058A7.003 7.003 0 0 0 8 15a7.002 7.002 0 0 0 6.53-4.47z"/>
1086
+ </svg>
1087
+ )
1088
+ const SunIcon = () => (
1089
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
1090
+ <circle cx="8" cy="8" r="2.5" fill="currentColor" stroke="none"/>
1091
+ <line x1="8" y1="1" x2="8" y2="3"/><line x1="8" y1="13" x2="8" y2="15"/>
1092
+ <line x1="1" y1="8" x2="3" y2="8"/><line x1="13" y1="8" x2="15" y2="8"/>
1093
+ <line x1="3.05" y1="3.05" x2="4.46" y2="4.46"/><line x1="11.54" y1="11.54" x2="12.95" y2="12.95"/>
1094
+ <line x1="12.95" y1="3.05" x2="11.54" y2="4.46"/><line x1="4.46" y1="11.54" x2="3.05" y2="12.95"/>
1095
+ </svg>
1096
+ )
1097
+ const BackIcon = () => (
1098
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
1099
+ <path d="M7.78 12.53a.75.75 0 0 1-1.06 0L2.47 8.28a.75.75 0 0 1 0-1.06l4.25-4.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L4.81 7h7.44a.75.75 0 0 1 0 1.5H4.81l2.97 2.97a.75.75 0 0 1 0 1.06Z"/>
1100
+ </svg>
1101
+ )
1102
+ const FileIcon = ({ size = 14 }) => (
1103
+ <svg className="flex-shrink-0 color-fg-muted" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1104
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
1105
+ </svg>
1106
+ )
1107
+ const ChevronIcon = ({ open }) => (
1108
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" className="flex-shrink-0 color-fg-muted" style={{ transform: open ? "rotate(180deg)" : "none", transition: "transform 0.15s" }}>
1109
+ <path d="M12.78 5.22a.749.749 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.06 0L3.22 6.28a.749.749 0 1 1 1.06-1.06L8 8.939l3.72-3.719a.749.749 0 0 1 1.06 0Z"/>
1110
+ </svg>
1111
+ )
1112
+ const CopyIcon = () => (
1113
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
1114
+ <path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/>
1115
+ <path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/>
1116
+ </svg>
1117
+ )
1118
+ const CheckIcon = () => (
1119
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="var(--color-success-fg, #1a7f37)">
1120
+ <path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/>
1121
+ </svg>
1122
+ )
1123
+
1124
+ // ═══════════════════════════════════════════════════════════════
1125
+ // Highlighted code block
1126
+ // ═══════════════════════════════════════════════════════════════
1127
+
1128
+ function CodeBlock({ code, lang }) {
1129
+ const [copied, setCopied] = useState(false)
1130
+ let html = escapeHtml(code)
1131
+ if (window.hljs && lang) {
1132
+ try { html = window.hljs.highlight(code, { language: lang }).value } catch {}
1133
+ }
1134
+ const handleCopy = () => {
1135
+ navigator.clipboard.writeText(code).then(() => {
1136
+ setCopied(true)
1137
+ setTimeout(() => setCopied(false), 2000)
1138
+ }).catch(() => {})
1139
+ }
1140
+ return (
1141
+ <div className="code-block-wrap">
1142
+ <button className="code-copy-btn" type="button" onClick={handleCopy} aria-label="Copy code">
1143
+ {copied ? <CheckIcon /> : <CopyIcon />}
1144
+ </button>
1145
+ <pre><code className={lang ? `language-${lang} hljs` : ""} dangerouslySetInnerHTML={{ __html: html }} /></pre>
1146
+ </div>
1147
+ )
1148
+ }
1149
+
1150
+ // ═══════════════════════════════════════════════════════════════
1151
+ // Layout components
1152
+ // ═══════════════════════════════════════════════════════════════
1153
+
1154
+ function Header({ dark, setDark, connected }) {
1155
+ return (
1156
+ <header className="Header" style={{ paddingLeft: 0, paddingRight: 0 }}>
1157
+ <div className="d-flex flex-items-center width-full px-4" style={{ maxWidth: 1012, margin: "0 auto", gap: 0 }}>
1158
+ <div className="Header-item mr-0">
1159
+ <img src="/favicon.png" alt="WAO" width="24" height="24" className="logo-invert" style={{ marginRight: 10 }} />
1160
+ <span className="Header-link f4 text-bold" style={{ color: "inherit", cursor: "default" }}>HyperADD</span>
1161
+ <span className="color-fg-muted d-none d-md-inline" style={{ marginLeft: 4, fontSize: 14 }}>/</span>
1162
+ <span className="color-fg-muted d-none d-md-inline" style={{ marginLeft: 4, fontSize: 14 }}>Agent Driven Development for AO &amp; HyperBEAM</span>
1163
+ </div>
1164
+ <div className="Header-item Header-item--full" />
1165
+ <div className="Header-item">
1166
+ <span className={`d-flex flex-items-center f6 ${connected ? "color-fg-success" : "color-fg-muted"}`} style={{ gap: 6 }}>
1167
+ <span className="d-inline-block" style={{ width: 8, height: 8, borderRadius: "50%", backgroundColor: "currentColor" }} />
1168
+ {connected ? "live" : "offline"}
1169
+ </span>
1170
+ </div>
1171
+ <div className="Header-item mr-0">
1172
+ <button className="dark-toggle" type="button" onClick={() => setDark(!dark)} aria-label="Toggle dark mode">
1173
+ {dark ? <SunIcon /> : <MoonIcon />}
1174
+ </button>
1175
+ </div>
1176
+ </div>
1177
+ </header>
1178
+ )
1179
+ }
1180
+
1181
+ // GitHub repo icon (octicon-repo)
1182
+ function RepoIcon() {
1183
+ return (
1184
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style={{ flexShrink: 0 }}>
1185
+ <path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z" />
1186
+ </svg>
1187
+ )
1188
+ }
1189
+
1190
+ function ProgressSection({ data }) {
1191
+ const tasks = data?.tasks || []
1192
+ const done = tasks.filter(t => t.status === "done").length
1193
+ const total = tasks.length
1194
+ const pct = total > 0 ? Math.round((done / total) * 100) : 0
1195
+ const current = tasks.find(t => t.status === "in_progress")
1196
+ const remaining = total - done
1197
+
1198
+ if (!data?.feature) {
1199
+ return <div className="px-4 pt-4"><p className="color-fg-muted m-0">No build in progress. Run /build to start.</p></div>
1200
+ }
1201
+
1202
+ return (
1203
+ <div className="px-4 pt-4">
1204
+ <div className="d-flex mb-2" style={{ gap: 8, alignItems: "center" }}>
1205
+ <span className="color-fg-muted d-flex" style={{ alignItems: "center" }}><RepoIcon /></span>
1206
+ <span className="f3 text-bold color-fg-default" style={{ lineHeight: 1 }}>{data.feature}</span>
1207
+ <span className={`Label ${done === total ? "Label--success" : "Label--attention"}`} style={{ fontSize: 12, fontWeight: 500 }}>{done === total ? "complete" : "building"}</span>
1208
+ </div>
1209
+ <div className="d-flex flex-items-center" style={{ gap: 8 }}>
1210
+ <span className="f6 color-fg-muted no-wrap">{done} / {total}</span>
1211
+ <div className="d-flex flex-1" style={{ gap: 2, height: 8 }}>
1212
+ {tasks.map(t => (
1213
+ <div key={t.id} style={{
1214
+ flex: 1,
1215
+ borderRadius: 2,
1216
+ backgroundColor: t.status === "done" ? "#8250df" : t.status === "in_progress" ? "#bf8700" : "#d0d7de",
1217
+ }} />
1218
+ ))}
1219
+ </div>
1220
+ <span className="f6 color-fg-muted no-wrap">{pct}%</span>
1221
+ </div>
1222
+ {current && (
1223
+ <p className="f6 color-fg-muted m-0 mt-2">
1224
+ <span className="anim-spin d-inline-block mr-1">&#x21bb;</span>
1225
+ {current.name} &middot; {done} done, {remaining} remaining
1226
+ </p>
1227
+ )}
1228
+ {!current && total > 0 && (
1229
+ <p className="f6 color-fg-muted m-0 mt-2">
1230
+ {done === total ? `All ${total} tasks complete` : `${done} done, ${remaining} remaining`}
1231
+ </p>
1232
+ )}
1233
+ </div>
1234
+ )
1235
+ }
1236
+
1237
+ const TRACK_LABEL = {
1238
+ Done: "Label Label--success",
1239
+ "In Progress": "Label Label--attention",
1240
+ Pending: "Label Label--secondary",
1241
+ }
1242
+ const TRACK_COLORS = {
1243
+ Done: { light: { bg: "#dafbe1", border: "rgba(26,127,55,0.4)" }, dark: { bg: "rgba(35,134,54,0.15)", border: "rgba(46,160,67,0.4)" } },
1244
+ "In Progress": { light: { bg: "#fff8c5", border: "rgba(154,103,0,0.4)" }, dark: { bg: "rgba(187,128,9,0.15)", border: "rgba(187,128,9,0.4)" } },
1245
+ Pending: { light: { bg: "transparent", border: "#d0d7de" }, dark: { bg: "transparent", border: "#30363d" } },
1246
+ }
1247
+
1248
+ function TrackCards({ data, dark }) {
1249
+ const cards = deriveTrackCards(data?.tasks || [])
1250
+ if (!cards.length) return null
1251
+ const mode = dark ? "dark" : "light"
1252
+ return (
1253
+ <div className="d-flex flex-wrap px-4 pt-3" style={{ gap: 8 }}>
1254
+ {cards.map(c => {
1255
+ const colors = TRACK_COLORS[c.status]?.[mode] || TRACK_COLORS.Pending[mode]
1256
+ return (
1257
+ <div key={c.name} className="d-flex flex-items-center flex-justify-between rounded-2 p-3" style={{ flex: "1 1 0%", minWidth: 140, border: `1px solid ${colors.border}`, background: colors.bg }}>
1258
+ <span className="f5 text-bold">{c.name}</span>
1259
+ <span className={TRACK_LABEL[c.status]}>{c.status}</span>
1260
+ </div>
1261
+ )
1262
+ })}
1263
+ </div>
1264
+ )
1265
+ }
1266
+
1267
+ function TabBar({ active, setActive, taskCount }) {
1268
+ const tabs = [
1269
+ { id: "tasks", label: "Tasks", counter: taskCount, icon: <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" /><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z" /></svg> },
1270
+ { id: "tests", label: "Tests", icon: <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M5.75 7.5a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5a.75.75 0 0 1 .75-.75Zm5.25.75a.75.75 0 0 0-1.5 0v1.5a.75.75 0 0 0 1.5 0ZM8 7.5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 7.5Z" /><path d="M4.25 1h2.5a.75.75 0 0 1 0 1.5h-.19l1.658 3.316A5.508 5.508 0 0 1 13.5 11.25a5.5 5.5 0 1 1-11 0 5.508 5.508 0 0 1 5.282-5.434L9.44 2.5h-.19a.75.75 0 0 1 0-1.5h2.5a.75.75 0 0 1 0 1.5h-.19L9.623 5.87A5.45 5.45 0 0 1 12 11.25a4 4 0 1 0-8 0 5.45 5.45 0 0 1 2.377-5.38L4.44 2.5h-.19a.75.75 0 0 1 0-1.5ZM8 7.25a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z" /></svg> },
1271
+ { id: "plan", label: "Plan", icon: <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Z" /></svg> },
1272
+ { id: "code", label: "Code", icon: <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="m11.28 3.22 4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L13.94 8l-3.72-3.72a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215Zm-6.56 0a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L2.06 8l3.72 3.72a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L.47 8.53a.75.75 0 0 1 0-1.06Z" /></svg> },
1273
+ { id: "readme", label: "README", icon: <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.743 3.743 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75Zm7.251 10.324.004-5.073-.002-2.253A2.25 2.25 0 0 0 5.003 2.5H1.5v9h3.757a3.75 3.75 0 0 1 1.994.574ZM8.755 4.75l-.004 7.322a3.752 3.752 0 0 1 1.992-.572H14.5v-9h-3.495a2.25 2.25 0 0 0-2.25 2.25Z" /></svg> },
1274
+ { id: "commands", label: "Commands", icon: <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25Zm7.25 8a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5h-1.5a.75.75 0 0 1-.75-.75Zm-7.25-6a.75.75 0 0 1 .75-.75h2a.75.75 0 0 1 0 1.5h-2A.75.75 0 0 1 1.75 4.5ZM4.22 6.22a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L5.94 8.5 4.22 6.78a.75.75 0 0 1 0-1.06Z" /></svg> },
1275
+ { id: "skills", label: "Skills", icon: <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M7.998 14.5c2.832 0 5-1.98 5-4.5 0-1.463-.68-2.19-1.879-3.383l-.036-.037c-1.013-1.008-2.3-2.29-2.834-4.434-.322.256-.63.579-.864.953-.432.696-.621 1.58-.046 2.73.473.947.67 2.284-.278 3.232-.61.61-1.545.84-2.403.508a2.18 2.18 0 0 1-.727-.467 2.2 2.2 0 0 1-.61-1.15c-.208.208-.401.397-.571.566C1.558 9.882 1 10.56 1 11.994 1 14.476 3.842 16 7.999 16c4.157 0 6.999-1.524 6.999-4.006 0-1.434-.558-2.112-1.749-3.298l-.037-.036c-1.175-1.168-2.665-2.646-3.28-5.348a.96.96 0 0 0-.109-.218.756.756 0 0 0-1.283.065c-.571.922-1.378 2.016-2.498 2.772-.484.326-1.037.504-1.592.504-.345 0-.69-.072-1.016-.22a2.52 2.52 0 0 1-.657-.428l-.122-.126c-.276.28-.543.574-.786.894A6.11 6.11 0 0 0 1 11.994c0 2.483 2.842 4.006 6.999 4.006Z" /><path d="M3.635 10.326c.199.63.758 1.024 1.363 1.024.474 0 .943-.227 1.255-.611a.755.755 0 0 0-.008-1.003c-.327-.375-.327-.94 0-1.316a2.078 2.078 0 0 0 .417-1.987c-1.08.591-1.833 1.376-2.484 2.253a5.38 5.38 0 0 0-.543 1.64Z" /></svg> },
1276
+ { id: "deploy", label: "Deploy", icon: <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01a.753.753 0 0 1-.07.063 3.04 3.04 0 0 1-.39.276 3.555 3.555 0 0 1-1.862.497c-.786 0-1.4-.227-1.862-.497a3.04 3.04 0 0 1-.39-.276.749.749 0 0 1-.07-.063l-.01-.01-.006-.005-.004-.004-.004-.004a.75.75 0 0 1-.154-.838L13.823 4.5h-.427a1.681 1.681 0 0 1-.497-.078l-1.29-.736a.164.164 0 0 0-.072-.019H8.75v8.172a2.332 2.332 0 0 1 1.422 1.161H11.5a.75.75 0 0 1 0 1.5h-1.328a2.343 2.343 0 0 1-4.344 0H4.5a.75.75 0 0 1 0-1.5h1.328A2.332 2.332 0 0 1 7.25 11.84V3.667h-.985a.164.164 0 0 0-.072.019l-1.29.736a1.68 1.68 0 0 1-.497.078h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01a.756.756 0 0 1-.07.063 3.04 3.04 0 0 1-.39.276 3.555 3.555 0 0 1-1.862.497c-.786 0-1.4-.227-1.862-.497a3.04 3.04 0 0 1-.39-.276.749.749 0 0 1-.07-.063l-.01-.01-.006-.005-.004-.004-.004-.004a.75.75 0 0 1-.154-.838L4.823 4.5H2.25a.75.75 0 0 1 0-1.5h2.234c.044 0 .086-.011.124-.033l1.29-.736A1.68 1.68 0 0 1 6.765 2H7.25V.75a.75.75 0 0 1 1.5 0Zm-4.384 8.892h.68l-.34-.754Zm6.588 0h.68l-.34-.754ZM8 13.5a.843.843 0 1 0 0-1.686.843.843 0 0 0 0 1.686Z" /></svg> },
1277
+ ]
1278
+ return (
1279
+ <nav className="UnderlineNav mx-4 mt-3">
1280
+ <div className="UnderlineNav-body">
1281
+ {tabs.map(t => (
1282
+ <button key={t.id} className="UnderlineNav-item" role="tab" type="button" aria-selected={active === t.id} onClick={() => setActive(t.id)}>
1283
+ <span className="d-flex flex-items-center" style={{ gap: 6 }}>
1284
+ <span className="d-flex color-fg-muted">{t.icon}</span>
1285
+ {t.label}
1286
+ {t.counter != null && <span className="Counter">{t.counter}</span>}
1287
+ </span>
1288
+ </button>
1289
+ ))}
1290
+ </div>
1291
+ </nav>
1292
+ )
1293
+ }
1294
+
1295
+ // ═══════════════════════════════════════════════════════════════
1296
+ // Tab: Tasks (GitHub Issues-style)
1297
+ // ═══════════════════════════════════════════════════════════════
1298
+
1299
+ // GitHub-style issue state icons
1300
+ const IssueOpenIcon = () => (
1301
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="#1a7f37">
1302
+ <path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"/>
1303
+ <path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"/>
1304
+ </svg>
1305
+ )
1306
+ const IssueClosedIcon = () => (
1307
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="#8250df">
1308
+ <path d="M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l3.5-3.5Z"/>
1309
+ <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z"/>
1310
+ </svg>
1311
+ )
1312
+ const IssueInProgressIcon = () => (
1313
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="#bf8700">
1314
+ <path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"/>
1315
+ <path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"/>
1316
+ </svg>
1317
+ )
1318
+
1319
+ function TaskRow({ t, isOpen, onToggle }) {
1320
+ const isDone = t.status === "done"
1321
+ const isActive = t.status === "in_progress"
1322
+ const color = TYPE_COLORS[t.type] || "#6b7280"
1323
+ const elapsed = useElapsed(isActive ? t.started_at : null)
1324
+ const duration = isDone && t.started_at && t.completed_at
1325
+ ? formatDuration(new Date(t.completed_at).getTime() - new Date(t.started_at).getTime())
1326
+ : null
1327
+
1328
+ const timeAgo = t.completed_at
1329
+ ? `completed ${formatDuration(Date.now() - new Date(t.completed_at).getTime())} ago`
1330
+ : t.started_at
1331
+ ? `started ${formatDuration(Date.now() - new Date(t.started_at).getTime())} ago`
1332
+ : null
1333
+
1334
+ return (
1335
+ <>
1336
+ <div className="Box-row d-flex flex-items-start px-3 py-2" style={{ gap: 8, cursor: "pointer" }} onClick={onToggle}>
1337
+ <div className="flex-shrink-0" style={{ paddingTop: 3 }}>
1338
+ {isDone && <IssueClosedIcon />}
1339
+ {isActive && <IssueInProgressIcon />}
1340
+ {!isDone && !isActive && <IssueOpenIcon />}
1341
+ </div>
1342
+ <div className="flex-1" style={{ minWidth: 0 }}>
1343
+ <div className="d-flex flex-items-center flex-wrap" style={{ gap: 6 }}>
1344
+ <a className={`f4 text-bold ${isDone ? "color-fg-muted" : "color-fg-default"}`} style={{ cursor: "pointer", textDecoration: "none" }}>
1345
+ {t.name}
1346
+ </a>
1347
+ <span className="Label" style={{ color, borderColor: color + "44", backgroundColor: color + "18", fontSize: 12, verticalAlign: "middle" }}>{t.type}</span>
1348
+ </div>
1349
+ <div className="f6 color-fg-muted" style={{ marginTop: 4 }}>
1350
+ #{t.id}
1351
+ {t.skill && <> &middot; <code style={{ fontSize: 11 }}>{t.skill}</code></>}
1352
+ {isActive && elapsed && <> &middot; <span className="color-fg-attention"><span className="anim-spin d-inline-block mr-1" style={{ fontSize: 10 }}>&#x21bb;</span>{elapsed}</span></>}
1353
+ {duration && <> &middot; took {duration}</>}
1354
+ {timeAgo && <> &middot; {timeAgo}</>}
1355
+ </div>
1356
+ </div>
1357
+ <ChevronIcon open={isOpen} />
1358
+ </div>
1359
+ {isOpen && (
1360
+ <div className="color-bg-subtle border-bottom" style={{ padding: "12px 16px 12px 40px" }}>
1361
+ <table className="f6" style={{ lineHeight: "22px", wordBreak: "break-word" }}>
1362
+ <tbody>
1363
+ <tr>
1364
+ <td className="color-fg-muted pr-3 no-wrap v-align-top">Status</td>
1365
+ <td><span className={`Label ${isDone ? "Label--success" : isActive ? "Label--attention" : "Label--secondary"}`}>{t.status.replace("_", " ")}</span></td>
1366
+ </tr>
1367
+ {t.done_when && <tr>
1368
+ <td className="color-fg-muted pr-3 no-wrap v-align-top">Done when</td>
1369
+ <td>{t.done_when}</td>
1370
+ </tr>}
1371
+ {t.files?.length > 0 && <tr>
1372
+ <td className="color-fg-muted pr-3 no-wrap v-align-top">Files</td>
1373
+ <td>{t.files.map(f => <code key={f} className="f6 mr-1">{f}</code>)}</td>
1374
+ </tr>}
1375
+ {t.started_at && <tr>
1376
+ <td className="color-fg-muted pr-3 no-wrap v-align-top">Started</td>
1377
+ <td>{new Date(t.started_at).toLocaleTimeString()}</td>
1378
+ </tr>}
1379
+ {t.completed_at && <tr>
1380
+ <td className="color-fg-muted pr-3 no-wrap v-align-top">Completed</td>
1381
+ <td>{new Date(t.completed_at).toLocaleTimeString()} ({duration})</td>
1382
+ </tr>}
1383
+ {isActive && elapsed && <tr>
1384
+ <td className="color-fg-muted pr-3 no-wrap v-align-top">Elapsed</td>
1385
+ <td className="color-fg-attention text-bold">{elapsed}</td>
1386
+ </tr>}
1387
+ </tbody>
1388
+ </table>
1389
+ </div>
1390
+ )}
1391
+ </>
1392
+ )
1393
+ }
1394
+
1395
+ // Group tasks by track (like GitHub Projects sections)
1396
+ const TASK_GROUPS = [
1397
+ { key: "plan", label: "Planning", match: t => t.type === "plan" },
1398
+ { key: "aos", label: "AOS", match: t => t.type.startsWith("aos") },
1399
+ { key: "device", label: "Device", match: t => t.type.startsWith("device") },
1400
+ { key: "frontend", label: "Frontend", match: t => t.type.startsWith("frontend") },
1401
+ { key: "module", label: "Custom Module", match: t => t.type.startsWith("module") },
1402
+ { key: "validation", label: "Validation", match: t => ["readme", "validate", "deploy"].includes(t.type) },
1403
+ ]
1404
+
1405
+ function groupTasks(tasks) {
1406
+ const groups = []
1407
+ const used = new Set()
1408
+ for (const g of TASK_GROUPS) {
1409
+ const items = tasks.filter((t, i) => { if (used.has(i)) return false; if (g.match(t)) { used.add(i); return true } return false })
1410
+ if (items.length) groups.push({ ...g, tasks: items })
1411
+ }
1412
+ // Catch-all for any unmatched
1413
+ const rest = tasks.filter((_, i) => !used.has(i))
1414
+ if (rest.length) groups.push({ key: "other", label: "Other", tasks: rest })
1415
+ return groups
1416
+ }
1417
+
1418
+ function TaskGroupHeader({ label, count, done, collapsed, onToggle }) {
1419
+ return (
1420
+ <div
1421
+ className="d-flex flex-items-center color-bg-subtle px-3 py-2 border-bottom"
1422
+ style={{ cursor: "pointer", userSelect: "none", gap: 8 }}
1423
+ onClick={onToggle}
1424
+ >
1425
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" className="color-fg-muted" style={{ transform: collapsed ? "rotate(-90deg)" : "rotate(0deg)", transition: "transform 0.15s" }}>
1426
+ <path d="M12.78 5.22a.749.749 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.06 0L3.22 6.28a.749.749 0 1 1 1.06-1.06L8 8.939l3.72-3.719a.749.749 0 0 1 1.06 0Z" />
1427
+ </svg>
1428
+ <span className="text-bold f5">{label}</span>
1429
+ <span className="Counter">{count}</span>
1430
+ <span className="f6 color-fg-muted">{done === count ? "Complete" : `${done} / ${count}`}</span>
1431
+ </div>
1432
+ )
1433
+ }
1434
+
1435
+ function TasksTab({ data }) {
1436
+ const [expanded, setExpanded] = useState(null)
1437
+ const [collapsed, setCollapsed] = useState({})
1438
+ const tasks = data?.tasks || []
1439
+ if (!tasks.length) return <p className="p-4 color-fg-muted">No tasks yet. Run /build to create a task list.</p>
1440
+
1441
+ const groups = groupTasks(tasks)
1442
+
1443
+ return (
1444
+ <div className="Box mx-4 mt-3 mb-4">
1445
+ {groups.map(g => {
1446
+ const done = g.tasks.filter(t => t.status === "done").length
1447
+ const isCollapsed = collapsed[g.key]
1448
+ return (
1449
+ <div key={g.key}>
1450
+ <TaskGroupHeader
1451
+ label={g.label}
1452
+ count={g.tasks.length}
1453
+ done={done}
1454
+ collapsed={isCollapsed}
1455
+ onToggle={() => setCollapsed(prev => ({ ...prev, [g.key]: !prev[g.key] }))}
1456
+ />
1457
+ {!isCollapsed && g.tasks.map(t => (
1458
+ <TaskRow key={t.id} t={t} isOpen={expanded === t.id} onToggle={() => setExpanded(expanded === t.id ? null : t.id)} />
1459
+ ))}
1460
+ </div>
1461
+ )
1462
+ })}
1463
+ </div>
1464
+ )
1465
+ }
1466
+
1467
+ // ═══════════════════════════════════════════════════════════════
1468
+ // Tab: Tests
1469
+ // ═══════════════════════════════════════════════════════════════
1470
+
1471
+ const DEMO_TESTS = [
1472
+ {
1473
+ file: "test/aos.test.js",
1474
+ group: "AOS",
1475
+ tests: [
1476
+ { name: "should deploy AOS process", status: "pass", duration: 742 },
1477
+ { name: "should increment counter", status: "pass", duration: 118 },
1478
+ { name: "should return count via dry-run", status: "pass", duration: 85 },
60
1479
  ],
61
1480
  },
62
1481
  {
63
- name: "Custom Modules",
64
- description: "WASM64 (Rust) or standalone Lua execution modules",
65
- deployments: [
66
- {
67
- target: "Local HyperBEAM (WASM64)",
68
- command: "yarn test test/module.test.js",
69
- code: `import { HyperBEAM } from "wao/test"
70
- const hbeam = await new HyperBEAM({ reset: true }).ready()
71
- const hb = hbeam.hb
72
- const imageId = await hb.cacheBinary(wasmBytes, "application/wasm")
73
- const { pid } = await hb.spawnAOS({ image: imageId })
74
- await hb.scheduleAOS({ pid, action: "Inc" })
75
- const result = await hb.computeAOS({ pid, slot: 0 })`,
76
- notes: "Cache WASM binary on local node, spawn process, test via HTTP.",
77
- },
78
- {
79
- target: "Local HyperBEAM (Lua)",
80
- command: "yarn test test/module.test.js",
81
- code: `import { HyperBEAM } from "wao/test"
82
- const hbeam = await new HyperBEAM({ reset: true }).ready()
83
- const hb = hbeam.hb
84
- const moduleId = await hb.cacheScript(luaSrc, "application/lua")
85
- const { pid } = await hb.spawn({
86
- "execution-device": "lua@5.3a",
87
- module: moduleId
88
- })
89
- await hb.scheduleLua({ pid, action: "Inc" })
90
- const result = await hb.computeLua({ pid, slot: 0 })`,
91
- notes: "Cache Lua script on local node, spawn with lua@5.3a device.",
92
- },
93
- {
94
- target: "Remote HyperBEAM (WASM64)",
95
- command: null,
96
- code: `import { AR, HB } from "wao"
97
- // 1. Upload WASM to Arweave (free for < 100KB)
98
- const ar = await new AR().init(jwk)
99
- const { id: imageId } = await ar.post({
100
- data: wasmBytes,
101
- tags: { "Content-Type": "application/wasm" }
102
- })
103
- // 2. Spawn on remote node
104
- const hb = await new HB({
105
- url: "https://push-1.forward.computer"
106
- }).init(jwk)
107
- const { pid } = await hb.spawnAOS({ image: imageId })`,
108
- notes: "Upload WASM to Arweave first, then spawn with image ID on remote node.",
109
- },
110
- {
111
- target: "Remote HyperBEAM (Lua)",
112
- command: null,
113
- code: `import { AR, HB } from "wao"
114
- // 1. Upload Lua to Arweave (free for < 100KB)
115
- const ar = await new AR().init(jwk)
116
- const { id: moduleId } = await ar.post({
117
- data: luaSrc,
118
- tags: { "Content-Type": "application/lua" }
119
- })
120
- // 2. Spawn on remote node
121
- const hb = await new HB({
122
- url: "https://push-1.forward.computer"
123
- }).init(jwk)
124
- const { pid } = await hb.spawn({
125
- "execution-device": "lua@5.3a",
126
- module: moduleId
127
- })`,
128
- notes: "Upload Lua to Arweave first, then spawn with module ID on remote node.",
129
- },
1482
+ file: "test/token.test.js",
1483
+ group: "AOS",
1484
+ tests: [
1485
+ { name: "should mint initial supply", status: "pass", duration: 856 },
1486
+ { name: "should transfer tokens between accounts", status: "pass", duration: 234 },
1487
+ { name: "should reject transfer exceeding balance", status: "pass", duration: 112 },
1488
+ { name: "should return correct balance", status: "pass", duration: 91 },
1489
+ ],
1490
+ },
1491
+ {
1492
+ file: "test/registry.test.js",
1493
+ group: "AOS",
1494
+ tests: [
1495
+ { name: "should register token", status: "pass", duration: 678 },
1496
+ { name: "should list registered tokens", status: "pass", duration: 145 },
1497
+ { name: "should reject duplicate registration", status: "pass", duration: 98 },
1498
+ { name: "should deregister token", status: "pass", duration: 134 },
1499
+ { name: "should query token metadata", status: "pass", duration: 107 },
1500
+ ],
1501
+ },
1502
+ {
1503
+ file: "HyperBEAM/src/dev_token.erl",
1504
+ group: "Device",
1505
+ tests: [
1506
+ { name: "mint_test", status: "pass", duration: 12 },
1507
+ { name: "transfer_test", status: "pass", duration: 15 },
1508
+ { name: "insufficient_balance_test", status: "pass", duration: 8 },
1509
+ ],
1510
+ },
1511
+ {
1512
+ file: "test/hyperbeam-token.test.js",
1513
+ group: "Device",
1514
+ tests: [
1515
+ { name: "should deploy token on HyperBEAM", status: "pass", duration: 2341 },
1516
+ { name: "should mint via HyperBEAM", status: "pass", duration: 1456 },
1517
+ { name: "should transfer via HyperBEAM", status: "pass", duration: 1289 },
1518
+ ],
1519
+ },
1520
+ {
1521
+ file: "test/token-device.test.js",
1522
+ group: "Device",
1523
+ tests: [
1524
+ { name: "should call device via HTTP", status: "pass", duration: 1823 },
1525
+ { name: "should return device state", status: "pass", duration: 945 },
1526
+ ],
1527
+ },
1528
+ {
1529
+ file: "test/hyperbeam.test.js",
1530
+ group: "Device",
1531
+ tests: [
1532
+ { name: "should start HyperBEAM node", status: "pass", duration: 3210 },
1533
+ { name: "should deploy AOS on HyperBEAM", status: "pass", duration: 1876 },
1534
+ { name: "should send message via slot", status: "pass", duration: 567 },
1535
+ { name: "should read state via dry-run", status: "pass", duration: 412 },
1536
+ ],
1537
+ },
1538
+ {
1539
+ file: "frontend/src/__tests__/App.test.jsx",
1540
+ group: "Frontend",
1541
+ tests: [
1542
+ { name: "renders app without crashing", status: "pass", duration: 45 },
1543
+ { name: "shows wallet connect button", status: "pass", duration: 32 },
1544
+ { name: "displays token info after connect", status: "pass", duration: 67 },
130
1545
  ],
131
1546
  },
132
1547
  {
133
- name: "Devices",
134
- description: "Erlang device modules for HyperBEAM",
135
- deployments: [
136
- {
137
- target: "Local HyperBEAM",
138
- command: "cd HyperBEAM && rebar3 as genesis_wasm compile",
139
- code: `// 1. Compile the device
140
- // cd HyperBEAM && rebar3 as genesis_wasm compile
141
-
142
- // 2. Register in HyperBEAM/src/hb_opts.erl preloaded_devices
143
- // #{<<"name">> => <<"mydevice@1.0">>, <<"module">> => dev_mydevice}
144
-
145
- // 3. Test via WAO SDK
146
- import { HyperBEAM } from "wao/test"
147
- const hbeam = await new HyperBEAM({ reset: true }).ready()
148
- const hb = hbeam.hb
149
- const { out } = await hb.g("/~mydevice@1.0/compute", { action: "get" })`,
150
- notes: "Compile with rebar3, register in preloaded_devices, start local node.",
151
- },
152
- {
153
- target: "Remote HyperBEAM",
154
- command: null,
155
- code: `// 1. Compile the device
156
- // cd HyperBEAM && rebar3 as genesis_wasm compile
157
-
158
- // 2. Deploy compiled .beam files to production HyperBEAM node
159
- // Copy _build/genesis_wasm/lib/hb/ebin/dev_*.beam to remote node
160
-
161
- // 3. Register device in remote node's preloaded_devices
162
- // #{<<"name">> => <<"mydevice@1.0">>, <<"module">> => dev_mydevice}
163
-
164
- // 4. Connect via WAO SDK
165
- import { HB } from "wao"
166
- const hb = await new HB({
167
- url: "https://push-1.forward.computer"
168
- }).init(jwk)
169
- const { out } = await hb.g("/~mydevice@1.0/compute", { action: "get" })`,
170
- notes: "Deploy .beam files to production node and register in preloaded_devices.",
171
- },
1548
+ file: "frontend/src/__tests__/TransferForm.test.jsx",
1549
+ group: "Frontend",
1550
+ tests: [
1551
+ { name: "renders transfer form", status: "pass", duration: 28 },
1552
+ { name: "validates recipient address", status: "pass", duration: 19 },
1553
+ { name: "validates amount field", status: "pass", duration: 21 },
1554
+ { name: "submits transfer", status: "pass", duration: 156 },
1555
+ { name: "shows error on failed transfer", status: "fail", duration: 203 },
172
1556
  ],
173
1557
  },
1558
+ {
1559
+ file: "frontend/e2e/token-transfer.spec.js",
1560
+ group: "Frontend",
1561
+ tests: [
1562
+ { name: "connects wallet and loads balance", status: "in_progress", duration: null },
1563
+ { name: "sends token transfer end-to-end", status: "pending", duration: null },
1564
+ { name: "shows transaction confirmation", status: "pending", duration: null },
1565
+ ],
1566
+ },
1567
+ ]
1568
+
1569
+ const TEST_GROUPS = [
1570
+ { key: "aos", label: "AOS", match: f => f.group === "AOS" },
1571
+ { key: "device", label: "Device", match: f => f.group === "Device" },
1572
+ { key: "frontend", label: "Frontend", match: f => f.group === "Frontend" },
174
1573
  ]
175
1574
 
176
- function DeployGuide() {
177
- const [activeTrack, setActiveTrack] = useState(0)
178
- const [activeDeploy, setActiveDeploy] = useState(0)
179
- const track = TRACKS[activeTrack]
180
- const deploy = track.deployments[activeDeploy]
1575
+ function groupTestFiles(files) {
1576
+ const groups = []
1577
+ const used = new Set()
1578
+ for (const g of TEST_GROUPS) {
1579
+ const items = files.filter((f, i) => { if (used.has(i)) return false; if (g.match(f)) { used.add(i); return true } return false })
1580
+ if (items.length) groups.push({ ...g, files: items })
1581
+ }
1582
+ const rest = files.filter((_, i) => !used.has(i))
1583
+ if (rest.length) groups.push({ key: "other", label: "Other", files: rest })
1584
+ return groups
1585
+ }
1586
+
1587
+ function TestFileRow({ file, isOpen, onToggle }) {
1588
+ const total = file.tests.length
1589
+ const passed = file.tests.filter(t => t.status === "pass").length
1590
+ const failed = file.tests.filter(t => t.status === "fail").length
1591
+ const running = file.tests.filter(t => t.status === "in_progress").length
1592
+ const allPass = passed === total
1593
+ const totalDuration = file.tests.reduce((s, t) => s + (t.duration || 0), 0)
181
1594
 
182
1595
  return (
183
- <div style={styles.section}>
184
- <h2 style={styles.heading}>Deployment Guide</h2>
185
- <div style={styles.trackTabs}>
186
- {TRACKS.map((t, i) => (
187
- <button
188
- key={i}
189
- onClick={() => { setActiveTrack(i); setActiveDeploy(0) }}
190
- style={i === activeTrack ? { ...styles.trackTab, ...styles.trackTabActive } : styles.trackTab}
191
- >
192
- {t.name}
193
- </button>
194
- ))}
195
- </div>
196
- <p style={styles.trackDesc}>{track.description}</p>
197
- <div style={styles.tabs}>
198
- {track.deployments.map((d, i) => (
199
- <button
200
- key={i}
201
- onClick={() => setActiveDeploy(i)}
202
- style={i === activeDeploy ? { ...styles.tab, ...styles.tabActive } : styles.tab}
203
- >
204
- {d.target}
205
- </button>
206
- ))}
1596
+ <>
1597
+ <div className="Box-row d-flex flex-items-center px-3 py-2" style={{ gap: 8, cursor: "pointer" }} onClick={onToggle}>
1598
+ <div className="flex-shrink-0" style={{ width: 22, display: "flex", justifyContent: "center" }}>
1599
+ {failed > 0 ? (
1600
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="#cf222e"><path d="M2.343 13.657A8 8 0 1 1 13.658 2.343 8 8 0 0 1 2.343 13.657ZM6.03 4.97a.751.751 0 0 0-1.042.018.751.751 0 0 0-.018 1.042L6.94 8 4.97 9.97a.749.749 0 0 0 .326 1.275.749.749 0 0 0 .734-.215L8 9.06l1.97 1.97a.749.749 0 0 0 1.275-.326.749.749 0 0 0-.215-.734L9.06 8l1.97-1.97a.749.749 0 0 0-.326-1.275.749.749 0 0 0-.734.215L8 6.94Z" /></svg>
1601
+ ) : running > 0 ? (
1602
+ <span className="anim-spin d-inline-block color-fg-attention" style={{ fontSize: 14 }}>&#x21bb;</span>
1603
+ ) : allPass ? (
1604
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="#1a7f37"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm3.78-9.72a.751.751 0 0 0-.018-1.042.751.751 0 0 0-1.042-.018L6.75 9.19 5.28 7.72a.751.751 0 0 0-1.042.018.751.751 0 0 0-.018 1.042l2 2a.75.75 0 0 0 1.06 0Z" /></svg>
1605
+ ) : (
1606
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" className="color-fg-muted"><circle cx="8" cy="8" r="7" fill="none" stroke="currentColor" strokeWidth="2" /></svg>
1607
+ )}
1608
+ </div>
1609
+ <div className="flex-1" style={{ minWidth: 0 }}>
1610
+ <span className="f5 text-bold color-fg-default" style={{ fontFamily: "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace", fontSize: 13 }}>{file.file}</span>
1611
+ </div>
1612
+ <span className="f6 color-fg-muted no-wrap">{totalDuration > 0 ? formatDuration(totalDuration) : ""}</span>
1613
+ <span className={`Label ${allPass ? "Label--success" : failed > 0 ? "Label--danger" : "Label--secondary"}`} style={{ fontSize: 12 }}>{passed}/{total}</span>
1614
+ <ChevronIcon open={isOpen} />
207
1615
  </div>
208
- <div style={styles.card}>
209
- {deploy.command && (
210
- <div style={styles.row}>
211
- <span style={styles.label}>Command:</span>
212
- <code style={styles.code}>{deploy.command}</code>
213
- </div>
214
- )}
215
- <pre style={styles.pre}>{deploy.code}</pre>
216
- <p style={styles.note}>{deploy.notes}</p>
1616
+ {isOpen && (
1617
+ <div className="color-bg-subtle border-bottom" style={{ padding: "4px 0" }}>
1618
+ {file.tests.map((t, i) => (
1619
+ <div key={i} className="d-flex flex-items-center px-3" style={{ gap: 8, padding: "4px 16px 4px 48px" }}>
1620
+ <div style={{ width: 16, display: "flex", justifyContent: "center", flexShrink: 0 }}>
1621
+ {t.status === "pass" && <svg width="12" height="12" viewBox="0 0 16 16" fill="#1a7f37"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" /></svg>}
1622
+ {t.status === "fail" && <svg width="12" height="12" viewBox="0 0 16 16" fill="#cf222e"><path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z" /></svg>}
1623
+ {t.status === "in_progress" && <span className="anim-spin d-inline-block color-fg-attention" style={{ fontSize: 10 }}>&#x21bb;</span>}
1624
+ {t.status === "pending" && <span className="d-inline-block" style={{ width: 8, height: 8, borderRadius: "50%", border: "2px solid var(--color-border-default)" }} />}
1625
+ </div>
1626
+ <span className={`f6 flex-1 ${t.status === "pass" ? "color-fg-muted" : t.status === "fail" ? "color-fg-danger" : "color-fg-default"}`}>{t.name}</span>
1627
+ {t.duration != null && <span className="f6 color-fg-muted no-wrap">{t.duration}ms</span>}
1628
+ </div>
1629
+ ))}
1630
+ </div>
1631
+ )}
1632
+ </>
1633
+ )
1634
+ }
1635
+
1636
+ function TestGroupHeader({ label, count, passed, failed, collapsed, onToggle }) {
1637
+ return (
1638
+ <div
1639
+ className="d-flex flex-items-center color-bg-subtle px-3 py-2 border-bottom"
1640
+ style={{ cursor: "pointer", userSelect: "none", gap: 8 }}
1641
+ onClick={onToggle}
1642
+ >
1643
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" className="color-fg-muted" style={{ transform: collapsed ? "rotate(-90deg)" : "rotate(0deg)", transition: "transform 0.15s" }}>
1644
+ <path d="M12.78 5.22a.749.749 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.06 0L3.22 6.28a.749.749 0 1 1 1.06-1.06L8 8.939l3.72-3.719a.749.749 0 0 1 1.06 0Z" />
1645
+ </svg>
1646
+ <span className="text-bold f5">{label}</span>
1647
+ <span className="Counter">{count} tests</span>
1648
+ {failed > 0
1649
+ ? <span className="f6 color-fg-danger">{failed} failed</span>
1650
+ : <span className="f6 color-fg-muted">{passed === count ? "All passing" : `${passed} / ${count}`}</span>
1651
+ }
1652
+ </div>
1653
+ )
1654
+ }
1655
+
1656
+ function TestsTab() {
1657
+ const [testData, setTestData] = useState(DEMO_TESTS)
1658
+ const [expanded, setExpanded] = useState(null)
1659
+ const [collapsed, setCollapsed] = useState({})
1660
+
1661
+ useEffect(() => {
1662
+ fetch("/api/tests").then(r => r.json()).then(d => { if (d?.length) setTestData(d) }).catch(() => {})
1663
+ }, [])
1664
+
1665
+ const groups = groupTestFiles(testData)
1666
+ const allTests = testData.flatMap(f => f.tests)
1667
+ const totalPass = allTests.filter(t => t.status === "pass").length
1668
+ const totalFail = allTests.filter(t => t.status === "fail").length
1669
+ const totalCount = allTests.length
1670
+ const pct = totalCount > 0 ? Math.round((totalPass / totalCount) * 100) : 0
1671
+
1672
+ return (
1673
+ <div className="mx-4 mt-3 mb-4">
1674
+ <div className="d-flex flex-items-center mb-3 px-1" style={{ gap: 12 }}>
1675
+ <span className="f5 text-bold">{totalPass} / {totalCount} passing</span>
1676
+ <span className="f6 color-fg-muted">({pct}%)</span>
1677
+ {totalFail > 0 && <span className="Label Label--danger">{totalFail} failed</span>}
217
1678
  </div>
218
- <div style={styles.card}>
219
- <h3 style={styles.subheading}>Wallet Setup</h3>
220
- <pre style={styles.pre}>{"# Generate a wallet\nyarn keygen\n\n# Or use npx\nnpx wao keygen"}</pre>
221
- <p style={styles.note}>
222
- Wallet saved to .wallet.json (gitignored). Required for remote deployment.
223
- </p>
1679
+ <div className="Box">
1680
+ {groups.map(g => {
1681
+ const tests = g.files.flatMap(f => f.tests)
1682
+ const passed = tests.filter(t => t.status === "pass").length
1683
+ const failed = tests.filter(t => t.status === "fail").length
1684
+ const isCollapsed = collapsed[g.key]
1685
+ return (
1686
+ <div key={g.key}>
1687
+ <TestGroupHeader
1688
+ label={g.label}
1689
+ count={tests.length}
1690
+ passed={passed}
1691
+ failed={failed}
1692
+ collapsed={isCollapsed}
1693
+ onToggle={() => setCollapsed(prev => ({ ...prev, [g.key]: !prev[g.key] }))}
1694
+ />
1695
+ {!isCollapsed && g.files.map(f => (
1696
+ <TestFileRow key={f.file} file={f} isOpen={expanded === f.file} onToggle={() => setExpanded(expanded === f.file ? null : f.file)} />
1697
+ ))}
1698
+ </div>
1699
+ )
1700
+ })}
224
1701
  </div>
225
1702
  </div>
226
1703
  )
227
1704
  }
228
1705
 
229
- function BuildProgress() {
230
- const [tasks, setTasks] = useState(null)
231
- const [error, setError] = useState(null)
1706
+ // ═══════════════════════════════════════════════════════════════
1707
+ // Tab: Plan
1708
+ // ═══════════════════════════════════════════════════════════════
232
1709
 
233
- useEffect(() => {
234
- let interval = null
235
- let es = null
1710
+ function PlanTab() {
1711
+ const [content, setContent] = useState(null)
1712
+ const [loading, setLoading] = useState(true)
236
1713
 
237
- // Initial fetch
238
- fetch("/api/progress")
1714
+ useEffect(() => {
1715
+ fetch("/api/plan")
239
1716
  .then(r => r.json())
240
- .then(setTasks)
241
- .catch(() => setError("No build in progress"))
1717
+ .then(d => { setContent(d.content || DEMO_PLAN); setLoading(false) })
1718
+ .catch(() => { setContent(DEMO_PLAN); setLoading(false) })
1719
+ }, [])
242
1720
 
243
- // Try SSE for real-time updates
244
- try {
245
- es = new EventSource("/api/events")
246
- es.addEventListener("progress", (e) => {
247
- try {
248
- setTasks(JSON.parse(e.data))
249
- setError(null)
250
- } catch {}
251
- })
252
- es.onerror = () => {
253
- // SSE failed — fall back to polling
254
- es.close()
255
- es = null
256
- if (!interval) {
257
- interval = setInterval(() => {
258
- fetch("/api/progress")
259
- .then(r => r.json())
260
- .then(data => { setTasks(data); setError(null) })
261
- .catch(() => {})
262
- }, 3000)
263
- }
264
- }
265
- } catch {
266
- // EventSource not available — poll
267
- interval = setInterval(() => {
268
- fetch("/api/progress")
269
- .then(r => r.json())
270
- .then(data => { setTasks(data); setError(null) })
271
- .catch(() => {})
272
- }, 3000)
1721
+ if (loading) return <p className="p-4 color-fg-muted">Loading plan...</p>
1722
+ if (!content) return <p className="p-4 color-fg-muted">No plan.md found. Run /plan to create one.</p>
1723
+
1724
+ const lines = content.split("\n")
1725
+ const elements = []
1726
+ let inCode = false, codeBlock = [], codeLang = ""
1727
+
1728
+ for (let i = 0; i < lines.length; i++) {
1729
+ const line = lines[i]
1730
+ if (line.startsWith("```")) {
1731
+ if (inCode) {
1732
+ elements.push(<pre key={`c${i}`}><code className={codeLang ? `language-${codeLang}` : ""}>{codeBlock.join("\n")}</code></pre>)
1733
+ codeBlock = []; inCode = false; codeLang = ""
1734
+ } else { inCode = true; codeLang = line.slice(3).trim() }
1735
+ continue
1736
+ }
1737
+ if (inCode) { codeBlock.push(line); continue }
1738
+ if (line.startsWith("# ")) elements.push(<h1 key={i}>{renderInline(line.slice(2))}</h1>)
1739
+ else if (line.startsWith("## ")) elements.push(<h2 key={i}>{renderInline(line.slice(3))}</h2>)
1740
+ else if (line.startsWith("### ")) elements.push(<h3 key={i}>{renderInline(line.slice(4))}</h3>)
1741
+ else if (line.startsWith("- ") || line.startsWith("* ")) elements.push(<li key={i}>{renderInline(line.slice(2))}</li>)
1742
+ else if (line.trim() !== "") elements.push(<p key={i}>{renderInline(line)}</p>)
1743
+ }
1744
+
1745
+ return <div className="markdown-body markdown-compact p-4">{elements}</div>
1746
+ }
1747
+
1748
+ // ═══════════════════════════════════════════════════════════════
1749
+ // Tab: Code (GitHub-style inline file viewer)
1750
+ // ═══════════════════════════════════════════════════════════════
1751
+
1752
+ function MarkdownPreview({ content }) {
1753
+ const lines = content.split("\n")
1754
+ const elements = []
1755
+ let inCode = false, codeBlock = [], codeLang = ""
1756
+ let listItems = [], listKey = 0
1757
+
1758
+ const flushList = () => {
1759
+ if (listItems.length) {
1760
+ elements.push(<ul key={`ul${listKey}`}>{listItems}</ul>)
1761
+ listItems = []
273
1762
  }
1763
+ }
274
1764
 
275
- return () => {
276
- if (es) es.close()
277
- if (interval) clearInterval(interval)
1765
+ for (let i = 0; i < lines.length; i++) {
1766
+ const line = lines[i]
1767
+ if (line.startsWith("```")) {
1768
+ if (inCode) {
1769
+ flushList()
1770
+ const lang = codeLang || undefined
1771
+ let html = escapeHtml(codeBlock.join("\n"))
1772
+ if (window.hljs && lang) {
1773
+ try { html = window.hljs.highlight(codeBlock.join("\n"), { language: lang }).value } catch {}
1774
+ }
1775
+ elements.push(<pre key={`c${i}`}><code className={lang ? `language-${lang} hljs` : ""} dangerouslySetInnerHTML={{ __html: html }} /></pre>)
1776
+ codeBlock = []; inCode = false; codeLang = ""
1777
+ } else { inCode = true; codeLang = line.slice(3).trim() }
1778
+ continue
278
1779
  }
1780
+ if (inCode) { codeBlock.push(line); continue }
1781
+ if (line.startsWith("# ")) { flushList(); elements.push(<h1 key={i}>{renderInline(line.slice(2))}</h1>) }
1782
+ else if (line.startsWith("## ")) { flushList(); elements.push(<h2 key={i}>{renderInline(line.slice(3))}</h2>) }
1783
+ else if (line.startsWith("### ")) { flushList(); elements.push(<h3 key={i}>{renderInline(line.slice(4))}</h3>) }
1784
+ else if (line.startsWith("#### ")) { flushList(); elements.push(<h4 key={i}>{renderInline(line.slice(5))}</h4>) }
1785
+ else if (line.startsWith("- ") || line.startsWith("* ")) { listItems.push(<li key={i}>{renderInline(line.slice(2))}</li>); listKey = i }
1786
+ else if (/^\d+\.\s/.test(line)) { listItems.push(<li key={i}>{renderInline(line.replace(/^\d+\.\s/, ""))}</li>); listKey = i }
1787
+ else if (line.startsWith("> ")) { flushList(); elements.push(<blockquote key={i}><p>{renderInline(line.slice(2))}</p></blockquote>) }
1788
+ else if (line.startsWith("---") || line.startsWith("***")) { flushList(); elements.push(<hr key={i} />) }
1789
+ else if (line.trim() !== "") { flushList(); elements.push(<p key={i}>{renderInline(line)}</p>) }
1790
+ else { flushList() }
1791
+ }
1792
+ flushList()
1793
+
1794
+ return <div className="markdown-body markdown-compact p-4">{elements}</div>
1795
+ }
1796
+
1797
+ function CodeTab() {
1798
+ const [files, setFiles] = useState(null)
1799
+ const [filter, setFilter] = useState("")
1800
+ const [viewing, setViewing] = useState(null)
1801
+ const [currentDir, setCurrentDir] = useState("")
1802
+ const [fileContent, setFileContent] = useState(null)
1803
+ const [hlLines, setHlLines] = useState([])
1804
+ const [fileLoading, setFileLoading] = useState(false)
1805
+ const [viewMode, setViewMode] = useState("code")
1806
+
1807
+ useEffect(() => {
1808
+ fetch("/api/files")
1809
+ .then(r => r.json())
1810
+ .then(d => setFiles(d.files?.length ? d.files : DEMO_FILES))
1811
+ .catch(() => setFiles(DEMO_FILES))
279
1812
  }, [])
280
1813
 
281
- if (error) return null
282
- if (!tasks) return <p>Loading build status...</p>
1814
+ useEffect(() => {
1815
+ if (!viewing) { setFileContent(null); setHlLines([]); setViewMode("code"); return }
1816
+ setFileLoading(true)
1817
+ const ext = getFileExt(viewing.path)
1818
+ setViewMode(ext === "md" ? "preview" : "code")
1819
+ fetch(`/api/file?path=${encodeURIComponent(viewing.path)}`)
1820
+ .then(r => { if (!r.ok) throw new Error("not found"); return r.json() })
1821
+ .then(d => { setFileContent(d.content ?? ""); setFileLoading(false) })
1822
+ .catch(() => {
1823
+ const demo = DEMO_CONTENT[viewing.path]
1824
+ setFileContent(demo ?? `// ${viewing.path}`)
1825
+ setFileLoading(false)
1826
+ })
1827
+ }, [viewing])
283
1828
 
284
- const done = tasks.tasks?.filter(t => t.status === "done").length ?? 0
285
- const total = tasks.tasks?.length ?? 0
286
- const inProgress = tasks.tasks?.filter(t => t.status === "in_progress").length ?? 0
287
- const pct = total > 0 ? Math.round((done / total) * 100) : 0
1829
+ useEffect(() => {
1830
+ if (fileContent == null) return
1831
+ const ext = getFileExt(viewing?.path || "")
1832
+ const lang = EXT_TO_LANG[ext]
1833
+ if (window.hljs && lang) {
1834
+ try {
1835
+ const result = window.hljs.highlight(fileContent, { language: lang })
1836
+ setHlLines(result.value.split("\n"))
1837
+ } catch { setHlLines(fileContent.split("\n").map(escapeHtml)) }
1838
+ } else {
1839
+ setHlLines(fileContent.split("\n").map(escapeHtml))
1840
+ }
1841
+ }, [fileContent])
1842
+
1843
+ if (!files) return <p className="p-4 color-fg-muted">Loading files...</p>
1844
+
1845
+ // File viewer
1846
+ if (viewing) {
1847
+ const ext = getFileExt(viewing.path)
1848
+ const isMd = ext === "md"
1849
+ const badge = FILE_BADGES[ext]
1850
+ return (
1851
+ <div className="px-4 pt-3 pb-4">
1852
+ <div className="d-flex flex-items-center mb-2" style={{ gap: 12 }}>
1853
+ <button className="back-btn" type="button" onClick={() => setViewing(null)}>
1854
+ <BackIcon /> Back
1855
+ </button>
1856
+ </div>
1857
+ <div className="Box">
1858
+ <div className="Box-header d-flex flex-items-center py-2 px-3" style={{ gap: 8 }}>
1859
+ <FileIcon size={16} />
1860
+ <span className="f6 text-bold text-mono">{viewing.path}</span>
1861
+ {badge && <span className="Label" style={{ color: badge.color, borderColor: badge.color + "44", backgroundColor: badge.color + "18" }}>{badge.label}</span>}
1862
+ <span className="flex-1" />
1863
+ {isMd && (
1864
+ <div className="BtnGroup" style={{ marginRight: 8 }}>
1865
+ <button type="button" className={`BtnGroup-item btn btn-sm ${viewMode === "code" ? "selected" : ""}`} onClick={() => setViewMode("code")}>Code</button>
1866
+ <button type="button" className={`BtnGroup-item btn btn-sm ${viewMode === "preview" ? "selected" : ""}`} onClick={() => setViewMode("preview")}>Preview</button>
1867
+ </div>
1868
+ )}
1869
+ <span className="f6 color-fg-muted">{hlLines.length} lines &middot; {formatSize(viewing.size)}</span>
1870
+ </div>
1871
+ {fileLoading ? (
1872
+ <div className="p-4 color-fg-muted"><span className="anim-spin d-inline-block mr-1">&#x21bb;</span> Loading...</div>
1873
+ ) : viewMode === "preview" && isMd && fileContent != null ? (
1874
+ <MarkdownPreview content={fileContent} />
1875
+ ) : (
1876
+ <div className="code-view">
1877
+ <table>
1878
+ <tbody>
1879
+ {hlLines.map((html, i) => (
1880
+ <tr key={i}>
1881
+ <td className="ln">{i + 1}</td>
1882
+ <td className="lc" dangerouslySetInnerHTML={{ __html: html || " " }} />
1883
+ </tr>
1884
+ ))}
1885
+ </tbody>
1886
+ </table>
1887
+ </div>
1888
+ )}
1889
+ </div>
1890
+ </div>
1891
+ )
1892
+ }
1893
+
1894
+ // File tree with breadcrumbs
1895
+ const filtered = filter ? files.filter(f => f.path.toLowerCase().includes(filter.toLowerCase())) : files
1896
+ const entries = filter ? getTreeEntries(filtered, "") : getTreeEntries(files, currentDir)
1897
+
1898
+ // Breadcrumb segments
1899
+ const breadcrumbs = currentDir ? currentDir.replace(/\/$/, "").split("/") : []
288
1900
 
289
1901
  return (
290
- <div style={styles.section}>
291
- <h2 style={styles.heading}>Build Progress</h2>
292
- <p style={styles.feature}>{tasks.feature}</p>
293
- <div style={styles.progressBar}>
294
- <div style={{ ...styles.progressFill, width: `${pct}%` }} />
1902
+ <div className="px-4 pt-3 pb-4">
1903
+ <input className="form-control width-full mb-2" type="text" placeholder="Filter files..." value={filter} onChange={e => setFilter(e.target.value)} />
1904
+
1905
+ {/* Breadcrumb */}
1906
+ <div className="d-flex flex-items-center f5 mb-2" style={{ gap: 4 }}>
1907
+ <a className={`${currentDir ? "color-fg-accent" : "text-bold color-fg-default"}`} style={{ cursor: "pointer", textDecoration: "none" }} onClick={() => { setCurrentDir(""); setFilter("") }}>
1908
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" style={{ verticalAlign: "text-bottom", marginRight: 4 }}>
1909
+ <path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z" />
1910
+ </svg>
1911
+ root
1912
+ </a>
1913
+ {breadcrumbs.map((seg, idx) => {
1914
+ const path = breadcrumbs.slice(0, idx + 1).join("/") + "/"
1915
+ const isLast = idx === breadcrumbs.length - 1
1916
+ return (
1917
+ <span key={idx} className="d-flex flex-items-center" style={{ gap: 4 }}>
1918
+ <span className="color-fg-muted">/</span>
1919
+ <a className={isLast ? "text-bold color-fg-default" : "color-fg-accent"} style={{ cursor: "pointer", textDecoration: "none" }} onClick={() => setCurrentDir(path)}>{seg}</a>
1920
+ </span>
1921
+ )
1922
+ })}
1923
+ </div>
1924
+
1925
+ <div className="Box">
1926
+ {/* Go up row */}
1927
+ {currentDir && !filter && (
1928
+ <div className="Box-row file-row d-flex flex-items-center px-3" style={{ gap: 8, paddingTop: 8, paddingBottom: 8, cursor: "pointer" }}
1929
+ onClick={() => {
1930
+ const parts = currentDir.replace(/\/$/, "").split("/")
1931
+ parts.pop()
1932
+ setCurrentDir(parts.length ? parts.join("/") + "/" : "")
1933
+ }}>
1934
+ <span className="color-fg-muted">..</span>
1935
+ </div>
1936
+ )}
1937
+ {entries.map(e => {
1938
+ if (e.type === "dir") {
1939
+ return (
1940
+ <div key={e.path} className="Box-row file-row d-flex flex-items-center px-3" style={{ gap: 8, paddingTop: 8, paddingBottom: 8, cursor: "pointer" }} onClick={() => { setCurrentDir(e.path); setFilter("") }}>
1941
+ <FolderIcon />
1942
+ <span className="flex-1 f5 text-bold">{e.name}</span>
1943
+ <span className="f6 color-fg-subtle">{e.count} items</span>
1944
+ </div>
1945
+ )
1946
+ }
1947
+ const ext = getFileExt(e.name)
1948
+ const badge = FILE_BADGES[ext]
1949
+ return (
1950
+ <div key={e.file.path} className="Box-row file-row d-flex flex-items-center px-3" style={{ gap: 8, paddingTop: 8, paddingBottom: 8, cursor: "pointer" }} onClick={() => setViewing(e.file)}>
1951
+ <FileIcon />
1952
+ <span className="flex-1 f5">{e.name}</span>
1953
+ {badge && <span className="Label" style={{ color: badge.color, borderColor: badge.color + "44", backgroundColor: badge.color + "18" }}>{badge.label}</span>}
1954
+ <span className="f6 color-fg-subtle text-right" style={{ minWidth: 48 }}>{formatSize(e.file.size)}</span>
1955
+ </div>
1956
+ )
1957
+ })}
295
1958
  </div>
296
- <p style={styles.progressText}>{done}/{total} tasks done{inProgress > 0 ? `, ${inProgress} in progress` : ""}</p>
297
- {tasks.tracks && (
298
- <div style={styles.trackBadges}>
299
- {tasks.tracks.map(t => (
300
- <span key={t} style={styles.badge}>{t}</span>
301
- ))}
302
- </div>
303
- )}
304
- <table style={styles.table}>
305
- <thead>
306
- <tr>
307
- <th style={styles.th}>Status</th>
308
- <th style={styles.th}>Task</th>
309
- <th style={styles.th}>Type</th>
310
- <th style={styles.th}>Done When</th>
311
- </tr>
312
- </thead>
313
- <tbody>
314
- {tasks.tasks?.map(t => (
315
- <tr key={t.id}>
316
- <td style={styles.td}>
317
- <span style={t.status === "done" ? styles.statusDone : t.status === "in_progress" ? styles.statusProgress : styles.statusPending}>
318
- {t.status === "done" ? "\u2714" : t.status === "in_progress" ? "\u25B6" : "\u00B7"}
319
- </span>
320
- </td>
321
- <td style={styles.td}>{t.name}</td>
322
- <td style={styles.td}><span style={styles.typeBadge}>{t.type}</span></td>
323
- <td style={{ ...styles.td, color: "#888", fontSize: "0.8rem" }}>{t.done_when}</td>
324
- </tr>
325
- ))}
326
- </tbody>
327
- </table>
328
1959
  </div>
329
1960
  )
330
1961
  }
331
1962
 
332
- function CommunicationFiles() {
1963
+ // ═══════════════════════════════════════════════════════════════
1964
+ // Tab: Commands
1965
+ // ═══════════════════════════════════════════════════════════════
1966
+
1967
+ function CommandsTab({ data }) {
1968
+ const commands = deriveCommands(data)
333
1969
  return (
334
- <div style={styles.section}>
335
- <h2 style={styles.heading}>Communication Files</h2>
336
- <p style={styles.note}>Wizard agents and the dashboard coordinate through these files:</p>
337
- <table style={styles.table}>
338
- <thead>
339
- <tr>
340
- <th style={styles.th}>File</th>
341
- <th style={styles.th}>Written By</th>
342
- <th style={styles.th}>Read By</th>
343
- <th style={styles.th}>Purpose</th>
344
- </tr>
345
- </thead>
346
- <tbody>
347
- <tr>
348
- <td style={styles.td}><code style={styles.code}>plan.md</code></td>
349
- <td style={styles.td}>/plan wizard</td>
350
- <td style={styles.td}>All wizards, dashboard</td>
351
- <td style={styles.td}>Architecture, components, edge cases</td>
352
- </tr>
353
- <tr>
354
- <td style={styles.td}><code style={styles.code}>tasks.json</code></td>
355
- <td style={styles.td}>Every wizard</td>
356
- <td style={styles.td}>All wizards, dashboard, hooks</td>
357
- <td style={styles.td}>Task status, current step, completion criteria</td>
358
- </tr>
359
- <tr>
360
- <td style={styles.td}><code style={styles.code}>CLAUDE.md</code></td>
361
- <td style={styles.td}>Scaffolder</td>
362
- <td style={styles.td}>Every wizard on start</td>
363
- <td style={styles.td}>Project context, commands, constraints</td>
364
- </tr>
365
- </tbody>
366
- </table>
1970
+ <div className="markdown-body markdown-compact p-4">
1971
+ {Object.entries(commands).map(([section, cmds]) => (
1972
+ <div key={section} className="mb-4">
1973
+ <h3>{section}</h3>
1974
+ {cmds.map(c => (
1975
+ <div key={c.cmd} className="mb-2">
1976
+ <CodeBlock code={`$ ${c.cmd}`} lang="bash" />
1977
+ <p className="f6 color-fg-muted mt-1">{c.desc}</p>
1978
+ </div>
1979
+ ))}
1980
+ </div>
1981
+ ))}
367
1982
  </div>
368
1983
  )
369
1984
  }
370
1985
 
371
- export default function App() {
1986
+ // ═══════════════════════════════════════════════════════════════
1987
+ // Tab: Skills
1988
+ // ═══════════════════════════════════════════════════════════════
1989
+
1990
+ function SkillsTab() {
372
1991
  return (
373
- <div style={styles.container}>
374
- <h1 style={styles.title}>WAO Dashboard</h1>
375
- <BuildProgress />
376
- <DeployGuide />
377
- <CommunicationFiles />
1992
+ <div className="markdown-body markdown-compact p-4">
1993
+ {Object.entries(SKILLS).map(([section, skills]) => (
1994
+ <div key={section} className="mb-4">
1995
+ <h3>{section}</h3>
1996
+ <table>
1997
+ <thead><tr><th>Command</th><th>Type</th><th>Description</th></tr></thead>
1998
+ <tbody>
1999
+ {skills.map(s => {
2000
+ const c = BADGE_COLORS[s.badge] || "#6b7280"
2001
+ return (
2002
+ <tr key={s.cmd}>
2003
+ <td><code className="text-bold">{s.cmd}</code></td>
2004
+ <td><span className="skill-badge" style={{ backgroundColor: c }}>{s.badge}</span></td>
2005
+ <td>{s.desc}</td>
2006
+ </tr>
2007
+ )
2008
+ })}
2009
+ </tbody>
2010
+ </table>
2011
+ </div>
2012
+ ))}
2013
+ <div className="Box color-bg-subtle p-3 f6 color-fg-muted mt-3">
2014
+ Type any skill name in the Claude Code conversation to invoke it. Skills with arguments: <code>/build "feature name"</code>, <code>/test test/aos.test.js</code>
2015
+ </div>
378
2016
  </div>
379
2017
  )
380
2018
  }
381
2019
 
382
- const styles = {
383
- container: { padding: "2rem", fontFamily: "monospace", maxWidth: 960, margin: "0 auto", color: "#e0e0e0", background: "#111", minHeight: "100vh" },
384
- title: { margin: 0, fontSize: "1.5rem" },
385
- section: { marginTop: "2rem" },
386
- heading: { margin: "0 0 1rem 0", fontSize: "1.2rem" },
387
- subheading: { margin: "0 0 0.5rem 0", fontSize: "1rem" },
388
- trackTabs: { display: "flex", gap: "0.5rem", marginBottom: "0.5rem" },
389
- trackTab: {
390
- padding: "0.5rem 1rem", border: "1px solid #444", background: "none",
391
- color: "#aaa", cursor: "pointer", fontFamily: "monospace", fontSize: "0.9rem",
392
- borderRadius: "4px", fontWeight: "bold",
2020
+ // ═══════════════════════════════════════════════════════════════
2021
+ // Tab: Deploy
2022
+ // ═══════════════════════════════════════════════════════════════
2023
+
2024
+ const DEPLOY_STEPS = [
2025
+ {
2026
+ title: "1. Generate Wallet",
2027
+ desc: "Create an Arweave JWK wallet for signing deploys. Saved to .wallet.json (gitignored).",
2028
+ cmds: [
2029
+ { cmd: "yarn keygen", desc: "Generate .wallet.json in project root" },
2030
+ ],
2031
+ },
2032
+ {
2033
+ title: "2. Test Lua Scripts",
2034
+ desc: "Run your Lua scripts against in-memory AOS to verify correctness before deploying.",
2035
+ cmds: [
2036
+ { cmd: "yarn test", desc: "Run all unit tests (in-memory AOS)" },
2037
+ { cmd: "yarn test test/<name>.test.js", desc: "Run a specific test file" },
2038
+ ],
393
2039
  },
394
- trackTabActive: { background: "#2a2a2a", color: "#fff", borderColor: "#888" },
395
- trackDesc: { color: "#888", fontSize: "0.85rem", margin: "0 0 0.75rem 0" },
396
- tabs: { display: "flex", gap: "0.5rem", flexWrap: "wrap", marginBottom: "1rem" },
397
- tab: {
398
- padding: "0.4rem 0.8rem", border: "1px solid #555", background: "none",
399
- color: "#ccc", cursor: "pointer", fontFamily: "monospace", fontSize: "0.8rem",
400
- borderRadius: "4px",
2040
+ {
2041
+ title: "3. Deploy",
2042
+ desc: "yarn deploy runs scripts/deploy.js — it reads all src/*.lua files (or specific files you pass), spawns a process for each, and loads the Lua code. On testnet this uses Eval (sends the source as a message with Action: \"Eval\"). On HyperBEAM it uses ao.deploy() which bundles spawn + source in one call.",
2043
+ cmds: [
2044
+ { cmd: "yarn deploy", desc: "Deploy all src/*.lua to AO testnet" },
2045
+ { cmd: "yarn deploy src/token.lua", desc: "Deploy a single script to testnet" },
2046
+ { cmd: "yarn deploy --local-hb", desc: "Deploy to local HyperBEAM (genesis-wasm)" },
2047
+ { cmd: "yarn deploy --local-hb --lua", desc: "Deploy to local HyperBEAM (Lua mode — faster, no receive())" },
2048
+ { cmd: "yarn deploy --mainnet", desc: "Deploy to remote HyperBEAM (push-1.forward.computer)" },
2049
+ { cmd: "yarn deploy --mainnet --lua", desc: "Deploy to remote HyperBEAM (Lua mode)" },
2050
+ ],
401
2051
  },
402
- tabActive: { background: "#333", color: "#fff", borderColor: "#888" },
403
- card: { background: "#1a1a1a", border: "1px solid #333", borderRadius: "6px", padding: "1rem", marginBottom: "1rem" },
404
- row: { marginBottom: "0.5rem" },
405
- label: { color: "#888", marginRight: "0.5rem" },
406
- code: { background: "#2a2a2a", padding: "0.2rem 0.4rem", borderRadius: "3px", color: "#e0e0e0" },
407
- pre: {
408
- background: "#0d0d0d", padding: "1rem", borderRadius: "4px", overflow: "auto",
409
- fontSize: "0.85rem", lineHeight: 1.5, color: "#d4d4d4", margin: "0.5rem 0",
2052
+ {
2053
+ title: "4. Verify on Explorers",
2054
+ desc: "After deploying, the CLI prints each process ID. Use these explorers to inspect your processes:",
2055
+ cmds: [
2056
+ { cmd: "https://aolink.ar.io/#/entity/<PROCESS_ID>", desc: "aolink AOS process explorer (messages, state, tags)" },
2057
+ { cmd: "https://lunar.ar.io/#/process/<PROCESS_ID>", desc: "lunar — HyperBEAM explorer (slots, devices, compute logs)" },
2058
+ ],
410
2059
  },
411
- note: { color: "#999", fontSize: "0.85rem", margin: "0.5rem 0 0 0" },
412
- feature: { color: "#ccc", marginTop: 4, fontSize: "1.1rem" },
413
- progressBar: { background: "#2a2a2a", borderRadius: "4px", height: "8px", marginBottom: "0.5rem", overflow: "hidden" },
414
- progressFill: { background: "#4ade80", height: "100%", borderRadius: "4px", transition: "width 0.3s ease" },
415
- progressText: { fontSize: "0.9rem", color: "#aaa", margin: "0 0 0.75rem 0" },
416
- trackBadges: { display: "flex", gap: "0.5rem", marginBottom: "1rem" },
417
- badge: { padding: "0.2rem 0.6rem", background: "#1e3a5f", color: "#7dd3fc", borderRadius: "4px", fontSize: "0.75rem" },
418
- typeBadge: { padding: "0.15rem 0.4rem", background: "#2a2a2a", color: "#aaa", borderRadius: "3px", fontSize: "0.75rem" },
419
- statusDone: { color: "#4ade80" },
420
- statusProgress: { color: "#facc15" },
421
- statusPending: { color: "#555" },
422
- table: { width: "100%", borderCollapse: "collapse", textAlign: "left" },
423
- th: { borderBottom: "1px solid #444", padding: "0.5rem", color: "#aaa", fontSize: "0.85rem" },
424
- td: { borderBottom: "1px solid #222", padding: "0.5rem", fontSize: "0.85rem" },
2060
+ ]
2061
+
2062
+ // ═══════════════════════════════════════════════════════════════
2063
+ // Tab: README
2064
+ // ═══════════════════════════════════════════════════════════════
2065
+
2066
+ const DEMO_README = `# Token Transfer App
2067
+
2068
+ A decentralized token transfer application built on [AO](https://ao.arweave.dev) and [HyperBEAM](https://docs.wao.eco). Users can mint tokens, transfer them between Arweave wallets, and query balances — all running as permanent, verifiable processes on the AO computer.
2069
+
2070
+ ## What It Does
2071
+
2072
+ - **Mint** Create new tokens with an initial supply assigned to the minting wallet
2073
+ - **Transfer** Send tokens between any two Arweave addresses with atomic balance updates
2074
+ - **Balance** — Query the balance of any address via dry-run (no gas, no state mutation)
2075
+ - **Registry** — Register tokens for discoverability so other apps can find and interact with them
2076
+ - **Frontend** — React SPA with ArConnect wallet integration for browser-based transfers
2077
+
2078
+ ## How It Works
2079
+
2080
+ AOS scripts run as permanent processes on the AO network. Each process has its own state (balances, registry entries) and responds to messages. The frontend connects via ArConnect and sends signed messages to the AOS process.
2081
+
2082
+ On HyperBEAM, the same scripts deploy as genesis-wasm devices with slot-based message scheduling. This gives you a local development environment identical to production.
2083
+
2084
+ ## Project Structure
2085
+
2086
+ \`\`\`
2087
+ src/token.lua # Token handler — mint, transfer, balance
2088
+ src/registry.lua # Token registry — register, list, query
2089
+ test/aos.test.js # In-memory AOS unit tests
2090
+ test/token.test.js # Token-specific unit tests
2091
+ test/registry.test.js # Registry unit tests
2092
+ test/hyperbeam-token.test.js # HyperBEAM integration tests
2093
+ test/token-device.test.js # Device HTTP integration tests
2094
+ frontend/src/App.jsx # Main app with wallet connect
2095
+ frontend/src/components/ # TransferForm, BalanceDisplay, etc.
2096
+ frontend/src/hooks/ # useToken, useWallet
2097
+ scripts/deploy.js # Multi-target deploy script
2098
+ \`\`\`
2099
+
2100
+ ## Setup
2101
+
2102
+ \`\`\`bash
2103
+ yarn install
2104
+ yarn keygen # generate .wallet.json (Arweave JWK)
2105
+ \`\`\`
2106
+
2107
+ ## Test
2108
+
2109
+ \`\`\`bash
2110
+ yarn test test/aos.test.js # in-memory AOS — fast, no server
2111
+ yarn test test/token.test.js # token unit tests
2112
+ yarn test test/registry.test.js # registry unit tests
2113
+ yarn test test/hyperbeam.test.js # HyperBEAM integration (requires local HB)
2114
+ cd frontend && npm run test:unit # frontend vitest
2115
+ cd frontend && npm run test:e2e # Playwright E2E with live backend
2116
+ \`\`\`
2117
+
2118
+ ## Deploy
2119
+
2120
+ \`\`\`bash
2121
+ yarn deploy # all src/*.lua to AO testnet
2122
+ yarn deploy src/token.lua # single script to testnet
2123
+ yarn deploy --local-hb # local HyperBEAM (genesis-wasm)
2124
+ yarn deploy --local-hb --lua # local HyperBEAM (Lua mode, faster)
2125
+ yarn deploy --mainnet # remote HyperBEAM (push-1.forward.computer)
2126
+ \`\`\`
2127
+
2128
+ Each script gets its own AOS process. On testnet, the source is loaded via Eval message. On HyperBEAM, spawn and source are bundled in a single deploy call.
2129
+
2130
+ ## AOS Script API
2131
+
2132
+ ### Token (\`src/token.lua\`)
2133
+
2134
+ - \`Action: "Mint"\` — Mint tokens. Tags: \`Quantity\`
2135
+ - \`Action: "Transfer"\` — Transfer tokens. Tags: \`Recipient\`, \`Quantity\`
2136
+ - \`Action: "Balance"\` — Query balance. Tags: \`Target\` (optional, defaults to sender)
2137
+ - \`Action: "Balances"\` — Returns all balances as JSON
2138
+
2139
+ ### Registry (\`src/registry.lua\`)
2140
+
2141
+ - \`Action: "Register"\` — Register a token. Tags: \`ProcessId\`, \`Name\`, \`Ticker\`
2142
+ - \`Action: "Deregister"\` — Remove a token. Tags: \`ProcessId\`
2143
+ - \`Action: "List"\` — List all registered tokens
2144
+ - \`Action: "Info"\` — Query token metadata. Tags: \`ProcessId\`
2145
+
2146
+ ## Explorers
2147
+
2148
+ After deploying, the CLI prints each process ID. Use these to inspect your processes:
2149
+
2150
+ - [aolink](https://aolink.ar.io) — AOS process explorer (messages, state, tags)
2151
+ - [lunar](https://lunar.ar.io) — HyperBEAM explorer (slots, devices, compute logs)
2152
+
2153
+ ## Built With
2154
+
2155
+ - [WAO](https://docs.wao.eco) — SDK for AO and HyperBEAM
2156
+ - [AOS](https://ao.arweave.dev) — Lua processes on the AO computer
2157
+ - [HyperBEAM](https://github.com/permaweb/HyperBEAM) — Erlang runtime for AO
2158
+ - [ArConnect](https://www.arconnect.io) — Arweave wallet browser extension
2159
+ `
2160
+
2161
+ function ReadmeTab() {
2162
+ const [content, setContent] = useState(null)
2163
+ const [loading, setLoading] = useState(true)
2164
+
2165
+ useEffect(() => {
2166
+ fetch("/api/readme")
2167
+ .then(r => r.json())
2168
+ .then(d => { setContent(d.content || DEMO_README); setLoading(false) })
2169
+ .catch(() => { setContent(DEMO_README); setLoading(false) })
2170
+ }, [])
2171
+
2172
+ if (loading) return <p className="p-4 color-fg-muted">Loading README...</p>
2173
+ if (!content) return <p className="p-4 color-fg-muted">No README.md yet. Run /readme to generate one.</p>
2174
+
2175
+ const lines = content.split("\n")
2176
+ const elements = []
2177
+ let inCode = false, codeBlock = [], codeLang = ""
2178
+
2179
+ for (let i = 0; i < lines.length; i++) {
2180
+ const line = lines[i]
2181
+ if (line.startsWith("```")) {
2182
+ if (inCode) {
2183
+ elements.push(<CodeBlock key={i} code={codeBlock.join("\n")} lang={codeLang} />)
2184
+ codeBlock = []; inCode = false; codeLang = ""
2185
+ } else {
2186
+ inCode = true; codeLang = line.slice(3).trim()
2187
+ }
2188
+ } else if (inCode) {
2189
+ codeBlock.push(line)
2190
+ } else if (line.startsWith("# ")) {
2191
+ elements.push(<h1 key={i}>{line.slice(2)}</h1>)
2192
+ } else if (line.startsWith("## ")) {
2193
+ elements.push(<h2 key={i}>{line.slice(3)}</h2>)
2194
+ } else if (line.startsWith("### ")) {
2195
+ elements.push(<h3 key={i}>{line.slice(4)}</h3>)
2196
+ } else if (line.startsWith("- ")) {
2197
+ const parts = []
2198
+ let j = i
2199
+ while (j < lines.length && lines[j].startsWith("- ")) {
2200
+ parts.push(<li key={j}>{renderInline(lines[j].slice(2))}</li>)
2201
+ j++
2202
+ }
2203
+ elements.push(<ul key={i}>{parts}</ul>)
2204
+ i = j - 1
2205
+ } else if (line.trim()) {
2206
+ elements.push(<p key={i}>{renderInline(line)}</p>)
2207
+ }
2208
+ }
2209
+
2210
+ return <div className="markdown-body markdown-compact p-4">{elements}</div>
2211
+ }
2212
+
2213
+ // ═══════════════════════════════════════════════════════════════
2214
+ // Tab: Deploy
2215
+ // ═══════════════════════════════════════════════════════════════
2216
+
2217
+ function DeployTab() {
2218
+ const [info, setInfo] = useState(null)
2219
+
2220
+ useEffect(() => {
2221
+ fetch("/api/deploy").then(r => r.json()).then(setInfo).catch(() => setInfo(null))
2222
+ }, [])
2223
+
2224
+ const hbPort = info?.hyperbeam?.port || "10001"
2225
+
2226
+ return (
2227
+ <div className="markdown-body markdown-compact p-4">
2228
+ <div className="d-flex flex-items-center flex-justify-between mb-3">
2229
+ <h2 style={{ margin: 0, border: "none", paddingBottom: 0 }}>Deploy</h2>
2230
+ <span className="f6 color-fg-muted d-flex flex-items-center" style={{ gap: 8 }}>
2231
+ <span className="d-flex flex-items-center" style={{ gap: 4 }}>
2232
+ <span className="d-inline-block" style={{ width: 8, height: 8, borderRadius: "50%", background: info?.wallet ? "#1a7f37" : "#cf222e" }} />
2233
+ {info?.wallet ? "wallet found" : "no wallet"}
2234
+ </span>
2235
+ &middot;
2236
+ <span className="d-flex flex-items-center" style={{ gap: 4 }}>
2237
+ <span className="d-inline-block" style={{ width: 8, height: 8, borderRadius: "50%", background: info?.hyperbeam?.configured ? "#1a7f37" : "#bf8700" }} />
2238
+ {info?.hyperbeam?.configured ? `HyperBEAM :${hbPort}` : "HyperBEAM not configured"}
2239
+ </span>
2240
+ </span>
2241
+ </div>
2242
+
2243
+ {DEPLOY_STEPS.map(step => (
2244
+ <div key={step.title} className="mb-4">
2245
+ <h3>{step.title}</h3>
2246
+ <p className="f6 color-fg-muted mb-2">{step.desc}</p>
2247
+ {step.cmds.map(c => (
2248
+ <div key={c.cmd} className="mb-2">
2249
+ <CodeBlock code={c.cmd.startsWith("http") ? c.cmd : `$ ${c.cmd}`} lang={c.cmd.startsWith("http") ? "" : "bash"} />
2250
+ <p className="f6 color-fg-muted mt-1">{c.desc}</p>
2251
+ </div>
2252
+ ))}
2253
+ </div>
2254
+ ))}
2255
+
2256
+ <h3>How Multi-Script Deploy Works</h3>
2257
+ <p className="f6 color-fg-muted mb-2">
2258
+ When you run <code>yarn deploy</code> without specifying files, <code>scripts/deploy.js</code> reads all <code>src/*.lua</code> files and deploys each one as a separate AOS process:
2259
+ </p>
2260
+ <ol className="f6 color-fg-muted">
2261
+ <li>Loads your wallet from <code>.wallet.json</code></li>
2262
+ <li>For each <code>.lua</code> file, spawns a new AOS process</li>
2263
+ <li>On <strong>testnet</strong>: sends the Lua source as an <code>Eval</code> message (Action: &quot;Eval&quot;, data = source code)</li>
2264
+ <li>On <strong>HyperBEAM</strong>: calls <code>ao.deploy()</code> which bundles spawn + source in one HTTP call</li>
2265
+ <li>Prints the process ID for each deployed script</li>
2266
+ </ol>
2267
+
2268
+ <h3>Remote HyperBEAM Nodes</h3>
2269
+ <p className="f6 color-fg-muted mb-2">Use <code>push-1</code> through <code>push-10</code> for full compute. <code>push.forward.computer</code> is push-only (no compute).</p>
2270
+
2271
+ </div>
2272
+ )
2273
+ }
2274
+
2275
+ // ═══════════════════════════════════════════════════════════════
2276
+ // Main App
2277
+ // ═══════════════════════════════════════════════════════════════
2278
+
2279
+ export default function App() {
2280
+ const [data, setData] = useState(DEMO_DATA)
2281
+ const [tab, setTab] = useState("tasks")
2282
+ const [dark, setDark] = useState(false)
2283
+ const [connected, setConnected] = useState(false)
2284
+
2285
+ useEffect(() => {
2286
+ const mode = dark ? "dark" : "light"
2287
+ document.documentElement.setAttribute("data-color-mode", mode)
2288
+ const ls = document.getElementById("hljs-light")
2289
+ const ds = document.getElementById("hljs-dark")
2290
+ if (ls) ls.disabled = dark
2291
+ if (ds) ds.disabled = !dark
2292
+ }, [dark])
2293
+
2294
+ useEffect(() => {
2295
+ let interval = null
2296
+ let es = null
2297
+
2298
+ fetch("/api/progress").then(r => r.json()).then(d => { if (d?.feature) { setData(d); setConnected(true) } }).catch(() => {})
2299
+
2300
+ try {
2301
+ es = new EventSource("/api/events")
2302
+ es.addEventListener("progress", e => {
2303
+ try { const d = JSON.parse(e.data); if (d?.feature) setData(d) } catch {}
2304
+ })
2305
+ es.onopen = () => setConnected(true)
2306
+ es.onerror = () => {
2307
+ setConnected(false); es.close(); es = null
2308
+ if (!interval) interval = setInterval(() => {
2309
+ fetch("/api/progress").then(r => r.json()).then(d => { if (d?.feature) setData(d) }).catch(() => {})
2310
+ }, 3000)
2311
+ }
2312
+ } catch {
2313
+ interval = setInterval(() => {
2314
+ fetch("/api/progress").then(r => r.json()).then(d => { if (d?.feature) setData(d) }).catch(() => {})
2315
+ }, 3000)
2316
+ }
2317
+
2318
+ return () => { if (es) es.close(); if (interval) clearInterval(interval) }
2319
+ }, [])
2320
+
2321
+ return (
2322
+ <div style={{ minHeight: "100vh", display: "flex", flexDirection: "column" }}>
2323
+ <Header dark={dark} setDark={setDark} connected={connected} />
2324
+ <div style={{ maxWidth: 1012, margin: "0 auto", flex: 1, width: "100%" }}>
2325
+ <ProgressSection data={data} />
2326
+ <TrackCards data={data} dark={dark} />
2327
+ <TabBar active={tab} setActive={setTab} taskCount={data?.tasks?.length || 0} />
2328
+ {tab === "tasks" && <TasksTab data={data} />}
2329
+ {tab === "tests" && <TestsTab />}
2330
+ {tab === "plan" && <PlanTab />}
2331
+ {tab === "code" && <CodeTab />}
2332
+ {tab === "readme" && <ReadmeTab />}
2333
+ {tab === "commands" && <CommandsTab data={data} />}
2334
+ {tab === "skills" && <SkillsTab />}
2335
+ {tab === "deploy" && <DeployTab />}
2336
+ </div>
2337
+ <footer style={{ marginTop: 40 }}>
2338
+ <div className="d-flex flex-items-center flex-justify-center flex-wrap f6 color-fg-muted" style={{ maxWidth: 1012, margin: "0 auto", padding: "24px 16px", gap: 16 }}>
2339
+ <span>&copy; {new Date().getFullYear()} <a href="https://docs.wao.eco/" className="color-fg-muted" style={{ textDecoration: "none" }}>WizardAO</a></span>
2340
+ <span>&middot;</span>
2341
+ <a href="https://docs.wao.eco/add/overview" className="color-fg-muted" style={{ textDecoration: "none" }}>Docs</a>
2342
+ <span>&middot;</span>
2343
+ <a href="https://aolink.ar.io" className="color-fg-muted" style={{ textDecoration: "none" }}>aolink</a>
2344
+ <span>&middot;</span>
2345
+ <a href="https://lunar.ar.io" className="color-fg-muted" style={{ textDecoration: "none" }}>lunar</a>
2346
+ </div>
2347
+ </footer>
2348
+ </div>
2349
+ )
425
2350
  }