Merge pull request #31 from alvinunreal/b
Refactor desktop control center
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"metadata": {
|
||||
"version": "1.0.0",
|
||||
"last_run": "2026-05-19T17:03:36.901Z",
|
||||
"last_run": "2026-05-24T09:29:48.346Z",
|
||||
"root": "/Volumes/external/repos/pets",
|
||||
"include_patterns": [
|
||||
"apps/**/*.ts",
|
||||
|
|
@ -30,19 +30,19 @@
|
|||
"file_hashes": {
|
||||
"apps/desktop/contracts/catalog-fixture.contract.ts": "8bc625d00ea7cc7dd3c3c846d517ad0a",
|
||||
"apps/desktop/contracts/local-ipc-protocol.contract.ts": "76d45670b202ebd96cbee64d1e8e53ec",
|
||||
"apps/desktop/contracts/plugin-manifest.contract.ts": "4ac9fd1cb75926e3d844c1bdeeb4a730",
|
||||
"apps/desktop/package.json": "d18219b7abe65ddee9fb37147b999933",
|
||||
"apps/desktop/contracts/plugin-manifest.contract.ts": "2b0eaa64d1e02dce6b14cfee9c6d2b3d",
|
||||
"apps/desktop/package.json": "b83883566725e5c9a97fc9d7b5fd189a",
|
||||
"apps/desktop/src/agent-pet-controller.ts": "179491dc5e838b5cf042b4630380d1e5",
|
||||
"apps/desktop/src/agent-setup.ts": "ae02936221b2d934e28ee023fb93c19b",
|
||||
"apps/desktop/src/agent-setup.ts": "c2193dd06fa7686b4446ea7dc5686b92",
|
||||
"apps/desktop/src/app-state-core.ts": "9f4ea019ddb620bf8ad70dfe83f27496",
|
||||
"apps/desktop/src/app-state.ts": "8cb602e19fd651c75942f2e6755e271f",
|
||||
"apps/desktop/src/app-state.ts": "89a68328e992076d114b90de5b8fbe1b",
|
||||
"apps/desktop/src/assets.ts": "d59129388a45ae10b5319242fd7a67ee",
|
||||
"apps/desktop/src/built-in-pet.ts": "eff5b3375b2a9eccad401f6048c9c5f1",
|
||||
"apps/desktop/src/catalog-validation.ts": "6bd6974357781c0f972ef2b2da76eb12",
|
||||
"apps/desktop/src/catalog.ts": "37fe90b36fa2eee976c3617fe1ee5989",
|
||||
"apps/desktop/src/check-cursor-desktop.ts": "80b0d5c1913a47143970447153762617",
|
||||
"apps/desktop/src/check-opencode-desktop-setup.ts": "08b86952c137b2e917d296c0c4590e5c",
|
||||
"apps/desktop/src/check-packaging-contract.ts": "8fef67305ffde508139ec5f3fca4651e",
|
||||
"apps/desktop/src/check-packaging-contract.ts": "c5f74134e8e5e98623e2c69e71a79eec",
|
||||
"apps/desktop/src/claude-memory.ts": "37f0a29734fad5e2bb5bd97110e7ddfa",
|
||||
"apps/desktop/src/codex-pets-core.ts": "f20c529df55934b632ac8e02dec4df15",
|
||||
"apps/desktop/src/codex-pets.ts": "885f214e14db799953b1ccf13a264478",
|
||||
|
|
@ -52,37 +52,40 @@
|
|||
"apps/desktop/src/lifecycle.ts": "a346d3ebe789e6d434858481af0757d0",
|
||||
"apps/desktop/src/local-ipc-paths.ts": "d69f7e0c0a451576d8fe02932c977f6f",
|
||||
"apps/desktop/src/local-ipc-protocol.ts": "738dee33acf186895798ceb42637ef88",
|
||||
"apps/desktop/src/local-ipc.ts": "939ccc0c7579db7018d1b547e1f85c83",
|
||||
"apps/desktop/src/logger.ts": "029c9601130c7fdc14c3db7e9f0a5f0b",
|
||||
"apps/desktop/src/main.ts": "0566296a91112db4aba9125b1963cb7b",
|
||||
"apps/desktop/src/local-ipc.ts": "a8f25fd0b47d560782f2332e30acc115",
|
||||
"apps/desktop/src/logger.ts": "a92e8de9ede984afc378d2d29d5fce0e",
|
||||
"apps/desktop/src/main.ts": "5678745b8ea5c311ada5259545bb694e",
|
||||
"apps/desktop/src/pet-installation.ts": "79cdc67778448f2e3876d22194c1ada1",
|
||||
"apps/desktop/src/pet-paths.ts": "73d7313fe19254ea0dcf24ab2f0929b3",
|
||||
"apps/desktop/src/pet-window.ts": "5fafece7a49d2c43fa42d84f327c4a83",
|
||||
"apps/desktop/src/plugin-catalog-validation.ts": "cf2e46636198dbd8f858a4e7eef40200",
|
||||
"apps/desktop/src/plugin-catalog.ts": "d765406c83155f17426cb2137dca6797",
|
||||
"apps/desktop/src/plugin-config.ts": "1472f4bc50d7af72e129d069499eb3bc",
|
||||
"apps/desktop/src/plugin-local-loader.ts": "3b4098baa8829312d81105ce4da948ba",
|
||||
"apps/desktop/src/plugin-catalog-validation.ts": "507deea55077a1130f9d4623a8fe8342",
|
||||
"apps/desktop/src/plugin-catalog.ts": "10cffa1505a621f2e4ec4d4e1379823c",
|
||||
"apps/desktop/src/plugin-config.ts": "4f1f0b6a83d6f705c7ef7b98b2412677",
|
||||
"apps/desktop/src/plugin-js-host.ts": "c61f52c9395897cb35c430ccb5cf7211",
|
||||
"apps/desktop/src/plugin-local-loader.ts": "6c95a5b381acea3b12ff9a4d90ca96d9",
|
||||
"apps/desktop/src/plugin-manifest-reader.ts": "2ba5d272ed3ae1b7e0131d2efdaf4caf",
|
||||
"apps/desktop/src/plugin-manifest.ts": "a6d0a95f946c31a68f32c870498b350e",
|
||||
"apps/desktop/src/plugin-package.ts": "069583f455f7c94144eb1385bc1dfe3e",
|
||||
"apps/desktop/src/plugin-manifest.ts": "15fe4e34408053cebe8b1b42e84cc6e7",
|
||||
"apps/desktop/src/plugin-package.ts": "2aa09526568e4fd4f961d883ef373a38",
|
||||
"apps/desktop/src/plugin-pet-api.ts": "b6d35ff512c9954462d82bf84dc50929",
|
||||
"apps/desktop/src/plugin-runtime.ts": "fad660f5dcf02bd359e4d3220be82e36",
|
||||
"apps/desktop/src/plugin-service.ts": "2aaa02e8860848ff8737b5e290dc5cc9",
|
||||
"apps/desktop/src/plugin-state.ts": "903457ed258ac93b636ebb903956a8f0",
|
||||
"apps/desktop/src/plugins-window.ts": "3a3dac2b433474eaf2c4928e6ad88a43",
|
||||
"apps/desktop/src/plugin-runtime.ts": "5065842b784ca91207bf8e6ce3d1fab5",
|
||||
"apps/desktop/src/plugin-sdk-bridge.ts": "a925bdd85834f00474e058e2c711d097",
|
||||
"apps/desktop/src/plugin-service.ts": "7a6ecfc1f62a35e5c6d440e876dac788",
|
||||
"apps/desktop/src/plugin-state.ts": "0f4575eec3a682c1256ed9aebe4003fc",
|
||||
"apps/desktop/src/reaction-animation-mapping.ts": "4353bdbca4bbfc2a9f70520f84cd2688",
|
||||
"apps/desktop/src/reaction-messages.ts": "99ec6ea6bf33cc8cc05cc895394e906a",
|
||||
"apps/desktop/src/renderer/src/vite-env.d.ts": "0352474ba2918efe13895edbc3780d94",
|
||||
"apps/desktop/src/state.ts": "34ff332215f43ab6ee0808018cbd1751",
|
||||
"apps/desktop/src/tray.ts": "e7efb701d33a56a48f40d324285ee06b",
|
||||
"apps/desktop/src/tray.ts": "152d6564121ccd19f4a814b8245a671c",
|
||||
"apps/desktop/src/update-checker.ts": "5f835427f52203f31c391eca7ac0634c",
|
||||
"apps/desktop/src/update-version.ts": "b8777871b655a26a04c0d6a92481664c",
|
||||
"apps/desktop/src/windows.ts": "7cc05fce6a874422849cbcc9c45b2f78",
|
||||
"apps/desktop/src/windows.ts": "4fe40c9a7f481fbab56cabb2fe284416",
|
||||
"apps/desktop/src/zip-safety.ts": "6e28cf03514254df74eb11e86db28d95",
|
||||
"package.json": "f25b55039f4dd82b83e701c0a9f241b8",
|
||||
"packages/agent-events/package.json": "c614b1baaa5ccfafb746c021d5f7cad7",
|
||||
"apps/desktop/vite.config.ts": "a2bd54426b257c13382e1c31065e5e51",
|
||||
"package.json": "659e909fc918656a9a3966cc740d2c4b",
|
||||
"packages/agent-events/package.json": "878d4cfd96549a41d93e7664912b1e57",
|
||||
"packages/agent-events/src/check-agent-events.ts": "c187e50df456d0ce83577c60a57564b4",
|
||||
"packages/agent-events/src/index.ts": "4c13fcc2e753390dd76524604bcbf555",
|
||||
"packages/claude/package.json": "439f51161fb533a0a33dc999f1f38925",
|
||||
"packages/claude/package.json": "0fd8a845544140e1e0285ff19f0c036c",
|
||||
"packages/claude/src/check-claude-code.ts": "79d583dec9d233e4c7791be466cec238",
|
||||
"packages/claude/src/check-claude-hooks.ts": "ad66153c3640f194bc72e310e70c7a8c",
|
||||
"packages/claude/src/claude-code.ts": "556806d7877488197915e12868a8b5cc",
|
||||
|
|
@ -91,46 +94,46 @@
|
|||
"packages/claude/src/hook-settings.ts": "0f18ba910e0b9534c695171e03fc29e8",
|
||||
"packages/claude/src/hooks.ts": "ff9ec6c55a2a779cd4b435219e97ef91",
|
||||
"packages/claude/src/index.ts": "13c65ce7d312b8bd08230195e0375ea5",
|
||||
"packages/cli/package.json": "9562cd5c31dc699455958c57386d389e",
|
||||
"packages/cli/package.json": "d30d63ad97c8953458692ce4048db0bd",
|
||||
"packages/cli/src/check-cli-contract.ts": "65e6488c8164b988f8758c0830bb3cb3",
|
||||
"packages/cli/src/index.ts": "70a27cdb0f986375f1ef945b51b5849c",
|
||||
"packages/client/contracts/client-protocol.contract.ts": "8d0f2908bdfd27341368dbaa1e12daed",
|
||||
"packages/client/package.json": "8282c3c4791a9591826b55b8dc7cdca7",
|
||||
"packages/client/package.json": "6eb5a6e9f959f79809de6623bf0859ca",
|
||||
"packages/client/src/discovery.ts": "ef4cd1346a516138226ba3659f8f73cf",
|
||||
"packages/client/src/index.ts": "3ed10c5a40d0c592d68833b27298d88b",
|
||||
"packages/client/src/protocol.ts": "f53294f725838877c19163642714a5d5",
|
||||
"packages/client/src/smoke.ts": "a80c46288c29e092899fad48d7dc013d",
|
||||
"packages/cursor/package.json": "46dc8d2ef47cf13853d1b3ff21c00194",
|
||||
"packages/cursor/package.json": "2214ed073ae8b9e24aa825f370a9bc5d",
|
||||
"packages/cursor/src/check-cursor.ts": "0b799f60156357cd5132eace2456097b",
|
||||
"packages/cursor/src/cursor-mcp.ts": "9c97c6f60665b5117da3fec3eaf6355a",
|
||||
"packages/cursor/src/cursor-previews.ts": "f253c1be8dee11cc878a6e80944e87f4",
|
||||
"packages/cursor/src/cursor-rules.ts": "a05f35efb92e61e2493ca122bcf42aab",
|
||||
"packages/cursor/src/cursor-status.ts": "c0d0a4dc377998336cf034c82d5bf56a",
|
||||
"packages/cursor/src/index.ts": "324347811550c7206820993f68651cba",
|
||||
"packages/install-pet/package.json": "e7ea02be04b33e6eb6d162c354ae0a7b",
|
||||
"packages/install-pet/package.json": "171c0fdaa8b13597c67c8c06be7adc6d",
|
||||
"packages/install-pet/src/check-install-pet.ts": "de740e86393f3d7e4e30bf807c8e489c",
|
||||
"packages/install-pet/src/index.ts": "04eaea5b38365061ae310c22626e077e",
|
||||
"packages/mcp/package.json": "e94983978c83cd0452586dcfcba6bf1c",
|
||||
"packages/mcp/package.json": "220512b0569b05ea9ec17950df4eb429",
|
||||
"packages/mcp/src/args.ts": "6b84e85ab049f35181294be5ad3efb7a",
|
||||
"packages/mcp/src/check-mcp-contract.ts": "0029e9dc79f600482b4c3d1ff1a0e630",
|
||||
"packages/mcp/src/ensure-executable.ts": "fab391e226668986f799293dce3239f5",
|
||||
"packages/mcp/src/index.ts": "af3f4b9aa83e42551290b63586fa643b",
|
||||
"packages/mcp/src/server.ts": "f7edb6891b9a3baf7266772b03c28a5e",
|
||||
"packages/mcp/src/tools.ts": "57b4e96d956da0acd0c21275c4953a27",
|
||||
"packages/opencode/package.json": "826da5b70e48fb269ff6f0fdb4b2f6f3",
|
||||
"packages/opencode/src/check-opencode-foundation.ts": "d4561f5bf88414cafef76d26ec8fc374",
|
||||
"packages/opencode/package.json": "8704c5e7f5737e71e918b5df849ce002",
|
||||
"packages/opencode/src/check-opencode-foundation.ts": "519e9712de23f60f723de265ff6f52f6",
|
||||
"packages/opencode/src/check-opencode-plugin.ts": "63c0f3b3782cb7501c1c31960a35c7c1",
|
||||
"packages/opencode/src/index.ts": "4931cbee8e079a669de99c7eebb0c9ce",
|
||||
"packages/opencode/src/opencode-config.ts": "b36fc3f17182e616680c3c74d9a60994",
|
||||
"packages/opencode/src/opencode-global-setup.ts": "0ffef4e42ecd914086e32e74c2ceb599",
|
||||
"packages/opencode/src/opencode-plugin-runtime.ts": "13077dc79254691664c8e1398feec6d6",
|
||||
"packages/opencode/src/opencode-previews.ts": "938cdd7e69da050d1dc9e07951687dcf",
|
||||
"packages/opencode/src/opencode-previews.ts": "7a7af0e3f189b0989a180143b04ba8ec",
|
||||
"packages/opencode/src/opencode-project-setup.ts": "84b378ea0b22f9a99eced35edd5e584b",
|
||||
"packages/opencode/src/opencode-status.ts": "f4c9b5a1c236ea2a3b9bd1c60d04de2b",
|
||||
"packages/opencode/src/opencode-status.ts": "2640eed91327a35c57a7162ab6730ccc",
|
||||
"packages/opencode/src/plugin.ts": "8265624c6f4d87274928dde47405d893",
|
||||
"packages/pet-format/package.json": "2b9a2455b93be85ce69473687b31706d",
|
||||
"packages/pet-format/package.json": "de5f4dee65e316fd9968a07928ec6558",
|
||||
"packages/pet-format/src/index.ts": "6e94caf8463f6c6139f8f7364691f2e4",
|
||||
"packages/pi/package.json": "f38f3401a6afeff030685067e43501c3",
|
||||
"packages/pi/package.json": "e539f5df26ab1f7aabe73ef0f924ebb6",
|
||||
"packages/pi/src/check-pi-compat.ts": "e2f02e876e051e7f302f9ccf835953e3",
|
||||
"packages/pi/src/check-pi.ts": "47a4f7d28b07fffe130419975f1cc7a4",
|
||||
"packages/pi/src/extension.ts": "b69b2da9b0a6e5814821dea20d6bf762",
|
||||
|
|
@ -139,32 +142,34 @@
|
|||
"pnpm-workspace.yaml": "a01ad104a2079e9efc5cb7aed169a93f"
|
||||
},
|
||||
"folder_hashes": {
|
||||
".": "4f805be432bed1abe6f3b2a587467dba",
|
||||
"apps": "b66e403c629764bbd4dd12760b6e74ab",
|
||||
"apps/desktop": "b66e403c629764bbd4dd12760b6e74ab",
|
||||
"apps/desktop/contracts": "20452e51d177d12d265d06642d4c3742",
|
||||
"apps/desktop/src": "86ebc20c1d90ad40889579e65065f05b",
|
||||
"packages": "1c25af6ba0c1ec45af6b667d97e2c7a5",
|
||||
"packages/agent-events": "6237e2a2220091929e2a9622c3a16714",
|
||||
".": "95a8be0f54a3b04c5c47e8c4f4eedaba",
|
||||
"apps": "fc772196f99084757739f4ec7cd5c56e",
|
||||
"apps/desktop": "fc772196f99084757739f4ec7cd5c56e",
|
||||
"apps/desktop/contracts": "5287707550b8f45a90cf8d8bfe0a242b",
|
||||
"apps/desktop/src": "2e7ab76b12f0a66080299739e930f991",
|
||||
"apps/desktop/src/renderer": "cf339a80cf66de177922ec0b5aa1c6b3",
|
||||
"apps/desktop/src/renderer/src": "cf339a80cf66de177922ec0b5aa1c6b3",
|
||||
"packages": "f988173854d800bfe8150ceaec477b89",
|
||||
"packages/agent-events": "e36c79c9b0c15cda8b2094d5a444682d",
|
||||
"packages/agent-events/src": "37e55a8851036d8a3e571c8c984e733f",
|
||||
"packages/claude": "d2c8126176367877921ec86b44ff1b11",
|
||||
"packages/claude": "78d460759fc5fd436fd09a92933d1918",
|
||||
"packages/claude/src": "99464dfa6a8f3791f6103514f81cfae0",
|
||||
"packages/cli": "7b40b9441951b158053b267b2dda4de3",
|
||||
"packages/cli": "2a347b33d0d889393133099f98b52f5d",
|
||||
"packages/cli/src": "f896d8bd032894346d84ff2354556039",
|
||||
"packages/client": "65944320e0e308c468818e1447b5f9ab",
|
||||
"packages/client": "a2b8e2dc2549beeedff2f322feded942",
|
||||
"packages/client/contracts": "5228f030046512baba4f7270556b0fb7",
|
||||
"packages/client/src": "a036a1f04099db816b2cd6b552fda034",
|
||||
"packages/cursor": "65f3a67d966b2f268634a75fbc0d81c9",
|
||||
"packages/cursor": "bc481b08e9fbe5e0464e6aef98f8c2ef",
|
||||
"packages/cursor/src": "e6ff31505d96d16dcbea0afcb680d2a3",
|
||||
"packages/install-pet": "8f24d0e2f6098debca5c52f2771a02d8",
|
||||
"packages/install-pet": "1ead7cc70de4c772c4c230c446d5e4ad",
|
||||
"packages/install-pet/src": "5882ef53808f2a7d7152446d1d46e196",
|
||||
"packages/mcp": "5f1970322674ed1ee75905f9cd748b65",
|
||||
"packages/mcp": "786e8bc32d478dbaa34bc3d1831cc6ec",
|
||||
"packages/mcp/src": "e89d476284c4ec2814c11a064fcb210b",
|
||||
"packages/opencode": "55a7384eb97145708cd21d89b96a0e59",
|
||||
"packages/opencode/src": "e095ac6c38594178a0a02e73919c2a7d",
|
||||
"packages/pet-format": "ec95c6f633b051bb148e6eb480f657e2",
|
||||
"packages/opencode": "dd2382941a7bfe672b2f27dcbfa51e45",
|
||||
"packages/opencode/src": "ed024a6207b42efd076da44ab9587adb",
|
||||
"packages/pet-format": "ca85af1412e98214135e22bdcba5f406",
|
||||
"packages/pet-format/src": "e01c13d845950b7c825b8ecde0c12b80",
|
||||
"packages/pi": "c5cc86775e4b68234ff7b3453bf105d7",
|
||||
"packages/pi": "df51ca2497141871a94e3383d042f051",
|
||||
"packages/pi/src": "2a70d56d1b7ce40ed8a0416141834dd4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
AGENTS.md
|
|
@ -8,3 +8,16 @@ Before working on any task, read `codemap.md` to understand:
|
|||
- Data flow and integration points between modules
|
||||
|
||||
For deep work on a specific folder, also read that folder's `codemap.md`.
|
||||
|
||||
## Catalog Direction
|
||||
|
||||
Catalog v2 is legacy and exists only for old app versions/fallback compatibility.
|
||||
For new work, migrations, and Control Center UI, do not optimize for v2 behavior.
|
||||
Use catalog v3 (`thumbnail`, `spritesheet`, paginated pages, and search index) as the source of truth.
|
||||
|
||||
## Logging for Fast DX
|
||||
|
||||
When working on desktop UI, renderer, IPC, catalog, plugin, or pet-window behavior, add targeted logging as part of the implementation when it helps diagnose issues quickly.
|
||||
Prefer concise, scoped logs that capture data shape, selected IDs, load/error states, and boundary decisions.
|
||||
Route renderer diagnostics into the app log when possible so failures are visible in `openpets.log`, not only DevTools.
|
||||
Avoid noisy permanent logs, secrets, full payload dumps, or logging in tight animation/render loops.
|
||||
|
|
|
|||
201
DESIGN.md
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# OpenPets Control Center: Design System & Engineering Manual
|
||||
|
||||
This document outlines the visual direction, component patterns, and front-end engineering guidelines for the OpenPets Control Center. It serves as a practical implementation guide for migrating existing views (Settings, Plugins, Integrations, Onboarding) and building new pages using a cohesive, high-fidelity tactile design system.
|
||||
|
||||
---
|
||||
|
||||
## 1. Visual Direction & Heritage
|
||||
|
||||
The OpenPets Control Center blends the nostalgic, playful charm of late-90s/early-2000s desktop pet managers with a modern, glassmorphic utility interface.
|
||||
|
||||
- **The Core Tension**: Playful nostalgia meets crisp, modern desktop productivity.
|
||||
- **Atmospheric Depth**: Instead of flat, sterile surfaces, interfaces leverage rich color gradients, subtle mesh backdrops, structured panels, and tactile control elements.
|
||||
- **Physicality**: Interactive components feel physical. They have distinct borders, inset highlights, drop shadows, and visual feedback on click/press.
|
||||
|
||||
---
|
||||
|
||||
## 2. Backgrounds & Surface Treatments
|
||||
|
||||
Surfaces are layered to establish clear spatial hierarchy and keep the interface readable.
|
||||
|
||||
### Page Backdrop
|
||||
- **Formula**: A radial ambient glow on top of a subtle linear gradient.
|
||||
- **Implementation**:
|
||||
```css
|
||||
background: radial-gradient(circle at 12% 8%, rgba(219, 234, 254, 0.9), transparent 24%),
|
||||
linear-gradient(180deg, #f8fbff 0%, #eff7ff 54%, #e9f3ff 100%);
|
||||
```
|
||||
|
||||
### Glass Panels (`.glass` / `GlassCard`)
|
||||
- Panels use semi-transparent white fills coupled with a distinct double-border look (using inset box shadows) to simulate thick, polished acrylic sheets.
|
||||
- **Specifications**:
|
||||
- Background: `rgba(255, 255, 255, 0.76)`
|
||||
- Border: `1px solid rgba(126, 161, 210, 0.48)`
|
||||
- Inner Highlight: `inset 0 1px 0 rgba(255, 255, 255, 0.96)`
|
||||
- Shadow: `0 24px 60px rgba(61, 99, 160, 0.15)`
|
||||
- Blur: `backdrop-blur-xl`
|
||||
- Border Radius: `28px` (`rounded-[28px]`)
|
||||
|
||||
---
|
||||
|
||||
## 3. Page Shell, Layout & Grids
|
||||
|
||||
The desktop shell is designed to be self-contained, fitting exactly within the viewport height without scrolling the entire window.
|
||||
|
||||
- **Page Shell**: Max-width of `1160px` centered, utilizing a full-height flexbox column (`h-screen p-6`).
|
||||
- **Hero Header**: A balanced layout placing title text and description on the left (`.hero-content`) and the wider brand artwork (`openpets.webp`) on the right (`.hero-logo-container`). Keep artwork large enough to feel intentional, use `object-contain`, and avoid rounded wrappers or distortion.
|
||||
- **Layout Split**: A clean grid split, typically:
|
||||
- **Left Gallery/List Panel**: `1.25fr` — for browsing, searching, and filtering.
|
||||
- **Right Detail/Action Panel**: `0.75fr` — for contextual inspection and heavy operations.
|
||||
- **Scroll Containers**: Sub-panels (like grids and lists) scroll internally (`overflow-y-auto`) with customized scrollbars so that headers, filters, and primary action bars remain pinned.
|
||||
|
||||
---
|
||||
|
||||
## 4. Typography & Tone
|
||||
|
||||
- **Font Family**: Refined sans-serif (e.g., `Inter`, `system-ui`) for maximum readability in body copy, labels, and metadata.
|
||||
- **Display Typography**: Strong, characterful monospace or display-sans fonts (e.g., Lucida Console, geometric display fonts) for headers and uppercase labels to evoke retro desktop applications.
|
||||
- **Typographic Hierarchy**:
|
||||
- **Hero Title**: `text-5xl font-black tracking-tight`
|
||||
- **Panel Title**: `text-3xl font-black`
|
||||
- **Eyebrow / Section Header**: `text-xs font-bold uppercase tracking-[.18em] text-brand`
|
||||
- **Card Title**: `text-sm font-semibold text-navy`
|
||||
- **Metadata / Descriptions**: `text-xs text-slate-500`
|
||||
- **Copy Tone**: Conversational, clean, and helpful. Avoid overly dense technical jargon; explain state transitions clearly (e.g., *"Ready to become your default pet"* instead of *"State: INSTALLED, ACTIVE: FALSE"*).
|
||||
|
||||
---
|
||||
|
||||
## 5. Colors & Semantic Palette
|
||||
|
||||
All colors are mapped to functional roles to ensure consistency across themes.
|
||||
|
||||
| Role | Color / Hex | Tailwind Equivalent | Semantic Usage |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Brand Primary** | `#176df2` / `#3b96ff` | `bg-brand` / `text-brand` | Active filters, primary buttons, hero accents |
|
||||
| **Dark Neutral** | `#102149` | `text-navy` | Main body text, card titles, heavy headings |
|
||||
| **Light Neutral** | `#f8fbff` to `#e9f3ff` | `bg-slate-50` / `bg-blue-50` | Page backdrops, card backgrounds, empty states |
|
||||
| **Slate Copy** | `#5c6e91` | `text-slatecopy` | Subtitles, helper text, inactive buttons, labels |
|
||||
| **Success / Ready** | `#059669` / `#34d399` | `emerald` | Active indicators, successfully installed badges |
|
||||
| **Warning / Import** | `#d97706` / `#fbbf24` | `amber` | Codex source indicators, imports, attention states |
|
||||
| **Featured** | `#7e22ce` / `#a855f7` | `purple` | Featured catalog filters and badges |
|
||||
| **Originals** | `#ca8a04` / `#facc15` | `yellow` | Original OpenPets catalog filters and badges |
|
||||
| **Danger / Remove** | `#dc2626` / `#ff6b6b` | `red` | Destructive actions, broken states, error messages |
|
||||
|
||||
---
|
||||
|
||||
## 6. Tactile Button System
|
||||
|
||||
Buttons are the core interactive elements. They reject flat modern trends in favor of a chunky, tangible 3D appearance inspired by physical pet hardware and OS controls.
|
||||
|
||||
### Visual Architecture
|
||||
- **Borders**: Darker semi-transparent bottom/side borders (`rgba(..., 0.32)`).
|
||||
- **Gradients**: Linear vertical gradients that transition from a lighter top to a richer bottom.
|
||||
- **Inset Highlights**: A subtle light line (`inset 0 1px 0 rgba(255,255,255,0.38)`) on the top edge to simulate light catching the button's crown.
|
||||
- **Shadows**: Keep buttons grounded. Use subtle neutral depth plus inset highlights; avoid large colored glow shadows.
|
||||
|
||||
### Interactive Rules
|
||||
- **Hover States**: Under current design guidelines, **avoid dramatic hover glows or float lifts**. Hovering should simply shift the gradient/background color slightly to maintain a grounded, crisp, and predictable interface.
|
||||
- **Active Click State**: On press, the button scales down slightly to `0.96` to provide a satisfying physical click response.
|
||||
- **Constraint**: Always use targeted transitions (`transition-[transform,background-color,border-color,box-shadow]`) instead of `transition-all` to prevent layout reflow stutter.
|
||||
|
||||
### Button Variants
|
||||
|
||||
1. **Primary** (`.btn-primary`):
|
||||
- Gradient: `#3b96ff` to `#176df2`
|
||||
- Shadow: `inset 0 1px 0 rgba(255,255,255,0.38), 0 2px 4px rgba(61,99,160,0.10)`
|
||||
2. **Secondary** (`.btn-secondary`):
|
||||
- Surface: `rgba(255,255,255,0.76)` with text `#176df2`
|
||||
- Border: `rgba(37, 99, 235, 0.42)`
|
||||
3. **Danger** (`.btn-danger`):
|
||||
- Gradient: `#ff6b6b` to `#dc2626`
|
||||
- Shadow: same subtle neutral depth as primary; do not add red glow
|
||||
4. **Warning** (`.btn-warning`):
|
||||
- Gradient: `#fbbf24` to `#d97706`
|
||||
5. **Success** (`.btn-success`):
|
||||
- Gradient: `#34d399` to `#059669`
|
||||
6. **Compact Button** (`.btn-compact`):
|
||||
- Designed for pagination (`.pager`) and secondary rows.
|
||||
- Reduces size to `min-h-[30px]`, padding to `px-2.5 py-1`, and font size to `text-xs` with a tighter border-radius (`rounded-xl`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Controls & Navigation Elements
|
||||
|
||||
### Filters (`.filter`)
|
||||
- Instead of simple pill badges, filters are structured as miniature tactile buttons.
|
||||
- Inactive filters use a soft white-to-slate gradient with a light blue-grey border.
|
||||
- Active filters swap to semantic gradients with only subtle grounded depth:
|
||||
- Brand blue for general filters (`All`, `Installed`, `Western`, `Asian`, `Codex`)
|
||||
- Purple for `Featured`
|
||||
- Amber/yellow-orange for `Originals`
|
||||
- Match catalog v3/web gallery taxonomy where possible: `Featured` excludes originals, and category filters like `Western`/`Asian` exclude featured and originals.
|
||||
|
||||
### Pet Cards & Badges
|
||||
- Card grids need inner padding so selected rings and focus states are not clipped by the scroll container.
|
||||
- Use badges to expose catalog identity, not just install state:
|
||||
- `Original` for original/OpenPets pets
|
||||
- `Featured` for featured non-original catalog pets
|
||||
- `Western` / `Asian` for standard category pets
|
||||
- `Default`, `Installed`, and `Codex` for local state/source
|
||||
- Avoid low-value detail badges such as `Available`; reserve detail badges for meaningful state or catalog identity.
|
||||
|
||||
### Search Inputs (`.search`)
|
||||
- Rendered as soft, inset text boxes.
|
||||
- Styled with `bg-white/80` and `shadow-inner` to look recessed into the panel.
|
||||
- Focus states use a visible but restrained brand ring (`focus:ring-brand/15 focus:border-brand`) for accessibility.
|
||||
|
||||
### Pager (`.pager`)
|
||||
- Placed cleanly at the **bottom** of list/grid containers.
|
||||
- Merges compact secondary buttons with centered, bold, uppercase status text (`text-xs font-bold tracking-wider`).
|
||||
|
||||
---
|
||||
|
||||
## 8. Icons & Visual Assets
|
||||
|
||||
- **Coherency**: Use clean, outline-style vectors with a consistent stroke weight (`stroke-width="2.5"` or `3` for smaller shapes).
|
||||
- **Style Alignment**: Align with the **Lucide / Iconify** icon libraries.
|
||||
- **Implementation**: Embed icons as stateless, inline React SVG components or a localized icon catalog within the component file to avoid heavy runtime dependencies.
|
||||
- **Sizing**:
|
||||
- Action/Button Icons: `16px x 16px`
|
||||
- Compact/Filter Icons: `12px x 12px` or `14px x 14px`
|
||||
|
||||
---
|
||||
|
||||
## 9. React & Tailwind Implementation Notes
|
||||
|
||||
### The Static Purge Constraint
|
||||
Tailwind CSS extracts classes statically by analyzing source code strings. **Do not construct class names dynamically**.
|
||||
|
||||
```tsx
|
||||
// ❌ BAD: Will be purged by Tailwind in production
|
||||
const color = "blue";
|
||||
return <span className={`pill-${color}`}>Label</span>;
|
||||
|
||||
// GOOD: Statically discoverable mapping
|
||||
const statusPillToneClass = {
|
||||
blue: "pill-blue",
|
||||
green: "pill-green",
|
||||
} as const;
|
||||
return <span className={`pill ${statusPillToneClass[tone]}`}>Label</span>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Migration Playbook for Future Pages
|
||||
|
||||
When refactoring other Control Center views, apply the following structural mapping:
|
||||
|
||||
### A. Settings Page
|
||||
- **Old Layout**: Long scrolling list of form controls.
|
||||
- **New Layout**: Split-pane layout. Left side lists settings categories (General, Hotkeys, Performance) using `.filter` tactile navigation. Right side contains the active form wrapped in a `.glass` card.
|
||||
- **Inputs**: Swap default inputs to the recessed `.search` input style. Use `.btn-primary` for the final "Save" action.
|
||||
|
||||
### B. Plugins Page
|
||||
- **Old Layout**: Flat list of available integrations.
|
||||
- **New Layout**: Grid-based layout matching the `.pets-grid` structure.
|
||||
- **Cards**: Use the `.pet-card` styling with a thumbnail on the left, author/description text on the right, and semantic status pills (e.g., `.pill-green` for "Active", `.pill-slate` for "Disabled").
|
||||
|
||||
### C. Onboarding
|
||||
- **Old Layout**: Full screen wizard.
|
||||
- **New Layout**: Centered, floating glass card (`.glass` with `max-w-xl shadow-glass`) layered over the atmospheric radial backdrop.
|
||||
- **Interaction**: Use staggered CSS keyframes for a smooth entrance, and a prominent `.btn-primary` with active scaling for the "Get Started" call to action.
|
||||
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 366 KiB After Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 488 KiB After Width: | Height: | Size: 115 KiB |
BIN
apps/desktop/assets/openpets.webp
Normal file
|
After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.3 KiB |
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
## Responsibility
|
||||
|
||||
OpenPets desktop companion application. Tray-first Electron app providing animated desktop pets that react to coding agent events. Manages pet installations, plugin automation, agent integrations (Claude Code, OpenCode), and local IPC for CLI communication.
|
||||
OpenPets desktop companion application. Tray-first Electron app providing animated desktop pets that react to coding agent events. Manages pet installations, the React/Tailwind Control Center, plugin automation/runtime, agent integrations (Claude Code, OpenCode, Cursor, Pi guidance), and local IPC for CLI communication.
|
||||
|
||||
## Design
|
||||
|
||||
- **Tray-First UX**: No main window; all interaction via tray menu or task windows (pet-manager, agent-setup, settings, onboarding)
|
||||
- **Tray-First UX**: No default main window; tray actions open the singleton React/Tailwind Control Center and route directly to Dashboard, Pets, Integrations, Plugins, and Settings.
|
||||
- **Single Instance**: Uses `app.requestSingleInstanceLock()` with second-instance focusing
|
||||
- **Security Model**:
|
||||
- Sandboxed renderers with contextIsolation
|
||||
|
|
@ -23,11 +23,11 @@ OpenPets desktop companion application. Tray-first Electron app providing animat
|
|||
- User-configurable reaction-to-animation mapping
|
||||
- **Lease Manager**: 15s TTL leases for agent pet routing with heartbeat renewal
|
||||
- **Logging**: Structured logging with scopes, log rotation (2MB max), and sensitive data redaction
|
||||
- **Plugin Subsystem**: Manifest-only v1 plugin runtime for timer-triggered `pet.speak`/`pet.react` actions, permission approval, config schemas, catalog/local installs, and safe path/ZIP/manifest validation
|
||||
- **Plugin Subsystem**: Declarative manifest plugins and JavaScript plugin hosting with permission approval, config schemas, command/status surfaces, catalog/local installs, SDK bridge quotas, storage, schedules, restricted HTTPS fetch, and safe path/ZIP/manifest validation
|
||||
|
||||
## Flow
|
||||
|
||||
**Startup**: `main.ts` → `installAppLifecycle()` → `initializeAppState()` → `initializeLogger()` → `createAppTray()` → `startLocalIpcServer()` → optionally `showDefaultPet()`/`openTaskWindow("onboarding")`
|
||||
**Startup**: `main.ts` → `installAppLifecycle()` → `initializeAppState()` → `initializeLogger()` → `createAppTray()` → `startLocalIpcServer()` → initialize plugin service with JavaScript host/SDK bridge → optionally `showDefaultPet()`
|
||||
|
||||
**Pet Display**: IPC Request → `local-ipc.ts` → `LeaseManager.acquire()` → `agent-pet-controller.ts` → `pet-window.ts` → HTML/CSS spritesheet animation with reaction-to-animation mapping
|
||||
|
||||
|
|
@ -35,7 +35,9 @@ OpenPets desktop companion application. Tray-first Electron app providing animat
|
|||
|
||||
**Agent Setup**: UI → `agent-setup.ts` → Claude/OpenCode/Cursor CLI detection → MCP config modification → hooks installation → memory file management
|
||||
|
||||
**Plugins**: Plugins window → `plugin-service.ts` → catalog or local manifest loader → permission approval/state update → `plugin-runtime.ts` schedules declarative timers → `plugin-pet-api.ts` applies speech/reactions to the default pet
|
||||
**Control Center**: Tray route → `openControlCenterWindow(route)` → `windows.ts` loads Vite renderer and sends route events → `control-center-preload.cjs` exposes narrow page APIs → React Dashboard/Pets/Integrations/Plugins/Settings routes render snapshots and invoke actions.
|
||||
|
||||
**Plugins**: Control Center plugins route → `plugin-service.ts` → catalog or local manifest/entry loader → permission approval/state update → `plugin-runtime.ts` schedules declarative timers or starts `plugin-js-host.ts` → `plugin-sdk-bridge.ts` applies approved SDK calls to pet/schedule/storage/command/status/network APIs
|
||||
|
||||
## Integration Points
|
||||
|
||||
|
|
@ -60,17 +62,17 @@ OpenPets desktop companion application. Tray-first Electron app providing animat
|
|||
|
||||
- `main.ts`: Entry point, lifecycle coordination
|
||||
- `tray.ts`: System tray icon and menu
|
||||
- `windows.ts`: Task window management (pet-manager, agent-setup, plugins, settings, onboarding)
|
||||
- `plugins-window.ts`: Sandboxed plugins task-window HTML for installed/discover/developer plugin management
|
||||
- `windows.ts`: Control Center BrowserWindow management, Dashboard snapshot, route targeting, IPC handlers, and internal protocols
|
||||
- `renderer/`: React/Tailwind Control Center for Dashboard, Pets, Integrations, Plugins, and Settings
|
||||
- `local-ipc.ts`: TCP/Unix socket server for CLI communication
|
||||
- `lease-manager.ts`: Pet routing lease lifecycle
|
||||
- `pet-window.ts`: Pet rendering (transparent frameless windows, CSS sprite animation, speech bubbles, status badges)
|
||||
- `default-pet-controller.ts`/`agent-pet-controller.ts`: Pet visibility/state management with transient displays
|
||||
- `app-state.ts`: Persistent state management (JSON file)
|
||||
- `agent-setup.ts`: Claude/OpenCode/Cursor integration logic
|
||||
- `plugin-service.ts`: Plugin orchestration for snapshots, enable/config/reload, catalog install/update/uninstall, local loading, permission approval, and runtime reloads
|
||||
- `plugin-manifest.ts`: `openpets.plugin.json` v1 schema/types/validator for declarative timer plugins, config fields, permissions, and actions
|
||||
- `plugin-runtime.ts`: Safe declarative runtime that compiles enabled plugin timers and runs approved pet speech/reaction actions
|
||||
- `plugin-service.ts`: Plugin orchestration for snapshots, enable/config/reload, command execution, catalog install/update/uninstall, local loading, permission approval, JavaScript host wiring, and runtime reloads
|
||||
- `plugin-manifest.ts`: `openpets.plugin.json` v1/v2 schema/types/validator for declarative timer plugins and JavaScript SDK plugins, config fields, permissions, commands/status/network, and actions
|
||||
- `plugin-runtime.ts`: Runtime that compiles enabled declarative timers and starts JavaScript plugin hosts for approved pet/schedule/storage/command/status/network actions
|
||||
- `plugin-state.ts`: Atomic JSON state store for installed plugins, enabled flag, approved permissions, config, broken state, and update metadata
|
||||
- `plugin-config.ts`: Plugin default/effective config validation and config reference resolution
|
||||
- `plugin-catalog.ts`/`plugin-catalog-validation.ts`: Plugin catalog fetch/cache and strict catalog entry validation
|
||||
|
|
@ -78,13 +80,15 @@ OpenPets desktop companion application. Tray-first Electron app providing animat
|
|||
- `plugin-local-loader.ts`: Local developer plugin folder validation and manifest snapshotting into app data
|
||||
- `plugin-manifest-reader.ts`: Safe installed-manifest reader enforcing allowed roots, size limits, path containment, and expected id/version
|
||||
- `plugin-pet-api.ts`: Runtime bridge from plugin actions to default pet speech/reaction APIs
|
||||
- `plugin-js-host.ts`: Hidden sandboxed BrowserWindow host for JavaScript plugin entry modules, SDK IPC tokening, session hardening, startup handshake, and teardown
|
||||
- `plugin-sdk-bridge.ts`: Permission-checked SDK API for JavaScript plugins with quotas, plugin storage, schedules, config listeners, commands/status, logs, and restricted HTTPS fetch
|
||||
- `pet-installation.ts`: Catalog ZIP download and extraction
|
||||
- `codex-pets.ts`: Local Codex pet import
|
||||
- `catalog.ts`: Remote catalog fetching with V3 pagination and fixture fallback
|
||||
- `logger.ts`: Structured logging with scopes (app, ipc, lease, pet, state, tray, ui)
|
||||
- `reaction-animation-mapping.ts`: Reaction-to-animation state mapping with user overrides
|
||||
- `reaction-messages.ts`: Message pools for each reaction type
|
||||
- `preload.cjs`/`pet-preload.cjs`: Renderer preload scripts (contextBridge APIs)
|
||||
- `control-center-preload.cjs`/`pet-preload.cjs`/`plugin-sdk-preload.cjs`: Narrow contextBridge APIs for the Control Center, pet windows, and plugin SDK host; the legacy `preload.cjs` task-window bridge and `plugins-window.ts` UI have been removed
|
||||
- `electron-builder.yml`: Packaging configuration
|
||||
- `scripts/release-local.mjs`: macOS-local release automation with GitHub draft creation
|
||||
- `contracts/catalog-fixture.contract.ts`: Catalog V2 validation contract tests against fixture data
|
||||
|
|
|
|||
43
apps/desktop/control-center-preload.cjs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
const api = {
|
||||
getPetsState: () => ipcRenderer.invoke("openpets:get-pets-state"),
|
||||
getDashboardSnapshot: () => ipcRenderer.invoke("openpets:get-dashboard-snapshot"),
|
||||
getSettingsState: () => ipcRenderer.invoke("openpets:get-settings-state"),
|
||||
updatePreferences: (patch) => ipcRenderer.invoke("openpets:update-preferences", patch),
|
||||
getReactionAnimationSettings: () => ipcRenderer.invoke("openpets:get-reaction-animation-settings"),
|
||||
getLaunchAtLogin: () => ipcRenderer.invoke("openpets:get-launch-at-login"),
|
||||
setLaunchAtLogin: (enabled) => ipcRenderer.invoke("openpets:set-launch-at-login", enabled),
|
||||
getUpdateStatus: () => ipcRenderer.invoke("openpets:get-update-status"),
|
||||
checkForUpdates: () => ipcRenderer.invoke("openpets:check-for-updates"),
|
||||
openUpdateReleasePage: () => ipcRenderer.invoke("openpets:open-update-release-page"),
|
||||
resetDefaultPetPosition: () => ipcRenderer.invoke("openpets:reset-default-pet-position"),
|
||||
getPluginsSnapshot: () => ipcRenderer.invoke("openpets:plugins-snapshot"),
|
||||
getPluginCatalogSnapshot: (refresh) => ipcRenderer.invoke("openpets:plugins-catalog-snapshot", refresh),
|
||||
setPluginEnabled: (id, enabled) => ipcRenderer.invoke("openpets:plugins-set-enabled", id, enabled),
|
||||
savePluginConfig: (id, config) => ipcRenderer.invoke("openpets:plugins-save-config", id, config),
|
||||
reloadPlugin: (id) => ipcRenderer.invoke("openpets:plugins-reload", id),
|
||||
executePluginCommand: (id, commandId) => ipcRenderer.invoke("openpets:plugins-execute-command", id, commandId),
|
||||
loadLocalPlugin: () => ipcRenderer.invoke("openpets:plugins-load-local"),
|
||||
installCatalogPlugin: (id) => ipcRenderer.invoke("openpets:plugins-install-catalog", id),
|
||||
updateCatalogPlugin: (id) => ipcRenderer.invoke("openpets:plugins-update-catalog", id),
|
||||
uninstallPlugin: (id) => ipcRenderer.invoke("openpets:plugins-uninstall", id),
|
||||
getCatalog: () => ipcRenderer.invoke("openpets:get-catalog"),
|
||||
getCatalogPage: (page) => ipcRenderer.invoke("openpets:get-catalog-page", page),
|
||||
getCatalogSearch: () => ipcRenderer.invoke("openpets:get-catalog-search"),
|
||||
getCodexPets: () => ipcRenderer.invoke("openpets:get-codex-pets"),
|
||||
setDefaultPet: (petId) => ipcRenderer.invoke("openpets:set-default-pet", petId),
|
||||
installPet: (petId) => ipcRenderer.invoke("openpets:install-pet", petId),
|
||||
importCodexPet: (petId) => ipcRenderer.invoke("openpets:import-codex-pet", petId),
|
||||
removePet: (petId) => ipcRenderer.invoke("openpets:remove-pet", petId),
|
||||
onRouteChange: (callback) => {
|
||||
const listener = (_event, route) => callback(route);
|
||||
ipcRenderer.on("openpets:control-center-route", listener);
|
||||
return () => ipcRenderer.removeListener("openpets:control-center-route", listener);
|
||||
},
|
||||
getIntegrationsState: (selectedPetId, commandMode) => ipcRenderer.invoke("openpets:agent-setup-snapshot", selectedPetId, commandMode),
|
||||
runIntegrationAction: (action, selectedPetId, commandMode) => ipcRenderer.invoke("openpets:agent-setup-action", action, selectedPetId, commandMode),
|
||||
updateIntegrationCommandPaths: (patch) => ipcRenderer.invoke("openpets:agent-setup-command-paths", patch),
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld("openPetsControlCenter", api);
|
||||
|
|
@ -10,7 +10,7 @@ artifactName: OpenPets-${version}-${os}-${arch}.${ext}
|
|||
|
||||
files:
|
||||
- dist/**
|
||||
- preload.cjs
|
||||
- control-center-preload.cjs
|
||||
- pet-preload.cjs
|
||||
- plugin-sdk-preload.cjs
|
||||
- assets/**
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
"dev": "pnpm build && electron .",
|
||||
"dev:electron": "pnpm build:main && electron .",
|
||||
"dev:control-center": "concurrently -k -n renderer,electron -c cyan,magenta \"pnpm dev:renderer\" \"wait-on http://127.0.0.1:5173 && cross-env OPENPETS_RENDERER_URL=http://127.0.0.1:5173 pnpm dev:electron\"",
|
||||
"dev:plugins": "OPENPETS_DISABLE_PLUGIN_CATALOG=1 OPENPETS_DEV_PLUGIN_ROOTS=../../web/plugins/official pnpm dev",
|
||||
"dev:debug": "OPENPETS_LOG_LEVEL=debug OPENPETS_LOG_CONSOLE=1 pnpm dev",
|
||||
"package": "pnpm build && node scripts/clean-package-output.cjs && electron-builder",
|
||||
|
|
@ -22,15 +24,28 @@
|
|||
"build:deps": "pnpm --filter @open-pets/desktop^... build",
|
||||
"test:build": "pnpm build:deps && tsc -p tsconfig.tests.json",
|
||||
"check": "pnpm typecheck && pnpm build && pnpm test",
|
||||
"typecheck": "pnpm build:deps && tsc --noEmit",
|
||||
"build": "pnpm build:deps && tsc"
|
||||
"typecheck": "pnpm build:deps && tsc --noEmit && tsc -p tsconfig.renderer.json --noEmit",
|
||||
"build:main": "pnpm build:deps && tsc",
|
||||
"build:renderer": "vite build",
|
||||
"dev:renderer": "vite --host 127.0.0.1",
|
||||
"build": "pnpm build:main && pnpm build:renderer"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.6.2",
|
||||
"@types/react": "^19.2.15",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/yauzl": "2.10.3",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"electron": "^42.0.0",
|
||||
"electron-builder": "^26.10.0",
|
||||
"typescript": "^6.0.3"
|
||||
"postcss": "^8.5.15",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.0.14",
|
||||
"wait-on": "^9.0.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@open-pets/agent-events": "workspace:*",
|
||||
|
|
@ -39,6 +54,8 @@
|
|||
"@open-pets/cursor": "workspace:*",
|
||||
"@open-pets/mcp": "workspace:*",
|
||||
"@open-pets/opencode": "workspace:*",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"sharp": "^0.34.5",
|
||||
"yauzl": "3.3.0"
|
||||
}
|
||||
|
|
|
|||
6
apps/desktop/postcss.config.cjs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
|
@ -11,7 +11,7 @@ import { dirname, join } from "node:path";
|
|||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, "..");
|
||||
|
||||
const preloadChecks = ["preload.cjs", "pet-preload.cjs", "plugin-sdk-preload.cjs"];
|
||||
const preloadChecks = ["control-center-preload.cjs", "pet-preload.cjs", "plugin-sdk-preload.cjs"];
|
||||
const behaviorTests = [
|
||||
".test-dist/tests/lease-manager.test.js",
|
||||
".test-dist/tests/onboarding-state.test.js",
|
||||
|
|
@ -26,7 +26,6 @@ const behaviorTests = [
|
|||
".test-dist/tests/plugin-catalog-validation.test.js",
|
||||
".test-dist/tests/plugin-package.test.js",
|
||||
".test-dist/tests/plugin-service.test.js",
|
||||
".test-dist/tests/plugins-window.test.js",
|
||||
".test-dist/tests/plugin-ui-static.test.js",
|
||||
];
|
||||
const contractTests = [
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ async function getOpenCodeSetup(commandMode: OpenPetsCommandMode, selectedPetId:
|
|||
label: configured ? "Installed" : globalState.status === "custom" || globalState.status === "conflict" ? "Needs attention" : detected.ok ? "Ready" : "Not detected",
|
||||
details: globalState.status === "custom" || globalState.status === "conflict" || globalState.status === "error" ? globalState.message : configured ? globalState.message : detected.ok ? "OpenCode was detected. Desktop setup writes global OpenCode config." : getPreferredOpenCodeCommand() === (process.platform === "win32" ? "opencode.cmd" : "opencode") ? "OpenCode was not found on PATH. You can still preview setup, but OpenCode must be installed to use it." : "OpenCode did not run from the saved command path. You can still preview setup, but OpenCode must be installed to use it.",
|
||||
configDir: formatUserPath(configDir) ?? configDir,
|
||||
canInstall: prepared.ok,
|
||||
canInstall: prepared.ok && !configured,
|
||||
canRemove: configured,
|
||||
},
|
||||
preview: {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { app } from "electron";
|
|||
import { defaultPetScale, markOnboardingCompleted, normalizeOnboardingCompleted, normalizePetScale, petScaleOptions, type PetScaleValue } from "./app-state-core.js";
|
||||
import { builtInPet } from "./built-in-pet.js";
|
||||
import type { Point } from "./display.js";
|
||||
import { allowedReactions, type OpenPetsReaction } from "./local-ipc-protocol.js";
|
||||
import { assertSafePetId, getInstalledPetDir } from "./pet-paths.js";
|
||||
import { normalizeReactionAnimationOverrides, type ReactionAnimationOverrides } from "./reaction-animation-mapping.js";
|
||||
|
||||
|
|
@ -48,8 +49,21 @@ export interface OpenPetsStateV1 {
|
|||
readonly defaultPet: {
|
||||
readonly position?: Point;
|
||||
};
|
||||
readonly analytics: OpenPetsAnalyticsState;
|
||||
}
|
||||
|
||||
export interface OpenPetsAnalyticsState {
|
||||
readonly messagesSent: number;
|
||||
readonly reactionsSent: number;
|
||||
readonly reactionCounts: Record<OpenPetsReaction, number>;
|
||||
readonly perPetActivityCounts: Record<string, number>;
|
||||
readonly lastActivityAt?: number;
|
||||
}
|
||||
|
||||
export type OpenPetsActivityRecord =
|
||||
| { readonly kind: "say"; readonly reaction?: OpenPetsReaction; readonly petId?: string }
|
||||
| { readonly kind: "react"; readonly reaction: OpenPetsReaction; readonly petId?: string };
|
||||
|
||||
export { defaultPetScale, normalizePetScale, petScaleOptions, type PetScaleValue };
|
||||
|
||||
const stateFileName = "openpets-state.json";
|
||||
|
|
@ -151,6 +165,30 @@ export function getDefaultPetPosition(): Point | undefined {
|
|||
return getInitializedState().defaultPet.position;
|
||||
}
|
||||
|
||||
export function recordOpenPetsActivity(activity: OpenPetsActivityRecord, now: number = Date.now()): OpenPetsStateV1 {
|
||||
const state = getInitializedState();
|
||||
const analytics = state.analytics;
|
||||
const reaction = activity.kind === "react" ? activity.reaction : activity.reaction;
|
||||
const petId = activity.petId;
|
||||
const nextState = normalizeState({
|
||||
...state,
|
||||
analytics: {
|
||||
messagesSent: analytics.messagesSent + (activity.kind === "say" ? 1 : 0),
|
||||
reactionsSent: analytics.reactionsSent + (reaction ? 1 : 0),
|
||||
reactionCounts: reaction
|
||||
? { ...analytics.reactionCounts, [reaction]: (analytics.reactionCounts[reaction] ?? 0) + 1 }
|
||||
: analytics.reactionCounts,
|
||||
perPetActivityCounts: petId
|
||||
? { ...analytics.perPetActivityCounts, [petId]: (analytics.perPetActivityCounts[petId] ?? 0) + 1 }
|
||||
: analytics.perPetActivityCounts,
|
||||
lastActivityAt: normalizeTimestamp(now) ?? Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
commitState(nextState);
|
||||
return getAppStateSnapshot();
|
||||
}
|
||||
|
||||
export function installPetState(pet: Omit<InstalledPetState, "builtIn" | "protected" | "installed">): OpenPetsStateV1 {
|
||||
const state = getInitializedState();
|
||||
|
||||
|
|
@ -280,9 +318,55 @@ function normalizeState(value: unknown): OpenPetsStateV1 {
|
|||
installed: installedPets,
|
||||
},
|
||||
defaultPet: position ? { position } : {},
|
||||
analytics: normalizeAnalytics(record.analytics),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAnalytics(value: unknown): OpenPetsAnalyticsState {
|
||||
const record = isRecord(value) ? value : {};
|
||||
return {
|
||||
messagesSent: normalizeCount(record.messagesSent),
|
||||
reactionsSent: normalizeCount(record.reactionsSent),
|
||||
reactionCounts: normalizeReactionCounts(record.reactionCounts),
|
||||
perPetActivityCounts: normalizePerPetActivityCounts(record.perPetActivityCounts),
|
||||
lastActivityAt: normalizeTimestamp(record.lastActivityAt),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeReactionCounts(value: unknown): Record<OpenPetsReaction, number> {
|
||||
const record = isRecord(value) ? value : {};
|
||||
const counts = {} as Record<OpenPetsReaction, number>;
|
||||
for (const reaction of allowedReactions) {
|
||||
counts[reaction] = normalizeCount(record[reaction]);
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function normalizePerPetActivityCounts(value: unknown): Record<string, number> {
|
||||
const record = isRecord(value) ? value : {};
|
||||
const counts: Record<string, number> = {};
|
||||
for (const [petId, rawCount] of Object.entries(record)) {
|
||||
if (petId !== builtInPet.id) {
|
||||
try {
|
||||
assertSafePetId(petId);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const count = normalizeCount(rawCount);
|
||||
if (count > 0) counts[petId] = count;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function normalizeCount(value: unknown): number {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
||||
}
|
||||
|
||||
function normalizeTimestamp(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined;
|
||||
}
|
||||
|
||||
function normalizePreferences(value: Partial<OpenPetsStateV1["preferences"]>): OpenPetsStateV1["preferences"] {
|
||||
const defaultState = createDefaultState();
|
||||
|
||||
|
|
@ -370,6 +454,13 @@ function createDefaultState(): OpenPetsStateV1 {
|
|||
installed: [builtInPet],
|
||||
},
|
||||
defaultPet: {},
|
||||
analytics: {
|
||||
messagesSent: 0,
|
||||
reactionsSent: 0,
|
||||
reactionCounts: normalizeReactionCounts(undefined),
|
||||
perPetActivityCounts: {},
|
||||
lastActivityAt: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,13 +45,13 @@ assert.match(builderConfig, /asar:\s*true/);
|
|||
assert.match(builderConfig, /asarUnpack:/);
|
||||
assert.match(builderConfig, /node_modules\/\*\*/);
|
||||
assert.match(builderConfig, /dist\/\*\*/);
|
||||
assert.match(builderConfig, /preload\.cjs/);
|
||||
assert.match(builderConfig, /control-center-preload\.cjs/);
|
||||
assert.match(builderConfig, /pet-preload\.cjs/);
|
||||
assert.match(builderConfig, /plugin-sdk-preload\.cjs/);
|
||||
assert.match(builderConfig, /assets\/\*\*/);
|
||||
assert.match(builderConfig, /icon:\s*assets\/app-icon\.icns/);
|
||||
|
||||
assert.ok(existsSync(join(appDir, "preload.cjs")), "preload.cjs must exist for packaging.");
|
||||
assert.ok(existsSync(join(appDir, "control-center-preload.cjs")), "control-center-preload.cjs must exist for Control Center IPC.");
|
||||
assert.ok(existsSync(join(appDir, "pet-preload.cjs")), "pet-preload.cjs must exist for pet window motion state updates.");
|
||||
assert.ok(existsSync(join(appDir, "plugin-sdk-preload.cjs")), "plugin-sdk-preload.cjs must exist for JavaScript plugin SDK hosting.");
|
||||
assert.ok(existsSync(join(appDir, "assets", "tray-icon.png")), "tray icon must exist for packaging.");
|
||||
|
|
@ -59,13 +59,13 @@ assert.ok(existsSync(join(appDir, "assets", "app-icon.icns")), "app icon must ex
|
|||
assert.ok(existsSync(join(appDir, "assets", "app-icon.ico")), "Windows app icon must exist for packaging.");
|
||||
assertNonEmptyFile(join(appDir, "assets", "default-pet-spritesheet.webp"), "default pet spritesheet must exist for packaging.");
|
||||
assertNonEmptyFile(join(appDir, "assets", "default-pet-thumbnail.png"), "default pet thumbnail must exist for Pet Manager preview.");
|
||||
assertNonEmptyFile(join(appDir, "assets", "onboarding-logo.webp"), "onboarding logo asset must exist for packaging.");
|
||||
assertNonEmptyFile(join(appDir, "assets", "onboarding-pets.webp"), "onboarding pet scene asset must exist for packaging.");
|
||||
for (const icon of ["claude.svg", "cursor.svg", "opencode.svg", "pi.svg", "vscode.svg", "windsurf.svg", "zed.svg"]) {
|
||||
assertSafeBundledSvg(join(appDir, "assets", "integrations", icon), `integration icon must be safe and packaged: ${icon}`);
|
||||
}
|
||||
assert.match(readFileSync(join(appDir, "src", "assets.ts"), "utf8"), /assets["']?,\s*["']tray-icon\.png|join\("assets", "tray-icon\.png"\)/, "tray icon code must keep using assets/tray-icon.png.");
|
||||
const petWindowSource = readFileSync(join(appDir, "src", "pet-window.ts"), "utf8");
|
||||
const controlCenterPreloadSource = readFileSync(join(appDir, "control-center-preload.cjs"), "utf8");
|
||||
const controlCenterRendererSource = readFileSync(join(appDir, "src", "renderer", "src", "main.tsx"), "utf8");
|
||||
const petPreloadSource = readFileSync(join(appDir, "pet-preload.cjs"), "utf8");
|
||||
const reactionMessagesSource = readFileSync(join(appDir, "src", "reaction-messages.ts"), "utf8");
|
||||
const displaySource = readFileSync(join(appDir, "src", "display.ts"), "utf8");
|
||||
|
|
@ -73,7 +73,6 @@ const updateCheckerSource = readFileSync(join(appDir, "src", "update-checker.ts"
|
|||
const traySource = readFileSync(join(appDir, "src", "tray.ts"), "utf8");
|
||||
const windowsSource = readFileSync(join(appDir, "src", "windows.ts"), "utf8");
|
||||
const agentSetupSource = readFileSync(join(appDir, "src", "agent-setup.ts"), "utf8");
|
||||
const preloadSource = readFileSync(join(appDir, "preload.cjs"), "utf8");
|
||||
const loggerSource = readFileSync(join(appDir, "src", "logger.ts"), "utf8");
|
||||
const mainSource = readFileSync(join(appDir, "src", "main.ts"), "utf8");
|
||||
const localIpcSourceForLogging = readFileSync(join(appDir, "src", "local-ipc.ts"), "utf8");
|
||||
|
|
@ -173,26 +172,11 @@ assert.match(windowsSource, /openpets:get-reaction-animation-settings/, "setting
|
|||
assert.match(windowsSource, /reactionAnimationOverrides/, "settings window must be able to persist reaction animation overrides.");
|
||||
assert.match(windowsSource, /openpets-pet-preview/, "settings reaction preview must use a scoped internal pet preview protocol.");
|
||||
assert.match(windowsSource, /openpets:open-update-release-page/, "settings window must be able to open the release page.");
|
||||
assert.match(windowsSource, /id="check-for-updates"/, "settings UI must include a Check for updates button.");
|
||||
assert.match(windowsSource, /id="open-update-release"/, "settings UI must include an Open release button.");
|
||||
assert.match(preloadSource, /checkForUpdates/, "settings preload must expose update checks.");
|
||||
assert.match(preloadSource, /getReactionAnimationSettings/, "settings preload must expose reaction animation settings metadata.");
|
||||
assert.match(preloadSource, /reaction-animation-table/, "settings preload must render the reaction animation mapping table.");
|
||||
assert.match(preloadSource, /\{ label: "Thinking", state: "thinking" \}/, "Pet Manager mini previews must use a non-idle thinking/review state because the hero already shows idle.");
|
||||
assert.match(preloadSource, /\{ label: "Happy", state: "happy" \}/, "Pet Manager mini previews must request the happy/jumping row.");
|
||||
assert.match(preloadSource, /\{ label: "Wave", state: "wave" \}/, "Pet Manager mini previews must request the waving row.");
|
||||
assert.doesNotMatch(preloadSource, /\{ label: "Idle", state: "idle" \}/, "Pet Manager mini previews must not duplicate the hero idle preview.");
|
||||
assert.match(preloadSource, /pm-mini-sprite[\s\S]*animated: true/, "Pet Manager mini state previews must animate spritesheet states.");
|
||||
assert.match(preloadSource, /pm-sprite-state-\$\{state\}/, "Pet Manager sprite frames must apply state-specific row classes.");
|
||||
assert.match(windowsSource, /pm-sprite-state-thinking \{ background-position: 0 100%; \}/, "Pet Manager thinking preview must use universal spritesheet row 8.");
|
||||
assert.match(windowsSource, /pm-sprite-state-wave \{ background-position: 0 37\.5%; \}/, "Pet Manager wave preview must use universal spritesheet row 3.");
|
||||
assert.match(windowsSource, /pm-sprite-state-happy \{ background-position: 0 50%; \}/, "Pet Manager happy preview must use universal spritesheet row 4.");
|
||||
assert.match(windowsSource, /pm-sprite-state-thinking\.pm-animate-sprite \{ animation: pm-sprite-thinking 1\.55s steps\(6\) infinite; \}/, "Pet Manager thinking preview must animate the 6-frame review row at a calm speed.");
|
||||
assert.match(windowsSource, /pm-sprite-state-wave\.pm-animate-sprite \{ animation: pm-sprite-wave 1\.25s steps\(4\) infinite; \}/, "Pet Manager wave preview must animate the 4-frame waving row at a calm speed.");
|
||||
assert.match(windowsSource, /pm-sprite-state-happy\.pm-animate-sprite \{ animation: pm-sprite-happy 1\.35s steps\(5\) infinite; \}/, "Pet Manager happy preview must animate the 5-frame jumping row at a calm speed.");
|
||||
assert.match(windowsSource, /@keyframes pm-sprite-thinking/, "Pet Manager must define thinking row animation keyframes.");
|
||||
assert.match(windowsSource, /@keyframes pm-sprite-wave/, "Pet Manager must define wave row animation keyframes.");
|
||||
assert.match(windowsSource, /@keyframes pm-sprite-happy/, "Pet Manager must define happy row animation keyframes.");
|
||||
assert.match(controlCenterPreloadSource, /checkForUpdates/, "Control Center preload must expose update checks.");
|
||||
assert.match(controlCenterPreloadSource, /getReactionAnimationSettings/, "Control Center preload must expose reaction animation settings metadata.");
|
||||
assert.match(controlCenterRendererSource, /function SettingsView\(\)/, "Control Center must include the settings page.");
|
||||
assert.match(controlCenterRendererSource, /getPetsState/, "Control Center must include the pets page data bridge.");
|
||||
assert.match(controlCenterRendererSource, /function IntegrationsView\(\)/, "Control Center must include the integrations page.");
|
||||
assert.match(petWindowSource, /max-width:\s*min\(220px/, "very long message bubbles must stay capped within the tight pet window.");
|
||||
assert.match(petWindowSource, /-webkit-line-clamp:\s*8/, "very long message bubbles must allow enough visible lines.");
|
||||
assert.match(petWindowSource, /createSpriteStateCss\("\.sprite"\)/, "built-in sprite CSS must react to reaction state.");
|
||||
|
|
@ -220,49 +204,13 @@ for (const reaction of allowedReactions) {
|
|||
}
|
||||
}
|
||||
assert.equal(pickReactionMessage("success", () => 0), reactionMessagePools.success[0], "reaction message picking must be deterministic when random is injected.");
|
||||
const agentSetupHtmlSource = windowsSource.match(/function createAgentSetupHtml[\s\S]*?function createSettingsHtml/)?.[0] ?? "";
|
||||
assert.match(windowsSource, /onboarding-logo\.webp/, "onboarding greeting must reference the bundled OpenPets logo asset.");
|
||||
assert.match(windowsSource, /onboarding-pets\.webp/, "onboarding greeting must reference the bundled pet scene asset.");
|
||||
assert.ok(windowsSource.includes(`content="default-src 'none'; img-src data:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-src 'none'"`), "onboarding image CSP must stay data-only for embedded packaged assets.");
|
||||
assert.doesNotMatch(windowsSource, /data-default-pet-sprite-src|createAssetDataUrl\("default-pet-spritesheet\.webp"/, "Pet Manager must not embed the large default spritesheet into the task-window data URL.");
|
||||
assert.match(windowsSource, /default-pet-thumbnail\.png/, "Pet Manager must use the small bundled default pet thumbnail for built-in preview.");
|
||||
assert.ok(windowsSource.includes(`content="default-src 'none'; img-src data: https://openpets.dev openpets-codex:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-src 'none'"`), "Pet Manager image CSP must stay scoped to data URLs, openpets.dev catalog previews, and the Codex spritesheet protocol.");
|
||||
assert.match(windowsSource, /petManagerWindowWidth\s*=\s*1160/, "Pet Manager should use the approved wider 1160px default window width.");
|
||||
assert.match(windowsSource, /petManagerWindowHeight\s*=\s*780/, "Pet Manager should use the approved taller 780px default window height.");
|
||||
assert.match(agentSetupHtmlSource, /content="default-src 'none'; img-src data:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-src 'none'"/, "Agent Setup image CSP must stay data-only for the bundled logo asset.");
|
||||
assert.match(windowsSource, /integrations\/claude\.svg/, "Agent Setup integrations hub must use bundled integration SVG assets.");
|
||||
assert.match(windowsSource, /integration-opencode-status/, "Agent Setup must show an enabled OpenCode integration card.");
|
||||
assert.match(windowsSource, /opencode-detail-view/, "Agent Setup must include an OpenCode detail pane.");
|
||||
assert.match(windowsSource, /integrations\/pi\.svg/, "Agent Setup integrations hub must use the bundled Pi SVG asset.");
|
||||
assert.match(windowsSource, /integration-pi-status/, "Agent Setup must show a Pi integration card.");
|
||||
assert.match(windowsSource, /pi-detail-view/, "Agent Setup must include a Pi manual setup detail pane.");
|
||||
assert.match(windowsSource, /Desktop OpenCode setup is global/, "OpenCode desktop setup must clearly warn that it is global.");
|
||||
assert.match(preloadSource, /opencode-install/, "Agent Setup preload must bind OpenCode install actions.");
|
||||
assert.match(preloadSource, /setIconButtonContent\(opencodeInstall, "spinner", "Installing…"\)/, "OpenCode integration-card install button must show a loading spinner.");
|
||||
assert.match(preloadSource, /opencode-remove/, "Agent Setup preload must bind OpenCode remove actions.");
|
||||
assert.match(preloadSource, /opencode-copy-config/, "Agent Setup preload must bind OpenCode copy-preview actions.");
|
||||
assert.match(preloadSource, /opencodePreview/, "Agent Setup preload must render OpenCode previews.");
|
||||
assert.match(preloadSource, /integration-pi-configure/, "Agent Setup preload must bind the Pi integration card.");
|
||||
assert.match(preloadSource, /pi-copy-global-install/, "Agent Setup preload must bind Pi copy commands.");
|
||||
assert.match(preloadSource, /cleanupConfigPaths/, "OpenCode preview must disclose stale overlay cleanup config paths.");
|
||||
assert.match(preloadSource, /configPreview/, "OpenCode preview copy must use the prepared config preview shape.");
|
||||
assert.match(agentSetupHtmlSource, /integration-cursor-status/, "Agent Setup must show an enabled Cursor integration card.");
|
||||
assert.match(agentSetupHtmlSource, /cursor-detail-view/, "Agent Setup must include a Cursor detail pane.");
|
||||
assert.match(preloadSource, /cursor-install/, "Agent Setup preload must bind Cursor install actions.");
|
||||
assert.match(preloadSource, /cursor-replace/, "Agent Setup preload must bind Cursor replace actions.");
|
||||
assert.match(preloadSource, /cursor-remove/, "Agent Setup preload must bind Cursor remove actions.");
|
||||
assert.match(preloadSource, /cursor-copy-preview/, "Agent Setup preload must bind Cursor copy-preview actions.");
|
||||
assert.match(agentSetupHtmlSource, /cursor-rules-preview/, "Agent Setup must show a Cursor project rules preview.");
|
||||
assert.match(agentSetupHtmlSource, /Desktop does not write project rules/, "Desktop Cursor rules UI must be preview/copy only.");
|
||||
assert.match(preloadSource, /cursor-copy-rules/, "Agent Setup preload must bind Cursor rules copy actions.");
|
||||
assert.doesNotMatch(agentSetupSource, /planCursorRules|executeCursorRules|cursor-rules-install|cursor-rules-remove/, "Desktop must not write Cursor project rules without a reviewed project picker.");
|
||||
assert.match(preloadSource, /updateCursorIntegration/, "Agent Setup preload must render Cursor integration.");
|
||||
assert.match(preloadSource, /cursorStatus/, "Agent Setup preload must check Cursor status.");
|
||||
assert.match(preloadSource, /cursorPreview/, "Agent Setup preload must render Cursor preview.");
|
||||
assert.doesNotMatch(controlCenterRendererSource, /OnboardingView|getOnboardingSnapshot|completeOnboarding/, "Control Center must not include the removed onboarding route.");
|
||||
assert.match(controlCenterRendererSource, /function IntegrationsView\(\)/, "Control Center must include integrations.");
|
||||
assert.match(controlCenterRendererSource, /Claude Code/, "Control Center integrations must include Claude Code.");
|
||||
assert.match(controlCenterRendererSource, /OpenCode/, "Control Center integrations must include OpenCode.");
|
||||
assert.match(controlCenterRendererSource, /Cursor/, "Control Center integrations must include Cursor.");
|
||||
assert.match(controlCenterRendererSource, /Pi/, "Control Center integrations must include Pi.");
|
||||
assert.doesNotMatch(agentSetupSource, /JSON\.parse\(prepared\.configWrite\.content\)/, "OpenCode desktop preview must parse JSONC planned config safely, not JSON.parse.");
|
||||
assert.doesNotMatch(agentSetupHtmlSource, /https?:\/\//, "Agent Setup must not allow or reference remote integration assets.");
|
||||
assert.match(windowsSource, /agentSetupWindowWidth\s*=\s*1160/, "Agent Setup should use the approved wider 1160px default window width.");
|
||||
assert.match(windowsSource, /agentSetupWindowHeight\s*=\s*780/, "Agent Setup should use the approved taller 780px default window height.");
|
||||
assert.match(windowsSource, /refreshDefaultPetContent\(\);\s*refreshAgentPetContent\(\);/, "pet scale preference changes must refresh default and agent pet windows.");
|
||||
assert.ok(existsSync(join(appDir, "scripts", "clean-package-output.cjs")), "package output cleanup helper must exist.");
|
||||
assert.ok(existsSync(join(distDir, "main.js")), "desktop main build output must exist before packaging checks run.");
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Responsibility
|
||||
|
||||
Core TypeScript source for the OpenPets desktop application. Organized into: lifecycle management, state persistence, UI windows, pet rendering, IPC server, agent integrations, pet installation/management, and the manifest-only plugin subsystem.
|
||||
Core TypeScript source for the OpenPets desktop application. Organized into: lifecycle management, state persistence, Control Center and pet windows, IPC server, agent integrations, pet installation/management, and declarative plus JavaScript plugin runtimes.
|
||||
|
||||
## Design
|
||||
|
||||
|
|
@ -11,10 +11,10 @@ Core TypeScript source for the OpenPets desktop application. Organized into: lif
|
|||
- **Defensive I/O**: All file operations use temp+rename for atomicity, path traversal validation, symlink checks
|
||||
- **Validation at Boundaries**: Catalog, ZIP entries, pet metadata, and IPC params all strictly validated
|
||||
- **Lease Pattern**: Agent pets use expiring leases (15s TTL) with heartbeats; default pet is persistent
|
||||
- **Sandboxed HTML**: All UI is data-URL or file-URL HTML with inline CSS, no external resources
|
||||
- **Sandboxed Renderers**: Control Center loads the Vite React/Tailwind bundle through a hardened BrowserWindow and narrow preload bridge; transparent pet windows and plugin SDK host windows stay separate
|
||||
- **Structured Logging**: Scoped logging (app, ipc, lease, pet.*, state, tray, ui) with log rotation and redaction
|
||||
- **Reaction Animation Mapping**: User-configurable mapping from reaction types to sprite animation states
|
||||
- **Manifest-Only Plugins**: Plugins are declarative `openpets.plugin.json` manifests with approved permissions, persisted config, safe path checks, and timer-triggered pet actions; JavaScript runtime is recognized but unsupported.
|
||||
- **Plugin Runtimes**: Plugins use validated manifests, approved permissions, persisted config, safe path checks, declarative timer-triggered actions, or sandboxed JavaScript entry modules through the SDK bridge.
|
||||
|
||||
## Flow
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ main.ts
|
|||
├── lifecycle.ts (app events, cleanup)
|
||||
├── logger.ts (structured logging init)
|
||||
├── app-state.ts (state init)
|
||||
├── plugin-service.ts (plugin state/runtime init)
|
||||
├── plugin-service.ts (plugin state/runtime init, JS host wiring)
|
||||
├── tray.ts (tray creation)
|
||||
├── local-ipc.ts (IPC server start)
|
||||
└── windows.ts (UI handlers)
|
||||
|
|
@ -78,17 +78,28 @@ pet-installation.ts
|
|||
└── importCodexPet() → codex-pets.ts
|
||||
```
|
||||
|
||||
**Control Center Flow**:
|
||||
```
|
||||
tray.ts → openControlCenterWindow(route) → windows.ts
|
||||
├── hardened BrowserWindow loads Vite renderer or packaged dist/renderer/index.html
|
||||
├── control-center-preload.cjs exposes page-specific APIs
|
||||
├── Dashboard snapshot: default pet, catalog, plugin health, update status, activity
|
||||
└── renderer/src/main.tsx routes Dashboard/Pets/Integrations/Plugins/Settings
|
||||
```
|
||||
|
||||
**Plugin Flow**:
|
||||
```
|
||||
main.ts → initializePluginService(userData, defaultPluginPetApi).start()
|
||||
main.ts → initializePluginService(userData, defaultPluginPetApi, appVersion, ElectronPluginJsHost).start()
|
||||
├── plugin-state.ts reads/writes userData/openpets-plugin-state.json
|
||||
├── plugin-runtime.ts reloads enabled manifests and schedules timer triggers
|
||||
│ └── plugin-pet-api.ts → default-pet-controller external say/react APIs
|
||||
├── plugin-runtime.ts reloads enabled manifests
|
||||
│ ├── declarative runtime schedules timer triggers
|
||||
│ ├── plugin-js-host.ts starts hidden sandboxed BrowserWindow hosts for JavaScript plugins
|
||||
│ └── plugin-sdk-bridge.ts → plugin-pet-api.ts/default-pet-controller plus schedules, storage, commands, status, logs, and restricted network
|
||||
├── plugin-service.ts orchestrates UI actions, permission confirmation, config validation, install/update/uninstall/load-local, and runtime reloads
|
||||
└── lifecycle.ts → stopPluginService() on quit
|
||||
|
||||
Plugins window:
|
||||
tray.ts → openTaskWindow("plugins") → windows.ts → plugins-window.ts
|
||||
Control Center plugins route:
|
||||
tray.ts → openControlCenterWindow("plugins") → windows.ts → renderer React app
|
||||
└── openpets:plugins-* IPC handlers call PluginService methods
|
||||
|
||||
Catalog install/update:
|
||||
|
|
@ -102,13 +113,13 @@ plugin-local-loader.ts validates selected folder manifest and snapshots only ope
|
|||
## Integration Points
|
||||
|
||||
- **Within src/**:
|
||||
- `main.ts` → all modules (orchestrator)
|
||||
- `main.ts` → all modules (orchestrator), including `ElectronPluginJsHost` for JavaScript plugins
|
||||
- `local-ipc.ts` ↔ `lease-manager.ts` ↔ `agent-pet-controller.ts`
|
||||
- `windows.ts` ↔ `app-state.ts`, `agent-setup.ts`, `catalog.ts`, `codex-pets.ts`
|
||||
- `windows.ts` ↔ `plugin-service.ts`, `plugins-window.ts` for plugin UI IPC and HTML
|
||||
- `windows.ts` ↔ `app-state.ts`, `agent-setup.ts`, `catalog.ts`, `codex-pets.ts`, `update-checker.ts` for Control Center route snapshots/actions
|
||||
- `windows.ts` ↔ `plugin-service.ts` for Control Center plugin UI IPC, plugin commands, and Dashboard plugin health
|
||||
- `pet-window.ts` ↔ `default-pet-controller.ts`, `agent-pet-controller.ts`
|
||||
- `pet-installation.ts` ↔ `app-state.ts`, `catalog.ts`, `zip-safety.ts`
|
||||
- `plugin-service.ts` ↔ `plugin-state.ts`, `plugin-runtime.ts`, `plugin-catalog.ts`, `plugin-package.ts`, `plugin-local-loader.ts`
|
||||
- `plugin-service.ts` ↔ `plugin-state.ts`, `plugin-runtime.ts`, `plugin-catalog.ts`, `plugin-package.ts`, `plugin-local-loader.ts`, `plugin-js-host.ts`, `plugin-sdk-bridge.ts`
|
||||
|
||||
- **To packages/**:
|
||||
- `@open-pets/claude`: `buildClaudeMcpPreview`, `installClaudeHooks`, `doctorClaudeHooks`, etc.
|
||||
|
|
@ -117,14 +128,14 @@ plugin-local-loader.ts validates selected folder manifest and snapshots only ope
|
|||
- `@open-pets/cli`: Version lookup for bundled mode
|
||||
|
||||
- **To System**:
|
||||
- File system: `app.getPath("userData")`, `userData/plugins/`, `userData/plugins-dev/`, `~/.codex/pets/`, `~/.claude/`, `~/.opencode/`
|
||||
- File system: `app.getPath("userData")`, `userData/plugins/`, `userData/plugins-dev/`, plugin storage JSON, `~/.codex/pets/`, `~/.claude/`, `~/.opencode/`
|
||||
- Network: `fetch()` to openpets.dev, GitHub API, plugin catalog at `https://openpets.dev/plugins/catalog.v1.json`, plugin ZIPs restricted to `https://zip.openpets.dev/plugins/`
|
||||
- Processes: `spawn()` for `claude`, `opencode`, `node`
|
||||
|
||||
## Key Modules
|
||||
|
||||
**Core**:
|
||||
- `main.ts`: Entry, single-instance lock, bootstrap sequence
|
||||
- `main.ts`: Entry, single-instance lock, bootstrap sequence, JavaScript plugin host construction
|
||||
- `lifecycle.ts`: App event handlers (quit, window-all-closed, second-instance) with logging; stops plugin service, IPC, and pet windows on quit
|
||||
- `state.ts`: Simple shell pause state
|
||||
- `app-state.ts`: Persistent JSON state with V1 schema, atomic writes, reaction animation overrides
|
||||
|
|
@ -132,11 +143,11 @@ plugin-local-loader.ts validates selected folder manifest and snapshots only ope
|
|||
- `logger.ts`: Structured logging with scopes (app, ipc, lease, pet.default, pet.agent, pet.window, state, tray, ui), log rotation, redaction
|
||||
|
||||
**UI**:
|
||||
- `tray.ts`: Tray icon (nativeImage), context menu builder, update status integration, plugins entry, logs folder
|
||||
- `windows.ts`: BrowserWindow factory, IPC handler registration, HTML generators for task windows, reaction animation settings, plugin UI IPC endpoints
|
||||
- `tray.ts`: Tray icon (nativeImage), context menu builder, update status integration, route-targeted Control Center entries, logs folder
|
||||
- `windows.ts`: Control Center BrowserWindow factory, Dashboard snapshot, IPC handler registration, route targeting, reaction animation settings, plugin/integration/pet/settings UI IPC endpoints, and scoped internal protocols
|
||||
- `assets.ts`: Tray icon loading with generated fallback
|
||||
- `display.ts`: Screen geometry helpers, pet window positioning
|
||||
- `plugins-window.ts`: Sandboxed data-URL HTML/CSS shell for Installed/Discover/Developer plugin management.
|
||||
- `renderer/`: Vite React/Tailwind Control Center shell for Dashboard, Pets, Integrations, Plugins, and Settings.
|
||||
|
||||
**Pets**:
|
||||
- `pet-window.ts`: Window creation (transparent, frameless, always-on-top), HTML/CSS generation, sprite animation states, speech bubbles, status badges, transient displays
|
||||
|
|
@ -162,17 +173,19 @@ plugin-local-loader.ts validates selected folder manifest and snapshots only ope
|
|||
- `zip-safety.ts`: ZIP entry path validation (traversal prevention, case collision detection)
|
||||
|
||||
**Plugins**:
|
||||
- `plugin-manifest.ts`: Manifest V1 schema/types and validation for declarative runtime, permissions (`timer`, `pet:speak`, `pet:reaction`), config schema, timer triggers, and pet actions.
|
||||
- `plugin-manifest.ts`: Manifest V1/V2 schema/types and validation for declarative and JavaScript runtimes, permissions (`timer`/`schedule`, `pet:speak`, `pet:reaction`, `storage`, `status`, `commands`, `network`), config schema, timer triggers, entry files, and pet actions.
|
||||
- `plugin-manifest-reader.ts`: Safe manifest reader with realpath/allowed-root checks, root filename enforcement, size limit, and expected id/version matching.
|
||||
- `plugin-config.ts`: Config defaulting, replacement validation, and runtime resolution for string/number config references.
|
||||
- `plugin-state.ts`: Persistent plugin state store (`openpets-plugin-state.json`) with atomic temp+rename writes, normalized records, approved permissions, config, source, and broken reason.
|
||||
- `plugin-runtime.ts`: Declarative runtime that compiles enabled plugin timer triggers, verifies approved permissions, validates say/react payloads, schedules cancellable timers, and marks broken plugins on validation/action failure.
|
||||
- `plugin-runtime.ts`: Runtime that compiles enabled declarative timer triggers, starts/stops JavaScript plugin hosts, verifies approved permissions, exposes public command/status state, validates actions, schedules cancellable timers, and marks broken plugins on validation/action failure.
|
||||
- `plugin-pet-api.ts`: Narrow adapter from plugin actions to default pet external `say`/`react` controller calls.
|
||||
- `plugin-service.ts`: Application-facing plugin orchestrator for snapshots, enable/disable, config save, reload, catalog install/update, local load, uninstall, permission prompts, compatibility checks, and runtime reloads.
|
||||
- `plugin-service.ts`: Application-facing plugin orchestrator for safe snapshots, enable/disable, config save, command execution, reload, catalog install/update, local load, uninstall, permission prompts, compatibility checks, JavaScript host/SDK bridge integration, and runtime reloads.
|
||||
- `plugin-catalog.ts`: Remote plugin catalog fetch with timeout, redirect rejection, response size cap, cache, and refresh support.
|
||||
- `plugin-catalog-validation.ts`: Catalog V1 schema validation, duplicate id checks, semver/SHA fields, permissions canonicalization, and optional minimum OpenPets version.
|
||||
- `plugin-package.ts`: Catalog plugin package download/install with HTTPS host/path allowlist, SHA-256 verification, ZIP size/entry restrictions, manifest/catalog consistency checks, and safe uninstall path resolution.
|
||||
- `plugin-local-loader.ts`: Developer loader that validates a selected local folder and snapshots only the manifest into `plugins-dev` with symlink/path/size protections.
|
||||
- `plugin-js-host.ts`: Sandboxed hidden BrowserWindow host for JavaScript plugin entry modules with per-plugin session partitioning, navigation/window-open hardening, SDK IPC tokening, registration handshake, config listener cleanup, and teardown.
|
||||
- `plugin-sdk-bridge.ts`: Permission-checked JavaScript plugin SDK for pet speech/reactions, one-shot/repeating/daily schedules, storage with quotas, config listeners, commands, status, logs, and HTTPS-only public-host fetch.
|
||||
|
||||
**Agent Integration**:
|
||||
- `agent-setup.ts`: Claude/OpenCode/Cursor detection, MCP configuration, hooks management, action journaling
|
||||
|
|
@ -200,5 +213,6 @@ plugin-local-loader.ts validates selected folder manifest and snapshots only ope
|
|||
| Plugin catalog | `plugin-catalog.ts`/`plugin-service.ts` | Discoverable plugin metadata filtered by app version and install state |
|
||||
| Plugin ZIP/local folder | `plugin-package.ts`/`plugin-local-loader.ts` | Validated manifest snapshot installed under `userData/plugins*` |
|
||||
| `plugin-state.ts` | `userData/openpets-plugin-state.json` | Installed plugins, enabled flag, approved permissions, config, broken status |
|
||||
| `plugin-runtime.ts` | `plugin-pet-api.ts` | Timer-triggered pet speech/reactions on the default pet |
|
||||
| Plugins renderer | `windows.ts`/`plugin-service.ts` | Snapshot, enable, config, reload, install/update/uninstall, local-load operations |
|
||||
| Control Center renderer | `control-center-preload.cjs`/`windows.ts` | Narrow Dashboard/Pets/Integrations/Plugins/Settings snapshots and route-targeted actions |
|
||||
| `plugin-runtime.ts` | `plugin-pet-api.ts`/`plugin-js-host.ts`/`plugin-sdk-bridge.ts` | Declarative timers and JavaScript SDK actions on default pet, schedules, storage, commands, status, logs, and network |
|
||||
| Plugins renderer | `windows.ts`/`plugin-service.ts` | Snapshot, enable, config, command, reload, install/update/uninstall, local-load operations |
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto";
|
|||
import net from "node:net";
|
||||
|
||||
import { applyAgentPetReaction, applyAgentPetSay, clearAgentPetLeaseState, showAgentPet } from "./agent-pet-controller.js";
|
||||
import { getAppStateSnapshot } from "./app-state.js";
|
||||
import { getAppStateSnapshot, recordOpenPetsActivity } from "./app-state.js";
|
||||
import { builtInPet } from "./built-in-pet.js";
|
||||
import { applyExternalPetReaction, applyExternalPetSay, getDefaultPetPaused, isDefaultPetVisible } from "./default-pet-controller.js";
|
||||
import { createStaleLeaseStatus, LeaseManager } from "./lease-manager.js";
|
||||
|
|
@ -306,13 +306,16 @@ async function handleRequest(request: OpenPetsIpcRequest): Promise<unknown> {
|
|||
const params = isRecord(request.params) ? request.params : {};
|
||||
const reaction = validateReaction(params.reaction);
|
||||
const lease = getLeaseTarget(params.leaseId);
|
||||
const petId = lease?.actualTargetPetId ?? getCurrentDefaultPet().id;
|
||||
debug("ipc", "pet react requested", { requestId: request.id, reaction, leaseId: lease?.leaseId, targetKind: lease?.targetKind, actualPetId: lease?.actualTargetPetId });
|
||||
if (lease?.targetKind === "explicit") {
|
||||
if (getDefaultPetPaused()) return { ok: true, reaction, shown: false, reason: "paused", leaseId: lease.leaseId };
|
||||
const applied = applyAgentPetReaction(lease.actualTargetPetId, reaction);
|
||||
safeRecordOpenPetsActivity({ kind: "react", reaction, petId });
|
||||
return { ok: true, reaction, shown: applied.shown, reason: applied.reason, leaseId: lease.leaseId };
|
||||
}
|
||||
const applied = applyExternalPetReaction(reaction);
|
||||
safeRecordOpenPetsActivity({ kind: "react", reaction, petId });
|
||||
return { ok: true, reaction, shown: applied.shown, reason: applied.reason };
|
||||
}
|
||||
|
||||
|
|
@ -320,16 +323,27 @@ async function handleRequest(request: OpenPetsIpcRequest): Promise<unknown> {
|
|||
const message = validateSayMessage(params.message);
|
||||
const reaction = params.reaction === undefined ? undefined : validateReaction(params.reaction);
|
||||
const lease = getLeaseTarget(params.leaseId);
|
||||
const petId = lease?.actualTargetPetId ?? getCurrentDefaultPet().id;
|
||||
debug("ipc", "pet say requested", { requestId: request.id, reaction, messageLength: message.length, leaseId: lease?.leaseId, targetKind: lease?.targetKind, actualPetId: lease?.actualTargetPetId });
|
||||
if (lease?.targetKind === "explicit") {
|
||||
if (getDefaultPetPaused()) return { ok: true, shown: false, reason: "paused", reaction, leaseId: lease.leaseId };
|
||||
const applied = applyAgentPetSay(lease.actualTargetPetId, message, reaction);
|
||||
safeRecordOpenPetsActivity({ kind: "say", reaction, petId });
|
||||
return { ok: true, shown: applied.shown, reason: applied.reason, reaction, leaseId: lease.leaseId };
|
||||
}
|
||||
const applied = applyExternalPetSay(message, reaction);
|
||||
safeRecordOpenPetsActivity({ kind: "say", reaction, petId });
|
||||
return { ok: true, shown: applied.shown, reason: applied.reason, reaction };
|
||||
}
|
||||
|
||||
function safeRecordOpenPetsActivity(activity: Parameters<typeof recordOpenPetsActivity>[0]): void {
|
||||
try {
|
||||
recordOpenPetsActivity(activity);
|
||||
} catch (error) {
|
||||
debug("ipc", "activity record failed", { error: error instanceof Error ? error.message : String(error), kind: activity.kind, reaction: activity.reaction, petId: activity.petId });
|
||||
}
|
||||
}
|
||||
|
||||
function validateRequiredLeaseId(value: unknown): string {
|
||||
const leaseId = validateOptionalLeaseId(value);
|
||||
if (!leaseId) throw new IpcProtocolError("invalid_params", "Lease id is required.");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { app } from "electron";
|
||||
import { delimiter, resolve } from "node:path";
|
||||
|
||||
import { initializeAppState, isOnboardingCompleted, releaseStartupInstallLock } from "./app-state.js";
|
||||
import { initializeAppState, releaseStartupInstallLock } from "./app-state.js";
|
||||
import { installDefaultPetDisplayHandlers, shouldOpenDefaultPetOnLaunch, showDefaultPet } from "./default-pet-controller.js";
|
||||
import { installAppLifecycle } from "./lifecycle.js";
|
||||
import { debug, error as logError, getLogFilePath, info, initializeLogger, warn } from "./logger.js";
|
||||
|
|
@ -11,7 +11,7 @@ import { ElectronPluginJsHost } from "./plugin-js-host.js";
|
|||
import { initializePluginService } from "./plugin-service.js";
|
||||
import { createAppTray, refreshTrayMenu } from "./tray.js";
|
||||
import { checkForGitHubReleaseUpdate } from "./update-checker.js";
|
||||
import { installInternalUiHandlers, installInternalUiProtocol, openTaskWindow } from "./windows.js";
|
||||
import { installInternalUiHandlers, installInternalUiProtocol } from "./windows.js";
|
||||
|
||||
// OpenPets does not store browser passwords, cookies, or encrypted app secrets.
|
||||
// Keep Chromium/Electron from prompting for macOS Keychain or Linux keyring access
|
||||
|
|
@ -51,22 +51,16 @@ if (!gotSingleInstanceLock) {
|
|||
installDefaultPetDisplayHandlers();
|
||||
await startLocalIpcServer();
|
||||
releaseStartupInstallLock();
|
||||
const roots = parseDevPluginEnv(process.env.OPENPETS_DEV_PLUGIN_ROOTS);
|
||||
const paths = parseDevPluginEnv(process.env.OPENPETS_DEV_PLUGIN_PATHS);
|
||||
const devPluginMode = roots.length > 0 || paths.length > 0;
|
||||
const pluginService = initializePluginService(app.getPath("userData"), defaultPluginPetApi, app.getVersion(), new ElectronPluginJsHost(), writePluginRuntimeLog, process.env.OPENPETS_DISABLE_PLUGIN_CATALOG === "1" || devPluginMode);
|
||||
if (shouldOpenDefaultPetOnLaunch()) {
|
||||
showDefaultPet();
|
||||
}
|
||||
if (!isOnboardingCompleted()) {
|
||||
try {
|
||||
openTaskWindow("onboarding");
|
||||
} catch (error) {
|
||||
console.error("Failed to open OpenPets onboarding; continuing with tray app.", error);
|
||||
}
|
||||
}
|
||||
refreshTrayMenu();
|
||||
void (async () => {
|
||||
const roots = parseDevPluginEnv(process.env.OPENPETS_DEV_PLUGIN_ROOTS);
|
||||
const paths = parseDevPluginEnv(process.env.OPENPETS_DEV_PLUGIN_PATHS);
|
||||
const devPluginMode = roots.length > 0 || paths.length > 0;
|
||||
const service = initializePluginService(app.getPath("userData"), defaultPluginPetApi, app.getVersion(), new ElectronPluginJsHost(), writePluginRuntimeLog, process.env.OPENPETS_DISABLE_PLUGIN_CATALOG === "1" || devPluginMode);
|
||||
const service = pluginService;
|
||||
await service.start();
|
||||
for (const path of paths) {
|
||||
const result = await service.loadLocalPath(path, { autoApprove: true });
|
||||
|
|
@ -78,7 +72,7 @@ if (!gotSingleInstanceLock) {
|
|||
}
|
||||
})().catch((error) => logError("app", "plugin service startup failed", error));
|
||||
void checkForGitHubReleaseUpdate().then(() => refreshTrayMenu());
|
||||
info("app", "startup complete", { logFile: getLogFilePath(), openDefaultPetOnLaunch: shouldOpenDefaultPetOnLaunch(), onboardingCompleted: isOnboardingCompleted() });
|
||||
info("app", "startup complete", { logFile: getLogFilePath(), openDefaultPetOnLaunch: shouldOpenDefaultPetOnLaunch() });
|
||||
console.log("OpenPets desktop shell ready.");
|
||||
}).catch((error: unknown) => {
|
||||
releaseStartupInstallLock();
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ export class PluginService {
|
|||
const record = this.stateStore.getRecord(id);
|
||||
if (!record) return this.#error("Plugin is not installed.");
|
||||
if (enabled && record.catalogDisabled) return this.#error("Plugin is disabled in the catalog.");
|
||||
if (enabled && record.brokenReason) return this.#error(record.brokenReason);
|
||||
this.stateStore.setEnabled(id, enabled);
|
||||
await this.runtime.reloadPlugin(id);
|
||||
return { ok: true, snapshot: await this.getSnapshot() };
|
||||
|
|
|
|||
|
|
@ -1,189 +0,0 @@
|
|||
export type PluginsWindowDefinition = {
|
||||
readonly title: string;
|
||||
readonly heading: string;
|
||||
readonly description: string;
|
||||
};
|
||||
|
||||
export function createPluginsHtml(definition: PluginsWindowDefinition): string {
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'; frame-src 'none'" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>${escapeHtml(definition.title)}</title>
|
||||
<style>
|
||||
:root { color-scheme: light; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
||||
body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 12% 8%, rgba(219, 234, 254, 0.9), transparent 24%), linear-gradient(180deg, #f8fbff 0%, #eff7ff 54%, #e9f3ff 100%); color: #102149; overflow-y: scroll; }
|
||||
main { width: min(1040px, calc(100vw - 48px)); margin: 0 auto; padding: 48px 0 64px; }
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3 { margin: 0; font-weight: 600; tracking: -0.02em; text-wrap: balance; }
|
||||
h1 { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 32px; line-height: 1; letter-spacing: -0.05em; color: #102149; margin-bottom: 8px; }
|
||||
h2 { font-size: 20px; margin-bottom: 12px; color: #102149; }
|
||||
h3 { font-size: 16px; margin-bottom: 16px; color: #102149; }
|
||||
p { color: #63708f; line-height: 1.6; margin: 0; text-wrap: pretty; }
|
||||
.muted { color: #63708f; font-size: 14px; }
|
||||
|
||||
/* Header */
|
||||
header { display: flex; justify-content: space-between; gap: 24px; align-items: flex-start; margin-bottom: 32px; }
|
||||
|
||||
/* Buttons */
|
||||
button, input, select, textarea { font: inherit; box-sizing: border-box; }
|
||||
button {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
||||
min-height: 38px; padding: 0 12px; border-radius: 11px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 12px; font-weight: 950;
|
||||
cursor: pointer; user-select: none;
|
||||
border: 1px solid rgba(37, 99, 235, 0.34); background: rgba(255,255,255,0.76); color: #176df2;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.9), 0 8px 18px rgba(61, 99, 160, 0.08);
|
||||
transition: transform 0.1s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
button:hover:not(:disabled) { transform: translateY(-1px); }
|
||||
button:active:not(:disabled) { transform: scale(0.96); }
|
||||
button:disabled { cursor: default; opacity: 0.54; transform: none; }
|
||||
|
||||
button.primary { background: linear-gradient(180deg, #3b96ff, #176df2); color: #fff; box-shadow: 0 14px 28px rgba(37, 99, 235, 0.24), inset 0 1px 0 rgba(255,255,255,0.38); border-color: transparent; }
|
||||
button.primary:hover:not(:disabled) { background: linear-gradient(180deg, #55a6ff, #176df2); }
|
||||
|
||||
button.secondary { background: rgba(255,255,255,0.76); color: #176df2; border-color: rgba(37, 99, 235, 0.42); box-shadow: inset 0 1px 0 rgba(255,255,255,0.9), 0 8px 18px rgba(61, 99, 160, 0.08); }
|
||||
button.secondary:hover:not(:disabled) { background: rgba(255,255,255,0.95); border-color: rgba(37, 99, 235, 0.55); box-shadow: inset 0 1px 0 rgba(255,255,255,1), 0 8px 18px rgba(61, 99, 160, 0.12); }
|
||||
|
||||
button.compact { min-height: 32px; padding: 0 10px; font-size: 11px; border-radius: 9px; }
|
||||
button.danger { color: #dc2626; border-color: rgba(220, 38, 38, 0.3); }
|
||||
|
||||
/* Layout & Views */
|
||||
.view-section { display: none; }
|
||||
.view-section.active { display: block; }
|
||||
|
||||
/* Plugin Hub Grid */
|
||||
.plugin-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; margin-top: 24px; }
|
||||
|
||||
/* Cards */
|
||||
.plugin-card { min-height: 248px; box-sizing: border-box; display: flex; flex-direction: column; gap: 9px; border: 1px solid rgba(126, 161, 210, 0.44); border-radius: 20px; background: rgba(255,255,255,0.76); box-shadow: 0 16px 38px rgba(61, 99, 160, 0.1), inset 0 1px 0 rgba(255,255,255,0.94); padding: 16px; }
|
||||
.plugin-card.featured { border-color: rgba(37, 99, 235, 0.36); background: linear-gradient(180deg, rgba(239, 247, 255, 0.92), rgba(255,255,255,0.78)); }
|
||||
.plugin-card.disabled { opacity: 0.74; filter: grayscale(0.5); }
|
||||
.plugin-card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
||||
.plugin-icon { width: 48px; height: 48px; display: grid; place-items: center; border: 1px solid rgba(126, 161, 210, 0.34); border-radius: 16px; background: rgba(255,255,255,0.76); box-shadow: inset 0 1px 0 rgba(255,255,255,0.9), 0 10px 20px rgba(61, 99, 160, 0.08); font-size: 24px; }
|
||||
.plugin-icon img, .plugin-icon svg { width: 28px; height: 28px; object-fit: contain; }
|
||||
.plugin-card h2 { margin: 0; font-size: 20px; color: #102149; }
|
||||
.plugin-card p { flex: 1 1 auto; margin: 0; color: #526483; line-height: 1.35; font-size: 14px; }
|
||||
|
||||
/* Status Pills */
|
||||
.status-pill { height: 30px; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; border: 1px solid rgba(126, 161, 210, 0.36); border-radius: 10px; padding: 0 10px; background: rgba(255,255,255,0.68); color: #526483; font-size: 10px; font-weight: 900; white-space: nowrap; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.status-pill.success { color: #047857; background: rgba(236, 253, 245, 0.86); border-color: rgba(16, 185, 129, 0.28); }
|
||||
.status-pill.info { color: #176df2; background: rgba(239, 246, 255, 0.9); border-color: rgba(37, 99, 235, 0.28); }
|
||||
.status-pill.error { color: #b91c1c; background: rgba(254, 242, 242, 0.9); border-color: rgba(248, 113, 113, 0.32); }
|
||||
.status-pill.muted { color: #64748b; background: rgba(248, 250, 252, 0.82); }
|
||||
|
||||
/* Actions */
|
||||
.plugin-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: auto; }
|
||||
.plugin-actions.stacked { grid-template-columns: 1fr; gap: 8px; }
|
||||
.plugin-actions button:only-child { grid-column: 1 / -1; }
|
||||
|
||||
/* Detail View */
|
||||
.detail-toolbar { display: flex; justify-content: flex-start; margin-bottom: 24px; padding: 4px 2px 0; }
|
||||
.detail-pane { max-width: 760px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px; }
|
||||
.detail-header { display: flex; align-items: flex-start; gap: 20px; margin-bottom: 16px; }
|
||||
.detail-header-text { flex: 1 1 auto; }
|
||||
.detail-header-text .eyebrow { margin: 0 0 4px; color: #2478ff; font-weight: 900; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
|
||||
/* Config Sections */
|
||||
.config-card { box-sizing: border-box; border: 1px solid rgba(126, 161, 210, 0.44); border-radius: 20px; background: rgba(255,255,255,0.76); box-shadow: 0 16px 38px rgba(61, 99, 160, 0.1), inset 0 1px 0 rgba(255,255,255,0.94); padding: 20px; display: flex; flex-direction: column; gap: 16px; }
|
||||
.config-section-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 4px; }
|
||||
.config-section-header span:first-child { display: grid; gap: 4px; }
|
||||
.config-section-header small { color: #2478ff; font-size: 11px; font-weight: 900; letter-spacing: 0.1em; text-transform: uppercase; }
|
||||
.config-section-header strong { color: #102149; font-size: 18px; line-height: 1.2; }
|
||||
|
||||
/* Form Controls */
|
||||
.form-group { display: flex; flex-direction: column; gap: 6px; }
|
||||
.form-group label { color: #102149; font-weight: 700; font-size: 14px; }
|
||||
.form-group .help-text { color: #63708f; font-size: 12px; margin-bottom: 4px; }
|
||||
.form-input { width: 100%; box-sizing: border-box; min-height: 38px; border: 1px solid rgba(126, 161, 210, 0.54); border-radius: 11px; background: rgba(255,255,255,0.82); color: #17284f; padding: 0 12px; outline: none; }
|
||||
.form-input:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); }
|
||||
textarea.form-input { padding: 10px 12px; min-height: 80px; resize: vertical; font-family: inherit; }
|
||||
|
||||
/* Toggle Row */
|
||||
.toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 0; }
|
||||
.toggle-row-text { display: flex; flex-direction: column; gap: 2px; }
|
||||
.toggle-row-text strong { color: #102149; font-size: 14px; }
|
||||
.toggle-row-text small { color: #63708f; font-size: 12px; }
|
||||
|
||||
/* CSS Toggle Switch */
|
||||
.toggle-switch { position: relative; width: 44px; height: 24px; display: inline-block; }
|
||||
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
||||
.toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #cbd5e1; transition: .2s; border-radius: 24px; }
|
||||
.toggle-slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 2px; bottom: 2px; background-color: white; transition: .2s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
input:checked + .toggle-slider { background-color: #10b981; }
|
||||
input:focus + .toggle-slider { box-shadow: 0 0 1px #10b981; }
|
||||
input:checked + .toggle-slider:before { transform: translateX(20px); }
|
||||
|
||||
/* Chips / MultiSelect */
|
||||
.chips-container { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.chip-label { display: inline-flex; align-items: center; cursor: pointer; }
|
||||
.chip-label input { position: absolute; opacity: 0; width: 0; height: 0; }
|
||||
.chip-text { display: inline-flex; align-items: center; justify-content: center; min-height: 32px; padding: 0 12px; border-radius: 16px; border: 1px solid rgba(126, 161, 210, 0.44); background: rgba(255,255,255,0.76); color: #526483; font-size: 13px; font-weight: 600; transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; user-select: none; }
|
||||
.chip-label input:checked + .chip-text { background: rgba(239, 246, 255, 0.9); border-color: rgba(37, 99, 235, 0.5); color: #176df2; box-shadow: inset 0 1px 0 rgba(255,255,255,0.9); }
|
||||
|
||||
/* List Editor */
|
||||
.list-editor { display: flex; flex-direction: column; gap: 12px; }
|
||||
.list-item-card { border: 1px solid rgba(126, 161, 210, 0.3); border-radius: 16px; background: rgba(255,255,255,0.5); padding: 16px; position: relative; display: flex; flex-direction: column; gap: 12px; }
|
||||
.list-item-remove { position: absolute; top: 12px; right: 12px; background: none; border: none; box-shadow: none; color: #94a3b8; padding: 4px; min-height: auto; border-radius: 6px; }
|
||||
.list-item-remove:hover { background: rgba(254, 242, 242, 0.8); color: #ef4444; }
|
||||
|
||||
/* Dev Mode Toggle */
|
||||
.dev-toggle-container { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #64748b; margin-top: 24px; justify-content: flex-end; }
|
||||
|
||||
/* Spinner */
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.loading-icon { animation: spin 0.85s linear infinite; }
|
||||
|
||||
/* Messages */
|
||||
.empty-state { text-align: center; padding: 48px; color: #64748b; }
|
||||
</style>
|
||||
</head>
|
||||
<body data-openpets-view="plugins">
|
||||
<main>
|
||||
<header>
|
||||
<div>
|
||||
<h1>${escapeHtml(definition.heading)}</h1>
|
||||
<p>${escapeHtml(definition.description)}</p>
|
||||
</div>
|
||||
<button id="plugins-refresh" type="button" class="secondary">Refresh</button>
|
||||
</header>
|
||||
|
||||
<section id="plugins-status" class="muted" aria-live="polite">Loading plugins…</section>
|
||||
|
||||
<div id="hub-view" class="view-section active">
|
||||
<div class="plugin-grid" id="plugins-grid">
|
||||
<!-- Cards rendered by preload -->
|
||||
</div>
|
||||
|
||||
<div class="dev-toggle-container">
|
||||
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
|
||||
<input type="checkbox" id="dev-mode-toggle" />
|
||||
Developer mode
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="dev-actions" style="display: none; margin-top: 16px; text-align: right;">
|
||||
<button id="plugins-load-local" class="secondary">Load local plugin folder</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="detail-view" class="view-section">
|
||||
<div class="detail-toolbar">
|
||||
<button id="back-to-hub" class="secondary compact">Back to plugins</button>
|
||||
</div>
|
||||
<div class="detail-pane" id="plugin-detail-content">
|
||||
<!-- Detail content rendered by preload -->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
||||
}
|
||||
18
apps/desktop/src/renderer/codemap.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# apps/desktop/src/renderer/
|
||||
|
||||
## Responsibility
|
||||
|
||||
Vite renderer workspace for the desktop React/Tailwind Control Center. It owns the packaged `index.html` entry and delegates UI implementation to `src/`, while Electron main-process code owns windows, IPC handlers, and service logic.
|
||||
|
||||
## Design
|
||||
|
||||
- Built as a sandboxed Electron renderer loaded by the singleton Control Center BrowserWindow.
|
||||
- Uses `control-center-preload.cjs` as the only bridge to the main process; `ipcRenderer` is not exposed directly.
|
||||
- Production output is emitted to `apps/desktop/dist/renderer/`; development can load a loopback-only Vite URL.
|
||||
- Transparent pet windows and plugin JavaScript host windows are separate lightweight renderers and do not live here.
|
||||
|
||||
## Key Files
|
||||
|
||||
- `index.html`: Vite HTML entry mounting the React app.
|
||||
- `src/main.tsx`: Route shell and pages for Dashboard, Pets, Integrations, Plugins, and Settings.
|
||||
- `src/styles.css`: Tailwind directives plus Control Center component styling.
|
||||
13
apps/desktop/src/renderer/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data: https://openpets.dev openpets-codex: openpets-pet-preview:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-src 'none'" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OpenPets Control Center</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
21
apps/desktop/src/renderer/src/codemap.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# apps/desktop/src/renderer/src/
|
||||
|
||||
## Responsibility
|
||||
|
||||
React/Tailwind source for the Control Center management UI. This renderer presents dashboard status, pet management, coding-agent integrations, plugin management, and settings using narrow preload APIs backed by `windows.ts` IPC handlers and desktop services.
|
||||
|
||||
## Design
|
||||
|
||||
- **Route Shell**: In-renderer route state supports `dashboard`, `pets`, `integrations`, `plugins`, and `settings`; tray actions retarget the singleton window through route-change events.
|
||||
- **Dashboard**: Reads a narrowed dashboard snapshot for default pet preview, install/catalog counts, plugin health, update status, and activity totals.
|
||||
- **Pets**: Combines installed pets, catalog v3 pages/search, Codex imports, filters, detail panes, set-default/install/import/remove actions, and animated sprite previews.
|
||||
- **Integrations**: Card-first setup UI for Claude Code, OpenCode, Cursor, and Pi guidance, including command mode/path controls and preview/action flows.
|
||||
- **Plugins**: Gallery-first plugin hub for installed/catalog/local/broken filters, catalog refresh, local load, install/update/uninstall, enable/disable, config modal, command execution, runtime/status display, and broken-state feedback.
|
||||
- **Settings**: Startup, launch-at-login, pet scale, reaction-animation mapping, update check, default-pet position reset, and pet reaction previews.
|
||||
- **Bridge Contract**: All data and actions go through `window.openPetsControlCenter`; page snapshots intentionally omit raw install paths and unrelated app state.
|
||||
|
||||
## Key Files
|
||||
|
||||
- `main.tsx`: Single-file React app containing type definitions, route shell, page components, icons, snapshot loading, and action handlers.
|
||||
- `styles.css`: Tailwind base/components/utilities plus glass-card layout, navigation, galleries, modals, status pills, previews, and notifications.
|
||||
- `vite-env.d.ts`: Vite/TypeScript renderer environment declarations.
|
||||
2338
apps/desktop/src/renderer/src/main.tsx
Normal file
268
apps/desktop/src/renderer/src/styles.css
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
body { margin:0; min-height:100vh; overflow:hidden; color:#102149; background: radial-gradient(circle at 12% 8%, rgba(219, 234, 254, 0.9), transparent 24%), linear-gradient(180deg, #f8fbff 0%, #eff7ff 54%, #e9f3ff 100%); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(96, 165, 250, 0.25); border-radius: 100px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(96, 165, 250, 0.45); }
|
||||
}
|
||||
@layer components {
|
||||
.app-shell { @apply mx-auto flex h-screen max-w-[1160px] flex-col p-6; }
|
||||
.hero { @apply mb-6 flex items-center justify-between gap-6; }
|
||||
.hero-content { @apply flex flex-col justify-center; }
|
||||
.hero h1 { @apply m-0 font-monoDisplay text-5xl font-black tracking-tight text-navy leading-none; }
|
||||
.hero-desc { @apply m-0 mt-1.5 min-h-10 max-w-md text-sm font-medium leading-5 text-slatecopy; }
|
||||
.hero-logo-container { @apply flex shrink-0 items-center justify-end; }
|
||||
.hero-brand-logo { @apply h-28 w-auto max-w-[480px] object-contain select-none; filter: drop-shadow(0 6px 12px rgba(61, 99, 160, 0.10)); }
|
||||
.eyebrow { @apply mb-1.5 font-monoDisplay text-xs font-black uppercase tracking-[.18em] text-brand; }
|
||||
.layout { @apply grid min-h-0 flex-1 grid-cols-1 gap-5; }
|
||||
.glass { @apply rounded-[28px] border border-[rgba(126,161,210,.48)] p-5 shadow-glass backdrop-blur-xl; background: rgba(255, 255, 255, 0.76); box-shadow: inset 0 1px 0 rgba(255,255,255,.96), 0 24px 60px rgba(61,99,160,.15); }
|
||||
.gallery { @apply flex min-h-0 flex-col overflow-hidden; }
|
||||
.toolbar { @apply flex items-center gap-3; } .search { @apply min-w-0 flex-1 rounded-2xl border border-blue-300/80 bg-white/80 px-4 py-3 text-navy outline-none shadow-inner transition-shadow duration-150 focus:border-brand focus:bg-white focus:ring-4 focus:ring-brand/15; }
|
||||
.filters { @apply my-3 flex flex-wrap gap-1.5; }
|
||||
.filter { @apply rounded-xl border px-3 py-1.5 font-monoDisplay text-xs font-black uppercase tracking-wide text-slate-600 active:scale-[0.96] transition-[transform,background-color,border-color,box-shadow,translate] duration-150 flex items-center gap-1.5 cursor-pointer select-none h-8 shadow-sm; border-color: rgba(126, 161, 210, 0.4); background: linear-gradient(180deg, rgba(255, 255, 255, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%); box-shadow: inset 0 1px 0 rgba(255,255,255,1), 0 2px 4px rgba(61,99,160,0.06); }
|
||||
.filter:hover:not(.active) { @apply text-brand; border-color: rgba(37, 99, 235, 0.35); background: linear-gradient(180deg, rgba(255, 255, 255, 1) 0%, rgba(239, 246, 255, 0.9) 100%); box-shadow: inset 0 1px 0 rgba(255,255,255,1), 0 2px 4px rgba(61,99,160,0.06); }
|
||||
.filter:active { translate: 0 0; }
|
||||
.filter.active { @apply text-white; border-color: rgba(29, 78, 216, 0.5); background: linear-gradient(180deg, #3b96ff 0%, #176df2 100%); box-shadow: inset 0 1.5px 0 rgba(255,255,255,0.35), 0 2px 4px rgba(61,99,160,0.10); }
|
||||
.filter.active:hover { background: linear-gradient(180deg, #55a6ff 0%, #176df2 100%); }
|
||||
.filter.active:active { translate: 0 0; }
|
||||
.filter.original.active { border-color: rgba(194, 65, 12, 0.5); background: linear-gradient(180deg, #fb923c 0%, #ea580c 100%); box-shadow: inset 0 1.5px 0 rgba(255,255,255,0.35), 0 2px 4px rgba(61,99,160,0.10); }
|
||||
.filter.original.active:hover { background: linear-gradient(180deg, #ffaa47 0%, #ea580c 100%); }
|
||||
.filter.featured.active { border-color: rgba(126, 34, 206, 0.5); background: linear-gradient(180deg, #a855f7 0%, #7e22ce 100%); box-shadow: inset 0 1.5px 0 rgba(255,255,255,0.35), 0 2px 4px rgba(61,99,160,0.10); }
|
||||
.filter.featured.active:hover { background: linear-gradient(180deg, #c084fc 0%, #7e22ce 100%); }
|
||||
.filter-icon-wrapper { @apply flex items-center justify-center shrink-0 opacity-70; }
|
||||
.filter.active .filter-icon-wrapper { @apply opacity-100; }
|
||||
.filter-icon { @apply h-3.5 w-3.5 stroke-[2.5]; }
|
||||
.pager { @apply mt-3 flex items-center justify-between gap-2.5 rounded-xl border border-blue-200/40 bg-blue-50/20 p-1.5 text-xs font-bold text-slatecopy; }
|
||||
.pets-grid { @apply grid min-h-0 flex-1 grid-cols-2 gap-3 overflow-y-auto p-1.5 pr-2.5 pb-3; overscroll-behavior: contain; scrollbar-color: rgba(96, 165, 250, 0.42) transparent; }
|
||||
.pet-card { @apply relative flex flex-row items-start gap-3.5 rounded-2xl border border-slate-200 bg-white/80 p-3 text-left shadow-sm transition-[transform,border-color,background-color,box-shadow] hover:-translate-y-0.5 hover:border-brand hover:bg-white min-h-[138px] w-full; }
|
||||
.pet-card.selected { @apply border-brand bg-blue-50/90 ring-4 ring-blue-200/50 shadow-md; }
|
||||
.pet-card-actions { @apply relative z-10 mt-2 grid grid-cols-3 gap-1.5; }
|
||||
.pet-card-actions .btn { @apply min-h-[30px] min-w-0 px-2 py-1 text-[10px] tracking-wide; }
|
||||
.thumb { @apply relative z-10 grid h-14 w-14 shrink-0 place-items-center overflow-hidden rounded-2xl bg-gradient-to-br from-white to-blue-100 border border-blue-100/50; }
|
||||
.thumb img { @apply max-h-12 max-w-12 object-contain; }
|
||||
.card-content { @apply relative z-10 flex flex-col flex-1 min-w-0 justify-center gap-1; }
|
||||
.card-title-row { @apply flex items-center justify-between gap-2; }
|
||||
.card-title { @apply font-semibold text-navy text-sm truncate block; }
|
||||
.card-desc { @apply text-xs text-slatecopy line-clamp-1 m-0; }
|
||||
.badges { @apply flex flex-wrap gap-1 mt-1; }
|
||||
.pill { @apply inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 font-sans text-[11px] font-semibold text-slatecopy/90 border border-transparent select-none; } .pill-blue { @apply bg-blue-50 border border-blue-100/50 text-blue-700; } .pill-green { @apply bg-emerald-50 border border-emerald-100/50 text-emerald-700; } .pill-orange { @apply bg-amber-50 border border-amber-100/50 text-amber-700; } .pill-purple { @apply bg-purple-50 border border-purple-100/60 text-purple-700; } .pill-yellow { @apply bg-yellow-100 border border-yellow-200/70 text-yellow-900; } .pill-red { @apply bg-red-50 border border-red-100/50 text-red-700; } .pill-slate { @apply bg-slate-50 border border-slate-200 text-slate-600; }
|
||||
.detail { @apply flex min-h-0 flex-col overflow-y-auto pr-2; overscroll-behavior: contain; }
|
||||
.detail h2 { @apply m-0 font-monoDisplay text-3xl font-black; } .desc { @apply text-slatecopy; } .stage { @apply my-5 grid min-h-72 place-items-center overflow-hidden rounded-[32px] border border-blue-100 bg-[radial-gradient(circle_at_center,rgba(59,150,255,.18),rgba(255,255,255,.74)_55%,rgba(239,247,255,.9))]; } .stage img { @apply max-h-56 max-w-64 object-contain [image-rendering:auto]; }
|
||||
.sprite-frame { display: block; flex: 0 0 auto; overflow: hidden; image-rendering: auto; }
|
||||
.sprite-frame image { pointer-events: none; user-select: none; }
|
||||
.sprite-thumb { width: 54px; height: 58px; filter: drop-shadow(0 9px 8px rgba(25, 44, 83, 0.16)); }
|
||||
.sprite-detail { width: 144px; height: 156px; filter: drop-shadow(0 18px 18px rgba(25, 44, 83, 0.22)); }
|
||||
.sprite-mini { width: 56px; height: 61px; border-radius: 14px; filter: drop-shadow(0 4px 6px rgba(25, 44, 83, 0.12)); }
|
||||
.sprite-detail.sprite-idle { animation: cc-idle-bob 2.6s ease-in-out infinite; }
|
||||
@keyframes cc-idle-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
|
||||
@media (prefers-reduced-motion: reduce) { .sprite-frame { animation: none !important; } .sprite-frame animate { display: none; } }
|
||||
@media (max-width: 900px) { body { overflow:auto; } .app-shell { @apply h-auto min-h-screen; } .layout, .gallery, .pets-grid, .detail { @apply overflow-visible; } .pets-grid { @apply grid-cols-1; } .pet-detail-content { @apply grid-cols-1; } .pet-preview-grid { @apply grid-cols-1; } }
|
||||
.meta { @apply flex flex-wrap gap-2; }
|
||||
.btn { @apply rounded-2xl px-4 py-3 font-monoDisplay text-sm font-black uppercase tracking-wide disabled:cursor-not-allowed disabled:opacity-60 active:scale-[0.96] transition-[transform,translate,background-color,border-color,box-shadow] duration-150 flex items-center justify-center min-h-[44px] cursor-pointer select-none; }
|
||||
.btn-compact { @apply rounded-xl px-2.5 py-1 text-xs min-h-[30px]; }
|
||||
.btn-primary { border: 1px solid rgba(37, 99, 235, 0.32); background: linear-gradient(180deg, #3b96ff, #176df2); color: #fff; box-shadow: inset 0 1px 0 rgba(255,255,255,0.38), 0 2px 4px rgba(61,99,160,0.10); }
|
||||
.btn-primary:hover:not(:disabled) { background: linear-gradient(180deg, #55a6ff, #176df2); }
|
||||
.btn-secondary { background: rgba(255,255,255,0.76); color: #176df2; border: 1px solid rgba(37, 99, 235, 0.42); box-shadow: inset 0 1px 0 rgba(255,255,255,0.9), 0 2px 4px rgba(61,99,160,0.08); }
|
||||
.btn-secondary:hover:not(:disabled) { background: rgba(255,255,255,0.95); border-color: rgba(37, 99, 235, 0.55); box-shadow: inset 0 1px 0 rgba(255,255,255,1), 0 2px 4px rgba(61,99,160,0.10); }
|
||||
.btn-danger { border: 1px solid rgba(220, 38, 38, 0.32); background: linear-gradient(180deg, #ff6b6b, #dc2626); color: #fff; box-shadow: inset 0 1px 0 rgba(255,255,255,0.38), 0 2px 4px rgba(61,99,160,0.10); }
|
||||
.btn-danger:hover:not(:disabled) { background: linear-gradient(180deg, #ff8787, #dc2626); }
|
||||
.btn-success { border: 1px solid rgba(5, 150, 105, 0.32); background: linear-gradient(180deg, #34d399, #059669); color: #fff; box-shadow: inset 0 1px 0 rgba(255,255,255,0.38), 0 2px 4px rgba(61,99,160,0.10); }
|
||||
.btn-success:hover:not(:disabled) { background: linear-gradient(180deg, #6ee7b7, #059669); }
|
||||
.btn-warning { border: 1px solid rgba(217, 119, 6, 0.32); background: linear-gradient(180deg, #fbbf24, #d97706); color: #fff; box-shadow: inset 0 1px 0 rgba(255,255,255,0.38), 0 2px 4px rgba(61,99,160,0.10); }
|
||||
.btn-warning:hover:not(:disabled) { background: linear-gradient(180deg, #fcd34d, #d97706); }
|
||||
|
||||
.btn-icon-wrapper { @apply flex items-center justify-center shrink-0; }
|
||||
.btn-icon { @apply h-4 w-4 stroke-[2.5]; }
|
||||
.pager-text { @apply text-xs font-bold uppercase tracking-wider text-slatecopy/80; }
|
||||
.error { @apply mb-4 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-red-700; }
|
||||
|
||||
.nav-bar { @apply mb-5 flex items-center gap-1.5 border-b border-blue-200/30 pb-3.5 flex-wrap; }
|
||||
.nav-tab { @apply rounded-xl border px-3.5 py-2 font-monoDisplay text-xs font-black uppercase tracking-wider text-slate-600 active:scale-[0.96] transition-[transform,background-color,border-color,box-shadow] duration-150 flex items-center gap-2 cursor-pointer select-none h-9 shadow-sm; border-color: rgba(126, 161, 210, 0.25); background: linear-gradient(180deg, rgba(255, 255, 255, 0.8) 0%, rgba(241, 245, 249, 0.8) 100%); box-shadow: inset 0 1px 0 rgba(255,255,255,1), 0 2px 4px rgba(61,99,160,0.04); }
|
||||
.nav-tab:hover:not(.active) { @apply text-brand; border-color: rgba(37, 99, 235, 0.3); background: linear-gradient(180deg, rgba(255, 255, 255, 0.95) 0%, rgba(239, 246, 255, 0.8) 100%); box-shadow: inset 0 1px 0 rgba(255,255,255,1), 0 2px 4px rgba(61,99,160,0.06); }
|
||||
.nav-tab.active { @apply text-white; border-color: rgba(29, 78, 216, 0.5); background: linear-gradient(180deg, #3b96ff 0%, #176df2 100%); box-shadow: inset 0 1.5px 0 rgba(255,255,255,0.35), 0 2px 4px rgba(61,99,160,0.10); }
|
||||
.nav-tab.active:hover { background: linear-gradient(180deg, #55a6ff 0%, #176df2 100%); }
|
||||
.nav-icon { @apply h-3.5 w-3.5 stroke-[2.5] opacity-75 shrink-0; }
|
||||
.nav-tab.active .nav-icon { @apply opacity-100; }
|
||||
|
||||
.settings-layout { @apply relative flex flex-col gap-6 overflow-y-auto pr-2 h-full; overscroll-behavior: contain; }
|
||||
.settings-container { @apply grid grid-cols-[280px_1fr] gap-6 min-h-0 flex-1; }
|
||||
.settings-sidebar { @apply flex flex-col gap-2; }
|
||||
.settings-content { @apply flex flex-col gap-6 overflow-y-auto pr-2 pb-8; overscroll-behavior: contain; }
|
||||
.settings-nav-item { @apply flex items-center gap-3 px-4 py-3 rounded-2xl border border-transparent text-sm font-bold text-slatecopy transition-[transform,background-color,border-color,box-shadow,color] duration-150 cursor-pointer select-none; }
|
||||
.settings-nav-item:hover { @apply bg-white/50 text-brand border-blue-100/50; }
|
||||
.settings-nav-item.active { @apply bg-white text-brand border-blue-200 shadow-sm ring-4 ring-blue-100/30; }
|
||||
.settings-nav-icon { @apply w-4 h-4 opacity-70; }
|
||||
.settings-nav-item.active .settings-nav-icon { @apply opacity-100; }
|
||||
|
||||
.settings-section { @apply flex flex-col gap-4; }
|
||||
.settings-section-title { @apply m-0 font-monoDisplay text-2xl font-black text-navy; }
|
||||
|
||||
.settings-group { @apply flex flex-col overflow-hidden rounded-[24px] border border-blue-100/70 bg-white/55 shadow-sm; }
|
||||
.settings-row { @apply flex items-center justify-between gap-4 p-5 transition-colors hover:bg-white/80 border-b border-blue-50 last:border-b-0; }
|
||||
.settings-row-info { @apply flex-1 min-w-0; }
|
||||
.settings-row-info strong { @apply block text-sm font-bold text-navy; }
|
||||
.settings-row-info small { @apply mt-0.5 block text-xs leading-relaxed text-slatecopy; }
|
||||
|
||||
.settings-toggle { @apply h-6 w-11 shrink-0 cursor-pointer accent-brand disabled:cursor-not-allowed; }
|
||||
.settings-select { @apply min-h-10 min-w-[140px] rounded-xl border border-blue-200/80 bg-white/80 px-3 py-2 text-sm font-semibold text-navy outline-none focus:border-brand focus:ring-4 focus:ring-brand/15 disabled:cursor-not-allowed disabled:opacity-60; }
|
||||
|
||||
.settings-system-footer { @apply mt-2 flex items-center justify-between gap-4 p-4 rounded-2xl bg-blue-50/40 border border-blue-100/50; }
|
||||
.settings-system-info { @apply flex items-center gap-3 text-xs font-bold text-slatecopy; }
|
||||
.settings-system-version { @apply px-2 py-0.5 rounded-md bg-blue-100/50 text-brand font-mono; }
|
||||
|
||||
.reaction-grid { @apply grid grid-cols-1 gap-px bg-blue-50; }
|
||||
.reaction-row { @apply flex items-center gap-4 bg-white/80 p-4 transition-colors hover:bg-white; }
|
||||
.reaction-preview-box { @apply flex h-14 w-14 shrink-0 items-center justify-center rounded-xl bg-blue-50/50 border border-blue-100/50 overflow-hidden; }
|
||||
.reaction-preview-sprite-shell { @apply flex h-12 w-12 items-center justify-center; }
|
||||
.reaction-preview-sprite { display: block; width: 36px; height: 36px; flex: 0 0 auto; overflow: hidden; image-rendering: pixelated; }
|
||||
.reaction-preview-sprite image { pointer-events: none; user-select: none; }
|
||||
.reaction-info { @apply flex-1 min-w-0; }
|
||||
.reaction-info strong { @apply block text-sm font-bold text-navy; }
|
||||
.reaction-info small { @apply mt-0.5 block text-xs leading-relaxed text-slatecopy; }
|
||||
|
||||
.settings-panel { @apply flex flex-col gap-4 p-6; }
|
||||
.settings-panel h2 { @apply m-0 font-monoDisplay text-2xl font-black text-navy; }
|
||||
.settings-wide { @apply w-full; }
|
||||
.settings-actions { @apply flex flex-wrap gap-3 mt-2; }
|
||||
.settings-message { @apply fixed bottom-8 left-1/2 z-40 w-[min(360px,calc(100vw-3rem))] -translate-x-1/2 text-center shadow-lg; }
|
||||
.settings-success { @apply mb-4 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-emerald-700; }
|
||||
.update-card-content { @apply p-4 rounded-2xl bg-brand/5 border border-brand/10; }
|
||||
.update-card-eyebrow { @apply text-xs text-brand font-bold uppercase tracking-wider mb-2; }
|
||||
.update-card-text { @apply text-sm text-navy mb-4; }
|
||||
.reaction-header { @apply flex items-center justify-between mb-2; }
|
||||
.reaction-desc { @apply desc mb-2; }
|
||||
|
||||
.plugins-layout { @apply relative flex min-h-0 flex-1; }
|
||||
.plugins-hub { @apply flex min-h-0 flex-col overflow-hidden; }
|
||||
.plugins-layout .plugins-hub { @apply w-full; }
|
||||
.plugin-grid { @apply grid min-h-0 flex-1 grid-cols-2 gap-4 overflow-y-auto p-1.5 pr-2.5 pb-3 max-[850px]:grid-cols-1; overscroll-behavior: contain; }
|
||||
.plugin-card { @apply flex w-full flex-col rounded-[24px] border border-slate-200 bg-white/80 p-0 text-left shadow-sm transition-[border-color,background-color,box-shadow]; }
|
||||
.plugin-card:hover { @apply border-brand/40 bg-white; }
|
||||
.plugin-card.broken { @apply border-red-200 bg-red-50/60; }
|
||||
|
||||
.plugin-card-body { @apply flex items-start gap-4 p-4; }
|
||||
.plugin-card-icon { @apply grid h-14 w-14 shrink-0 place-items-center rounded-2xl border border-blue-100/60 bg-gradient-to-br from-white to-blue-100 text-brand shadow-sm; }
|
||||
.plugin-glyph { @apply h-7 w-7 stroke-[2.5]; }
|
||||
|
||||
.plugin-card-content { @apply flex min-w-0 flex-1 flex-col gap-1; }
|
||||
.plugin-card-content strong { @apply truncate text-base font-black text-navy tracking-tight; }
|
||||
.plugin-card-content small { @apply line-clamp-2 text-xs leading-relaxed text-slatecopy; }
|
||||
|
||||
.plugin-card-footer { @apply flex items-center justify-between border-t border-blue-50/50 bg-blue-50/10 px-4 py-3 rounded-b-[24px]; }
|
||||
.plugin-card-meta { @apply flex items-center gap-2; }
|
||||
|
||||
.plugin-card-actions { @apply flex items-center gap-3; }
|
||||
.plugin-card-toggle-zone { @apply flex items-center gap-2 border-r border-blue-100/50 pr-3; }
|
||||
.plugin-card-toggle-label { @apply font-monoDisplay text-[10px] font-black uppercase tracking-wider text-slatecopy/60; }
|
||||
.plugin-card-toggle { @apply h-5 w-9; }
|
||||
|
||||
.plugin-hub-footer { @apply mt-3 flex items-center justify-between gap-3 rounded-xl border border-blue-200/40 bg-blue-50/20 p-2 text-xs font-bold text-slatecopy; }
|
||||
.plugin-hub-actions { @apply flex flex-wrap items-center justify-end gap-2; }
|
||||
.plugin-empty { @apply flex min-h-48 flex-col items-center justify-center gap-2 rounded-3xl border border-dashed border-blue-200 bg-blue-50/30 p-8 text-center text-slatecopy; }
|
||||
.plugin-empty strong { @apply text-sm font-black text-navy; }
|
||||
.plugin-empty small { @apply max-w-xs text-xs leading-relaxed; }
|
||||
.plugin-empty-detail { @apply min-h-full; }
|
||||
|
||||
.plugin-config-overlay { @apply fixed inset-0 z-30 flex items-center justify-center p-6; animation: plugin-fade-in 0.2s ease-out; }
|
||||
.plugin-config-backdrop { @apply absolute inset-0 cursor-default border-0 bg-navy/[0.12] p-0 backdrop-blur-[4px]; }
|
||||
.plugin-inspector { @apply relative z-10 flex max-h-[calc(100vh-4rem)] w-[min(760px,calc(100vw-3rem))] flex-col gap-4 overflow-y-auto pr-3 shadow-[0_32px_80px_rgba(16,33,73,0.25)]; overscroll-behavior: contain; scrollbar-gutter: stable; animation: plugin-pop-in 0.25s cubic-bezier(0.16, 1, 0.3, 1); }
|
||||
.pet-detail-inspector { @apply w-[min(900px,calc(100vw-3rem))]; }
|
||||
.pet-detail-content { @apply grid gap-5 grid-cols-[1.1fr_0.9fr] items-start; }
|
||||
.pet-detail-main { @apply flex flex-col gap-3; }
|
||||
.pet-detail-reactions { @apply rounded-2xl border border-blue-100/70 bg-white/55 p-3; }
|
||||
.pet-preview-grid { @apply grid gap-3; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.pet-preview-item { @apply flex flex-col items-center justify-center gap-2 rounded-2xl border border-blue-100 bg-white/60 p-2.5 shadow-sm; }
|
||||
.pet-detail-actions .grid { @apply grid-cols-1 sm:grid-cols-2; }
|
||||
@keyframes plugin-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes plugin-pop-in { from { opacity: 0; transform: scale(0.96) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } }
|
||||
.plugin-inspector-head { @apply flex items-center gap-4; }
|
||||
.plugin-inspector-icon { @apply grid h-16 w-16 shrink-0 place-items-center rounded-[22px] border border-blue-100/70 bg-gradient-to-br from-white to-blue-100 text-brand shadow-sm; }
|
||||
.plugin-inspector h2 { @apply m-0 font-monoDisplay text-3xl font-black text-navy leading-tight; }
|
||||
.plugin-status-strip { @apply flex items-center gap-2 rounded-2xl border border-blue-100/60 bg-blue-50/60 p-3 text-xs font-semibold text-slatecopy shadow-inner; }
|
||||
.plugin-section { @apply flex flex-col gap-3 rounded-[24px] border border-blue-100/70 bg-white/65 p-5 shadow-sm; }
|
||||
.plugin-section-danger { @apply border-red-200 bg-red-50/70 text-red-700; }
|
||||
.plugin-section-danger ul { @apply m-0 pl-5 text-sm; }
|
||||
.plugin-section-title { @apply flex flex-col gap-0.5; }
|
||||
.plugin-section-title small { @apply font-monoDisplay text-xs font-black uppercase tracking-[.16em] text-brand; }
|
||||
.plugin-section-title strong { @apply text-base font-black text-navy; }
|
||||
.plugin-toggle-row { @apply rounded-2xl border-b-0 bg-white/65; }
|
||||
.plugin-permissions { @apply mt-0; }
|
||||
.plugin-config-form { @apply flex flex-col gap-3; }
|
||||
.plugin-config-row { @apply flex flex-col gap-2 rounded-2xl border border-blue-50 bg-white/60 p-3; }
|
||||
.plugin-config-row > span:first-child { @apply flex flex-col gap-0.5; }
|
||||
.plugin-config-row strong { @apply text-sm font-bold text-navy; }
|
||||
.plugin-config-row small { @apply text-xs leading-relaxed text-slatecopy; }
|
||||
.plugin-config-row-boolean { @apply flex-row items-center justify-between; }
|
||||
.plugin-input { @apply min-h-10 w-full rounded-xl border border-blue-200/80 bg-white/80 px-3 py-2 text-sm font-semibold text-navy outline-none shadow-inner focus:border-brand focus:ring-4 focus:ring-brand/15; }
|
||||
.plugin-textarea { @apply min-h-24 resize-y font-sans; }
|
||||
.plugin-select { @apply w-full; }
|
||||
.plugin-chip-list { @apply flex flex-wrap gap-2; }
|
||||
.plugin-chip { @apply min-h-10 rounded-xl border border-blue-200/60 bg-white/80 px-3 py-1.5 text-xs font-bold text-slatecopy shadow-sm transition-[transform,background-color,border-color,box-shadow] active:scale-[0.96]; }
|
||||
.plugin-chip.active { @apply border-brand bg-blue-50 text-brand ring-2 ring-blue-100; }
|
||||
.plugin-list-editor { @apply flex flex-col gap-3; }
|
||||
.plugin-list-item { @apply flex flex-col gap-3 rounded-2xl border border-blue-100/70 bg-blue-50/25 p-3; }
|
||||
.plugin-list-item-header { @apply flex items-center justify-between text-xs font-bold uppercase tracking-wider text-slatecopy; }
|
||||
.plugin-command-list { @apply flex flex-wrap gap-2; }
|
||||
.plugin-actions-section { @apply grid grid-cols-3 gap-2; }
|
||||
.plugin-actions-section .btn-secondary { @apply bg-blue-50/70; }
|
||||
.plugin-actions-section .btn-primary { @apply min-w-[96px]; }
|
||||
.plugin-actions-section .btn-danger { @apply min-w-[112px]; }
|
||||
@media (max-width: 900px) { .plugins-layout, .plugins-hub, .plugin-grid { @apply overflow-visible; } .plugin-actions-section { @apply grid-cols-1; } }
|
||||
|
||||
.integration-grid { @apply grid grid-cols-2 gap-4 max-[850px]:grid-cols-1; }
|
||||
.integration-card { @apply flex min-h-[190px] flex-col justify-between rounded-3xl border border-blue-100/70 bg-white/70 p-4 text-left shadow-sm transition-[transform,border-color,background-color,box-shadow] active:scale-[0.96]; }
|
||||
.integration-card:hover { @apply border-brand/45 bg-white/90 shadow-md; }
|
||||
.integration-icon { @apply grid h-14 w-14 shrink-0 place-items-center overflow-hidden rounded-2xl border border-blue-100/70 bg-blue-50/70 text-brand shadow-sm; }
|
||||
.integration-logo { @apply h-9 w-9 object-contain; }
|
||||
.integration-icon svg { @apply w-8 h-8; }
|
||||
|
||||
/* Dashboard */
|
||||
.dashboard-layout { @apply flex flex-col gap-6 overflow-y-auto pr-2 pb-8 h-full; overscroll-behavior: contain; }
|
||||
.dashboard-hero { @apply relative flex items-center justify-between overflow-hidden rounded-[32px] border border-blue-200/50 bg-gradient-to-br from-blue-500 to-blue-600 p-8 text-white shadow-lg min-h-[180px]; }
|
||||
.dashboard-hero::before { content: ""; @apply absolute inset-0 opacity-20; background-image: radial-gradient(circle at 2px 2px, white 1px, transparent 0); background-size: 24px 24px; }
|
||||
.dashboard-hero-content { @apply relative z-10 flex flex-col gap-1.5 flex-1 min-w-0; }
|
||||
.dashboard-hero-title { @apply m-0 font-monoDisplay text-4xl font-black leading-tight tracking-tight; text-wrap: balance; }
|
||||
.dashboard-hero-desc { @apply max-w-md text-sm font-medium text-blue-50 opacity-90; text-wrap: pretty; }
|
||||
.dashboard-hero-pet { @apply relative z-10 flex shrink-0 items-center justify-center ml-6; }
|
||||
.dashboard-hero-pet .sprite-detail { filter: drop-shadow(0 20px 25px rgba(0, 0, 0, 0.25)); }
|
||||
|
||||
.dashboard-grid { @apply grid grid-cols-3 gap-5; }
|
||||
.dashboard-stat-card { @apply flex flex-col gap-4 rounded-[28px] border border-blue-100/50 bg-white/80 p-6 shadow-sm transition-[border-color,background-color,box-shadow] duration-150 hover:border-blue-200 hover:bg-white hover:shadow-md; }
|
||||
.dashboard-stat-header { @apply flex items-center gap-3; }
|
||||
.dashboard-stat-icon { @apply flex h-10 w-10 items-center justify-center rounded-2xl bg-blue-50 text-brand; }
|
||||
.dashboard-stat-label { @apply font-monoDisplay text-[11px] font-black uppercase tracking-wider text-slatecopy/70; }
|
||||
.dashboard-stat-value { @apply font-monoDisplay text-3xl font-black tracking-tight text-navy tabular-nums; }
|
||||
.dashboard-stat-footer { @apply text-[11px] font-bold text-slatecopy opacity-60; }
|
||||
|
||||
.dashboard-row { @apply grid grid-cols-[1fr_320px] gap-5; }
|
||||
.dashboard-activity-card { @apply flex flex-col gap-4; }
|
||||
.dashboard-section-title { @apply flex items-center gap-2 font-monoDisplay text-sm font-black uppercase tracking-widest text-brand; }
|
||||
.dashboard-reaction-list { @apply flex flex-wrap gap-2; }
|
||||
.dashboard-reaction-item { @apply flex items-center gap-2.5 rounded-2xl border border-blue-50 bg-white/60 px-3.5 py-2.5 shadow-sm transition-[border-color,background-color,box-shadow] duration-150 hover:border-blue-100 hover:bg-white; }
|
||||
.dashboard-reaction-count { @apply font-mono text-sm font-bold text-navy tabular-nums; }
|
||||
.dashboard-reaction-label { @apply text-xs font-medium text-slatecopy; }
|
||||
|
||||
.dashboard-system-card { @apply flex flex-col gap-4; }
|
||||
.dashboard-system-list { @apply flex flex-col gap-2.5; }
|
||||
.dashboard-system-item { @apply flex items-center justify-between rounded-2xl border border-blue-50/50 bg-white/40 p-3.5 text-sm transition-[background-color] duration-150 hover:bg-white/60; }
|
||||
.dashboard-system-info { @apply flex items-center gap-3; }
|
||||
.dashboard-system-icon { @apply flex h-8 w-8 items-center justify-center rounded-xl bg-blue-50/50 text-brand; }
|
||||
.dashboard-system-label { @apply font-bold text-navy; }
|
||||
.dashboard-system-value { @apply font-mono text-xs font-bold text-slatecopy; }
|
||||
@media (max-width: 900px) {
|
||||
.dashboard-layout { @apply overflow-visible pr-0; }
|
||||
.dashboard-grid { @apply grid-cols-2; }
|
||||
.dashboard-row { @apply grid-cols-1; }
|
||||
.dashboard-hero { @apply flex-col items-center text-center gap-6 p-6; }
|
||||
.dashboard-hero-content { @apply items-center; }
|
||||
.dashboard-hero-desc { @apply max-w-none; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.dashboard-grid { @apply grid-cols-1; }
|
||||
}
|
||||
|
||||
}
|
||||
1
apps/desktop/src/renderer/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { Menu, Tray, type MenuItemConstructorOptions } from "electron";
|
||||
|
||||
import { getAppStateSnapshot, isOnboardingCompleted } from "./app-state.js";
|
||||
import { getAppStateSnapshot } from "./app-state.js";
|
||||
import { createTrayIcon } from "./assets.js";
|
||||
import { hideDefaultPet, isDefaultPetVisible, setDefaultPetPaused, showDefaultPet } from "./default-pet-controller.js";
|
||||
import { quitOpenPets } from "./lifecycle.js";
|
||||
import { info, openLogsFolder } from "./logger.js";
|
||||
import { shellState, togglePaused } from "./state.js";
|
||||
import { getUpdateStatus, openUpdateReleasePage } from "./update-checker.js";
|
||||
import { openTaskWindow } from "./windows.js";
|
||||
import { openControlCenterWindow } from "./windows.js";
|
||||
|
||||
let tray: Tray | null = null;
|
||||
|
||||
|
|
@ -34,16 +34,6 @@ export function refreshTrayMenu(): void {
|
|||
const defaultPet = state.pets.installed.find((pet) => pet.id === state.preferences.defaultPetId && !pet.broken) ?? state.pets.installed[0];
|
||||
const defaultPetName = defaultPet?.displayName ?? "Built-in Pet";
|
||||
|
||||
const continueSetupItems = isOnboardingCompleted()
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: "Continue Setup...",
|
||||
click: () => openTaskWindow("onboarding"),
|
||||
},
|
||||
{ type: "separator" as const },
|
||||
];
|
||||
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "OpenPets",
|
||||
|
|
@ -51,10 +41,9 @@ export function refreshTrayMenu(): void {
|
|||
},
|
||||
...createUpdateMenuItems(),
|
||||
{ type: "separator" },
|
||||
...continueSetupItems,
|
||||
{
|
||||
label: `Default Pet: ${defaultPetName}`,
|
||||
click: () => openTaskWindow("pet-manager"),
|
||||
click: () => openControlCenterWindow("pets"),
|
||||
},
|
||||
{
|
||||
label: isDefaultPetVisible() ? "Hide Default Pet" : "Show Default Pet",
|
||||
|
|
@ -81,19 +70,23 @@ export function refreshTrayMenu(): void {
|
|||
{ type: "separator" },
|
||||
{
|
||||
label: "Manage Pets...",
|
||||
click: () => openTaskWindow("pet-manager"),
|
||||
click: () => openControlCenterWindow("pets"),
|
||||
},
|
||||
{
|
||||
label: "Control Center...",
|
||||
click: () => openControlCenterWindow(),
|
||||
},
|
||||
{
|
||||
label: "Integrations...",
|
||||
click: () => openTaskWindow("agent-setup"),
|
||||
click: () => openControlCenterWindow("integrations"),
|
||||
},
|
||||
{
|
||||
label: "Plugins...",
|
||||
click: () => openTaskWindow("plugins"),
|
||||
click: () => openControlCenterWindow("plugins"),
|
||||
},
|
||||
{
|
||||
label: "Settings...",
|
||||
click: () => openTaskWindow("settings"),
|
||||
click: () => openControlCenterWindow("settings"),
|
||||
},
|
||||
{
|
||||
label: "Open Logs Folder...",
|
||||
|
|
|
|||
18
apps/desktop/tailwind.config.cjs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
module.exports = {
|
||||
content: ["./src/renderer/**/*.{ts,tsx,html}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
navy: "#102149",
|
||||
slatecopy: "#63708f",
|
||||
brand: { DEFAULT: "#176df2", light: "#3b96ff" },
|
||||
},
|
||||
fontFamily: {
|
||||
monoDisplay: ['"SFMono-Regular"', '"Cascadia Code"', '"Roboto Mono"', "monospace"],
|
||||
},
|
||||
boxShadow: {
|
||||
glass: "0 24px 70px rgba(50, 104, 180, 0.18)",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -5,12 +5,13 @@ import { fileURLToPath } from "node:url";
|
|||
|
||||
const desktopRoot = process.env.OPENPETS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const windowsSource = readFileSync(resolve(desktopRoot, "src/windows.ts"), "utf8");
|
||||
const preloadSource = readFileSync(resolve(desktopRoot, "preload.cjs"), "utf8");
|
||||
const controlCenterPreloadSource = readFileSync(resolve(desktopRoot, "control-center-preload.cjs"), "utf8");
|
||||
const controlCenterRendererSource = readFileSync(resolve(desktopRoot, "src/renderer/src/main.tsx"), "utf8");
|
||||
const jsHostSource = readFileSync(resolve(desktopRoot, "src/plugin-js-host.ts"), "utf8");
|
||||
const pluginSdkPreloadSource = readFileSync(resolve(desktopRoot, "plugin-sdk-preload.cjs"), "utf8");
|
||||
|
||||
assert.match(windowsSource, /kind === "plugins"[\s\S]*preload: getPreloadPath\(\)/);
|
||||
assert.match(windowsSource, /assertAllowedSender\(event, \["plugins"\]\)/);
|
||||
assert.doesNotMatch(windowsSource, /openTaskWindow|TaskWindowKind|createPluginsHtml|getPreloadPath|"preload\.cjs"/);
|
||||
assert.match(windowsSource, /assertAllowedSender\(event, \["control-center"\]\)/);
|
||||
assert.match(windowsSource, /openpets:plugins-snapshot/);
|
||||
assert.match(windowsSource, /openpets:plugins-save-config/);
|
||||
assert.match(windowsSource, /openpets:plugins-load-local/);
|
||||
|
|
@ -18,33 +19,27 @@ assert.match(windowsSource, /openpets:plugins-catalog-snapshot/);
|
|||
assert.match(windowsSource, /openpets:plugins-install-catalog/);
|
||||
assert.match(windowsSource, /openpets:plugins-update-catalog/);
|
||||
assert.match(windowsSource, /openpets:plugins-uninstall/);
|
||||
assert.match(windowsSource, /openpets:plugins-load-local[\s\S]*assertAllowedSender\(event, \["plugins"\]\)/);
|
||||
assert.match(preloadSource, /contextBridge\.exposeInMainWorld\("openpetsPlugins", pluginsApi\)/);
|
||||
assert.match(preloadSource, /snapshot: \(\) => ipcRenderer\.invoke\("openpets:plugins-snapshot"\)/);
|
||||
assert.match(preloadSource, /loadLocal: \(\) => ipcRenderer\.invoke\("openpets:plugins-load-local"\)/);
|
||||
assert.match(preloadSource, /catalogSnapshot: \(refresh\) => ipcRenderer\.invoke\("openpets:plugins-catalog-snapshot", refresh\)/);
|
||||
assert.match(preloadSource, /installCatalog: \(id\) => ipcRenderer\.invoke\("openpets:plugins-install-catalog", id\)/);
|
||||
assert.match(preloadSource, /updateCatalog: \(id\) => ipcRenderer\.invoke\("openpets:plugins-update-catalog", id\)/);
|
||||
assert.match(preloadSource, /uninstall: \(id\) => ipcRenderer\.invoke\("openpets:plugins-uninstall", id\)/);
|
||||
assert.doesNotMatch(preloadSource, /plugins-load-local",\s*[^)]/);
|
||||
assert.doesNotMatch(preloadSource, /manifestPath/);
|
||||
assert.doesNotMatch(preloadSource, /installPath/);
|
||||
assert.doesNotMatch(preloadSource, /openpets:plugins-install-catalog",\s*[^)]+,\s*[^)]/);
|
||||
assert.match(windowsSource, /openpets:plugins-load-local[\s\S]*assertAllowedSender\(event, \["control-center"\]\)/);
|
||||
|
||||
// Ensure no plugin tabs remain
|
||||
assert.doesNotMatch(preloadSource, /plugin-tabs/);
|
||||
assert.doesNotMatch(preloadSource, /plugin-tab-content/);
|
||||
assert.match(controlCenterPreloadSource, /getPluginsSnapshot: \(\) => ipcRenderer\.invoke\("openpets:plugins-snapshot"\)/);
|
||||
assert.match(controlCenterPreloadSource, /getPluginCatalogSnapshot: \(refresh\) => ipcRenderer\.invoke\("openpets:plugins-catalog-snapshot", refresh\)/);
|
||||
assert.match(controlCenterPreloadSource, /setPluginEnabled: \(id, enabled\) => ipcRenderer\.invoke\("openpets:plugins-set-enabled", id, enabled\)/);
|
||||
assert.match(controlCenterPreloadSource, /savePluginConfig: \(id, config\) => ipcRenderer\.invoke\("openpets:plugins-save-config", id, config\)/);
|
||||
assert.match(controlCenterPreloadSource, /loadLocalPlugin: \(\) => ipcRenderer\.invoke\("openpets:plugins-load-local"\)/);
|
||||
assert.match(controlCenterPreloadSource, /installCatalogPlugin: \(id\) => ipcRenderer\.invoke\("openpets:plugins-install-catalog", id\)/);
|
||||
assert.match(controlCenterPreloadSource, /uninstallPlugin: \(id\) => ipcRenderer\.invoke\("openpets:plugins-uninstall", id\)/);
|
||||
assert.doesNotMatch(controlCenterPreloadSource, /onboarding/i);
|
||||
assert.doesNotMatch(controlCenterPreloadSource, /plugins-load-local",\s*[^)]/);
|
||||
assert.doesNotMatch(controlCenterPreloadSource, /manifestPath/);
|
||||
assert.doesNotMatch(controlCenterPreloadSource, /installPath/);
|
||||
assert.doesNotMatch(controlCenterPreloadSource, /openpets:plugins-install-catalog",\s*[^)]+,\s*[^)]/);
|
||||
|
||||
// Ensure developer load button is bound
|
||||
assert.match(preloadSource, /loadLocalBtn\.onclick/);
|
||||
|
||||
// Ensure option object label/value handling exists
|
||||
assert.match(preloadSource, /opt\.value/);
|
||||
assert.match(preloadSource, /opt\.label/);
|
||||
|
||||
// Ensure list item collection avoids nested data loss
|
||||
assert.match(preloadSource, /dataset\.schemaKey/);
|
||||
assert.match(preloadSource, /Array\.from\(container\.children\)/);
|
||||
assert.match(controlCenterRendererSource, /function PluginsView\(\)/);
|
||||
assert.match(controlCenterRendererSource, /currentRoute === "plugins"[\s\S]*<PluginsView \/>/);
|
||||
assert.doesNotMatch(controlCenterRendererSource, /OnboardingView|currentRoute === "onboarding"/);
|
||||
assert.match(controlCenterRendererSource, /materializeListItemDefaults/);
|
||||
assert.match(controlCenterRendererSource, /updateCatalogEntry[\s\S]*api\.updateCatalogPlugin/);
|
||||
assert.match(controlCenterRendererSource, /installed\.source === "catalog"[\s\S]*updateCatalogEntry/);
|
||||
|
||||
assert.match(jsHostSource, /OpenPetsPlugin[\s\S]*register/);
|
||||
assert.match(jsHostSource, /start\(sdk\)/);
|
||||
|
|
@ -54,4 +49,4 @@ assert.match(pluginSdkPreloadSource, /contextBridge\.exposeInMainWorld\("__openP
|
|||
assert.match(pluginSdkPreloadSource, /speak: \(message\) => call\("pet\.speak", \[message\]\)/);
|
||||
assert.match(pluginSdkPreloadSource, /register: \(command, handler\) => call\("commands\.register"/);
|
||||
|
||||
console.error("Plugin UI static validation passed.");
|
||||
console.error("Plugin Control Center static validation passed.");
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
|
||||
import { createPluginsHtml } from "../src/plugins-window.js";
|
||||
|
||||
const html = createPluginsHtml({ title: "Plugins", heading: "Plugins", description: "Manage plugins." });
|
||||
|
||||
assert.match(html, /data-openpets-view="plugins"/);
|
||||
assert.match(html, /Content-Security-Policy/);
|
||||
assert.doesNotMatch(html, /script-src/);
|
||||
assert.doesNotMatch(html, /https:\/\//);
|
||||
assert.match(html, /id="plugins-grid"/);
|
||||
assert.match(html, /id="detail-view"/);
|
||||
assert.match(html, /id="hub-view"/);
|
||||
|
||||
console.error("Plugins window validation passed.");
|
||||
|
|
@ -4,5 +4,6 @@
|
|||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/renderer/**"]
|
||||
}
|
||||
|
|
|
|||
16
apps/desktop/tsconfig.renderer.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"rootDir": "src/renderer",
|
||||
"outDir": "dist/renderer-types",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"types": ["vite/client", "react", "react-dom"]
|
||||
},
|
||||
"include": ["src/renderer/**/*.ts", "src/renderer/**/*.tsx", "src/renderer/**/*.d.ts"]
|
||||
}
|
||||
|
|
@ -9,5 +9,5 @@
|
|||
"lib": ["ES2022"]
|
||||
},
|
||||
"include": ["tests/**/*.ts", "contracts/**/*.ts", "src/**/*.ts"],
|
||||
"exclude": ["dist", ".test-dist", "node_modules"]
|
||||
"exclude": ["dist", ".test-dist", "node_modules", "src/renderer/**"]
|
||||
}
|
||||
|
|
|
|||
23
apps/desktop/vite.config.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
const productionCsp = "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data: https://openpets.dev openpets-codex: openpets-pet-preview:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-src 'none'";
|
||||
const devCsp = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://openpets.dev openpets-codex: openpets-pet-preview:; connect-src 'self' http://127.0.0.1:5173 ws://127.0.0.1:5173; base-uri 'none'; form-action 'none'; frame-src 'none'";
|
||||
|
||||
export default defineConfig(({ command }) => ({
|
||||
root: "src/renderer",
|
||||
base: "./",
|
||||
plugins: [
|
||||
react(),
|
||||
command === "serve" && {
|
||||
name: "openpets-dev-csp",
|
||||
transformIndexHtml(html: string) {
|
||||
return html.replace(productionCsp, devCsp);
|
||||
},
|
||||
},
|
||||
].filter(Boolean),
|
||||
build: {
|
||||
outDir: "../../dist/renderer",
|
||||
emptyOutDir: true,
|
||||
},
|
||||
}));
|
||||
BIN
assets/logo.png
Normal file
|
After Width: | Height: | Size: 590 KiB |
|
|
@ -23,6 +23,8 @@ OpenPets is a pnpm/TypeScript monorepo for an Electron desktop companion app plu
|
|||
| `apps/desktop/` | User-facing Electron companion app: tray UX, pet windows, pet installation, plugin automation/runtime, agent setup, update checks, and local IPC server. | [View Map](apps/desktop/codemap.md) |
|
||||
| `apps/desktop/contracts/` | Desktop public-boundary contract tests for catalog fixtures, local IPC protocol behavior, and plugin manifest schema validation. | [View Map](apps/desktop/contracts/codemap.md) |
|
||||
| `apps/desktop/src/` | Main-process service layer for app lifecycle, state, tray/windows, IPC routing, lease-managed agent pets, catalog installation, plugin subsystem, and editor integration. | [View Map](apps/desktop/src/codemap.md) |
|
||||
| `apps/desktop/src/renderer/` | Vite renderer workspace for the sandboxed React/Tailwind Control Center bundle. | [View Map](apps/desktop/src/renderer/codemap.md) |
|
||||
| `apps/desktop/src/renderer/src/` | React/Tailwind Control Center UI for Dashboard, Pets, Integrations, Plugins, and Settings via narrow preload APIs. | [View Map](apps/desktop/src/renderer/src/codemap.md) |
|
||||
| `apps/desktop/scripts/` | Desktop package cleanup and local release automation scripts. | [View Map](apps/desktop/scripts/codemap.md) |
|
||||
| `packages/` | Publishable npm package workspace for shared protocol, CLI, MCP, and coding-agent integrations. | [View Map](packages/codemap.md) |
|
||||
| `packages/agent-events/` | Shared agent speech/event message pools and validation utilities. | [View Map](packages/agent-events/codemap.md) |
|
||||
|
|
|
|||
326
docs/front_refactor.md
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
# Desktop Frontend Refactor
|
||||
|
||||
## Current Situation
|
||||
|
||||
The desktop app now uses the React Control Center as the production management UI. Pets, Settings, Plugins, and Integrations are routed inside the singleton Control Center window, and tray actions target those routes directly.
|
||||
|
||||
The legacy generated task-window UI has been removed: `apps/desktop/preload.cjs`, `apps/desktop/src/plugins-window.ts`, and the legacy plugins-window test are gone. Transparent pet windows and plugin SDK windows remain separate lightweight renderers.
|
||||
|
||||
## Goal
|
||||
|
||||
Move the desktop management UI from generated HTML/CSS strings and preload-driven DOM updates to a React + Tailwind Control Center, while keeping desktop pet windows lightweight and separate.
|
||||
|
||||
## End Goal
|
||||
|
||||
The end goal is a single polished desktop management experience for OpenPets:
|
||||
|
||||
```text
|
||||
Tray app
|
||||
├─ animated desktop pet windows
|
||||
└─ one React/Tailwind Control Center
|
||||
├─ Dashboard
|
||||
├─ Pets
|
||||
├─ Integrations
|
||||
├─ Plugins
|
||||
└─ Settings
|
||||
```
|
||||
|
||||
The Control Center should become the one place where users manage everything:
|
||||
|
||||
- choose and install pets
|
||||
- configure coding-agent integrations
|
||||
- manage plugins
|
||||
- tune settings
|
||||
- inspect status/errors/update prompts
|
||||
|
||||
The old separate task windows should eventually disappear. The tray should still exist, but tray actions should route into the Control Center instead of opening independent windows.
|
||||
|
||||
The pet windows themselves should stay separate from React. They are performance- and behavior-sensitive transparent desktop windows, so they should remain small, focused renderers responsible only for showing pets, speech bubbles, and reactions.
|
||||
|
||||
Final architecture target:
|
||||
|
||||
- **Main process**: app lifecycle, tray, state, IPC handlers, services, pet/plugin/integration logic.
|
||||
- **Control Center renderer**: React UI, routes, forms, visual polish, local UI state.
|
||||
- **Preloads**: narrow typed bridges only, no DOM rendering.
|
||||
- **Pet renderers**: lightweight sprite/speech renderers, not part of Control Center.
|
||||
|
||||
From a product perspective, the end state should feel like a clean native-quality companion app rather than a set of separate utility dialogs.
|
||||
|
||||
## Current Direction
|
||||
|
||||
- Keep the app tray-first.
|
||||
- Keep existing transparent pet windows outside React.
|
||||
- Use a single Control Center window for management UI.
|
||||
- Route tray actions into Control Center pages instead of independent task windows.
|
||||
- Keep transparent pet windows separate from React.
|
||||
|
||||
## Phase 1: Pets Preview
|
||||
|
||||
Implemented first as a reviewable prototype and now promoted into the primary management shell:
|
||||
|
||||
- `Control Center...` opens the React renderer.
|
||||
- Pet management opens through the Control Center Pets route.
|
||||
- Renderer lives under `apps/desktop/src/renderer/`.
|
||||
- Vite outputs packaged assets to `apps/desktop/dist/renderer/`.
|
||||
- `control-center-preload.cjs` exposes narrow page-specific APIs.
|
||||
|
||||
The Pets page includes:
|
||||
|
||||
- installed pets
|
||||
- catalog pets
|
||||
- catalog pagination
|
||||
- catalog search
|
||||
- Codex pets
|
||||
- pet selection/detail pane
|
||||
- install/import/set default/remove actions
|
||||
- animated sprite-frame preview
|
||||
- styling based on the existing Pet Manager and Integrations pages
|
||||
|
||||
## What We Changed
|
||||
|
||||
### Renderer foundation
|
||||
|
||||
Added a modern renderer stack for the desktop app:
|
||||
|
||||
- React
|
||||
- ReactDOM
|
||||
- Vite
|
||||
- TailwindCSS
|
||||
- PostCSS
|
||||
- Autoprefixer
|
||||
|
||||
New renderer files live in:
|
||||
|
||||
```text
|
||||
apps/desktop/src/renderer/
|
||||
```
|
||||
|
||||
Production renderer output is built to:
|
||||
|
||||
```text
|
||||
apps/desktop/dist/renderer/
|
||||
```
|
||||
|
||||
### Control Center window
|
||||
|
||||
Added a dedicated Control Center BrowserWindow in `apps/desktop/src/windows.ts`.
|
||||
|
||||
Important details:
|
||||
|
||||
- It is a separate singleton window.
|
||||
- It does not use the old task-window data URL system.
|
||||
- It keeps the usual Electron hardening:
|
||||
- sandbox enabled
|
||||
- context isolation enabled
|
||||
- Node integration disabled
|
||||
- navigation blocked
|
||||
- `window.open` blocked
|
||||
- In development, it can load `OPENPETS_RENDERER_URL`, but only from loopback hosts.
|
||||
- In packaged builds, it loads `dist/renderer/index.html`.
|
||||
|
||||
### Tray integration
|
||||
|
||||
Tray items now route to the Control Center singleton:
|
||||
|
||||
- `Control Center...`
|
||||
- `Manage Pets...` → Pets route
|
||||
- Plugins, Integrations, and Settings → matching Control Center routes
|
||||
|
||||
### Narrow preload bridge
|
||||
|
||||
Added:
|
||||
|
||||
```text
|
||||
apps/desktop/control-center-preload.cjs
|
||||
```
|
||||
|
||||
It exposes narrow, page-specific methods to the React renderer. It now covers Pets, Settings, Plugins, and Integrations without exposing `ipcRenderer` directly.
|
||||
|
||||
Pets methods include:
|
||||
|
||||
- `getPetsState`
|
||||
- `getCatalog`
|
||||
- `getCatalogPage`
|
||||
- `getCatalogSearch`
|
||||
- `getCodexPets`
|
||||
- `setDefaultPet`
|
||||
- `installPet`
|
||||
- `importCodexPet`
|
||||
- `removePet`
|
||||
|
||||
Settings methods include startup preferences, launch-at-login, pet scale, reaction animation mapping, update checks, and default pet position reset.
|
||||
|
||||
Plugins methods include plugin snapshots, catalog snapshots, enable/disable, config save, reload, command execution, local plugin loading, catalog install/update, and uninstall.
|
||||
|
||||
Integrations methods include agent setup snapshots, setup actions, and command path updates for Claude Code, OpenCode, and Cursor.
|
||||
|
||||
It does not expose `ipcRenderer` directly.
|
||||
|
||||
### Narrow page state
|
||||
|
||||
The Control Center does not receive the full app state. It receives narrowed page snapshots only.
|
||||
|
||||
The Pets page receives:
|
||||
|
||||
- default pet id
|
||||
- installed pets
|
||||
|
||||
The Settings page receives only settings preferences/options needed by the route.
|
||||
|
||||
The Plugins page receives safe plugin and catalog records from `PluginService`, intentionally excluding raw install paths and manifest paths.
|
||||
|
||||
The Integrations page receives the existing narrowed agent setup snapshot used by the legacy task window: status/details for Claude Code, OpenCode, Cursor, Pi guidance, command paths, pet routing choices, and safe previews.
|
||||
|
||||
This avoids exposing unrelated state to the new renderer.
|
||||
|
||||
### Pets page UI
|
||||
|
||||
The new Pets page now supports:
|
||||
|
||||
- installed pet list
|
||||
- remote catalog pets
|
||||
- catalog pagination
|
||||
- catalog search
|
||||
- Codex pets
|
||||
- search/filter UI
|
||||
- set default
|
||||
- install catalog pet
|
||||
- import Codex pet
|
||||
- remove pet
|
||||
- detail panel
|
||||
- animated sprite-frame preview
|
||||
|
||||
### Settings page UI
|
||||
|
||||
The Control Center Settings page now supports:
|
||||
|
||||
- show-pet-on-launch preference
|
||||
- launch-at-login preference
|
||||
- pet scale selection
|
||||
- reaction-to-animation mapping
|
||||
- default pet position reset
|
||||
- compact system/update status
|
||||
- default-pet reaction previews using the `openpets-pet-preview:` protocol
|
||||
- bottom-center floating notifications that do not shift layout
|
||||
|
||||
Settings is now served by the Control Center route.
|
||||
|
||||
### Plugins page UI
|
||||
|
||||
The Control Center Plugins page now supports:
|
||||
|
||||
- gallery-first plugin hub
|
||||
- installed/catalog/local/broken filters
|
||||
- catalog refresh and local plugin loading in the bottom utility row
|
||||
- plugin install from catalog
|
||||
- enable/disable directly from plugin cards
|
||||
- on-demand configuration modal instead of a persistent split inspector
|
||||
- dynamic config forms for supported plugin schema fields
|
||||
- command execution when plugins expose commands
|
||||
- reload, update, and uninstall actions
|
||||
- safe no-op/cancel feedback for install/update/local-load flows
|
||||
|
||||
Plugins is now served by the Control Center route; broken plugins cannot be re-enabled until fixed.
|
||||
|
||||
### Integrations page UI
|
||||
|
||||
The Control Center Integrations page now supports:
|
||||
|
||||
- grid-first integration hub with icons and status pills
|
||||
- direct install/connect actions from Claude Code, OpenCode, and Cursor cards
|
||||
- detail inspector opened by Configure/View setup buttons
|
||||
- Claude MCP setup, replace/remove, hooks, instructions, pet routing, command paths, and advanced previews
|
||||
- OpenCode global setup, remove, pet routing, command paths, and config preview
|
||||
- Cursor MCP setup, replace/remove, pet routing, MCP preview, and Cursor rules preview
|
||||
- Pi manual setup guidance
|
||||
- polished disabled cards for future editor integrations
|
||||
|
||||
Integrations is now served by the Control Center route, including command-source selection for published, bundled, or local development CLI modes.
|
||||
|
||||
### Dashboard UI
|
||||
|
||||
The Control Center Dashboard now supports:
|
||||
|
||||
- default companion overview with animated sprite preview
|
||||
- quick navigation into pet selection
|
||||
- message and reaction activity totals
|
||||
- top companion and top reaction summaries
|
||||
- last-interaction timestamp
|
||||
- compact system health for installed pets, plugins, catalog, updates, and app version
|
||||
|
||||
Dashboard is now the default Control Center landing route.
|
||||
|
||||
### Build/test integration
|
||||
|
||||
Updated desktop scripts/configs so the new renderer participates in normal validation.
|
||||
|
||||
Important files:
|
||||
|
||||
- `apps/desktop/package.json`
|
||||
- `apps/desktop/vite.config.ts`
|
||||
- `apps/desktop/tailwind.config.cjs`
|
||||
- `apps/desktop/postcss.config.cjs`
|
||||
- `apps/desktop/tsconfig.renderer.json`
|
||||
- `apps/desktop/electron-builder.yml`
|
||||
- `apps/desktop/scripts/run-tests.mjs`
|
||||
|
||||
Validated with:
|
||||
|
||||
```bash
|
||||
pnpm --filter @open-pets/desktop typecheck
|
||||
pnpm --filter @open-pets/desktop build
|
||||
pnpm --filter @open-pets/desktop test
|
||||
node --check apps/desktop/control-center-preload.cjs
|
||||
```
|
||||
|
||||
All passed.
|
||||
|
||||
## Styling Principles
|
||||
|
||||
Reuse the current polished desktop visual language:
|
||||
|
||||
- light blue gradient background
|
||||
- white glass cards
|
||||
- soft blue shadows
|
||||
- navy text
|
||||
- brand blue CTAs
|
||||
- rounded mono buttons and status pills
|
||||
- orange Originals filter accent
|
||||
- two-column gallery/detail layout
|
||||
|
||||
## Security Constraints
|
||||
|
||||
- `nodeIntegration: false`
|
||||
- `contextIsolation: true`
|
||||
- `sandbox: true`
|
||||
- no direct `ipcRenderer` exposure
|
||||
- deny navigation, redirects, and `window.open`
|
||||
- dev renderer URL allowed only for loopback hosts in non-packaged builds
|
||||
- Control Center receives narrow page snapshots, not full app state
|
||||
|
||||
## Migration Status and Next Order
|
||||
|
||||
Completed phases:
|
||||
|
||||
- Manual Pets review and refinement
|
||||
- Control Center routing/shell
|
||||
- Settings migration
|
||||
- Plugins migration
|
||||
- Integrations migration
|
||||
- Tray route switch to Control Center
|
||||
- Legacy generated task-window UI removal
|
||||
|
||||
Remaining follow-up:
|
||||
|
||||
- Continue manual smoke coverage for tray route retargeting, plugin workflows, and integration setup flows.
|
||||
|
||||
## Validation Commands
|
||||
|
||||
Use these after each phase:
|
||||
|
||||
```bash
|
||||
pnpm --filter @open-pets/desktop typecheck
|
||||
pnpm --filter @open-pets/desktop build
|
||||
pnpm --filter @open-pets/desktop test
|
||||
node --check apps/desktop/control-center-preload.cjs
|
||||
```
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"dev:desktop": "pnpm --filter @open-pets/desktop dev",
|
||||
"dev:desktop:control-center": "pnpm --filter @open-pets/desktop dev:control-center",
|
||||
"dev:desktop:plugins": "pnpm --filter @open-pets/desktop dev:plugins",
|
||||
"package:desktop:dir": "pnpm build && pnpm --filter @open-pets/desktop package:dir",
|
||||
"package:desktop": "pnpm build && pnpm --filter @open-pets/desktop package",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ Provides modular, reusable components for the OpenPets ecosystem:
|
|||
- **opencode**: OpenCode editor integration (plugin, config management)
|
||||
- **claude**: Claude Code integration (hooks, MCP config)
|
||||
- **cursor**: Cursor editor integration (MCP config, project rules)
|
||||
- **pi**: Pi coding-agent extension integration (event hooks, slash commands)
|
||||
- **install-pet**: Standalone pet installer from gallery catalog
|
||||
|
||||
## Design/Patterns
|
||||
|
|
@ -28,7 +29,7 @@ Provides modular, reusable components for the OpenPets ecosystem:
|
|||
|
||||
**ESM-First**: All packages are ESM (`"type": "module"`) with dual exports for types.
|
||||
|
||||
**Versioning**: Independent versioning per package (all currently 2.0.x).
|
||||
**Versioning**: Independent versioning per package (currently 2.1.x for active integrations).
|
||||
|
||||
## Flow
|
||||
|
||||
|
|
@ -52,6 +53,9 @@ Claude Hooks (packages/claude/src/hooks.ts)
|
|||
|
||||
Cursor Setup (packages/cursor/src/cursor-project-setup.ts)
|
||||
└── Writes MCP config + rules → @open-pets/client
|
||||
|
||||
Pi Extension (packages/pi/src/extension.ts)
|
||||
└── Registers Pi extension hooks/commands → @open-pets/client
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
|
@ -62,6 +66,7 @@ Cursor Setup (packages/cursor/src/cursor-project-setup.ts)
|
|||
- `claude` depends on: `client`, `agent-events`
|
||||
- `opencode` depends on: `client`, `agent-events`
|
||||
- `cursor` depends on: `client`
|
||||
- `pi` depends on: `client`, `agent-events` and declares optional `@earendil-works/pi-coding-agent` peer support
|
||||
- `install-pet` depends on: `client`
|
||||
|
||||
**External Integrations**:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ OpenCode editor integration for OpenPets.
|
|||
|
||||
## Responsibility
|
||||
|
||||
Provides comprehensive OpenCode editor integration including: MCP server configuration, plugin runtime with event hooks, project/global setup management, and instruction file generation.
|
||||
Provides comprehensive OpenCode editor integration including: MCP server configuration, plugin runtime with event hooks, project/global setup management/removal, and instruction file generation.
|
||||
|
||||
## Design
|
||||
|
||||
|
|
@ -36,8 +36,10 @@ Provides comprehensive OpenCode editor integration including: MCP server configu
|
|||
|
||||
**Global Setup** (`opencode-global-setup.ts`):
|
||||
- Similar to project setup but for `~/.config/opencode/`
|
||||
- Cleanup writes: Removes duplicate entries from other config files
|
||||
- Setup cleanup writes: Removes managed duplicate entries from other config files
|
||||
- Remove support: `prepareOpenCodeGlobalRemove()` / `writePreparedOpenCodeGlobalRemove()` remove managed MCP, instruction, and plugin entries plus the managed instruction block
|
||||
- Doctor command: `doctorOpenCodeGlobalSetup()` for status checking
|
||||
- Config precedence handling: chooses the effective config file across `config.json`, `opencode.json`, and `opencode.jsonc`, preserving existing user arrays when safe
|
||||
|
||||
**Status Classification** (`opencode-status.ts`):
|
||||
- MCP entry detection: `isManagedOpenPetsMcpEntry()`
|
||||
|
|
@ -50,6 +52,7 @@ Provides comprehensive OpenCode editor integration including: MCP server configu
|
|||
- Plugin spec builder: `buildOpenCodePluginPreview()`
|
||||
- Instruction path builder: `buildOpenCodeInstructionPath()`
|
||||
- Pet ID validation: `validateOpenPetsPetArg()`
|
||||
- MCP config formatter: `formatOpenCodeMcpConfig()`
|
||||
|
||||
## Flow
|
||||
|
||||
|
|
@ -80,6 +83,11 @@ writePreparedOpenCodeProjectSetup() → Execute writes atomically
|
|||
- `@open-pets/agent-events` - Speech pools and validation
|
||||
- `jsonc-parser` - JSONC config parsing and editing
|
||||
|
||||
**Package Surface**:
|
||||
- Package version: `2.1.1`
|
||||
- Main export (`.`): setup, config, preview, status, and runtime APIs from `dist/index.js`
|
||||
- Server export (`./server`): OpenCode plugin default export from `dist/plugin.js`
|
||||
|
||||
**Consumers**:
|
||||
- `@open-pets/cli` - `configure` command for OpenCode projects
|
||||
|
||||
|
|
@ -87,4 +95,5 @@ writePreparedOpenCodeProjectSetup() → Execute writes atomically
|
|||
- `plugin.ts` - Default plugin export for OpenCode
|
||||
- `prepareOpenCodeProjectSetup()`, `writePreparedOpenCodeProjectSetup()`
|
||||
- `prepareOpenCodeGlobalSetup()`, `writePreparedOpenCodeGlobalSetup()`
|
||||
- `prepareOpenCodeGlobalRemove()`, `writePreparedOpenCodeGlobalRemove()`
|
||||
- Config management utilities
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
- **opencode-plugin-runtime.ts**: Plugin hook implementations (229 lines). `createOpenPetsOpenCodeHooks()`, event classification, tool reaction mapping, lease management, throttling.
|
||||
- **opencode-config.ts**: Config file management (221 lines). Path resolution, JSONC parsing, safe file operations, atomic writes with backups.
|
||||
- **opencode-project-setup.ts**: Project-level setup (182 lines). `prepareOpenCodeProjectSetup()`, `writePreparedOpenCodeProjectSetup()`, instruction block management.
|
||||
- **opencode-global-setup.ts**: Global setup management (354 lines). `prepareOpenCodeGlobalSetup()`, cleanup writes, doctor command, global state classification.
|
||||
- **opencode-status.ts**: Status classification (136 lines). `classifyOpenCodeMcpStatus()`, `classifyOpenCodeInstructionsStatus()`, `classifyOpenCodePluginStatus()`, entry detection functions.
|
||||
- **opencode-previews.ts**: Config entry builders (52 lines). `buildOpenCodeMcpEntry()`, `buildOpenCodePluginPreview()`, `buildOpenCodeInstructionPath()`, `validateOpenPetsPetArg()`.
|
||||
- **opencode-global-setup.ts**: Global setup management (354 lines). `prepareOpenCodeGlobalSetup()`, `prepareOpenCodeGlobalRemove()`, cleanup writes, doctor command, config precedence handling, global state classification.
|
||||
- **opencode-status.ts**: Status classification (147 lines). `classifyOpenCodeMcpStatus()`, `classifyOpenCodeInstructionsStatus()`, `classifyOpenCodePluginStatus()`, managed MCP/plugin/instruction detection helpers, OpenPets-like entry detection.
|
||||
- **opencode-previews.ts**: Config entry builders (55 lines). `buildOpenCodeMcpEntry()`, `buildOpenCodePluginPreview()`, `buildOpenCodeInstructionPath()`, `formatOpenCodeMcpConfig()`, `validateOpenPetsPetArg()`.
|
||||
- **check-opencode-foundation.ts**: Contract validation (excluded from detailed documentation).
|
||||
- **check-opencode-plugin.ts**: Plugin contract validation (excluded from detailed documentation).
|
||||
|
|
|
|||