diff --git a/docs/docs.json b/docs/docs.json index 069f0c48b..9f986fa1c 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -52,8 +52,6 @@ "history", "context-management", "document-graph", - "usage-dashboard", - "symphony", "git-worktrees", "group-chat", "remote-access", @@ -74,7 +72,7 @@ { "group": "Encore Features", "icon": "flask", - "pages": ["encore-features", "director-notes"] + "pages": ["encore-features", "director-notes", "usage-dashboard", "symphony"] }, { "group": "Providers & CLI", diff --git a/docs/encore-features.md b/docs/encore-features.md index 9b4928de7..0f6338d7d 100644 --- a/docs/encore-features.md +++ b/docs/encore-features.md @@ -19,8 +19,8 @@ Open **Settings** (`Cmd+,` / `Ctrl+,`) and navigate to the **Encore Features** t | Feature | Shortcut | Description | | ------------------------------------ | ------------------------------ | --------------------------------------------------------------- | | [Director's Notes](./director-notes) | `Cmd+Shift+O` / `Ctrl+Shift+O` | Unified timeline of all agent activity with AI-powered synopses | - -More features will be added here as they ship. +| [Usage Dashboard](./usage-dashboard) | `Opt+Cmd+U` / `Alt+Ctrl+U` | Comprehensive analytics for tracking AI usage patterns | +| [Maestro Symphony](./symphony) | `Cmd+Shift+Y` / `Ctrl+Shift+Y` | Contribute to open source by donating AI tokens | ## For Developers diff --git a/docs/features.md b/docs/features.md index 8c556e17f..baaadf7fa 100644 --- a/docs/features.md +++ b/docs/features.md @@ -9,7 +9,7 @@ icon: sparkles - ๐ŸŒณ **[Git Worktrees](./git-worktrees)** - Run AI agents in parallel on isolated branches. Create worktree sub-agents from the git branch menu, each operating in their own directory. Work interactively in the main repo while sub-agents process tasks independently โ€” then create PRs with one click. True parallel development without conflicts. - ๐Ÿค– **[Auto Run & Playbooks](./autorun-playbooks)** - File-system-based task runner that processes markdown checklists through AI agents. Create Playbooks (collections of Auto Run documents) for repeatable workflows, run in loops, and track progress with full history. Each task gets its own AI session for clean conversation context. - ๐Ÿช **[Playbook Exchange](./playbook-exchange)** - Browse and import community-contributed playbooks directly into your Auto Run folder. Categories, search, and one-click import get you started with proven workflows for security audits, code reviews, documentation, and more. -- ๐ŸŽต **[Maestro Symphony](./symphony)** - Contribute to open source by donating AI tokens. Browse registered projects, select GitHub issues, and let Maestro clone, process Auto Run docs, and create PRs automatically. Distributed computing for AI-assisted development. +- ๐ŸŽต **[Maestro Symphony](./symphony)** - Contribute to open source by donating AI tokens. Browse registered projects, select GitHub issues, and let Maestro clone, process Auto Run docs, and create PRs automatically. Distributed computing for AI-assisted development. _(Encore Feature โ€” enable in Settings > Encore Features)_ - ๐Ÿ’ฌ **[Group Chat](./group-chat)** - Coordinate multiple AI agents in a single conversation. A moderator AI orchestrates discussions, routing questions to the right agents and synthesizing their responses for cross-project questions and architecture discussions. - ๐ŸŒ **[Remote Access](./remote-access)** - Built-in web server with QR code access. Monitor and control all your agents from your phone. Supports local network access and remote tunneling via Cloudflare for access from anywhere. - ๐Ÿ”— **[SSH Remote Execution](./ssh-remote-execution)** - Run AI agents on remote hosts via SSH. Leverage powerful cloud VMs, access tools not installed locally, or work with projects requiring specific environments โ€” all while controlling everything from your local Maestro instance. @@ -34,7 +34,7 @@ icon: sparkles - ๐ŸŽจ **[Beautiful Themes](https://github.com/RunMaestro/Maestro/blob/main/THEMES.md)** - 17 built-in themes across dark (Dracula, Monokai, Nord, Tokyo Night, Catppuccin Mocha, Gruvbox Dark), light (GitHub, Solarized, One Light, Gruvbox Light, Catppuccin Latte, Ayu Light), and vibe (Pedurple, Maestro's Choice, Dre Synth, InQuest) categories, plus a fully customizable theme builder. - โฑ๏ธ **[WakaTime Integration](./configuration#wakatime-integration)** - Automatic time tracking via WakaTime with optional per-file write activity tracking across all supported agents. - ๐Ÿ’ฐ **Cost Tracking** - Real-time token usage and cost tracking per session and globally. -- ๐Ÿ“Š **[Usage Dashboard](./usage-dashboard)** - Comprehensive analytics for tracking AI usage patterns. View aggregated statistics, compare agent performance, analyze activity heatmaps, and export data to CSV. Access via `Opt+Cmd+U` / `Alt+Ctrl+U`. +- ๐Ÿ“Š **[Usage Dashboard](./usage-dashboard)** - Comprehensive analytics for tracking AI usage patterns. View aggregated statistics, compare agent performance, analyze activity heatmaps, and export data to CSV. Access via `Opt+Cmd+U` / `Alt+Ctrl+U`. _(Encore Feature โ€” enable in Settings > Encore Features)_ - ๐ŸŽฌ **[Director's Notes](./director-notes)** - Bird's-eye view of all agent activity in a unified timeline. Aggregate history from every agent, search and filter entries, and generate AI-powered synopses of recent work. Access via `Cmd+Shift+O` / `Ctrl+Shift+O`. _(Encore Feature โ€” enable in Settings > Encore Features)_ - ๐Ÿ† **[Achievements](./achievements)** - Level up from Apprentice to Titan of the Baton based on cumulative Auto Run time. 11 conductor-themed ranks to unlock. diff --git a/package-lock.json b/package-lock.json index 7482623e1..52ae04685 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "maestro", - "version": "0.15.0", + "version": "0.15.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "maestro", - "version": "0.15.0", + "version": "0.15.1", "hasInstallScript": true, "license": "AGPL 3.0", "dependencies": { @@ -264,6 +264,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -667,6 +668,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -710,6 +712,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2283,6 +2286,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2304,6 +2308,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz", "integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==", "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -2316,6 +2321,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2331,6 +2337,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.208.0.tgz", "integrity": "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.208.0", "import-in-the-middle": "^2.0.0", @@ -2718,6 +2725,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2734,6 +2742,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", @@ -2751,6 +2760,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" } @@ -3809,8 +3819,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4348,6 +4357,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -4359,6 +4369,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4484,6 +4495,7 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -4914,6 +4926,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4995,6 +5008,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5998,6 +6012,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -6480,6 +6495,7 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -7205,6 +7221,7 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -7614,6 +7631,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -8111,6 +8129,7 @@ "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "24.13.3", "builder-util": "24.13.1", @@ -8206,8 +8225,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dompurify": { "version": "3.3.0", @@ -8351,7 +8369,6 @@ "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "24.13.3", "archiver": "^5.3.1", @@ -8365,7 +8382,6 @@ "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", @@ -8385,7 +8401,6 @@ "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", @@ -8408,7 +8423,6 @@ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -8425,7 +8439,6 @@ "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", @@ -8442,7 +8455,6 @@ "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" @@ -8457,7 +8469,6 @@ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -8473,7 +8484,6 @@ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -8486,8 +8496,7 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/electron-builder-squirrel-windows/node_modules/string_decoder": { "version": "1.1.1", @@ -8495,7 +8504,6 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -8506,7 +8514,6 @@ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -8517,7 +8524,6 @@ "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", @@ -8533,7 +8539,6 @@ "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", @@ -9215,6 +9220,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -11134,6 +11140,7 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -11954,6 +11961,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -12423,16 +12431,14 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", @@ -12445,8 +12451,7 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.isequal": { "version": "4.5.0", @@ -12460,8 +12465,7 @@ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -12475,8 +12479,7 @@ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/log-symbols": { "version": "4.1.0", @@ -12567,7 +12570,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -15065,6 +15067,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -15305,7 +15308,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -15321,7 +15323,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -15666,6 +15667,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -15695,6 +15697,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -15742,6 +15745,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -15928,7 +15932,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -17685,6 +17690,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -17995,6 +18001,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18368,6 +18375,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -18873,6 +18881,7 @@ "integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.15", "@vitest/mocker": "4.0.15", @@ -19463,6 +19472,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -19476,6 +19486,7 @@ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -20073,6 +20084,7 @@ "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/__tests__/renderer/components/Settings/tabs/EncoreTab.test.tsx b/src/__tests__/renderer/components/Settings/tabs/EncoreTab.test.tsx index 6378957b4..7bd5a17fa 100644 --- a/src/__tests__/renderer/components/Settings/tabs/EncoreTab.test.tsx +++ b/src/__tests__/renderer/components/Settings/tabs/EncoreTab.test.tsx @@ -87,6 +87,11 @@ vi.mock('../../../../../renderer/components/Wizard/screens/AgentSelectionScreen' // Shared mock fns for useSettings setters const mockSetEncoreFeatures = vi.fn(); const mockSetDirectorNotesSettings = vi.fn(); +const mockSetStatsCollectionEnabled = vi.fn(); +const mockSetDefaultStatsTimeRange = vi.fn(); +const mockSetWakatimeEnabled = vi.fn(); +const mockSetWakatimeApiKey = vi.fn(); +const mockSetWakatimeDetailedTracking = vi.fn(); // Override mechanism for per-test customization let mockUseSettingsOverrides: Record = {}; @@ -100,6 +105,21 @@ vi.mock('../../../../../renderer/hooks/settings/useSettings', () => ({ defaultLookbackDays: 7, }, setDirectorNotesSettings: mockSetDirectorNotesSettings, + // Stats + statsCollectionEnabled: true, + setStatsCollectionEnabled: mockSetStatsCollectionEnabled, + defaultStatsTimeRange: 'week', + setDefaultStatsTimeRange: mockSetDefaultStatsTimeRange, + // WakaTime + wakatimeEnabled: false, + setWakatimeEnabled: mockSetWakatimeEnabled, + wakatimeApiKey: '', + setWakatimeApiKey: mockSetWakatimeApiKey, + wakatimeDetailedTracking: false, + setWakatimeDetailedTracking: mockSetWakatimeDetailedTracking, + // Symphony + symphonyRegistryUrls: [], + setSymphonyRegistryUrls: vi.fn(), ...mockUseSettingsOverrides, }), })); @@ -170,6 +190,10 @@ describe('EncoreTab', () => { vi.mocked(window.maestro.agents.getConfig).mockResolvedValue({}); vi.mocked(window.maestro.agents.setConfig).mockResolvedValue(undefined); vi.mocked(window.maestro.agents.getModels).mockResolvedValue([]); + vi.mocked(window.maestro.stats.getDatabaseSize).mockResolvedValue(1024 * 1024); + vi.mocked(window.maestro.stats.getEarliestTimestamp).mockResolvedValue(null); + vi.mocked(window.maestro.wakatime.checkCli).mockResolvedValue({ available: false }); + vi.mocked(window.maestro.wakatime.validateApiKey).mockResolvedValue({ valid: false }); }); afterEach(() => { diff --git a/src/__tests__/renderer/components/Settings/tabs/GeneralTab.test.tsx b/src/__tests__/renderer/components/Settings/tabs/GeneralTab.test.tsx index ed7288383..264388d46 100644 --- a/src/__tests__/renderer/components/Settings/tabs/GeneralTab.test.tsx +++ b/src/__tests__/renderer/components/Settings/tabs/GeneralTab.test.tsx @@ -17,8 +17,6 @@ * - Rendering options (GPU acceleration, confetti) * - Update settings (check on startup, beta updates) * - Crash reporting toggle - * - Stats collection toggle and time range selector - * - WakaTime integration (toggle, API key, detailed tracking, CLI check) * - Storage location display */ @@ -53,11 +51,6 @@ const mockSetDisableConfetti = vi.fn(); const mockSetCheckForUpdatesOnStartup = vi.fn(); const mockSetEnableBetaUpdates = vi.fn(); const mockSetCrashReportingEnabled = vi.fn(); -const mockSetStatsCollectionEnabled = vi.fn(); -const mockSetDefaultStatsTimeRange = vi.fn(); -const mockSetWakatimeEnabled = vi.fn(); -const mockSetWakatimeApiKey = vi.fn(); -const mockSetWakatimeDetailedTracking = vi.fn(); // Allow per-test overrides of settings let mockUseSettingsOverrides: Record = {}; @@ -109,18 +102,6 @@ vi.mock('../../../../../renderer/hooks/settings/useSettings', () => ({ setEnableBetaUpdates: mockSetEnableBetaUpdates, crashReportingEnabled: true, setCrashReportingEnabled: mockSetCrashReportingEnabled, - // Stats - statsCollectionEnabled: true, - setStatsCollectionEnabled: mockSetStatsCollectionEnabled, - defaultStatsTimeRange: 'week', - setDefaultStatsTimeRange: mockSetDefaultStatsTimeRange, - // WakaTime - wakatimeEnabled: false, - setWakatimeEnabled: mockSetWakatimeEnabled, - wakatimeApiKey: '', - setWakatimeApiKey: mockSetWakatimeApiKey, - wakatimeDetailedTracking: false, - setWakatimeDetailedTracking: mockSetWakatimeDetailedTracking, ...mockUseSettingsOverrides, }), })); @@ -158,10 +139,6 @@ describe('GeneralTab', () => { // Reset window.maestro mocks vi.mocked(window.maestro.shells.detect).mockResolvedValue(mockShells); - vi.mocked(window.maestro.wakatime.checkCli).mockResolvedValue({ available: false }); - vi.mocked(window.maestro.wakatime.validateApiKey).mockResolvedValue({ valid: false }); - vi.mocked(window.maestro.stats.getDatabaseSize).mockResolvedValue(1024 * 1024); - vi.mocked(window.maestro.stats.getEarliestTimestamp).mockResolvedValue(null); vi.mocked(window.maestro.sync.getDefaultPath).mockResolvedValue('/default/path'); vi.mocked(window.maestro.sync.getSettings).mockResolvedValue({ customSyncPath: undefined, @@ -200,7 +177,6 @@ describe('GeneralTab', () => { expect(screen.getByText('Updates')).toBeInTheDocument(); expect(screen.getByText('Pre-release Channel')).toBeInTheDocument(); expect(screen.getByText('Privacy')).toBeInTheDocument(); - expect(screen.getByText('Usage & Stats')).toBeInTheDocument(); expect(screen.getByText('Storage Location')).toBeInTheDocument(); }); @@ -212,9 +188,8 @@ describe('GeneralTab', () => { }); // The component still renders its JSX, but effects that fetch data won't fire - // Verify the sync/stats load effects didn't run + // Verify the sync load effects didn't run expect(window.maestro.sync.getDefaultPath).not.toHaveBeenCalled(); - expect(window.maestro.stats.getDatabaseSize).not.toHaveBeenCalled(); }); }); @@ -1319,274 +1294,6 @@ describe('GeneralTab', () => { }); }); - // ========================================================================= - // 18. WakaTime - // ========================================================================= - describe('WakaTime', () => { - it('should render WakaTime enable toggle', async () => { - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - expect(screen.getByText('Enable WakaTime tracking')).toBeInTheDocument(); - }); - - it('should call setWakatimeEnabled when toggle is clicked', async () => { - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - // The WakaTime toggle is a standalone switch (not wrapped in SettingCheckbox). - // Find it via aria-checked attribute on the WakaTime section's switch. - // Since wakatimeEnabled is false by default, find the switch with aria-checked=false - // that is adjacent to the WakaTime label. - const titleElement = screen.getByText('Enable WakaTime tracking'); - // Walk up from

->

->
- const outerContainer = titleElement.parentElement?.parentElement; - const toggleSwitch = outerContainer?.querySelector('button[role="switch"]'); - - fireEvent.click(toggleSwitch!); - expect(mockSetWakatimeEnabled).toHaveBeenCalledWith(true); - }); - - it('should show API key input when WakaTime is enabled', async () => { - mockUseSettingsOverrides = { wakatimeEnabled: true }; - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - expect(screen.getByPlaceholderText('waka_...')).toBeInTheDocument(); - }); - - it('should not show API key input when WakaTime is disabled', async () => { - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - expect(screen.queryByPlaceholderText('waka_...')).not.toBeInTheDocument(); - }); - - it('should call setWakatimeApiKey when API key input changes', async () => { - mockUseSettingsOverrides = { wakatimeEnabled: true }; - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - const apiKeyInput = screen.getByPlaceholderText('waka_...'); - fireEvent.change(apiKeyInput, { target: { value: 'waka_test123' } }); - - expect(mockSetWakatimeApiKey).toHaveBeenCalledWith('waka_test123'); - }); - - it('should show detailed tracking toggle when WakaTime is enabled', async () => { - mockUseSettingsOverrides = { wakatimeEnabled: true }; - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - expect(screen.getByText('Detailed file tracking')).toBeInTheDocument(); - }); - - it('should call setWakatimeDetailedTracking when toggle is clicked', async () => { - mockUseSettingsOverrides = { wakatimeEnabled: true }; - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - const titleElement = screen.getByText('Detailed file tracking'); - const parentDiv = titleElement.closest('.flex'); - const toggleSwitch = parentDiv?.querySelector('button[role="switch"]'); - - fireEvent.click(toggleSwitch!); - expect(mockSetWakatimeDetailedTracking).toHaveBeenCalledWith(true); - }); - - it('should check WakaTime CLI when enabled and modal is open', async () => { - mockUseSettingsOverrides = { wakatimeEnabled: true }; - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - expect(window.maestro.wakatime.checkCli).toHaveBeenCalled(); - }); - - it('should not check WakaTime CLI when disabled', async () => { - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - expect(window.maestro.wakatime.checkCli).not.toHaveBeenCalled(); - }); - - it('should show CLI installing message when CLI is not available', async () => { - mockUseSettingsOverrides = { wakatimeEnabled: true }; - vi.mocked(window.maestro.wakatime.checkCli).mockResolvedValue({ available: false }); - - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - expect( - screen.getByText('WakaTime CLI is being installed automatically...') - ).toBeInTheDocument(); - }); - - it('should not show CLI installing message when CLI is available', async () => { - mockUseSettingsOverrides = { wakatimeEnabled: true }; - vi.mocked(window.maestro.wakatime.checkCli).mockResolvedValue({ - available: true, - version: '1.0.0', - }); - - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - expect( - screen.queryByText('WakaTime CLI is being installed automatically...') - ).not.toBeInTheDocument(); - }); - - it('should validate API key on blur', async () => { - mockUseSettingsOverrides = { - wakatimeEnabled: true, - wakatimeApiKey: 'waka_test123', - }; - vi.mocked(window.maestro.wakatime.validateApiKey).mockResolvedValue({ valid: true }); - - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - const apiKeyInput = screen.getByPlaceholderText('waka_...'); - fireEvent.blur(apiKeyInput); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - expect(window.maestro.wakatime.validateApiKey).toHaveBeenCalledWith('waka_test123'); - }); - }); - - // ========================================================================= - // 19. Stats Collection - // ========================================================================= - describe('Stats Collection', () => { - it('should render stats collection toggle', async () => { - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - expect(screen.getByText('Enable stats collection')).toBeInTheDocument(); - }); - - it('should call setStatsCollectionEnabled when toggle is clicked', async () => { - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - const titleElement = screen.getByText('Enable stats collection'); - const parentDiv = titleElement.closest('.flex'); - const toggleSwitch = parentDiv?.querySelector('button[role="switch"]'); - - fireEvent.click(toggleSwitch!); - expect(mockSetStatsCollectionEnabled).toHaveBeenCalledWith(false); - }); - - it('should render time range select', async () => { - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - expect(screen.getByText('Default dashboard time range')).toBeInTheDocument(); - const select = screen.getByDisplayValue('Last 7 days') as HTMLSelectElement; - expect(select).toBeInTheDocument(); - }); - - it('should call setDefaultStatsTimeRange when time range is changed', async () => { - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - const select = screen.getByDisplayValue('Last 7 days'); - fireEvent.change(select, { target: { value: 'month' } }); - - expect(mockSetDefaultStatsTimeRange).toHaveBeenCalledWith('month'); - }); - - it('should show all time range options', async () => { - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - expect(screen.getByText('Last 24 hours')).toBeInTheDocument(); - expect(screen.getByText('Last 7 days')).toBeInTheDocument(); - expect(screen.getByText('Last 30 days')).toBeInTheDocument(); - expect(screen.getByText('Last 365 days')).toBeInTheDocument(); - expect(screen.getByText('All time')).toBeInTheDocument(); - }); - - it('should display database size', async () => { - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - expect(screen.getByText('Database size')).toBeInTheDocument(); - // 1024*1024 bytes = 1.00 MB - expect(screen.getByText(/1\.00 MB/)).toBeInTheDocument(); - }); - - it('should display Loading... when stats size is not yet loaded', async () => { - vi.mocked(window.maestro.stats.getDatabaseSize).mockImplementation( - () => new Promise(() => {}) // never resolves - ); - - render(); - - await act(async () => { - await vi.advanceTimersByTimeAsync(100); - }); - - expect(screen.getByText('Loading...')).toBeInTheDocument(); - }); - }); // ========================================================================= // 20. Shell Detection Failure diff --git a/src/__tests__/renderer/components/SettingsModal.test.tsx b/src/__tests__/renderer/components/SettingsModal.test.tsx index ab4aa7e61..d2a11fa4a 100644 --- a/src/__tests__/renderer/components/SettingsModal.test.tsx +++ b/src/__tests__/renderer/components/SettingsModal.test.tsx @@ -207,7 +207,7 @@ vi.mock('../../../renderer/hooks/settings/useSettings', () => ({ customAICommands: [], setCustomAICommands: mockSetCustomAICommands, // Encore features - encoreFeatures: { directorNotes: false }, + encoreFeatures: { directorNotes: false, usageStats: true, symphony: true }, setEncoreFeatures: mockSetEncoreFeatures, // Conductor profile settings conductorProfile: '', @@ -273,6 +273,9 @@ vi.mock('../../../renderer/hooks/settings/useSettings', () => ({ setUseNativeTitleBar: vi.fn(), autoHideMenuBar: false, setAutoHideMenuBar: vi.fn(), + // Symphony registry URLs + symphonyRegistryUrls: [], + setSymphonyRegistryUrls: vi.fn(), ...mockUseSettingsOverrides, }), })); @@ -2073,206 +2076,205 @@ describe('SettingsModal', () => { }); }); - describe('WakaTime CLI status', () => { - it('should not check CLI when WakaTime is disabled', async () => { + describe('Shell selection with mouseEnter and focus', () => { + it('should load shells on mouseEnter', async () => { render(); await act(async () => { await vi.advanceTimersByTimeAsync(100); }); - expect(window.maestro.wakatime.checkCli).not.toHaveBeenCalled(); - }); + // Trigger shell loading via mouseEnter + const detectButton = screen.getByText('Detect other available shells...'); - it('should check CLI when WakaTime is enabled', async () => { - mockUseSettingsOverrides = { wakatimeEnabled: true }; - vi.mocked(window.maestro.wakatime.checkCli).mockResolvedValue({ - available: true, - version: '1.0.0', + // Load shells first + fireEvent.click(detectButton); + + await act(async () => { + await vi.advanceTimersByTimeAsync(100); }); - render(); + // Now shells should be loaded, find a shell button + const zshButton = screen.getByText('Zsh').closest('button'); + expect(zshButton).toBeInTheDocument(); + + // Trigger mouseEnter - should not reload (already loaded) + fireEvent.mouseEnter(zshButton!); await act(async () => { - await vi.advanceTimersByTimeAsync(100); + await vi.advanceTimersByTimeAsync(50); }); - expect(window.maestro.wakatime.checkCli).toHaveBeenCalled(); + // shells.detect should only have been called once + expect(window.maestro.shells.detect).toHaveBeenCalledTimes(1); }); + }); - it('should show auto-install message when CLI is not available', async () => { - mockUseSettingsOverrides = { wakatimeEnabled: true }; - vi.mocked(window.maestro.wakatime.checkCli).mockResolvedValue({ available: false }); - + describe('Encore Features settings tab', () => { + it('should render Encore Features tab button', async () => { render(); await act(async () => { - await vi.advanceTimersByTimeAsync(100); + await vi.advanceTimersByTimeAsync(50); }); - expect( - screen.getByText('WakaTime CLI is being installed automatically...') - ).toBeInTheDocument(); + expect(screen.getByTitle('Encore Features')).toBeInTheDocument(); }); - it('should retry CLI check after 3 seconds when first check returns unavailable', async () => { - mockUseSettingsOverrides = { wakatimeEnabled: true }; - vi.mocked(window.maestro.wakatime.checkCli) - .mockResolvedValueOnce({ available: false }) - .mockResolvedValueOnce({ available: true, version: '1.0.0' }); - + it('should switch to Encore Features tab when clicked', async () => { render(); await act(async () => { - await vi.advanceTimersByTimeAsync(100); + await vi.advanceTimersByTimeAsync(50); }); - // First check should have been called - expect(window.maestro.wakatime.checkCli).toHaveBeenCalledTimes(1); + const tab = screen.getByTitle('Encore Features'); + fireEvent.click(tab); - // Advance to trigger retry await act(async () => { - await vi.advanceTimersByTimeAsync(3000); + await vi.advanceTimersByTimeAsync(50); }); - // Second check should have been called - expect(window.maestro.wakatime.checkCli).toHaveBeenCalledTimes(2); + expect(screen.getByText('Encore Features', { selector: 'h3' })).toBeInTheDocument(); }); - it('should not retry CLI check when first check returns available', async () => { - mockUseSettingsOverrides = { wakatimeEnabled: true }; - vi.mocked(window.maestro.wakatime.checkCli).mockResolvedValue({ - available: true, - version: '1.0.0', - }); - + it('should show description text for Encore Features', async () => { render(); await act(async () => { - await vi.advanceTimersByTimeAsync(100); + await vi.advanceTimersByTimeAsync(50); }); - expect(window.maestro.wakatime.checkCli).toHaveBeenCalledTimes(1); + fireEvent.click(screen.getByTitle('Encore Features')); - // Advance past retry timeout await act(async () => { - await vi.advanceTimersByTimeAsync(3000); + await vi.advanceTimersByTimeAsync(50); }); - // Should still be 1 โ€” no retry needed - expect(window.maestro.wakatime.checkCli).toHaveBeenCalledTimes(1); + expect( + screen.getByText(/Optional features that extend Maestro's capabilities/) + ).toBeInTheDocument(); + expect( + screen.getByText(/Contributors building new features should consider gating them here/) + ).toBeInTheDocument(); }); - it('should not show auto-install message when CLI is available', async () => { - mockUseSettingsOverrides = { wakatimeEnabled: true }; - vi.mocked(window.maestro.wakatime.checkCli).mockResolvedValue({ - available: true, - version: '1.0.0', + it("should show Director's Notes feature toggle defaulting to off", async () => { + render(); + + await act(async () => { + await vi.advanceTimersByTimeAsync(50); }); - render(); + fireEvent.click(screen.getByTitle('Encore Features')); await act(async () => { - await vi.advanceTimersByTimeAsync(100); + await vi.advanceTimersByTimeAsync(50); }); - expect( - screen.queryByText('WakaTime CLI is being installed automatically...') - ).not.toBeInTheDocument(); + // Director's Notes section is visible but DN settings are hidden + expect(screen.getByText("Director's Notes")).toBeInTheDocument(); + expect(screen.queryByText('Synopsis Provider')).not.toBeInTheDocument(); }); - it('should retry on error and update status after retry succeeds', async () => { - mockUseSettingsOverrides = { wakatimeEnabled: true }; - vi.mocked(window.maestro.wakatime.checkCli) - .mockRejectedValueOnce(new Error('Network error')) - .mockResolvedValueOnce({ available: true, version: '1.0.0' }); + it("should call setEncoreFeatures when Director's Notes toggle is clicked", async () => { + mockSetEncoreFeatures.mockClear(); render(); await act(async () => { - await vi.advanceTimersByTimeAsync(100); + await vi.advanceTimersByTimeAsync(50); }); - // Should show the auto-install message after error - expect( - screen.getByText('WakaTime CLI is being installed automatically...') - ).toBeInTheDocument(); + fireEvent.click(screen.getByTitle('Encore Features')); - // Advance to trigger retry await act(async () => { - await vi.advanceTimersByTimeAsync(3000); + await vi.advanceTimersByTimeAsync(50); }); - // After retry succeeds, message should disappear - expect( - screen.queryByText('WakaTime CLI is being installed automatically...') - ).not.toBeInTheDocument(); + // Click the Director's Notes feature section to toggle + const dnSection = screen.getByText("Director's Notes").closest('button'); + expect(dnSection).toBeInTheDocument(); + fireEvent.click(dnSection!); + + expect(mockSetEncoreFeatures).toHaveBeenCalledWith({ + directorNotes: true, + usageStats: true, + symphony: true, + }); }); - }); - describe('Shell selection with mouseEnter and focus', () => { - it('should load shells on mouseEnter', async () => { + it('should call setEncoreFeatures with false when toggling DN off', async () => { + mockSetEncoreFeatures.mockClear(); + mockUseSettingsOverrides = { encoreFeatures: { directorNotes: true, usageStats: true, symphony: true } }; render(); await act(async () => { - await vi.advanceTimersByTimeAsync(100); + await vi.advanceTimersByTimeAsync(50); }); - // Trigger shell loading via mouseEnter - const detectButton = screen.getByText('Detect other available shells...'); - - // Load shells first - fireEvent.click(detectButton); + fireEvent.click(screen.getByTitle('Encore Features')); await act(async () => { - await vi.advanceTimersByTimeAsync(100); + await vi.advanceTimersByTimeAsync(50); }); - // Now shells should be loaded, find a shell button - const zshButton = screen.getByText('Zsh').closest('button'); - expect(zshButton).toBeInTheDocument(); + const dnSection = screen.getByText("Director's Notes").closest('button'); + expect(dnSection).toBeInTheDocument(); + fireEvent.click(dnSection!); - // Trigger mouseEnter - should not reload (already loaded) - fireEvent.mouseEnter(zshButton!); + expect(mockSetEncoreFeatures).toHaveBeenCalledWith({ + directorNotes: false, + usageStats: true, + symphony: true, + }); + }); + + it('should show Usage & Stats feature toggle defaulting to on', async () => { + render(); await act(async () => { await vi.advanceTimersByTimeAsync(50); }); - // shells.detect should only have been called once - expect(window.maestro.shells.detect).toHaveBeenCalledTimes(1); - }); - }); - - describe('Encore Features settings tab', () => { - it('should render Encore Features tab button', async () => { - render(); + fireEvent.click(screen.getByTitle('Encore Features')); await act(async () => { await vi.advanceTimersByTimeAsync(50); }); - expect(screen.getByTitle('Encore Features')).toBeInTheDocument(); + expect(screen.getByText('Usage & Stats')).toBeInTheDocument(); + // Settings should be visible when enabled (default on) + expect(screen.getByText('Enable stats collection')).toBeInTheDocument(); }); - it('should switch to Encore Features tab when clicked', async () => { + it('should call setEncoreFeatures when Usage & Stats toggle is clicked off', async () => { + mockSetEncoreFeatures.mockClear(); + render(); await act(async () => { await vi.advanceTimersByTimeAsync(50); }); - const tab = screen.getByTitle('Encore Features'); - fireEvent.click(tab); + fireEvent.click(screen.getByTitle('Encore Features')); await act(async () => { await vi.advanceTimersByTimeAsync(50); }); - expect(screen.getByText('Encore Features', { selector: 'h3' })).toBeInTheDocument(); + const usSection = screen.getByText('Usage & Stats').closest('button'); + expect(usSection).toBeInTheDocument(); + fireEvent.click(usSection!); + + expect(mockSetEncoreFeatures).toHaveBeenCalledWith({ + directorNotes: false, + usageStats: false, + symphony: true, + }); }); - it('should show description text for Encore Features', async () => { + it('should show Maestro Symphony feature toggle defaulting to on', async () => { render(); await act(async () => { @@ -2285,15 +2287,14 @@ describe('SettingsModal', () => { await vi.advanceTimersByTimeAsync(50); }); - expect( - screen.getByText(/Optional features that extend Maestro's capabilities/) - ).toBeInTheDocument(); - expect( - screen.getByText(/Contributors building new features should consider gating them here/) - ).toBeInTheDocument(); + expect(screen.getByText('Maestro Symphony')).toBeInTheDocument(); + // Settings should be visible when enabled (default on) + expect(screen.getByText('Registry Sources')).toBeInTheDocument(); }); - it("should show Director's Notes feature toggle defaulting to off", async () => { + it('should call setEncoreFeatures when Symphony toggle is clicked off', async () => { + mockSetEncoreFeatures.mockClear(); + render(); await act(async () => { @@ -2306,13 +2307,20 @@ describe('SettingsModal', () => { await vi.advanceTimersByTimeAsync(50); }); - // Director's Notes section is visible but DN settings are hidden - expect(screen.getByText("Director's Notes")).toBeInTheDocument(); - expect(screen.queryByText('Synopsis Provider')).not.toBeInTheDocument(); + const symphonySection = screen.getByText('Maestro Symphony').closest('button'); + expect(symphonySection).toBeInTheDocument(); + fireEvent.click(symphonySection!); + + expect(mockSetEncoreFeatures).toHaveBeenCalledWith({ + directorNotes: false, + usageStats: true, + symphony: false, + }); }); - it("should call setEncoreFeatures when Director's Notes toggle is clicked", async () => { + it('should call setEncoreFeatures when Symphony toggle is clicked on', async () => { mockSetEncoreFeatures.mockClear(); + mockUseSettingsOverrides = { encoreFeatures: { directorNotes: false, usageStats: true, symphony: false } }; render(); @@ -2326,19 +2334,20 @@ describe('SettingsModal', () => { await vi.advanceTimersByTimeAsync(50); }); - // Click the Director's Notes feature section to toggle - const dnSection = screen.getByText("Director's Notes").closest('button'); - expect(dnSection).toBeInTheDocument(); - fireEvent.click(dnSection!); + const symphonySection = screen.getByText('Maestro Symphony').closest('button'); + expect(symphonySection).toBeInTheDocument(); + fireEvent.click(symphonySection!); expect(mockSetEncoreFeatures).toHaveBeenCalledWith({ - directorNotes: true, + directorNotes: false, + usageStats: true, + symphony: true, }); }); - it('should call setEncoreFeatures with false when toggling DN off', async () => { - mockSetEncoreFeatures.mockClear(); - mockUseSettingsOverrides = { encoreFeatures: { directorNotes: true } }; + it('should hide Symphony registry settings when symphony is disabled', async () => { + mockUseSettingsOverrides = { encoreFeatures: { directorNotes: false, usageStats: true, symphony: false } }; + render(); await act(async () => { @@ -2351,18 +2360,13 @@ describe('SettingsModal', () => { await vi.advanceTimersByTimeAsync(50); }); - const dnSection = screen.getByText("Director's Notes").closest('button'); - expect(dnSection).toBeInTheDocument(); - fireEvent.click(dnSection!); - - expect(mockSetEncoreFeatures).toHaveBeenCalledWith({ - directorNotes: false, - }); + expect(screen.getByText('Maestro Symphony')).toBeInTheDocument(); + expect(screen.queryByText('Registry Sources')).not.toBeInTheDocument(); }); describe("with Director's Notes enabled", () => { beforeEach(() => { - mockUseSettingsOverrides = { encoreFeatures: { directorNotes: true } }; + mockUseSettingsOverrides = { encoreFeatures: { directorNotes: true, usageStats: true, symphony: true } }; }); it('should render provider dropdown with detected available agents', async () => { diff --git a/src/main/index.ts b/src/main/index.ts index 0afff4436..d74f8dc58 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -670,6 +670,7 @@ function setupIpcHandlers() { app, getMainWindow: () => mainWindow, sessionsStore, + settingsStore: store, }); // Register tab naming handlers for automatic tab naming diff --git a/src/main/ipc/handlers/index.ts b/src/main/ipc/handlers/index.ts index ba41c326b..dac6b3ae2 100644 --- a/src/main/ipc/handlers/index.ts +++ b/src/main/ipc/handlers/index.ts @@ -265,6 +265,7 @@ export function registerAllHandlers(deps: HandlerDependencies): void { app: deps.app, getMainWindow: deps.getMainWindow, sessionsStore: deps.sessionsStore, + settingsStore: deps.settingsStore, }); // Register agent error handlers (error state management) registerAgentErrorHandlers(); diff --git a/src/main/ipc/handlers/symphony.ts b/src/main/ipc/handlers/symphony.ts index 195754594..e2e2652d7 100644 --- a/src/main/ipc/handlers/symphony.ts +++ b/src/main/ipc/handlers/symphony.ts @@ -200,6 +200,7 @@ export interface SymphonyHandlerDependencies { app: App; getMainWindow: () => BrowserWindow | null; sessionsStore: Store; + settingsStore: Store; } // ============================================================================ @@ -381,37 +382,82 @@ function parseDocumentPaths(body: string): DocumentReference[] { // ============================================================================ /** - * Fetch the symphony registry from GitHub. + * Fetch a single symphony registry from a URL. + * Returns null on failure instead of throwing (isolated error handling per URL). */ -async function fetchRegistry(): Promise { - logger.info('Fetching Symphony registry', LOG_CONTEXT); - +/** + * Redact a URL for safe logging โ€” strips credentials, query params, and fragments. + */ +function redactUrlForLog(rawUrl: string): string { try { - const response = await fetch(SYMPHONY_REGISTRY_URL); + const parsed = new URL(rawUrl); + parsed.username = ''; + parsed.password = ''; + parsed.search = ''; + parsed.hash = ''; + return parsed.toString(); + } catch { + return '[invalid-url]'; + } +} +async function fetchSingleRegistry(url: string): Promise { + const safeUrl = redactUrlForLog(url); + try { + const response = await fetch(url); if (!response.ok) { - throw new SymphonyError( - `Failed to fetch registry: ${response.status} ${response.statusText}`, - 'network' - ); + logger.warn(`Failed to fetch registry from ${safeUrl}: ${response.status}`, LOG_CONTEXT); + return null; } - const data = (await response.json()) as SymphonyRegistry; - if (!data.repositories || !Array.isArray(data.repositories)) { - throw new SymphonyError('Invalid registry structure', 'parse'); + logger.warn(`Invalid registry structure from ${safeUrl}`, LOG_CONTEXT); + return null; } - - logger.info(`Fetched registry with ${data.repositories.length} repos`, LOG_CONTEXT); + logger.info(`Fetched ${data.repositories.length} repos from ${safeUrl}`, LOG_CONTEXT); return data; } catch (error) { - if (error instanceof SymphonyError) throw error; - throw new SymphonyError( - `Network error: ${error instanceof Error ? error.message : String(error)}`, - 'network', - error - ); + logger.warn(`Network error fetching registry from ${safeUrl}: ${error instanceof Error ? error.message : String(error)}`, LOG_CONTEXT); + return null; + } +} + +/** + * Fetch and merge symphony registries from all configured URLs. + * Default URL always fetched first (wins on slug conflicts). + * Custom URL failures are isolated โ€” other registries still load. + */ +async function fetchRegistries(customUrls: string[]): Promise { + logger.info(`Fetching Symphony registries (1 default + ${customUrls.length} custom)`, LOG_CONTEXT); + + const allUrls = [SYMPHONY_REGISTRY_URL, ...customUrls]; + const results = await Promise.allSettled(allUrls.map(fetchSingleRegistry)); + + const seenSlugs = new Set(); + const mergedRepos: SymphonyRegistry['repositories'] = []; + + for (const result of results) { + if (result.status === 'fulfilled' && result.value) { + for (const repo of result.value.repositories) { + if (!seenSlugs.has(repo.slug)) { + seenSlugs.add(repo.slug); + mergedRepos.push(repo); + } + } + } + } + + if (mergedRepos.length === 0) { + throw new SymphonyError('Failed to fetch registry from all configured URLs', 'network'); } + + logger.info(`Merged registry: ${mergedRepos.length} repos from ${allUrls.length} sources`, LOG_CONTEXT); + + return { + schemaVersion: '1.0', + lastUpdated: new Date().toISOString(), + repositories: mergedRepos, + }; } /** @@ -1014,6 +1060,7 @@ export function registerSymphonyHandlers({ app, getMainWindow, sessionsStore, + settingsStore, }: SymphonyHandlerDependencies): void { // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // Registry Operations @@ -1088,9 +1135,20 @@ export function registerSymphonyHandlers({ async (forceRefresh?: boolean): Promise> => { const cache = await readCache(app); + // Runtime-validate custom URLs from settings + const rawCustomUrls = settingsStore.get('symphonyRegistryUrls'); + const customUrls = Array.isArray(rawCustomUrls) + ? rawCustomUrls.filter((u): u is string => typeof u === 'string' && u.trim().length > 0) + : []; + + // Skip cache when custom sources are configured โ€” cache doesn't track + // which source URLs produced it, so URL changes would serve stale data. + const hasCustomSources = customUrls.length > 0; + // Check cache validity if ( !forceRefresh && + !hasCustomSources && cache?.registry && isCacheValid(cache.registry.fetchedAt, REGISTRY_CACHE_TTL_MS) ) { @@ -1102,9 +1160,9 @@ export function registerSymphonyHandlers({ }; } - // Fetch fresh data + // Fetch fresh data from all configured registries try { - const registry = await fetchRegistry(); + const registry = await fetchRegistries(customUrls); const enriched = await enrichWithStars(registry, cache, !!forceRefresh); // Update cache (enriched registry includes stars on repo objects, diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 667edde5c..1e36dca0a 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -415,6 +415,15 @@ function MaestroConsoleInner() { encoreFeatures, } = settings; + // Reset modal-open flags when their Encore Feature toggle is disabled + useEffect(() => { + if (!encoreFeatures.symphony) setSymphonyModalOpen(false); + }, [encoreFeatures.symphony, setSymphonyModalOpen]); + + useEffect(() => { + if (!encoreFeatures.usageStats) setUsageDashboardOpen(false); + }, [encoreFeatures.usageStats, setUsageDashboardOpen]); + // --- KEYBOARD SHORTCUT HELPERS --- const { isShortcut, isTabShortcut } = useKeyboardShortcutHelpers({ shortcuts, @@ -2551,7 +2560,7 @@ function MaestroConsoleInner() { setAboutModalOpen={setAboutModalOpen} setLogViewerOpen={setLogViewerOpen} setProcessMonitorOpen={setProcessMonitorOpen} - setUsageDashboardOpen={setUsageDashboardOpen} + setUsageDashboardOpen={encoreFeatures.usageStats ? setUsageDashboardOpen : undefined} setActiveRightTab={setActiveRightTab} setAgentSessionsOpen={setAgentSessionsOpen} setActiveAgentSessionId={setActiveAgentSessionId} @@ -2626,7 +2635,7 @@ function MaestroConsoleInner() { getDocumentTaskCount={getDocumentTaskCount} onAutoRunRefresh={handleAutoRunRefresh} onOpenMarketplace={handleOpenMarketplace} - onOpenSymphony={() => setSymphonyModalOpen(true)} + onOpenSymphony={encoreFeatures.symphony ? () => setSymphonyModalOpen(true) : undefined} onOpenDirectorNotes={ encoreFeatures.directorNotes ? () => setDirectorNotesOpen(true) : undefined } @@ -2790,7 +2799,7 @@ function MaestroConsoleInner() { )} {/* --- SYMPHONY MODAL (lazy-loaded) --- */} - {symphonyModalOpen && ( + {encoreFeatures.symphony && symphonyModalOpen && ( void; setLogViewerOpen: (open: boolean) => void; setProcessMonitorOpen: (open: boolean) => void; - setUsageDashboardOpen: (open: boolean) => void; + setUsageDashboardOpen?: (open: boolean) => void; setActiveRightTab: (tab: RightPanelTab) => void; setAgentSessionsOpen: (open: boolean) => void; setActiveAgentSessionId: (id: string | null) => void; @@ -1911,7 +1911,7 @@ export interface AppModalsProps { setAboutModalOpen: (open: boolean) => void; setLogViewerOpen: (open: boolean) => void; setProcessMonitorOpen: (open: boolean) => void; - setUsageDashboardOpen: (open: boolean) => void; + setUsageDashboardOpen?: (open: boolean) => void; setActiveRightTab: (tab: RightPanelTab) => void; setAgentSessionsOpen: (open: boolean) => void; setActiveAgentSessionId: (id: string | null) => void; diff --git a/src/renderer/components/QuickActionsModal.tsx b/src/renderer/components/QuickActionsModal.tsx index d80133411..c0c9983f7 100644 --- a/src/renderer/components/QuickActionsModal.tsx +++ b/src/renderer/components/QuickActionsModal.tsx @@ -51,7 +51,7 @@ interface QuickActionsModalProps { setAboutModalOpen: (open: boolean) => void; setLogViewerOpen: (open: boolean) => void; setProcessMonitorOpen: (open: boolean) => void; - setUsageDashboardOpen: (open: boolean) => void; + setUsageDashboardOpen?: (open: boolean) => void; setAgentSessionsOpen: (open: boolean) => void; setActiveAgentSessionId: (id: string | null) => void; setGitDiffPreview: (diff: string | null) => void; @@ -695,15 +695,19 @@ export const QuickActionsModal = memo(function QuickActionsModal(props: QuickAct setQuickActionOpen(false); }, }, - { - id: 'usageDashboard', - label: 'Usage Dashboard', - shortcut: shortcuts.usageDashboard, - action: () => { - setUsageDashboardOpen(true); - setQuickActionOpen(false); - }, - }, + ...(setUsageDashboardOpen + ? [ + { + id: 'usageDashboard', + label: 'Usage Dashboard', + shortcut: shortcuts.usageDashboard, + action: () => { + setUsageDashboardOpen(true); + setQuickActionOpen(false); + }, + }, + ] + : []), ...(activeSession && hasActiveSessionCapability?.('supportsSessionStorage') ? [ { diff --git a/src/renderer/components/SessionList/HamburgerMenuContent.tsx b/src/renderer/components/SessionList/HamburgerMenuContent.tsx index 104509ea6..c47a71f5e 100644 --- a/src/renderer/components/SessionList/HamburgerMenuContent.tsx +++ b/src/renderer/components/SessionList/HamburgerMenuContent.tsx @@ -36,7 +36,7 @@ export function HamburgerMenuContent({ setMenuOpen, }: HamburgerMenuContentProps) { const shortcuts = useSettingsStore((s) => s.shortcuts); - const directorNotesEnabled = useSettingsStore((s) => s.encoreFeatures.directorNotes); + const encoreFeatures = useSettingsStore((s) => s.encoreFeatures); const { setShortcutsHelpOpen, setSettingsModalOpen, @@ -239,53 +239,57 @@ export function HamburgerMenuContent({ {formatShortcutKeys(shortcuts.processMonitor.keys)} - - + )} + {encoreFeatures.symphony && ( + - {directorNotesEnabled && ( + +
+
+ Maestro Symphony +
+
+ Contribute to open source +
+
+ + {shortcuts.openSymphony ? formatShortcutKeys(shortcuts.openSymphony.keys) : 'โ‡งโŒ˜Y'} + + + )} + {encoreFeatures.directorNotes && (
+ {/* Usage & Stats Feature Section */} +
+ + + {encoreFeatures.usageStats && ( +
+ {/* Enable/Disable Stats Collection */} +
+
+

+ Enable stats collection +

+

+ Track queries and Auto Run sessions for the dashboard. +

+
+ +
+ + {/* Default Time Range */} +
+
Default dashboard time range
+ +

+ Time range shown when opening the Usage Dashboard. +

+
+ + {/* Divider */} +
+ + {/* Database Size Display */} +
+ + Database size + + + {statsDbSize !== null + ? (statsDbSize / 1024 / 1024).toFixed(2) + ' MB' + : 'Loading...'} + {statsEarliestDate && ( + (since {statsEarliestDate}) + )} + +
+ + {/* Clear Old Data Dropdown */} +
+
Clear stats older than...
+
+ + +
+

+ Remove old query events, Auto Run sessions, and tasks from the stats database. +

+
+ + {/* Clear Result Feedback */} + {statsClearResult && ( +
+ {statsClearResult.success ? ( + <> + + + Cleared{' '} + {statsClearResult.deletedQueryEvents + + statsClearResult.deletedAutoRunSessions + + statsClearResult.deletedAutoRunTasks}{' '} + records ({statsClearResult.deletedQueryEvents} queries,{' '} + {statsClearResult.deletedAutoRunSessions} sessions,{' '} + {statsClearResult.deletedAutoRunTasks} tasks) + + + ) : ( + <> + + {statsClearResult.error || 'Failed to clear stats data'} + + )} +
+ )} + + {/* Divider */} +
+ + {/* WakaTime Integration */} +
+
+

+ + Enable WakaTime tracking +

+

+ Track coding activity in Maestro sessions via WakaTime. +

+
+ +
+ + {/* CLI not found warning */} + {wakatimeEnabled && wakatimeCliStatus && !wakatimeCliStatus.available && ( +

+ WakaTime CLI is being installed automatically... +

+ )} + + {/* Detailed file tracking toggle (only shown when enabled) */} + {wakatimeEnabled && ( +
+
+

+ Detailed file tracking +

+

+ Track per-file write activity. Sends file paths (not content) to WakaTime. +

+
+ +
+ )} + + {/* API Key Input (only shown when enabled) */} + {wakatimeEnabled && ( +
+
API Key
+
+ + handleWakatimeApiKeyChange(e.target.value)} + onBlur={() => { + if (wakatimeApiKey) { + setWakatimeKeyValidating(true); + setWakatimeKeyValid(null); + window.maestro.wakatime + .validateApiKey(wakatimeApiKey) + .then((result) => setWakatimeKeyValid(result.valid)) + .catch(() => setWakatimeKeyValid(false)) + .finally(() => setWakatimeKeyValidating(false)); + } + }} + className="bg-transparent flex-1 text-sm outline-none" + style={{ color: theme.colors.textMain }} + placeholder="waka_..." + /> + {wakatimeKeyValidating && ...} + {!wakatimeKeyValidating && wakatimeKeyValid === true && ( + + )} + {!wakatimeKeyValidating && wakatimeKeyValid === false && wakatimeApiKey && ( + + )} + {wakatimeApiKey && ( + + )} +
+

+ Get your API key from wakatime.com/settings/api-key. Keys are stored locally in + ~/.maestro/settings.json. +

+
+ )} +
+ )} +
+ + {/* Maestro Symphony Feature Section */} +
+ + + {/* Registry URL Management (shown when enabled) */} + {encoreFeatures.symphony && ( +
+
+ +

+ Repositories are loaded from all configured registry URLs. The default registry + cannot be removed. +

+ + {/* Default URL (immutable) */} +
+ + + {SYMPHONY_REGISTRY_URL} + + + default + +
+ + {/* Custom URLs list */} + {symphonyRegistryUrls.map((url: string) => ( +
+ + {url} + + +
+ ))} + + {/* Add new URL input */} +
+
+ { + setNewRegistryUrl(e.target.value); + setRegistryUrlError(null); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAddRegistryUrl(); + } + }} + placeholder="https://example.com/registry.json" + className="w-full px-3 py-2 rounded text-sm font-mono outline-none" + style={{ + backgroundColor: theme.colors.bgActivity, + borderColor: registryUrlError ? theme.colors.error : theme.colors.border, + border: '1px solid', + color: theme.colors.textMain, + }} + /> + {registryUrlError && ( +

+ {registryUrlError} +

+ )} +
+ +
+
+
+ )} +
+ {/* Director's Notes Feature Section */}
(null); const [syncMigratedCount, setSyncMigratedCount] = useState(null); - // Stats data management state - const [statsDbSize, setStatsDbSize] = useState(null); - const [statsEarliestDate, setStatsEarliestDate] = useState(null); - const [statsClearing, setStatsClearing] = useState(false); - const [statsClearResult, setStatsClearResult] = useState<{ - success: boolean; - deletedQueryEvents: number; - deletedAutoRunSessions: number; - deletedAutoRunTasks: number; - error?: string; - } | null>(null); - // WakaTime CLI check and API key validation state - const [wakatimeCliStatus, setWakatimeCliStatus] = useState<{ - available: boolean; - version?: string; - } | null>(null); - const [wakatimeKeyValid, setWakatimeKeyValid] = useState(null); - const [wakatimeKeyValidating, setWakatimeKeyValidating] = useState(false); - const handleWakatimeApiKeyChange = useCallback( - (value: string) => { - setWakatimeApiKey(value); - setWakatimeKeyValid(null); - }, - [setWakatimeApiKey] - ); - - // Check WakaTime CLI availability when section renders or toggle is enabled - useEffect(() => { - if (!isOpen || !wakatimeEnabled) return; - let cancelled = false; - let retryTimer: ReturnType | null = null; - - window.maestro.wakatime - .checkCli() - .then((status) => { - if (cancelled) return; - setWakatimeCliStatus(status); - if (!status.available) { - retryTimer = setTimeout(() => { - if (!cancelled) { - window.maestro.wakatime - .checkCli() - .then((retryStatus) => { - if (!cancelled) setWakatimeCliStatus(retryStatus); - }) - .catch(() => { - if (!cancelled) setWakatimeCliStatus({ available: false }); - }); - } - }, 3000); - } - }) - .catch(() => { - if (cancelled) return; - setWakatimeCliStatus({ available: false }); - retryTimer = setTimeout(() => { - if (!cancelled) { - window.maestro.wakatime - .checkCli() - .then((retryStatus) => { - if (!cancelled) setWakatimeCliStatus(retryStatus); - }) - .catch(() => { - if (!cancelled) setWakatimeCliStatus({ available: false }); - }); - } - }, 3000); - }); - - return () => { - cancelled = true; - if (retryTimer) clearTimeout(retryTimer); - }; - }, [isOpen, wakatimeEnabled]); - - // Load sync settings and stats data when modal opens + // Load sync settings when modal opens useEffect(() => { if (!isOpen) return; @@ -224,33 +133,6 @@ export function GeneralTab({ theme, isOpen }: GeneralTabProps) { setSyncError('Failed to load storage settings'); }); - // Load stats database size and earliest timestamp - window.maestro.stats - .getDatabaseSize() - .then((size) => { - setStatsDbSize(size); - }) - .catch((err) => { - console.error('Failed to load stats database size:', err); - }); - - window.maestro.stats - .getEarliestTimestamp() - .then((timestamp) => { - if (timestamp) { - const date = new Date(timestamp); - const formatted = date.toISOString().split('T')[0]; // YYYY-MM-DD - setStatsEarliestDate(formatted); - } else { - setStatsEarliestDate(null); - } - }) - .catch((err) => { - console.error('Failed to load earliest stats timestamp:', err); - }); - - // Reset stats clear state - setStatsClearResult(null); }, [isOpen]); const loadShells = async () => { @@ -964,328 +846,6 @@ export function GeneralTab({ theme, isOpen }: GeneralTabProps) { theme={theme} /> - {/* Stats Data Management */} -
-
- - Usage & Stats - - Beta - -
-
- {/* Enable/Disable Stats Collection */} -
-
-

