From 2f156f4ef562d7bcc907e32bd7e84da2cdb58849 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
+
+
+ Start
+
+
`;
+ 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 += `
+
+
+
+
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 `
+
+ ${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}
+
+ Show System Prompt
+
+
+
`;
+}
+
+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 = '' +
+ kids.map(renderTreeNode).join('') + ' ';
+ }
+ return '' +
+ '' + icon + ' ' +
+ '' + escapeHtml(label) + ' ' +
+ 'turns=' + n.turn_count + ' ' +
+ childHtml + ' ';
+ }
+
+ var html = 'Session Graph
' +
+ '
' + roots.map(renderTreeNode).join('') + ' ';
+ 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) + '
' +
+ '
' +
+ '
' +
+ 'Merge ' +
+ 'Dispose ' +
+ '
' +
+ '
';
+ });
+ 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 = '\u2190 ' + escapeHtml(repo.name || gitSelectedRepo) + ' ';
+
+ 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
+
+
+ Start
+
+
`;
+ 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 += `
+
+
+
+
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 `
+
+ ${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}
+
+ Show System Prompt
+
+
+
`;
+ }
+ 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 = '' + kids.map(renderTreeNode).join("") + " ";
+ }
+ return '' + '' + icon + " " + '' + escapeHtml2(label) + " " + 'turns=' + n.turn_count + " " + childHtml + " ";
+ }
+ var html = 'Session Graph
' + '
' + roots.map(renderTreeNode).join("") + " ";
+ 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) + "
" + "
" + '
' + 'Merge ' + 'Dispose ' + "
" + "
";
+ });
+ 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 = '← ' + escapeHtml2(repo.name || gitSelectedRepo) + " ";
+ 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 @@
Console
+
Save Snapshot
@@ -1042,1301 +160,10 @@
+