diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml
new file mode 100644
index 0000000000..3323f5464c
--- /dev/null
+++ b/.github/actionlint.yaml
@@ -0,0 +1,8 @@
+# actionlint configuration
+# https://github.com/rhysd/actionlint/blob/main/docs/config.md
+
+paths:
+ .github/workflows/release.yml:
+ ignore:
+ # Optional signing secrets — linter cannot verify repo secrets exist
+ - "Context access might be invalid:.+"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 851d89a897..a455f5f96c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -163,19 +163,19 @@ jobs:
[ -z "$CSC_LINK" ] && unset CSC_LINK CSC_KEY_PASSWORD
[ -z "$APPLE_ID" ] && unset APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID
[ -z "$AZURE_TENANT_ID" ] && unset AZURE_TENANT_ID AZURE_CLIENT_ID AZURE_CLIENT_SECRET
- bunx electron-builder build ${{ matrix.platform.electron-args }} -c ./electron-builder.config.json -p never
+ npx electron-builder build ${{ matrix.platform.electron-args }} -c ./electron-builder.config.json -p never
working-directory: apps/app/electron
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- CSC_LINK: ${{ secrets.CSC_LINK }}
- CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
- APPLE_ID: ${{ secrets.APPLE_ID }}
- APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
- APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
- AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
- AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
+ CSC_LINK: ${{ secrets.CSC_LINK || '' }}
+ CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD || '' }}
+ APPLE_ID: ${{ secrets.APPLE_ID || '' }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD || '' }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID || '' }}
+ AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID || '' }}
+ AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID || '' }}
+ AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET || '' }}
# If signing failed, retry without signing so we still get an unsigned artifact
- name: Package desktop app (unsigned fallback)
@@ -193,7 +193,7 @@ jobs:
"
unset CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID
unset AZURE_TENANT_ID AZURE_CLIENT_ID AZURE_CLIENT_SECRET
- CSC_IDENTITY_AUTO_DISCOVERY=false bunx electron-builder build ${{ matrix.platform.electron-args }} -c ./eb-unsigned.json -p never
+ CSC_IDENTITY_AUTO_DISCOVERY=false npx electron-builder build ${{ matrix.platform.electron-args }} -c ./eb-unsigned.json -p never
working-directory: apps/app/electron
shell: bash
env:
diff --git a/.gitignore b/.gitignore
index 5475a3bd65..9c49ba7fef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,6 +41,9 @@ skills/
apps/app/electron/.eliza
apps/app/electron/skills/
+# Temporary capture files
+temp_capture_*.jpg
+
# Agent learning docs & knowledge base
knowledge/
scripts/knowledge-loop.mjs
\ No newline at end of file
diff --git a/apps/app/electron/package.json b/apps/app/electron/package.json
index c33e1a9c99..3c1dca1871 100644
--- a/apps/app/electron/package.json
+++ b/apps/app/electron/package.json
@@ -33,8 +33,8 @@
"whisper-node": "^1.1.1"
},
"devDependencies": {
- "electron": "^26.6.10",
- "electron-builder": "^25.1.8",
+ "electron": "26.6.10",
+ "electron-builder": "^26.7.0",
"electron-rebuild": "^3.2.9",
"typescript": "^5.0.4"
},
diff --git a/apps/app/electron/src/setup.ts b/apps/app/electron/src/setup.ts
index 6e07b0c489..3dabefdb90 100644
--- a/apps/app/electron/src/setup.ts
+++ b/apps/app/electron/src/setup.ts
@@ -278,7 +278,7 @@ export function setupContentSecurityPolicy(customScheme: string): void {
`default-src 'self' ${customScheme}://* https://*`,
`script-src 'self' ${customScheme}://* 'unsafe-inline'${electronIsDev ? " 'unsafe-eval' devtools://*" : ''}`,
`style-src 'self' ${customScheme}://* 'unsafe-inline'`,
- `connect-src 'self' ${customScheme}://* http://localhost:* ws://localhost:* wss://localhost:* https://* wss://*`,
+ `connect-src 'self' ${customScheme}://* blob: http://localhost:* ws://localhost:* wss://localhost:* https://* wss://*`,
`img-src 'self' ${customScheme}://* data: blob: https://*`,
`media-src 'self' ${customScheme}://* blob: https://*`,
`font-src 'self' ${customScheme}://* data:`,
diff --git a/apps/app/index.html b/apps/app/index.html
index 25f15841ec..a51726184c 100644
--- a/apps/app/index.html
+++ b/apps/app/index.html
@@ -12,7 +12,7 @@
content="default-src 'self' capacitor-electron://* https://*;
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
- connect-src 'self' http://localhost:* ws://localhost:* wss://localhost:* https://* wss://*;
+ connect-src 'self' blob: http://localhost:* ws://localhost:* wss://localhost:* https://* wss://*;
img-src 'self' data: blob: https://*;
media-src 'self' blob: https://*;
font-src 'self' data:;" />
diff --git a/apps/app/package-lock.json b/apps/app/package-lock.json
index 2455452566..36f21ec3ab 100644
--- a/apps/app/package-lock.json
+++ b/apps/app/package-lock.json
@@ -27,14 +27,20 @@
"@milaidy/capacitor-talkmode": "file:plugins/talkmode",
"@noble/ed25519": "3.0.0",
"dompurify": "^3.3.1",
- "lit": "^3.3.2",
- "marked": "^17.0.1"
+ "marked": "^17.0.1",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
},
"devDependencies": {
"@capacitor-community/electron": "^5.0.1",
"@capacitor/cli": "8.0.2",
+ "@tailwindcss/vite": "^4.1.18",
"@types/dompurify": "^3.0.5",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "@vitejs/plugin-react": "^5.1.3",
"esbuild": "^0.27.3",
+ "tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^5.4.2",
"vitest": "^2.0.0"
@@ -51,13 +57,238 @@
"@babel/highlight": "^7.10.4"
}
},
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core/node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
"engines": {
"node": ">=6.9.0"
}
@@ -164,6 +395,132 @@
"node": ">=4"
}
},
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template/node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@capacitor-community/electron": {
"version": "5.0.1",
"dev": true,
@@ -1565,20 +1922,52 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"dev": true,
"license": "MIT"
},
- "node_modules/@lit-labs/ssr-dom-shim": {
- "version": "1.5.1",
- "license": "BSD-3-Clause"
- },
- "node_modules/@lit/reactive-element": {
- "version": "2.1.2",
- "license": "BSD-3-Clause",
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "@lit-labs/ssr-dom-shim": "^1.5.0"
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@milaidy/capacitor-agent": {
@@ -1673,6 +2062,13 @@
"node": ">=14"
}
},
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz",
+ "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@rollup/plugin-node-resolve": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
@@ -2083,6 +2479,323 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@tailwindcss/node": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
+ "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.4",
+ "enhanced-resolve": "^5.18.3",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.30.2",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.1.18"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
+ "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.1.18",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.18",
+ "@tailwindcss/oxide-darwin-x64": "4.1.18",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.18",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.18",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.18",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
+ "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
+ "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
+ "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
+ "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
+ "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
+ "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
+ "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
+ "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
+ "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
+ "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@emnapi/wasi-threads": "^1.1.0",
+ "@napi-rs/wasm-runtime": "^1.1.0",
+ "@tybys/wasm-util": "^0.10.1",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
+ "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
+ "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz",
+ "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.1.18",
+ "@tailwindcss/oxide": "4.1.18",
+ "tailwindcss": "4.1.18"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
"node_modules/@types/dompurify": {
"version": "3.2.0",
"deprecated": "This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.",
@@ -2127,6 +2840,26 @@
"undici-types": "~7.16.0"
}
},
+ "node_modules/@types/react": {
+ "version": "19.2.13",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
+ "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -2141,7 +2874,8 @@
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "4.33.0",
@@ -2314,6 +3048,27 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.3.tgz",
+ "integrity": "sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.29.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-rc.2",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
"node_modules/@vitest/expect": {
"version": "2.1.9",
"dev": true,
@@ -2712,6 +3467,16 @@
],
"license": "MIT"
},
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
+ "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
"node_modules/big-integer": {
"version": "1.6.52",
"dev": true,
@@ -2762,6 +3527,40 @@
"node": ">=8"
}
},
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
"node_modules/buffer": {
"version": "5.7.1",
"dev": true,
@@ -2861,6 +3660,27 @@
"node": ">=6"
}
},
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001769",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
+ "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
"node_modules/chai": {
"version": "5.3.3",
"dev": true,
@@ -2975,6 +3795,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"dev": true,
@@ -2988,6 +3815,13 @@
"node": ">= 8"
}
},
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/data-view-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@@ -3138,6 +3972,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -3201,6 +4045,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.286",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
+ "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/elementtree": {
"version": "0.1.7",
"dev": true,
@@ -3222,6 +4073,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/enhanced-resolve": {
+ "version": "5.19.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
+ "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/enquirer": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
@@ -3441,6 +4306,16 @@
"@esbuild/win32-x64": "0.27.3"
}
},
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -4274,6 +5149,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -5133,13 +6018,22 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/js-yaml": {
"version": "3.14.2",
@@ -5156,6 +6050,19 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/json-buffer": {
"version": "3.0.1",
"dev": true,
@@ -5229,29 +6136,265 @@
"node": ">= 0.8.0"
}
},
- "node_modules/lit": {
- "version": "3.3.2",
- "license": "BSD-3-Clause",
+ "node_modules/lightningcss": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
+ "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
+ "dev": true,
+ "license": "MPL-2.0",
"dependencies": {
- "@lit/reactive-element": "^2.1.0",
- "lit-element": "^4.2.0",
- "lit-html": "^3.3.0"
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.30.2",
+ "lightningcss-darwin-arm64": "1.30.2",
+ "lightningcss-darwin-x64": "1.30.2",
+ "lightningcss-freebsd-x64": "1.30.2",
+ "lightningcss-linux-arm-gnueabihf": "1.30.2",
+ "lightningcss-linux-arm64-gnu": "1.30.2",
+ "lightningcss-linux-arm64-musl": "1.30.2",
+ "lightningcss-linux-x64-gnu": "1.30.2",
+ "lightningcss-linux-x64-musl": "1.30.2",
+ "lightningcss-win32-arm64-msvc": "1.30.2",
+ "lightningcss-win32-x64-msvc": "1.30.2"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
+ "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/lit-element": {
- "version": "4.2.2",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@lit-labs/ssr-dom-shim": "^1.5.0",
- "@lit/reactive-element": "^2.1.0",
- "lit-html": "^3.3.0"
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
+ "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/lit-html": {
- "version": "3.3.2",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@types/trusted-types": "^2.0.2"
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
+ "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
+ "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
+ "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
+ "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
+ "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
+ "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
+ "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
+ "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
+ "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
"node_modules/locate-path": {
@@ -5512,6 +6655,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -5991,6 +7141,37 @@
],
"license": "MIT"
},
+ "node_modules/react": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.4"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/readable-stream": {
"version": "3.6.2",
"dev": true,
@@ -6293,6 +7474,12 @@
"node": ">=11.0.0"
}
},
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
"node_modules/semver": {
"version": "7.7.3",
"dev": true,
@@ -6749,6 +7936,27 @@
"license": "MIT",
"peer": true
},
+ "node_modules/tailwindcss": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
+ "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
+ "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
"node_modules/tar": {
"version": "7.5.7",
"dev": true,
@@ -7030,6 +8238,37 @@
"node": ">=8"
}
},
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
diff --git a/apps/app/package.json b/apps/app/package.json
index f9528216ff..7d94a5fad4 100644
--- a/apps/app/package.json
+++ b/apps/app/package.json
@@ -33,6 +33,7 @@
"@capacitor/keyboard": "8.0.0",
"@capacitor/preferences": "8.0.0",
"@capacitor/status-bar": "8.0.0",
+ "@milaidy/capacitor-agent": "file:plugins/agent",
"@milaidy/capacitor-camera": "file:plugins/camera",
"@milaidy/capacitor-canvas": "file:plugins/canvas",
"@milaidy/capacitor-desktop": "file:plugins/desktop",
@@ -40,23 +41,19 @@
"@milaidy/capacitor-location": "file:plugins/location",
"@milaidy/capacitor-screencapture": "file:plugins/screencapture",
"@milaidy/capacitor-swabble": "file:plugins/swabble",
- "@milaidy/capacitor-agent": "file:plugins/agent",
"@milaidy/capacitor-talkmode": "file:plugins/talkmode",
- "@noble/ed25519": "3.0.0",
- "dompurify": "^3.3.1",
- "marked": "^17.0.1",
+ "@pixiv/three-vrm": "^3.4.5",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "three": "^0.182.0"
},
"devDependencies": {
- "@capacitor-community/electron": "^5.0.1",
"@capacitor/cli": "8.0.2",
- "@types/dompurify": "^3.0.5",
"@tailwindcss/vite": "^4.1.18",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
+ "@types/three": "^0.182.0",
"@vitejs/plugin-react": "^5.1.3",
- "esbuild": "^0.27.3",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^5.4.2",
diff --git a/apps/app/public/animations/Idle.fbx b/apps/app/public/animations/Idle.fbx
new file mode 100644
index 0000000000..5a5a107432
Binary files /dev/null and b/apps/app/public/animations/Idle.fbx differ
diff --git a/apps/app/public/animations/Standing Greeting.fbx b/apps/app/public/animations/Standing Greeting.fbx
new file mode 100644
index 0000000000..3db5cb1ca0
Binary files /dev/null and b/apps/app/public/animations/Standing Greeting.fbx differ
diff --git a/apps/app/public/animations/idle.glb b/apps/app/public/animations/idle.glb
new file mode 100644
index 0000000000..e1b00c6347
Binary files /dev/null and b/apps/app/public/animations/idle.glb differ
diff --git a/apps/app/public/logos/anthropic-icon-white.png b/apps/app/public/logos/anthropic-icon-white.png
new file mode 100644
index 0000000000..a4760d1665
Binary files /dev/null and b/apps/app/public/logos/anthropic-icon-white.png differ
diff --git a/apps/app/public/logos/anthropic-icon.png b/apps/app/public/logos/anthropic-icon.png
new file mode 100644
index 0000000000..ea30ab63a5
Binary files /dev/null and b/apps/app/public/logos/anthropic-icon.png differ
diff --git a/apps/app/public/logos/claude-icon.png b/apps/app/public/logos/claude-icon.png
new file mode 100644
index 0000000000..3bb97a9293
Binary files /dev/null and b/apps/app/public/logos/claude-icon.png differ
diff --git a/apps/app/public/logos/deepseek-icon.png b/apps/app/public/logos/deepseek-icon.png
new file mode 100644
index 0000000000..50f74f7b0d
Binary files /dev/null and b/apps/app/public/logos/deepseek-icon.png differ
diff --git a/apps/app/public/logos/elizaos-icon.png b/apps/app/public/logos/elizaos-icon.png
new file mode 100644
index 0000000000..f4939a99fb
Binary files /dev/null and b/apps/app/public/logos/elizaos-icon.png differ
diff --git a/apps/app/public/logos/gemini-icon.png b/apps/app/public/logos/gemini-icon.png
new file mode 100644
index 0000000000..9c186fa08a
Binary files /dev/null and b/apps/app/public/logos/gemini-icon.png differ
diff --git a/apps/app/public/logos/grok-icon-white.png b/apps/app/public/logos/grok-icon-white.png
new file mode 100644
index 0000000000..e1d798668e
Binary files /dev/null and b/apps/app/public/logos/grok-icon-white.png differ
diff --git a/apps/app/public/logos/grok-icon.png b/apps/app/public/logos/grok-icon.png
new file mode 100644
index 0000000000..ac83021513
Binary files /dev/null and b/apps/app/public/logos/grok-icon.png differ
diff --git a/apps/app/public/logos/groq-icon-white.png b/apps/app/public/logos/groq-icon-white.png
new file mode 100644
index 0000000000..b82486039b
Binary files /dev/null and b/apps/app/public/logos/groq-icon-white.png differ
diff --git a/apps/app/public/logos/groq-icon.png b/apps/app/public/logos/groq-icon.png
new file mode 100644
index 0000000000..12180dab1d
Binary files /dev/null and b/apps/app/public/logos/groq-icon.png differ
diff --git a/apps/app/public/logos/mistral-icon.png b/apps/app/public/logos/mistral-icon.png
new file mode 100644
index 0000000000..f6810142f7
Binary files /dev/null and b/apps/app/public/logos/mistral-icon.png differ
diff --git a/apps/app/public/logos/ollama-icon-white.png b/apps/app/public/logos/ollama-icon-white.png
new file mode 100644
index 0000000000..873fe7c02c
Binary files /dev/null and b/apps/app/public/logos/ollama-icon-white.png differ
diff --git a/apps/app/public/logos/ollama-icon.png b/apps/app/public/logos/ollama-icon.png
new file mode 100644
index 0000000000..0299134546
Binary files /dev/null and b/apps/app/public/logos/ollama-icon.png differ
diff --git a/apps/app/public/logos/openai-icon-white.png b/apps/app/public/logos/openai-icon-white.png
new file mode 100644
index 0000000000..44c0560c30
Binary files /dev/null and b/apps/app/public/logos/openai-icon-white.png differ
diff --git a/apps/app/public/logos/openai-icon.png b/apps/app/public/logos/openai-icon.png
new file mode 100644
index 0000000000..147e939a46
Binary files /dev/null and b/apps/app/public/logos/openai-icon.png differ
diff --git a/apps/app/public/logos/openrouter-icon-white.png b/apps/app/public/logos/openrouter-icon-white.png
new file mode 100644
index 0000000000..8118d79fa3
Binary files /dev/null and b/apps/app/public/logos/openrouter-icon-white.png differ
diff --git a/apps/app/public/logos/openrouter-icon.png b/apps/app/public/logos/openrouter-icon.png
new file mode 100644
index 0000000000..12ea4bcf61
Binary files /dev/null and b/apps/app/public/logos/openrouter-icon.png differ
diff --git a/apps/app/public/logos/together-ai-icon.png b/apps/app/public/logos/together-ai-icon.png
new file mode 100644
index 0000000000..af4e63ecfc
Binary files /dev/null and b/apps/app/public/logos/together-ai-icon.png differ
diff --git a/apps/app/public/logos/zai-icon-white.png b/apps/app/public/logos/zai-icon-white.png
new file mode 100644
index 0000000000..b4e730fadf
Binary files /dev/null and b/apps/app/public/logos/zai-icon-white.png differ
diff --git a/apps/app/public/logos/zai-icon.png b/apps/app/public/logos/zai-icon.png
new file mode 100644
index 0000000000..8cfe5351cd
Binary files /dev/null and b/apps/app/public/logos/zai-icon.png differ
diff --git a/apps/app/public/screenshotter.html b/apps/app/public/screenshotter.html
new file mode 100644
index 0000000000..994cdfaf17
--- /dev/null
+++ b/apps/app/public/screenshotter.html
@@ -0,0 +1,399 @@
+
+
+
+
+VRM Screenshotter
+
+
+
+VRM Screenshotter
+Renders each VRM in idle pose and captures a framed preview image.
+
+
+
+
+
+
+
+
+
diff --git a/apps/app/public/vrms/1.vrm b/apps/app/public/vrms/1.vrm
new file mode 100644
index 0000000000..41d4915df9
Binary files /dev/null and b/apps/app/public/vrms/1.vrm differ
diff --git a/apps/app/public/vrms/2.vrm b/apps/app/public/vrms/2.vrm
new file mode 100644
index 0000000000..2af0a87baf
Binary files /dev/null and b/apps/app/public/vrms/2.vrm differ
diff --git a/apps/app/public/vrms/3.vrm b/apps/app/public/vrms/3.vrm
new file mode 100644
index 0000000000..ced900e0d0
Binary files /dev/null and b/apps/app/public/vrms/3.vrm differ
diff --git a/apps/app/public/vrms/4.vrm b/apps/app/public/vrms/4.vrm
new file mode 100644
index 0000000000..936b46687e
Binary files /dev/null and b/apps/app/public/vrms/4.vrm differ
diff --git a/apps/app/public/vrms/5.vrm b/apps/app/public/vrms/5.vrm
new file mode 100644
index 0000000000..98f9972312
Binary files /dev/null and b/apps/app/public/vrms/5.vrm differ
diff --git a/apps/app/public/vrms/6.vrm b/apps/app/public/vrms/6.vrm
new file mode 100644
index 0000000000..6290378f26
Binary files /dev/null and b/apps/app/public/vrms/6.vrm differ
diff --git a/apps/app/public/vrms/7.vrm b/apps/app/public/vrms/7.vrm
new file mode 100644
index 0000000000..d624e449ef
Binary files /dev/null and b/apps/app/public/vrms/7.vrm differ
diff --git a/apps/app/public/vrms/8.vrm b/apps/app/public/vrms/8.vrm
new file mode 100644
index 0000000000..d80d4d6293
Binary files /dev/null and b/apps/app/public/vrms/8.vrm differ
diff --git a/apps/app/public/vrms/previews/milady-1.png b/apps/app/public/vrms/previews/milady-1.png
new file mode 100644
index 0000000000..d079f964c3
Binary files /dev/null and b/apps/app/public/vrms/previews/milady-1.png differ
diff --git a/apps/app/public/vrms/previews/milady-2.png b/apps/app/public/vrms/previews/milady-2.png
new file mode 100644
index 0000000000..57058180b6
Binary files /dev/null and b/apps/app/public/vrms/previews/milady-2.png differ
diff --git a/apps/app/public/vrms/previews/milady-3.png b/apps/app/public/vrms/previews/milady-3.png
new file mode 100644
index 0000000000..92b509e3fb
Binary files /dev/null and b/apps/app/public/vrms/previews/milady-3.png differ
diff --git a/apps/app/public/vrms/previews/milady-4.png b/apps/app/public/vrms/previews/milady-4.png
new file mode 100644
index 0000000000..9fa215b8c4
Binary files /dev/null and b/apps/app/public/vrms/previews/milady-4.png differ
diff --git a/apps/app/public/vrms/previews/milady-5.png b/apps/app/public/vrms/previews/milady-5.png
new file mode 100644
index 0000000000..ffd5aa31a7
Binary files /dev/null and b/apps/app/public/vrms/previews/milady-5.png differ
diff --git a/apps/app/public/vrms/previews/milady-6.png b/apps/app/public/vrms/previews/milady-6.png
new file mode 100644
index 0000000000..121903f32c
Binary files /dev/null and b/apps/app/public/vrms/previews/milady-6.png differ
diff --git a/apps/app/public/vrms/previews/milady-7.png b/apps/app/public/vrms/previews/milady-7.png
new file mode 100644
index 0000000000..146bcc3ce8
Binary files /dev/null and b/apps/app/public/vrms/previews/milady-7.png differ
diff --git a/apps/app/public/vrms/previews/milady-8.png b/apps/app/public/vrms/previews/milady-8.png
new file mode 100644
index 0000000000..89593ab35e
Binary files /dev/null and b/apps/app/public/vrms/previews/milady-8.png differ
diff --git a/apps/app/src/App.tsx b/apps/app/src/App.tsx
index 46f9be9f6d..95bc11446d 100644
--- a/apps/app/src/App.tsx
+++ b/apps/app/src/App.tsx
@@ -11,14 +11,15 @@ import { OnboardingWizard } from "./components/OnboardingWizard.js";
import { ChatView } from "./components/ChatView.js";
import { ConversationsSidebar } from "./components/ConversationsSidebar.js";
import { WidgetSidebar } from "./components/WidgetSidebar.js";
-import { PluginsView } from "./components/PluginsView.js";
+import { FeaturesView, ConnectorsView } from "./components/PluginsView.js";
import { SkillsView } from "./components/SkillsView.js";
import { InventoryView } from "./components/InventoryView.js";
+import { CharacterView } from "./components/CharacterView.js";
import { ConfigView } from "./components/ConfigView.js";
-import { LogsView } from "./components/LogsView.js";
+import { AdminView } from "./components/AdminView.js";
import { AppsView } from "./components/AppsView.js";
import { GameView } from "./components/GameView.js";
-import { DatabaseView } from "./components/DatabaseView.js";
+import { LoadingScreen } from "./components/LoadingScreen.js";
function ViewRouter() {
const { tab } = useApp();
@@ -27,11 +28,12 @@ function ViewRouter() {
case "apps": return ;
case "game": return ;
case "inventory": return ;
- case "plugins": return ;
+ case "features": return ;
+ case "connectors": return ;
case "skills": return ;
- case "database": return ;
+ case "character": return ;
case "config": return ;
- case "logs": return ;
+ case "admin": return ;
default: return ;
}
}
@@ -40,11 +42,7 @@ export function App() {
const { onboardingLoading, authRequired, onboardingComplete, tab, actionNotice } = useApp();
if (onboardingLoading) {
- return (
-
- );
+ return ;
}
if (authRequired) return ;
diff --git a/apps/app/src/AppContext.tsx b/apps/app/src/AppContext.tsx
index a13ca57a33..3dee6fbf81 100644
--- a/apps/app/src/AppContext.tsx
+++ b/apps/app/src/AppContext.tsx
@@ -44,6 +44,21 @@ import {
import { tabFromPath, pathForTab, type Tab } from "./navigation";
import { SkillScanReportSummary } from "./api-client";
+// ── VRM helpers ─────────────────────────────────────────────────────────
+
+/** Number of built-in milady VRM avatars shipped with the app. */
+export const VRM_COUNT = 8;
+
+/** Resolve a built-in VRM index (1–8) to its public asset URL. */
+export function getVrmUrl(index: number): string {
+ return `/vrms/${index}.vrm`;
+}
+
+/** Resolve a built-in VRM index (1–8) to its preview thumbnail URL. */
+export function getVrmPreviewUrl(index: number): string {
+ return `/vrms/previews/milady-${index}.png`;
+}
+
// ── Theme ──────────────────────────────────────────────────────────────
const THEME_STORAGE_KEY = "milaidy:theme";
@@ -95,6 +110,7 @@ function applyTheme(name: ThemeName) {
export type OnboardingStep =
| "welcome"
| "name"
+ | "avatar"
| "style"
| "theme"
| "runMode"
@@ -102,7 +118,8 @@ export type OnboardingStep =
| "modelSelection"
| "cloudLogin"
| "llmProvider"
- | "inventorySetup";
+ | "inventorySetup"
+ | "connectors";
// ── Action notice ──────────────────────────────────────────────────────
@@ -195,6 +212,8 @@ export interface AppState {
characterSaveSuccess: string | null;
characterSaveError: string | null;
characterDraft: CharacterData;
+ selectedVrmIndex: number;
+ customVrmUrl: string;
// Cloud
cloudEnabled: boolean;
@@ -271,9 +290,21 @@ export interface AppState {
onboardingLargeModel: string;
onboardingProvider: string;
onboardingApiKey: string;
+ onboardingOpenRouterModel: string;
+ onboardingTelegramToken: string;
+ onboardingDiscordToken: string;
+ onboardingWhatsAppSessionPath: string;
+ onboardingTwilioAccountSid: string;
+ onboardingTwilioAuthToken: string;
+ onboardingTwilioPhoneNumber: string;
+ onboardingBlooioApiKey: string;
+ onboardingBlooioPhoneNumber: string;
+ onboardingSubscriptionTab: "token" | "oauth";
onboardingSelectedChains: Set;
onboardingRpcSelections: Record;
onboardingRpcKeys: Record;
+ onboardingAvatar: number;
+ onboardingRestarting: boolean;
// Command palette
commandPaletteOpen: boolean;
@@ -303,9 +334,6 @@ export interface AppState {
activeGameSandbox: string;
activeGamePostMessageAuth: boolean;
- // Config text
- configRaw: Record;
- configText: string;
}
export interface AppActions {
@@ -498,6 +526,8 @@ export function AppProvider({ children }: { children: ReactNode }) {
const [characterSaveSuccess, setCharacterSaveSuccess] = useState(null);
const [characterSaveError, setCharacterSaveError] = useState(null);
const [characterDraft, setCharacterDraft] = useState({});
+ const [selectedVrmIndex, setSelectedVrmIndex] = useState(1);
+ const [customVrmUrl, setCustomVrmUrl] = useState("");
// --- Cloud ---
const [cloudEnabled, setCloudEnabled] = useState(false);
@@ -567,16 +597,28 @@ export function AppProvider({ children }: { children: ReactNode }) {
const [onboardingOptions, setOnboardingOptions] = useState(null);
const [onboardingName, setOnboardingName] = useState("");
const [onboardingStyle, setOnboardingStyle] = useState("");
- const [onboardingTheme, setOnboardingTheme] = useState("milady");
+ const [onboardingTheme, setOnboardingTheme] = useState(loadTheme);
const [onboardingRunMode, setOnboardingRunMode] = useState<"local" | "cloud" | "">("");
const [onboardingCloudProvider, setOnboardingCloudProvider] = useState("");
- const [onboardingSmallModel, setOnboardingSmallModel] = useState("claude-haiku");
- const [onboardingLargeModel, setOnboardingLargeModel] = useState("claude-sonnet-4-5");
+ const [onboardingSmallModel, setOnboardingSmallModel] = useState("moonshotai/kimi-k2-turbo");
+ const [onboardingLargeModel, setOnboardingLargeModel] = useState("moonshotai/kimi-k2-0905");
const [onboardingProvider, setOnboardingProvider] = useState("");
const [onboardingApiKey, setOnboardingApiKey] = useState("");
+ const [onboardingOpenRouterModel, setOnboardingOpenRouterModel] = useState("");
+ const [onboardingTelegramToken, setOnboardingTelegramToken] = useState("");
+ const [onboardingDiscordToken, setOnboardingDiscordToken] = useState("");
+ const [onboardingWhatsAppSessionPath, setOnboardingWhatsAppSessionPath] = useState("");
+ const [onboardingTwilioAccountSid, setOnboardingTwilioAccountSid] = useState("");
+ const [onboardingTwilioAuthToken, setOnboardingTwilioAuthToken] = useState("");
+ const [onboardingTwilioPhoneNumber, setOnboardingTwilioPhoneNumber] = useState("");
+ const [onboardingBlooioApiKey, setOnboardingBlooioApiKey] = useState("");
+ const [onboardingBlooioPhoneNumber, setOnboardingBlooioPhoneNumber] = useState("");
+ const [onboardingSubscriptionTab, setOnboardingSubscriptionTab] = useState<"token" | "oauth">("token");
const [onboardingSelectedChains, setOnboardingSelectedChains] = useState>(new Set(["evm", "solana"]));
const [onboardingRpcSelections, setOnboardingRpcSelections] = useState>({});
const [onboardingRpcKeys, setOnboardingRpcKeys] = useState>({});
+ const [onboardingAvatar, setOnboardingAvatar] = useState(1);
+ const [onboardingRestarting, setOnboardingRestarting] = useState(false);
// --- Command palette ---
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
@@ -606,14 +648,14 @@ export function AppProvider({ children }: { children: ReactNode }) {
const [activeGameSandbox, setActiveGameSandbox] = useState("allow-scripts allow-same-origin allow-popups");
const [activeGamePostMessageAuth, setActiveGamePostMessageAuth] = useState(false);
- // --- Config ---
- const [configRaw, setConfigRaw] = useState>({});
- const [configText, setConfigText] = useState("");
// --- Refs for timers ---
const actionNoticeTimer = useRef(null);
const cloudPollInterval = useRef(null);
const cloudLoginPollTimer = useRef(null);
+ const prevAgentStateRef = useRef(null);
+ /** Guards against double-greeting when both init and state-transition paths fire. */
+ const greetingFiredRef = useRef(false);
// ── Action notice ──────────────────────────────────────────────────
@@ -730,7 +772,13 @@ export function AppProvider({ children }: { children: ReactNode }) {
try {
const { messages } = await client.getConversationMessages(convId);
setConversationMessages(messages);
- } catch {
+ } catch (err) {
+ // If the conversation no longer exists (server restarted), clear it
+ const status = (err as { status?: number }).status;
+ if (status === 404) {
+ setActiveConversationId(null);
+ setConversations([]);
+ }
setConversationMessages([]);
}
}, []);
@@ -843,9 +891,17 @@ export function AppProvider({ children }: { children: ReactNode }) {
const cloudStatus = await client.getCloudStatus().catch(() => null);
if (!cloudStatus) return;
setCloudEnabled(cloudStatus.enabled ?? false);
- setCloudConnected(cloudStatus.connected);
+ // Treat as connected if EITHER the backend says connected OR the config
+ // has the API key saved (hasApiKey). The CLOUD_AUTH service may not have
+ // refreshed yet, but the key is persisted and model calls will work.
+ const isConnected = cloudStatus.connected || (cloudStatus.enabled && cloudStatus.hasApiKey);
+ setCloudConnected(Boolean(isConnected));
setCloudUserId(cloudStatus.userId ?? null);
if (cloudStatus.topUpUrl) setCloudTopUpUrl(cloudStatus.topUpUrl);
+ // Only fetch credits when the backend confirms the auth service is
+ // actually connected. When hasApiKey is true but connected is false,
+ // the CLOUD_AUTH service hasn't authenticated yet -- fetching credits
+ // would just fail and spam warnings in the backend logs.
if (cloudStatus.connected) {
const credits = await client.getCloudCredits().catch(() => null);
if (credits) {
@@ -896,6 +952,10 @@ export function AppProvider({ children }: { children: ReactNode }) {
...(agentStatus ?? { agentName: "Milaidy", model: undefined, uptime: undefined, startedAt: undefined }),
state: "restarting",
});
+ // Server restart clears in-memory conversations — reset client state
+ setActiveConversationId(null);
+ setConversationMessages([]);
+ setConversations([]);
const s = await client.restartAgent();
setAgentStatus(s);
} catch {
@@ -940,16 +1000,37 @@ export function AppProvider({ children }: { children: ReactNode }) {
// ── Chat ───────────────────────────────────────────────────────────
+ /** Request an agent greeting for a conversation and add it to messages. */
+ const fetchGreeting = useCallback(async (convId: string) => {
+ setChatSending(true);
+ try {
+ const data = await client.requestGreeting(convId);
+ if (data.text) {
+ setConversationMessages((prev: ConversationMessage[]) => [
+ ...prev,
+ { id: `greeting-${Date.now()}`, role: "assistant", text: data.text, timestamp: Date.now() },
+ ]);
+ }
+ } catch {
+ /* greeting failed silently — user can still chat */
+ } finally {
+ setChatSending(false);
+ }
+ }, []);
+
const handleNewConversation = useCallback(async () => {
try {
const { conversation } = await client.createConversation();
setConversations((prev) => [conversation, ...prev]);
setActiveConversationId(conversation.id);
setConversationMessages([]);
+ // Agent sends the first message
+ greetingFiredRef.current = true;
+ void fetchGreeting(conversation.id);
} catch {
/* ignore */
}
- }, []);
+ }, [fetchGreeting]);
const handleChatSend = useCallback(async () => {
const text = chatInput.trim();
@@ -980,8 +1061,29 @@ export function AppProvider({ children }: { children: ReactNode }) {
...prev,
{ id: `temp-resp-${Date.now()}`, role: "assistant", text: data.text, timestamp: Date.now() },
]);
- } catch {
- await loadConversationMessages(convId);
+ } catch (err) {
+ // If the conversation was lost (server restart), create a fresh one and retry
+ const status = (err as { status?: number }).status;
+ if (status === 404) {
+ try {
+ const { conversation } = await client.createConversation();
+ setConversations((prev) => [conversation, ...prev]);
+ setActiveConversationId(conversation.id);
+ convId = conversation.id;
+ const data = await client.sendConversationMessage(convId, text);
+ setConversationMessages([
+ { id: `temp-${Date.now()}`, role: "user", text, timestamp: Date.now() },
+ { id: `temp-resp-${Date.now()}`, role: "assistant", text: data.text, timestamp: Date.now() },
+ ]);
+ } catch {
+ // Give up — show whatever we have
+ setConversationMessages([
+ { id: `temp-${Date.now()}`, role: "user", text, timestamp: Date.now() },
+ ]);
+ }
+ } else {
+ await loadConversationMessages(convId);
+ }
} finally {
setChatSending(false);
}
@@ -1441,6 +1543,9 @@ export function AppProvider({ children }: { children: ReactNode }) {
setOnboardingStep("name");
break;
case "name":
+ setOnboardingStep("avatar");
+ break;
+ case "avatar":
setOnboardingStep("style");
break;
case "style":
@@ -1455,10 +1560,8 @@ export function AppProvider({ children }: { children: ReactNode }) {
if (onboardingRunMode === "cloud") {
if (opts && opts.cloudProviders.length === 1) {
setOnboardingCloudProvider(opts.cloudProviders[0].id);
- setOnboardingStep("modelSelection");
- } else {
- setOnboardingStep("cloudProvider");
}
+ setOnboardingStep("cloudProvider");
} else {
setOnboardingStep("llmProvider");
}
@@ -1467,29 +1570,38 @@ export function AppProvider({ children }: { children: ReactNode }) {
setOnboardingStep("modelSelection");
break;
case "modelSelection":
- setOnboardingStep("cloudLogin");
+ if (cloudConnected) {
+ setOnboardingStep("connectors");
+ } else {
+ setOnboardingStep("cloudLogin");
+ }
break;
case "cloudLogin":
- // Finish onboarding
- await handleOnboardingFinish();
+ setOnboardingStep("connectors");
break;
case "llmProvider":
setOnboardingStep("inventorySetup");
break;
case "inventorySetup":
+ setOnboardingStep("connectors");
+ break;
+ case "connectors":
await handleOnboardingFinish();
break;
}
- }, [onboardingStep, onboardingOptions, onboardingRunMode, onboardingTheme, setTheme]);
+ }, [onboardingStep, onboardingOptions, onboardingRunMode, onboardingTheme, setTheme, cloudConnected]);
const handleOnboardingBack = useCallback(() => {
switch (onboardingStep) {
case "name":
setOnboardingStep("welcome");
break;
- case "style":
+ case "avatar":
setOnboardingStep("name");
break;
+ case "style":
+ setOnboardingStep("avatar");
+ break;
case "theme":
setOnboardingStep("style");
break;
@@ -1500,11 +1612,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
setOnboardingStep("runMode");
break;
case "modelSelection":
- if (onboardingOptions && onboardingOptions.cloudProviders.length > 1) {
- setOnboardingStep("cloudProvider");
- } else {
- setOnboardingStep("runMode");
- }
+ setOnboardingStep("cloudProvider");
break;
case "cloudLogin":
setOnboardingStep("modelSelection");
@@ -1521,8 +1629,16 @@ export function AppProvider({ children }: { children: ReactNode }) {
case "inventorySetup":
setOnboardingStep("llmProvider");
break;
+ case "connectors":
+ // Go back to whichever path we came from
+ if (onboardingRunMode === "cloud") {
+ setOnboardingStep("modelSelection");
+ } else {
+ setOnboardingStep("inventorySetup");
+ }
+ break;
}
- }, [onboardingStep, onboardingOptions]);
+ }, [onboardingStep, onboardingOptions, onboardingRunMode]);
const handleOnboardingFinish = useCallback(async () => {
if (!onboardingOptions) return;
@@ -1540,6 +1656,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
}
}
+ setOnboardingRestarting(true);
try {
await client.submitOnboarding({
name: onboardingName,
@@ -1550,6 +1667,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
style: style?.style,
adjectives: style?.adjectives,
topics: style?.topics,
+ postExamples: style?.postExamples,
messageExamples: style?.messageExamples,
cloudProvider: onboardingRunMode === "cloud" ? onboardingCloudProvider : undefined,
smallModel: onboardingRunMode === "cloud" ? onboardingSmallModel : undefined,
@@ -1557,23 +1675,39 @@ export function AppProvider({ children }: { children: ReactNode }) {
provider: onboardingRunMode === "local" ? onboardingProvider || undefined : undefined,
providerApiKey: onboardingRunMode === "local" ? onboardingApiKey || undefined : undefined,
inventoryProviders: inventoryProviders.length > 0 ? inventoryProviders : undefined,
+ // Connectors
+ telegramToken: onboardingTelegramToken.trim() || undefined,
+ discordToken: onboardingDiscordToken.trim() || undefined,
+ whatsappSessionPath: onboardingWhatsAppSessionPath.trim() || undefined,
+ twilioAccountSid: onboardingTwilioAccountSid.trim() || undefined,
+ twilioAuthToken: onboardingTwilioAuthToken.trim() || undefined,
+ twilioPhoneNumber: onboardingTwilioPhoneNumber.trim() || undefined,
+ blooioApiKey: onboardingBlooioApiKey.trim() || undefined,
+ blooioPhoneNumber: onboardingBlooioPhoneNumber.trim() || undefined,
});
} catch (err) {
+ setOnboardingRestarting(false);
window.alert(`Setup failed: ${err instanceof Error ? err.message : "network error"}. Please try again.`);
return;
}
setOnboardingComplete(true);
+ setTab("chat");
try {
setAgentStatus(await client.restartAgent());
} catch {
/* ignore */
}
+ setOnboardingRestarting(false);
}, [
onboardingOptions, onboardingStyle, onboardingName, onboardingTheme,
onboardingRunMode, onboardingCloudProvider, onboardingSmallModel,
onboardingLargeModel, onboardingProvider, onboardingApiKey,
onboardingSelectedChains, onboardingRpcSelections, onboardingRpcKeys,
+ onboardingTelegramToken, onboardingDiscordToken, onboardingWhatsAppSessionPath,
+ onboardingTwilioAccountSid, onboardingTwilioAuthToken, onboardingTwilioPhoneNumber,
+ onboardingBlooioApiKey, onboardingBlooioPhoneNumber,
+ setTab,
]);
// ── Cloud ──────────────────────────────────────────────────────────
@@ -1600,8 +1734,14 @@ export function AppProvider({ children }: { children: ReactNode }) {
if (poll.status === "authenticated") {
if (cloudLoginPollTimer.current) clearInterval(cloudLoginPollTimer.current);
setCloudLoginBusy(false);
+ // Immediately reflect the login in the UI — don't wait for the
+ // background poll which may race with the config save.
+ setCloudConnected(true);
+ setCloudEnabled(true);
setActionNotice("Logged in to Eliza Cloud successfully.", "success", 6000);
- void pollCloudCredits();
+ // Delay the credit fetch slightly so the backend has time to
+ // persist the API key before we query cloud status / credits.
+ setTimeout(() => void pollCloudCredits(), 2000);
} else if (poll.status === "expired" || poll.status === "error") {
if (cloudLoginPollTimer.current) clearInterval(cloudLoginPollTimer.current);
setCloudLoginError(poll.error ?? "Session expired. Please try again.");
@@ -1757,7 +1897,21 @@ export function AppProvider({ children }: { children: ReactNode }) {
onboardingApiKey: setOnboardingApiKey as (v: never) => void,
onboardingSelectedChains: setOnboardingSelectedChains as (v: never) => void,
onboardingRpcSelections: setOnboardingRpcSelections as (v: never) => void,
+ onboardingOpenRouterModel: setOnboardingOpenRouterModel as (v: never) => void,
+ onboardingTelegramToken: setOnboardingTelegramToken as (v: never) => void,
+ onboardingDiscordToken: setOnboardingDiscordToken as (v: never) => void,
+ onboardingWhatsAppSessionPath: setOnboardingWhatsAppSessionPath as (v: never) => void,
+ onboardingTwilioAccountSid: setOnboardingTwilioAccountSid as (v: never) => void,
+ onboardingTwilioAuthToken: setOnboardingTwilioAuthToken as (v: never) => void,
+ onboardingTwilioPhoneNumber: setOnboardingTwilioPhoneNumber as (v: never) => void,
+ onboardingBlooioApiKey: setOnboardingBlooioApiKey as (v: never) => void,
+ onboardingBlooioPhoneNumber: setOnboardingBlooioPhoneNumber as (v: never) => void,
+ onboardingSubscriptionTab: setOnboardingSubscriptionTab as (v: never) => void,
onboardingRpcKeys: setOnboardingRpcKeys as (v: never) => void,
+ onboardingAvatar: setOnboardingAvatar as (v: never) => void,
+ onboardingRestarting: setOnboardingRestarting as (v: never) => void,
+ selectedVrmIndex: setSelectedVrmIndex as (v: never) => void,
+ customVrmUrl: setCustomVrmUrl as (v: never) => void,
commandQuery: setCommandQuery as (v: never) => void,
commandActiveIndex: setCommandActiveIndex as (v: never) => void,
storeSearch: setStoreSearch as (v: never) => void,
@@ -1799,8 +1953,6 @@ export function AppProvider({ children }: { children: ReactNode }) {
mcpHeaderInputs: setMcpHeaderInputs as (v: never) => void,
droppedFiles: setDroppedFiles as (v: never) => void,
shareIngestNotice: setShareIngestNotice as (v: never) => void,
- configRaw: setConfigRaw as (v: never) => void,
- configText: setConfigText as (v: never) => void,
};
const setter = setterMap[key as string];
if (setter) setter(value as never);
@@ -1849,7 +2001,8 @@ export function AppProvider({ children }: { children: ReactNode }) {
if (authRequired) return;
- // Load conversations
+ // Load conversations — if none exist, create one and request a greeting
+ let greetConvId: string | null = null;
try {
const { conversations: c } = await client.listConversations();
setConversations(c);
@@ -1859,6 +2012,21 @@ export function AppProvider({ children }: { children: ReactNode }) {
try {
const { messages } = await client.getConversationMessages(latest.id);
setConversationMessages(messages);
+ // If the latest conversation has no messages, queue a greeting
+ if (messages.length === 0) {
+ greetConvId = latest.id;
+ }
+ } catch {
+ /* ignore */
+ }
+ } else {
+ // First launch — create a conversation and greet
+ try {
+ const { conversation } = await client.createConversation();
+ setConversations([conversation]);
+ setActiveConversationId(conversation.id);
+ setConversationMessages([]);
+ greetConvId = conversation.id;
} catch {
/* ignore */
}
@@ -1867,6 +2035,29 @@ export function AppProvider({ children }: { children: ReactNode }) {
/* ignore */
}
+ // If the agent is already running and we have a conversation needing a
+ // greeting, fire it now. Otherwise the agent-state-transition effect
+ // below will trigger it once the agent starts.
+ if (greetConvId) {
+ try {
+ const s = await client.getStatus();
+ if (s.state === "running" && !greetingFiredRef.current) {
+ greetingFiredRef.current = true;
+ setChatSending(true);
+ try {
+ const data = await client.requestGreeting(greetConvId);
+ if (data.text) {
+ setConversationMessages((prev: ConversationMessage[]) => [
+ ...prev,
+ { id: `greeting-${Date.now()}`, role: "assistant", text: data.text, timestamp: Date.now() },
+ ]);
+ }
+ } catch { /* ignore */ }
+ setChatSending(false);
+ }
+ } catch { /* ignore */ }
+ }
+
void loadWorkbench();
// Connect WebSocket
@@ -1898,7 +2089,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
const urlTab = tabFromPath(window.location.pathname);
if (urlTab) {
setTabRaw(urlTab);
- if (urlTab === "plugins") void loadPlugins();
+ if (urlTab === "features") void loadPlugins();
if (urlTab === "skills") void loadSkills();
if (urlTab === "config") {
void checkExtensionStatus();
@@ -1907,7 +2098,6 @@ export function AppProvider({ children }: { children: ReactNode }) {
void loadUpdateStatus();
void loadPlugins();
}
- if (urlTab === "logs") void loadLogs();
if (urlTab === "inventory") void loadInventory();
}
};
@@ -1930,6 +2120,25 @@ export function AppProvider({ children }: { children: ReactNode }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+ // When agent transitions to "running", send a greeting if conversation is empty
+ useEffect(() => {
+ const current = agentStatus?.state ?? null;
+ const prev = prevAgentStateRef.current;
+ prevAgentStateRef.current = current;
+
+ if (current === "running" && prev !== "running") {
+ void loadWorkbench();
+
+ // Agent just started — greet if conversation is empty.
+ // The greetingFiredRef guard prevents double-greeting when both the
+ // init mount effect and this state-transition effect race to fire.
+ if (activeConversationId && conversationMessages.length === 0 && !chatSending && !greetingFiredRef.current) {
+ greetingFiredRef.current = true;
+ void fetchGreeting(activeConversationId);
+ }
+ }
+ }, [agentStatus?.state, loadWorkbench, activeConversationId, conversationMessages.length, chatSending, fetchGreeting]);
+
// ── Context value ──────────────────────────────────────────────────
const value: AppContextValue = {
@@ -1949,7 +2158,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
walletNftsLoading, inventoryView, walletExportData, walletExportVisible,
walletApiKeySaving, inventorySort, walletError,
characterData, characterLoading, characterSaving, characterSaveSuccess,
- characterSaveError, characterDraft,
+ characterSaveError, characterDraft, selectedVrmIndex, customVrmUrl,
cloudEnabled, cloudConnected, cloudCredits, cloudCreditsLow, cloudCreditsCritical,
cloudTopUpUrl, cloudUserId, cloudLoginBusy, cloudLoginError, cloudDisconnecting,
updateStatus, updateLoading, updateChannelSaving,
@@ -1964,8 +2173,12 @@ export function AppProvider({ children }: { children: ReactNode }) {
importBusy, importPassword, importFile, importError, importSuccess,
onboardingStep, onboardingOptions, onboardingName, onboardingStyle, onboardingTheme,
onboardingRunMode, onboardingCloudProvider, onboardingSmallModel, onboardingLargeModel,
- onboardingProvider, onboardingApiKey, onboardingSelectedChains,
- onboardingRpcSelections, onboardingRpcKeys,
+ onboardingProvider, onboardingApiKey, onboardingOpenRouterModel,
+ onboardingTelegramToken, onboardingDiscordToken, onboardingWhatsAppSessionPath,
+ onboardingTwilioAccountSid, onboardingTwilioAuthToken, onboardingTwilioPhoneNumber,
+ onboardingBlooioApiKey, onboardingBlooioPhoneNumber, onboardingSubscriptionTab,
+ onboardingSelectedChains, onboardingRpcSelections, onboardingRpcKeys,
+ onboardingAvatar, onboardingRestarting,
commandPaletteOpen, commandQuery, commandActiveIndex,
mcpConfiguredServers, mcpServerStatuses, mcpMarketplaceQuery, mcpMarketplaceResults,
mcpMarketplaceLoading, mcpAction, mcpAddingServer, mcpAddingResult,
@@ -1973,7 +2186,6 @@ export function AppProvider({ children }: { children: ReactNode }) {
droppedFiles, shareIngestNotice,
activeGameApp, activeGameDisplayName, activeGameViewerUrl, activeGameSandbox,
activeGamePostMessageAuth,
- configRaw, configText,
// Actions
setTab, setTheme,
diff --git a/apps/app/src/api-client.ts b/apps/app/src/api-client.ts
index 82e1b5c033..7503d31a4d 100644
--- a/apps/app/src/api-client.ts
+++ b/apps/app/src/api-client.ts
@@ -104,6 +104,7 @@ export interface StylePreset {
};
adjectives: string[];
topics: string[];
+ postExamples: string[];
messageExamples: MessageExample[][];
}
@@ -164,6 +165,15 @@ export interface OnboardingOptions {
sharedStyleRules: string;
}
+/** Configuration for a single messaging connector. */
+export interface ConnectorConfig {
+ enabled?: boolean;
+ botToken?: string;
+ token?: string;
+ apiKey?: string;
+ [key: string]: string | boolean | number | string[] | Record | undefined;
+}
+
export interface OnboardingData {
name: string;
theme: string;
@@ -177,6 +187,7 @@ export interface OnboardingData {
};
adjectives?: string[];
topics?: string[];
+ postExamples?: string[];
messageExamples?: MessageExample[][];
// Cloud-specific
cloudProvider?: string;
@@ -186,12 +197,25 @@ export interface OnboardingData {
provider?: string;
providerApiKey?: string;
openrouterModel?: string;
+ subscriptionProvider?: string;
+ // Messaging channel setup
+ channels?: Record;
// Inventory / wallet setup
inventoryProviders?: Array<{
chain: string;
rpcProvider: string;
rpcApiKey?: string;
}>;
+ // Connector setup (Telegram, Discord, etc.)
+ connectors?: Record;
+ telegramToken?: string;
+ discordToken?: string;
+ whatsappSessionPath?: string;
+ twilioAccountSid?: string;
+ twilioAuthToken?: string;
+ twilioPhoneNumber?: string;
+ blooioApiKey?: string;
+ blooioPhoneNumber?: string;
}
export interface PluginParamDef {
@@ -214,7 +238,7 @@ export interface PluginInfo {
enabled: boolean;
configured: boolean;
envKey: string | null;
- category: "ai-provider" | "connector" | "database" | "feature";
+ category: "ai-provider" | "connector" | "database" | "app" | "feature";
source: "bundled" | "store";
parameters: PluginParamDef[];
validationErrors: Array<{ field: string; message: string }>;
@@ -224,6 +248,20 @@ export interface PluginInfo {
pluginDeps?: string[];
}
+export interface CorePluginEntry {
+ npmName: string;
+ id: string;
+ name: string;
+ isCore: boolean;
+ loaded: boolean;
+ enabled: boolean;
+}
+
+export interface CorePluginsResponse {
+ core: CorePluginEntry[];
+ optional: CorePluginEntry[];
+}
+
export interface ChatMessage {
role: "user" | "assistant";
text: string;
@@ -508,6 +546,28 @@ export interface McpServerStatus {
error?: string;
}
+// Voice / TTS config
+export type VoiceProvider = "elevenlabs" | "simple-voice" | "edge";
+
+export interface VoiceConfig {
+ provider?: VoiceProvider;
+ elevenlabs?: {
+ apiKey?: string;
+ voiceId?: string;
+ modelId?: string;
+ stability?: number;
+ similarityBoost?: number;
+ speed?: number;
+ };
+ edge?: {
+ voice?: string;
+ lang?: string;
+ rate?: string;
+ pitch?: string;
+ volume?: string;
+ };
+}
+
// Character
export interface CharacterData {
name?: string;
@@ -725,6 +785,38 @@ export class MilaidyClient {
});
}
+ async startAnthropicLogin(): Promise<{ authUrl: string }> {
+ return this.fetch("/api/subscription/anthropic/start", { method: "POST" });
+ }
+
+ async exchangeAnthropicCode(code: string): Promise<{ success: boolean; expiresAt?: string }> {
+ return this.fetch("/api/subscription/anthropic/exchange", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ code }),
+ });
+ }
+
+ async submitAnthropicSetupToken(token: string): Promise<{ success: boolean }> {
+ return this.fetch("/api/subscription/anthropic/setup-token", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ token }),
+ });
+ }
+
+ async startOpenAILogin(): Promise<{ authUrl: string; state: string; instructions: string }> {
+ return this.fetch("/api/subscription/openai/start", { method: "POST" });
+ }
+
+ async exchangeOpenAICode(code: string): Promise<{ success: boolean; expiresAt?: string; accountId?: string }> {
+ return this.fetch("/api/subscription/openai/exchange", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ code }),
+ });
+ }
+
async startAgent(): Promise {
const res = await this.fetch<{ status: AgentStatus }>("/api/agent/start", { method: "POST" });
return res.status;
@@ -766,17 +858,51 @@ export class MilaidyClient {
});
}
+ // ── Connectors ──────────────────────────────────────────────────────
+
+ async getConnectors(): Promise<{ connectors: Record }> {
+ return this.fetch("/api/connectors");
+ }
+
+ async saveConnector(name: string, config: ConnectorConfig): Promise<{ connectors: Record }> {
+ return this.fetch("/api/connectors", {
+ method: "POST",
+ body: JSON.stringify({ name, config }),
+ });
+ }
+
+ async deleteConnector(name: string): Promise<{ connectors: Record }> {
+ return this.fetch(`/api/connectors/${encodeURIComponent(name)}`, {
+ method: "DELETE",
+ });
+ }
+
async getPlugins(): Promise<{ plugins: PluginInfo[] }> {
return this.fetch("/api/plugins");
}
- async updatePlugin(id: string, config: Record): Promise {
- await this.fetch(`/api/plugins/${id}`, {
+ async getCorePlugins(): Promise {
+ return this.fetch("/api/plugins/core");
+ }
+
+ async toggleCorePlugin(npmName: string, enabled: boolean): Promise<{ ok: boolean; restarting?: boolean; message?: string }> {
+ return this.fetch("/api/plugins/core/toggle", {
+ method: "POST",
+ body: JSON.stringify({ npmName, enabled }),
+ });
+ }
+
+ async updatePlugin(id: string, config: Record): Promise<{ ok: boolean; restarting?: boolean }> {
+ return this.fetch(`/api/plugins/${id}`, {
method: "PUT",
body: JSON.stringify(config),
});
}
+ async restart(): Promise<{ ok: boolean }> {
+ return this.fetch("/api/restart", { method: "POST" });
+ }
+
async getSkills(): Promise<{ skills: SkillInfo[] }> {
return this.fetch("/api/skills");
}
@@ -1304,6 +1430,12 @@ export class MilaidyClient {
});
}
+ async requestGreeting(id: string): Promise<{ text: string; agentName: string; generated: boolean }> {
+ return this.fetch(`/api/conversations/${encodeURIComponent(id)}/greeting`, {
+ method: "POST",
+ });
+ }
+
async renameConversation(id: string, title: string): Promise<{ conversation: Conversation }> {
return this.fetch(`/api/conversations/${encodeURIComponent(id)}`, {
method: "PATCH",
diff --git a/apps/app/src/bridge/index.ts b/apps/app/src/bridge/index.ts
deleted file mode 100644
index f8196c0097..0000000000
--- a/apps/app/src/bridge/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Capacitor Bridge Module
- *
- * This module exports all bridge utilities for use by the app and plugins.
- */
-
-export {
- initializeCapacitorBridge,
- getCapabilities,
- haptics,
- registerPlugin,
- getPlugin,
- hasPlugin,
- waitForBridge,
- type MilaidyBridge,
- type CapacitorCapabilities,
-} from "./capacitor-bridge.js";
-
-export {
- initializeStorageBridge,
- getStorageValue,
- setStorageValue,
- removeStorageValue,
- registerSyncedKey,
- isStorageBridgeInitialized,
-} from "./storage-bridge.js";
diff --git a/apps/app/src/bridge/plugin-bridge.ts b/apps/app/src/bridge/plugin-bridge.ts
index 7aaab825ae..d2ce9c7e64 100644
--- a/apps/app/src/bridge/plugin-bridge.ts
+++ b/apps/app/src/bridge/plugin-bridge.ts
@@ -19,8 +19,6 @@ import { Location as LocationPlugin } from "@milaidy/capacitor-location";
import { ScreenCapture as ScreenCapturePlugin } from "@milaidy/capacitor-screencapture";
import { Canvas as CanvasPlugin } from "@milaidy/capacitor-canvas";
import { Desktop as DesktopPlugin } from "@milaidy/capacitor-desktop";
-import { Agent as AgentPlugin } from "@milaidy/capacitor-agent";
-
// Import types
import type { GatewayPlugin as IGatewayPlugin } from "@milaidy/capacitor-gateway";
import type { SwabblePlugin as ISwabblePlugin } from "@milaidy/capacitor-swabble";
diff --git a/apps/app/src/components/AdminView.tsx b/apps/app/src/components/AdminView.tsx
new file mode 100644
index 0000000000..d01b91eb33
--- /dev/null
+++ b/apps/app/src/components/AdminView.tsx
@@ -0,0 +1,220 @@
+/**
+ * Admin view — logs, database management, and core plugin status.
+ *
+ * Contains three sub-tabs:
+ * - Logs: agent runtime logs
+ * - Plugins: core plugin status & optional plugin toggles
+ * - Database: database explorer
+ */
+
+import { useCallback, useEffect, useState } from "react";
+import { client } from "../api-client";
+import type { CorePluginEntry } from "../api-client";
+import { LogsView } from "./LogsView";
+import { DatabaseView } from "./DatabaseView";
+
+type AdminTab = "logs" | "plugins" | "database";
+
+const ADMIN_TABS: { id: AdminTab; label: string }[] = [
+ { id: "logs", label: "Logs" },
+ { id: "plugins", label: "Plugins" },
+ { id: "database", label: "Database" },
+];
+
+/* ── Core Plugins sub-view ──────────────────────────────────────────── */
+
+function CorePluginsView() {
+ const [core, setCore] = useState([]);
+ const [optional, setOptional] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [toggling, setToggling] = useState(null);
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const data = await client.getCorePlugins();
+ setCore(data.core);
+ setOptional(data.optional);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to load");
+ }
+ setLoading(false);
+ }, []);
+
+ useEffect(() => { void load(); }, [load]);
+
+ const handleToggle = useCallback(async (plugin: CorePluginEntry) => {
+ setToggling(plugin.id);
+ try {
+ await client.toggleCorePlugin(plugin.npmName, !plugin.enabled);
+ // Optimistic update
+ setOptional(prev =>
+ prev.map(p =>
+ p.id === plugin.id ? { ...p, enabled: !p.enabled } : p,
+ ),
+ );
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Toggle failed");
+ }
+ setToggling(null);
+ }, []);
+
+ if (loading) {
+ return Loading plugin status...
;
+ }
+ if (error) {
+ return (
+
+ {error}
+
+
+ );
+ }
+
+ const loadedCore = core.filter(p => p.loaded).length;
+ const enabledOptional = optional.filter(p => p.enabled).length;
+
+ return (
+
+ {/* Summary */}
+
+ {loadedCore}/{core.length} core running · {enabledOptional}/{optional.length} optional enabled
+
+
+
+ {/* Core plugins */}
+
+
Core Plugins
+
Always loaded. Required for the agent to function.
+
+ {core.map(p => (
+
+ ))}
+
+
+
+ {/* Optional plugins */}
+
+
Optional Plugins
+
+ Toggle to enable or disable. Agent will restart automatically.
+
+
+ {optional.map(p => (
+
handleToggle(p)}
+ />
+ ))}
+
+
+
+ );
+}
+
+function PluginRow({
+ plugin,
+ toggleable,
+ toggling,
+ onToggle,
+}: {
+ plugin: CorePluginEntry;
+ toggleable?: boolean;
+ toggling?: boolean;
+ onToggle?: () => void;
+}) {
+ return (
+
+ {/* Status dot */}
+
+
+ {/* Name */}
+
+ {plugin.name}
+
+
+ {/* Status label */}
+
+ {plugin.loaded ? "Running" : plugin.enabled ? "Enabled" : "Off"}
+
+
+ {/* Toggle button for optional plugins */}
+ {toggleable && onToggle && (
+
+ )}
+
+ );
+}
+
+/* ── Main AdminView ─────────────────────────────────────────────────── */
+
+export function AdminView() {
+ const [activeTab, setActiveTab] = useState("logs");
+
+ return (
+
+ {/* Sub-tab bar */}
+
+ {ADMIN_TABS.map((t) => (
+
+ ))}
+
+
+ {/* Sub-tab content */}
+ {activeTab === "logs" &&
}
+ {activeTab === "plugins" &&
}
+ {activeTab === "database" &&
}
+
+ );
+}
diff --git a/apps/app/src/components/AppsView.tsx b/apps/app/src/components/AppsView.tsx
index c43ec0ca9c..26b7f5592e 100644
--- a/apps/app/src/components/AppsView.tsx
+++ b/apps/app/src/components/AppsView.tsx
@@ -47,7 +47,7 @@ export function AppsView() {
setState("activeGameApp", app.name);
setState("activeGameDisplayName", app.displayName ?? app.name);
setState("activeGameViewerUrl", result.viewer.url);
- setState("tab", "game" as never);
+ setState("tab", "game");
}
} catch (err) {
setActionNotice(
diff --git a/apps/app/src/components/AvatarSelector.tsx b/apps/app/src/components/AvatarSelector.tsx
new file mode 100644
index 0000000000..6225066145
--- /dev/null
+++ b/apps/app/src/components/AvatarSelector.tsx
@@ -0,0 +1,94 @@
+/**
+ * Reusable avatar/character VRM selector.
+ *
+ * Shows a single row of the 8 built-in milady VRMs as thumbnail images.
+ * The selected avatar gets a highlight ring. No text labels.
+ */
+
+import { useRef } from "react";
+import { VRM_COUNT, getVrmPreviewUrl } from "../AppContext";
+
+export interface AvatarSelectorProps {
+ /** Currently selected index (1-8 for built-in, 0 for custom) */
+ selected: number;
+ /** Called when a built-in avatar is selected */
+ onSelect: (index: number) => void;
+ /** Called when a custom VRM is uploaded */
+ onUpload?: (file: File) => void;
+ /** Whether to show the upload option */
+ showUpload?: boolean;
+}
+
+export function AvatarSelector({
+ selected,
+ onSelect,
+ onUpload,
+ showUpload = true,
+}: AvatarSelectorProps) {
+ const fileInputRef = useRef(null);
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ if (!file.name.endsWith(".vrm")) {
+ alert("Please select a .vrm file");
+ return;
+ }
+ onUpload?.(file);
+ onSelect(0); // 0 = custom
+ };
+
+ const avatarIndices = Array.from({ length: VRM_COUNT }, (_, i) => i + 1);
+
+ return (
+
+
+ {avatarIndices.map((i) => (
+
+ ))}
+
+ {/* Upload custom VRM */}
+ {showUpload && (
+ <>
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/apps/app/src/components/CharacterView.tsx b/apps/app/src/components/CharacterView.tsx
new file mode 100644
index 0000000000..bcc9820f8a
--- /dev/null
+++ b/apps/app/src/components/CharacterView.tsx
@@ -0,0 +1,1029 @@
+/**
+ * Character view — agent identity, personality, and avatar.
+ *
+ * Features:
+ * - Import/export character as JSON
+ * - Unsaved changes indicator
+ * - Adjectives and topics editors
+ */
+
+import { useEffect, useState, useCallback, useRef } from "react";
+import { useApp } from "../AppContext";
+import { client, type VoiceProvider, type VoiceConfig } from "../api-client";
+import { AvatarSelector } from "./AvatarSelector";
+
+export function CharacterView() {
+ const {
+ characterData,
+ characterDraft,
+ characterLoading,
+ characterSaving,
+ characterSaveSuccess,
+ characterSaveError,
+ handleCharacterFieldInput,
+ handleCharacterArrayInput,
+ handleCharacterStyleInput,
+ handleSaveCharacter,
+ loadCharacter,
+ setState,
+ selectedVrmIndex,
+ // Cloud (for ElevenLabs via Eliza Cloud)
+ cloudConnected,
+ cloudUserId,
+ cloudLoginBusy,
+ cloudLoginError,
+ cloudDisconnecting,
+ handleCloudLogin,
+ handleCloudDisconnect,
+ } = useApp();
+
+ useEffect(() => {
+ void loadCharacter();
+ }, [loadCharacter]);
+
+ const fileInputRef = useRef(null);
+
+ const handleFieldEdit = useCallback((field: string, value: string | string[] | Record[]) => {
+ handleCharacterFieldInput(field as never, value as never);
+ }, [handleCharacterFieldInput]);
+
+ const handleStyleEdit = useCallback((key: "all" | "chat" | "post", value: string) => {
+ handleCharacterStyleInput(key, value);
+ }, [handleCharacterStyleInput]);
+
+ /* ── Import / Export ────────────────────────────────────────────── */
+ const handleExport = useCallback(() => {
+ const d = characterDraft;
+ const exportData = {
+ name: d.name ?? "",
+ bio: typeof d.bio === "string" ? d.bio.split("\n").filter(Boolean) : (d.bio ?? []),
+ system: d.system ?? "",
+ style: {
+ all: d.style?.all ?? [],
+ chat: d.style?.chat ?? [],
+ post: d.style?.post ?? [],
+ },
+ adjectives: d.adjectives ?? [],
+ topics: d.topics ?? [],
+ messageExamples: (d.messageExamples ?? []).map((convo: any) =>
+ (convo.examples ?? []).map((msg: any) => ({
+ user: msg.name,
+ content: { text: msg.content?.text ?? "" },
+ }))
+ ),
+ postExamples: d.postExamples ?? [],
+ };
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: "application/json" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `${(d.name ?? "character").toLowerCase().replace(/\s+/g, "-")}.character.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ }, [characterDraft]);
+
+ const handleImport = useCallback((e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = () => {
+ try {
+ const data = JSON.parse(reader.result as string);
+ if (data.name) handleCharacterFieldInput("name", data.name);
+ if (data.bio) handleCharacterFieldInput("bio",
+ Array.isArray(data.bio) ? data.bio.join("\n") : data.bio);
+ if (data.system) handleCharacterFieldInput("system", data.system);
+ if (data.adjectives) handleCharacterFieldInput("adjectives" as any, data.adjectives);
+ if (data.topics) handleCharacterFieldInput("topics" as any, data.topics);
+ if (data.style) {
+ if (data.style.all) handleCharacterStyleInput("all",
+ Array.isArray(data.style.all) ? data.style.all.join("\n") : data.style.all);
+ if (data.style.chat) handleCharacterStyleInput("chat",
+ Array.isArray(data.style.chat) ? data.style.chat.join("\n") : data.style.chat);
+ if (data.style.post) handleCharacterStyleInput("post",
+ Array.isArray(data.style.post) ? data.style.post.join("\n") : data.style.post);
+ }
+ if (data.messageExamples) {
+ const formatted = data.messageExamples.map((convo: any[]) => ({
+ examples: convo.map((msg: any) => ({
+ name: msg.user ?? msg.name ?? "{{user1}}",
+ content: { text: msg.content?.text ?? msg.text ?? "" },
+ })),
+ }));
+ handleCharacterFieldInput("messageExamples" as any, formatted);
+ }
+ if (data.postExamples) handleCharacterFieldInput("postExamples" as any, data.postExamples);
+ } catch {
+ alert("invalid json file");
+ }
+ };
+ reader.readAsText(file);
+ // Reset input so same file can be re-imported
+ e.target.value = "";
+ }, [handleCharacterFieldInput, handleCharacterStyleInput]);
+
+ /* ── Character generation state ─────────────────────────────────── */
+ const [generating, setGenerating] = useState(null);
+
+ /* ── Voice config state ─────────────────────────────────────────── */
+ const [voiceProvider, setVoiceProvider] = useState("elevenlabs");
+ const [voiceConfig, setVoiceConfig] = useState({});
+ const [voiceLoading, setVoiceLoading] = useState(false);
+ const [voiceSaving, setVoiceSaving] = useState(false);
+ const [voiceSaveSuccess, setVoiceSaveSuccess] = useState(false);
+ const [voiceSaveError, setVoiceSaveError] = useState(null);
+ const [voiceMode, setVoiceMode] = useState<"cloud" | "own-key">("cloud");
+ const [voiceTesting, setVoiceTesting] = useState(false);
+ const [voiceTestAudio, setVoiceTestAudio] = useState(null);
+ const [selectedPresetId, setSelectedPresetId] = useState(null);
+
+ /* ── ElevenLabs voice presets ──────────────────────────────────── */
+ type VoicePreset = { id: string; name: string; voiceId: string; gender: "female" | "male" | "character"; hint: string; previewUrl: string };
+ const VOICE_PRESETS: VoicePreset[] = [
+ // Female
+ { id: "rachel", name: "Rachel", voiceId: "21m00Tcm4TlvDq8ikWAM", gender: "female", hint: "Calm, clear", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/21m00Tcm4TlvDq8ikWAM/df6788f9-5c96-470d-8312-aab3b3d8f50a.mp3" },
+ { id: "sarah", name: "Sarah", voiceId: "EXAVITQu4vr4xnSDxMaL", gender: "female", hint: "Soft, warm", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/EXAVITQu4vr4xnSDxMaL/6851ec91-9950-471f-8586-357c52539069.mp3" },
+ { id: "matilda", name: "Matilda", voiceId: "XrExE9yKIg1WjnnlVkGX", gender: "female", hint: "Warm, friendly", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/XrExE9yKIg1WjnnlVkGX/b930e18d-6b4d-466e-bab2-0ae97c6d8535.mp3" },
+ { id: "lily", name: "Lily", voiceId: "pFZP5JQG7iQjIQuC4Bku", gender: "female", hint: "British, raspy", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/pFZP5JQG7iQjIQuC4Bku/0ab8bd74-fcd2-489d-b70a-3e1bcde8c999.mp3" },
+ { id: "alice", name: "Alice", voiceId: "Xb7hH8MSUJpSbSDYk0k2", gender: "female", hint: "British, confident", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/Xb7hH8MSUJpSbSDYk0k2/f5409e2f-d9c3-4ac9-9e7d-916a5dbd1ef1.mp3" },
+ // Male
+ { id: "brian", name: "Brian", voiceId: "nPczCjzI2devNBz1zQrb", gender: "male", hint: "Deep, smooth", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/nPczCjzI2devNBz1zQrb/f4dbda0c-aff0-45c0-93fa-f5d5ec95a2eb.mp3" },
+ { id: "adam", name: "Adam", voiceId: "pNInz6obpgDQGcFmaJgB", gender: "male", hint: "Deep, authoritative", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/pNInz6obpgDQGcFmaJgB/38a69695-2ca9-4b9e-b9ec-f07ced494a58.mp3" },
+ { id: "josh", name: "Josh", voiceId: "TxGEqnHWrfWFTfGW9XjX", gender: "male", hint: "Young, deep", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/TxGEqnHWrfWFTfGW9XjX/3ae2fc71-d5f9-4769-bb71-2a43633cd186.mp3" },
+ { id: "daniel", name: "Daniel", voiceId: "onwK4e9ZLuTAKqWW03F9", gender: "male", hint: "British, presenter", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/onwK4e9ZLuTAKqWW03F9/7eee0236-1a72-4b86-b303-5dcadc007ba9.mp3" },
+ { id: "liam", name: "Liam", voiceId: "TX3LPaxmHKxFdv7VOQHJ", gender: "male", hint: "Young, natural", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/TX3LPaxmHKxFdv7VOQHJ/63148076-6363-42db-aea8-31424308b92c.mp3" },
+ // Character / Cutesy / Game
+ { id: "gigi", name: "Gigi", voiceId: "jBpfuIE2acCO8z3wKNLl", gender: "character", hint: "Childish, cute", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/jBpfuIE2acCO8z3wKNLl/3a7e4339-78fa-404e-8d10-c3ef5587935b.mp3" },
+ { id: "mimi", name: "Mimi", voiceId: "zrHiDhphv9ZnVXBqCLjz", gender: "character", hint: "Cute, animated", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/zrHiDhphv9ZnVXBqCLjz/decbf20b-0f57-4fac-985b-a4f0290ebfc4.mp3" },
+ { id: "dorothy", name: "Dorothy", voiceId: "ThT5KcBeYPX3keUQqHPh", gender: "character", hint: "Sweet, storybook", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/ThT5KcBeYPX3keUQqHPh/981f0855-6598-48d2-9f8f-b6d92fbbe3fc.mp3" },
+ { id: "glinda", name: "Glinda", voiceId: "z9fAnlkpzviPz146aGWa", gender: "character", hint: "Magical, whimsical", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/z9fAnlkpzviPz146aGWa/cbc60443-7b61-4ebb-b8e1-5c03237ea01d.mp3" },
+ { id: "charlotte", name: "Charlotte", voiceId: "XB0fDUnXU5powFXDhCwa", gender: "character", hint: "Alluring, game NPC", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/XB0fDUnXU5powFXDhCwa/942356dc-f10d-4d89-bda5-4f8505ee038b.mp3" },
+ { id: "callum", name: "Callum", voiceId: "N2lVS1w4EtoT3dr4eOWO", gender: "character", hint: "Gruff, game hero", previewUrl: "https://storage.googleapis.com/eleven-public-prod/premade/voices/N2lVS1w4EtoT3dr4eOWO/ac833bd8-ffda-4938-9ebc-b0f99ca25481.mp3" },
+ ];
+
+ /* Load voice config on mount */
+ useEffect(() => {
+ void (async () => {
+ setVoiceLoading(true);
+ try {
+ const cfg = await client.getConfig();
+ const messages = cfg.messages as Record> | undefined;
+ const tts = messages?.tts as VoiceConfig | undefined;
+ if (tts?.provider) setVoiceProvider(tts.provider);
+ if (tts) {
+ setVoiceConfig(tts);
+ // Detect voice mode: if user has own API key set, default to own-key mode
+ if (tts.elevenlabs?.apiKey) setVoiceMode("own-key");
+ // Detect selected preset
+ if (tts.elevenlabs?.voiceId) {
+ const preset = VOICE_PRESETS.find((p) => p.voiceId === tts.elevenlabs?.voiceId);
+ setSelectedPresetId(preset?.id ?? "custom");
+ }
+ }
+ } catch { /* ignore */ }
+ setVoiceLoading(false);
+ })();
+ }, []);
+
+ const handleVoiceFieldChange = useCallback(
+ (provider: "elevenlabs" | "edge", key: string, value: string | number) => {
+ setVoiceConfig((prev) => ({
+ ...prev,
+ [provider]: { ...(prev[provider] ?? {}), [key]: value },
+ }));
+ },
+ [],
+ );
+
+ const handleSelectPreset = useCallback(
+ (preset: VoicePreset) => {
+ setSelectedPresetId(preset.id);
+ setVoiceConfig((prev) => ({
+ ...prev,
+ elevenlabs: { ...(prev.elevenlabs ?? {}), voiceId: preset.voiceId },
+ }));
+ },
+ [],
+ );
+
+ const handleTestVoice = useCallback(
+ (previewUrl: string) => {
+ // Stop any existing playback
+ if (voiceTestAudio) {
+ voiceTestAudio.pause();
+ voiceTestAudio.currentTime = 0;
+ }
+ setVoiceTesting(true);
+ const audio = new Audio(previewUrl);
+ setVoiceTestAudio(audio);
+ audio.onended = () => setVoiceTesting(false);
+ audio.onerror = () => setVoiceTesting(false);
+ audio.play().catch(() => setVoiceTesting(false));
+ },
+ [voiceTestAudio],
+ );
+
+ const handleStopTest = useCallback(() => {
+ if (voiceTestAudio) {
+ voiceTestAudio.pause();
+ voiceTestAudio.currentTime = 0;
+ }
+ setVoiceTesting(false);
+ }, [voiceTestAudio]);
+
+ const handleVoiceSave = useCallback(async () => {
+ setVoiceSaving(true);
+ setVoiceSaveError(null);
+ setVoiceSaveSuccess(false);
+ try {
+ const elConfig = { ...(voiceConfig.elevenlabs ?? {}) };
+ // In cloud mode, don't send apiKey — cloud handles it
+ if (voiceMode === "cloud") {
+ delete elConfig.apiKey;
+ }
+ await client.updateConfig({
+ messages: {
+ tts: {
+ provider: voiceProvider,
+ ...(voiceProvider === "elevenlabs" ? { elevenlabs: elConfig } : {}),
+ ...(voiceProvider === "edge" && voiceConfig.edge
+ ? { edge: voiceConfig.edge }
+ : {}),
+ },
+ },
+ });
+ setVoiceSaveSuccess(true);
+ setTimeout(() => setVoiceSaveSuccess(false), 2500);
+ } catch (err) {
+ setVoiceSaveError(err instanceof Error ? err.message : "Failed to save — is the agent running?");
+ }
+ setVoiceSaving(false);
+ }, [voiceProvider, voiceConfig, voiceMode]);
+
+ const d = characterDraft;
+ const bioText = typeof d.bio === "string" ? d.bio : Array.isArray(d.bio) ? d.bio.join("\n") : "";
+ const styleAllText = (d.style?.all ?? []).join("\n");
+ const styleChatText = (d.style?.chat ?? []).join("\n");
+ const stylePostText = (d.style?.post ?? []).join("\n");
+
+ const getCharContext = useCallback(() => ({
+ name: d.name ?? "",
+ system: d.system ?? "",
+ bio: bioText,
+ style: d.style ?? { all: [], chat: [], post: [] },
+ postExamples: d.postExamples ?? [],
+ }), [d, bioText]);
+
+ const handleGenerate = useCallback(async (field: string, mode: "append" | "replace" = "replace") => {
+ setGenerating(field);
+ try {
+ const { generated } = await client.generateCharacterField(field, getCharContext(), mode);
+ if (field === "bio") {
+ handleFieldEdit("bio", generated.trim());
+ } else if (field === "style") {
+ try {
+ const parsed = JSON.parse(generated);
+ if (mode === "append") {
+ handleStyleEdit("all", [...(d.style?.all ?? []), ...(parsed.all ?? [])].join("\n"));
+ handleStyleEdit("chat", [...(d.style?.chat ?? []), ...(parsed.chat ?? [])].join("\n"));
+ handleStyleEdit("post", [...(d.style?.post ?? []), ...(parsed.post ?? [])].join("\n"));
+ } else {
+ if (parsed.all) handleStyleEdit("all", parsed.all.join("\n"));
+ if (parsed.chat) handleStyleEdit("chat", parsed.chat.join("\n"));
+ if (parsed.post) handleStyleEdit("post", parsed.post.join("\n"));
+ }
+ } catch { /* raw text fallback */ }
+ } else if (field === "chatExamples") {
+ try {
+ const parsed = JSON.parse(generated);
+ if (Array.isArray(parsed)) {
+ const formatted = parsed.map((convo: Array<{ user: string; content: { text: string } }>) => ({
+ examples: convo.map((msg) => ({ name: msg.user, content: { text: msg.content.text } })),
+ }));
+ handleFieldEdit("messageExamples", formatted);
+ }
+ } catch { /* raw text fallback */ }
+ } else if (field === "postExamples") {
+ try {
+ const parsed = JSON.parse(generated);
+ if (Array.isArray(parsed)) {
+ if (mode === "append") {
+ handleCharacterArrayInput("postExamples", [...(d.postExamples ?? []), ...parsed].join("\n"));
+ } else {
+ handleCharacterArrayInput("postExamples", parsed.join("\n"));
+ }
+ }
+ } catch { /* raw text fallback */ }
+ }
+ } catch { /* generation failed */ }
+ setGenerating(null);
+ }, [getCharContext, d, handleFieldEdit, handleStyleEdit, handleCharacterArrayInput]);
+
+ const handleRandomName = useCallback(async () => {
+ try {
+ const { name } = await client.getRandomName();
+ handleFieldEdit("name", name);
+ } catch { /* ignore */ }
+ }, [handleFieldEdit]);
+
+ /* ── Helpers ────────────────────────────────────────────────────── */
+ const inputCls = "px-2.5 py-1.5 border border-[var(--border)] bg-[var(--card)] text-xs focus:border-[var(--accent)] focus:outline-none";
+ const textareaCls = `${inputCls} font-inherit resize-y leading-relaxed`;
+ const labelCls = "font-semibold text-xs";
+ const hintCls = "text-[11px] text-[var(--muted)]";
+ const tinyBtnCls = "text-[10px] px-1.5 py-0.5 border border-[var(--border)] bg-[var(--card)] cursor-pointer hover:border-[var(--accent)] hover:text-[var(--accent)] transition-colors disabled:opacity-40";
+
+ return (
+
+
+ {characterLoading && !characterData ? (
+
+ loading character data...
+
+ ) : (
+
+ {/* Name + reload */}
+
+
+
+
+
+
+ handleFieldEdit("name", e.target.value)}
+ className={inputCls + " flex-1 text-[13px]"}
+ />
+
+
+
+
+ {/* Avatar (below name, full width) */}
+
+
+
setState("selectedVrmIndex", i)}
+ onUpload={(file) => {
+ const url = URL.createObjectURL(file);
+ setState("customVrmUrl", url);
+ setState("selectedVrmIndex", 0);
+ }}
+ showUpload
+ />
+
+
+ {/* Identity (bio) */}
+
+
+
+
+
+
+
+ {/* Soul (system prompt) */}
+
+
+
+
+
+
+
+ {/* Style */}
+
+
+
+
+ — communication guidelines
+
+
+
+
+ {(["all", "chat", "post"] as const).map((key) => {
+ const val = key === "all" ? styleAllText : key === "chat" ? styleChatText : stylePostText;
+ return (
+
+
+
+ );
+ })}
+
+
+
+ {/* Chat Examples */}
+
+
+ ▶
+ chat examples
+ — how the agent responds
+
+
+
+ {(d.messageExamples ?? []).map((convo, ci) => (
+
+
+ conversation {ci + 1}
+
+
+ {convo.examples.map((msg: any, mi: number) => (
+
+
+ {msg.name === "{{user1}}" ? "user" : "agent"}
+
+ {
+ const updated = [...(d.messageExamples ?? [])];
+ const convoClone = { examples: [...updated[ci].examples] };
+ convoClone.examples[mi] = { ...convoClone.examples[mi], content: { text: e.target.value } };
+ updated[ci] = convoClone;
+ handleFieldEdit("messageExamples", updated);
+ }}
+ className={inputCls + " flex-1"}
+ />
+
+ ))}
+
+ ))}
+ {(d.messageExamples ?? []).length === 0 && (
+
no chat examples yet. click generate to create some.
+ )}
+
+
+
+ {/* Post Examples */}
+
+
+ ▶
+ post examples
+ — social media voice
+
+
+
+ {(d.postExamples ?? []).map((post: string, pi: number) => (
+
+ {
+ const updated = [...(d.postExamples ?? [])];
+ updated[pi] = e.target.value;
+ handleFieldEdit("postExamples", updated);
+ }}
+ className={inputCls + " flex-1"}
+ />
+
+
+ ))}
+ {(d.postExamples ?? []).length === 0 && (
+
no post examples yet. click generate to create some.
+ )}
+
+
+
+
+ {/* Save */}
+
+
+ {characterSaveSuccess && (
+ {characterSaveSuccess}
+ )}
+ {characterSaveError && (
+ {characterSaveError}
+ )}
+
+
+ )}
+
+
+ {/* ═══ VOICE ═══ */}
+
+
Voice
+
+ {voiceLoading ? (
+
Loading voice config...
+ ) : (
+ <>
+ {/* Provider selector buttons */}
+
+ {([
+ { id: "elevenlabs" as const, label: "ElevenLabs", hint: "Premium neural voices" },
+ { id: "simple-voice" as const, label: "Simple Voice", hint: "Retro SAM TTS" },
+ { id: "edge" as const, label: "Microsoft Edge", hint: "Free browser voices" },
+ ] as const).map((p) => {
+ const active = voiceProvider === p.id;
+ return (
+
+ );
+ })}
+
+
+ {/* ── ElevenLabs settings ─────────────────────────────── */}
+ {voiceProvider === "elevenlabs" && (
+
+ {/* Cloud / Own Key toggle */}
+
+
+
+
+
+ {/* Cloud mode status */}
+ {voiceMode === "cloud" && (
+
+ {cloudConnected ? (
+
+
+
+ Logged into Eliza Cloud
+ {cloudUserId && (
+ {cloudUserId}
+ )}
+
+
+
+ ) : (
+
+
+
+ Not connected — log in to use cloud TTS
+
+
+
+ )}
+ {cloudLoginError && (
+
{cloudLoginError}
+ )}
+
+ )}
+
+ {/* Own key mode */}
+ {voiceMode === "own-key" && (
+
+
+
+ {voiceConfig.elevenlabs?.apiKey && (
+
configured
+ )}
+
Get key
+
+
handleVoiceFieldChange("elevenlabs", "apiKey", e.target.value)}
+ className="w-full px-2.5 py-[7px] border border-[var(--border)] bg-[var(--card)] text-[13px] font-[var(--mono)] transition-colors focus:border-[var(--accent)] focus:outline-none"
+ />
+
+ )}
+
+ {/* ── Voice presets ──────────────────────────────────── */}
+
+
Choose a Voice
+
+ {/* Female */}
+
Female
+
+ {VOICE_PRESETS.filter((p) => p.gender === "female").map((p) => {
+ const active = selectedPresetId === p.id;
+ return (
+
+ );
+ })}
+
+
+ {/* Male */}
+
Male
+
+ {VOICE_PRESETS.filter((p) => p.gender === "male").map((p) => {
+ const active = selectedPresetId === p.id;
+ return (
+
+ );
+ })}
+
+
+ {/* Character / Game */}
+
Character / Game
+
+ {VOICE_PRESETS.filter((p) => p.gender === "character").map((p) => {
+ const active = selectedPresetId === p.id;
+ return (
+
+ );
+ })}
+
+
+ {/* Custom voice ID */}
+
+
+ {selectedPresetId === "custom" && (
+ handleVoiceFieldChange("elevenlabs", "voiceId", e.target.value)}
+ className="flex-1 px-2.5 py-[7px] border border-[var(--border)] bg-[var(--card)] text-[13px] font-[var(--mono)] transition-colors focus:border-[var(--accent)] focus:outline-none"
+ />
+ )}
+
+
+
+ {/* ── Advanced settings (collapsed) ─────────────────── */}
+
+
+ ▶
+ Advanced Settings
+
+
+
+
+
+
+
+
+
+
+ {/* Stop test button */}
+ {voiceTesting && (
+
+
+
+ )}
+
+ )}
+
+ {/* ── Simple Voice settings ───────────────────────────── */}
+ {voiceProvider === "simple-voice" && (
+
+
+ No configuration needed. Works offline.
+
+
+ )}
+
+ {/* ── Microsoft Edge TTS settings ─────────────────────── */}
+ {voiceProvider === "edge" && (
+
+ )}
+
+ {/* Save button */}
+
+
+ {voiceSaveError && (
+ {voiceSaveError}
+ )}
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/app/src/components/ChatAvatar.tsx b/apps/app/src/components/ChatAvatar.tsx
new file mode 100644
index 0000000000..6b565f829d
--- /dev/null
+++ b/apps/app/src/components/ChatAvatar.tsx
@@ -0,0 +1,66 @@
+/**
+ * Chat avatar overlay component.
+ *
+ * Renders a 3D VRM avatar on the right side of the chat area.
+ * The avatar sits behind the chat text (lower z-index) and does not scroll.
+ *
+ * Voice controls are managed externally — this component accepts mouthOpen
+ * and renders the VRM viewer.
+ */
+
+import { useCallback, useRef, useState } from "react";
+import { VrmViewer } from "./avatar/VrmViewer";
+import type { VrmEngine } from "./avatar/VrmEngine";
+import { useApp, getVrmUrl } from "../AppContext";
+
+export interface ChatAvatarProps {
+ /** Mouth openness value (0-1) for lip sync animation */
+ mouthOpen?: number;
+ /** Whether the agent is currently speaking (drives engine-side mouth anim) */
+ isSpeaking?: boolean;
+}
+
+export function ChatAvatar({ mouthOpen = 0, isSpeaking = false }: ChatAvatarProps) {
+ const { selectedVrmIndex, customVrmUrl } = useApp();
+
+ // Resolve VRM path from selected index or custom upload
+ const vrmPath = selectedVrmIndex === 0 && customVrmUrl
+ ? customVrmUrl
+ : getVrmUrl(selectedVrmIndex || 1);
+
+ const vrmEngineRef = useRef(null);
+ const [avatarReady, setAvatarReady] = useState(false);
+
+ const handleEngineReady = useCallback((engine: VrmEngine) => {
+ vrmEngineRef.current = engine;
+ setAvatarReady(true);
+ }, []);
+
+ return (
+
+ {/* Avatar canvas — pushed right (overflows edge), shifted down 10% */}
+
+
+
+
+ );
+}
diff --git a/apps/app/src/components/ChatView.tsx b/apps/app/src/components/ChatView.tsx
index f71f0c2549..f6a68e1142 100644
--- a/apps/app/src/components/ChatView.tsx
+++ b/apps/app/src/components/ChatView.tsx
@@ -1,24 +1,83 @@
/**
* Chat view component.
*
- * Layout: flex column filling parent. Header row (title + clear).
+ * Layout: flex column filling parent. Header row (title + clear + toggles).
* Scrollable messages area. Share/file notices below messages.
- * Input row at bottom with textarea + send button.
+ * Input row at bottom with mic + textarea + send button.
*/
-import { useRef, useEffect } from "react";
+import { useRef, useEffect, useCallback, useState } from "react";
import { useApp } from "../AppContext.js";
+import { ChatAvatar } from "./ChatAvatar.js";
+import { useVoiceChat } from "../hooks/useVoiceChat.js";
+import { client, type VoiceConfig } from "../api-client.js";
+
+// ── Typewriter streaming component ────────────────────────────────────
+// Reveals text progressively using direct DOM manipulation (no React
+// re-renders per frame). Speed auto-scales so streaming never takes
+// longer than ~3 seconds regardless of text length.
+
+interface StreamingTextProps {
+ text: string;
+ onComplete?: () => void;
+ /** Called every frame so the parent can auto-scroll. */
+ onProgress?: () => void;
+}
+
+function StreamingText({ text, onComplete, onProgress }: StreamingTextProps) {
+ const spanRef = useRef(null);
+ const onCompleteRef = useRef(onComplete);
+ const onProgressRef = useRef(onProgress);
+ onCompleteRef.current = onComplete;
+ onProgressRef.current = onProgress;
+
+ useEffect(() => {
+ const span = spanRef.current;
+ if (!span) return;
+
+ let current = 0;
+ let cancelled = false;
+
+ // Auto-scale: at least 4 chars/frame, but cap total duration at ~3 s
+ const MAX_DURATION_MS = 3000;
+ const framesAtMax = MAX_DURATION_MS / 16.67; // ~180 frames
+ const charsPerFrame = Math.max(4, Math.ceil(text.length / framesAtMax));
+
+ const frame = () => {
+ if (cancelled) return;
+ current = Math.min(current + charsPerFrame, text.length);
+ span.textContent = text.slice(0, current);
+ onProgressRef.current?.();
+ if (current < text.length) {
+ requestAnimationFrame(frame);
+ } else {
+ onCompleteRef.current?.();
+ }
+ };
+ requestAnimationFrame(frame);
+
+ return () => {
+ cancelled = true;
+ // On unmount / text change, show full text immediately
+ if (span) span.textContent = text;
+ };
+ }, [text]);
+
+ return (
+ <>
+
+
+ >
+ );
+}
export function ChatView() {
const {
agentStatus,
chatInput,
chatSending,
- conversations,
- activeConversationId,
conversationMessages,
handleChatSend,
- handleChatClear,
setState,
droppedFiles,
shareIngestNotice,
@@ -28,9 +87,112 @@ export function ChatView() {
const messagesRef = useRef(null);
const textareaRef = useRef(null);
+ // ── Toggles (persisted in localStorage) ──────────────────────────
+ const [avatarVisible, setAvatarVisible] = useState(() => {
+ try {
+ const v = localStorage.getItem("milaidy:chat:avatarVisible");
+ return v === null ? true : v === "true";
+ } catch { return true; }
+ });
+ const [agentVoiceMuted, setAgentVoiceMuted] = useState(() => {
+ try {
+ const v = localStorage.getItem("milaidy:chat:voiceMuted");
+ return v === null ? true : v === "true"; // muted by default
+ } catch { return true; }
+ });
+
+ // Persist toggle changes
+ useEffect(() => {
+ try { localStorage.setItem("milaidy:chat:avatarVisible", String(avatarVisible)); } catch { /* ignore */ }
+ }, [avatarVisible]);
+ useEffect(() => {
+ try { localStorage.setItem("milaidy:chat:voiceMuted", String(agentVoiceMuted)); } catch { /* ignore */ }
+ }, [agentVoiceMuted]);
+
+ // ── Streaming text reveal ──────────────────────────────────────────
+ // Tracks the ID of the message currently being "streamed in" via
+ // typewriter. Set during render (React bail-out) so the very first
+ // frame already renders StreamingText — no flash.
+ const [streamingMsgId, setStreamingMsgId] = useState(null);
+ const lastStreamedIdRef = useRef(null);
+
+ // ── Voice config (ElevenLabs / browser TTS) ────────────────────────
+ const [voiceConfig, setVoiceConfig] = useState(null);
+
+ // Load saved voice config on mount so the correct TTS provider is used
+ useEffect(() => {
+ void (async () => {
+ try {
+ const cfg = await client.getConfig();
+ const messages = cfg.messages as Record> | undefined;
+ const tts = messages?.tts as VoiceConfig | undefined;
+ if (tts) setVoiceConfig(tts);
+ } catch { /* ignore — will use browser TTS fallback */ }
+ })();
+ }, []);
+
+ // ── Voice chat ────────────────────────────────────────────────────
+ const handleVoiceTranscript = useCallback(
+ (text: string) => {
+ if (chatSending) return;
+ setState("chatInput", text);
+ setTimeout(() => void handleChatSend(), 50);
+ },
+ [chatSending, setState, handleChatSend],
+ );
+
+ const voice = useVoiceChat({ onTranscript: handleVoiceTranscript, voiceConfig });
+
+ // ── Detect new assistant messages ────────────────────────────────
+ const lastMsg = conversationMessages.length > 0
+ ? conversationMessages[conversationMessages.length - 1]
+ : null;
+
+ // Initialise refs to the current last assistant message so we don't
+ // replay old history or re-stream on mount.
+ const lastSpokenIdRef = useRef(
+ lastMsg?.role === "assistant" ? lastMsg.id : null,
+ );
+ if (lastStreamedIdRef.current === null && lastMsg?.role === "assistant") {
+ lastStreamedIdRef.current = lastMsg.id;
+ }
+
+ // When a new assistant message appears:
+ // 1. Start typewriter streaming (text appears progressively)
+ // 2. Start voice immediately in parallel (full text, non-blocking)
+ if (
+ lastMsg &&
+ lastMsg.role === "assistant" &&
+ lastMsg.id !== lastStreamedIdRef.current &&
+ !chatSending
+ ) {
+ lastStreamedIdRef.current = lastMsg.id;
+
+ // Start typewriter — React bails out of this render and immediately
+ // re-renders with the new streamingMsgId, so the first visible frame
+ // already shows StreamingText (no flash of full text).
+ if (streamingMsgId !== lastMsg.id) {
+ setStreamingMsgId(lastMsg.id);
+ }
+
+ // Start voice (independently of typewriter)
+ if (lastMsg.id !== lastSpokenIdRef.current && !agentVoiceMuted) {
+ lastSpokenIdRef.current = lastMsg.id;
+ voice.speak(lastMsg.text);
+ }
+ }
+
+ const handleStreamComplete = useCallback(() => {
+ setStreamingMsgId(null);
+ }, []);
+
+ const handleStreamProgress = useCallback(() => {
+ const el = messagesRef.current;
+ if (el) el.scrollTop = el.scrollHeight;
+ }, []);
+
const agentName = agentStatus?.agentName ?? "Agent";
const agentState = agentStatus?.state ?? "not_started";
- const convTitle = conversations.find((c) => c.id === activeConversationId)?.title ?? "Chat";
const msgs = conversationMessages;
// Scroll to bottom when messages change
@@ -61,11 +223,9 @@ export function ChatView() {
if (agentState === "not_started" || agentState === "stopped") {
return (
- {/* Header row */}
Chat
- {/* Start agent box */}
Agent is not running. Start it to begin chatting.