- Enable stats collection -

-

- Track queries and Auto Run sessions for the dashboard. -

-
- -
- - {/* Default Time Range */} -
-
Default dashboard time range
- -

- Time range shown when opening the Usage Dashboard. -

-
- - {/* Divider */} -
- - {/* Database Size Display */} -
- - Database size - - - {statsDbSize !== null ? (statsDbSize / 1024 / 1024).toFixed(2) + ' MB' : 'Loading...'} - {statsEarliestDate && ( - (since {statsEarliestDate}) - )} - -
- - {/* Clear Old Data Dropdown */} -
-
Clear stats older than...
-
- - -
-

- Remove old query events, Auto Run sessions, and tasks from the stats database. -

-
- - {/* Clear Result Feedback */} - {statsClearResult && ( -
- {statsClearResult.success ? ( - <> - - - Cleared{' '} - {statsClearResult.deletedQueryEvents + - statsClearResult.deletedAutoRunSessions + - statsClearResult.deletedAutoRunTasks}{' '} - records ({statsClearResult.deletedQueryEvents} queries,{' '} - {statsClearResult.deletedAutoRunSessions} sessions,{' '} - {statsClearResult.deletedAutoRunTasks} tasks) - - - ) : ( - <> - - {statsClearResult.error || 'Failed to clear stats data'} - - )} -
- )} - - {/* Divider */} -
- - {/* WakaTime Integration */} -
-
-

