From 457a04e64ffc60391c05b4d9b755540e7ba47c87 Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:31:41 +0900 Subject: [PATCH] feat(task4): refactor miniapp static serving and frontend bundle/tests --- .github/workflows/pr.yml | 23 +- .gitignore | 8 + CLAUDE.md | 3 +- pkg/miniapp/frontend/build.mjs | 27 + pkg/miniapp/frontend/package.json | 14 + pkg/miniapp/frontend/pnpm-lock.yaml | 900 ++++++++ pkg/miniapp/frontend/src/app.js | 1341 +++++++++++ pkg/miniapp/frontend/src/logs_view.js | 106 + pkg/miniapp/frontend/src/logs_view.test.js | 70 + pkg/miniapp/{static => frontend/src}/map.js | 7 + pkg/miniapp/frontend/src/styles.css | 912 ++++++++ pkg/miniapp/frontend/vitest.config.mjs | 8 + pkg/miniapp/miniapp.go | 48 +- pkg/miniapp/miniapp_test.go | 55 + pkg/miniapp/static/dist/app.css | 1230 +++++++++++ pkg/miniapp/static/dist/app.js | 1459 ++++++++++++ pkg/miniapp/static/dist/map.js | 212 ++ pkg/miniapp/static/index.html | 2195 +------------------ pkg/miniapp/static/map-preview.html | 3 +- todo/TASKS-4.md | 31 + 20 files changed, 6448 insertions(+), 2204 deletions(-) create mode 100644 pkg/miniapp/frontend/build.mjs create mode 100644 pkg/miniapp/frontend/package.json create mode 100644 pkg/miniapp/frontend/pnpm-lock.yaml create mode 100644 pkg/miniapp/frontend/src/app.js create mode 100644 pkg/miniapp/frontend/src/logs_view.js create mode 100644 pkg/miniapp/frontend/src/logs_view.test.js rename pkg/miniapp/{static => frontend/src}/map.js (97%) create mode 100644 pkg/miniapp/frontend/src/styles.css create mode 100644 pkg/miniapp/frontend/vitest.config.mjs create mode 100644 pkg/miniapp/static/dist/app.css create mode 100644 pkg/miniapp/static/dist/app.js create mode 100644 pkg/miniapp/static/dist/map.js diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index be1c10c52..8d3ca4ff9 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,7 +1,7 @@ name: PR on: - pull_request: { } + pull_request: {} jobs: lint: @@ -16,6 +16,9 @@ jobs: with: go-version-file: go.mod + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + - name: Run go generate run: go generate ./... @@ -36,8 +39,26 @@ jobs: with: go-version-file: go.mod + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: '24' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Run go generate run: go generate ./... + - name: Run frontend tests + run: | + pnpm --dir pkg/miniapp/frontend install --frozen-lockfile + pnpm --dir pkg/miniapp/frontend test + - name: Run go test run: go test ./... diff --git a/.gitignore b/.gitignore index 02ef18d1f..ac52098f0 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,14 @@ tasks/ # Added by goreleaser init: dist/ +!pkg/miniapp/static/dist/ +!pkg/miniapp/static/dist/** # Windows Application Icon/Resource *.syso + + +# Frontend dependencies +node_modules/ + + diff --git a/CLAUDE.md b/CLAUDE.md index 64134ffeb..e638833c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,8 +63,9 @@ Lint: `golangci-lint run` | [`todo/TASKS-1.md`](todo/TASKS-1.md) | ~~**Memory & Performance Optimization**~~ ✅ 実装済み(MemoryStore キャッシュ+パース済み state、FunctionCall.Arguments map統一、ToolDefinition.Parameters RawMessage化、検索結果フォーマット共通化、stats 定期フラッシュ) | | [`todo/TASKS-2.md`](todo/TASKS-2.md) | **Subagent Orchestration (Container Model)** — SubagentContainer、Orchestrator、Presets enforcement、Subagent Plan Mode(TASKS-1 の型変更前提メモ追記済み) | | [`todo/TASKS-3.md`](todo/TASKS-3.md) | ~~**Session DAG (SQLite Store)**~~ ✅ 実装済み(Phase 0–3: SQLite SessionStore、LegacyAdapter、Fork/Report、CompactOldTurns、`/session` CLI コマンド、Mini App グラフ UI) | -| [`todo/TASKS-4.md`](todo/TASKS-4.md) | **Mini App & Static Serving** — 静的配信の汎用化、バンドラ導入、フロントエンドテスト追加 | +| [`todo/TASKS-4.md`](todo/TASKS-4.md) | ~~**Mini App & Static Serving**~~ ✅ 実装済み(`http.FileServer` 統合、テンプレート注入、Bun ビルド導線、frontend unit test + CI `pnpm test`) | | [`todo/TASKS-5.md`](todo/TASKS-5.md) | ~~**Heartbeat Worktree Management**~~ ✅ 実装済み(`/plan worktrees` の `list/inspect/merge/dispose`、安全化した `PruneOrphaned`、Mini App `/miniapp/api/worktrees` + Git タブ UI) | | [`todo/TASKS-6.md`](todo/TASKS-6.md) | **SOUL.md — AI Persona Evolution** — 睡眠フェーズで体験を統合・忘却し人格を再構成。TASKS-2 完了後に着手 | | [`todo/TASKS-7.md`](todo/TASKS-7.md) | **Provider Wire Compatibility Hardening** — openai_compat の provider 別 wire 分岐(OpenAI strict / Gemini)、thought_signature round-trip 保全、互換テスト追加 | + diff --git a/pkg/miniapp/frontend/build.mjs b/pkg/miniapp/frontend/build.mjs new file mode 100644 index 000000000..facccb114 --- /dev/null +++ b/pkg/miniapp/frontend/build.mjs @@ -0,0 +1,27 @@ +import { copyFile, mkdir, rm } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const rootDir = path.dirname(fileURLToPath(import.meta.url)); +const srcDir = path.join(rootDir, 'src'); +const outDir = path.join(rootDir, '..', 'static', 'dist'); + +await rm(outDir, { recursive: true, force: true }); +await mkdir(outDir, { recursive: true }); + +const result = await Bun.build({ + entrypoints: [path.join(srcDir, 'app.js')], + outdir: outDir, + target: 'browser', + format: 'iife', + sourcemap: 'none', +}); + +if (!result.success) { + for (const log of result.logs) { + console.error(log); + } + process.exit(1); +} + +await copyFile(path.join(srcDir, 'map.js'), path.join(outDir, 'map.js')); diff --git a/pkg/miniapp/frontend/package.json b/pkg/miniapp/frontend/package.json new file mode 100644 index 000000000..b36db1154 --- /dev/null +++ b/pkg/miniapp/frontend/package.json @@ -0,0 +1,14 @@ +{ + "name": "miniapp-frontend", + "private": true, + "type": "module", + "scripts": { + "build": "bun run ./build.mjs", + "test": "vitest run" + }, + "devDependencies": { + "happy-dom": "^16.8.1", + "vitest": "^2.1.9" + } +} + diff --git a/pkg/miniapp/frontend/pnpm-lock.yaml b/pkg/miniapp/frontend/pnpm-lock.yaml new file mode 100644 index 000000000..804c36b14 --- /dev/null +++ b/pkg/miniapp/frontend/pnpm-lock.yaml @@ -0,0 +1,900 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + happy-dom: + specifier: ^16.8.1 + version: 16.8.1 + vitest: + specifier: ^2.1.9 + version: 2.1.9(happy-dom@16.8.1) + +packages: + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + happy-dom@16.8.1: + resolution: {integrity: sha512-n0QrmT9lD81rbpKsyhnlz3DgnMZlaOkJPpgi746doA+HvaMC79bdWkwjrNnGJRvDrWTI8iOcJiVTJ5CdT/AZRw==} + engines: {node: '>=18.0.0'} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + +snapshots: + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@types/estree@1.0.8': {} + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21)': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21 + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + assertion-error@2.0.1: {} + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + + fsevents@2.3.3: + optional: true + + happy-dom@16.8.1: + dependencies: + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + pathe@1.1.2: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + vite-node@2.1.9: + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21 + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.8 + rollup: 4.59.0 + optionalDependencies: + fsevents: 2.3.3 + + vitest@2.1.9(happy-dom@16.8.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21 + vite-node: 2.1.9 + why-is-node-running: 2.3.0 + optionalDependencies: + happy-dom: 16.8.1 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + webidl-conversions@7.0.0: {} + + whatwg-mimetype@3.0.0: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 diff --git a/pkg/miniapp/frontend/src/app.js b/pkg/miniapp/frontend/src/app.js new file mode 100644 index 000000000..400f57676 --- /dev/null +++ b/pkg/miniapp/frontend/src/app.js @@ -0,0 +1,1341 @@ +import './styles.css'; +import { renderLogs as renderLogsView } from './logs_view.js'; + +const tg = window.Telegram.WebApp; +tg.ready(); + +const API_BASE = location.origin; +let initData = tg.initData || ''; +let selectedSkill = null; +var lastSSE = { plan: 0, skills: 0, session: 0, dev: 0 }; + +// Hide Orch tab when orchestration is not enabled (ORCH_ENABLED injected by server). +if (!window.ORCH_ENABLED) { + var orchTabBtn = document.querySelector('.tab[data-panel="orch"]'); + var orchPanel = document.getElementById('orch'); + if (orchTabBtn) orchTabBtn.style.display = 'none'; + if (orchPanel) orchPanel.style.display = 'none'; + document.documentElement.style.setProperty('--tab-count', '6'); +} + +// Tab switching — re-fetch data unless SSE delivered recently +const tabs = document.querySelectorAll('.tab:not([style*="display: none"])'); +const tabIndicator = document.querySelector('.tab-indicator'); + +function moveIndicator(index) { + tabIndicator.style.transform = 'translateX(' + (index * 100) + '%)'; +} + +tabs.forEach((tab, index) => { + tab.addEventListener('click', () => { + tabs.forEach(t => t.classList.remove('active')); + document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); + tab.classList.add('active'); + document.getElementById(tab.dataset.panel).classList.add('active'); + moveIndicator(index); + + document.getElementById('send-bar').classList.toggle('hidden', + !(tab.dataset.panel === 'skills' && selectedSkill)); + + var p = tab.dataset.panel; + var fresh = lastSSE[p] && (Date.now() - lastSSE[p] < 5000); + if (p === 'plan' && !fresh) loadPlan(); + if (p === 'skills' && !fresh) loadSkills(); + if (p === 'session' && !fresh) loadSession(); + if (p === 'git') loadGit(); + if (p === 'dev' && !fresh) loadDev(); + if (p === 'config') connectLogsWs(); + else disconnectLogsWs(); + if (p === 'orch') connectOrchWs(); + else disconnectOrchWs(); + }); +}); + +// Quick command tiles +document.querySelectorAll('.cmd-tile').forEach(tile => { + tile.addEventListener('click', async () => { + const ok = await sendCommand(tile.dataset.cmd); + if (ok) flashSent(tile); + }); +}); + +async function sendCommand(cmd) { + if (!cmd.startsWith('/')) return false; + try { + const res = await fetch(API_BASE + '/miniapp/api/command?initData=' + encodeURIComponent(initData), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ command: cmd }), + }); + if (!res.ok) throw new Error('API error: ' + res.status); + return true; + } catch (e) { + return false; + } +} + +async function sendCustomCmd() { + const input = document.getElementById('custom-cmd'); + const btn = input.nextElementSibling; + const cmd = input.value.trim(); + if (!cmd) return; + if (!cmd.startsWith('/')) return; + const ok = await sendCommand(cmd); + if (ok) { + input.value = ''; + flashSent(btn); + } +} + +async function sendSkillCommand() { + if (!selectedSkill) return; + const msg = document.getElementById('skill-msg').value.trim(); + const cmd = msg ? '/skill ' + selectedSkill + ' ' + msg : '/skill ' + selectedSkill; + const btn = document.getElementById('send-skill-btn'); + const ok = await sendCommand(cmd); + if (ok) flashSent(btn); +} + +async function startPlan() { + const input = document.getElementById('plan-task'); + const btn = input.nextElementSibling; + const task = input.value.trim(); + if (!task) return; + const ok = await sendCommand('/plan ' + task); + if (ok) { + input.value = ''; + flashSent(btn); + } +} + +function flashSent(el) { + el.classList.add('sent'); + setTimeout(() => el.classList.remove('sent'), 600); +} + +async function apiFetch(path) { + const sep = path.includes('?') ? '&' : '?'; + const res = await fetch(API_BASE + path + sep + 'initData=' + encodeURIComponent(initData)); + if (!res.ok) throw new Error('API error: ' + res.status); + return res.json(); +} + +// ── Plan tab ── +function renderPlanFromData(data) { + var loading = document.getElementById('plan-loading'); + var el = document.getElementById('plan-content'); + loading.classList.add('hidden'); + el.classList.remove('hidden'); + + if (!data.has_plan) { + el.innerHTML = `
No active plan.
+
+
Start a Plan
+
+ + +
+
`; + return; + } + + var html = `
+
Status
+
${escapeHtml(data.status)}
+
Phase ${data.current_phase} / ${data.total_phases}
+
`; + + if (data.status === 'interviewing' || data.status === 'review') { + if (data.memory) { + html += `
${renderSimpleMarkdown(data.memory)}
`; + } + if (data.status === 'review') { + html += `
+
+
+
Slide to Approve
+
+
+
+
+
+
Approve & Clear History
+
+
`; + } + } else { + if (data.phases && data.phases.length > 0) { + html += renderPhases(data.phases, data.current_phase); + } + } + + el.innerHTML = html; + if (data.status === 'review') setupSlideApprove(); +} + +var slideApproveAC = null; +function setupSlideApprove() { + if (slideApproveAC) slideApproveAC.abort(); + slideApproveAC = new AbortController(); + var signal = slideApproveAC.signal; + + var tracks = document.querySelectorAll('.slide-approve-track'); + if (!tracks.length) return; + + tracks.forEach(function(track) { + var thumb = track.querySelector('.slide-approve-thumb'); + var label = track.querySelector('.slide-approve-label'); + var cmd = track.getAttribute('data-cmd') || '/plan start'; + var dragging = false; + var startX = 0; + var thumbStartLeft = 0; + + function getMaxLeft() { + return track.offsetWidth - thumb.offsetWidth - 6; + } + + function markAllApproved() { + tracks.forEach(function(t) { + t.classList.add('approved'); + t.querySelector('.slide-approve-label').textContent = 'Approved!'; + t.querySelector('.slide-approve-thumb').classList.add('hidden'); + }); + } + + function onStart(e) { + if (track.classList.contains('approved')) return; + dragging = true; + thumb.classList.add('dragging'); + var clientX = e.touches ? e.touches[0].clientX : e.clientX; + startX = clientX; + thumbStartLeft = thumb.offsetLeft - 3; + e.preventDefault(); + } + + function onMove(e) { + if (!dragging) return; + var clientX = e.touches ? e.touches[0].clientX : e.clientX; + var dx = clientX - startX; + var newLeft = Math.max(0, Math.min(thumbStartLeft + dx, getMaxLeft())); + thumb.style.left = (newLeft + 3) + 'px'; + e.preventDefault(); + } + + function onEnd(e) { + if (!dragging) return; + dragging = false; + thumb.classList.remove('dragging'); + var currentLeft = thumb.offsetLeft - 3; + var maxLeft = getMaxLeft(); + if (currentLeft >= maxLeft * 0.8) { + markAllApproved(); + sendCommand(cmd); + } else { + thumb.style.left = '3px'; + } + } + + thumb.addEventListener('touchstart', onStart, { passive: false, signal: signal }); + thumb.addEventListener('mousedown', onStart, { signal: signal }); + document.addEventListener('touchmove', onMove, { passive: false, signal: signal }); + document.addEventListener('mousemove', onMove, { signal: signal }); + document.addEventListener('touchend', onEnd, { signal: signal }); + document.addEventListener('mouseup', onEnd, { signal: signal }); + }); +} + +function renderSimpleMarkdown(text) { + var lines = text.split('\n'); + var out = []; + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + // Headings + if (/^### /.test(line)) { + out.push('
' + escapeHtml(line.slice(4)) + '
'); + } else if (/^## /.test(line)) { + out.push('
' + escapeHtml(line.slice(3)) + '
'); + } else if (/^# /.test(line)) { + out.push('
' + escapeHtml(line.slice(2)) + '
'); + // Checkboxes + } else if (/^- \[x\] /.test(line)) { + out.push('
' + escapeHtml(line.slice(6)) + '
'); + } else if (/^- \[ \] /.test(line)) { + out.push('
' + escapeHtml(line.slice(6)) + '
'); + // Blockquote + } else if (/^> /.test(line)) { + out.push('
' + escapeHtml(line.slice(2)) + '
'); + // Bullet list + } else if (/^- /.test(line)) { + out.push('
' + escapeHtml(line.slice(2)) + '
'); + // Empty line + } else if (line.trim() === '') { + out.push('
'); + // Plain text + } else { + out.push('
' + escapeHtml(line) + '
'); + } + } + return out.join(''); +} + +async function loadTab(loadingId, contentId, label, fetchFn, renderFn) { + var loading = document.getElementById(loadingId); + var el = document.getElementById(contentId); + loading.classList.remove('hidden'); + loading.textContent = 'Loading ' + label + '...'; + el.classList.add('hidden'); + try { renderFn(await fetchFn()); } + catch (e) { loading.textContent = 'Failed to load ' + label + '.'; } +} + +function loadPlan() { + return loadTab('plan-loading', 'plan-content', 'plan', + function() { return apiFetch('/miniapp/api/plan'); }, + renderPlanFromData); +} + +function renderPhases(phases, currentPhase) { + return phases.map(phase => { + const doneCount = phase.steps.filter(s => s.done).length; + const total = phase.steps.length; + + let indicatorClass, indicator; + if (phase.number < currentPhase || (total > 0 && doneCount === total)) { + indicatorClass = 'done'; + indicator = '\u2713'; + } else if (phase.number === currentPhase) { + indicatorClass = 'current'; + indicator = String(phase.number); + } else { + indicatorClass = 'pending'; + indicator = String(phase.number); + } + + const progressHtml = total > 0 ? `${doneCount}/${total}` : ''; + const stepsHtml = phase.steps.map(step => { + const doneClass = step.done ? 'done' : ''; + const stepClass = step.done ? 'step step-done' : 'step'; + return `
+
+
${escapeHtml(step.description)}
+
`; + }).join(''); + + return `
+
+
${indicator}
+ ${escapeHtml(phase.title || 'Phase ' + phase.number)} + ${progressHtml} +
+ ${stepsHtml} +
`; + }).join(''); +} + +// Delegate click on steps — tap to mark done (sends command, closes app) +document.getElementById('plan-content').addEventListener('click', function(e) { + const step = e.target.closest('.step'); + if (!step) return; + if (step.dataset.done === 'true') return; // already done + + const phase = step.dataset.phase; + const stepIdx = step.dataset.step; + sendCommand('/plan done ' + stepIdx); +}); + +// ── Skills tab ── +function renderSkillsFromData(data) { + var loading = document.getElementById('skills-loading'); + var el = document.getElementById('skills-list'); + loading.classList.add('hidden'); + el.classList.remove('hidden'); + + if (!data || data.length === 0) { + el.innerHTML = '
No skills installed.
'; + return; + } + + el.innerHTML = data.map(s => `
+
+
${escapeHtml(s.name)}
+
${escapeHtml(s.description || 'No description')}
+ ${escapeHtml(s.source)} +
+ \u203A +
`).join(''); + + if (selectedSkill) { + var prev = el.querySelector('[data-skill="' + CSS.escape(selectedSkill) + '"]'); + if (prev) prev.classList.add('selected'); + } +} + +// Delegate click on skills — single listener, registered once +document.getElementById('skills-list').addEventListener('click', function(e) { + var item = e.target.closest('.skill-item'); + if (!item) return; + var el = document.getElementById('skills-list'); + if (selectedSkill === item.dataset.skill) { + item.classList.remove('selected'); + selectedSkill = null; + document.getElementById('send-bar').classList.add('hidden'); + return; + } + el.querySelectorAll('.skill-item').forEach(function(i) { i.classList.remove('selected'); }); + item.classList.add('selected'); + selectedSkill = item.dataset.skill; + document.getElementById('send-bar').classList.remove('hidden'); + document.getElementById('skill-msg').placeholder = 'Message for /' + selectedSkill + '...'; + document.getElementById('skill-msg').focus(); +}); + +function loadSkills() { + return loadTab('skills-loading', 'skills-list', 'skills', + function() { return apiFetch('/miniapp/api/skills'); }, + renderSkillsFromData); +} + +// ── Session tab ── +function formatAge(sec) { + if (sec < 60) return sec + 's ago'; + if (sec < 3600) return Math.floor(sec / 60) + 'm ago'; + return Math.floor(sec / 3600) + 'h ago'; +} + +function shortSessionKey(key) { + // "agent:default:telegram:123" → "telegram:123" + var parts = key.split(':'); + if (parts.length > 2) return parts.slice(2).join(':'); + return key; +} + +function renderActiveSessions(sessions) { + if (!sessions || sessions.length === 0) { + return `
+
Active Sessions
+
No active sessions
+
`; + } + return `
Active Sessions
+ ${sessions.map(s => { + var touchDir = s.touch_dir || '\u2014'; + return `
+
+ \u25CF + ${escapeHtml(shortSessionKey(s.session_key))} + ${formatAge(s.age_sec)} +
+
touch: ${escapeHtml(touchDir)}
+
`; + }).join('')} +
`; +} + +function renderSessionFromData(sessions, stats) { + var loading = document.getElementById('session-loading'); + var el = document.getElementById('session-content'); + loading.classList.add('hidden'); + el.classList.remove('hidden'); + + var html = renderActiveSessions(sessions); + + if (!stats || stats.status === 'stats not enabled') { + html += '
Stats tracking not enabled.
Start gateway with --stats flag.
'; + el.innerHTML = html; + return; + } + + var since = stats.since ? new Date(stats.since).toLocaleDateString() : 'N/A'; + var today = stats.today || {}; + html += `
+
Today
+
Prompts${today.prompts || 0}
+
Requests${today.requests || 0}
+
Tokens${formatTokens(today.total_tokens || 0)}
+
+
+
All Time (since ${escapeHtml(since)})
+
Prompts${stats.total_prompts || 0}
+
Requests${stats.total_requests || 0}
+
Total Tokens${formatTokens(stats.total_tokens || 0)}
+
Prompt Tokens${formatTokens(stats.total_prompt_tokens || 0)}
+
Completion Tokens${formatTokens(stats.total_completion_tokens || 0)}
+
`; + + el.innerHTML = html; +} + +var cachedContextInfo = null; + +function renderContextCard(ctx) { + if (!ctx) return ''; + cachedContextInfo = ctx; + var wd = ctx.work_dir || '\u2014'; + var pwd = ctx.plan_work_dir || '\u2014'; + var ws = ctx.workspace || '\u2014'; + var filesHtml = ''; + if (ctx.bootstrap && ctx.bootstrap.length) { + filesHtml = ctx.bootstrap.map(function(b) { + var path = b.path ? escapeHtml(b.path) : '\u2014'; + var scope = b.scope === 'global' ? 'global' : 'project'; + var found = b.path ? 'var(--text)' : 'var(--hint)'; + return `
+ ${escapeHtml(b.name)} + ${path} + ${scope} +
`; + }).join(''); + } + return `
+
Context
+
+
workDir${escapeHtml(wd)}
+
planWorkDir${escapeHtml(pwd)}
+
workspace${escapeHtml(ws)}
+
+
${filesHtml}
+
+ +
+ +
`; +} + +function toggleSystemPrompt() { + var view = document.getElementById('system-prompt-view'); + var btn = document.getElementById('prompt-toggle-btn'); + if (!view || !btn) return; + if (view.style.display === 'none') { + btn.textContent = 'Loading...'; + apiFetch('/miniapp/api/prompt').then(function(data) { + view.textContent = data.prompt || '(empty)'; + view.style.display = 'block'; + btn.textContent = 'Hide System Prompt'; + }).catch(function() { + btn.textContent = 'Show System Prompt'; + }); + } else { + view.style.display = 'none'; + btn.textContent = 'Show System Prompt'; + } +} + +function renderContextFromData(ctx) { + var el = document.getElementById('context-content'); + if (el) el.innerHTML = renderContextCard(ctx); +} + +function loadSession() { + return loadTab('session-loading', 'session-content', 'session', + function() { + return Promise.all([ + apiFetch('/miniapp/api/session'), + apiFetch('/miniapp/api/sessions').catch(function() { return []; }), + apiFetch('/miniapp/api/context').catch(function() { return null; }), + apiFetch('/miniapp/api/sessions/graph').catch(function() { return null; }), + ]); + }, + function(results) { + renderSessionFromData(results[1], results[0]); + renderContextFromData(results[2]); + renderSessionGraph(results[3]); + }); +} + +function renderSessionGraph(graph) { + var el = document.getElementById('session-graph'); + if (!el) return; + if (!graph || !graph.nodes || graph.nodes.length === 0) { + el.classList.add('hidden'); + return; + } + el.classList.remove('hidden'); + + // Build parent→children map + var childrenMap = {}; + var roots = []; + graph.nodes.forEach(function(n) { + childrenMap[n.key] = []; + }); + graph.edges.forEach(function(e) { + if (childrenMap[e.from]) childrenMap[e.from].push(e.to); + }); + var nodeMap = {}; + graph.nodes.forEach(function(n) { + nodeMap[n.key] = n; + // Check if this node is a root (no incoming edges) + var isChild = graph.edges.some(function(e) { return e.to === n.key; }); + if (!isChild) roots.push(n.key); + }); + + function renderTreeNode(key) { + var n = nodeMap[key]; + if (!n) return ''; + var icon = n.status === 'completed' ? '\u2713' : '\u25CF'; + var iconClass = n.status === 'completed' ? 'completed' : 'active'; + var label = n.label || n.short_key || n.key; + var kids = childrenMap[key] || []; + var childHtml = ''; + if (kids.length > 0) { + childHtml = ''; + } + return '
  • ' + + '' + icon + '' + + '' + escapeHtml(label) + '' + + 'turns=' + n.turn_count + '' + + childHtml + '
  • '; + } + + var html = '
    Session Graph
    ' + + '
    '; + el.innerHTML = html; +} + +function formatTokens(n) { + if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; + if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; + return String(n); +} + +function escapeHtml(s) { + if (!s) return ''; + return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +function escapeAttr(s) { + if (!s) return ''; + return s.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, '''); +} + +// ── Git tab ── +var gitSelectedRepo = null; + +function loadGit() { + gitSelectedRepo = null; + return loadTab('git-loading', 'git-content', 'git', + function() { + return Promise.all([ + apiFetch('/miniapp/api/git'), + apiFetch('/miniapp/api/worktrees').catch(function() { return []; }), + ]); + }, + function(results) { + renderGitRepos(results[0], results[1]); + }); +} + +function renderWorktrees(worktrees) { + var items = Array.isArray(worktrees) ? worktrees : []; + var html = '
    Worktrees
    '; + + if (items.length === 0) { + html += '
    No active worktrees.
    '; + html += '
    '; + return html; + } + + html += '
    '; + items.forEach(function(wt) { + var dirtyClass = wt.has_uncommitted ? ' dirty' : ''; + var dirtyBadge = wt.has_uncommitted ? 'DIRTY' : 'CLEAN'; + var last = '(no commits)'; + if (wt.last_commit_hash) { + last = wt.last_commit_hash + ' ' + (wt.last_commit_subject || ''); + if (wt.last_commit_age) last += ' (' + wt.last_commit_age + ')'; + } + html += '
    ' + + '
    ' + + '
    ' + + '' + escapeHtml(wt.name) + '' + + dirtyBadge + + '
    ' + + '
    ' + escapeHtml(wt.branch || '?') + '
    ' + + '
    ' + escapeHtml(last) + '
    ' + + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    '; + }); + html += '
    '; + return html; +} + +function renderGitRepos(repos, worktrees) { + var loading = document.getElementById('git-loading'); + var el = document.getElementById('git-content'); + loading.classList.add('hidden'); + el.classList.remove('hidden'); + + var html = renderWorktrees(worktrees); + + if (!repos || repos.length === 0) { + html += '
    No git repositories found.
    '; + el.innerHTML = html; + return; + } + + html += '
    Repositories
    '; + html += repos.map(function(r) { + return '
    ' + + '
    ' + + '
    ' + escapeHtml(r.name) + '
    ' + + '
    ' + escapeHtml(r.branch || '?') + '
    ' + + '
    ' + + '\u203A' + + '
    '; + }).join(''); + + el.innerHTML = html; +} + +async function postWorktreeAction(action, name, force) { + var res = await fetch(API_BASE + '/miniapp/api/worktrees?initData=' + encodeURIComponent(initData), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: action, name: name, force: !!force }), + }); + var data = {}; + try { data = await res.json(); } catch (e) {} + if (!res.ok) { + throw new Error(data.error || ('API error: ' + res.status)); + } + return data; +} + +document.getElementById('git-content').addEventListener('click', async function(e) { + var wtBtn = e.target.closest('[data-wt-action]'); + if (wtBtn) { + var action = wtBtn.dataset.wtAction; + var name = wtBtn.dataset.wtName; + var isDirty = wtBtn.dataset.wtDirty === '1'; + var force = false; + + if (action === 'merge') { + if (!confirm('Merge "' + name + '" into base branch?')) return; + } else if (action === 'dispose') { + if (isDirty) { + if (!confirm('"' + name + '" has uncommitted changes. Force dispose and auto-commit before removal?')) return; + force = true; + } else if (!confirm('Dispose worktree "' + name + '"?')) { + return; + } + } + + var originalText = wtBtn.textContent; + wtBtn.disabled = true; + wtBtn.textContent = action === 'merge' ? 'Merging...' : 'Disposing...'; + try { + await postWorktreeAction(action, name, force); + await loadGit(); + } catch (err) { + alert(err.message || 'Action failed'); + wtBtn.disabled = false; + wtBtn.textContent = originalText; + } + return; + } + + var item = e.target.closest('.git-repo-item'); + if (!item) return; + loadGitDetail(item.dataset.repo); +}); + +function loadGitDetail(name) { + gitSelectedRepo = name; + return loadTab('git-loading', 'git-content', name, + function() { return apiFetch('/miniapp/api/git?repo=' + encodeURIComponent(name)); }, + renderGitDetail); +} + +function renderGitDetail(repo) { + var loading = document.getElementById('git-loading'); + var el = document.getElementById('git-content'); + loading.classList.add('hidden'); + el.classList.remove('hidden'); + + var html = ''; + + html += '
    ' + + escapeHtml(repo.name) + ' — ' + escapeHtml(repo.branch || '?') + '
    '; + + if (repo.modified && repo.modified.length > 0) { + html += '
    Changes (' + repo.modified.length + ')
    '; + repo.modified.forEach(function(f) { + html += '
    ' + + '' + escapeHtml(f.status) + '' + + '' + escapeHtml(f.path) + '' + + '
    '; + }); + } + + if (repo.commits && repo.commits.length > 0) { + html += '
    Commits
    '; + repo.commits.forEach(function(c) { + html += '
    ' + + '' + escapeHtml(c.hash) + '' + + '' + escapeHtml(c.subject) + '' + + '' + escapeHtml(c.date) + '' + + '
    '; + }); + } else { + html += '
    No commits found.
    '; + } + html += '
    '; + + el.innerHTML = html; +} + +// ── Dev tab ── +var devActiveId = ''; + +function renderDevFromData(data) { + var dot = document.getElementById('dev-dot'); + var headerTarget = document.getElementById('dev-header-target'); + var targetsList = document.getElementById('dev-targets-list'); + var iframeWrap = document.getElementById('dev-iframe-wrap'); + var iframe = document.getElementById('dev-iframe'); + + var targets = data.targets || []; + devActiveId = data.active_id || ''; + + if (data.active) { + dot.classList.add('on'); + headerTarget.textContent = data.target ? data.target.replace(/^https?:\/\//, '') : ''; + iframeWrap.classList.remove('hidden'); + var iframeSrc = location.origin + '/miniapp/dev/'; + if (iframe.src !== iframeSrc) iframe.src = iframeSrc; + } else { + dot.classList.remove('on'); + headerTarget.textContent = ''; + iframeWrap.classList.add('hidden'); + iframe.src = ''; + } + + if (targets.length === 0) { + targetsList.innerHTML = '
    No targets registered.
    Ask the agent to start a dev server.
    '; + return; + } + + targetsList.innerHTML = targets.map(function(t) { + var isActive = t.id === devActiveId; + var activeClass = isActive ? ' active' : ''; + var dotClass = isActive ? ' on' : ''; + var displayUrl = t.target.replace(/^https?:\/\//, ''); + return '
    ' + + '' + + '' + escapeHtml(t.name) + '' + + '' + escapeHtml(displayUrl) + '' + + '×' + + '
    '; + }).join(''); +} + +// Delegate click on target cards +document.getElementById('dev-targets-list').addEventListener('click', function(e) { + var delBtn = e.target.closest('.dev-target-delete'); + if (delBtn) { + e.stopPropagation(); + var id = delBtn.dataset.delId; + var name = delBtn.dataset.delName; + if (confirm('Remove "' + name + '"?')) { + postDevUnregister(id); + } + return; + } + var card = e.target.closest('[data-dev-id]'); + if (!card) return; + postDevAction(card.dataset.devId); +}); + +async function postDevAction(id) { + var action = (id === devActiveId) ? 'deactivate' : 'activate'; + var body = action === 'activate' ? { action: 'activate', id: id } : { action: 'deactivate' }; + + try { + var res = await fetch(API_BASE + '/miniapp/api/dev?initData=' + encodeURIComponent(initData), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + var data = await res.json(); + if (!data.error) renderDevFromData(data); + } catch(e) {} +} + +async function postDevUnregister(id) { + try { + var res = await fetch(API_BASE + '/miniapp/api/dev?initData=' + encodeURIComponent(initData), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'unregister', id: id }), + }); + var data = await res.json(); + if (!data.error) renderDevFromData(data); + } catch(e) {} +} + +function loadDev() { + apiFetch('/miniapp/api/dev').then(renderDevFromData).catch(function() {}); +} + +// ── SSE real-time updates ── +var eventSource = null; + +function connectSSE() { + if (eventSource) eventSource.close(); + eventSource = new EventSource( + API_BASE + '/miniapp/api/events?initData=' + encodeURIComponent(initData) + ); + eventSource.addEventListener('plan', function(e) { + try { lastSSE.plan = Date.now(); renderPlanFromData(JSON.parse(e.data)); } catch(err) {} + }); + eventSource.addEventListener('session', function(e) { + try { + lastSSE.session = Date.now(); + var d = JSON.parse(e.data); + renderSessionFromData(d.sessions, d.stats); + if (d.graph) renderSessionGraph(d.graph); + } catch(err) {} + }); + eventSource.addEventListener('skills', function(e) { + try { lastSSE.skills = Date.now(); renderSkillsFromData(JSON.parse(e.data)); } catch(err) {} + }); + eventSource.addEventListener('dev', function(e) { + try { lastSSE.dev = Date.now(); renderDevFromData(JSON.parse(e.data)); } catch(err) {} + }); + eventSource.addEventListener('context', function(e) { + try { renderContextFromData(JSON.parse(e.data)); } catch(err) {} + }); + eventSource.addEventListener('prompt', function(e) { + try { + var d = JSON.parse(e.data); + var view = document.getElementById('system-prompt-view'); + if (view && view.style.display !== 'none') { + view.textContent = d.prompt || '(empty)'; + } + } catch(err) {} + }); + eventSource.onerror = function() { + // Browser will auto-reconnect EventSource + }; +} + +connectSSE(); + +// Initial load (fallback for tabs not covered by initial SSE burst) +loadPlan(); + +// ── Logs WebSocket ── +var logsWs = null; +var logsComponent = ''; +var logsEntries = []; +var logsReconnectTimer = null; +var logsPage = 1; +var LOGS_PAGE_SIZE = 60; + +function connectLogsWs() { + if (logsWs && logsWs.readyState <= 1) return; + var wsProto = (location.protocol === 'https:') ? 'wss:' : 'ws:'; + var wsUrl = wsProto + '//' + location.host + '/miniapp/api/logs/ws?initData=' + + encodeURIComponent(initData); + if (logsComponent) wsUrl += '&component=' + encodeURIComponent(logsComponent); + + logsWs = new WebSocket(wsUrl); + var statusDot = document.getElementById('logs-status'); + + logsWs.onopen = function() { + statusDot.classList.add('on'); + }; + + logsWs.onmessage = function(e) { + var msg = JSON.parse(e.data); + if (msg.type === 'init') { + logsEntries = msg.entries || []; + logsPage = 1; + } else if (msg.type === 'entry') { + logsEntries.push(msg.entry); + if (logsEntries.length > 200) logsEntries.shift(); + } + renderLogs(); + }; + + logsWs.onclose = function() { + statusDot.classList.remove('on'); + logsWs = null; + // Auto-reconnect if Config tab is active + var activeTab = document.querySelector('.tab.active'); + if (activeTab && activeTab.dataset.panel === 'config') { + logsReconnectTimer = setTimeout(connectLogsWs, 3000); + } + }; + + logsWs.onerror = function() { + // onclose will handle reconnect + }; +} + +function disconnectLogsWs() { + if (logsReconnectTimer) { clearTimeout(logsReconnectTimer); logsReconnectTimer = null; } + if (logsWs) { logsWs.close(); logsWs = null; } + document.getElementById('logs-status').classList.remove('on'); +} + +function renderLogs() { + var container = document.getElementById('logs-content'); + if (!container) return; + var wasScrolledToBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 30; + + var view = renderLogsView(logsEntries, { + component: logsComponent, + page: logsPage, + pageSize: LOGS_PAGE_SIZE, + }); + if (logsPage > view.totalPages) { + logsPage = view.totalPages; + view = renderLogsView(logsEntries, { + component: logsComponent, + page: logsPage, + pageSize: LOGS_PAGE_SIZE, + }); + } + + if (!view.html) { + container.innerHTML = '
    No logs.
    '; + } else { + container.innerHTML = view.html; + } + updateLogsPager(view); + if (wasScrolledToBottom) container.scrollTop = container.scrollHeight; +} + +function updateLogsPager(view) { + var info = document.getElementById('logs-page-info'); + var prev = document.getElementById('logs-page-prev'); + var next = document.getElementById('logs-page-next'); + if (!info || !prev || !next) return; + + info.textContent = view.currentPage + '/' + view.totalPages + ' (' + view.totalItems + ')'; + prev.disabled = view.currentPage <= 1; + next.disabled = view.currentPage >= view.totalPages; +} +// Filter chip click +document.querySelector('.log-filter-chips').addEventListener('click', function(e) { + var chip = e.target.closest('.log-filter-chip'); + if (!chip) return; + document.querySelectorAll('.log-filter-chip').forEach(function(c) { c.classList.remove('active'); }); + chip.classList.add('active'); + logsComponent = chip.dataset.component || ''; + logsPage = 1; + logsEntries = []; + renderLogs(); + disconnectLogsWs(); + connectLogsWs(); +}); + +var logsPrevButton = document.getElementById('logs-page-prev'); +if (logsPrevButton) { + logsPrevButton.addEventListener('click', function() { + if (logsPage <= 1) return; + logsPage--; + renderLogs(); + }); +} + +var logsNextButton = document.getElementById('logs-page-next'); +if (logsNextButton) { + logsNextButton.addEventListener('click', function() { + logsPage++; + renderLogs(); + }); +} + + +// ─── Orchestration room ─────────────────────────────────────────────────── +var orchCanvas = null, orchCtx = null, orchInited = false; +var orchWs = null, orchReconnectTimer = null; +var _orchLastTs = null; +var _orchBOB = [0, -1, -2, -1]; +var _orchFRAME_MS = {idle:450, waiting:650, toolcall:90, talking:280, entering:220, exiting:220}; +var _orchWALK = 55; +var _orchConductor, _orchSecretary, _orchHeartbeat, _orchSubagents, _orchSlots, _orchFreeSlots; + +function _orchMakeChar(id, emoji, home) { + return {id:id, emoji:emoji, x:home.x, y:home.y, home:home, target:null, state:'idle', + frame:0, frameTimer:0, bubble:null, alive:false, _onArrive:null}; +} +function _orchInitChars() { + _orchConductor = _orchMakeChar('conductor', '👑', MAP_POSITIONS.conductor); + _orchSecretary = _orchMakeChar('secretary', '👩‍💼', MAP_POSITIONS.secretary); + _orchHeartbeat = _orchMakeChar('heartbeat', '🕊️', MAP_POSITIONS.heartbeat || {x:230,y:58}); + _orchConductor.alive = true; _orchSecretary.alive = false; _orchHeartbeat.alive = true; + _orchConductor.statusText = null; + _orchHeartbeat.facing = 1; _orchHeartbeat.flipTimer = 0; + var ps = [{id:'s0',emoji:'🔍'},{id:'s1',emoji:'📊'},{id:'s2',emoji:'💻'}, + {id:'s3',emoji:'🔧'},{id:'s4',emoji:'🎯'}]; + _orchSubagents = ps.map(function(p,i){ + var c = _orchMakeChar(p.id, p.emoji, MAP_POSITIONS.stations[i]); + c.x = MAP_POSITIONS.door.x; c.y = MAP_POSITIONS.door.y; return c; + }); + _orchSlots = {}; _orchFreeSlots = _orchSubagents.slice(); +} +function _orchAllChars() { return [_orchConductor, _orchSecretary, _orchHeartbeat].concat(_orchSubagents); } + +function _orchSyncBadge(id, state, alive) { + var el = document.getElementById('orch-badge-' + id); if (!el) return; + el.className = 'orch-badge' + + (alive ? ' alive' : '') + + (state==='talking' ? ' talking' : '') + + (state==='toolcall'? ' toolcall' : '') + + (state==='waiting' ? ' waiting' : ''); +} +function _orchSetState(c, state, tool) { + c.state=state; _orchSyncBadge(c.id, state, c.alive); + if (c === _orchConductor) { + if (state==='waiting') c.statusText='🤔'; + else if (state==='toolcall') c.statusText='⌨'; + else if (state==='user_waiting') c.statusText='⏳'; + else if (state==='plan_interviewing') c.statusText='📋'; + else if (state==='plan_review') c.statusText='🔍'; + else if (state==='plan_executing') c.statusText='▶️'; + else if (state==='plan_completed') c.statusText='✅'; + else c.statusText=null; + // Secretary appears only during plan mode. + var inPlan = state.indexOf('plan_')===0; + if (_orchSecretary.alive !== inPlan) { + _orchSecretary.alive = inPlan; + _orchSyncBadge('secretary', _orchSecretary.state, _orchSecretary.alive); + } + } +} +function _orchMoveTo(c, pos, cb) { c.target=pos; c._onArrive=cb||null; } +function _orchSay(c, text, ttl) { c.bubble={text:text, ttl:ttl||2200}; } + +function _orchCharForId(id) { + if (id === 'heartbeat') return _orchHeartbeat; + if (_orchSlots[id]) return _orchSlots[id]; + return _orchConductor; +} +function _orchSpawn(id) { + if (/^subagent-/.test(id)) { + var c = _orchFreeSlots.shift(); if (!c) return; + _orchSlots[id]=c; c.alive=true; + c.x=MAP_POSITIONS.door.x; c.y=MAP_POSITIONS.door.y; + _orchSetState(c,'entering'); + _orchMoveTo(c, c.home, function(){ _orchSetState(c,'idle'); }); + } else { + var ch = _orchCharForId(id); ch.alive=true; _orchSetState(ch,'waiting'); + } +} +function _orchGC(id) { + if (/^subagent-/.test(id)) { + var c=_orchSlots[id]; if (!c) return; + delete _orchSlots[id]; _orchFreeSlots.push(c); + _orchSetState(c,'exiting'); + _orchMoveTo(c, MAP_POSITIONS.door, function(){ c.alive=false; _orchSetState(c,'idle'); }); + } else { + var ch=_orchCharForId(id); + if (ch === _orchHeartbeat) { + // Heartbeat pigeon is permanent — keep alive, just return to idle. + _orchSetState(ch,'idle'); + } else if (ch === _orchConductor) { + // Conductor is permanent — keep alive, show ⏳ waiting for user. + _orchSetState(ch,'user_waiting'); + } else { + ch.alive=false; _orchSetState(ch,'idle'); + } + } +} +function _orchConverse(fromId, toId, text) { + var from=_orchCharForId(fromId), to=_orchCharForId(toId); + if (!from||!to||from===to) return; + var label=(text||'').slice(0,18); + var mid={x:(from.x+to.x)/2, y:(from.y+to.y)/2}; + _orchSetState(from,'talking'); _orchSetState(to,'talking'); + _orchMoveTo(from, {x:mid.x-18,y:mid.y}, function(){ _orchSay(from,label,2400); }); + _orchMoveTo(to, {x:mid.x+18,y:mid.y}, function(){ + setTimeout(function(){ + _orchMoveTo(from, from.home, function(){ _orchSetState(from,'idle'); }); + _orchMoveTo(to, to.home, function(){ _orchSetState(to, 'idle'); }); + }, 2600); + }); +} +function _orchUpdate(dt) { + _orchAllChars().forEach(function(c){ + if (!c.alive && c.state!=='entering') return; + // Frame animation (bob): pigeon uses state-specific timing instead of shared table. + if (c === _orchHeartbeat) { + if (c.state === 'idle') { + c.frame = 0; // pin still — no bob when inactive + } else { + c.frameTimer += dt; + var pDur = c.state==='toolcall' ? 130 : 380; + if (c.frameTimer >= pDur) { c.frame=(c.frame+1)%4; c.frameTimer-=pDur; } + } + } else { + c.frameTimer+=dt; + var dur=_orchFRAME_MS[c.state]||450; + if (c.frameTimer>=dur){ c.frame=(c.frame+1)%4; c.frameTimer-=dur; } + } + if (c.target){ + var dx=c.target.x-c.x, dy=c.target.y-c.y, dist=Math.sqrt(dx*dx+dy*dy); + if (dist>1.5){ var spd=_orchWALK*dt/1000; c.x+=dx/dist*spd; c.y+=dy/dist*spd; } + else { c.x=c.target.x; c.y=c.target.y; c.target=null; if(c._onArrive){c._onArrive();c._onArrive=null;} } + } + if (c.bubble){ c.bubble.ttl-=dt; if(c.bubble.ttl<=0) c.bubble=null; } + // Heartbeat pigeon: direction flip rate reflects activity level. + if (c === _orchHeartbeat) { + if (c.target) { + var pdx = c.target.x - c.x; + if (Math.abs(pdx) > 1) c.facing = pdx > 0 ? 1 : -1; + } else { + var flipRate = c.state==='toolcall' ? 280 : c.state==='waiting' ? 600 : 2800; + c.flipTimer += dt; + if (c.flipTimer >= flipRate) { c.flipTimer -= flipRate; c.facing = -c.facing; } + } + } + }); +} +function _orchDrawStatus(c) { + if (!c.statusText) return; + var yOff=_orchBOB[c.frame], cx=Math.floor(c.x), cy=Math.floor(c.y+yOff)-20; + orchCtx.font='11px serif'; orchCtx.textAlign='center'; orchCtx.textBaseline='middle'; + orchCtx.fillText(c.statusText, cx, cy); +} +function _orchDrawBubble(c) { + if (!c.bubble) return; + var yOff=_orchBOB[c.frame], bx=c.x, by=c.y+yOff-18; + orchCtx.font='7px Silkscreen,monospace'; + var tw=orchCtx.measureText(c.bubble.text).width, pw=tw+8, ph=12; + var lx=Math.max(4,Math.min(316-pw, bx-pw/2)); + orchCtx.fillStyle='#facc15'; + orchCtx.fillRect(Math.floor(lx),Math.floor(by-ph),Math.ceil(pw),Math.ceil(ph)); + orchCtx.fillRect(Math.floor(bx)-1,Math.floor(by),3,3); + orchCtx.fillStyle='#0a0a00'; orchCtx.textAlign='left'; orchCtx.textBaseline='middle'; + orchCtx.fillText(c.bubble.text, Math.floor(lx+4), Math.floor(by-ph/2)); +} +function _orchDrawChar(c) { + if (!c.alive && c.state!=='entering' && c.state!=='exiting') return; + var yOff=_orchBOB[c.frame], cx=Math.floor(c.x), cy=Math.floor(c.y+yOff); + if (c.state==='toolcall'){ + orchCtx.fillStyle='rgba(251,146,60,0.35)'; orchCtx.beginPath(); + orchCtx.arc(cx,cy,13,0,Math.PI*2); orchCtx.fill(); + } else if (c.state==='waiting'){ + orchCtx.fillStyle='rgba(96,165,250,0.25)'; orchCtx.beginPath(); + orchCtx.arc(cx,cy,11,0,Math.PI*2); orchCtx.fill(); + } else if (c.state==='user_waiting' || c.state==='plan_review'){ + orchCtx.fillStyle='rgba(167,139,250,0.18)'; orchCtx.beginPath(); + orchCtx.arc(cx,cy,10,0,Math.PI*2); orchCtx.fill(); + } else if (c.state==='plan_executing'){ + orchCtx.fillStyle='rgba(74,222,128,0.18)'; orchCtx.beginPath(); + orchCtx.arc(cx,cy,10,0,Math.PI*2); orchCtx.fill(); + } + orchCtx.font='18px serif'; orchCtx.textAlign='center'; orchCtx.textBaseline='middle'; + if (c.facing === -1) { + orchCtx.save(); + orchCtx.translate(cx, cy); orchCtx.scale(-1, 1); + orchCtx.fillText(c.emoji, 0, 0); + orchCtx.restore(); + } else { + orchCtx.fillText(c.emoji, cx, cy); + } + orchCtx.font='6px Silkscreen,monospace'; orchCtx.textAlign='center'; orchCtx.textBaseline='top'; + orchCtx.fillStyle=c.state==='talking'?'#facc15':'#3a4a7a'; + orchCtx.fillText(c.id.toUpperCase(), cx, cy+11); + _orchDrawStatus(c); + _orchDrawBubble(c); +} +function _orchRender(ts) { + if (_orchLastTs===null) _orchLastTs=ts; + var dt=Math.min(ts-_orchLastTs,80); _orchLastTs=ts; + _orchUpdate(dt); + orchCtx.imageSmoothingEnabled=false; + drawMap(orchCtx); + _orchAllChars().forEach(_orchDrawChar); + requestAnimationFrame(_orchRender); +} +function orchInit() { + if (orchInited) return; orchInited=true; + orchCanvas=document.getElementById('orch-canvas'); + orchCtx=orchCanvas.getContext('2d'); + orchCtx.imageSmoothingEnabled=false; + _orchInitChars(); + loadMapAsset(function(){ _orchLastTs=null; requestAnimationFrame(_orchRender); }); +} +function connectOrchWs() { + orchInit(); + if (orchWs && orchWs.readyState<=1) return; + var proto=location.protocol==='https:'?'wss:':'ws:'; + var url=proto+'//'+location.host+'/miniapp/api/orchestration/ws?initData='+encodeURIComponent(initData); + orchWs=new WebSocket(url); + orchWs.onopen=function(){ + document.getElementById('orch-status-dot').classList.add('on'); + document.getElementById('orch-status-text').textContent='Live'; + }; + orchWs.onmessage=function(e){ + var msg; try{ msg=JSON.parse(e.data); }catch(_){ return; } + if (msg.type==='init') { + (msg.agents||[]).forEach(function(info){ + _orchSpawn(info.id); + if (info.state && info.state!=='idle'){ + var c=_orchCharForId(info.id); if(c) _orchSetState(c, info.state); + } + }); + } else if (msg.type==='event') { + var ev=msg.event||{}; + if (ev.type==='agent_spawn') _orchSpawn(ev.id); + if (ev.type==='agent_state') { var c=_orchCharForId(ev.id); if(c) _orchSetState(c,ev.state,ev.tool); } + if (ev.type==='agent_gc') _orchGC(ev.id); + if (ev.type==='conversation') _orchConverse(ev.from, ev.to, ev.text); + } + }; + orchWs.onclose=function(){ + document.getElementById('orch-status-dot').classList.remove('on'); + document.getElementById('orch-status-text').textContent='Disconnected'; + orchWs=null; + var at=document.querySelector('.tab.active'); + if (at && at.dataset.panel==='orch') orchReconnectTimer=setTimeout(connectOrchWs,3000); + }; + orchWs.onerror=function(){}; +} +function disconnectOrchWs() { + if (orchReconnectTimer){ clearTimeout(orchReconnectTimer); orchReconnectTimer=null; } + if (orchWs){ orchWs.close(); orchWs=null; } + var dot=document.getElementById('orch-status-dot'); + var txt=document.getElementById('orch-status-text'); + if (dot) dot.classList.remove('on'); + if (txt) txt.textContent='Offline'; +} + +async function saveLogSnapshot() { + try { + var res = await fetch(API_BASE + '/miniapp/api/logs/snapshot?initData=' + encodeURIComponent(initData), { + method: 'POST' + }); + if (!res.ok) throw new Error('API error: ' + res.status); + var data = await res.json(); + if (data.download_url) { + var a = document.createElement('a'); + a.href = API_BASE + data.download_url + '?initData=' + encodeURIComponent(initData); + a.download = ''; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + } catch(e) { + // silent fail + } +} + + +window.sendCustomCmd = sendCustomCmd; +window.sendSkillCommand = sendSkillCommand; +window.startPlan = startPlan; +window.toggleSystemPrompt = toggleSystemPrompt; +window.loadGit = loadGit; +window.saveLogSnapshot = saveLogSnapshot; + + + diff --git a/pkg/miniapp/frontend/src/logs_view.js b/pkg/miniapp/frontend/src/logs_view.js new file mode 100644 index 000000000..73e42a019 --- /dev/null +++ b/pkg/miniapp/frontend/src/logs_view.js @@ -0,0 +1,106 @@ +const SAFE_LEVELS = new Set(['debug', 'info', 'warn', 'error']); + +function stringifyFieldValue(value) { + if (value === null || value === undefined) { + return ''; + } + if (typeof value === 'string') { + return value; + } + if (typeof value === 'number' || typeof value === 'boolean') { + return String(value); + } + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +export function escapeHtml(value) { + return String(value == null ? '' : value) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +export function renderFields(fields) { + if (!fields || typeof fields !== 'object' || Array.isArray(fields)) { + return ''; + } + const keys = Object.keys(fields); + if (keys.length === 0) { + return ''; + } + + const parts = keys.map((key) => `${key}=${stringifyFieldValue(fields[key])}`); + return ` {${escapeHtml(parts.join(', '))}}`; +} + +export function filterLogs(entries, component = '') { + if (!Array.isArray(entries) || entries.length === 0) { + return []; + } + if (!component) { + return entries.slice(); + } + return entries.filter((entry) => (entry?.component || '') === component); +} + +export function paginateLogs(entries, page = 1, pageSize = 100) { + const list = Array.isArray(entries) ? entries : []; + const size = Math.max(1, Number(pageSize) || 100); + const totalPages = Math.max(1, Math.ceil(list.length / size)); + const currentPage = Math.min(Math.max(1, Number(page) || 1), totalPages); + + // Page 1 is the newest page (tail of the list). + const end = list.length - (currentPage - 1) * size; + const start = Math.max(0, end - size); + return { + items: list.slice(start, Math.max(start, end)), + currentPage, + totalPages, + pageSize: size, + }; +} + +export function renderLogs(entries, options = {}) { + const component = options.component || ''; + const filtered = filterLogs(entries, component); + const paged = paginateLogs(filtered, options.page, options.pageSize); + + let html = ''; + for (const entry of paged.items) { + const levelRaw = String(entry?.level || 'info').toLowerCase(); + const level = SAFE_LEVELS.has(levelRaw) ? levelRaw : 'info'; + const ts = entry?.timestamp ? String(entry.timestamp).substring(11, 19) : ''; + const componentHTML = entry?.component + ? `${escapeHtml(entry.component)}` + : ''; + const fieldsHTML = renderFields(entry?.fields); + const message = escapeHtml(entry?.message || ''); + + html += '
    ' + + `${ts}` + + `${level}` + + componentHTML + + `${message}${fieldsHTML}` + + '
    '; + } + + return { + html, + totalItems: filtered.length, + currentPage: paged.currentPage, + totalPages: paged.totalPages, + pageSize: paged.pageSize, + }; +} + +export function renderLogsInto(container, entries, options = {}) { + const view = renderLogs(entries, options); + container.innerHTML = view.html; + return view; +} diff --git a/pkg/miniapp/frontend/src/logs_view.test.js b/pkg/miniapp/frontend/src/logs_view.test.js new file mode 100644 index 000000000..c6a485603 --- /dev/null +++ b/pkg/miniapp/frontend/src/logs_view.test.js @@ -0,0 +1,70 @@ +import { describe, expect, it } from 'vitest'; + +import { escapeHtml, filterLogs, paginateLogs, renderFields, renderLogs, renderLogsInto } from './logs_view.js'; + +describe('logs_view', () => { + it('renders a normal log message row', () => { + const view = renderLogs([ + { + timestamp: '2026-03-05T12:34:56Z', + level: 'INFO', + component: 'telego', + message: 'connected', + }, + ]); + + expect(view.totalItems).toBe(1); + expect(view.html).toContain('12:34:56'); + expect(view.html).toContain('log-badge info'); + expect(view.html).toContain('connected'); + expect(view.html).toContain('telego'); + }); + + it('renders fields for empty/single/multiple cases', () => { + expect(renderFields(null)).toBe(''); + expect(renderFields({})).toBe(''); + expect(renderFields({ req_id: 42 })).toContain('{req_id=42}'); + + const multi = renderFields({ a: 'x', b: 2 }); + expect(multi).toContain('a=x'); + expect(multi).toContain('b=2'); + }); + + it('sanitizes potentially dangerous HTML', () => { + const xss = ''; + const escaped = escapeHtml(xss); + expect(escaped).not.toContain(' { + const entries = []; + for (let i = 0; i < 25; i++) { + entries.push({ + level: 'debug', + component: i % 2 === 0 ? 'telego' : 'dev-console', + message: `entry-${i}`, + }); + } + + const filtered = filterLogs(entries, 'telego'); + expect(filtered.length).toBe(13); + + const pageInfo = paginateLogs(filtered, 2, 5); + expect(pageInfo.totalPages).toBe(3); + expect(pageInfo.items.length).toBe(5); + + const page1 = renderLogs(entries, { component: 'telego', page: 1, pageSize: 5 }); + const page2 = renderLogs(entries, { component: 'telego', page: 2, pageSize: 5 }); + expect(page1.totalPages).toBe(3); + expect(page1.html).not.toBe(page2.html); + expect(page1.html).toContain('entry-24'); + expect(page2.html).toContain('entry-14'); + }); +}); diff --git a/pkg/miniapp/static/map.js b/pkg/miniapp/frontend/src/map.js similarity index 97% rename from pkg/miniapp/static/map.js rename to pkg/miniapp/frontend/src/map.js index b16f72730..3aad21712 100644 --- a/pkg/miniapp/static/map.js +++ b/pkg/miniapp/frontend/src/map.js @@ -203,3 +203,10 @@ function _drawMapFallback(ctx) { _r(ctx, '#4a2408', 162, 284, 12, 16); _r(ctx, _C.doorGold, 170, 291, 5, 5); // handle } + +// Expose map helpers for app.js runtime. +globalThis.MAP_POSITIONS = MAP_POSITIONS; +globalThis.loadMapAsset = loadMapAsset; +globalThis.drawMap = drawMap; + + diff --git a/pkg/miniapp/frontend/src/styles.css b/pkg/miniapp/frontend/src/styles.css new file mode 100644 index 000000000..538f1715d --- /dev/null +++ b/pkg/miniapp/frontend/src/styles.css @@ -0,0 +1,912 @@ +:root { + --bg: var(--tg-theme-bg-color, #ffffff); + --text: var(--tg-theme-text-color, #000000); + --hint: var(--tg-theme-hint-color, #999999); + --link: var(--tg-theme-link-color, #2481cc); + --btn: var(--tg-theme-button-color, #2481cc); + --btn-text: var(--tg-theme-button-text-color, #ffffff); + --secondary-bg: var(--tg-theme-secondary-bg-color, #f0f0f0); + --done: #34c759; + --current: var(--btn); + --pending-phase: var(--hint); + /* Liquid Glass */ + --glass-bg: rgba(255, 255, 255, 0.55); + --glass-border: rgba(0, 0, 0, 0.08); + --glass-border-interactive: rgba(0, 0, 0, 0.15); + --glass-shadow: rgba(0, 0, 0, 0.06); + --glass-divider: rgba(0, 0, 0, 0.06); + --tab-bar-bg: rgba(255, 255, 255, 0.72); + --tab-track-bg: rgba(0, 0, 0, 0.06); + --tab-pill-bg: rgba(255, 255, 255, 0.9); + } + + @media (prefers-color-scheme: dark) { + :root { + --bg: var(--tg-theme-bg-color, #1c1c1e); + --text: var(--tg-theme-text-color, #ffffff); + --hint: var(--tg-theme-hint-color, #8e8e93); + --link: var(--tg-theme-link-color, #5ac8fa); + --btn: var(--tg-theme-button-color, #5ac8fa); + --btn-text: var(--tg-theme-button-text-color, #ffffff); + --secondary-bg: var(--tg-theme-secondary-bg-color, #2c2c2e); + /* Liquid Glass — dark */ + --glass-bg: rgba(255, 255, 255, 0.08); + --glass-border: rgba(255, 255, 255, 0.1); + --glass-border-interactive: rgba(255, 255, 255, 0.22); + --glass-shadow: rgba(0, 0, 0, 0.2); + --glass-divider: rgba(255, 255, 255, 0.08); + --tab-bar-bg: rgba(28, 28, 30, 0.72); + --tab-track-bg: rgba(255, 255, 255, 0.1); + --tab-pill-bg: rgba(255, 255, 255, 0.16); + } + } + + * { box-sizing: border-box; margin: 0; padding: 0; } + + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: var(--bg); + color: var(--text); + font-size: 14px; + padding-bottom: 80px; + } + + .tabs { + display: flex; + position: sticky; + top: 0; + z-index: 10; + padding: 8px 12px; + background: var(--tab-bar-bg); + -webkit-backdrop-filter: saturate(180%) blur(20px); + backdrop-filter: saturate(180%) blur(20px); + } + + .tabs-inner { + display: flex; + position: relative; + width: 100%; + background: var(--tab-track-bg); + border-radius: 10px; + padding: 2px; + } + + .tab-indicator { + position: absolute; + top: 2px; + bottom: 2px; + left: 2px; + width: calc(100% / var(--tab-count, 7) - 2px); + border-radius: 8px; + background: var(--tab-pill-bg); + box-shadow: 0 0.5px 2px rgba(0,0,0,0.12), 0 0.5px 1px rgba(0,0,0,0.08); + transition: transform 0.38s cubic-bezier(0.25, 1, 0.5, 1); + z-index: 0; + } + + .tab { + flex: 1; + padding: 7px 4px; + text-align: center; + font-size: 13px; + font-weight: 500; + color: var(--hint); + border: none; + background: none; + cursor: pointer; + transition: color 0.25s ease; + position: relative; + z-index: 1; + -webkit-tap-highlight-color: transparent; + } + + .tab.active { + color: var(--text); + font-weight: 600; + } + + .hidden { display: none !important; } + + .glass { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + border-radius: 16px; + box-shadow: 0 1px 3px var(--glass-shadow); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); + } + .glass-interactive { border-color: var(--glass-border-interactive); } + + .panel { display: none; padding: 16px; } + .panel.active { display: block; } + + .card { + padding: 16px; + margin-bottom: 12px; + } + + .card-title { + font-size: 12px; + color: var(--hint); + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.6px; + font-weight: 600; + } + + .card-value { + font-size: 22px; + font-weight: 700; + } + + /* Plan - phase/step list */ + .phase { + margin-bottom: 16px; + } + + .phase-header { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 0 8px; + font-weight: 600; + font-size: 15px; + } + + .phase-indicator { + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 700; + color: #fff; + flex-shrink: 0; + } + + .phase-indicator.done { background: var(--done); } + .phase-indicator.current { background: var(--current); } + .phase-indicator.pending { background: var(--glass-divider); color: var(--hint); } + + .phase-title { flex: 1; } + .phase-progress { + font-size: 12px; + color: var(--hint); + font-weight: 500; + } + + .step { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px 12px 34px; + margin-bottom: 6px; + border-radius: 12px; + cursor: pointer; + transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1), box-shadow 0.2s; + -webkit-tap-highlight-color: transparent; + } + + .step:active { transform: scale(0.97); } + .step:not(.step-done) { + background: var(--glass-bg); + border: 1px solid var(--glass-border-interactive); + box-shadow: 0 0.5px 2px var(--glass-shadow); + } + + .step-check { + width: 22px; + height: 22px; + border-radius: 50%; + border: 2px solid var(--hint); + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + margin-top: 0; + transition: all 0.2s; + } + + .step-check.done { + background: var(--done); + border-color: var(--done); + } + + .step-check.done::after { + content: ''; + width: 6px; + height: 10px; + border: solid #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg); + margin-top: -2px; + } + + .step-text { + flex: 1; + font-size: 14px; + line-height: 1.4; + } + + .step-text.done { + color: var(--hint); + text-decoration: line-through; + } + + /* Skills */ + .skill-item { + padding: 14px 14px 14px 16px; + margin-bottom: 10px; + cursor: pointer; + transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1), box-shadow 0.2s, border-color 0.2s; + display: flex; + align-items: center; + gap: 12px; + -webkit-tap-highlight-color: transparent; + } + + .skill-item:active { + transform: scale(0.98); + } + .skill-item.selected { + border-color: var(--btn); + box-shadow: 0 2px 8px var(--glass-shadow); + } + + .skill-body { flex: 1; min-width: 0; } + + .skill-name { + font-weight: 600; + font-size: 15px; + margin-bottom: 4px; + } + + .skill-desc { + font-size: 13px; + color: var(--hint); + line-height: 1.4; + } + + .skill-source { + display: inline-block; + font-size: 11px; + padding: 3px 10px; + border-radius: 20px; + background: var(--tab-track-bg); + color: var(--hint); + margin-top: 6px; + font-weight: 500; + } + + .skill-arrow { + color: var(--hint); + font-size: 22px; + flex-shrink: 0; + transition: color 0.2s; + } + .skill-item.selected .skill-arrow { color: var(--btn); } + + /* Stats */ + .stat-row { + display: flex; + justify-content: space-between; + padding: 11px 0; + border-bottom: 1px solid var(--glass-divider); + } + + .stat-row:last-child { border-bottom: none; } + .stat-label { color: var(--hint); font-size: 14px; } + .stat-value { font-weight: 600; font-size: 14px; } + + /* Send bar */ + .send-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: var(--tab-bar-bg); + -webkit-backdrop-filter: saturate(180%) blur(20px); + backdrop-filter: saturate(180%) blur(20px); + padding: 12px 16px; + display: flex; + gap: 8px; + border-top: 1px solid var(--glass-divider); + } + + .send-input { + flex: 1; + padding: 10px 16px; + border-radius: 20px; + color: var(--text); + font-size: 14px; + outline: none; + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + transition: border-color 0.2s; + } + + .send-input:focus { border-color: var(--btn); } + + .send-btn { + padding: 10px 20px; + border-radius: 20px; + border: none; + background: var(--btn); + color: var(--btn-text); + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1), background 0.15s; + -webkit-tap-highlight-color: transparent; + } + + .send-btn:active { transform: scale(0.95); } + .send-btn:disabled { opacity: 0.5; } + .send-btn.sent { background: var(--done); } + + .empty-state { + text-align: center; + color: var(--hint); + padding: 48px 20px; + font-size: 15px; + } + + .loading { + text-align: center; + color: var(--hint); + padding: 48px 20px; + font-size: 15px; + } + + .cmd-tiles { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + } + + .cmd-tile { + padding: 16px 14px; + border-radius: 14px; + color: var(--text); + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1), background 0.15s; + text-align: center; + -webkit-tap-highlight-color: transparent; + } + + .cmd-tile:active { + transform: scale(0.96); + } + + .cmd-tile.sent { + background: var(--btn); + color: var(--btn-text); + border-color: var(--btn); + } + + /* MEMORY.md raw display */ + .memory-view { + font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; + font-size: 13px; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-word; + padding: 14px 16px; + } + + .memory-view .md-h1 { + font-size: 18px; + font-weight: 700; + margin: 16px 0 8px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + } + .memory-view .md-h1:first-child { margin-top: 0; } + + .memory-view .md-h2 { + font-size: 15px; + font-weight: 700; + margin: 14px 0 6px; + color: var(--link); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + } + + .memory-view .md-h3 { + font-size: 14px; + font-weight: 600; + margin: 12px 0 4px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + } + + .memory-view .md-checkbox { + margin: 2px 0; + } + + .memory-view .md-checkbox-icon { + display: inline-block; + width: 16px; + height: 16px; + border-radius: 4px; + border: 1.5px solid var(--hint); + vertical-align: middle; + margin-right: 6px; + position: relative; + top: -1px; + } + + .memory-view .md-checkbox-icon.checked { + background: var(--done); + border-color: var(--done); + } + + .memory-view .md-checkbox-icon.checked::after { + content: ''; + position: absolute; + left: 4px; + top: 1px; + width: 4px; + height: 8px; + border: solid #fff; + border-width: 0 1.5px 1.5px 0; + transform: rotate(45deg); + } + + .memory-view .md-quote { + border-left: 3px solid var(--hint); + padding-left: 10px; + color: var(--hint); + margin: 4px 0; + } + + .memory-view .md-bullet { + margin: 2px 0; + padding-left: 12px; + text-indent: -12px; + } + + .memory-view .md-bullet::before { + content: '\2022 '; + color: var(--hint); + } + + /* Slide to Approve */ + .slide-approve-wrap { + margin-top: 16px; + } + + .slide-approve-track { + position: relative; + height: 56px; + border-radius: 28px; + overflow: hidden; + touch-action: none; + border: 1.5px solid; + border-image: linear-gradient(135deg, var(--btn), var(--glass-border)) 1; + border-image: none; + border-color: var(--btn); + box-shadow: 0 2px 8px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.08); + transition: background 0.3s, border-color 0.3s, box-shadow 0.3s; + } + + .slide-approve-track.approved { + background: var(--done); + border-color: var(--done); + box-shadow: 0 0 16px rgba(76,175,80,0.4), 0 2px 8px rgba(0,0,0,0.18); + } + + .slide-approve-thumb { + position: absolute; + top: 3px; + left: 3px; + width: 50px; + height: 50px; + border-radius: 50%; + background: var(--btn); + color: var(--btn-text); + display: flex; + align-items: center; + justify-content: center; + cursor: grab; + transition: left 0.3s cubic-bezier(0.25, 1, 0.5, 1); + z-index: 1; + user-select: none; + -webkit-user-select: none; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); + } + + .slide-approve-thumb svg { + width: 22px; + height: 22px; + } + + .slide-approve-thumb.dragging { + transition: none; + cursor: grabbing; + } + + @keyframes shimmer { + 0%, 100% { opacity: 0.7; } + 50% { opacity: 1; } + } + + .slide-approve-label { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + color: var(--text); + font-size: 15px; + font-weight: 600; + pointer-events: none; + user-select: none; + -webkit-user-select: none; + transition: color 0.3s; + animation: shimmer 2.5s ease-in-out infinite; + } + + .slide-approve-track.approved .slide-approve-label { + color: #fff; + animation: none; + } + + /* Git log */ + .git-commit { + display: flex; + gap: 8px; + padding: 10px 12px; + border-bottom: 1px solid var(--glass-border); + font-size: 13px; + line-height: 1.4; + } + .git-commit:last-child { border-bottom: none; } + .git-hash { + font-family: monospace; + color: var(--btn); + flex-shrink: 0; + } + .git-subject { flex: 1; color: var(--text); } + .git-meta { color: var(--hint); font-size: 11px; flex-shrink: 0; text-align: right; } + .git-status { + font-family: monospace; + font-weight: 700; + font-size: 12px; + flex-shrink: 0; + width: 24px; + text-align: center; + } + .git-status-m { color: #e2b93d; } + .git-status-a { color: #4caf50; } + .git-status-d { color: #ef5350; } + .git-status-u { color: var(--hint); } + + .git-repo-item { + padding: 14px 14px 14px 16px; + margin-bottom: 10px; + cursor: pointer; + transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1), box-shadow 0.2s; + display: flex; + align-items: center; + gap: 12px; + -webkit-tap-highlight-color: transparent; + } + .git-repo-item:active { transform: scale(0.98); } + .git-repo-body { flex: 1; min-width: 0; } + .git-repo-name { font-weight: 600; font-size: 15px; margin-bottom: 4px; } + .git-repo-branch { + font-size: 13px; + color: var(--hint); + font-family: monospace; + } + .git-repo-arrow { + color: var(--hint); + font-size: 22px; + flex-shrink: 0; + } + .git-back-btn { + display: inline-flex; + align-items: center; + gap: 4px; + color: var(--btn); + font-size: 14px; + font-weight: 600; + background: none; + border: none; + cursor: pointer; + padding: 8px 0; + margin-bottom: 8px; + -webkit-tap-highlight-color: transparent; + } + .git-back-btn:active { opacity: 0.6; } + + .worktree-list { + margin-top: 8px; + display: flex; + flex-direction: column; + gap: 8px; + } + .worktree-item { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px; + border-radius: 12px; + border: 1px solid var(--glass-border); + background: var(--glass-bg); + } + .worktree-item.dirty { + border-color: rgba(255, 152, 0, 0.45); + } + .worktree-main { + flex: 1; + min-width: 0; + } + .worktree-name-row { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; + } + .worktree-name { + font-size: 14px; + font-weight: 600; + word-break: break-word; + } + .worktree-branch { + font-family: monospace; + font-size: 12px; + color: var(--hint); + margin-bottom: 3px; + } + .worktree-last { + font-size: 11px; + color: var(--hint); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .worktree-dirty, + .worktree-clean { + font-size: 10px; + font-weight: 700; + padding: 2px 6px; + border-radius: 999px; + } + .worktree-dirty { + color: #c26b00; + background: rgba(255, 152, 0, 0.2); + } + .worktree-clean { + color: #1b8f3a; + background: rgba(76, 175, 80, 0.18); + } + .worktree-actions { + display: flex; + flex-direction: column; + gap: 6px; + flex-shrink: 0; + } + .worktree-btn { + border: 1px solid var(--glass-border-interactive); + background: var(--glass-bg); + color: var(--text); + border-radius: 8px; + padding: 6px 10px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + min-width: 74px; + } + .worktree-btn.merge { color: var(--btn); } + .worktree-btn.dispose { color: #d14b4b; } + .worktree-btn:disabled { + opacity: 0.6; + cursor: default; + } + /* Dev header */ + .dev-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 4px; + margin-bottom: 8px; + } + .dev-header-title { font-weight: 600; font-size: 15px; } + .dev-header-target { + color: var(--hint); + font-size: 13px; + margin-left: auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + /* Dev target cards */ + .dev-target-item { + padding: 12px 14px; + margin-bottom: 8px; + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1), box-shadow 0.2s, border-color 0.2s; + -webkit-tap-highlight-color: transparent; + } + .dev-target-item:active { transform: scale(0.98); } + .dev-target-item.active { + border-color: var(--btn); + box-shadow: 0 2px 8px var(--glass-shadow); + } + .dev-target-dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; + background: var(--hint); + } + .dev-target-dot.on { background: var(--done); } + .dev-target-name { font-weight: 600; font-size: 14px; } + .dev-target-url { + color: var(--hint); + font-size: 13px; + margin-left: auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .dev-target-delete { + flex-shrink: 0; + width: 24px; + height: 24px; + padding: 6px; + margin: -6px; + margin-left: 8px; + border-radius: 50%; + color: var(--hint); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.15s, background 0.15s; + -webkit-tap-highlight-color: transparent; + } + .dev-target-delete:active { + color: #ff3b30; + background: rgba(255, 59, 48, 0.1); + } + + /* Log viewer */ + .log-filter-chips { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; } + .log-filter-chip { + padding: 4px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + border: 1px solid var(--glass-border-interactive); + background: var(--glass-bg); + color: var(--hint); + cursor: pointer; + transition: all 0.2s; + -webkit-tap-highlight-color: transparent; + } + .log-filter-chip.active { + background: var(--btn); + color: var(--btn-text); + border-color: var(--btn); + } + + #logs-content { + max-height: 50vh; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + .log-pagination { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-top: 8px; + color: var(--hint); + font-size: 11px; + } + .log-page-btn { + padding: 4px 10px; + border-radius: 10px; + font-size: 11px; + font-weight: 500; + border: 1px solid var(--glass-border-interactive); + background: var(--glass-bg); + color: var(--text); + cursor: pointer; + } + .log-page-btn:disabled { + opacity: 0.45; + cursor: default; + } + .log-entry { + display: flex; + align-items: flex-start; + gap: 6px; + padding: 3px 0; + font-size: 11px; + font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; + line-height: 1.4; + border-bottom: 1px solid var(--glass-divider); + } + .log-ts { color: var(--hint); flex-shrink: 0; white-space: nowrap; } + .log-badge { + flex-shrink: 0; + padding: 0 4px; + border-radius: 4px; + font-size: 9px; + font-weight: 700; + text-transform: uppercase; + line-height: 16px; + } + .log-badge.error { background: rgba(255,59,48,0.15); color: #ff3b30; } + .log-badge.warn { background: rgba(255,204,0,0.15); color: #cc9900; } + .log-badge.info { background: rgba(52,199,89,0.12); color: #34c759; } + .log-badge.debug { background: rgba(142,142,147,0.12); color: #8e8e93; } + .log-comp { color: var(--link); flex-shrink: 0; font-size: 10px; } + .log-msg { flex: 1; word-break: break-all; color: var(--text); } + .log-fields { color: var(--hint); font-size: 10px; } + .log-actions { display: flex; gap: 8px; align-items: center; margin-top: 8px; } + .log-snap-btn { + padding: 6px 12px; + border-radius: 10px; + font-size: 12px; + font-weight: 500; + background: var(--glass-bg); + border: 1px solid var(--glass-border-interactive); + color: var(--text); + cursor: pointer; + } + + /* ── Orch panel ── */ + .orch-room-row { display:flex; align-items:flex-start; padding:12px 0; } + .orch-side { display:flex; flex-direction:column; align-items:center; gap:12px; padding-top:20px; width:40px; flex-shrink:0; } + .orch-badge { display:flex; flex-direction:column; align-items:center; gap:3px; opacity:0.3; transition:opacity 0.3s; } + .orch-badge.alive { opacity:1; } + .orch-badge.alive .orch-badge-label { color:#4a6ac0; } + .orch-badge.alive .orch-badge-dot { background:#4ade80; } + .orch-badge.toolcall .orch-badge-dot { background:#fb923c; animation:orch-blink 0.2s step-end infinite; } + .orch-badge.waiting .orch-badge-dot { background:#60a5fa; } + .orch-badge.talking .orch-badge-label{ color:#facc15; } + .orch-badge.talking .orch-badge-dot { background:#facc15; animation:orch-blink 0.6s step-end infinite; } + .orch-badge-emoji { font-size:16px; line-height:1; } + .orch-badge-label { font-size:7px; color:var(--hint); letter-spacing:0.05em; text-transform:uppercase; } + .orch-badge-dot { width:4px; height:4px; border-radius:50%; background:var(--hint); } + @keyframes orch-blink { 50% { opacity:0; } } + .orch-canvas-wrap { flex:1; min-width:0; border-radius:12px; overflow:hidden; background:#060810; } + .orch-canvas-wrap canvas { image-rendering:pixelated; image-rendering:crisp-edges; display:block; width:100%; aspect-ratio:1/1; } + .orch-status { text-align:center; font-size:11px; color:var(--hint); padding:6px 0 4px; } + .orch-dot { display:inline-block; width:6px; height:6px; border-radius:50%; background:var(--hint); margin-right:4px; vertical-align:middle; } + .orch-dot.on { background:#4ade80; } + + /* Session graph tree */ + .session-tree { padding:0; margin:0; list-style:none; } + .session-tree-node { position:relative; padding:4px 0 4px 20px; font-size:13px; } + .session-tree-node::before { + content:''; position:absolute; left:0; top:0; bottom:0; + border-left:1px solid var(--secondary-bg); + } + .session-tree-node::after { + content:''; position:absolute; left:0; top:14px; width:16px; + border-top:1px solid var(--secondary-bg); + } + .session-tree-node:last-child::before { height:14px; } + .session-tree-children { padding-left:20px; margin:0; list-style:none; } + .session-tree-label { font-weight:600; } + .session-tree-meta { color:var(--hint); font-size:11px; margin-left:6px; } + .session-tree-icon { font-size:10px; margin-right:4px; } + .session-tree-icon.active { color:var(--done); } + .session-tree-icon.completed { color:var(--hint); } + + + diff --git a/pkg/miniapp/frontend/vitest.config.mjs b/pkg/miniapp/frontend/vitest.config.mjs new file mode 100644 index 000000000..67a3bd7ac --- /dev/null +++ b/pkg/miniapp/frontend/vitest.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'happy-dom', + include: ['src/**/*.test.js'], + }, +}); diff --git a/pkg/miniapp/miniapp.go b/pkg/miniapp/miniapp.go index f7e21fc80..f79e10cf0 100644 --- a/pkg/miniapp/miniapp.go +++ b/pkg/miniapp/miniapp.go @@ -2,18 +2,33 @@ package miniapp import ( "embed" + "html/template" + "io/fs" "net/http" "net/http/httputil" "net/url" - "strings" "sync" "github.com/sipeed/picoclaw/pkg/orch" ) -//go:embed static/index.html static/map.js +//go:generate bun run --cwd frontend build +//go:embed static var staticFS embed.FS +var ( + miniappStaticFS = mustMiniappStaticFS() + miniappTemplate = template.Must(template.ParseFS(staticFS, "static/index.html")) +) + +func mustMiniappStaticFS() fs.FS { + sub, err := fs.Sub(staticFS, "static") + if err != nil { + panic("miniapp: failed to create static sub filesystem: " + err.Error()) + } + return sub +} + // Handler serves the Mini App HTML and API endpoints. type Handler struct { provider DataProvider @@ -68,6 +83,8 @@ func (h *Handler) SetOrchBroadcaster(b *orch.Broadcaster) { // RegisterRoutes registers Mini App routes on the given mux. func (h *Handler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("/miniapp", h.serveIndex) + mux.HandleFunc("/miniapp/index.html", h.serveIndex) + mux.HandleFunc("/miniapp/", h.serveStatic) mux.HandleFunc("/miniapp/api/skills", h.requireAuth(h.apiSkills)) mux.HandleFunc("/miniapp/api/plan", h.requireAuth(h.apiPlan)) mux.HandleFunc("/miniapp/api/session", h.requireAuth(h.apiSession)) @@ -84,30 +101,27 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("/miniapp/api/logs/snapshot", h.requireAuth(h.apiLogsSnapshot)) mux.HandleFunc("/miniapp/api/logs/snapshot/", h.requireAuth(h.apiLogsSnapshotDownload)) mux.HandleFunc("/miniapp/api/orchestration/ws", h.requireAuth(h.wsOrchestration)) - mux.HandleFunc("/miniapp/map.js", h.serveMapJS) mux.HandleFunc("/miniapp/dev/console", h.apiDevConsole) mux.HandleFunc("/miniapp/dev/", h.serveDevProxy) } func (h *Handler) serveIndex(w http.ResponseWriter, r *http.Request) { - data, err := staticFS.ReadFile("static/index.html") - if err != nil { - http.Error(w, "not found", http.StatusNotFound) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + data := struct { + OrchEnabled bool + }{ + OrchEnabled: h.orchBroadcaster != nil, + } + if err := miniappTemplate.Execute(w, data); err != nil { + http.Error(w, "failed to render template", http.StatusInternalServerError) return } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - if h.orchBroadcaster != nil { - data = []byte(strings.Replace(string(data), "", "", 1)) - } - w.Write(data) } -func (h *Handler) serveMapJS(w http.ResponseWriter, r *http.Request) { - data, err := staticFS.ReadFile("static/map.js") - if err != nil { - http.Error(w, "not found", http.StatusNotFound) +func (h *Handler) serveStatic(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/miniapp/" { + h.serveIndex(w, r) return } - w.Header().Set("Content-Type", "application/javascript; charset=utf-8") - w.Write(data) + http.StripPrefix("/miniapp/", http.FileServer(http.FS(miniappStaticFS))).ServeHTTP(w, r) } diff --git a/pkg/miniapp/miniapp_test.go b/pkg/miniapp/miniapp_test.go index 368561980..86a07baee 100644 --- a/pkg/miniapp/miniapp_test.go +++ b/pkg/miniapp/miniapp_test.go @@ -23,6 +23,7 @@ import ( "time" gitpkg "github.com/sipeed/picoclaw/pkg/git" + "github.com/sipeed/picoclaw/pkg/orch" "github.com/sipeed/picoclaw/pkg/skills" "github.com/sipeed/picoclaw/pkg/stats" ) @@ -227,6 +228,60 @@ func testInitData() string { }, testBotToken) } +func TestMiniApp_IndexTemplateInjectsOrchFlag(t *testing.T) { + notifier := NewStateNotifier() + h := NewHandler(&mockDataProvider{}, &mockSender{}, testBotToken, notifier, nil, "") + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + w := httptest.NewRecorder() + mux.ServeHTTP(w, httptest.NewRequest("GET", "/miniapp", nil)) + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", w.Code) + } + if !strings.Contains(w.Body.String(), "window.ORCH_ENABLED = false;") { + t.Fatalf("expected ORCH_ENABLED=false in rendered template") + } + + h.SetOrchBroadcaster(orch.NewBroadcaster()) + w2 := httptest.NewRecorder() + mux.ServeHTTP(w2, httptest.NewRequest("GET", "/miniapp", nil)) + if w2.Code != http.StatusOK { + t.Fatalf("expected 200 with broadcaster, got %d", w2.Code) + } + if !strings.Contains(w2.Body.String(), "window.ORCH_ENABLED = true;") { + t.Fatalf("expected ORCH_ENABLED=true in rendered template") + } +} + +func TestMiniApp_StaticFileServerServesAssets(t *testing.T) { + notifier := NewStateNotifier() + h := NewHandler(&mockDataProvider{}, &mockSender{}, testBotToken, notifier, nil, "") + mux := http.NewServeMux() + h.RegisterRoutes(mux) + + tests := []struct { + path string + want string + }{ + {path: "/miniapp/map-preview.html", want: "Orchestration Room"}, + {path: "/miniapp/dist/map.js", want: "MAP_POSITIONS"}, + {path: "/miniapp/dist/app.js", want: "renderLogs"}, + } + + for _, tc := range tests { + t.Run(tc.path, func(t *testing.T) { + w := httptest.NewRecorder() + mux.ServeHTTP(w, httptest.NewRequest("GET", tc.path, nil)) + if w.Code != http.StatusOK { + t.Fatalf("expected 200 for %s, got %d", tc.path, w.Code) + } + if !strings.Contains(w.Body.String(), tc.want) { + t.Fatalf("expected %q in %s", tc.want, tc.path) + } + }) + } +} func TestSSE_AuthRequired(t *testing.T) { notifier := NewStateNotifier() h := NewHandler(&mockDataProvider{}, &mockSender{}, testBotToken, notifier, nil, "") diff --git a/pkg/miniapp/static/dist/app.css b/pkg/miniapp/static/dist/app.css new file mode 100644 index 000000000..b871d83f5 --- /dev/null +++ b/pkg/miniapp/static/dist/app.css @@ -0,0 +1,1230 @@ +/* src/styles.css */ +:root { + --bg: var(--tg-theme-bg-color, #fff); + --text: var(--tg-theme-text-color, #000); + --hint: var(--tg-theme-hint-color, #999); + --link: var(--tg-theme-link-color, #2481cc); + --btn: var(--tg-theme-button-color, #2481cc); + --btn-text: var(--tg-theme-button-text-color, #fff); + --secondary-bg: var(--tg-theme-secondary-bg-color, #f0f0f0); + --done: #34c759; + --current: var(--btn); + --pending-phase: var(--hint); + --glass-bg: #ffffff8c; + --glass-border: #00000014; + --glass-border-interactive: #00000026; + --glass-shadow: #0000000f; + --glass-divider: #0000000f; + --tab-bar-bg: #ffffffb8; + --tab-track-bg: #0000000f; + --tab-pill-bg: #ffffffe6; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: var(--tg-theme-bg-color, #1c1c1e); + --text: var(--tg-theme-text-color, #fff); + --hint: var(--tg-theme-hint-color, #8e8e93); + --link: var(--tg-theme-link-color, #5ac8fa); + --btn: var(--tg-theme-button-color, #5ac8fa); + --btn-text: var(--tg-theme-button-text-color, #fff); + --secondary-bg: var(--tg-theme-secondary-bg-color, #2c2c2e); + --glass-bg: #ffffff14; + --glass-border: #ffffff1a; + --glass-border-interactive: #ffffff38; + --glass-shadow: #0003; + --glass-divider: #ffffff14; + --tab-bar-bg: #1c1c1eb8; + --tab-track-bg: #ffffff1a; + --tab-pill-bg: #ffffff29; + } +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: var(--bg); + color: var(--text); + padding-bottom: 80px; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; + font-size: 14px; +} + +.tabs { + display: flex; + position: sticky; + z-index: 10; + background: var(--tab-bar-bg); + -webkit-backdrop-filter: saturate(180%) blur(20px); + backdrop-filter: saturate(180%) blur(20px); + padding: 8px 12px; + top: 0; +} + +.tabs-inner { + display: flex; + position: relative; + background: var(--tab-track-bg); + border-radius: 10px; + width: 100%; + padding: 2px; +} + +.tab-indicator { + position: absolute; + width: calc(100% / var(--tab-count, 7) - 2px); + background: var(--tab-pill-bg); + z-index: 0; + border-radius: 8px; + transition: transform .38s cubic-bezier(.25,1,.5,1); + top: 2px; + bottom: 2px; + left: 2px; + box-shadow: 0 .5px 2px #0000001f, 0 .5px 1px #00000014; +} + +.tab { + text-align: center; + color: var(--hint); + cursor: pointer; + position: relative; + z-index: 1; + -webkit-tap-highlight-color: transparent; + background: none; + border: none; + flex: 1; + padding: 7px 4px; + transition: color .25s; + font-size: 13px; + font-weight: 500; +} + +.tab.active { + color: var(--text); + font-weight: 600; +} + +.hidden { + display: none !important; +} + +.glass { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + box-shadow: 0 1px 3px var(--glass-shadow); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); + border-radius: 16px; +} + +.glass-interactive { + border-color: var(--glass-border-interactive); +} + +.panel { + display: none; + padding: 16px; +} + +.panel.active { + display: block; +} + +.card { + margin-bottom: 12px; + padding: 16px; +} + +.card-title { + color: var(--hint); + text-transform: uppercase; + letter-spacing: .6px; + margin-bottom: 8px; + font-size: 12px; + font-weight: 600; +} + +.card-value { + font-size: 22px; + font-weight: 700; +} + +.phase { + margin-bottom: 16px; +} + +.phase-header { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 0 8px; + font-size: 15px; + font-weight: 600; +} + +.phase-indicator { + display: flex; + color: #fff; + border-radius: 50%; + flex-shrink: 0; + justify-content: center; + align-items: center; + width: 24px; + height: 24px; + font-size: 11px; + font-weight: 700; +} + +.phase-indicator.done { + background: var(--done); +} + +.phase-indicator.current { + background: var(--current); +} + +.phase-indicator.pending { + background: var(--glass-divider); + color: var(--hint); +} + +.phase-title { + flex: 1; +} + +.phase-progress { + color: var(--hint); + font-size: 12px; + font-weight: 500; +} + +.step { + display: flex; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + border-radius: 12px; + align-items: flex-start; + gap: 10px; + margin-bottom: 6px; + padding: 12px 14px 12px 34px; + transition: transform .2s cubic-bezier(.25,1,.5,1), box-shadow .2s; +} + +.step:active { + transform: scale(.97); +} + +.step:not(.step-done) { + background: var(--glass-bg); + border: 1px solid var(--glass-border-interactive); + box-shadow: 0 .5px 2px var(--glass-shadow); +} + +.step-check { + border: 2px solid var(--hint); + display: flex; + border-radius: 50%; + flex-shrink: 0; + justify-content: center; + align-items: center; + width: 22px; + height: 22px; + margin-top: 0; + transition: all .2s; +} + +.step-check.done { + background: var(--done); + border-color: var(--done); +} + +.step-check.done:after { + content: ""; + border: 2px solid #fff; + border-width: 0 2px 2px 0; + width: 6px; + height: 10px; + margin-top: -2px; + transform: rotate(45deg); +} + +.step-text { + flex: 1; + font-size: 14px; + line-height: 1.4; +} + +.step-text.done { + color: var(--hint); + text-decoration: line-through; +} + +.skill-item { + cursor: pointer; + display: flex; + -webkit-tap-highlight-color: transparent; + align-items: center; + gap: 12px; + margin-bottom: 10px; + padding: 14px 14px 14px 16px; + transition: transform .2s cubic-bezier(.25,1,.5,1), box-shadow .2s, border-color .2s; +} + +.skill-item:active { + transform: scale(.98); +} + +.skill-item.selected { + border-color: var(--btn); + box-shadow: 0 2px 8px var(--glass-shadow); +} + +.skill-body { + flex: 1; + min-width: 0; +} + +.skill-name { + margin-bottom: 4px; + font-size: 15px; + font-weight: 600; +} + +.skill-desc { + color: var(--hint); + font-size: 13px; + line-height: 1.4; +} + +.skill-source { + display: inline-block; + background: var(--tab-track-bg); + color: var(--hint); + border-radius: 20px; + margin-top: 6px; + padding: 3px 10px; + font-size: 11px; + font-weight: 500; +} + +.skill-arrow { + color: var(--hint); + flex-shrink: 0; + transition: color .2s; + font-size: 22px; +} + +.skill-item.selected .skill-arrow { + color: var(--btn); +} + +.stat-row { + display: flex; + border-bottom: 1px solid var(--glass-divider); + justify-content: space-between; + padding: 11px 0; +} + +.stat-row:last-child { + border-bottom: none; +} + +.stat-label { + color: var(--hint); + font-size: 14px; +} + +.stat-value { + font-size: 14px; + font-weight: 600; +} + +.send-bar { + position: fixed; + background: var(--tab-bar-bg); + -webkit-backdrop-filter: saturate(180%) blur(20px); + backdrop-filter: saturate(180%) blur(20px); + display: flex; + border-top: 1px solid var(--glass-divider); + gap: 8px; + padding: 12px 16px; + bottom: 0; + left: 0; + right: 0; +} + +.send-input { + color: var(--text); + outline: none; + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + border-radius: 20px; + flex: 1; + padding: 10px 16px; + transition: border-color .2s; + font-size: 14px; +} + +.send-input:focus { + border-color: var(--btn); +} + +.send-btn { + background: var(--btn); + color: var(--btn-text); + cursor: pointer; + -webkit-tap-highlight-color: transparent; + border: none; + border-radius: 20px; + padding: 10px 20px; + transition: transform .2s cubic-bezier(.25,1,.5,1), background .15s; + font-size: 14px; + font-weight: 600; +} + +.send-btn:active { + transform: scale(.95); +} + +.send-btn:disabled { + opacity: .5; +} + +.send-btn.sent { + background: var(--done); +} + +.empty-state, .loading { + text-align: center; + color: var(--hint); + padding: 48px 20px; + font-size: 15px; +} + +.cmd-tiles { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.cmd-tile { + color: var(--text); + cursor: pointer; + text-align: center; + -webkit-tap-highlight-color: transparent; + border-radius: 14px; + padding: 16px 14px; + transition: transform .2s cubic-bezier(.25,1,.5,1), background .15s; + font-size: 15px; + font-weight: 600; +} + +.cmd-tile:active { + transform: scale(.96); +} + +.cmd-tile.sent { + background: var(--btn); + color: var(--btn-text); + border-color: var(--btn); +} + +.memory-view { + white-space: pre-wrap; + word-break: break-word; + padding: 14px 16px; + font-family: SF Mono, Menlo, Consolas, monospace; + font-size: 13px; + line-height: 1.6; +} + +.memory-view .md-h1 { + margin: 16px 0 8px; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; + font-size: 18px; + font-weight: 700; +} + +.memory-view .md-h1:first-child { + margin-top: 0; +} + +.memory-view .md-h2 { + color: var(--link); + margin: 14px 0 6px; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; + font-size: 15px; + font-weight: 700; +} + +.memory-view .md-h3 { + margin: 12px 0 4px; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; + font-size: 14px; + font-weight: 600; +} + +.memory-view .md-checkbox { + margin: 2px 0; +} + +.memory-view .md-checkbox-icon { + display: inline-block; + border: 1.5px solid var(--hint); + vertical-align: middle; + position: relative; + border-radius: 4px; + width: 16px; + height: 16px; + margin-right: 6px; + top: -1px; +} + +.memory-view .md-checkbox-icon.checked { + background: var(--done); + border-color: var(--done); +} + +.memory-view .md-checkbox-icon.checked:after { + content: ""; + position: absolute; + border: 1.5px solid #fff; + border-width: 0 1.5px 1.5px 0; + width: 4px; + height: 8px; + top: 1px; + left: 4px; + transform: rotate(45deg); +} + +.memory-view .md-quote { + border-left: 3px solid var(--hint); + color: var(--hint); + margin: 4px 0; + padding-left: 10px; +} + +.memory-view .md-bullet { + text-indent: -12px; + margin: 2px 0; + padding-left: 12px; +} + +.memory-view .md-bullet:before { + content: "•"; + color: var(--hint); +} + +.slide-approve-wrap { + margin-top: 16px; +} + +.slide-approve-track { + position: relative; + overflow: hidden; + touch-action: none; + border-image: 1; + border: 1.5px solid; + border-color: var(--btn); + border-image: ; + border-radius: 28px; + height: 56px; + transition: background .3s, border-color .3s, box-shadow .3s; + box-shadow: 0 2px 8px #0000002e, inset 0 1px #ffffff14; +} + +.slide-approve-track.approved { + background: var(--done); + border-color: var(--done); + box-shadow: 0 0 16px #4caf5066, 0 2px 8px #0000002e; +} + +.slide-approve-thumb { + position: absolute; + background: var(--btn); + color: var(--btn-text); + display: flex; + cursor: grab; + z-index: 1; + user-select: none; + -webkit-user-select: none; + border-radius: 50%; + justify-content: center; + align-items: center; + width: 50px; + height: 50px; + transition: left .3s cubic-bezier(.25,1,.5,1); + top: 3px; + left: 3px; + box-shadow: 0 2px 6px #00000040; +} + +.slide-approve-thumb svg { + width: 22px; + height: 22px; +} + +.slide-approve-thumb.dragging { + cursor: grabbing; + transition: none; +} + +@keyframes shimmer { + 0%, 100% { + opacity: .7; + } + + 50% { + opacity: 1; + } +} + +.slide-approve-label { + position: absolute; + display: flex; + color: var(--text); + pointer-events: none; + user-select: none; + -webkit-user-select: none; + animation: shimmer 2.5s ease-in-out infinite; + justify-content: center; + align-items: center; + transition: color .3s; + font-size: 15px; + font-weight: 600; + inset: 0; +} + +.slide-approve-track.approved .slide-approve-label { + color: #fff; + animation: none; +} + +.git-commit { + display: flex; + border-bottom: 1px solid var(--glass-border); + gap: 8px; + padding: 10px 12px; + font-size: 13px; + line-height: 1.4; +} + +.git-commit:last-child { + border-bottom: none; +} + +.git-hash { + color: var(--btn); + flex-shrink: 0; + font-family: monospace; +} + +.git-subject { + color: var(--text); + flex: 1; +} + +.git-meta { + color: var(--hint); + text-align: right; + flex-shrink: 0; + font-size: 11px; +} + +.git-status { + text-align: center; + flex-shrink: 0; + width: 24px; + font-family: monospace; + font-size: 12px; + font-weight: 700; +} + +.git-status-m { + color: #e2b93d; +} + +.git-status-a { + color: #4caf50; +} + +.git-status-d { + color: #ef5350; +} + +.git-status-u { + color: var(--hint); +} + +.git-repo-item { + cursor: pointer; + display: flex; + -webkit-tap-highlight-color: transparent; + align-items: center; + gap: 12px; + margin-bottom: 10px; + padding: 14px 14px 14px 16px; + transition: transform .2s cubic-bezier(.25,1,.5,1), box-shadow .2s; +} + +.git-repo-item:active { + transform: scale(.98); +} + +.git-repo-body { + flex: 1; + min-width: 0; +} + +.git-repo-name { + margin-bottom: 4px; + font-size: 15px; + font-weight: 600; +} + +.git-repo-branch { + color: var(--hint); + font-family: monospace; + font-size: 13px; +} + +.git-repo-arrow { + color: var(--hint); + flex-shrink: 0; + font-size: 22px; +} + +.git-back-btn { + display: inline-flex; + color: var(--btn); + cursor: pointer; + -webkit-tap-highlight-color: transparent; + background: none; + border: none; + align-items: center; + gap: 4px; + margin-bottom: 8px; + padding: 8px 0; + font-size: 14px; + font-weight: 600; +} + +.git-back-btn:active { + opacity: .6; +} + +.worktree-list { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 8px; +} + +.worktree-item { + display: flex; + border: 1px solid var(--glass-border); + background: var(--glass-bg); + border-radius: 12px; + align-items: flex-start; + gap: 10px; + padding: 10px; +} + +.worktree-item.dirty { + border-color: #ff980073; +} + +.worktree-main { + flex: 1; + min-width: 0; +} + +.worktree-name-row { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; +} + +.worktree-name { + word-break: break-word; + font-size: 14px; + font-weight: 600; +} + +.worktree-branch { + color: var(--hint); + margin-bottom: 3px; + font-family: monospace; + font-size: 12px; +} + +.worktree-last { + color: var(--hint); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 11px; +} + +.worktree-dirty, .worktree-clean { + border-radius: 999px; + padding: 2px 6px; + font-size: 10px; + font-weight: 700; +} + +.worktree-dirty { + color: #c26b00; + background: #ff980033; +} + +.worktree-clean { + color: #1b8f3a; + background: #4caf502e; +} + +.worktree-actions { + display: flex; + flex-direction: column; + flex-shrink: 0; + gap: 6px; +} + +.worktree-btn { + border: 1px solid var(--glass-border-interactive); + background: var(--glass-bg); + color: var(--text); + cursor: pointer; + border-radius: 8px; + min-width: 74px; + padding: 6px 10px; + font-size: 12px; + font-weight: 600; +} + +.worktree-btn.merge { + color: var(--btn); +} + +.worktree-btn.dispose { + color: #d14b4b; +} + +.worktree-btn:disabled { + opacity: .6; + cursor: default; +} + +.dev-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + padding: 8px 4px; +} + +.dev-header-title { + font-size: 15px; + font-weight: 600; +} + +.dev-header-target { + color: var(--hint); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-left: auto; + font-size: 13px; +} + +.dev-target-item { + cursor: pointer; + display: flex; + -webkit-tap-highlight-color: transparent; + align-items: center; + gap: 10px; + margin-bottom: 8px; + padding: 12px 14px; + transition: transform .2s cubic-bezier(.25,1,.5,1), box-shadow .2s, border-color .2s; +} + +.dev-target-item:active { + transform: scale(.98); +} + +.dev-target-item.active { + border-color: var(--btn); + box-shadow: 0 2px 8px var(--glass-shadow); +} + +.dev-target-dot { + background: var(--hint); + border-radius: 50%; + flex-shrink: 0; + width: 10px; + height: 10px; +} + +.dev-target-dot.on { + background: var(--done); +} + +.dev-target-name { + font-size: 14px; + font-weight: 600; +} + +.dev-target-url { + color: var(--hint); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-left: auto; + font-size: 13px; +} + +.dev-target-delete { + color: var(--hint); + cursor: pointer; + display: flex; + -webkit-tap-highlight-color: transparent; + border-radius: 50%; + flex-shrink: 0; + justify-content: center; + align-items: center; + width: 24px; + height: 24px; + margin: -6px -6px -6px 8px; + padding: 6px; + transition: color .15s, background .15s; +} + +.dev-target-delete:active { + color: #ff3b30; + background: #ff3b301a; +} + +.log-filter-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 8px; +} + +.log-filter-chip { + border: 1px solid var(--glass-border-interactive); + background: var(--glass-bg); + color: var(--hint); + cursor: pointer; + -webkit-tap-highlight-color: transparent; + border-radius: 12px; + padding: 4px 10px; + transition: all .2s; + font-size: 12px; + font-weight: 500; +} + +.log-filter-chip.active { + background: var(--btn); + color: var(--btn-text); + border-color: var(--btn); +} + +#logs-content { + overflow-y: auto; + -webkit-overflow-scrolling: touch; + max-height: 50vh; +} + +.log-pagination { + display: flex; + color: var(--hint); + justify-content: space-between; + align-items: center; + gap: 8px; + margin-top: 8px; + font-size: 11px; +} + +.log-page-btn { + border: 1px solid var(--glass-border-interactive); + background: var(--glass-bg); + color: var(--text); + cursor: pointer; + border-radius: 10px; + padding: 4px 10px; + font-size: 11px; + font-weight: 500; +} + +.log-page-btn:disabled { + opacity: .45; + cursor: default; +} + +.log-entry { + display: flex; + border-bottom: 1px solid var(--glass-divider); + align-items: flex-start; + gap: 6px; + padding: 3px 0; + font-family: SF Mono, Menlo, Consolas, monospace; + font-size: 11px; + line-height: 1.4; +} + +.log-ts { + color: var(--hint); + white-space: nowrap; + flex-shrink: 0; +} + +.log-badge { + text-transform: uppercase; + border-radius: 4px; + flex-shrink: 0; + padding: 0 4px; + font-size: 9px; + font-weight: 700; + line-height: 16px; +} + +.log-badge.error { + color: #ff3b30; + background: #ff3b3026; +} + +.log-badge.warn { + color: #c90; + background: #ffcc0026; +} + +.log-badge.info { + color: #34c759; + background: #34c7591f; +} + +.log-badge.debug { + color: #8e8e93; + background: #8e8e931f; +} + +.log-comp { + color: var(--link); + flex-shrink: 0; + font-size: 10px; +} + +.log-msg { + word-break: break-all; + color: var(--text); + flex: 1; +} + +.log-fields { + color: var(--hint); + font-size: 10px; +} + +.log-actions { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; +} + +.log-snap-btn { + background: var(--glass-bg); + border: 1px solid var(--glass-border-interactive); + color: var(--text); + cursor: pointer; + border-radius: 10px; + padding: 6px 12px; + font-size: 12px; + font-weight: 500; +} + +.orch-room-row { + display: flex; + align-items: flex-start; + padding: 12px 0; +} + +.orch-side { + display: flex; + flex-direction: column; + flex-shrink: 0; + align-items: center; + gap: 12px; + width: 40px; + padding-top: 20px; +} + +.orch-badge { + display: flex; + opacity: .3; + flex-direction: column; + align-items: center; + gap: 3px; + transition: opacity .3s; +} + +.orch-badge.alive { + opacity: 1; +} + +.orch-badge.alive .orch-badge-label { + color: #4a6ac0; +} + +.orch-badge.alive .orch-badge-dot { + background: #4ade80; +} + +.orch-badge.toolcall .orch-badge-dot { + animation: orch-blink .2s step-end infinite; + background: #fb923c; +} + +.orch-badge.waiting .orch-badge-dot { + background: #60a5fa; +} + +.orch-badge.talking .orch-badge-label { + color: #facc15; +} + +.orch-badge.talking .orch-badge-dot { + animation: orch-blink .6s step-end infinite; + background: #facc15; +} + +.orch-badge-emoji { + font-size: 16px; + line-height: 1; +} + +.orch-badge-label { + color: var(--hint); + letter-spacing: .05em; + text-transform: uppercase; + font-size: 7px; +} + +.orch-badge-dot { + background: var(--hint); + border-radius: 50%; + width: 4px; + height: 4px; +} + +@keyframes orch-blink { + 50% { + opacity: 0; + } +} + +.orch-canvas-wrap { + overflow: hidden; + background: #060810; + border-radius: 12px; + flex: 1; + min-width: 0; +} + +.orch-canvas-wrap canvas { + image-rendering: pixelated; + image-rendering: crisp-edges; + display: block; + aspect-ratio: 1; + width: 100%; +} + +.orch-status { + text-align: center; + color: var(--hint); + padding: 6px 0 4px; + font-size: 11px; +} + +.orch-dot { + display: inline-block; + background: var(--hint); + vertical-align: middle; + border-radius: 50%; + width: 6px; + height: 6px; + margin-right: 4px; +} + +.orch-dot.on { + background: #4ade80; +} + +.session-tree { + list-style: none; + margin: 0; + padding: 0; +} + +.session-tree-node { + position: relative; + padding: 4px 0 4px 20px; + font-size: 13px; +} + +.session-tree-node:before { + content: ""; + position: absolute; + border-left: 1px solid var(--secondary-bg); + top: 0; + bottom: 0; + left: 0; +} + +.session-tree-node:after { + content: ""; + position: absolute; + border-top: 1px solid var(--secondary-bg); + width: 16px; + top: 14px; + left: 0; +} + +.session-tree-node:last-child:before { + height: 14px; +} + +.session-tree-children { + list-style: none; + margin: 0; + padding-left: 20px; +} + +.session-tree-label { + font-weight: 600; +} + +.session-tree-meta { + color: var(--hint); + margin-left: 6px; + font-size: 11px; +} + +.session-tree-icon { + margin-right: 4px; + font-size: 10px; +} + +.session-tree-icon.active { + color: var(--done); +} + +.session-tree-icon.completed { + color: var(--hint); +} diff --git a/pkg/miniapp/static/dist/app.js b/pkg/miniapp/static/dist/app.js new file mode 100644 index 000000000..ba4140aac --- /dev/null +++ b/pkg/miniapp/static/dist/app.js @@ -0,0 +1,1459 @@ +(() => { + // src/logs_view.js + var SAFE_LEVELS = new Set(["debug", "info", "warn", "error"]); + function stringifyFieldValue(value) { + if (value === null || value === undefined) { + return ""; + } + if (typeof value === "string") { + return value; + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + try { + return JSON.stringify(value); + } catch { + return String(value); + } + } + function escapeHtml(value) { + return String(value == null ? "" : value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); + } + function renderFields(fields) { + if (!fields || typeof fields !== "object" || Array.isArray(fields)) { + return ""; + } + const keys = Object.keys(fields); + if (keys.length === 0) { + return ""; + } + const parts = keys.map((key) => `${key}=${stringifyFieldValue(fields[key])}`); + return ` {${escapeHtml(parts.join(", "))}}`; + } + function filterLogs(entries, component = "") { + if (!Array.isArray(entries) || entries.length === 0) { + return []; + } + if (!component) { + return entries.slice(); + } + return entries.filter((entry) => (entry?.component || "") === component); + } + function paginateLogs(entries, page = 1, pageSize = 100) { + const list = Array.isArray(entries) ? entries : []; + const size = Math.max(1, Number(pageSize) || 100); + const totalPages = Math.max(1, Math.ceil(list.length / size)); + const currentPage = Math.min(Math.max(1, Number(page) || 1), totalPages); + const end = list.length - (currentPage - 1) * size; + const start = Math.max(0, end - size); + return { + items: list.slice(start, Math.max(start, end)), + currentPage, + totalPages, + pageSize: size + }; + } + function renderLogs(entries, options = {}) { + const component = options.component || ""; + const filtered = filterLogs(entries, component); + const paged = paginateLogs(filtered, options.page, options.pageSize); + let html = ""; + for (const entry of paged.items) { + const levelRaw = String(entry?.level || "info").toLowerCase(); + const level = SAFE_LEVELS.has(levelRaw) ? levelRaw : "info"; + const ts = entry?.timestamp ? String(entry.timestamp).substring(11, 19) : ""; + const componentHTML = entry?.component ? `${escapeHtml(entry.component)}` : ""; + const fieldsHTML = renderFields(entry?.fields); + const message = escapeHtml(entry?.message || ""); + html += '
    ' + `${ts}` + `${level}` + componentHTML + `${message}${fieldsHTML}` + "
    "; + } + return { + html, + totalItems: filtered.length, + currentPage: paged.currentPage, + totalPages: paged.totalPages, + pageSize: paged.pageSize + }; + } + + // src/app.js + var tg = window.Telegram.WebApp; + tg.ready(); + var API_BASE = location.origin; + var initData = tg.initData || ""; + var selectedSkill = null; + var lastSSE = { plan: 0, skills: 0, session: 0, dev: 0 }; + if (!window.ORCH_ENABLED) { + orchTabBtn = document.querySelector('.tab[data-panel="orch"]'); + orchPanel = document.getElementById("orch"); + if (orchTabBtn) + orchTabBtn.style.display = "none"; + if (orchPanel) + orchPanel.style.display = "none"; + document.documentElement.style.setProperty("--tab-count", "6"); + } + var orchTabBtn; + var orchPanel; + var tabs = document.querySelectorAll('.tab:not([style*="display: none"])'); + var tabIndicator = document.querySelector(".tab-indicator"); + function moveIndicator(index) { + tabIndicator.style.transform = "translateX(" + index * 100 + "%)"; + } + tabs.forEach((tab, index) => { + tab.addEventListener("click", () => { + tabs.forEach((t) => t.classList.remove("active")); + document.querySelectorAll(".panel").forEach((p2) => p2.classList.remove("active")); + tab.classList.add("active"); + document.getElementById(tab.dataset.panel).classList.add("active"); + moveIndicator(index); + document.getElementById("send-bar").classList.toggle("hidden", !(tab.dataset.panel === "skills" && selectedSkill)); + var p = tab.dataset.panel; + var fresh = lastSSE[p] && Date.now() - lastSSE[p] < 5000; + if (p === "plan" && !fresh) + loadPlan(); + if (p === "skills" && !fresh) + loadSkills(); + if (p === "session" && !fresh) + loadSession(); + if (p === "git") + loadGit(); + if (p === "dev" && !fresh) + loadDev(); + if (p === "config") + connectLogsWs(); + else + disconnectLogsWs(); + if (p === "orch") + connectOrchWs(); + else + disconnectOrchWs(); + }); + }); + document.querySelectorAll(".cmd-tile").forEach((tile) => { + tile.addEventListener("click", async () => { + const ok = await sendCommand(tile.dataset.cmd); + if (ok) + flashSent(tile); + }); + }); + async function sendCommand(cmd) { + if (!cmd.startsWith("/")) + return false; + try { + const res = await fetch(API_BASE + "/miniapp/api/command?initData=" + encodeURIComponent(initData), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ command: cmd }) + }); + if (!res.ok) + throw new Error("API error: " + res.status); + return true; + } catch (e) { + return false; + } + } + async function sendCustomCmd() { + const input = document.getElementById("custom-cmd"); + const btn = input.nextElementSibling; + const cmd = input.value.trim(); + if (!cmd) + return; + if (!cmd.startsWith("/")) + return; + const ok = await sendCommand(cmd); + if (ok) { + input.value = ""; + flashSent(btn); + } + } + async function sendSkillCommand() { + if (!selectedSkill) + return; + const msg = document.getElementById("skill-msg").value.trim(); + const cmd = msg ? "/skill " + selectedSkill + " " + msg : "/skill " + selectedSkill; + const btn = document.getElementById("send-skill-btn"); + const ok = await sendCommand(cmd); + if (ok) + flashSent(btn); + } + async function startPlan() { + const input = document.getElementById("plan-task"); + const btn = input.nextElementSibling; + const task = input.value.trim(); + if (!task) + return; + const ok = await sendCommand("/plan " + task); + if (ok) { + input.value = ""; + flashSent(btn); + } + } + function flashSent(el) { + el.classList.add("sent"); + setTimeout(() => el.classList.remove("sent"), 600); + } + async function apiFetch(path) { + const sep = path.includes("?") ? "&" : "?"; + const res = await fetch(API_BASE + path + sep + "initData=" + encodeURIComponent(initData)); + if (!res.ok) + throw new Error("API error: " + res.status); + return res.json(); + } + function renderPlanFromData(data) { + var loading = document.getElementById("plan-loading"); + var el = document.getElementById("plan-content"); + loading.classList.add("hidden"); + el.classList.remove("hidden"); + if (!data.has_plan) { + el.innerHTML = `
    No active plan.
    +
    +
    Start a Plan
    +
    + + +
    +
    `; + return; + } + var html = `
    +
    Status
    +
    ${escapeHtml2(data.status)}
    +
    Phase ${data.current_phase} / ${data.total_phases}
    +
    `; + if (data.status === "interviewing" || data.status === "review") { + if (data.memory) { + html += `
    ${renderSimpleMarkdown(data.memory)}
    `; + } + if (data.status === "review") { + html += `
    +
    +
    +
    Slide to Approve
    +
    +
    +
    +
    +
    +
    Approve & Clear History
    +
    +
    `; + } + } else { + if (data.phases && data.phases.length > 0) { + html += renderPhases(data.phases, data.current_phase); + } + } + el.innerHTML = html; + if (data.status === "review") + setupSlideApprove(); + } + var slideApproveAC = null; + function setupSlideApprove() { + if (slideApproveAC) + slideApproveAC.abort(); + slideApproveAC = new AbortController; + var signal = slideApproveAC.signal; + var tracks = document.querySelectorAll(".slide-approve-track"); + if (!tracks.length) + return; + tracks.forEach(function(track) { + var thumb = track.querySelector(".slide-approve-thumb"); + var label = track.querySelector(".slide-approve-label"); + var cmd = track.getAttribute("data-cmd") || "/plan start"; + var dragging = false; + var startX = 0; + var thumbStartLeft = 0; + function getMaxLeft() { + return track.offsetWidth - thumb.offsetWidth - 6; + } + function markAllApproved() { + tracks.forEach(function(t) { + t.classList.add("approved"); + t.querySelector(".slide-approve-label").textContent = "Approved!"; + t.querySelector(".slide-approve-thumb").classList.add("hidden"); + }); + } + function onStart(e) { + if (track.classList.contains("approved")) + return; + dragging = true; + thumb.classList.add("dragging"); + var clientX = e.touches ? e.touches[0].clientX : e.clientX; + startX = clientX; + thumbStartLeft = thumb.offsetLeft - 3; + e.preventDefault(); + } + function onMove(e) { + if (!dragging) + return; + var clientX = e.touches ? e.touches[0].clientX : e.clientX; + var dx = clientX - startX; + var newLeft = Math.max(0, Math.min(thumbStartLeft + dx, getMaxLeft())); + thumb.style.left = newLeft + 3 + "px"; + e.preventDefault(); + } + function onEnd(e) { + if (!dragging) + return; + dragging = false; + thumb.classList.remove("dragging"); + var currentLeft = thumb.offsetLeft - 3; + var maxLeft = getMaxLeft(); + if (currentLeft >= maxLeft * 0.8) { + markAllApproved(); + sendCommand(cmd); + } else { + thumb.style.left = "3px"; + } + } + thumb.addEventListener("touchstart", onStart, { passive: false, signal }); + thumb.addEventListener("mousedown", onStart, { signal }); + document.addEventListener("touchmove", onMove, { passive: false, signal }); + document.addEventListener("mousemove", onMove, { signal }); + document.addEventListener("touchend", onEnd, { signal }); + document.addEventListener("mouseup", onEnd, { signal }); + }); + } + function renderSimpleMarkdown(text) { + var lines = text.split(` +`); + var out = []; + for (var i = 0;i < lines.length; i++) { + var line = lines[i]; + if (/^### /.test(line)) { + out.push('
    ' + escapeHtml2(line.slice(4)) + "
    "); + } else if (/^## /.test(line)) { + out.push('
    ' + escapeHtml2(line.slice(3)) + "
    "); + } else if (/^# /.test(line)) { + out.push('
    ' + escapeHtml2(line.slice(2)) + "
    "); + } else if (/^- \[x\] /.test(line)) { + out.push('
    ' + escapeHtml2(line.slice(6)) + "
    "); + } else if (/^- \[ \] /.test(line)) { + out.push('
    ' + escapeHtml2(line.slice(6)) + "
    "); + } else if (/^> /.test(line)) { + out.push('
    ' + escapeHtml2(line.slice(2)) + "
    "); + } else if (/^- /.test(line)) { + out.push('
    ' + escapeHtml2(line.slice(2)) + "
    "); + } else if (line.trim() === "") { + out.push("
    "); + } else { + out.push("
    " + escapeHtml2(line) + "
    "); + } + } + return out.join(""); + } + async function loadTab(loadingId, contentId, label, fetchFn, renderFn) { + var loading = document.getElementById(loadingId); + var el = document.getElementById(contentId); + loading.classList.remove("hidden"); + loading.textContent = "Loading " + label + "..."; + el.classList.add("hidden"); + try { + renderFn(await fetchFn()); + } catch (e) { + loading.textContent = "Failed to load " + label + "."; + } + } + function loadPlan() { + return loadTab("plan-loading", "plan-content", "plan", function() { + return apiFetch("/miniapp/api/plan"); + }, renderPlanFromData); + } + function renderPhases(phases, currentPhase) { + return phases.map((phase) => { + const doneCount = phase.steps.filter((s) => s.done).length; + const total = phase.steps.length; + let indicatorClass, indicator; + if (phase.number < currentPhase || total > 0 && doneCount === total) { + indicatorClass = "done"; + indicator = "✓"; + } else if (phase.number === currentPhase) { + indicatorClass = "current"; + indicator = String(phase.number); + } else { + indicatorClass = "pending"; + indicator = String(phase.number); + } + const progressHtml = total > 0 ? `${doneCount}/${total}` : ""; + const stepsHtml = phase.steps.map((step) => { + const doneClass = step.done ? "done" : ""; + const stepClass = step.done ? "step step-done" : "step"; + return `
    +
    +
    ${escapeHtml2(step.description)}
    +
    `; + }).join(""); + return `
    +
    +
    ${indicator}
    + ${escapeHtml2(phase.title || "Phase " + phase.number)} + ${progressHtml} +
    + ${stepsHtml} +
    `; + }).join(""); + } + document.getElementById("plan-content").addEventListener("click", function(e) { + const step = e.target.closest(".step"); + if (!step) + return; + if (step.dataset.done === "true") + return; + const phase = step.dataset.phase; + const stepIdx = step.dataset.step; + sendCommand("/plan done " + stepIdx); + }); + function renderSkillsFromData(data) { + var loading = document.getElementById("skills-loading"); + var el = document.getElementById("skills-list"); + loading.classList.add("hidden"); + el.classList.remove("hidden"); + if (!data || data.length === 0) { + el.innerHTML = '
    No skills installed.
    '; + return; + } + el.innerHTML = data.map((s) => `
    +
    +
    ${escapeHtml2(s.name)}
    +
    ${escapeHtml2(s.description || "No description")}
    + ${escapeHtml2(s.source)} +
    + +
    `).join(""); + if (selectedSkill) { + var prev = el.querySelector('[data-skill="' + CSS.escape(selectedSkill) + '"]'); + if (prev) + prev.classList.add("selected"); + } + } + document.getElementById("skills-list").addEventListener("click", function(e) { + var item = e.target.closest(".skill-item"); + if (!item) + return; + var el = document.getElementById("skills-list"); + if (selectedSkill === item.dataset.skill) { + item.classList.remove("selected"); + selectedSkill = null; + document.getElementById("send-bar").classList.add("hidden"); + return; + } + el.querySelectorAll(".skill-item").forEach(function(i) { + i.classList.remove("selected"); + }); + item.classList.add("selected"); + selectedSkill = item.dataset.skill; + document.getElementById("send-bar").classList.remove("hidden"); + document.getElementById("skill-msg").placeholder = "Message for /" + selectedSkill + "..."; + document.getElementById("skill-msg").focus(); + }); + function loadSkills() { + return loadTab("skills-loading", "skills-list", "skills", function() { + return apiFetch("/miniapp/api/skills"); + }, renderSkillsFromData); + } + function formatAge(sec) { + if (sec < 60) + return sec + "s ago"; + if (sec < 3600) + return Math.floor(sec / 60) + "m ago"; + return Math.floor(sec / 3600) + "h ago"; + } + function shortSessionKey(key) { + var parts = key.split(":"); + if (parts.length > 2) + return parts.slice(2).join(":"); + return key; + } + function renderActiveSessions(sessions) { + if (!sessions || sessions.length === 0) { + return `
    +
    Active Sessions
    +
    No active sessions
    +
    `; + } + return `
    Active Sessions
    + ${sessions.map((s) => { + var touchDir = s.touch_dir || "—"; + return `
    +
    + + ${escapeHtml2(shortSessionKey(s.session_key))} + ${formatAge(s.age_sec)} +
    +
    touch: ${escapeHtml2(touchDir)}
    +
    `; + }).join("")} +
    `; + } + function renderSessionFromData(sessions, stats) { + var loading = document.getElementById("session-loading"); + var el = document.getElementById("session-content"); + loading.classList.add("hidden"); + el.classList.remove("hidden"); + var html = renderActiveSessions(sessions); + if (!stats || stats.status === "stats not enabled") { + html += '
    Stats tracking not enabled.
    Start gateway with --stats flag.
    '; + el.innerHTML = html; + return; + } + var since = stats.since ? new Date(stats.since).toLocaleDateString() : "N/A"; + var today = stats.today || {}; + html += `
    +
    Today
    +
    Prompts${today.prompts || 0}
    +
    Requests${today.requests || 0}
    +
    Tokens${formatTokens(today.total_tokens || 0)}
    +
    +
    +
    All Time (since ${escapeHtml2(since)})
    +
    Prompts${stats.total_prompts || 0}
    +
    Requests${stats.total_requests || 0}
    +
    Total Tokens${formatTokens(stats.total_tokens || 0)}
    +
    Prompt Tokens${formatTokens(stats.total_prompt_tokens || 0)}
    +
    Completion Tokens${formatTokens(stats.total_completion_tokens || 0)}
    +
    `; + el.innerHTML = html; + } + var cachedContextInfo = null; + function renderContextCard(ctx) { + if (!ctx) + return ""; + cachedContextInfo = ctx; + var wd = ctx.work_dir || "—"; + var pwd = ctx.plan_work_dir || "—"; + var ws = ctx.workspace || "—"; + var filesHtml = ""; + if (ctx.bootstrap && ctx.bootstrap.length) { + filesHtml = ctx.bootstrap.map(function(b) { + var path = b.path ? escapeHtml2(b.path) : "—"; + var scope = b.scope === "global" ? "global" : "project"; + var found = b.path ? "var(--text)" : "var(--hint)"; + return `
    + ${escapeHtml2(b.name)} + ${path} + ${scope} +
    `; + }).join(""); + } + return `
    +
    Context
    +
    +
    workDir${escapeHtml2(wd)}
    +
    planWorkDir${escapeHtml2(pwd)}
    +
    workspace${escapeHtml2(ws)}
    +
    +
    ${filesHtml}
    +
    + +
    + +
    `; + } + function toggleSystemPrompt() { + var view = document.getElementById("system-prompt-view"); + var btn = document.getElementById("prompt-toggle-btn"); + if (!view || !btn) + return; + if (view.style.display === "none") { + btn.textContent = "Loading..."; + apiFetch("/miniapp/api/prompt").then(function(data) { + view.textContent = data.prompt || "(empty)"; + view.style.display = "block"; + btn.textContent = "Hide System Prompt"; + }).catch(function() { + btn.textContent = "Show System Prompt"; + }); + } else { + view.style.display = "none"; + btn.textContent = "Show System Prompt"; + } + } + function renderContextFromData(ctx) { + var el = document.getElementById("context-content"); + if (el) + el.innerHTML = renderContextCard(ctx); + } + function loadSession() { + return loadTab("session-loading", "session-content", "session", function() { + return Promise.all([ + apiFetch("/miniapp/api/session"), + apiFetch("/miniapp/api/sessions").catch(function() { + return []; + }), + apiFetch("/miniapp/api/context").catch(function() { + return null; + }), + apiFetch("/miniapp/api/sessions/graph").catch(function() { + return null; + }) + ]); + }, function(results) { + renderSessionFromData(results[1], results[0]); + renderContextFromData(results[2]); + renderSessionGraph(results[3]); + }); + } + function renderSessionGraph(graph) { + var el = document.getElementById("session-graph"); + if (!el) + return; + if (!graph || !graph.nodes || graph.nodes.length === 0) { + el.classList.add("hidden"); + return; + } + el.classList.remove("hidden"); + var childrenMap = {}; + var roots = []; + graph.nodes.forEach(function(n) { + childrenMap[n.key] = []; + }); + graph.edges.forEach(function(e) { + if (childrenMap[e.from]) + childrenMap[e.from].push(e.to); + }); + var nodeMap = {}; + graph.nodes.forEach(function(n) { + nodeMap[n.key] = n; + var isChild = graph.edges.some(function(e) { + return e.to === n.key; + }); + if (!isChild) + roots.push(n.key); + }); + function renderTreeNode(key) { + var n = nodeMap[key]; + if (!n) + return ""; + var icon = n.status === "completed" ? "✓" : "●"; + var iconClass = n.status === "completed" ? "completed" : "active"; + var label = n.label || n.short_key || n.key; + var kids = childrenMap[key] || []; + var childHtml = ""; + if (kids.length > 0) { + childHtml = '"; + } + return '
  • ' + '' + icon + "" + '' + escapeHtml2(label) + "" + 'turns=' + n.turn_count + "" + childHtml + "
  • "; + } + var html = '
    Session Graph
    ' + '
    "; + el.innerHTML = html; + } + function formatTokens(n) { + if (n >= 1e6) + return (n / 1e6).toFixed(1) + "M"; + if (n >= 1000) + return (n / 1000).toFixed(1) + "K"; + return String(n); + } + function escapeHtml2(s) { + if (!s) + return ""; + return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + } + function escapeAttr(s) { + if (!s) + return ""; + return s.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'"); + } + var gitSelectedRepo = null; + function loadGit() { + gitSelectedRepo = null; + return loadTab("git-loading", "git-content", "git", function() { + return Promise.all([ + apiFetch("/miniapp/api/git"), + apiFetch("/miniapp/api/worktrees").catch(function() { + return []; + }) + ]); + }, function(results) { + renderGitRepos(results[0], results[1]); + }); + } + function renderWorktrees(worktrees) { + var items = Array.isArray(worktrees) ? worktrees : []; + var html = '
    Worktrees
    '; + if (items.length === 0) { + html += '
    No active worktrees.
    '; + html += "
    "; + return html; + } + html += '
    '; + items.forEach(function(wt) { + var dirtyClass = wt.has_uncommitted ? " dirty" : ""; + var dirtyBadge = wt.has_uncommitted ? 'DIRTY' : 'CLEAN'; + var last = "(no commits)"; + if (wt.last_commit_hash) { + last = wt.last_commit_hash + " " + (wt.last_commit_subject || ""); + if (wt.last_commit_age) + last += " (" + wt.last_commit_age + ")"; + } + html += '
    ' + '
    ' + '
    ' + '' + escapeHtml2(wt.name) + "" + dirtyBadge + "
    " + '
    ' + escapeHtml2(wt.branch || "?") + "
    " + '
    ' + escapeHtml2(last) + "
    " + "
    " + '
    ' + '' + '' + "
    " + "
    "; + }); + html += "
    "; + return html; + } + function renderGitRepos(repos, worktrees) { + var loading = document.getElementById("git-loading"); + var el = document.getElementById("git-content"); + loading.classList.add("hidden"); + el.classList.remove("hidden"); + var html = renderWorktrees(worktrees); + if (!repos || repos.length === 0) { + html += '
    No git repositories found.
    '; + el.innerHTML = html; + return; + } + html += '
    Repositories
    '; + html += repos.map(function(r) { + return '
    ' + '
    ' + '
    ' + escapeHtml2(r.name) + "
    " + '
    ' + escapeHtml2(r.branch || "?") + "
    " + "
    " + '' + "
    "; + }).join(""); + el.innerHTML = html; + } + async function postWorktreeAction(action, name, force) { + var res = await fetch(API_BASE + "/miniapp/api/worktrees?initData=" + encodeURIComponent(initData), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action, name, force: !!force }) + }); + var data = {}; + try { + data = await res.json(); + } catch (e) {} + if (!res.ok) { + throw new Error(data.error || "API error: " + res.status); + } + return data; + } + document.getElementById("git-content").addEventListener("click", async function(e) { + var wtBtn = e.target.closest("[data-wt-action]"); + if (wtBtn) { + var action = wtBtn.dataset.wtAction; + var name = wtBtn.dataset.wtName; + var isDirty = wtBtn.dataset.wtDirty === "1"; + var force = false; + if (action === "merge") { + if (!confirm('Merge "' + name + '" into base branch?')) + return; + } else if (action === "dispose") { + if (isDirty) { + if (!confirm('"' + name + '" has uncommitted changes. Force dispose and auto-commit before removal?')) + return; + force = true; + } else if (!confirm('Dispose worktree "' + name + '"?')) { + return; + } + } + var originalText = wtBtn.textContent; + wtBtn.disabled = true; + wtBtn.textContent = action === "merge" ? "Merging..." : "Disposing..."; + try { + await postWorktreeAction(action, name, force); + await loadGit(); + } catch (err) { + alert(err.message || "Action failed"); + wtBtn.disabled = false; + wtBtn.textContent = originalText; + } + return; + } + var item = e.target.closest(".git-repo-item"); + if (!item) + return; + loadGitDetail(item.dataset.repo); + }); + function loadGitDetail(name) { + gitSelectedRepo = name; + return loadTab("git-loading", "git-content", name, function() { + return apiFetch("/miniapp/api/git?repo=" + encodeURIComponent(name)); + }, renderGitDetail); + } + function renderGitDetail(repo) { + var loading = document.getElementById("git-loading"); + var el = document.getElementById("git-content"); + loading.classList.add("hidden"); + el.classList.remove("hidden"); + var html = '"; + html += '
    ' + escapeHtml2(repo.name) + " — " + escapeHtml2(repo.branch || "?") + "
    "; + if (repo.modified && repo.modified.length > 0) { + html += '
    Changes (' + repo.modified.length + ")
    "; + repo.modified.forEach(function(f) { + html += '
    ' + '' + escapeHtml2(f.status) + "" + '' + escapeHtml2(f.path) + "" + "
    "; + }); + } + if (repo.commits && repo.commits.length > 0) { + html += '
    Commits
    '; + repo.commits.forEach(function(c) { + html += '
    ' + '' + escapeHtml2(c.hash) + "" + '' + escapeHtml2(c.subject) + "" + '' + escapeHtml2(c.date) + "" + "
    "; + }); + } else { + html += '
    No commits found.
    '; + } + html += "
    "; + el.innerHTML = html; + } + var devActiveId = ""; + function renderDevFromData(data) { + var dot = document.getElementById("dev-dot"); + var headerTarget = document.getElementById("dev-header-target"); + var targetsList = document.getElementById("dev-targets-list"); + var iframeWrap = document.getElementById("dev-iframe-wrap"); + var iframe = document.getElementById("dev-iframe"); + var targets = data.targets || []; + devActiveId = data.active_id || ""; + if (data.active) { + dot.classList.add("on"); + headerTarget.textContent = data.target ? data.target.replace(/^https?:\/\//, "") : ""; + iframeWrap.classList.remove("hidden"); + var iframeSrc = location.origin + "/miniapp/dev/"; + if (iframe.src !== iframeSrc) + iframe.src = iframeSrc; + } else { + dot.classList.remove("on"); + headerTarget.textContent = ""; + iframeWrap.classList.add("hidden"); + iframe.src = ""; + } + if (targets.length === 0) { + targetsList.innerHTML = '
    No targets registered.
    Ask the agent to start a dev server.
    '; + return; + } + targetsList.innerHTML = targets.map(function(t) { + var isActive = t.id === devActiveId; + var activeClass = isActive ? " active" : ""; + var dotClass = isActive ? " on" : ""; + var displayUrl = t.target.replace(/^https?:\/\//, ""); + return '
    ' + '' + '' + escapeHtml2(t.name) + "" + '' + escapeHtml2(displayUrl) + "" + '×' + "
    "; + }).join(""); + } + document.getElementById("dev-targets-list").addEventListener("click", function(e) { + var delBtn = e.target.closest(".dev-target-delete"); + if (delBtn) { + e.stopPropagation(); + var id = delBtn.dataset.delId; + var name = delBtn.dataset.delName; + if (confirm('Remove "' + name + '"?')) { + postDevUnregister(id); + } + return; + } + var card = e.target.closest("[data-dev-id]"); + if (!card) + return; + postDevAction(card.dataset.devId); + }); + async function postDevAction(id) { + var action = id === devActiveId ? "deactivate" : "activate"; + var body = action === "activate" ? { action: "activate", id } : { action: "deactivate" }; + try { + var res = await fetch(API_BASE + "/miniapp/api/dev?initData=" + encodeURIComponent(initData), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body) + }); + var data = await res.json(); + if (!data.error) + renderDevFromData(data); + } catch (e) {} + } + async function postDevUnregister(id) { + try { + var res = await fetch(API_BASE + "/miniapp/api/dev?initData=" + encodeURIComponent(initData), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "unregister", id }) + }); + var data = await res.json(); + if (!data.error) + renderDevFromData(data); + } catch (e) {} + } + function loadDev() { + apiFetch("/miniapp/api/dev").then(renderDevFromData).catch(function() {}); + } + var eventSource = null; + function connectSSE() { + if (eventSource) + eventSource.close(); + eventSource = new EventSource(API_BASE + "/miniapp/api/events?initData=" + encodeURIComponent(initData)); + eventSource.addEventListener("plan", function(e) { + try { + lastSSE.plan = Date.now(); + renderPlanFromData(JSON.parse(e.data)); + } catch (err) {} + }); + eventSource.addEventListener("session", function(e) { + try { + lastSSE.session = Date.now(); + var d = JSON.parse(e.data); + renderSessionFromData(d.sessions, d.stats); + if (d.graph) + renderSessionGraph(d.graph); + } catch (err) {} + }); + eventSource.addEventListener("skills", function(e) { + try { + lastSSE.skills = Date.now(); + renderSkillsFromData(JSON.parse(e.data)); + } catch (err) {} + }); + eventSource.addEventListener("dev", function(e) { + try { + lastSSE.dev = Date.now(); + renderDevFromData(JSON.parse(e.data)); + } catch (err) {} + }); + eventSource.addEventListener("context", function(e) { + try { + renderContextFromData(JSON.parse(e.data)); + } catch (err) {} + }); + eventSource.addEventListener("prompt", function(e) { + try { + var d = JSON.parse(e.data); + var view = document.getElementById("system-prompt-view"); + if (view && view.style.display !== "none") { + view.textContent = d.prompt || "(empty)"; + } + } catch (err) {} + }); + eventSource.onerror = function() {}; + } + connectSSE(); + loadPlan(); + var logsWs = null; + var logsComponent = ""; + var logsEntries = []; + var logsReconnectTimer = null; + var logsPage = 1; + var LOGS_PAGE_SIZE = 60; + function connectLogsWs() { + if (logsWs && logsWs.readyState <= 1) + return; + var wsProto = location.protocol === "https:" ? "wss:" : "ws:"; + var wsUrl = wsProto + "//" + location.host + "/miniapp/api/logs/ws?initData=" + encodeURIComponent(initData); + if (logsComponent) + wsUrl += "&component=" + encodeURIComponent(logsComponent); + logsWs = new WebSocket(wsUrl); + var statusDot = document.getElementById("logs-status"); + logsWs.onopen = function() { + statusDot.classList.add("on"); + }; + logsWs.onmessage = function(e) { + var msg = JSON.parse(e.data); + if (msg.type === "init") { + logsEntries = msg.entries || []; + logsPage = 1; + } else if (msg.type === "entry") { + logsEntries.push(msg.entry); + if (logsEntries.length > 200) + logsEntries.shift(); + } + renderLogs2(); + }; + logsWs.onclose = function() { + statusDot.classList.remove("on"); + logsWs = null; + var activeTab = document.querySelector(".tab.active"); + if (activeTab && activeTab.dataset.panel === "config") { + logsReconnectTimer = setTimeout(connectLogsWs, 3000); + } + }; + logsWs.onerror = function() {}; + } + function disconnectLogsWs() { + if (logsReconnectTimer) { + clearTimeout(logsReconnectTimer); + logsReconnectTimer = null; + } + if (logsWs) { + logsWs.close(); + logsWs = null; + } + document.getElementById("logs-status").classList.remove("on"); + } + function renderLogs2() { + var container = document.getElementById("logs-content"); + if (!container) + return; + var wasScrolledToBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 30; + var view = renderLogs(logsEntries, { + component: logsComponent, + page: logsPage, + pageSize: LOGS_PAGE_SIZE + }); + if (logsPage > view.totalPages) { + logsPage = view.totalPages; + view = renderLogs(logsEntries, { + component: logsComponent, + page: logsPage, + pageSize: LOGS_PAGE_SIZE + }); + } + if (!view.html) { + container.innerHTML = '
    No logs.
    '; + } else { + container.innerHTML = view.html; + } + updateLogsPager(view); + if (wasScrolledToBottom) + container.scrollTop = container.scrollHeight; + } + function updateLogsPager(view) { + var info = document.getElementById("logs-page-info"); + var prev = document.getElementById("logs-page-prev"); + var next = document.getElementById("logs-page-next"); + if (!info || !prev || !next) + return; + info.textContent = view.currentPage + "/" + view.totalPages + " (" + view.totalItems + ")"; + prev.disabled = view.currentPage <= 1; + next.disabled = view.currentPage >= view.totalPages; + } + document.querySelector(".log-filter-chips").addEventListener("click", function(e) { + var chip = e.target.closest(".log-filter-chip"); + if (!chip) + return; + document.querySelectorAll(".log-filter-chip").forEach(function(c) { + c.classList.remove("active"); + }); + chip.classList.add("active"); + logsComponent = chip.dataset.component || ""; + logsPage = 1; + logsEntries = []; + renderLogs2(); + disconnectLogsWs(); + connectLogsWs(); + }); + var logsPrevButton = document.getElementById("logs-page-prev"); + if (logsPrevButton) { + logsPrevButton.addEventListener("click", function() { + if (logsPage <= 1) + return; + logsPage--; + renderLogs2(); + }); + } + var logsNextButton = document.getElementById("logs-page-next"); + if (logsNextButton) { + logsNextButton.addEventListener("click", function() { + logsPage++; + renderLogs2(); + }); + } + var orchCanvas = null; + var orchCtx = null; + var orchInited = false; + var orchWs = null; + var orchReconnectTimer = null; + var _orchLastTs = null; + var _orchBOB = [0, -1, -2, -1]; + var _orchFRAME_MS = { idle: 450, waiting: 650, toolcall: 90, talking: 280, entering: 220, exiting: 220 }; + var _orchWALK = 55; + var _orchConductor; + var _orchSecretary; + var _orchHeartbeat; + var _orchSubagents; + var _orchSlots; + var _orchFreeSlots; + function _orchMakeChar(id, emoji, home) { + return { + id, + emoji, + x: home.x, + y: home.y, + home, + target: null, + state: "idle", + frame: 0, + frameTimer: 0, + bubble: null, + alive: false, + _onArrive: null + }; + } + function _orchInitChars() { + _orchConductor = _orchMakeChar("conductor", "\uD83D\uDC51", MAP_POSITIONS.conductor); + _orchSecretary = _orchMakeChar("secretary", "\uD83D\uDC69‍\uD83D\uDCBC", MAP_POSITIONS.secretary); + _orchHeartbeat = _orchMakeChar("heartbeat", "\uD83D\uDD4A️", MAP_POSITIONS.heartbeat || { x: 230, y: 58 }); + _orchConductor.alive = true; + _orchSecretary.alive = false; + _orchHeartbeat.alive = true; + _orchConductor.statusText = null; + _orchHeartbeat.facing = 1; + _orchHeartbeat.flipTimer = 0; + var ps = [ + { id: "s0", emoji: "\uD83D\uDD0D" }, + { id: "s1", emoji: "\uD83D\uDCCA" }, + { id: "s2", emoji: "\uD83D\uDCBB" }, + { id: "s3", emoji: "\uD83D\uDD27" }, + { id: "s4", emoji: "\uD83C\uDFAF" } + ]; + _orchSubagents = ps.map(function(p, i) { + var c = _orchMakeChar(p.id, p.emoji, MAP_POSITIONS.stations[i]); + c.x = MAP_POSITIONS.door.x; + c.y = MAP_POSITIONS.door.y; + return c; + }); + _orchSlots = {}; + _orchFreeSlots = _orchSubagents.slice(); + } + function _orchAllChars() { + return [_orchConductor, _orchSecretary, _orchHeartbeat].concat(_orchSubagents); + } + function _orchSyncBadge(id, state, alive) { + var el = document.getElementById("orch-badge-" + id); + if (!el) + return; + el.className = "orch-badge" + (alive ? " alive" : "") + (state === "talking" ? " talking" : "") + (state === "toolcall" ? " toolcall" : "") + (state === "waiting" ? " waiting" : ""); + } + function _orchSetState(c, state, tool) { + c.state = state; + _orchSyncBadge(c.id, state, c.alive); + if (c === _orchConductor) { + if (state === "waiting") + c.statusText = "\uD83E\uDD14"; + else if (state === "toolcall") + c.statusText = "⌨"; + else if (state === "user_waiting") + c.statusText = "⏳"; + else if (state === "plan_interviewing") + c.statusText = "\uD83D\uDCCB"; + else if (state === "plan_review") + c.statusText = "\uD83D\uDD0D"; + else if (state === "plan_executing") + c.statusText = "▶️"; + else if (state === "plan_completed") + c.statusText = "✅"; + else + c.statusText = null; + var inPlan = state.indexOf("plan_") === 0; + if (_orchSecretary.alive !== inPlan) { + _orchSecretary.alive = inPlan; + _orchSyncBadge("secretary", _orchSecretary.state, _orchSecretary.alive); + } + } + } + function _orchMoveTo(c, pos, cb) { + c.target = pos; + c._onArrive = cb || null; + } + function _orchSay(c, text, ttl) { + c.bubble = { text, ttl: ttl || 2200 }; + } + function _orchCharForId(id) { + if (id === "heartbeat") + return _orchHeartbeat; + if (_orchSlots[id]) + return _orchSlots[id]; + return _orchConductor; + } + function _orchSpawn(id) { + if (/^subagent-/.test(id)) { + var c = _orchFreeSlots.shift(); + if (!c) + return; + _orchSlots[id] = c; + c.alive = true; + c.x = MAP_POSITIONS.door.x; + c.y = MAP_POSITIONS.door.y; + _orchSetState(c, "entering"); + _orchMoveTo(c, c.home, function() { + _orchSetState(c, "idle"); + }); + } else { + var ch = _orchCharForId(id); + ch.alive = true; + _orchSetState(ch, "waiting"); + } + } + function _orchGC(id) { + if (/^subagent-/.test(id)) { + var c = _orchSlots[id]; + if (!c) + return; + delete _orchSlots[id]; + _orchFreeSlots.push(c); + _orchSetState(c, "exiting"); + _orchMoveTo(c, MAP_POSITIONS.door, function() { + c.alive = false; + _orchSetState(c, "idle"); + }); + } else { + var ch = _orchCharForId(id); + if (ch === _orchHeartbeat) { + _orchSetState(ch, "idle"); + } else if (ch === _orchConductor) { + _orchSetState(ch, "user_waiting"); + } else { + ch.alive = false; + _orchSetState(ch, "idle"); + } + } + } + function _orchConverse(fromId, toId, text) { + var from = _orchCharForId(fromId), to = _orchCharForId(toId); + if (!from || !to || from === to) + return; + var label = (text || "").slice(0, 18); + var mid = { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 }; + _orchSetState(from, "talking"); + _orchSetState(to, "talking"); + _orchMoveTo(from, { x: mid.x - 18, y: mid.y }, function() { + _orchSay(from, label, 2400); + }); + _orchMoveTo(to, { x: mid.x + 18, y: mid.y }, function() { + setTimeout(function() { + _orchMoveTo(from, from.home, function() { + _orchSetState(from, "idle"); + }); + _orchMoveTo(to, to.home, function() { + _orchSetState(to, "idle"); + }); + }, 2600); + }); + } + function _orchUpdate(dt) { + _orchAllChars().forEach(function(c) { + if (!c.alive && c.state !== "entering") + return; + if (c === _orchHeartbeat) { + if (c.state === "idle") { + c.frame = 0; + } else { + c.frameTimer += dt; + var pDur = c.state === "toolcall" ? 130 : 380; + if (c.frameTimer >= pDur) { + c.frame = (c.frame + 1) % 4; + c.frameTimer -= pDur; + } + } + } else { + c.frameTimer += dt; + var dur = _orchFRAME_MS[c.state] || 450; + if (c.frameTimer >= dur) { + c.frame = (c.frame + 1) % 4; + c.frameTimer -= dur; + } + } + if (c.target) { + var dx = c.target.x - c.x, dy = c.target.y - c.y, dist = Math.sqrt(dx * dx + dy * dy); + if (dist > 1.5) { + var spd = _orchWALK * dt / 1000; + c.x += dx / dist * spd; + c.y += dy / dist * spd; + } else { + c.x = c.target.x; + c.y = c.target.y; + c.target = null; + if (c._onArrive) { + c._onArrive(); + c._onArrive = null; + } + } + } + if (c.bubble) { + c.bubble.ttl -= dt; + if (c.bubble.ttl <= 0) + c.bubble = null; + } + if (c === _orchHeartbeat) { + if (c.target) { + var pdx = c.target.x - c.x; + if (Math.abs(pdx) > 1) + c.facing = pdx > 0 ? 1 : -1; + } else { + var flipRate = c.state === "toolcall" ? 280 : c.state === "waiting" ? 600 : 2800; + c.flipTimer += dt; + if (c.flipTimer >= flipRate) { + c.flipTimer -= flipRate; + c.facing = -c.facing; + } + } + } + }); + } + function _orchDrawStatus(c) { + if (!c.statusText) + return; + var yOff = _orchBOB[c.frame], cx = Math.floor(c.x), cy = Math.floor(c.y + yOff) - 20; + orchCtx.font = "11px serif"; + orchCtx.textAlign = "center"; + orchCtx.textBaseline = "middle"; + orchCtx.fillText(c.statusText, cx, cy); + } + function _orchDrawBubble(c) { + if (!c.bubble) + return; + var yOff = _orchBOB[c.frame], bx = c.x, by = c.y + yOff - 18; + orchCtx.font = "7px Silkscreen,monospace"; + var tw = orchCtx.measureText(c.bubble.text).width, pw = tw + 8, ph = 12; + var lx = Math.max(4, Math.min(316 - pw, bx - pw / 2)); + orchCtx.fillStyle = "#facc15"; + orchCtx.fillRect(Math.floor(lx), Math.floor(by - ph), Math.ceil(pw), Math.ceil(ph)); + orchCtx.fillRect(Math.floor(bx) - 1, Math.floor(by), 3, 3); + orchCtx.fillStyle = "#0a0a00"; + orchCtx.textAlign = "left"; + orchCtx.textBaseline = "middle"; + orchCtx.fillText(c.bubble.text, Math.floor(lx + 4), Math.floor(by - ph / 2)); + } + function _orchDrawChar(c) { + if (!c.alive && c.state !== "entering" && c.state !== "exiting") + return; + var yOff = _orchBOB[c.frame], cx = Math.floor(c.x), cy = Math.floor(c.y + yOff); + if (c.state === "toolcall") { + orchCtx.fillStyle = "rgba(251,146,60,0.35)"; + orchCtx.beginPath(); + orchCtx.arc(cx, cy, 13, 0, Math.PI * 2); + orchCtx.fill(); + } else if (c.state === "waiting") { + orchCtx.fillStyle = "rgba(96,165,250,0.25)"; + orchCtx.beginPath(); + orchCtx.arc(cx, cy, 11, 0, Math.PI * 2); + orchCtx.fill(); + } else if (c.state === "user_waiting" || c.state === "plan_review") { + orchCtx.fillStyle = "rgba(167,139,250,0.18)"; + orchCtx.beginPath(); + orchCtx.arc(cx, cy, 10, 0, Math.PI * 2); + orchCtx.fill(); + } else if (c.state === "plan_executing") { + orchCtx.fillStyle = "rgba(74,222,128,0.18)"; + orchCtx.beginPath(); + orchCtx.arc(cx, cy, 10, 0, Math.PI * 2); + orchCtx.fill(); + } + orchCtx.font = "18px serif"; + orchCtx.textAlign = "center"; + orchCtx.textBaseline = "middle"; + if (c.facing === -1) { + orchCtx.save(); + orchCtx.translate(cx, cy); + orchCtx.scale(-1, 1); + orchCtx.fillText(c.emoji, 0, 0); + orchCtx.restore(); + } else { + orchCtx.fillText(c.emoji, cx, cy); + } + orchCtx.font = "6px Silkscreen,monospace"; + orchCtx.textAlign = "center"; + orchCtx.textBaseline = "top"; + orchCtx.fillStyle = c.state === "talking" ? "#facc15" : "#3a4a7a"; + orchCtx.fillText(c.id.toUpperCase(), cx, cy + 11); + _orchDrawStatus(c); + _orchDrawBubble(c); + } + function _orchRender(ts) { + if (_orchLastTs === null) + _orchLastTs = ts; + var dt = Math.min(ts - _orchLastTs, 80); + _orchLastTs = ts; + _orchUpdate(dt); + orchCtx.imageSmoothingEnabled = false; + drawMap(orchCtx); + _orchAllChars().forEach(_orchDrawChar); + requestAnimationFrame(_orchRender); + } + function orchInit() { + if (orchInited) + return; + orchInited = true; + orchCanvas = document.getElementById("orch-canvas"); + orchCtx = orchCanvas.getContext("2d"); + orchCtx.imageSmoothingEnabled = false; + _orchInitChars(); + loadMapAsset(function() { + _orchLastTs = null; + requestAnimationFrame(_orchRender); + }); + } + function connectOrchWs() { + orchInit(); + if (orchWs && orchWs.readyState <= 1) + return; + var proto = location.protocol === "https:" ? "wss:" : "ws:"; + var url = proto + "//" + location.host + "/miniapp/api/orchestration/ws?initData=" + encodeURIComponent(initData); + orchWs = new WebSocket(url); + orchWs.onopen = function() { + document.getElementById("orch-status-dot").classList.add("on"); + document.getElementById("orch-status-text").textContent = "Live"; + }; + orchWs.onmessage = function(e) { + var msg; + try { + msg = JSON.parse(e.data); + } catch (_) { + return; + } + if (msg.type === "init") { + (msg.agents || []).forEach(function(info) { + _orchSpawn(info.id); + if (info.state && info.state !== "idle") { + var c2 = _orchCharForId(info.id); + if (c2) + _orchSetState(c2, info.state); + } + }); + } else if (msg.type === "event") { + var ev = msg.event || {}; + if (ev.type === "agent_spawn") + _orchSpawn(ev.id); + if (ev.type === "agent_state") { + var c = _orchCharForId(ev.id); + if (c) + _orchSetState(c, ev.state, ev.tool); + } + if (ev.type === "agent_gc") + _orchGC(ev.id); + if (ev.type === "conversation") + _orchConverse(ev.from, ev.to, ev.text); + } + }; + orchWs.onclose = function() { + document.getElementById("orch-status-dot").classList.remove("on"); + document.getElementById("orch-status-text").textContent = "Disconnected"; + orchWs = null; + var at = document.querySelector(".tab.active"); + if (at && at.dataset.panel === "orch") + orchReconnectTimer = setTimeout(connectOrchWs, 3000); + }; + orchWs.onerror = function() {}; + } + function disconnectOrchWs() { + if (orchReconnectTimer) { + clearTimeout(orchReconnectTimer); + orchReconnectTimer = null; + } + if (orchWs) { + orchWs.close(); + orchWs = null; + } + var dot = document.getElementById("orch-status-dot"); + var txt = document.getElementById("orch-status-text"); + if (dot) + dot.classList.remove("on"); + if (txt) + txt.textContent = "Offline"; + } + async function saveLogSnapshot() { + try { + var res = await fetch(API_BASE + "/miniapp/api/logs/snapshot?initData=" + encodeURIComponent(initData), { + method: "POST" + }); + if (!res.ok) + throw new Error("API error: " + res.status); + var data = await res.json(); + if (data.download_url) { + var a = document.createElement("a"); + a.href = API_BASE + data.download_url + "?initData=" + encodeURIComponent(initData); + a.download = ""; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + } catch (e) {} + } + window.sendCustomCmd = sendCustomCmd; + window.sendSkillCommand = sendSkillCommand; + window.startPlan = startPlan; + window.toggleSystemPrompt = toggleSystemPrompt; + window.loadGit = loadGit; + window.saveLogSnapshot = saveLogSnapshot; +})(); diff --git a/pkg/miniapp/static/dist/map.js b/pkg/miniapp/static/dist/map.js new file mode 100644 index 000000000..3aad21712 --- /dev/null +++ b/pkg/miniapp/static/dist/map.js @@ -0,0 +1,212 @@ +// map.js — Orchestration Room +// +// External asset: drop map.png (320×320px) next to index.html to replace +// the procedural fallback. Character positions (MAP_POSITIONS) are defined +// in canvas-pixel coordinates and remain valid regardless of which rendering +// path is used — just make sure your map.png matches them. +// +// Usage: +// loadMapAsset(function() { drawMap(ctx); }); // call once on init +// drawMap(ctx); // call each frame + +// ─── Character home positions (px, canvas 320×320) ───────────────────────── +// +// ┌──────────────────────────────┐ +// │ [conductor desk] │ y ≈ 20–50 +// │ 👑(160,58) 👩‍💼(108,58) │ +// │ [carpet] │ +// │ [WS1] [WS2] [WS3] │ y ≈ 80–100 +// │ 🔍40 💻144 📊248 │ y = 106 +// │ [meeting area] │ y ≈ 130–192 +// │ [WS4] [WS5] │ y ≈ 200–220 +// │ 🔧40 🎯144 │ y = 222 +// │ 🚪(160,308) │ door +// └──────────────────────────────┘ + +var MAP_POSITIONS = { + door: { x: 160, y: 314 }, // entry / exit point + conductor: { x: 160, y: 58 }, + secretary: { x: 108, y: 58 }, + heartbeat: { x: 230, y: 58 }, // pigeon messenger — periodic heartbeat agent + meeting: { x: 160, y: 161 }, // neutral zone for conversations + stations: [ + { x: 40, y: 106 }, // S0 scout + { x: 144, y: 106 }, // S1 analyst + { x: 248, y: 106 }, // S2 coder + { x: 40, y: 222 }, // S3 worker + { x: 144, y: 222 }, // S4 coordinator + ], +}; + +// ─── Asset loading ────────────────────────────────────────────────────────── + +var _mapImage = null; + +// Call once before first draw. cb() is invoked when ready (image or fallback). +function loadMapAsset(cb) { + var img = new Image(); + img.onload = function() { _mapImage = img; cb(); }; + img.onerror = function() { cb(); }; // no map.png → use fallback + img.src = './map.png'; +} + +// ─── Public draw entry point ──────────────────────────────────────────────── + +function drawMap(ctx) { + ctx.imageSmoothingEnabled = false; + if (_mapImage) { + ctx.drawImage(_mapImage, 0, 0, 320, 320); + } else { + _drawMapFallback(ctx); + } +} + +// ─── Procedural fallback ──────────────────────────────────────────────────── + +var _C = { + wallDark: '#0c1018', + wallHighlight: '#252d3f', + floorA: '#171b2c', + floorB: '#1b2033', + carpetBase: '#1a2050', + carpetBorder: '#2a3480', + deskBack: '#2c3e6b', + deskTop: '#3a50a0', + deskEdge: '#4a6ac0', + deskShadow: '#1a2448', + monitorFrame: '#070b14', + monitorBlue: '#1040a0', + monitorGlow: '#4488ff', + wsBase: '#162818', + wsTop: '#1e3822', + wsEdge: '#2a5030', + termGlow: '#00dd55', + rugFill: '#1c2248', + rugBorder: '#283070', + doorMid: '#8a5818', + doorLight: '#a06820', + doorGold: '#c8940a', +}; + +function _r(ctx, color, x, y, w, h, alpha) { + ctx.globalAlpha = alpha === undefined ? 1 : alpha; + ctx.fillStyle = color; + ctx.fillRect(x, y, w, h); + ctx.globalAlpha = 1; +} + +function _b(ctx, color, x, y, w, h) { + ctx.strokeStyle = color; + ctx.lineWidth = 1; + ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1); +} + +function _dot(ctx, color, x, y) { + ctx.fillStyle = color; + ctx.fillRect(x, y, 2, 2); +} + +function _workstation(ctx, x, y) { + _r(ctx, _C.wsBase, x, y, 48, 20); + _r(ctx, _C.wsTop, x, y, 48, 8); + _r(ctx, _C.wsEdge, x, y, 2, 20); + _r(ctx, _C.wsEdge, x+46, y, 2, 20); + _r(ctx, _C.wsEdge, x, y, 48, 2); + // terminal screen + _r(ctx, _C.monitorFrame, x+16, y+2, 16, 12); + _r(ctx, '#041008', x+17, y+3, 14, 10); + _r(ctx, '#003315', x+18, y+4, 12, 8); + _r(ctx, _C.termGlow, x+20, y+6, 8, 3); + _dot(ctx, '#00ff88', x+22, y+6); +} + +function _drawMapFallback(ctx) { + var T = 16; + + // floor tiles + for (var ty = 0; ty < 20; ty++) { + for (var tx = 0; tx < 20; tx++) { + ctx.fillStyle = (tx + ty) % 2 === 0 ? _C.floorA : _C.floorB; + ctx.fillRect(tx * T, ty * T, T, T); + } + } + + // conductor carpet + _r(ctx, _C.carpetBase, 16, 16, 288, 50); + _b(ctx, _C.carpetBorder, 18, 18, 284, 46); + + // conductor desk + _r(ctx, _C.deskBack, 96, 20, 128, 30); + _r(ctx, _C.deskTop, 96, 20, 128, 12); + _r(ctx, _C.deskEdge, 96, 20, 128, 2); + _r(ctx, _C.deskEdge, 96, 20, 2, 30); + _r(ctx, _C.deskEdge, 222, 20, 2, 30); + _r(ctx, _C.deskShadow,96,48, 128, 4); + // monitor + _r(ctx, _C.monitorFrame, 138, 22, 44, 14); + _r(ctx, _C.monitorBlue, 140, 23, 40, 12); + _r(ctx, _C.monitorGlow, 156, 26, 8, 6); + _r(ctx, '#6699ff', 158, 27, 4, 3); + + // workstations + _workstation(ctx, 16, 80); // S0 + _workstation(ctx, 128, 80); // S1 (x+24 = 152 ≈ 144 center) + _workstation(ctx, 224, 80); // S2 + _workstation(ctx, 16, 200); // S3 + _workstation(ctx, 128, 200); // S4 + + // meeting rug + _r(ctx, _C.rugFill, 64, 130, 192, 62, 0.55); + _b(ctx, _C.rugBorder, 66, 132, 188, 58); + _b(ctx, '#202860', 70, 136, 180, 50); + + // bulletin board (left wall) + _r(ctx, '#2c1a06', 18, 148, 36, 44); + _r(ctx, '#3a2508', 20, 150, 32, 40); + _r(ctx, '#cc9900', 22, 153, 12, 8); + _r(ctx, '#dd8800', 22, 164, 10, 6); + _r(ctx, '#bb7700', 34, 155, 13, 8); + _r(ctx, '#ccaa00', 33, 165, 11, 6); + _dot(ctx, '#ff4444', 28, 153); + _dot(ctx, '#44aaff', 41, 158); + _dot(ctx, '#44ff88', 27, 165); + + // server rack (right wall) + _r(ctx, '#111122', 285, 80, 18, 112); + _r(ctx, '#181830', 287, 82, 14, 108); + for (var i = 0; i < 10; i++) { + var ry = 85 + i * 10; + _r(ctx, '#0a0a12', 288, ry, 12, 8); + var lc = ['#00ff44','#0044ff','#ff3300','#111111'][i % 4]; + _r(ctx, lc, 296, ry + 2, 3, 4); + } + + // walls (drawn last to cover any overruns) + _r(ctx, _C.wallDark, 0, 0, 320, 16); + _r(ctx, _C.wallHighlight, 0, 14, 320, 2); + _r(ctx, _C.wallDark, 0, 0, 16, 320); + _r(ctx, _C.wallHighlight,14, 0, 2, 320); + _r(ctx, _C.wallDark, 304, 0, 16, 320); + _r(ctx, _C.wallHighlight,304, 0, 2, 320); + _r(ctx, _C.wallDark, 0, 304, 144, 16); + _r(ctx, _C.wallDark, 176, 304, 144, 16); + _r(ctx, _C.wallHighlight, 0, 304, 144, 2); + _r(ctx, _C.wallHighlight,176, 304, 144, 2); + + // door + _r(ctx, '#0a0808', 144, 292, 32, 12); // outside (dark) + _r(ctx, _C.doorMid, 144, 280, 32, 24); + _r(ctx, _C.doorLight, 144, 280, 32, 3); + _r(ctx, _C.doorLight, 144, 280, 3, 24); + _r(ctx, _C.doorLight, 173, 280, 3, 24); + _r(ctx, '#4a2408', 146, 284, 12, 16); // door panels + _r(ctx, '#4a2408', 162, 284, 12, 16); + _r(ctx, _C.doorGold, 170, 291, 5, 5); // handle +} + +// Expose map helpers for app.js runtime. +globalThis.MAP_POSITIONS = MAP_POSITIONS; +globalThis.loadMapAsset = loadMapAsset; +globalThis.drawMap = drawMap; + + diff --git a/pkg/miniapp/static/index.html b/pkg/miniapp/static/index.html index 47237c1a3..8a2d6b7e1 100644 --- a/pkg/miniapp/static/index.html +++ b/pkg/miniapp/static/index.html @@ -5,896 +5,9 @@ PicoClaw Dashboard - + - + @@ -955,6 +68,11 @@
    +
    + + 1/1 (0) + +
    @@ -1042,1301 +160,10 @@ + + + diff --git a/pkg/miniapp/static/map-preview.html b/pkg/miniapp/static/map-preview.html index 5cadcae5d..394dc802b 100644 --- a/pkg/miniapp/static/map-preview.html +++ b/pkg/miniapp/static/map-preview.html @@ -96,7 +96,7 @@ .demo-bar span { color: #3a4a80; } .demo-bar span.on { color: #4ade80; } - + @@ -456,3 +456,4 @@ loadMapAsset(function() { + diff --git a/todo/TASKS-4.md b/todo/TASKS-4.md index 94ec0b50b..feae9611f 100644 --- a/todo/TASKS-4.md +++ b/todo/TASKS-4.md @@ -72,3 +72,34 @@ Fields 表示バグが検出されずに ship された前科あり。 - `ORCH_ENABLED` の注入がテンプレートまたはビルド時変数で動作 - `renderLogs()` のユニットテストが CI で実行される - `go build ./...` が変わらず動作 (embed パスの整合性) + +## 作業報告 (2026-03-05) + +- 実装完了: タスク 1 / 2 / 3 +- 主要変更: + - `pkg/miniapp/miniapp.go` + - `//go:embed static` に変更 + - `/miniapp/` を `http.FileServer(http.FS(...))` で一括配信 + - `/miniapp` / `/miniapp/index.html` は `html/template` で描画し `OrchEnabled` を注入 + - 旧 `serveMapJS` 個別ルートを削除 + - フロントエンド分離 + - `pkg/miniapp/frontend/src/` に `app.js` / `styles.css` / `map.js` / `logs_view.js` を配置 + - `pkg/miniapp/static/index.html` はテンプレート + 外部アセット参照へ縮小 + - `pkg/miniapp/static/dist/` に `app.js` / `app.css` / `map.js` を生成 + - バンドル導線 + - `pkg/miniapp/frontend/build.mjs` + `package.json` を追加 + - `go generate ./...` で `bun run --cwd frontend build` が実行されるよう接続 + - Log viewer 強化 + - `logs_view.js` へレンダリングロジックを抽出 + - フィルタ/ページネーションを追加 + - `vitest + happy-dom` による unit test を追加 (`logs_view.test.js`) + - CI 更新 (`.github/workflows/pr.yml`) + - Bun セットアップを追加 + - frontend の `pnpm test` を追加 + +- 検証結果: + - `bun run --cwd pkg/miniapp/frontend build`: 成功 + - `pnpm --dir pkg/miniapp/frontend test`: 成功 + - `go generate ./...`: 成功 + - `go test ./pkg/miniapp/...`: 成功 + - `go test ./...`: 一部既存の環境依存テストが Windows 環境で失敗(TASK-4差分外)