- - Enable WakaTime tracking -

-

- Track coding activity in Maestro sessions via WakaTime. -

-
- -
- - {/* CLI not found warning */} - {wakatimeEnabled && wakatimeCliStatus && !wakatimeCliStatus.available && ( -

- WakaTime CLI is being installed automatically... -

- )} - - {/* Detailed file tracking toggle (only shown when enabled) */} - {wakatimeEnabled && ( -
-
-

- Detailed file tracking -

-

- Track per-file write activity. Sends file paths (not content) to WakaTime. -

-
- -
- )} - - {/* API Key Input (only shown when enabled) */} - {wakatimeEnabled && ( -
-
API Key
-
- - handleWakatimeApiKeyChange(e.target.value)} - onBlur={() => { - if (wakatimeApiKey) { - setWakatimeKeyValidating(true); - setWakatimeKeyValid(null); - window.maestro.wakatime - .validateApiKey(wakatimeApiKey) - .then((result) => setWakatimeKeyValid(result.valid)) - .catch(() => setWakatimeKeyValid(false)) - .finally(() => setWakatimeKeyValidating(false)); - } - }} - className="bg-transparent flex-1 text-sm outline-none" - style={{ color: theme.colors.textMain }} - placeholder="waka_..." - /> - {wakatimeKeyValidating && ...} - {!wakatimeKeyValidating && wakatimeKeyValid === true && ( - - )} - {!wakatimeKeyValidating && wakatimeKeyValid === false && wakatimeApiKey && ( - - )} - {wakatimeApiKey && ( - - )} -
-

- Get your API key from wakatime.com/settings/api-key. Keys are stored locally in - ~/.maestro/settings.json. -

-
- )} -
-
{/* Settings Storage Location */}
diff --git a/src/renderer/components/UsageDashboard/UsageDashboardModal.tsx b/src/renderer/components/UsageDashboard/UsageDashboardModal.tsx index 842aa51e3..cfb98d448 100644 --- a/src/renderer/components/UsageDashboard/UsageDashboardModal.tsx +++ b/src/renderer/components/UsageDashboard/UsageDashboardModal.tsx @@ -533,12 +533,6 @@ export function UsageDashboardModal({

Usage Dashboard

- - Beta - {/* New Data Indicator - appears briefly when real-time data arrives */} {showNewDataIndicator && (
void; + // Symphony registry URLs (additional user-configured registries) + symphonyRegistryUrls: string[]; + setSymphonyRegistryUrls: (value: string[]) => void; + // Director's Notes settings directorNotesSettings: DirectorNotesSettings; setDirectorNotesSettings: (value: DirectorNotesSettings) => void; diff --git a/src/renderer/stores/settingsStore.ts b/src/renderer/stores/settingsStore.ts index 0400cfb00..13b7e52ed 100644 --- a/src/renderer/stores/settingsStore.ts +++ b/src/renderer/stores/settingsStore.ts @@ -116,6 +116,8 @@ export const DEFAULT_ONBOARDING_STATS: OnboardingStats = { export const DEFAULT_ENCORE_FEATURES: EncoreFeatureFlags = { directorNotes: false, + usageStats: true, + symphony: true, }; export const DEFAULT_DIRECTOR_NOTES_SETTINGS: DirectorNotesSettings = { @@ -245,6 +247,7 @@ export interface SettingsStoreState { autoScrollAiMode: boolean; userMessageAlignment: 'left' | 'right'; encoreFeatures: EncoreFeatureFlags; + symphonyRegistryUrls: string[]; directorNotesSettings: DirectorNotesSettings; wakatimeApiKey: string; wakatimeEnabled: boolean; @@ -316,6 +319,7 @@ export interface SettingsStoreActions { setAutoScrollAiMode: (value: boolean) => void; setUserMessageAlignment: (value: 'left' | 'right') => void; setEncoreFeatures: (value: EncoreFeatureFlags) => void; + setSymphonyRegistryUrls: (value: string[]) => void; setDirectorNotesSettings: (value: DirectorNotesSettings) => void; setWakatimeApiKey: (value: string) => void; setWakatimeEnabled: (value: boolean) => void; @@ -463,6 +467,7 @@ export const useSettingsStore = create()((set, get) => ({ autoScrollAiMode: false, userMessageAlignment: 'right', encoreFeatures: DEFAULT_ENCORE_FEATURES, + symphonyRegistryUrls: [], directorNotesSettings: DEFAULT_DIRECTOR_NOTES_SETTINGS, wakatimeApiKey: '', wakatimeEnabled: false, @@ -788,6 +793,11 @@ export const useSettingsStore = create()((set, get) => ({ window.maestro.settings.set('encoreFeatures', value); }, + setSymphonyRegistryUrls: (value) => { + set({ symphonyRegistryUrls: value }); + window.maestro.settings.set('symphonyRegistryUrls', value); + }, + setDirectorNotesSettings: (value) => { set({ directorNotesSettings: value }); window.maestro.settings.set('directorNotesSettings', value); @@ -1696,6 +1706,13 @@ export async function loadAllSettings(): Promise { }; } + // Symphony registry URLs (additional user-configured registries) + if (Array.isArray(allSettings['symphonyRegistryUrls'])) { + patch.symphonyRegistryUrls = (allSettings['symphonyRegistryUrls'] as unknown[]) + .filter((v): v is string => typeof v === 'string' && v.trim().length > 0) + .map((v) => v.trim()); + } + // Director's Notes settings (merge with defaults to preserve new fields) if (allSettings['directorNotesSettings'] !== undefined) { patch.directorNotesSettings = { @@ -1827,6 +1844,7 @@ export function getSettingsActions() { setSuppressWindowsWarning: state.setSuppressWindowsWarning, setAutoScrollAiMode: state.setAutoScrollAiMode, setEncoreFeatures: state.setEncoreFeatures, + setSymphonyRegistryUrls: state.setSymphonyRegistryUrls, setDirectorNotesSettings: state.setDirectorNotesSettings, setWakatimeApiKey: state.setWakatimeApiKey, setWakatimeEnabled: state.setWakatimeEnabled, diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts index 5ccc6bb36..cccdda3f0 100644 --- a/src/renderer/types/index.ts +++ b/src/renderer/types/index.ts @@ -907,6 +907,8 @@ export interface LeaderboardSubmitResponse { // Each key is a feature ID, value indicates whether it's enabled export interface EncoreFeatureFlags { directorNotes: boolean; + usageStats: boolean; + symphony: boolean; } // Director's Notes settings for synopsis generation