Compare commits

..
Author SHA1 Message Date
Gemini Nexus Bot ccc5d36a0d chore: update publisher to evandrodevbr and consolidate icon references
- Change author from Draculabo to evandrodevbr in package.json
- Update GitHub publisher config in forge.config.ts (owner/repo/iconUrl)
- Add favicon link in index.html pointing to src/assets/icon.png
- Ensure all app icon references use images/icon.* consistently
2026-04-22 06:01:11 -03:00
Gemini Nexus Bot bfdb58fc81 i18n(pt-BR): add Portuguese translation for install notice dialog 2026-04-22 05:55:34 -03:00
Gemini Nexus Bot b32dd24765 build: add automated WiX extraction script for Windows builds
- Create scripts/build-windows.js that extracts wix314-binaries.zip to tools/wix314/
- Adds extracted dir to PATH so electron-wix-msi can find candle.exe/light.exe
- Adds npm script 'make:win' for one-command Windows installer build
2026-04-22 05:44:41 -03:00
Gemini Nexus Bot 2c81d129f9 fix(streaming): resolve Gemini v1internal SSE wrapped format and OpenCode compatibility
- Fix processStreamResponse to unwrap json.response ?? json for Gemini v1internal wrapped SSE chunks
- Add initial role:assistant delta chunk for Vercel AI SDK / OpenCode compatibility
- Add trafficLogger console transport for real-time debugging
- Add upstream request/response logging throughout proxy paths
- Update proxy-controller.integration and proxy-retry-mock tests for new signatures
- Add comprehensive proxy-stream-debug unit tests (8 cases including wrapped format regression)
- Add OpenCode integration documentation
2026-04-22 05:26:57 -03:00
Gemini Nexus Bot f4b713c3f4 Merge branch 'vk/53e9-pesquise-os-melh' into main 2026-04-22 03:26:52 -03:00
Gemini Nexus Bot 5b130aca66 fix(stream): restore processStreamResponse to original working behavior
The initial role chunk (role:'assistant', content:'') added in a3eabc2
was causing the OpenCode TUI to receive an empty first delta and
discard all subsequent content, showing only the completion time with
no text output.

Restored to the exact streaming behavior from e524a340 that worked:
- Remove the pre-emptive initial chunk entirely
- Move hasEmittedChunk=true back inside pushChunk (original position)
- Use reasoning_content delta field for thought parts instead of
  wrapping in <think> tags (which corrupts plain text responses)
- Remove streamOptions/include_usage/lastUsageMetadata tracking
  (not present in the working version)
- Remove isThinking state machine (was adding </think> noise to output)

Kept from the spec compliance work (a3eabc2):
- toolCallIndex counter for correct finish_reason:'tool_calls' mapping
- Comment explaining why we override Gemini's STOP finish reason
2026-04-22 03:26:51 -03:00
Gemini Nexus Bot 4653fa2b41 Merge branch 'vk/53e9-pesquise-os-melh' into main 2026-04-22 03:17:50 -03:00
Gemini Nexus Bot a3eabc2aaa fix(api): full OpenAI-spec compliance for OpenCode integration
Route fixes:
- Add POST /v1/messages/chat/completions alias so OpenCode clients
  misconfigured with baseURL ending in /v1/messages no longer get 404

Session affinity:
- Extract x-session-affinity HTTP header (sent by OpenCode) and use it
  as the session key for token stickiness, falling back to body-level
  session_id fields if the header is absent

Tool calling (streaming):
- finish_reason is now 'tool_calls' when function calls were emitted in
  the stream (Gemini always sends STOP, so we check toolCallIndex > 0)
- createSyntheticOpenAIStream (stream fallback path) now emits all
  tool_call deltas before the finish chunk instead of silently dropping
  them when content is null

Tool calling (non-streaming):
- content is now always null when tool_calls are present, satisfying
  strict clients that reject non-null content alongside tool_calls
- system_fingerprint field added to OpenAIChatResponse interface and
  response builder for OpenAI spec compliance

Usage accounting:
- thoughtsTokenCount is now included in completion_tokens and
  total_tokens for the streaming usage chunk, fixing undercount for
  thinking/reasoning model runs
2026-04-22 03:17:46 -03:00
Gemini Nexus Bot 099671a4e5 Merge branch 'vk/53e9-pesquise-os-melh' into main 2026-04-22 02:34:27 -03:00
Gemini Nexus Bot 0f9d08018b fix: resolve startup issues with account activation, credit fetch and poll noise
- GoogleAPIService: try loadCodeAssist first for AI credits since
  fetchCredits returns 404 for many accounts; keep fetchCredits as
  fallback, removing the spurious warn on every poll cycle
- CloudAccountRepo: auto-activate the most-recently-used account when
  no account has is_active=1; downgrade noisy [DEBUG] log from info
  to debug level
- StatusBar: increase process-status refetch interval from 10s to 30s
  to reduce redundant IPC calls and log noise
2026-04-22 02:33:51 -03:00
Gemini Nexus Bot 83489d5084 Merge branch 'vk/5b60-veja-o-que-ja-te' into main
Add comprehensive API request/response logging with structured winston logs
2026-04-22 00:21:44 -03:00
Gemini Nexus Bot 0454aecf70 changes 2026-04-22 00:09:29 -03:00
Gemini Nexus Bot 27da62afc9 feat(logging): add comprehensive API request/response logging with structured winston logs
- Add dedicated API logger (api-logger.ts) with winston daily rotation
- Create Fastify plugin (api-logging.plugin.ts) to intercept all API requests/responses
- Log request start, body, response, errors, and summary with precise timestamps
- Auto-mask sensitive data using existing sanitizeObject utility
- Register API logging plugin in NestJS bootstrap (server/main.ts)
- Add npm run dev script with cross-env for automatic DEBUG on Windows
- Install cross-env as devDependency for cross-platform env vars
2026-04-22 00:07:30 -03:00
Gemini Nexus Bot faabf7ba7a docs: rename AntigravityManager to GeminiNexus in cli readme 2026-04-22 00:01:26 -03:00
Gemini Nexus Bot 433dccbaa9 feat: complete rebranding to Gemini Nexus with improved UI, proxies and documentation 2026-04-22 00:00:56 -03:00
semantic-release-bot e524a340cb chore(release): 0.11.1
## [0.11.1](https://github.com/Draculabo/AntigravityManager/compare/v0.11.0...v0.11.1) (2026-04-18)

### 🐛 Bug Fixes

* add unit label to AI credit display ([6577f1c](https://github.com/Draculabo/AntigravityManager/commit/6577f1c51fd155004b81f6171eb5feffa2448128))
* remaining localization gaps ([#156](https://github.com/Draculabo/AntigravityManager/issues/156)) ([7093c98](https://github.com/Draculabo/AntigravityManager/commit/7093c982ba790380cda7d1e8a53891970df6b3cf))
* remove AI credit refresh time scraping ([3e1e10b](https://github.com/Draculabo/AntigravityManager/commit/3e1e10b2a580ebf5a64b9a8982bd812a6a1f2924))

### 📝 Documentation

* **repo:** update project docs ([440a934](https://github.com/Draculabo/AntigravityManager/commit/440a9342800f244287c758b1a77a05fac9096df7))

### ♻️ Code Refactoring

* **ipc:** migrate renderer IPC client to official oRPC client ([d0c282d](https://github.com/Draculabo/AntigravityManager/commit/d0c282dffff24bb74a81cfa523f2f72f949bf8f9))
* standardize runtime guard patterns with lodash-es ([#154](https://github.com/Draculabo/AntigravityManager/issues/154)) ([bc35b96](https://github.com/Draculabo/AntigravityManager/commit/bc35b96b5d2d873cf0f187a57de695aedc32108f))
2026-04-18 17:26:35 +00:00
Draculabo 6577f1c51f fix: add unit label to AI credit display 2026-04-19 01:24:11 +08:00
Draculabo 3e1e10b2a5 fix: remove AI credit refresh time scraping 2026-04-19 01:10:51 +08:00
Lê Trung HiếuandLê Trung Hiếu 7093c982ba fix: remaining localization gaps (#156)
Co-authored-by: Lê Trung Hiếu <[email protected]>
2026-04-14 12:59:57 +08:00
Draculabo 440a934280 docs(repo): update project docs 2026-04-12 15:20:41 +08:00
Draculabo 87887dc4b9 chore(repo): remove tracked local database 2026-04-12 15:20:35 +08:00
Draculabo 00ff50055f chore(security): bump runtime/server dependencies 2026-04-12 15:20:30 +08:00
Draculabo d0c282dfff refactor(ipc): migrate renderer IPC client to official oRPC client 2026-04-12 15:20:22 +08:00
vin bc35b96b5d refactor: standardize runtime guard patterns with lodash-es (#154)
* refactor(ui): replace runtime type guards with lodash-es predicates

* refactor(runtime): migrate typeof checks to lodash-es in core modules

* refactor(ipc): standardize runtime guards with lodash-es

* test(refactor): align runtime guard checks with lodash-es

* docs(agents): add lodash-es runtime guard skill summary
2026-04-12 11:41:56 +08:00
semantic-release-bot 0223e8d807 chore(release): 0.11.0
## [0.11.0](https://github.com/Draculabo/AntigravityManager/compare/v0.10.0...v0.11.0) (2026-04-11)

### ✨ Features

* account management overhaul + 7 issue resolutions ([#147](https://github.com/Draculabo/AntigravityManager/issues/147)) ([ed0dd81](https://github.com/Draculabo/AntigravityManager/commit/ed0dd811a8958928a2702839db07444e89f9fdb2)), closes [#79](https://github.com/Draculabo/AntigravityManager/issues/79) [#126](https://github.com/Draculabo/AntigravityManager/issues/126) [#99](https://github.com/Draculabo/AntigravityManager/issues/99) [#145](https://github.com/Draculabo/AntigravityManager/issues/145) [#134](https://github.com/Draculabo/AntigravityManager/issues/134) [#53](https://github.com/Draculabo/AntigravityManager/issues/53) [#117](https://github.com/Draculabo/AntigravityManager/issues/117)
* **antigravity:** add model specs and update mapping logic ([0a60c0e](https://github.com/Draculabo/AntigravityManager/commit/0a60c0eafa698202722be9c283cb52831e2e0179))
* **cloud-oauth:** support selectable google oauth clients ([bcdf2a6](https://github.com/Draculabo/AntigravityManager/commit/bcdf2a6c24fc992d32b4ae883f2a34c8e64969e3))
* **cloud-status:** classify blocked accounts and surface validation state ([388fbcb](https://github.com/Draculabo/AntigravityManager/commit/388fbcb2a9522285eb0bb4a541b48cd3e1049470))
* **cloud-sync:** persist project and status metadata for synced accounts ([385036d](https://github.com/Draculabo/AntigravityManager/commit/385036d577495caeebf95301542811b08c9f333e))
* **protobuf:** add unified state helpers for oauth payloads ([f970ccb](https://github.com/Draculabo/AntigravityManager/commit/f970ccb2ed1f3a3e9de09221895f0b650c1115b9))
* **proxy:** add upstream error and rate-limit tracking support ([20fffff](https://github.com/Draculabo/AntigravityManager/commit/20fffff29c47d1147e9b429884d9e8b613e7da6f))
* **proxy:** improve parity, rate limiting, and token scheduling ([5401e40](https://github.com/Draculabo/AntigravityManager/commit/5401e40943824d54bc95971229a83934e55ad88f))
* **proxy:** improve quota fallback and proxy parity handling ([4fa0a3c](https://github.com/Draculabo/AntigravityManager/commit/4fa0a3c63dd4b973d0d07feb7351831ad827176d))
* **ui:** update cloud account and provider visibility experience ([c95f494](https://github.com/Draculabo/AntigravityManager/commit/c95f494569b49ff1fae871970764c9f0a3fbda59))

### 🐛 Bug Fixes

* **cloud:** add missing oauth action bindings and validation status helper ([87c17a1](https://github.com/Draculabo/AntigravityManager/commit/87c17a1ed0856572f0f8c46ede923b563b726108))
* **cloud:** fetch AI credits on startup with endpoint fallback and preserve cached credits ([61716bb](https://github.com/Draculabo/AntigravityManager/commit/61716bb51259c0a5899b8659aa864c5772b75cbb))
* **linux:** dynamic executable path detection ([#121](https://github.com/Draculabo/AntigravityManager/issues/121)) ([30e38dc](https://github.com/Draculabo/AntigravityManager/commit/30e38dcb8b9329b68601c9df896fd815ccadc1ec))
* **linux:** harden account-switch relaunch against GPU process crashes ([2c1755e](https://github.com/Draculabo/AntigravityManager/commit/2c1755e578ac91962e813a141ec7d3df6fa61a74))

### 📝 Documentation

* **proxyman:** add setup and debugging guides and link them from readmes ([3100db9](https://github.com/Draculabo/AntigravityManager/commit/3100db93ed92f9f8ef19aceae49a699768e20ca9))

### ✅ Tests

* **proxy:**  refine signature test text ([50a325b](https://github.com/Draculabo/AntigravityManager/commit/50a325be113ef59dd9d0144b84d64f9eb40ea67a))
2026-04-11 11:10:10 +00:00
Draculabo 1fd0fde700 chore(release): remove [skip ci] from semantic-release git commit message 2026-04-11 18:32:34 +08:00
Draculabo 2c1755e578 fix(linux): harden account-switch relaunch against GPU process crashes 2026-04-11 18:03:30 +08:00
Draculabo 3100db93ed docs(proxyman): add setup and debugging guides and link them from readmes 2026-04-11 17:31:13 +08:00
Draculabo e9c4f72883 chore(debug): add proxyman proxy debugging tools 2026-04-11 16:49:56 +08:00
Draculabo 87c17a1ed0 fix(cloud): add missing oauth action bindings and validation status helper 2026-04-11 16:49:10 +08:00
Draculabo 4fa0a3c63d feat(proxy): improve quota fallback and proxy parity handling 2026-04-11 16:47:57 +08:00
Draculabo 388fbcb2a9 feat(cloud-status): classify blocked accounts and surface validation state 2026-04-11 16:46:36 +08:00
Draculabo bcdf2a6c24 feat(cloud-oauth): support selectable google oauth clients 2026-04-11 16:45:39 +08:00
Draculabo 385036d577 feat(cloud-sync): persist project and status metadata for synced accounts 2026-04-11 16:44:30 +08:00
Draculabo f970ccb2ed feat(protobuf): add unified state helpers for oauth payloads 2026-04-11 16:43:43 +08:00
vinandYoussef fce38ec6f0 Continue PR #142: security + Nix docs follow-up (#148)
* feat: add Nix Flake release and update documentation for Nix integration

- Added flake.nix with native library patching for Linux
- Included SHA256 hashes for v0.10.0 DEB releases (x86_64 and arm64)
- Updated README.md with instructions for nix run, NixOS, Home Manager, and devShells
- Generated flake.lock for reproducibility

* refactor: improve Nix Flake patterns for better consumer control

- Extracted derivation logic into a reusable `mkPackage` function
- Exposed `overlays.default` to allow consumers to use their own nixpkgs instance
- Updated `pkgs.system` to `pkgs.stdenv.hostPlatform.system` to follow modern Nix patterns
- Ensured default `packages` definition correctly handles unfree configuration for testing with `nix build` or `nix run`

* docs: update README with advanced Nix integration patterns

- Added Option 1 (Direct Integration) and Option 2 (Overlay) documentation
- Explained how to use the overlay for full nixpkgs control
- Fixed Markdown styling and lint issues in the Nix integration section

* docs: simplify Nix documentation to favor Overlay integration

- Removed "Option 1" (direct integration) as per user feedback
- Promoted the Overlay method as the standard way to consume the package
- Updated devShell examples to rely on the overlayed package set for consistency

* fix(security): prevent master key overwriting on decryption failure and improve account loading resilience

- Modify readSafeStorageKey and generatePrimaryMasterKey to stop if decryption fails for an existing file, preventing permanent data loss when keyring is temporarily locked.
- Update CloudAccountRepo.getAccounts and getAccount to gracefully skip corrupted account records instead of throwing errors.
- Added detailed logging for security failures to aid in diagnostics.

* docs(readme): sync Nix/NixOS integration docs in README.zh-CN

---------

Co-authored-by: Youssef <[email protected]>
2026-04-06 21:34:32 +08:00
Draculabo 61716bb512 fix(cloud): fetch AI credits on startup with endpoint fallback and preserve cached credits 2026-04-06 20:41:42 +08:00
vd ed0dd811a8 feat: account management overhaul + 7 issue resolutions (#147)
- Add smart JSON export/import with schema validation and deduplication
- Add per-account proxy configurations (Issue #79)
- Add advanced account sorting engine (Issues #126 & #99)
- Add compact horizontal layout, AI credits, inline model visibility (Issues #145 & #134)
- Add desktop push notifications for low quota (Issue #53)
- Fix Arch Linux AUR process detection bug (Issue #117)
- Default theme set to Dark Mode (it's better)
- UI Polishing: Improved sort dropdown and symmetrical dialog layouts
- Dev Mode: Resolved port conflicts and single-instance locks
- Updated README and README.zh-CN with new features
2026-04-06 20:32:42 +08:00
Draculabo 50a325be11 test(proxy): refine signature test text 2026-03-06 21:56:52 +08:00
Draculabo 20fffff29c feat(proxy): add upstream error and rate-limit tracking support 2026-03-06 21:56:30 +08:00
Draculabo c95f494569 feat(ui): update cloud account and provider visibility experience 2026-03-06 21:52:11 +08:00
Draculabo 5401e40943 feat(proxy): improve parity, rate limiting, and token scheduling 2026-03-06 21:50:18 +08:00
Draculabo 0a60c0eafa feat(antigravity): add model specs and update mapping logic 2026-03-06 21:48:24 +08:00
30e38dcb8b fix(linux): dynamic executable path detection (#121)
* fix(linux): dynamic executable path detection

The hardcoded path '/usr/share/antigravity/antigravity' was causing
profile switching to fail on Linux when the app was installed in
standard locations like /usr/bin or /opt.

Implemented multi-path probing and /usr/local/bin:/usr/bin:/usr/local/sbin:/var/lib/flatpak/exports/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl lookup to correctly find
the binary for relaunch after profile swap.

* fix: return empty string when no executable found

Address codereview feedback: both windows and linux branches now
return '' when no binary is found at any known location, consistent
with the default case. Callers handle empty string as 'binary not
found' and will surface a clear error.

* chore: update package-lock.json to reflect changes in dependency configurations

---------

Co-authored-by: Muhammad Usama <[email protected]>
Co-authored-by: Draculabo <[email protected]>
2026-02-21 20:11:31 +08:00
semantic-release-bot cad8657148 chore(release): 0.10.0 [skip ci]
## [0.10.0](https://github.com/Draculabo/AntigravityManager/compare/v0.9.2...v0.10.0) (2026-02-19)

### ✨ Features

* Add powerful CLI for account management ([#115](https://github.com/Draculabo/AntigravityManager/issues/115)) ([b949764](https://github.com/Draculabo/AntigravityManager/commit/b9497648c6a7f50dd9b05a7f3d54ac95fa373349))
* Implement provider groupings with account calculator and compre… ([#113](https://github.com/Draculabo/AntigravityManager/issues/113)) ([1e59a1c](https://github.com/Draculabo/AntigravityManager/commit/1e59a1c901d5feedb1089015060fc8c31b614d4d))

### 🐛 Bug Fixes

* refactor layout containers for proxy and settings pages to ensure full height and unified scrolling ([#102](https://github.com/Draculabo/AntigravityManager/issues/102)) ([caaaaf5](https://github.com/Draculabo/AntigravityManager/commit/caaaaf571864aeb2bd68dc271972fd197c999bd1))
* **statusbar:** reduce process polling from 2s to 10s to prevent heap corruption crash ([#110](https://github.com/Draculabo/AntigravityManager/issues/110)) ([118fad5](https://github.com/Draculabo/AntigravityManager/commit/118fad567eeaac4e173b97d3eeb84e1f3edd4556))
2026-02-19 14:03:23 +00:00
AngeloooooandDraculabo 8ed4593f73 Refactor Gemini model display: filter old versions, group low/high pкo, and improve naming (#114)
* Refactor Gemini model display: filter old versions, group low/high pro, and improve naming

* Fix potential negative minutes and refine Gemini 3 Pro grouping logic based on PR feedback

* The account change process has been sped up

* Address PR review: enhance error message and parameterize polling interval in process handler

* Address PR review: fix potential race condition in database backup logic

* fix: prevent renderer white screen and harden sentry gating

* feat:  localize model labels

* chore: enable hoisted node-linker

* chore: ignore .agent/.claude/.codex in prettier

---------

Co-authored-by: Draculabo <[email protected]>
2026-02-19 19:37:30 +08:00
Angelooooo b9497648c6 feat: Add powerful CLI for account management (#115)
* feat: Add powerful CLI for account management

- 18 commands for quota monitoring, switching, validation
- Interactive mode with keyboard navigation
- Smart auto-switch based on quotas
- Alias support for quick access
- Token auto-refresh and diagnostics
- Full documentation in cli/README.md"

* fix: Address code review issues - Add SQLite transactions for atomicity\n- Add input validation for import_accounts\n- Add per-account error handling in bulk operations\n- Add graceful error handling in interactive mode\n- Add validation for varint encoding (negative values)\n- Improve user feedback on partial failures
2026-02-19 19:19:25 +08:00
ArtemisAIandDraculabo 1e59a1c901 feat: Implement provider groupings with account calculator and compre… (#113)
* feat: Implement provider groupings with account calculator and comprehensive fixes

- Add provider detection utilities (detectProvider, PROVIDER_REGISTRY) for Claude/Gemini/Others categorization
- Implement grouping logic (groupModelsByProvider, calculateProviderStats, calculateAccountStats) with health status calculation
- Create useProviderGrouping hook for collapsible state management and persistence
- Add ProviderGroup component with health indicators, progress bars, and model visibility integration
- Enhance CloudAccountList with account-level calculator showing overall percentage and earliest reset time
- Implement grid layout selector (auto/2-col/3-col/list) with config persistence and tooltips
- Add global quota aggregate bar across all accounts in top statistics
- Fix model visibility settings regression by accessing account.quota.models instead of account.quota
- Show 'Rate Limited' instead of '0%' for exhausted quotas with consistent getQuotaLabel function
- Add comprehensive i18n translations for layout selector and rate limited labels (en, zh-CN, ru)
- Disable batch operations when provider groupings are enabled to prevent conflicts
- Include unit tests for provider detection logic and grouping functionality

Addresses critical issues: model list regression, rate limited display, layout control, global quota visibility, and consistent quota labeling across all components.

* fix: resolve ESLint no-ex-assign violations in proxy service

- Fix exception parameter reassignment in catch blocks
- Rename catch parameters to avoid no-ex-assign rule violations
- Maintain same error handling logic while complying with linting rules

* fix: add missing Models tab to settings page

- Add Models tab between General and Proxy tabs
- Import and integrate ModelVisibilitySettings component
- Fix critical regression where model visibility settings were inaccessible

* fix: add missing React hook imports in settings route

* test: remove obsolete tests for removed controller APIs

---------

Co-authored-by: Draculabo <[email protected]>
2026-02-18 16:38:15 +08:00
ArtemisAI 118fad567e fix(statusbar): reduce process polling from 2s to 10s to prevent heap corruption crash (#110)
## Problem
Application crashes with exit code 0xC0000374 (heap corruption) at ~7 minute mark due to excessive process monitoring subprocess spawning.

## Root Cause
StatusBar component continuously polls process status every 2 seconds (2000ms refetchInterval), triggering:
- ~210 subprocess invocations over 7 minutes
- Each invocation spawns find-process subprocess
- Accumulated process handles lead to heap corruption
- OS terminates renderer with 0xC0000374 exit code

## Solution
Reduce UI polling interval from 2 seconds to 10 seconds:
- Reduces subprocess spawning by 5x (210  42 calls over 7 minutes)
- 10-second UI update delay is imperceptible to users
- Purely cosmetic feature doesn't require real-time responsiveness
- Expected result: App survives 30+ minutes instead of 7 minutes

## Testing
- Can be tested by running app continuously for 30+ minutes
- Monitor for absence of 0xC0000374 exit codes
- Verify StatusBar still updates (with 10s delay instead of 2s)

## Impact
- Risk: LOW (configuration-only change)
- User impact: Minimal (UI status indicator updates slightly slower)
- System impact: CRITICAL - Prevents crash loop
2026-02-14 22:14:28 +08:00
vin 41a3a10ab6 chore(deps): move @tailwindcss/vite to devDependencie (#106) 2026-02-12 21:12:14 +08:00
gu yan caaaaf5718 fix: refactor layout containers for proxy and settings pages to ensure full height and unified scrolling (#102) 2026-02-12 20:18:01 +08:00
gu yan 4dc4e4e5a7 add model list endpoint (#96)
* add model list endpoint

* fix: guard models list endpoint errors
2026-02-12 00:38:12 +08:00
semantic-release-bot 6f8e8085ef chore(release): 0.9.2 [skip ci]
## [0.9.2](https://github.com/Draculabo/AntigravityManager/compare/v0.9.1...v0.9.2) (2026-02-11)

### 🐛 Bug Fixes

* project id fallback and stream error regression ([#94](https://github.com/Draculabo/AntigravityManager/issues/94)) ([caf9d58](https://github.com/Draculabo/AntigravityManager/commit/caf9d5849ac86f4c83cb7b33804b873e0c2ff545))
2026-02-11 09:13:22 +00:00
vin caf9d5849a fix: project id fallback and stream error regression (#94)
* fix(proxy): harden project id handling for upgraded accounts

* fix(proxy): parse stream error payloads to preserve upstream project errors
2026-02-11 17:11:27 +08:00
semantic-release-bot 4fba53a382 chore(release): 0.9.1 [skip ci]
## [0.9.1](https://github.com/Draculabo/AntigravityManager/compare/v0.9.0...v0.9.1) (2026-02-11)

### 🐛 Bug Fixes

* project id forwarding regression ([#93](https://github.com/Draculabo/AntigravityManager/issues/93)) ([ab78d93](https://github.com/Draculabo/AntigravityManager/commit/ab78d9325213623067b5bd867b35464cd4eefd73))
2026-02-11 04:37:14 +00:00
vin ab78d93252 fix: project id forwarding regression (#93)
* fix(proxy): avoid empty project context and hydrate project_id from account token

* test(proxy): add regressions for empty project and token project_id hydration

* test(scripts): add live request validator for project_id forwarding
2026-02-11 12:36:01 +08:00
313 changed files with 19985 additions and 6169 deletions

No files matched your search

+5 -2
View File
@@ -5,8 +5,8 @@ license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
version: '1.0'
generatedBy: '1.1.1'
---
Implement tasks from an OpenSpec change.
@@ -25,9 +25,11 @@ Implement tasks from an OpenSpec change.
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
@@ -139,6 +141,7 @@ What would you like to do?
```
**Guardrails**
- Keep going through tasks until done or blocked
- Always read context files before starting (from the apply instructions output)
- If task is ambiguous, pause and ask before implementing
@@ -5,8 +5,8 @@ license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
version: '1.0'
generatedBy: '1.1.1'
---
Archive a completed change in the experimental workflow.
@@ -68,6 +68,7 @@ Archive a completed change in the experimental workflow.
5. **Perform the archive**
Create the archive directory if it doesn't exist:
```bash
mkdir -p openspec/changes/archive
```
@@ -105,6 +106,7 @@ All artifacts complete. All tasks complete.
```
**Guardrails**
- Always prompt for change selection if not provided
- Use artifact graph (openspec status --json) for completion checking
- Don't block archive on warnings - just inform and confirm
@@ -5,8 +5,8 @@ license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
version: '1.0'
generatedBy: '1.1.1'
---
Archive multiple completed changes in a single operation.
@@ -37,16 +37,16 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
For each selected change, collect:
a. **Artifact status** - Run `openspec status --change "<name>" --json`
- Parse `schemaName` and `artifacts` list
- Note which artifacts are `done` vs other states
- Parse `schemaName` and `artifacts` list
- Note which artifacts are `done` vs other states
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
- If no tasks file exists, note as "No tasks"
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
- If no tasks file exists, note as "No tasks"
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
- List which capability specs exist
- For each, extract requirement names (lines matching `### Requirement: <name>`)
- List which capability specs exist
- For each, extract requirement names (lines matching `### Requirement: <name>`)
4. **Detect spec conflicts**
@@ -66,18 +66,18 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
b. **Search the codebase** for implementation evidence:
- Look for code implementing requirements from each delta spec
- Check for related files, functions, or tests
- Look for code implementing requirements from each delta spec
- Check for related files, functions, or tests
c. **Determine resolution**:
- If only one change is actually implemented -> sync that one's specs
- If both implemented -> apply in chronological order (older first, newer overwrites)
- If neither implemented -> skip spec sync, warn user
- If only one change is actually implemented -> sync that one's specs
- If both implemented -> apply in chronological order (older first, newer overwrites)
- If neither implemented -> skip spec sync, warn user
d. **Record resolution** for each conflict:
- Which change's specs to apply
- In what order (if both)
- Rationale (what was found in codebase)
- Which change's specs to apply
- In what order (if both)
- Rationale (what was found in codebase)
6. **Show consolidated status table**
@@ -93,12 +93,14 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
```
For conflicts, show the resolution:
```
* Conflict resolution:
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
```
For incomplete changes, show warnings:
```
Warnings:
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
@@ -107,7 +109,6 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
7. **Confirm batch operation**
Use **AskUserQuestion tool** with a single confirmation:
- "Archive N changes?" with options based on status
- Options might include:
- "Archive all N changes"
@@ -121,20 +122,21 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
Process changes in the determined order (respecting conflict resolution):
a. **Sync specs** if delta specs exist:
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
- For conflicts, apply in resolved order
- Track if sync was done
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
- For conflicts, apply in resolved order
- Track if sync was done
b. **Perform the archive**:
```bash
mkdir -p openspec/changes/archive
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
```
```bash
mkdir -p openspec/changes/archive
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
```
c. **Track outcome** for each change:
- Success: archived successfully
- Failed: error during archive (record error)
- Skipped: user chose not to archive (if applicable)
- Success: archived successfully
- Failed: error during archive (record error)
- Skipped: user chose not to archive (if applicable)
9. **Display summary**
@@ -157,6 +159,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
```
If any failures:
```
Failed 1 change:
- some-change: Archive directory already exists
@@ -165,6 +168,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
**Conflict Resolution Examples**
Example 1: Only one implemented
```
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
@@ -180,6 +184,7 @@ Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
```
Example 2: Both implemented
```
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
@@ -233,6 +238,7 @@ No active changes found. Use `/opsx:new` to create a new change.
```
**Guardrails**
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
- Always prompt for selection, never auto-select
- Detect spec conflicts early and resolve by checking codebase
@@ -5,8 +5,8 @@ license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
version: '1.0'
generatedBy: '1.1.1'
---
Continue working on a change by creating the next artifact.
@@ -30,9 +30,11 @@ Continue working on a change by creating the next artifact.
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check current status**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand current state. The response includes:
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
@@ -40,7 +42,7 @@ Continue working on a change by creating the next artifact.
3. **Act based on status**:
---
***
**If all artifacts are complete (`isComplete: true`)**:
- Congratulate the user
@@ -48,7 +50,7 @@ Continue working on a change by creating the next artifact.
- Suggest: "All artifacts created! You can now implement this change or archive it."
- STOP
---
***
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
- Pick the FIRST artifact with `status: "ready"` from the status output
@@ -71,7 +73,7 @@ Continue working on a change by creating the next artifact.
- Show what was created and what's now unlocked
- STOP after creating ONE artifact
---
***
**If no artifacts are ready (all blocked)**:
- This shouldn't happen with a valid schema
@@ -85,6 +87,7 @@ Continue working on a change by creating the next artifact.
**Output**
After each invocation, show:
- Which artifact was created
- Schema workflow being used
- Current progress (N/M complete)
@@ -98,6 +101,7 @@ The artifact types and their purpose depend on the schema. Use the `instruction`
Common artifact patterns:
**spec-driven schema** (proposal → specs → design → tasks):
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
- The Capabilities section is critical - each capability listed will need a spec file.
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
@@ -107,6 +111,7 @@ Common artifact patterns:
For other schemas, follow the `instruction` field from the CLI output.
**Guardrails**
- Create ONE artifact per invocation
- Always read dependency artifacts before creating a new one
- Never skip artifacts or create out of order
+20 -9
View File
@@ -5,8 +5,8 @@ license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
version: '1.0'
generatedBy: '1.1.1'
---
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
@@ -33,24 +33,28 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher
Depending on what the user brings, you might:
**Explore the problem space**
- Ask clarifying questions that emerge from what they said
- Challenge assumptions
- Reframe the problem
- Find analogies
**Investigate the codebase**
- Map existing architecture relevant to the discussion
- Find integration points
- Identify patterns already in use
- Surface hidden complexity
**Compare options**
- Brainstorm multiple approaches
- Build comparison tables
- Sketch tradeoffs
- Recommend a path (if asked)
**Visualize**
```
┌─────────────────────────────────────────┐
│ Use ASCII diagrams liberally │
@@ -69,6 +73,7 @@ Depending on what the user brings, you might:
```
**Surface risks and unknowns**
- Identify what could go wrong
- Find gaps in understanding
- Suggest spikes or investigations
@@ -82,11 +87,13 @@ You have full context of the OpenSpec system. Use it naturally, don't force it.
### Check for context
At the start, quickly check what exists:
```bash
openspec list --json
```
This tells you:
- If there are active changes
- Their names, schemas, and status
- What the user might be working on
@@ -115,14 +122,14 @@ If the user mentions a change or you detect one is relevant:
3. **Offer to capture when decisions are made**
| Insight Type | Where to Capture |
|--------------|------------------|
| Insight Type | Where to Capture |
| -------------------------- | ---------------------------- |
| New requirement discovered | `specs/<capability>/spec.md` |
| Requirement changed | `specs/<capability>/spec.md` |
| Design decision made | `design.md` |
| Scope changed | `proposal.md` |
| New work identified | `tasks.md` |
| Assumption invalidated | Relevant artifact |
| Requirement changed | `specs/<capability>/spec.md` |
| Design decision made | `design.md` |
| Scope changed | `proposal.md` |
| New work identified | `tasks.md` |
| Assumption invalidated | Relevant artifact |
Example offers:
- "That's a design decision. Capture it in design.md?"
@@ -147,6 +154,7 @@ If the user mentions a change or you detect one is relevant:
## Handling Different Entry Points
**User brings a vague idea:**
```
User: I'm thinking about adding real-time collaboration
@@ -170,6 +178,7 @@ You: Real-time collab is a big space. Let me think about this...
```
**User brings a specific problem:**
```
User: The auth system is a mess
@@ -201,6 +210,7 @@ You: [reads codebase]
```
**User is stuck mid-implementation:**
```
User: /opsx:explore add-auth-system
The OAuth integration is more complex than expected
@@ -218,6 +228,7 @@ You: [reads change artifacts]
```
**User wants to compare options:**
```
User: Should we use Postgres or SQLite?
+29 -22
View File
@@ -5,8 +5,8 @@ license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
version: '1.0'
generatedBy: '1.1.1'
---
Fast-forward through artifact creation - generate everything needed to start implementation in one go.
@@ -18,6 +18,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
1. **If no clear input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
@@ -25,15 +26,19 @@ Fast-forward through artifact creation - generate everything needed to start imp
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
```bash
openspec new change "<name>"
```
This creates a scaffolded change at `openspec/changes/<name>/`.
3. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
@@ -45,30 +50,30 @@ Fast-forward through artifact creation - generate everything needed to start imp
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "✓ Created <artifact-id>"
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "✓ Created <artifact-id>"
b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
5. **Show final status**
```bash
@@ -78,6 +83,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
@@ -94,6 +100,7 @@ After completing all artifacts, summarize:
- These guide what you write, but should never appear in the output
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
+11 -2
View File
@@ -5,8 +5,8 @@ license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
version: '1.0'
generatedBy: '1.1.1'
---
Start a new change using the experimental artifact-driven approach.
@@ -18,6 +18,7 @@ Start a new change using the experimental artifact-driven approach.
1. **If no clear input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
@@ -35,24 +36,30 @@ Start a new change using the experimental artifact-driven approach.
**Otherwise**: Omit `--schema` to use the default.
3. **Create the change directory**
```bash
openspec new change "<name>"
```
Add `--schema <name>` only if the user requested a specific workflow.
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
4. **Show the artifact status**
```bash
openspec status --change "<name>"
```
This shows which artifacts need to be created and which are ready (dependencies satisfied).
5. **Get instructions for the first artifact**
The first artifact depends on the schema (e.g., `proposal` for spec-driven).
Check the status output to find the first artifact with status "ready".
```bash
openspec instructions <first-artifact-id> --change "<name>"
```
This outputs the template and context for creating the first artifact.
6. **STOP and wait for user direction**
@@ -60,6 +67,7 @@ Start a new change using the experimental artifact-driven approach.
**Output**
After completing the steps, summarize:
- Change name and location
- Schema/workflow being used and its artifact sequence
- Current status (0/N artifacts complete)
@@ -67,6 +75,7 @@ After completing the steps, summarize:
- Prompt: "Ready to create the first artifact? Just describe what this change is about and I'll draft it, or ask me to continue."
**Guardrails**
- Do NOT create any artifacts yet - just show the instructions
- Do NOT advance beyond showing the first artifact template
- If the name is invalid (not kebab-case), ask for a valid name
+26 -6
View File
@@ -5,8 +5,8 @@ license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
version: '1.0'
generatedBy: '1.1.1'
---
Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step.
@@ -22,6 +22,7 @@ openspec status --json 2>&1 || echo "NOT_INITIALIZED"
```
**If not initialized:**
> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`.
Stop here if not initialized.
@@ -66,6 +67,7 @@ Scan the codebase for small improvement opportunities. Look for:
6. **Missing validation** - User input handlers without validation
Also check recent git activity:
```bash
git log --oneline -10 2>/dev/null || echo "No git history"
```
@@ -101,6 +103,7 @@ Which task interests you? (Pick a number or describe your own)
```
**If nothing found:** Fall back to asking what the user wants to build:
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
### Scope Guardrail
@@ -133,6 +136,7 @@ Before we create a change, let me quickly show you **explore mode**—it's how y
```
Spend 1-2 minutes investigating the relevant code:
- Read the file(s) involved
- Draw a quick ASCII diagram if it helps
- Note any considerations
@@ -158,6 +162,7 @@ Now let's create a change to hold our work.
## Phase 4: Create the Change
**EXPLAIN:**
```
## Creating a Change
@@ -167,21 +172,25 @@ Let me create one for our task.
```
**DO:** Create the change with a derived kebab-case name:
```bash
openspec new change "<derived-name>"
```
**SHOW:**
```
Created: `openspec/changes/<name>/`
The folder structure:
```
openspec/changes/<name>/
├── proposal.md ← Why we're doing this (empty, we'll fill it)
├── design.md ← How we'll build it (empty)
├── specs/ ← Detailed requirements (empty)
└── tasks.md ← Implementation checklist (empty)
├── proposal.md ← Why we're doing this (empty, we'll fill it)
├── design.md ← How we'll build it (empty)
├── specs/ ← Detailed requirements (empty)
└── tasks.md ← Implementation checklist (empty)
```
Now let's fill in the first artifact—the proposal.
@@ -192,6 +201,7 @@ Now let's fill in the first artifact—the proposal.
## Phase 5: Proposal
**EXPLAIN:**
```
## The Proposal
@@ -236,9 +246,11 @@ Does this capture the intent? I can adjust before we save it.
**PAUSE** - Wait for user approval/feedback.
After approval, save the proposal:
```bash
openspec instructions proposal --change "<name>" --json
```
Then write the content to `openspec/changes/<name>/proposal.md`.
```
@@ -252,6 +264,7 @@ Next up: specs.
## Phase 6: Specs
**EXPLAIN:**
```
## Specs
@@ -261,6 +274,7 @@ For a small task like this, we might only need one spec file.
```
**DO:** Create the spec file:
```bash
mkdir -p openspec/changes/<name>/specs/<capability-name>
```
@@ -296,6 +310,7 @@ Save to `openspec/changes/<name>/specs/<capability>/spec.md`.
## Phase 7: Design
**EXPLAIN:**
```
## Design
@@ -341,6 +356,7 @@ Save to `openspec/changes/<name>/design.md`.
## Phase 8: Tasks
**EXPLAIN:**
```
## Tasks
@@ -379,6 +395,7 @@ Save to `openspec/changes/<name>/tasks.md`.
## Phase 9: Apply (Implementation)
**EXPLAIN:**
```
## Implementation
@@ -413,6 +430,7 @@ The change is implemented! One more step—let's archive it.
## Phase 10: Archive
**EXPLAIN:**
```
## Archiving
@@ -422,11 +440,13 @@ Archived changes become your project's decision history—you can always find th
```
**DO:**
```bash
openspec archive "<name>"
```
**SHOW:**
```
Archived to: `openspec/changes/archive/YYYY-MM-DD-<name>/`
+26 -20
View File
@@ -5,8 +5,8 @@ license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
version: '1.0'
generatedBy: '1.1.1'
---
Sync delta specs from a change to main specs.
@@ -47,28 +47,28 @@ This is an **agent-driven** operation - you will read delta specs and directly e
c. **Apply changes intelligently**:
**ADDED Requirements:**
- If requirement doesn't exist in main spec → add it
- If requirement already exists → update it to match (treat as implicit MODIFIED)
**ADDED Requirements:**
- If requirement doesn't exist in main spec → add it
- If requirement already exists → update it to match (treat as implicit MODIFIED)
**MODIFIED Requirements:**
- Find the requirement in main spec
- Apply the changes - this can be:
- Adding new scenarios (don't need to copy existing ones)
- Modifying existing scenarios
- Changing the requirement description
- Preserve scenarios/content not mentioned in the delta
**MODIFIED Requirements:**
- Find the requirement in main spec
- Apply the changes - this can be:
- Adding new scenarios (don't need to copy existing ones)
- Modifying existing scenarios
- Changing the requirement description
- Preserve scenarios/content not mentioned in the delta
**REMOVED Requirements:**
- Remove the entire requirement block from main spec
**REMOVED Requirements:**
- Remove the entire requirement block from main spec
**RENAMED Requirements:**
- Find the FROM requirement, rename to TO
**RENAMED Requirements:**
- Find the FROM requirement, rename to TO
d. **Create new main spec** if capability doesn't exist yet:
- Create `openspec/specs/<capability>/spec.md`
- Add Purpose section (can be brief, mark as TBD)
- Add Requirements section with the ADDED requirements
- Create `openspec/specs/<capability>/spec.md`
- Add Purpose section (can be brief, mark as TBD)
- Add Requirements section with the ADDED requirements
4. **Show summary**
@@ -82,16 +82,20 @@ This is an **agent-driven** operation - you will read delta specs and directly e
## ADDED Requirements
### Requirement: New Feature
The system SHALL do something new.
#### Scenario: Basic case
- **WHEN** user does X
- **THEN** system does Y
## MODIFIED Requirements
### Requirement: Existing Feature
#### Scenario: New scenario to add
- **WHEN** user does A
- **THEN** system does B
@@ -108,8 +112,9 @@ The system SHALL do something new.
**Key Principle: Intelligent Merging**
Unlike programmatic merging, you can apply **partial updates**:
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
- The delta represents *intent*, not a wholesale replacement
- The delta represents _intent_, not a wholesale replacement
- Use your judgment to merge changes sensibly
**Output On Success**
@@ -131,6 +136,7 @@ Main specs are now updated. The change remains active - archive when implementat
```
**Guardrails**
- Read both delta and main specs before making changes
- Preserve existing content not mentioned in delta
- If something is unclear, ask for clarification
@@ -5,8 +5,8 @@ license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.1.1"
version: '1.0'
generatedBy: '1.1.1'
---
Verify that an implementation matches the change artifacts (specs, tasks, design).
@@ -26,9 +26,11 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifacts exist for this change
@@ -110,6 +112,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
8. **Generate Verification Report**
**Summary Scorecard**:
```
## Verification Report: <change-name>
@@ -122,7 +125,6 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
```
**Issues by Priority**:
1. **CRITICAL** (Must fix before archive):
- Incomplete tasks
- Missing requirement implementations
@@ -161,6 +163,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
**Output Format**
Use clear markdown with:
- Table for summary scorecard
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
- Code references in format: `file.ts:123`
+5 -5
View File
@@ -1,13 +1,13 @@
---
name: antigravity-manager
description: Comprehensive guide to Antigravity Manager architecture, workflows, and development. Use this to understand how to work on the project.
name: geminiNexus-manager
description: Comprehensive guide to Gemini Nexus Manager architecture, workflows, and development. Use this to understand how to work on the project.
---
# Antigravity Manager Developer Guide
# Gemini Nexus Manager Developer Guide
## 🏗️ Architecture Overview
Antigravity Manager is a hybrid Desktop Application built with Electron, React, and NestJS. It follows a modular architecture where the frontend (Renderer) communicates with the backend (Main) via type-safe IPC (ORPC).
Gemini Nexus Manager is a hybrid Desktop Application built with Electron, React, and NestJS. It follows a modular architecture where the frontend (Renderer) communicates with the backend (Main) via type-safe IPC (ORPC).
```mermaid
graph TD
@@ -69,7 +69,7 @@ The project uses `orpc` for type-safe communication.
### Database Access
Data is stored in a local SQLite file (`test.db` in dev, user data in prod).
Data is stored in a local SQLite file.
- Use `Better-SQLite3` for direct access.
- Logic should be encapsulated in `src/services` or `src/ipc`.
+69 -52
View File
@@ -2,6 +2,7 @@
name: ui-ux-pro-max
description: UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 9 stacks.
---
# ui-ux-pro-max
Comprehensive design guide for web and mobile applications. Contains 67 styles, 96 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 13 technology stacks. Searchable database with priority-based recommendations.
@@ -17,16 +18,19 @@ python3 --version || python --version
If Python is not installed, install it based on user's OS:
**macOS:**
```bash
brew install python3
```
**Ubuntu/Debian:**
```bash
sudo apt update && sudo apt install python3
```
**Windows:**
```powershell
winget install Python.Python.3.12
```
@@ -40,6 +44,7 @@ When user requests UI/UX work (design, build, create, implement, review, fix, im
### Step 1: Analyze User Requirements
Extract key information from user request:
- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc.
- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc.
- **Industry**: healthcare, fintech, gaming, education, etc.
@@ -54,12 +59,14 @@ python3 skills/ui-ux-pro-max/scripts/search.py "<product_type> <industry> <keywo
```
This command:
1. Searches 5 domains in parallel (product, style, color, landing, typography)
2. Applies reasoning rules from `ui-reasoning.csv` to select best matches
3. Returns complete design system: pattern, style, colors, typography, effects
4. Includes anti-patterns to avoid
**Example:**
```bash
python3 skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --design-system -p "Serenity Spa"
```
@@ -73,18 +80,22 @@ python3 skills/ui-ux-pro-max/scripts/search.py "<query>" --design-system --persi
```
This creates:
- `design-system/MASTER.md` — Global Source of Truth with all design rules
- `design-system/pages/` — Folder for page-specific overrides
**With page-specific override:**
```bash
python3 skills/ui-ux-pro-max/scripts/search.py "<query>" --design-system --persist -p "Project Name" --page "dashboard"
```
This also creates:
- `design-system/pages/dashboard.md` — Page-specific deviations from Master
**How hierarchical retrieval works:**
1. When building a specific page (e.g., "Checkout"), first check `design-system/pages/checkout.md`
2. If the page file exists, its rules **override** the Master file
3. If not, use `design-system/MASTER.md` exclusively
@@ -99,13 +110,13 @@ python3 skills/ui-ux-pro-max/scripts/search.py "<keyword>" --domain <domain> [-n
**When to use detailed searches:**
| Need | Domain | Example |
|------|--------|---------|
| More style options | `style` | `--domain style "glassmorphism dark"` |
| Chart recommendations | `chart` | `--domain chart "real-time dashboard"` |
| UX best practices | `ux` | `--domain ux "animation accessibility"` |
| Alternative fonts | `typography` | `--domain typography "elegant luxury"` |
| Landing structure | `landing` | `--domain landing "hero social-proof"` |
| Need | Domain | Example |
| --------------------- | ------------ | --------------------------------------- |
| More style options | `style` | `--domain style "glassmorphism dark"` |
| Chart recommendations | `chart` | `--domain chart "real-time dashboard"` |
| UX best practices | `ux` | `--domain ux "animation accessibility"` |
| Alternative fonts | `typography` | `--domain typography "elegant luxury"` |
| Landing structure | `landing` | `--domain landing "hero social-proof"` |
### Step 4: Stack Guidelines (Default: html-tailwind)
@@ -123,32 +134,32 @@ Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`
### Available Domains
| Domain | Use For | Example Keywords |
|--------|---------|------------------|
| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service |
| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism |
| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern |
| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service |
| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof |
| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie |
| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading |
| `react` | React/Next.js performance | waterfall, bundle, suspense, memo, rerender, cache |
| `web` | Web interface guidelines | aria, focus, keyboard, semantic, virtualize |
| `prompt` | AI prompts, CSS keywords | (style name) |
| Domain | Use For | Example Keywords |
| ------------ | ------------------------------------ | -------------------------------------------------------- |
| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service |
| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism |
| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern |
| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service |
| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof |
| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie |
| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading |
| `react` | React/Next.js performance | waterfall, bundle, suspense, memo, rerender, cache |
| `web` | Web interface guidelines | aria, focus, keyboard, semantic, virtualize |
| `prompt` | AI prompts, CSS keywords | (style name) |
### Available Stacks
| Stack | Focus |
|-------|-------|
| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) |
| `react` | State, hooks, performance, patterns |
| `nextjs` | SSR, routing, images, API routes |
| `vue` | Composition API, Pinia, Vue Router |
| `svelte` | Runes, stores, SvelteKit |
| `swiftui` | Views, State, Navigation, Animation |
| `react-native` | Components, Navigation, Lists |
| `flutter` | Widgets, State, Layout, Theming |
| `shadcn` | shadcn/ui components, theming, forms, patterns |
| Stack | Focus |
| ----------------- | ----------------------------------------------------- |
| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) |
| `react` | State, hooks, performance, patterns |
| `nextjs` | SSR, routing, images, API routes |
| `vue` | Composition API, Pinia, Vue Router |
| `svelte` | Runes, stores, SvelteKit |
| `swiftui` | Views, State, Navigation, Animation |
| `react-native` | Components, Navigation, Lists |
| `flutter` | Widgets, State, Layout, Theming |
| `shadcn` | shadcn/ui components, theming, forms, patterns |
| `jetpack-compose` | Composables, Modifiers, State Hoisting, Recomposition |
---
@@ -158,6 +169,7 @@ Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`
**User request:** "Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp"
### Step 1: Analyze Requirements
- Product type: Beauty/Spa service
- Style keywords: elegant, professional, soft
- Industry: Beauty/Wellness
@@ -222,37 +234,37 @@ These are frequently overlooked issues that make UI look unprofessional:
### Icons & Visual Elements
| Rule | Do | Don't |
|------|----|----- |
| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons |
| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout |
| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths |
| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly |
| Rule | Do | Don't |
| -------------------------- | ----------------------------------------------- | -------------------------------------- |
| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons |
| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout |
| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths |
| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly |
### Interaction & Cursor
| Rule | Do | Don't |
|------|----|----- |
| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements |
| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive |
| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) |
| Rule | Do | Don't |
| ---------------------- | ----------------------------------------------------- | -------------------------------------------- |
| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements |
| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive |
| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) |
### Light/Dark Mode Contrast
| Rule | Do | Don't |
|------|----|----- |
| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) |
| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text |
| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter |
| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) |
| Rule | Do | Don't |
| ------------------------- | ----------------------------------- | --------------------------------------- |
| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) |
| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text |
| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter |
| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) |
### Layout & Spacing
| Rule | Do | Don't |
|------|----|----- |
| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` |
| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements |
| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths |
| Rule | Do | Don't |
| ------------------------ | ----------------------------------- | -------------------------------------- |
| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` |
| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements |
| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths |
---
@@ -261,6 +273,7 @@ These are frequently overlooked issues that make UI look unprofessional:
Before delivering UI code, verify these items:
### Visual Quality
- [ ] No emojis used as icons (use SVG instead)
- [ ] All icons from consistent icon set (Heroicons/Lucide)
- [ ] Brand logos are correct (verified from Simple Icons)
@@ -268,24 +281,28 @@ Before delivering UI code, verify these items:
- [ ] Use theme colors directly (bg-primary) not var() wrapper
### Interaction
- [ ] All clickable elements have `cursor-pointer`
- [ ] Hover states provide clear visual feedback
- [ ] Transitions are smooth (150-300ms)
- [ ] Focus states visible for keyboard navigation
### Light/Dark Mode
- [ ] Light mode text has sufficient contrast (4.5:1 minimum)
- [ ] Glass/transparent elements visible in light mode
- [ ] Borders visible in both modes
- [ ] Test both modes before delivery
### Layout
- [ ] Floating elements have proper spacing from edges
- [ ] No content hidden behind fixed navbars
- [ ] Responsive at 375px, 768px, 1024px, 1440px
- [ ] No horizontal scroll on mobile
### Accessibility
- [ ] All images have alt text
- [ ] Form inputs have labels
- [ ] Color is not the only indicator
@@ -73,16 +73,10 @@ function Composer({
) : isThread ? (
<AlsoSendToChannelField id={channelId} />
) : null}
{isEditing ? (
<EditActions />
) : isForwarding ? (
<ForwardActions />
) : (
<DefaultActions />
)}
{isEditing ? <EditActions /> : isForwarding ? <ForwardActions /> : <DefaultActions />}
<Footer onSubmit={onSubmit} />
</form>
)
);
}
```
@@ -102,7 +96,7 @@ function ChannelComposer() {
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
)
);
}
// Thread composer - adds "also send to channel" field
@@ -118,7 +112,7 @@ function ThreadComposer({ channelId }: { channelId: string }) {
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
)
);
}
// Edit composer - different footer actions
@@ -133,7 +127,7 @@ function EditComposer() {
<Composer.SaveEdit />
</Composer.Footer>
</Composer.Frame>
)
);
}
```
@@ -177,25 +171,21 @@ function Composer({
</Footer>
)}
</form>
)
);
}
```
**Correct: compound components with shared context**
```tsx
const ComposerContext = createContext<ComposerContextValue | null>(null)
const ComposerContext = createContext<ComposerContextValue | null>(null);
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
return (
<ComposerContext value={{ state, actions, meta }}>
{children}
</ComposerContext>
)
return <ComposerContext value={{ state, actions, meta }}>{children}</ComposerContext>;
}
function ComposerFrame({ children }: { children: React.ReactNode }) {
return <form>{children}</form>
return <form>{children}</form>;
}
function ComposerInput() {
@@ -203,21 +193,21 @@ function ComposerInput() {
state,
actions: { update },
meta: { inputRef },
} = use(ComposerContext)
} = use(ComposerContext);
return (
<TextInput
ref={inputRef}
value={state.input}
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
/>
)
);
}
function ComposerSubmit() {
const {
actions: { submit },
} = use(ComposerContext)
return <Button onPress={submit}>Send</Button>
} = use(ComposerContext);
return <Button onPress={submit}>Send</Button>;
}
// Export as compound component
@@ -231,7 +221,7 @@ const Composer = {
Attachments: ComposerAttachments,
Formatting: ComposerFormatting,
Emojis: ComposerEmojis,
}
};
```
**Usage:**
@@ -275,18 +265,15 @@ useState, Zustand, or a server sync.
```tsx
function ChannelComposer({ channelId }: { channelId: string }) {
// UI component knows about global state implementation
const state = useGlobalChannelState(channelId)
const { submit, updateInput } = useChannelSync(channelId)
const state = useGlobalChannelState(channelId);
const { submit, updateInput } = useChannelSync(channelId);
return (
<Composer.Frame>
<Composer.Input
value={state.input}
onChange={(text) => sync.updateInput(text)}
/>
<Composer.Input value={state.input} onChange={(text) => sync.updateInput(text)} />
<Composer.Submit onPress={() => sync.submit()} />
</Composer.Frame>
)
);
}
```
@@ -298,21 +285,17 @@ function ChannelProvider({
channelId,
children,
}: {
channelId: string
children: React.ReactNode
channelId: string;
children: React.ReactNode;
}) {
const { state, update, submit } = useGlobalChannel(channelId)
const inputRef = useRef(null)
const { state, update, submit } = useGlobalChannel(channelId);
const inputRef = useRef(null);
return (
<Composer.Provider
state={state}
actions={{ update, submit }}
meta={{ inputRef }}
>
<Composer.Provider state={state} actions={{ update, submit }} meta={{ inputRef }}>
{children}
</Composer.Provider>
)
);
}
// UI component only knows about the context interface
@@ -325,7 +308,7 @@ function ChannelComposer() {
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
)
);
}
// Usage
@@ -334,7 +317,7 @@ function Channel({ channelId }: { channelId: string }) {
<ChannelProvider channelId={channelId}>
<ChannelComposer />
</ChannelProvider>
)
);
}
```
@@ -343,28 +326,25 @@ function Channel({ channelId }: { channelId: string }) {
```tsx
// Local state for ephemeral forms
function ForwardMessageProvider({ children }) {
const [state, setState] = useState(initialState)
const forwardMessage = useForwardMessage()
const [state, setState] = useState(initialState);
const forwardMessage = useForwardMessage();
return (
<Composer.Provider
state={state}
actions={{ update: setState, submit: forwardMessage }}
>
<Composer.Provider state={state} actions={{ update: setState, submit: forwardMessage }}>
{children}
</Composer.Provider>
)
);
}
// Global synced state for channels
function ChannelProvider({ channelId, children }) {
const { state, update, submit } = useGlobalChannel(channelId)
const { state, update, submit } = useGlobalChannel(channelId);
return (
<Composer.Provider state={state} actions={{ update, submit }}>
{children}
</Composer.Provider>
)
);
}
```
@@ -393,8 +373,8 @@ dependency-injectable.
```tsx
function ComposerInput() {
// Tightly coupled to a specific hook
const { input, setInput } = useChannelComposerState()
return <TextInput value={input} onChangeText={setInput} />
const { input, setInput } = useChannelComposerState();
return <TextInput value={input} onChangeText={setInput} />;
}
```
@@ -403,27 +383,27 @@ function ComposerInput() {
```tsx
// Define a GENERIC interface that any provider can implement
interface ComposerState {
input: string
attachments: Attachment[]
isSubmitting: boolean
input: string;
attachments: Attachment[];
isSubmitting: boolean;
}
interface ComposerActions {
update: (updater: (state: ComposerState) => ComposerState) => void
submit: () => void
update: (updater: (state: ComposerState) => ComposerState) => void;
submit: () => void;
}
interface ComposerMeta {
inputRef: React.RefObject<TextInput>
inputRef: React.RefObject<TextInput>;
}
interface ComposerContextValue {
state: ComposerState
actions: ComposerActions
meta: ComposerMeta
state: ComposerState;
actions: ComposerActions;
meta: ComposerMeta;
}
const ComposerContext = createContext<ComposerContextValue | null>(null)
const ComposerContext = createContext<ComposerContextValue | null>(null);
```
**UI components consume the interface, not the implementation:**
@@ -434,7 +414,7 @@ function ComposerInput() {
state,
actions: { update },
meta,
} = use(ComposerContext)
} = use(ComposerContext);
// This component works with ANY provider that implements the interface
return (
@@ -443,7 +423,7 @@ function ComposerInput() {
value={state.input}
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
/>
)
);
}
```
@@ -452,9 +432,9 @@ function ComposerInput() {
```tsx
// Provider A: Local state for ephemeral forms
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState(initialState)
const inputRef = useRef(null)
const submit = useForwardMessage()
const [state, setState] = useState(initialState);
const inputRef = useRef(null);
const submit = useForwardMessage();
return (
<ComposerContext
@@ -466,13 +446,13 @@ function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
>
{children}
</ComposerContext>
)
);
}
// Provider B: Global synced state for channels
function ChannelProvider({ channelId, children }: Props) {
const { state, update, submit } = useGlobalChannel(channelId)
const inputRef = useRef(null)
const { state, update, submit } = useGlobalChannel(channelId);
const inputRef = useRef(null);
return (
<ComposerContext
@@ -484,7 +464,7 @@ function ChannelProvider({ channelId, children }: Props) {
>
{children}
</ComposerContext>
)
);
}
```
@@ -534,21 +514,21 @@ function ForwardMessageDialog() {
</DialogActions>
</Dialog>
</ForwardMessageProvider>
)
);
}
// This button lives OUTSIDE Composer.Frame but can still submit based on its context!
function ForwardButton() {
const {
actions: { submit },
} = use(ComposerContext)
return <Button onPress={submit}>Forward</Button>
} = use(ComposerContext);
return <Button onPress={submit}>Forward</Button>;
}
// This preview lives OUTSIDE Composer.Frame but can read composer's state!
function MessagePreview() {
const { state } = use(ComposerContext)
return <Preview message={state.input} attachments={state.attachments} />
const { state } = use(ComposerContext);
return <Preview message={state.input} attachments={state.attachments} />;
}
```
@@ -582,15 +562,15 @@ or awkward refs.
```tsx
function ForwardMessageComposer() {
const [state, setState] = useState(initialState)
const forwardMessage = useForwardMessage()
const [state, setState] = useState(initialState);
const forwardMessage = useForwardMessage();
return (
<Composer.Frame>
<Composer.Input />
<Composer.Footer />
</Composer.Frame>
)
);
}
// Problem: How does this button access composer state?
@@ -604,7 +584,7 @@ function ForwardMessageDialog() {
<ForwardButton /> {/* Needs to call submit */}
</DialogActions>
</Dialog>
)
);
}
```
@@ -612,20 +592,20 @@ function ForwardMessageDialog() {
```tsx
function ForwardMessageDialog() {
const [input, setInput] = useState('')
const [input, setInput] = useState('');
return (
<Dialog>
<ForwardMessageComposer onInputChange={setInput} />
<MessagePreview input={input} />
</Dialog>
)
);
}
function ForwardMessageComposer({ onInputChange }) {
const [state, setState] = useState(initialState)
const [state, setState] = useState(initialState);
useEffect(() => {
onInputChange(state.input) // Sync on every change 😬
}, [state.input])
onInputChange(state.input); // Sync on every change 😬
}, [state.input]);
}
```
@@ -633,13 +613,13 @@ function ForwardMessageComposer({ onInputChange }) {
```tsx
function ForwardMessageDialog() {
const stateRef = useRef(null)
const stateRef = useRef(null);
return (
<Dialog>
<ForwardMessageComposer stateRef={stateRef} />
<ForwardButton onPress={() => submit(stateRef.current)} />
</Dialog>
)
);
}
```
@@ -647,9 +627,9 @@ function ForwardMessageDialog() {
```tsx
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState(initialState)
const forwardMessage = useForwardMessage()
const inputRef = useRef(null)
const [state, setState] = useState(initialState);
const forwardMessage = useForwardMessage();
const inputRef = useRef(null);
return (
<Composer.Provider
@@ -659,7 +639,7 @@ function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
>
{children}
</Composer.Provider>
)
);
}
function ForwardMessageDialog() {
@@ -674,12 +654,12 @@ function ForwardMessageDialog() {
</DialogActions>
</Dialog>
</ForwardMessageProvider>
)
);
}
function ForwardButton() {
const { actions } = use(Composer.Context)
return <Button onPress={actions.submit}>Forward</Button>
const { actions } = use(Composer.Context);
return <Button onPress={actions.submit}>Forward</Button>;
}
```
@@ -718,13 +698,7 @@ itself.
```tsx
// What does this component actually render?
<Composer
isThread
isEditing={false}
channelId='abc'
showAttachments
showFormatting={false}
/>
<Composer isThread isEditing={false} channelId="abc" showAttachments showFormatting={false} />
```
**Correct: explicit variants**
@@ -760,7 +734,7 @@ function ThreadComposer({ channelId }: { channelId: string }) {
</Composer.Footer>
</Composer.Frame>
</ThreadProvider>
)
);
}
function EditMessageComposer({ messageId }: { messageId: string }) {
@@ -776,7 +750,7 @@ function EditMessageComposer({ messageId }: { messageId: string }) {
</Composer.Footer>
</Composer.Frame>
</EditMessageProvider>
)
);
}
function ForwardMessageComposer({ messageId }: { messageId: string }) {
@@ -791,7 +765,7 @@ function ForwardMessageComposer({ messageId }: { messageId: string }) {
</Composer.Footer>
</Composer.Frame>
</ForwardMessageProvider>
)
);
}
```
@@ -823,9 +797,9 @@ function Composer({
renderFooter,
renderActions,
}: {
renderHeader?: () => React.ReactNode
renderFooter?: () => React.ReactNode
renderActions?: () => React.ReactNode
renderHeader?: () => React.ReactNode;
renderFooter?: () => React.ReactNode;
renderActions?: () => React.ReactNode;
}) {
return (
<form>
@@ -834,7 +808,7 @@ function Composer({
{renderFooter ? renderFooter() : <DefaultFooter />}
{renderActions?.()}
</form>
)
);
}
// Usage is awkward and inflexible
@@ -849,18 +823,18 @@ return (
)}
renderActions={() => <SubmitButton />}
/>
)
);
```
**Correct: compound components with children**
```tsx
function ComposerFrame({ children }: { children: React.ReactNode }) {
return <form>{children}</form>
return <form>{children}</form>;
}
function ComposerFooter({ children }: { children: React.ReactNode }) {
return <footer className='flex'>{children}</footer>
return <footer className="flex">{children}</footer>;
}
// Usage is flexible
@@ -874,17 +848,14 @@ return (
<SubmitButton />
</Composer.Footer>
</Composer.Frame>
)
);
```
**When render props are appropriate:**
```tsx
// Render props work well when you need to pass data back
<List
data={items}
renderItem={({ item, index }) => <Item item={item} index={index} />}
/>
<List data={items} renderItem={({ item, index }) => <Item item={item} index={index} />} />
```
Use render props when the parent needs to provide data or state to the child.
@@ -911,28 +882,28 @@ In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `
```tsx
const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
return <TextInput ref={ref} {...props} />
})
return <TextInput ref={ref} {...props} />;
});
```
**Correct: ref as a regular prop**
```tsx
function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
return <TextInput ref={ref} {...props} />
return <TextInput ref={ref} {...props} />;
}
```
**Incorrect: useContext in React 19**
```tsx
const value = useContext(MyContext)
const value = useContext(MyContext);
```
**Correct: use instead of useContext**
```tsx
const value = use(MyContext)
const value = use(MyContext);
```
`use()` can also be called conditionally, unlike `useContext()`.
@@ -1,7 +1,6 @@
---
name: vercel-composition-patterns
description:
React composition patterns that scale. Use when refactoring components with
description: React composition patterns that scale. Use when refactoring components with
boolean prop proliferation, building flexible component libraries, or
designing reusable APIs. Triggers on tasks involving compound components,
render props, context providers, or component architecture. Includes React 19
@@ -32,16 +32,10 @@ function Composer({
) : isThread ? (
<AlsoSendToChannelField id={channelId} />
) : null}
{isEditing ? (
<EditActions />
) : isForwarding ? (
<ForwardActions />
) : (
<DefaultActions />
)}
{isEditing ? <EditActions /> : isForwarding ? <ForwardActions /> : <DefaultActions />}
<Footer onSubmit={onSubmit} />
</form>
)
);
}
```
@@ -61,7 +55,7 @@ function ChannelComposer() {
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
)
);
}
// Thread composer - adds "also send to channel" field
@@ -77,7 +71,7 @@ function ThreadComposer({ channelId }: { channelId: string }) {
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
)
);
}
// Edit composer - different footer actions
@@ -92,7 +86,7 @@ function EditComposer() {
<Composer.SaveEdit />
</Composer.Footer>
</Composer.Frame>
)
);
}
```
@@ -37,25 +37,21 @@ function Composer({
</Footer>
)}
</form>
)
);
}
```
**Correct (compound components with shared context):**
```tsx
const ComposerContext = createContext<ComposerContextValue | null>(null)
const ComposerContext = createContext<ComposerContextValue | null>(null);
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
return (
<ComposerContext value={{ state, actions, meta }}>
{children}
</ComposerContext>
)
return <ComposerContext value={{ state, actions, meta }}>{children}</ComposerContext>;
}
function ComposerFrame({ children }: { children: React.ReactNode }) {
return <form>{children}</form>
return <form>{children}</form>;
}
function ComposerInput() {
@@ -63,21 +59,21 @@ function ComposerInput() {
state,
actions: { update },
meta: { inputRef },
} = use(ComposerContext)
} = use(ComposerContext);
return (
<TextInput
ref={inputRef}
value={state.input}
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
/>
)
);
}
function ComposerSubmit() {
const {
actions: { submit },
} = use(ComposerContext)
return <Button onPress={submit}>Send</Button>
} = use(ComposerContext);
return <Button onPress={submit}>Send</Button>;
}
// Export as compound component
@@ -91,7 +87,7 @@ const Composer = {
Attachments: ComposerAttachments,
Formatting: ComposerFormatting,
Emojis: ComposerEmojis,
}
};
```
**Usage:**
@@ -19,9 +19,9 @@ function Composer({
renderFooter,
renderActions,
}: {
renderHeader?: () => React.ReactNode
renderFooter?: () => React.ReactNode
renderActions?: () => React.ReactNode
renderHeader?: () => React.ReactNode;
renderFooter?: () => React.ReactNode;
renderActions?: () => React.ReactNode;
}) {
return (
<form>
@@ -30,7 +30,7 @@ function Composer({
{renderFooter ? renderFooter() : <DefaultFooter />}
{renderActions?.()}
</form>
)
);
}
// Usage is awkward and inflexible
@@ -45,18 +45,18 @@ return (
)}
renderActions={() => <SubmitButton />}
/>
)
);
```
**Correct (compound components with children):**
```tsx
function ComposerFrame({ children }: { children: React.ReactNode }) {
return <form>{children}</form>
return <form>{children}</form>;
}
function ComposerFooter({ children }: { children: React.ReactNode }) {
return <footer className='flex'>{children}</footer>
return <footer className="flex">{children}</footer>;
}
// Usage is flexible
@@ -70,17 +70,14 @@ return (
<SubmitButton />
</Composer.Footer>
</Composer.Frame>
)
);
```
**When render props are appropriate:**
```tsx
// Render props work well when you need to pass data back
<List
data={items}
renderItem={({ item, index }) => <Item item={item} index={index} />}
/>
<List data={items} renderItem={({ item, index }) => <Item item={item} index={index} />} />
```
Use render props when the parent needs to provide data or state to the child.
@@ -15,13 +15,7 @@ itself.
```tsx
// What does this component actually render?
<Composer
isThread
isEditing={false}
channelId='abc'
showAttachments
showFormatting={false}
/>
<Composer isThread isEditing={false} channelId="abc" showAttachments showFormatting={false} />
```
**Correct (explicit variants):**
@@ -56,7 +50,7 @@ function ThreadComposer({ channelId }: { channelId: string }) {
</Composer.Footer>
</Composer.Frame>
</ThreadProvider>
)
);
}
function EditMessageComposer({ messageId }: { messageId: string }) {
@@ -72,7 +66,7 @@ function EditMessageComposer({ messageId }: { messageId: string }) {
</Composer.Footer>
</Composer.Frame>
</EditMessageProvider>
)
);
}
function ForwardMessageComposer({ messageId }: { messageId: string }) {
@@ -87,7 +81,7 @@ function ForwardMessageComposer({ messageId }: { messageId: string }) {
</Composer.Footer>
</Composer.Frame>
</ForwardMessageProvider>
)
);
}
```
@@ -15,28 +15,28 @@ In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `
```tsx
const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
return <TextInput ref={ref} {...props} />
})
return <TextInput ref={ref} {...props} />;
});
```
**Correct (ref as a regular prop):**
```tsx
function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
return <TextInput ref={ref} {...props} />
return <TextInput ref={ref} {...props} />;
}
```
**Incorrect (useContext in React 19):**
```tsx
const value = useContext(MyContext)
const value = useContext(MyContext);
```
**Correct (use instead of useContext):**
```tsx
const value = use(MyContext)
const value = use(MyContext);
```
`use()` can also be called conditionally, unlike `useContext()`.
@@ -20,8 +20,8 @@ dependency-injectable.
```tsx
function ComposerInput() {
// Tightly coupled to a specific hook
const { input, setInput } = useChannelComposerState()
return <TextInput value={input} onChangeText={setInput} />
const { input, setInput } = useChannelComposerState();
return <TextInput value={input} onChangeText={setInput} />;
}
```
@@ -30,27 +30,27 @@ function ComposerInput() {
```tsx
// Define a GENERIC interface that any provider can implement
interface ComposerState {
input: string
attachments: Attachment[]
isSubmitting: boolean
input: string;
attachments: Attachment[];
isSubmitting: boolean;
}
interface ComposerActions {
update: (updater: (state: ComposerState) => ComposerState) => void
submit: () => void
update: (updater: (state: ComposerState) => ComposerState) => void;
submit: () => void;
}
interface ComposerMeta {
inputRef: React.RefObject<TextInput>
inputRef: React.RefObject<TextInput>;
}
interface ComposerContextValue {
state: ComposerState
actions: ComposerActions
meta: ComposerMeta
state: ComposerState;
actions: ComposerActions;
meta: ComposerMeta;
}
const ComposerContext = createContext<ComposerContextValue | null>(null)
const ComposerContext = createContext<ComposerContextValue | null>(null);
```
**UI components consume the interface, not the implementation:**
@@ -61,7 +61,7 @@ function ComposerInput() {
state,
actions: { update },
meta,
} = use(ComposerContext)
} = use(ComposerContext);
// This component works with ANY provider that implements the interface
return (
@@ -70,7 +70,7 @@ function ComposerInput() {
value={state.input}
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
/>
)
);
}
```
@@ -79,9 +79,9 @@ function ComposerInput() {
```tsx
// Provider A: Local state for ephemeral forms
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState(initialState)
const inputRef = useRef(null)
const submit = useForwardMessage()
const [state, setState] = useState(initialState);
const inputRef = useRef(null);
const submit = useForwardMessage();
return (
<ComposerContext
@@ -93,13 +93,13 @@ function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
>
{children}
</ComposerContext>
)
);
}
// Provider B: Global synced state for channels
function ChannelProvider({ channelId, children }: Props) {
const { state, update, submit } = useGlobalChannel(channelId)
const inputRef = useRef(null)
const { state, update, submit } = useGlobalChannel(channelId);
const inputRef = useRef(null);
return (
<ComposerContext
@@ -111,7 +111,7 @@ function ChannelProvider({ channelId, children }: Props) {
>
{children}
</ComposerContext>
)
);
}
```
@@ -165,21 +165,21 @@ function ForwardMessageDialog() {
</DialogActions>
</Dialog>
</ForwardMessageProvider>
)
);
}
// This button lives OUTSIDE Composer.Frame but can still submit based on its context!
function ForwardButton() {
const {
actions: { submit },
} = use(ComposerContext)
return <Button onPress={submit}>Forward</Button>
} = use(ComposerContext);
return <Button onPress={submit}>Forward</Button>;
}
// This preview lives OUTSIDE Composer.Frame but can read composer's state!
function MessagePreview() {
const { state } = use(ComposerContext)
return <Preview message={state.input} attachments={state.attachments} />
const { state } = use(ComposerContext);
return <Preview message={state.input} attachments={state.attachments} />;
}
```
@@ -16,18 +16,15 @@ useState, Zustand, or a server sync.
```tsx
function ChannelComposer({ channelId }: { channelId: string }) {
// UI component knows about global state implementation
const state = useGlobalChannelState(channelId)
const { submit, updateInput } = useChannelSync(channelId)
const state = useGlobalChannelState(channelId);
const { submit, updateInput } = useChannelSync(channelId);
return (
<Composer.Frame>
<Composer.Input
value={state.input}
onChange={(text) => sync.updateInput(text)}
/>
<Composer.Input value={state.input} onChange={(text) => sync.updateInput(text)} />
<Composer.Submit onPress={() => sync.submit()} />
</Composer.Frame>
)
);
}
```
@@ -39,21 +36,17 @@ function ChannelProvider({
channelId,
children,
}: {
channelId: string
children: React.ReactNode
channelId: string;
children: React.ReactNode;
}) {
const { state, update, submit } = useGlobalChannel(channelId)
const inputRef = useRef(null)
const { state, update, submit } = useGlobalChannel(channelId);
const inputRef = useRef(null);
return (
<Composer.Provider
state={state}
actions={{ update, submit }}
meta={{ inputRef }}
>
<Composer.Provider state={state} actions={{ update, submit }} meta={{ inputRef }}>
{children}
</Composer.Provider>
)
);
}
// UI component only knows about the context interface
@@ -66,7 +59,7 @@ function ChannelComposer() {
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
)
);
}
// Usage
@@ -75,7 +68,7 @@ function Channel({ channelId }: { channelId: string }) {
<ChannelProvider channelId={channelId}>
<ChannelComposer />
</ChannelProvider>
)
);
}
```
@@ -84,28 +77,25 @@ function Channel({ channelId }: { channelId: string }) {
```tsx
// Local state for ephemeral forms
function ForwardMessageProvider({ children }) {
const [state, setState] = useState(initialState)
const forwardMessage = useForwardMessage()
const [state, setState] = useState(initialState);
const forwardMessage = useForwardMessage();
return (
<Composer.Provider
state={state}
actions={{ update: setState, submit: forwardMessage }}
>
<Composer.Provider state={state} actions={{ update: setState, submit: forwardMessage }}>
{children}
</Composer.Provider>
)
);
}
// Global synced state for channels
function ChannelProvider({ channelId, children }) {
const { state, update, submit } = useGlobalChannel(channelId)
const { state, update, submit } = useGlobalChannel(channelId);
return (
<Composer.Provider state={state} actions={{ update, submit }}>
{children}
</Composer.Provider>
)
);
}
```
@@ -15,15 +15,15 @@ or awkward refs.
```tsx
function ForwardMessageComposer() {
const [state, setState] = useState(initialState)
const forwardMessage = useForwardMessage()
const [state, setState] = useState(initialState);
const forwardMessage = useForwardMessage();
return (
<Composer.Frame>
<Composer.Input />
<Composer.Footer />
</Composer.Frame>
)
);
}
// Problem: How does this button access composer state?
@@ -37,7 +37,7 @@ function ForwardMessageDialog() {
<ForwardButton /> {/* Needs to call submit */}
</DialogActions>
</Dialog>
)
);
}
```
@@ -45,20 +45,20 @@ function ForwardMessageDialog() {
```tsx
function ForwardMessageDialog() {
const [input, setInput] = useState('')
const [input, setInput] = useState('');
return (
<Dialog>
<ForwardMessageComposer onInputChange={setInput} />
<MessagePreview input={input} />
</Dialog>
)
);
}
function ForwardMessageComposer({ onInputChange }) {
const [state, setState] = useState(initialState)
const [state, setState] = useState(initialState);
useEffect(() => {
onInputChange(state.input) // Sync on every change 😬
}, [state.input])
onInputChange(state.input); // Sync on every change 😬
}, [state.input]);
}
```
@@ -66,13 +66,13 @@ function ForwardMessageComposer({ onInputChange }) {
```tsx
function ForwardMessageDialog() {
const stateRef = useRef(null)
const stateRef = useRef(null);
return (
<Dialog>
<ForwardMessageComposer stateRef={stateRef} />
<ForwardButton onPress={() => submit(stateRef.current)} />
</Dialog>
)
);
}
```
@@ -80,9 +80,9 @@ function ForwardMessageDialog() {
```tsx
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState(initialState)
const forwardMessage = useForwardMessage()
const inputRef = useRef(null)
const [state, setState] = useState(initialState);
const forwardMessage = useForwardMessage();
const inputRef = useRef(null);
return (
<Composer.Provider
@@ -92,7 +92,7 @@ function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
>
{children}
</Composer.Provider>
)
);
}
function ForwardMessageDialog() {
@@ -107,12 +107,12 @@ function ForwardMessageDialog() {
</DialogActions>
</Dialog>
</ForwardMessageProvider>
)
);
}
function ForwardButton() {
const { actions } = use(Composer.Context)
return <Button onPress={actions.submit}>Forward</Button>
const { actions } = use(Composer.Context);
return <Button onPress={actions.submit}>Forward</Button>;
}
```
File diff suppressed because it is too large. Load diff
@@ -4,7 +4,7 @@ description: React and Next.js performance optimization guidelines from Vercel E
license: MIT
metadata:
author: vercel
version: "1.0.0"
version: '1.0.0'
---
# Vercel React Best Practices
@@ -14,6 +14,7 @@ Comprehensive performance optimization guide for React and Next.js applications,
## When to Apply
Reference these guidelines when:
- Writing new React components or Next.js pages
- Implementing data fetching (client or server-side)
- Reviewing code for performance issues
@@ -22,16 +23,16 @@ Reference these guidelines when:
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
| 3 | Server-Side Performance | HIGH | `server-` |
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
| 6 | Rendering Performance | MEDIUM | `rendering-` |
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
| 8 | Advanced Patterns | LOW | `advanced-` |
| Priority | Category | Impact | Prefix |
| -------- | ------------------------- | ----------- | ------------ |
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
| 3 | Server-Side Performance | HIGH | `server-` |
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
| 6 | Rendering Performance | MEDIUM | `rendering-` |
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
| 8 | Advanced Patterns | LOW | `advanced-` |
## Quick Reference
@@ -126,6 +127,7 @@ rules/bundle-barrel-imports.md
```
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
@@ -14,9 +14,9 @@ Store callbacks in refs when used in effects that shouldn't re-subscribe on call
```tsx
function useWindowEvent(event: string, handler: (e) => void) {
useEffect(() => {
window.addEventListener(event, handler)
return () => window.removeEventListener(event, handler)
}, [event, handler])
window.addEventListener(event, handler);
return () => window.removeEventListener(event, handler);
}, [event, handler]);
}
```
@@ -24,31 +24,31 @@ function useWindowEvent(event: string, handler: (e) => void) {
```tsx
function useWindowEvent(event: string, handler: (e) => void) {
const handlerRef = useRef(handler)
const handlerRef = useRef(handler);
useEffect(() => {
handlerRef.current = handler
}, [handler])
handlerRef.current = handler;
}, [handler]);
useEffect(() => {
const listener = (e) => handlerRef.current(e)
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}, [event])
const listener = (e) => handlerRef.current(e);
window.addEventListener(event, listener);
return () => window.removeEventListener(event, listener);
}, [event]);
}
```
**Alternative: use `useEffectEvent` if you're on latest React:**
```tsx
import { useEffectEvent } from 'react'
import { useEffectEvent } from 'react';
function useWindowEvent(event: string, handler: (e) => void) {
const onEvent = useEffectEvent(handler)
const onEvent = useEffectEvent(handler);
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
window.addEventListener(event, onEvent);
return () => window.removeEventListener(event, onEvent);
}, [event]);
}
```
@@ -14,9 +14,9 @@ Do not put app-wide initialization that must run once per app load inside `useEf
```tsx
function Comp() {
useEffect(() => {
loadFromStorage()
checkAuthToken()
}, [])
loadFromStorage();
checkAuthToken();
}, []);
// ...
}
@@ -25,15 +25,15 @@ function Comp() {
**Correct (once per app load):**
```tsx
let didInit = false
let didInit = false;
function Comp() {
useEffect(() => {
if (didInit) return
didInit = true
loadFromStorage()
checkAuthToken()
}, [])
if (didInit) return;
didInit = true;
loadFromStorage();
checkAuthToken();
}, []);
// ...
}
@@ -13,12 +13,12 @@ Access latest values in callbacks without adding them to dependency arrays. Prev
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
const [query, setQuery] = useState('');
useEffect(() => {
const timeout = setTimeout(() => onSearch(query), 300)
return () => clearTimeout(timeout)
}, [query, onSearch])
const timeout = setTimeout(() => onSearch(query), 300);
return () => clearTimeout(timeout);
}, [query, onSearch]);
}
```
@@ -28,12 +28,12 @@ function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
import { useEffectEvent } from 'react';
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
const onSearchEvent = useEffectEvent(onSearch)
const [query, setQuery] = useState('');
const onSearchEvent = useEffectEvent(onSearch);
useEffect(() => {
const timeout = setTimeout(() => onSearchEvent(query), 300)
return () => clearTimeout(timeout)
}, [query])
const timeout = setTimeout(() => onSearchEvent(query), 300);
return () => clearTimeout(timeout);
}, [query]);
}
```
@@ -13,10 +13,10 @@ In API routes and Server Actions, start independent operations immediately, even
```typescript
export async function GET(request: Request) {
const session = await auth()
const config = await fetchConfig()
const data = await fetchData(session.user.id)
return Response.json({ data, config })
const session = await auth();
const config = await fetchConfig();
const data = await fetchData(session.user.id);
return Response.json({ data, config });
}
```
@@ -24,14 +24,11 @@ export async function GET(request: Request) {
```typescript
export async function GET(request: Request) {
const sessionPromise = auth()
const configPromise = fetchConfig()
const session = await sessionPromise
const [config, data] = await Promise.all([
configPromise,
fetchData(session.user.id)
])
return Response.json({ data, config })
const sessionPromise = auth();
const configPromise = fetchConfig();
const session = await sessionPromise;
const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)]);
return Response.json({ data, config });
}
```
@@ -13,15 +13,15 @@ Move `await` operations into the branches where they're actually used to avoid b
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
const userData = await fetchUserData(userId);
if (skipProcessing) {
// Returns immediately but still waited for userData
return { skipped: true }
return { skipped: true };
}
// Only this branch uses userData
return processUserData(userData)
return processUserData(userData);
}
```
@@ -31,12 +31,12 @@ async function handleRequest(userId: string, skipProcessing: boolean) {
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
// Returns immediately without waiting
return { skipped: true }
return { skipped: true };
}
// Fetch only when needed
const userData = await fetchUserData(userId)
return processUserData(userData)
const userData = await fetchUserData(userId);
return processUserData(userData);
}
```
@@ -45,35 +45,35 @@ async function handleRequest(userId: string, skipProcessing: boolean) {
```typescript
// Incorrect: always fetches permissions
async function updateResource(resourceId: string, userId: string) {
const permissions = await fetchPermissions(userId)
const resource = await getResource(resourceId)
const permissions = await fetchPermissions(userId);
const resource = await getResource(resourceId);
if (!resource) {
return { error: 'Not found' }
return { error: 'Not found' };
}
if (!permissions.canEdit) {
return { error: 'Forbidden' }
return { error: 'Forbidden' };
}
return await updateResourceData(resource, permissions)
return await updateResourceData(resource, permissions);
}
// Correct: fetches only when needed
async function updateResource(resourceId: string, userId: string) {
const resource = await getResource(resourceId)
const resource = await getResource(resourceId);
if (!resource) {
return { error: 'Not found' }
return { error: 'Not found' };
}
const permissions = await fetchPermissions(userId)
const permissions = await fetchPermissions(userId);
if (!permissions.canEdit) {
return { error: 'Forbidden' }
return { error: 'Forbidden' };
}
return await updateResourceData(resource, permissions)
return await updateResourceData(resource, permissions);
}
```
@@ -12,25 +12,26 @@ For operations with partial dependencies, use `better-all` to maximize paralleli
**Incorrect (profile waits for config unnecessarily):**
```typescript
const [user, config] = await Promise.all([
fetchUser(),
fetchConfig()
])
const profile = await fetchProfile(user.id)
const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);
const profile = await fetchProfile(user.id);
```
**Correct (config and profile run in parallel):**
```typescript
import { all } from 'better-all'
import { all } from 'better-all';
const { user, config, profile } = await all({
async user() { return fetchUser() },
async config() { return fetchConfig() },
async user() {
return fetchUser();
},
async config() {
return fetchConfig();
},
async profile() {
return fetchProfile((await this.$.user).id)
}
})
return fetchProfile((await this.$.user).id);
},
});
```
**Alternative without extra dependencies:**
@@ -38,14 +39,10 @@ const { user, config, profile } = await all({
We can also create all the promises first, and do `Promise.all()` at the end.
```typescript
const userPromise = fetchUser()
const profilePromise = userPromise.then(user => fetchProfile(user.id))
const userPromise = fetchUser();
const profilePromise = userPromise.then((user) => fetchProfile(user.id));
const [user, config, profile] = await Promise.all([
userPromise,
fetchConfig(),
profilePromise
])
const [user, config, profile] = await Promise.all([userPromise, fetchConfig(), profilePromise]);
```
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
@@ -12,17 +12,13 @@ When async operations have no interdependencies, execute them concurrently using
**Incorrect (sequential execution, 3 round trips):**
```typescript
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();
```
**Correct (parallel execution, 1 round trip):**
```typescript
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()]);
```
@@ -13,8 +13,8 @@ Instead of awaiting data in async components before returning JSX, use Suspense
```tsx
async function Page() {
const data = await fetchData() // Blocks entire page
const data = await fetchData(); // Blocks entire page
return (
<div>
<div>Sidebar</div>
@@ -24,7 +24,7 @@ async function Page() {
</div>
<div>Footer</div>
</div>
)
);
}
```
@@ -45,12 +45,12 @@ function Page() {
</div>
<div>Footer</div>
</div>
)
);
}
async function DataDisplay() {
const data = await fetchData() // Only blocks this component
return <div>{data.content}</div>
const data = await fetchData(); // Only blocks this component
return <div>{data.content}</div>;
}
```
@@ -61,8 +61,8 @@ Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
```tsx
function Page() {
// Start fetch immediately, but don't await
const dataPromise = fetchData()
const dataPromise = fetchData();
return (
<div>
<div>Sidebar</div>
@@ -73,17 +73,17 @@ function Page() {
</Suspense>
<div>Footer</div>
</div>
)
);
}
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Unwraps the promise
return <div>{data.content}</div>
const data = use(dataPromise); // Unwraps the promise
return <div>{data.content}</div>;
}
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Reuses the same promise
return <div>{data.summary}</div>
const data = use(dataPromise); // Reuses the same promise
return <div>{data.summary}</div>;
}
```
@@ -16,24 +16,24 @@ Popular icon and component libraries can have **up to 10,000 re-exports** in the
**Incorrect (imports entire library):**
```tsx
import { Check, X, Menu } from 'lucide-react'
import { Check, X, Menu } from 'lucide-react';
// Loads 1,583 modules, takes ~2.8s extra in dev
// Runtime cost: 200-800ms on every cold start
import { Button, TextField } from '@mui/material'
import { Button, TextField } from '@mui/material';
// Loads 2,225 modules, takes ~4.2s extra in dev
```
**Correct (imports only what you need):**
```tsx
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
import Menu from 'lucide-react/dist/esm/icons/menu'
import Check from 'lucide-react/dist/esm/icons/check';
import X from 'lucide-react/dist/esm/icons/x';
import Menu from 'lucide-react/dist/esm/icons/menu';
// Loads only 3 modules (~2KB vs ~1MB)
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField'
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
// Loads only what you use
```
@@ -43,12 +43,12 @@ import TextField from '@mui/material/TextField'
// next.config.js - use optimizePackageImports
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react', '@mui/material']
}
}
optimizePackageImports: ['lucide-react', '@mui/material'],
},
};
// Then you can keep the ergonomic barrel imports:
import { Check, X, Menu } from 'lucide-react'
import { Check, X, Menu } from 'lucide-react';
// Automatically transformed to direct imports at build time
```
@@ -12,19 +12,25 @@ Load large data or modules only when a feature is activated.
**Example (lazy-load animation frames):**
```tsx
function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
const [frames, setFrames] = useState<Frame[] | null>(null)
function AnimationPlayer({
enabled,
setEnabled,
}: {
enabled: boolean;
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const [frames, setFrames] = useState<Frame[] | null>(null);
useEffect(() => {
if (enabled && !frames && typeof window !== 'undefined') {
import('./animation-frames.js')
.then(mod => setFrames(mod.frames))
.catch(() => setEnabled(false))
.then((mod) => setFrames(mod.frames))
.catch(() => setEnabled(false));
}
}, [enabled, frames, setEnabled])
}, [enabled, frames, setEnabled]);
if (!frames) return <Skeleton />
return <Canvas frames={frames} />
if (!frames) return <Skeleton />;
return <Canvas frames={frames} />;
}
```
@@ -12,7 +12,7 @@ Analytics, logging, and error tracking don't block user interaction. Load them a
**Incorrect (blocks initial bundle):**
```tsx
import { Analytics } from '@vercel/analytics/react'
import { Analytics } from '@vercel/analytics/react';
export default function RootLayout({ children }) {
return (
@@ -22,19 +22,18 @@ export default function RootLayout({ children }) {
<Analytics />
</body>
</html>
)
);
}
```
**Correct (loads after hydration):**
```tsx
import dynamic from 'next/dynamic'
import dynamic from 'next/dynamic';
const Analytics = dynamic(
() => import('@vercel/analytics/react').then(m => m.Analytics),
{ ssr: false }
)
const Analytics = dynamic(() => import('@vercel/analytics/react').then((m) => m.Analytics), {
ssr: false,
});
export default function RootLayout({ children }) {
return (
@@ -44,6 +43,6 @@ export default function RootLayout({ children }) {
<Analytics />
</body>
</html>
)
);
}
```
@@ -12,24 +12,23 @@ Use `next/dynamic` to lazy-load large components not needed on initial render.
**Incorrect (Monaco bundles with main chunk ~300KB):**
```tsx
import { MonacoEditor } from './monaco-editor'
import { MonacoEditor } from './monaco-editor';
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
return <MonacoEditor value={code} />;
}
```
**Correct (Monaco loads on demand):**
```tsx
import dynamic from 'next/dynamic'
import dynamic from 'next/dynamic';
const MonacoEditor = dynamic(
() => import('./monaco-editor').then(m => m.MonacoEditor),
{ ssr: false }
)
const MonacoEditor = dynamic(() => import('./monaco-editor').then((m) => m.MonacoEditor), {
ssr: false,
});
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
return <MonacoEditor value={code} />;
}
```
@@ -15,19 +15,15 @@ Preload heavy bundles before they're needed to reduce perceived latency.
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== 'undefined') {
void import('./monaco-editor')
void import('./monaco-editor');
}
}
};
return (
<button
onMouseEnter={preload}
onFocus={preload}
onClick={onClick}
>
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
Open Editor
</button>
)
);
}
```
@@ -37,13 +33,11 @@ function EditorButton({ onClick }: { onClick: () => void }) {
function FlagsProvider({ children, flags }: Props) {
useEffect(() => {
if (flags.editorEnabled && typeof window !== 'undefined') {
void import('./monaco-editor').then(mod => mod.init())
void import('./monaco-editor').then((mod) => mod.init());
}
}, [flags.editorEnabled])
}, [flags.editorEnabled]);
return <FlagsContext.Provider value={flags}>
{children}
</FlagsContext.Provider>
return <FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>;
}
```
@@ -16,12 +16,12 @@ function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.key === key) {
callback()
callback();
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [key, callback])
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [key, callback]);
}
```
@@ -30,45 +30,49 @@ When using the `useKeyboardShortcut` hook multiple times, each instance will reg
**Correct (N instances = 1 listener):**
```tsx
import useSWRSubscription from 'swr/subscription'
import useSWRSubscription from 'swr/subscription';
// Module-level Map to track callbacks per key
const keyCallbacks = new Map<string, Set<() => void>>()
const keyCallbacks = new Map<string, Set<() => void>>();
function useKeyboardShortcut(key: string, callback: () => void) {
// Register this callback in the Map
useEffect(() => {
if (!keyCallbacks.has(key)) {
keyCallbacks.set(key, new Set())
keyCallbacks.set(key, new Set());
}
keyCallbacks.get(key)!.add(callback)
keyCallbacks.get(key)!.add(callback);
return () => {
const set = keyCallbacks.get(key)
const set = keyCallbacks.get(key);
if (set) {
set.delete(callback)
set.delete(callback);
if (set.size === 0) {
keyCallbacks.delete(key)
keyCallbacks.delete(key);
}
}
}
}, [key, callback])
};
}, [key, callback]);
useSWRSubscription('global-keydown', () => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && keyCallbacks.has(e.key)) {
keyCallbacks.get(e.key)!.forEach(cb => cb())
keyCallbacks.get(e.key)!.forEach((cb) => cb());
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
})
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
});
}
function Profile() {
// Multiple shortcuts will share the same listener
useKeyboardShortcut('p', () => { /* ... */ })
useKeyboardShortcut('k', () => { /* ... */ })
useKeyboardShortcut('p', () => {
/* ... */
});
useKeyboardShortcut('k', () => {
/* ... */
});
// ...
}
```
@@ -13,18 +13,18 @@ Add version prefix to keys and store only needed fields. Prevents schema conflic
```typescript
// No version, stores everything, no error handling
localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
const data = localStorage.getItem('userConfig')
localStorage.setItem('userConfig', JSON.stringify(fullUserObject));
const data = localStorage.getItem('userConfig');
```
**Correct:**
```typescript
const VERSION = 'v2'
const VERSION = 'v2';
function saveConfig(config: { theme: string; language: string }) {
try {
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config));
} catch {
// Throws in incognito/private browsing, quota exceeded, or disabled
}
@@ -32,21 +32,21 @@ function saveConfig(config: { theme: string; language: string }) {
function loadConfig() {
try {
const data = localStorage.getItem(`userConfig:${VERSION}`)
return data ? JSON.parse(data) : null
const data = localStorage.getItem(`userConfig:${VERSION}`);
return data ? JSON.parse(data) : null;
} catch {
return null
return null;
}
}
// Migration from v1 to v2
function migrate() {
try {
const v1 = localStorage.getItem('userConfig:v1')
const v1 = localStorage.getItem('userConfig:v1');
if (v1) {
const old = JSON.parse(v1)
saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
localStorage.removeItem('userConfig:v1')
const old = JSON.parse(v1);
saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang });
localStorage.removeItem('userConfig:v1');
}
} catch {}
}
@@ -58,10 +58,13 @@ function migrate() {
// User object has 20+ fields, only store what UI needs
function cachePrefs(user: FullUser) {
try {
localStorage.setItem('prefs:v1', JSON.stringify({
theme: user.preferences.theme,
notifications: user.preferences.notifications
}))
localStorage.setItem(
'prefs:v1',
JSON.stringify({
theme: user.preferences.theme,
notifications: user.preferences.notifications,
}),
);
} catch {}
}
```
@@ -13,34 +13,34 @@ Add `{ passive: true }` to touch and wheel event listeners to enable immediate s
```typescript
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
document.addEventListener('touchstart', handleTouch)
document.addEventListener('wheel', handleWheel)
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
document.addEventListener('touchstart', handleTouch);
document.addEventListener('wheel', handleWheel);
return () => {
document.removeEventListener('touchstart', handleTouch)
document.removeEventListener('wheel', handleWheel)
}
}, [])
document.removeEventListener('touchstart', handleTouch);
document.removeEventListener('wheel', handleWheel);
};
}, []);
```
**Correct:**
```typescript
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
document.addEventListener('touchstart', handleTouch, { passive: true })
document.addEventListener('wheel', handleWheel, { passive: true })
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
document.addEventListener('touchstart', handleTouch, { passive: true });
document.addEventListener('wheel', handleWheel, { passive: true });
return () => {
document.removeEventListener('touchstart', handleTouch)
document.removeEventListener('wheel', handleWheel)
}
}, [])
document.removeEventListener('touchstart', handleTouch);
document.removeEventListener('wheel', handleWheel);
};
}, []);
```
**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
@@ -13,43 +13,43 @@ SWR enables request deduplication, caching, and revalidation across component in
```tsx
function UserList() {
const [users, setUsers] = useState([])
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(setUsers)
}, [])
.then((r) => r.json())
.then(setUsers);
}, []);
}
```
**Correct (multiple instances share one request):**
```tsx
import useSWR from 'swr'
import useSWR from 'swr';
function UserList() {
const { data: users } = useSWR('/api/users', fetcher)
const { data: users } = useSWR('/api/users', fetcher);
}
```
**For immutable data:**
```tsx
import { useImmutableSWR } from '@/lib/swr'
import { useImmutableSWR } from '@/lib/swr';
function StaticContent() {
const { data } = useImmutableSWR('/api/config', fetcher)
const { data } = useImmutableSWR('/api/config', fetcher);
}
```
**For mutations:**
```tsx
import { useSWRMutation } from 'swr/mutation'
import { useSWRMutation } from 'swr/mutation';
function UpdateButton() {
const { trigger } = useSWRMutation('/api/user', updateUser)
return <button onClick={() => trigger()}>Update</button>
const { trigger } = useSWRMutation('/api/user', updateUser);
return <button onClick={() => trigger()}>Update</button>;
}
```
@@ -10,55 +10,60 @@ tags: javascript, dom, css, performance, reflow, layout-thrashing
Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
**This is OK (browser batches style changes):**
```typescript
function updateElementStyles(element: HTMLElement) {
// Each line invalidates style, but browser batches the recalculation
element.style.width = '100px'
element.style.height = '200px'
element.style.backgroundColor = 'blue'
element.style.border = '1px solid black'
element.style.width = '100px';
element.style.height = '200px';
element.style.backgroundColor = 'blue';
element.style.border = '1px solid black';
}
```
**Incorrect (interleaved reads and writes force reflows):**
```typescript
function layoutThrashing(element: HTMLElement) {
element.style.width = '100px'
const width = element.offsetWidth // Forces reflow
element.style.height = '200px'
const height = element.offsetHeight // Forces another reflow
element.style.width = '100px';
const width = element.offsetWidth; // Forces reflow
element.style.height = '200px';
const height = element.offsetHeight; // Forces another reflow
}
```
**Correct (batch writes, then read once):**
```typescript
function updateElementStyles(element: HTMLElement) {
// Batch all writes together
element.style.width = '100px'
element.style.height = '200px'
element.style.backgroundColor = 'blue'
element.style.border = '1px solid black'
element.style.width = '100px';
element.style.height = '200px';
element.style.backgroundColor = 'blue';
element.style.border = '1px solid black';
// Read after all writes are done (single reflow)
const { width, height } = element.getBoundingClientRect()
const { width, height } = element.getBoundingClientRect();
}
```
**Correct (batch reads, then writes):**
```typescript
function avoidThrashing(element: HTMLElement) {
// Read phase - all layout queries first
const rect1 = element.getBoundingClientRect()
const offsetWidth = element.offsetWidth
const offsetHeight = element.offsetHeight
const rect1 = element.getBoundingClientRect();
const offsetWidth = element.offsetWidth;
const offsetHeight = element.offsetHeight;
// Write phase - all style changes after
element.style.width = '100px'
element.style.height = '200px'
element.style.width = '100px';
element.style.height = '200px';
}
```
**Better: use CSS classes**
```css
.highlighted-box {
width: 100px;
@@ -67,38 +72,36 @@ function avoidThrashing(element: HTMLElement) {
border: 1px solid black;
}
```
```typescript
function updateElementStyles(element: HTMLElement) {
element.classList.add('highlighted-box')
const { width, height } = element.getBoundingClientRect()
element.classList.add('highlighted-box');
const { width, height } = element.getBoundingClientRect();
}
```
**React example:**
```tsx
// Incorrect: interleaving style changes with layout queries
function Box({ isHighlighted }: { isHighlighted: boolean }) {
const ref = useRef<HTMLDivElement>(null)
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (ref.current && isHighlighted) {
ref.current.style.width = '100px'
const width = ref.current.offsetWidth // Forces layout
ref.current.style.height = '200px'
ref.current.style.width = '100px';
const width = ref.current.offsetWidth; // Forces layout
ref.current.style.height = '200px';
}
}, [isHighlighted])
return <div ref={ref}>Content</div>
}, [isHighlighted]);
return <div ref={ref}>Content</div>;
}
// Correct: toggle class
function Box({ isHighlighted }: { isHighlighted: boolean }) {
return (
<div className={isHighlighted ? 'highlighted-box' : ''}>
Content
</div>
)
return <div className={isHighlighted ? 'highlighted-box' : ''}>Content</div>;
}
```
@@ -18,7 +18,7 @@ function ProjectList({ projects }: { projects: Project[] }) {
{projects.map(project => {
// slugify() called 100+ times for same project names
const slug = slugify(project.name)
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
@@ -47,7 +47,7 @@ function ProjectList({ projects }: { projects: Project[] }) {
{projects.map(project => {
// Computed only once per unique project name
const slug = cachedSlugify(project.name)
return <ProjectCard key={project.id} slug={slug} />
})}
</div>
@@ -58,20 +58,20 @@ function ProjectList({ projects }: { projects: Project[] }) {
**Simpler pattern for single-value functions:**
```typescript
let isLoggedInCache: boolean | null = null
let isLoggedInCache: boolean | null = null;
function isLoggedIn(): boolean {
if (isLoggedInCache !== null) {
return isLoggedInCache
return isLoggedInCache;
}
isLoggedInCache = document.cookie.includes('auth=')
return isLoggedInCache
isLoggedInCache = document.cookie.includes('auth=');
return isLoggedInCache;
}
// Clear cache when auth changes
function onAuthChange() {
isLoggedInCache = null
isLoggedInCache = null;
}
```
@@ -13,16 +13,16 @@ Cache object property lookups in hot paths.
```typescript
for (let i = 0; i < arr.length; i++) {
process(obj.config.settings.value)
process(obj.config.settings.value);
}
```
**Correct (1 lookup total):**
```typescript
const value = obj.config.settings.value
const len = arr.length
const value = obj.config.settings.value;
const len = arr.length;
for (let i = 0; i < len; i++) {
process(value)
process(value);
}
```
@@ -13,7 +13,7 @@ tags: javascript, localStorage, storage, caching, performance
```typescript
function getTheme() {
return localStorage.getItem('theme') ?? 'light'
return localStorage.getItem('theme') ?? 'light';
}
// Called 10 times = 10 storage reads
```
@@ -21,18 +21,18 @@ function getTheme() {
**Correct (Map cache):**
```typescript
const storageCache = new Map<string, string | null>()
const storageCache = new Map<string, string | null>();
function getLocalStorage(key: string) {
if (!storageCache.has(key)) {
storageCache.set(key, localStorage.getItem(key))
storageCache.set(key, localStorage.getItem(key));
}
return storageCache.get(key)
return storageCache.get(key);
}
function setLocalStorage(key: string, value: string) {
localStorage.setItem(key, value)
storageCache.set(key, value) // keep cache in sync
localStorage.setItem(key, value);
storageCache.set(key, value); // keep cache in sync
}
```
@@ -41,15 +41,13 @@ Use a Map (not a hook) so it works everywhere: utilities, event handlers, not ju
**Cookie caching:**
```typescript
let cookieCache: Record<string, string> | null = null
let cookieCache: Record<string, string> | null = null;
function getCookie(name: string) {
if (!cookieCache) {
cookieCache = Object.fromEntries(
document.cookie.split('; ').map(c => c.split('='))
)
cookieCache = Object.fromEntries(document.cookie.split('; ').map((c) => c.split('=')));
}
return cookieCache[name]
return cookieCache[name];
}
```
@@ -59,12 +57,12 @@ If storage can change externally (another tab, server-set cookies), invalidate c
```typescript
window.addEventListener('storage', (e) => {
if (e.key) storageCache.delete(e.key)
})
if (e.key) storageCache.delete(e.key);
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
storageCache.clear()
storageCache.clear();
}
})
});
```
@@ -12,21 +12,21 @@ Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine
**Incorrect (3 iterations):**
```typescript
const admins = users.filter(u => u.isAdmin)
const testers = users.filter(u => u.isTester)
const inactive = users.filter(u => !u.isActive)
const admins = users.filter((u) => u.isAdmin);
const testers = users.filter((u) => u.isTester);
const inactive = users.filter((u) => !u.isActive);
```
**Correct (1 iteration):**
```typescript
const admins: User[] = []
const testers: User[] = []
const inactive: User[] = []
const admins: User[] = [];
const testers: User[] = [];
const inactive: User[] = [];
for (const user of users) {
if (user.isAdmin) admins.push(user)
if (user.isTester) testers.push(user)
if (!user.isActive) inactive.push(user)
if (user.isAdmin) admins.push(user);
if (user.isTester) testers.push(user);
if (!user.isActive) inactive.push(user);
}
```
@@ -13,22 +13,22 @@ Return early when result is determined to skip unnecessary processing.
```typescript
function validateUsers(users: User[]) {
let hasError = false
let errorMessage = ''
let hasError = false;
let errorMessage = '';
for (const user of users) {
if (!user.email) {
hasError = true
errorMessage = 'Email required'
hasError = true;
errorMessage = 'Email required';
}
if (!user.name) {
hasError = true
errorMessage = 'Name required'
hasError = true;
errorMessage = 'Name required';
}
// Continues checking all users even after error found
}
return hasError ? { valid: false, error: errorMessage } : { valid: true }
return hasError ? { valid: false, error: errorMessage } : { valid: true };
}
```
@@ -38,13 +38,13 @@ function validateUsers(users: User[]) {
function validateUsers(users: User[]) {
for (const user of users) {
if (!user.email) {
return { valid: false, error: 'Email required' }
return { valid: false, error: 'Email required' };
}
if (!user.name) {
return { valid: false, error: 'Name required' }
return { valid: false, error: 'Name required' };
}
}
return { valid: true }
return { valid: true };
}
```
@@ -39,7 +39,7 @@ function Highlighter({ text, query }: Props) {
Global regex (`/g`) has mutable `lastIndex` state:
```typescript
const regex = /foo/g
regex.test('foo') // true, lastIndex = 3
regex.test('foo') // false, lastIndex = 0
const regex = /foo/g;
regex.test('foo'); // true, lastIndex = 3
regex.test('foo'); // false, lastIndex = 0
```
@@ -13,10 +13,10 @@ Multiple `.find()` calls by the same key should use a Map.
```typescript
function processOrders(orders: Order[], users: User[]) {
return orders.map(order => ({
return orders.map((order) => ({
...order,
user: users.find(u => u.id === order.userId)
}))
user: users.find((u) => u.id === order.userId),
}));
}
```
@@ -24,12 +24,12 @@ function processOrders(orders: Order[], users: User[]) {
```typescript
function processOrders(orders: Order[], users: User[]) {
const userById = new Map(users.map(u => [u.id, u]))
const userById = new Map(users.map((u) => [u.id, u]));
return orders.map(order => ({
return orders.map((order) => ({
...order,
user: userById.get(order.userId)
}))
user: userById.get(order.userId),
}));
}
```
@@ -16,7 +16,7 @@ In real-world applications, this optimization is especially valuable when the co
```typescript
function hasChanges(current: string[], original: string[]) {
// Always sorts and joins, even when lengths differ
return current.sort().join() !== original.sort().join()
return current.sort().join() !== original.sort().join();
}
```
@@ -28,21 +28,22 @@ Two O(n log n) sorts run even when `current.length` is 5 and `original.length` i
function hasChanges(current: string[], original: string[]) {
// Early return if lengths differ
if (current.length !== original.length) {
return true
return true;
}
// Only sort when lengths match
const currentSorted = current.toSorted()
const originalSorted = original.toSorted()
const currentSorted = current.toSorted();
const originalSorted = original.toSorted();
for (let i = 0; i < currentSorted.length; i++) {
if (currentSorted[i] !== originalSorted[i]) {
return true
return true;
}
}
return false
return false;
}
```
This new approach is more efficient because:
- It avoids the overhead of sorting and joining the arrays when lengths differ
- It avoids consuming memory for the joined strings (especially important for large arrays)
- It avoids mutating the original arrays
@@ -13,14 +13,14 @@ Finding the smallest or largest element only requires a single pass through the
```typescript
interface Project {
id: string
name: string
updatedAt: number
id: string;
name: string;
updatedAt: number;
}
function getLatestProject(projects: Project[]) {
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
return sorted[0]
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt);
return sorted[0];
}
```
@@ -30,8 +30,8 @@ Sorts the entire array just to find the maximum value.
```typescript
function getOldestAndNewest(projects: Project[]) {
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt);
return { oldest: sorted[0], newest: sorted[sorted.length - 1] };
}
```
@@ -41,31 +41,31 @@ Still sorts unnecessarily when only min/max are needed.
```typescript
function getLatestProject(projects: Project[]) {
if (projects.length === 0) return null
let latest = projects[0]
if (projects.length === 0) return null;
let latest = projects[0];
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt > latest.updatedAt) {
latest = projects[i]
latest = projects[i];
}
}
return latest
return latest;
}
function getOldestAndNewest(projects: Project[]) {
if (projects.length === 0) return { oldest: null, newest: null }
let oldest = projects[0]
let newest = projects[0]
if (projects.length === 0) return { oldest: null, newest: null };
let oldest = projects[0];
let newest = projects[0];
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i];
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i];
}
return { oldest, newest }
return { oldest, newest };
}
```
@@ -74,9 +74,9 @@ Single pass through the array, no copying, no sorting.
**Alternative (Math.min/Math.max for small arrays):**
```typescript
const numbers = [5, 2, 8, 1, 9]
const min = Math.min(...numbers)
const max = Math.max(...numbers)
const numbers = [5, 2, 8, 1, 9];
const min = Math.min(...numbers);
const max = Math.max(...numbers);
```
This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.
@@ -46,7 +46,7 @@ function UserList({ users }: { users: User[] }) {
```typescript
// Fallback for older browsers
const sorted = [...items].sort((a, b) => a.value - b.value)
const sorted = [...items].sort((a, b) => a.value - b.value);
```
**Other immutable array methods:**
@@ -12,14 +12,14 @@ Use React's `<Activity>` to preserve state/DOM for expensive components that fre
**Usage:**
```tsx
import { Activity } from 'react'
import { Activity } from 'react';
function Dropdown({ isOpen }: Props) {
return (
<Activity mode={isOpen ? 'visible' : 'hidden'}>
<ExpensiveMenu />
</Activity>
)
);
}
```
@@ -14,15 +14,10 @@ Many browsers don't have hardware acceleration for CSS3 animations on SVG elemen
```tsx
function LoadingSpinner() {
return (
<svg
className="animate-spin"
width="24"
height="24"
viewBox="0 0 24 24"
>
<svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
)
);
}
```
@@ -32,15 +27,11 @@ function LoadingSpinner() {
function LoadingSpinner() {
return (
<div className="animate-spin">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<svg width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
</div>
)
);
}
```
@@ -13,11 +13,7 @@ Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering
```tsx
function Badge({ count }: { count: number }) {
return (
<div>
{count && <span className="badge">{count}</span>}
</div>
)
return <div>{count && <span className="badge">{count}</span>}</div>;
}
// When count = 0, renders: <div>0</div>
@@ -28,11 +24,7 @@ function Badge({ count }: { count: number }) {
```tsx
function Badge({ count }: { count: number }) {
return (
<div>
{count > 0 ? <span className="badge">{count}</span> : null}
</div>
)
return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>;
}
// When count = 0, renders: <div></div>
@@ -23,15 +23,15 @@ Apply `content-visibility: auto` to defer off-screen rendering.
```tsx
function MessageList({ messages }: { messages: Message[] }) {
return (
<div className="overflow-y-auto h-screen">
{messages.map(msg => (
<div className="h-screen overflow-y-auto">
{messages.map((msg) => (
<div key={msg.id} className="message-item">
<Avatar user={msg.author} />
<div>{msg.content}</div>
</div>
))}
</div>
)
);
}
```
@@ -13,31 +13,21 @@ Extract static JSX outside components to avoid re-creation.
```tsx
function LoadingSkeleton() {
return <div className="animate-pulse h-20 bg-gray-200" />
return <div className="h-20 animate-pulse bg-gray-200" />;
}
function Container() {
return (
<div>
{loading && <LoadingSkeleton />}
</div>
)
return <div>{loading && <LoadingSkeleton />}</div>;
}
```
**Correct (reuses same element):**
```tsx
const loadingSkeleton = (
<div className="animate-pulse h-20 bg-gray-200" />
)
const loadingSkeleton = <div className="h-20 animate-pulse bg-gray-200" />;
function Container() {
return (
<div>
{loading && loadingSkeleton}
</div>
)
return <div>{loading && loadingSkeleton}</div>;
}
```
@@ -14,13 +14,9 @@ When rendering content that depends on client-side storage (localStorage, cookie
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
// localStorage is not available on server - throws error
const theme = localStorage.getItem('theme') || 'light'
return (
<div className={theme}>
{children}
</div>
)
const theme = localStorage.getItem('theme') || 'light';
return <div className={theme}>{children}</div>;
}
```
@@ -30,21 +26,17 @@ Server-side rendering will fail because `localStorage` is undefined.
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState('light')
const [theme, setTheme] = useState('light');
useEffect(() => {
// Runs after hydration - causes visible flash
const stored = localStorage.getItem('theme')
const stored = localStorage.getItem('theme');
if (stored) {
setTheme(stored)
setTheme(stored);
}
}, [])
return (
<div className={theme}>
{children}
</div>
)
}, []);
return <div className={theme}>{children}</div>;
}
```
@@ -56,9 +48,7 @@ Component first renders with default value (`light`), then updates after hydrati
function ThemeWrapper({ children }: { children: ReactNode }) {
return (
<>
<div id="theme-wrapper">
{children}
</div>
<div id="theme-wrapper">{children}</div>
<script
dangerouslySetInnerHTML={{
__html: `
@@ -73,7 +63,7 @@ function ThemeWrapper({ children }: { children: ReactNode }) {
}}
/>
</>
)
);
}
```
@@ -7,13 +7,13 @@ tags: rendering, hydration, ssr, nextjs
## Suppress Expected Hydration Mismatches
In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these _expected_ mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
**Incorrect (known mismatch warnings):**
```tsx
function Timestamp() {
return <span>{new Date().toLocaleString()}</span>
return <span>{new Date().toLocaleString()}</span>;
}
```
@@ -21,10 +21,6 @@ function Timestamp() {
```tsx
function Timestamp() {
return (
<span suppressHydrationWarning>
{new Date().toLocaleString()}
</span>
)
return <span suppressHydrationWarning>{new Date().toLocaleString()}</span>;
}
```
@@ -13,17 +13,17 @@ Use `useTransition` instead of manual `useState` for loading states. This provid
```tsx
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isLoading, setIsLoading] = useState(false)
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const handleSearch = async (value: string) => {
setIsLoading(true)
setQuery(value)
const data = await fetchResults(value)
setResults(data)
setIsLoading(false)
}
setIsLoading(true);
setQuery(value);
const data = await fetchResults(value);
setResults(data);
setIsLoading(false);
};
return (
<>
@@ -31,29 +31,29 @@ function SearchResults() {
{isLoading && <Spinner />}
<ResultsList results={results} />
</>
)
);
}
```
**Correct (useTransition with built-in pending state):**
```tsx
import { useTransition, useState } from 'react'
import { useTransition, useState } from 'react';
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (value: string) => {
setQuery(value) // Update input immediately
setQuery(value); // Update input immediately
startTransition(async () => {
// Fetch and update results
const data = await fetchResults(value)
setResults(data)
})
}
const data = await fetchResults(value);
setResults(data);
});
};
return (
<>
@@ -61,7 +61,7 @@ function SearchResults() {
{isPending && <Spinner />}
<ResultsList results={results} />
</>
)
);
}
```
@@ -13,14 +13,14 @@ Don't subscribe to dynamic state (searchParams, localStorage) if you only read i
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const searchParams = useSearchParams()
const searchParams = useSearchParams();
const handleShare = () => {
const ref = searchParams.get('ref')
shareChat(chatId, { ref })
}
const ref = searchParams.get('ref');
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>
return <button onClick={handleShare}>Share</button>;
}
```
@@ -29,11 +29,11 @@ function ShareButton({ chatId }: { chatId: string }) {
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
const ref = params.get('ref')
shareChat(chatId, { ref })
}
const params = new URLSearchParams(window.location.search);
const ref = params.get('ref');
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>
return <button onClick={handleShare}>Share</button>;
}
```
@@ -13,16 +13,16 @@ Specify primitive dependencies instead of objects to minimize effect re-runs.
```tsx
useEffect(() => {
console.log(user.id)
}, [user])
console.log(user.id);
}, [user]);
```
**Correct (re-runs only when id changes):**
```tsx
useEffect(() => {
console.log(user.id)
}, [user.id])
console.log(user.id);
}, [user.id]);
```
**For derived state, compute outside effect:**
@@ -31,15 +31,15 @@ useEffect(() => {
// Incorrect: runs on width=767, 766, 765...
useEffect(() => {
if (width < 768) {
enableMobileMode()
enableMobileMode();
}
}, [width])
}, [width]);
// Correct: runs only on boolean transition
const isMobile = width < 768
const isMobile = width < 768;
useEffect(() => {
if (isMobile) {
enableMobileMode()
enableMobileMode();
}
}, [isMobile])
}, [isMobile]);
```
@@ -13,15 +13,15 @@ If a value can be computed from current props/state, do not store it in state or
```tsx
function Form() {
const [firstName, setFirstName] = useState('First')
const [lastName, setLastName] = useState('Last')
const [fullName, setFullName] = useState('')
const [firstName, setFirstName] = useState('First');
const [lastName, setLastName] = useState('Last');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName)
}, [firstName, lastName])
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <p>{fullName}</p>
return <p>{fullName}</p>;
}
```
@@ -29,11 +29,11 @@ function Form() {
```tsx
function Form() {
const [firstName, setFirstName] = useState('First')
const [lastName, setLastName] = useState('Last')
const fullName = firstName + ' ' + lastName
const [firstName, setFirstName] = useState('First');
const [lastName, setLastName] = useState('Last');
const fullName = firstName + ' ' + lastName;
return <p>{fullName}</p>
return <p>{fullName}</p>;
}
```
@@ -13,9 +13,9 @@ Subscribe to derived boolean state instead of continuous values to reduce re-ren
```tsx
function Sidebar() {
const width = useWindowWidth() // updates continuously
const isMobile = width < 768
return <nav className={isMobile ? 'mobile' : 'desktop'} />
const width = useWindowWidth(); // updates continuously
const isMobile = width < 768;
return <nav className={isMobile ? 'mobile' : 'desktop'} />;
}
```
@@ -23,7 +23,7 @@ function Sidebar() {
```tsx
function Sidebar() {
const isMobile = useMediaQuery('(max-width: 767px)')
return <nav className={isMobile ? 'mobile' : 'desktop'} />
const isMobile = useMediaQuery('(max-width: 767px)');
return <nav className={isMobile ? 'mobile' : 'desktop'} />;
}
```
@@ -13,19 +13,22 @@ When updating state based on the current state value, use the functional update
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems)
const [items, setItems] = useState(initialItems);
// Callback must depend on items, recreated on every items change
const addItems = useCallback((newItems: Item[]) => {
setItems([...items, ...newItems])
}, [items]) // ❌ items dependency causes recreations
const addItems = useCallback(
(newItems: Item[]) => {
setItems([...items, ...newItems]);
},
[items],
); // ❌ items dependency causes recreations
// Risk of stale closure if dependency is forgotten
const removeItem = useCallback((id: string) => {
setItems(items.filter(item => item.id !== id))
}, []) // ❌ Missing items dependency - will use stale items!
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
setItems(items.filter((item) => item.id !== id));
}, []); // ❌ Missing items dependency - will use stale items!
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
}
```
@@ -35,19 +38,19 @@ The first callback is recreated every time `items` changes, which can cause chil
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems)
const [items, setItems] = useState(initialItems);
// Stable callback, never recreated
const addItems = useCallback((newItems: Item[]) => {
setItems(curr => [...curr, ...newItems])
}, []) // ✅ No dependencies needed
setItems((curr) => [...curr, ...newItems]);
}, []); // ✅ No dependencies needed
// Always uses latest state, no stale closure risk
const removeItem = useCallback((id: string) => {
setItems(curr => curr.filter(item => item.id !== id))
}, []) // ✅ Safe and stable
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
setItems((curr) => curr.filter((item) => item.id !== id));
}, []); // ✅ Safe and stable
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
}
```
@@ -14,20 +14,18 @@ Pass a function to `useState` for expensive initial values. Without the function
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs on EVERY render, even after initialization
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
const [query, setQuery] = useState('')
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items));
const [query, setQuery] = useState('');
// When query changes, buildSearchIndex runs again unnecessarily
return <SearchResults index={searchIndex} query={query} />
return <SearchResults index={searchIndex} query={query} />;
}
function UserProfile() {
// JSON.parse runs on every render
const [settings, setSettings] = useState(
JSON.parse(localStorage.getItem('settings') || '{}')
)
return <SettingsForm settings={settings} onChange={setSettings} />
const [settings, setSettings] = useState(JSON.parse(localStorage.getItem('settings') || '{}'));
return <SettingsForm settings={settings} onChange={setSettings} />;
}
```
@@ -36,20 +34,20 @@ function UserProfile() {
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs ONLY on initial render
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
const [query, setQuery] = useState('')
return <SearchResults index={searchIndex} query={query} />
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items));
const [query, setQuery] = useState('');
return <SearchResults index={searchIndex} query={query} />;
}
function UserProfile() {
// JSON.parse runs only on initial render
const [settings, setSettings] = useState(() => {
const stored = localStorage.getItem('settings')
return stored ? JSON.parse(stored) : {}
})
return <SettingsForm settings={settings} onChange={setSettings} />
const stored = localStorage.getItem('settings');
return stored ? JSON.parse(stored) : {};
});
return <SettingsForm settings={settings} onChange={setSettings} />;
}
```
@@ -1,10 +1,8 @@
---
title: Extract Default Non-primitive Parameter Value from Memoized Component to Constant
impact: MEDIUM
impactDescription: restores memoization by using a constant for default value
tags: rerender, memo, optimization
---
## Extract Default Non-primitive Parameter Value from Memoized Component to Constant
@@ -14,12 +14,12 @@ Extract expensive work into memoized components to enable early returns before c
```tsx
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => {
const id = computeAvatarId(user)
return <Avatar id={id} />
}, [user])
const id = computeAvatarId(user);
return <Avatar id={id} />;
}, [user]);
if (loading) return <Skeleton />
return <div>{avatar}</div>
if (loading) return <Skeleton />;
return <div>{avatar}</div>;
}
```
@@ -27,17 +27,17 @@ function Profile({ user, loading }: Props) {
```tsx
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user])
return <Avatar id={id} />
})
const id = useMemo(() => computeAvatarId(user), [user]);
return <Avatar id={id} />;
});
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton />
if (loading) return <Skeleton />;
return (
<div>
<UserAvatar user={user} />
</div>
)
);
}
```
@@ -13,17 +13,17 @@ If a side effect is triggered by a specific user action (submit, click, drag), r
```tsx
function Form() {
const [submitted, setSubmitted] = useState(false)
const theme = useContext(ThemeContext)
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);
useEffect(() => {
if (submitted) {
post('/api/register')
showToast('Registered', theme)
post('/api/register');
showToast('Registered', theme);
}
}, [submitted, theme])
}, [submitted, theme]);
return <button onClick={() => setSubmitted(true)}>Submit</button>
return <button onClick={() => setSubmitted(true)}>Submit</button>;
}
```
@@ -31,14 +31,14 @@ function Form() {
```tsx
function Form() {
const theme = useContext(ThemeContext)
const theme = useContext(ThemeContext);
function handleSubmit() {
post('/api/register')
showToast('Registered', theme)
post('/api/register');
showToast('Registered', theme);
}
return <button onClick={handleSubmit}>Submit</button>
return <button onClick={handleSubmit}>Submit</button>;
}
```
@@ -15,10 +15,10 @@ Calling `useMemo` and comparing hook dependencies may consume more resources tha
```tsx
function Header({ user, notifications }: Props) {
const isLoading = useMemo(() => {
return user.isLoading || notifications.isLoading
}, [user.isLoading, notifications.isLoading])
return user.isLoading || notifications.isLoading;
}, [user.isLoading, notifications.isLoading]);
if (isLoading) return <Skeleton />
if (isLoading) return <Skeleton />;
// return some markup
}
```
@@ -27,9 +27,9 @@ function Header({ user, notifications }: Props) {
```tsx
function Header({ user, notifications }: Props) {
const isLoading = user.isLoading || notifications.isLoading
const isLoading = user.isLoading || notifications.isLoading;
if (isLoading) return <Skeleton />
if (isLoading) return <Skeleton />;
// return some markup
}
```
@@ -13,28 +13,28 @@ Mark frequent, non-urgent state updates as transitions to maintain UI responsive
```tsx
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handler = () => setScrollY(window.scrollY)
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
const handler = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handler, { passive: true });
return () => window.removeEventListener('scroll', handler);
}, []);
}
```
**Correct (non-blocking updates):**
```tsx
import { startTransition } from 'react'
import { startTransition } from 'react';
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY))
}
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
startTransition(() => setScrollY(window.scrollY));
};
window.addEventListener('scroll', handler, { passive: true });
return () => window.removeEventListener('scroll', handler);
}, []);
}
```
@@ -13,13 +13,13 @@ When a value changes frequently and you don't want a re-render on every update (
```tsx
function Tracker() {
const [lastX, setLastX] = useState(0)
const [lastX, setLastX] = useState(0);
useEffect(() => {
const onMove = (e: MouseEvent) => setLastX(e.clientX)
window.addEventListener('mousemove', onMove)
return () => window.removeEventListener('mousemove', onMove)
}, [])
const onMove = (e: MouseEvent) => setLastX(e.clientX);
window.addEventListener('mousemove', onMove);
return () => window.removeEventListener('mousemove', onMove);
}, []);
return (
<div
@@ -32,7 +32,7 @@ function Tracker() {
background: 'black',
}}
/>
)
);
}
```
@@ -40,20 +40,20 @@ function Tracker() {
```tsx
function Tracker() {
const lastXRef = useRef(0)
const dotRef = useRef<HTMLDivElement>(null)
const lastXRef = useRef(0);
const dotRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const onMove = (e: MouseEvent) => {
lastXRef.current = e.clientX
const node = dotRef.current
lastXRef.current = e.clientX;
const node = dotRef.current;
if (node) {
node.style.transform = `translateX(${e.clientX}px)`
node.style.transform = `translateX(${e.clientX}px)`;
}
}
window.addEventListener('mousemove', onMove)
return () => window.removeEventListener('mousemove', onMove)
}, [])
};
window.addEventListener('mousemove', onMove);
return () => window.removeEventListener('mousemove', onMove);
}, []);
return (
<div
@@ -68,6 +68,6 @@ function Tracker() {
transform: 'translateX(0px)',
}}
/>
)
);
}
```
@@ -12,46 +12,46 @@ Use Next.js's `after()` to schedule work that should execute after a response is
**Incorrect (blocks response):**
```tsx
import { logUserAction } from '@/app/utils'
import { logUserAction } from '@/app/utils';
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
await updateDatabase(request);
// Logging blocks the response
const userAgent = request.headers.get('user-agent') || 'unknown'
await logUserAction({ userAgent })
const userAgent = request.headers.get('user-agent') || 'unknown';
await logUserAction({ userAgent });
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
headers: { 'Content-Type': 'application/json' },
});
}
```
**Correct (non-blocking):**
```tsx
import { after } from 'next/server'
import { headers, cookies } from 'next/headers'
import { logUserAction } from '@/app/utils'
import { after } from 'next/server';
import { headers, cookies } from 'next/headers';
import { logUserAction } from '@/app/utils';
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
await updateDatabase(request);
// Log after response is sent
after(async () => {
const userAgent = (await headers()).get('user-agent') || 'unknown'
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
logUserAction({ sessionCookie, userAgent })
})
const userAgent = (await headers()).get('user-agent') || 'unknown';
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous';
logUserAction({ sessionCookie, userAgent });
});
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
headers: { 'Content-Type': 'application/json' },
});
}
```
@@ -16,80 +16,80 @@ Next.js documentation explicitly states: "Treat Server Actions with the same sec
**Incorrect (no authentication check):**
```typescript
'use server'
'use server';
export async function deleteUser(userId: string) {
// Anyone can call this! No auth check
await db.user.delete({ where: { id: userId } })
return { success: true }
await db.user.delete({ where: { id: userId } });
return { success: true };
}
```
**Correct (authentication inside the action):**
```typescript
'use server'
'use server';
import { verifySession } from '@/lib/auth'
import { unauthorized } from '@/lib/errors'
import { verifySession } from '@/lib/auth';
import { unauthorized } from '@/lib/errors';
export async function deleteUser(userId: string) {
// Always check auth inside the action
const session = await verifySession()
const session = await verifySession();
if (!session) {
throw unauthorized('Must be logged in')
throw unauthorized('Must be logged in');
}
// Check authorization too
if (session.user.role !== 'admin' && session.user.id !== userId) {
throw unauthorized('Cannot delete other users')
throw unauthorized('Cannot delete other users');
}
await db.user.delete({ where: { id: userId } })
return { success: true }
await db.user.delete({ where: { id: userId } });
return { success: true };
}
```
**With input validation:**
```typescript
'use server'
'use server';
import { verifySession } from '@/lib/auth'
import { z } from 'zod'
import { verifySession } from '@/lib/auth';
import { z } from 'zod';
const updateProfileSchema = z.object({
userId: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email()
})
email: z.string().email(),
});
export async function updateProfile(data: unknown) {
// Validate input first
const validated = updateProfileSchema.parse(data)
const validated = updateProfileSchema.parse(data);
// Then authenticate
const session = await verifySession()
const session = await verifySession();
if (!session) {
throw new Error('Unauthorized')
throw new Error('Unauthorized');
}
// Then authorize
if (session.user.id !== validated.userId) {
throw new Error('Can only update own profile')
throw new Error('Can only update own profile');
}
// Finally perform the mutation
await db.user.update({
where: { id: validated.userId },
data: {
name: validated.name,
email: validated.email
}
})
return { success: true }
email: validated.email,
},
});
return { success: true };
}
```
@@ -12,20 +12,20 @@ tags: server, cache, lru, cross-request
**Implementation:**
```typescript
import { LRUCache } from 'lru-cache'
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, any>({
max: 1000,
ttl: 5 * 60 * 1000 // 5 minutes
})
ttl: 5 * 60 * 1000, // 5 minutes
});
export async function getUser(id: string) {
const cached = cache.get(id)
if (cached) return cached
const cached = cache.get(id);
if (cached) return cached;
const user = await db.user.findUnique({ where: { id } })
cache.set(id, user)
return user
const user = await db.user.findUnique({ where: { id } });
cache.set(id, user);
return user;
}
// Request 1: DB query, result cached
@@ -12,15 +12,15 @@ Use `React.cache()` for server-side request deduplication. Authentication and da
**Usage:**
```typescript
import { cache } from 'react'
import { cache } from 'react';
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
const session = await auth();
if (!session?.user?.id) return null;
return await db.user.findUnique({
where: { id: session.user.id }
})
})
where: { id: session.user.id },
});
});
```
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
@@ -33,32 +33,32 @@ Within a single request, multiple calls to `getCurrentUser()` execute the query
```typescript
const getUser = cache(async (params: { uid: number }) => {
return await db.user.findUnique({ where: { id: params.uid } })
})
return await db.user.findUnique({ where: { id: params.uid } });
});
// Each call creates new object, never hits cache
getUser({ uid: 1 })
getUser({ uid: 1 }) // Cache miss, runs query again
getUser({ uid: 1 });
getUser({ uid: 1 }); // Cache miss, runs query again
```
**Correct (cache hit):**
```typescript
const getUser = cache(async (uid: number) => {
return await db.user.findUnique({ where: { id: uid } })
})
return await db.user.findUnique({ where: { id: uid } });
});
// Primitive args use value equality
getUser(1)
getUser(1) // Cache hit, returns cached result
getUser(1);
getUser(1); // Cache hit, returns cached result
```
If you must pass objects, pass the same reference:
```typescript
const params = { uid: 1 }
getUser(params) // Query runs
getUser(params) // Cache hit (same reference)
const params = { uid: 1 };
getUser(params); // Query runs
getUser(params); // Cache hit (same reference)
```
**Next.js-Specific Note:**
@@ -22,11 +22,11 @@ RSC→client serialization deduplicates by object reference, not value. Same ref
```tsx
// RSC: send once
<ClientList usernames={usernames} />
<ClientList usernames={usernames} />;
// Client: transform there
'use client'
const sorted = useMemo(() => [...usernames].sort(), [usernames])
('use client');
const sorted = useMemo(() => [...usernames].sort(), [usernames]);
```
**Nested deduplication behavior:**
@@ -13,18 +13,18 @@ React Server Components execute sequentially within a tree. Restructure with com
```tsx
export default async function Page() {
const header = await fetchHeader()
const header = await fetchHeader();
return (
<div>
<div>{header}</div>
<Sidebar />
</div>
)
);
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
```
@@ -32,13 +32,13 @@ async function Sidebar() {
```tsx
async function Header() {
const data = await fetchHeader()
return <div>{data}</div>
const data = await fetchHeader();
return <div>{data}</div>;
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
export default function Page() {
@@ -47,7 +47,7 @@ export default function Page() {
<Header />
<Sidebar />
</div>
)
);
}
```
@@ -55,13 +55,13 @@ export default function Page() {
```tsx
async function Header() {
const data = await fetchHeader()
return <div>{data}</div>
const data = await fetchHeader();
return <div>{data}</div>;
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
function Layout({ children }: { children: ReactNode }) {
@@ -70,7 +70,7 @@ function Layout({ children }: { children: ReactNode }) {
<Header />
{children}
</div>
)
);
}
export default function Page() {
@@ -78,6 +78,6 @@ export default function Page() {
<Layout>
<Sidebar />
</Layout>
)
);
}
```
@@ -13,13 +13,13 @@ The React Server/Client boundary serializes all object properties into strings a
```tsx
async function Page() {
const user = await fetchUser() // 50 fields
return <Profile user={user} />
const user = await fetchUser(); // 50 fields
return <Profile user={user} />;
}
'use client'
('use client');
function Profile({ user }: { user: User }) {
return <div>{user.name}</div> // uses 1 field
return <div>{user.name}</div>; // uses 1 field
}
```
@@ -27,12 +27,12 @@ function Profile({ user }: { user: User }) {
```tsx
async function Page() {
const user = await fetchUser()
return <Profile name={user.name} />
const user = await fetchUser();
return <Profile name={user.name} />;
}
'use client'
('use client');
function Profile({ name }: { name: string }) {
return <div>{name}</div>
return <div>{name}</div>;
}
```
+2 -1
View File
@@ -3,7 +3,7 @@ name: web-design-guidelines
description: Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
metadata:
author: vercel
version: "1.0.0"
version: '1.0.0'
argument-hint: <file-or-pattern>
---
@@ -31,6 +31,7 @@ Use WebFetch to retrieve the latest rules. The fetched content contains all the
## Usage
When a user provides a file or pattern argument:
1. Fetch guidelines from the source URL above
2. Read the specified files
3. Apply all rules from the fetched guidelines
+3
View File
@@ -18,9 +18,11 @@ Implement tasks from an OpenSpec change.
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
@@ -132,6 +134,7 @@ What would you like to do?
```
**Guardrails**
- Keep going through tasks until done or blocked
- Always read context files before starting (from the apply instructions output)
- If task is ambiguous, pause and ask before implementing
+2
View File
@@ -61,6 +61,7 @@ Archive a completed change in the experimental workflow.
5. **Perform the archive**
Create the archive directory if it doesn't exist:
```bash
mkdir -p openspec/changes/archive
```
@@ -145,6 +146,7 @@ Target archive directory already exists.
```
**Guardrails**
- Always prompt for change selection if not provided
- Use artifact graph (openspec status --json) for completion checking
- Don't block archive on warnings - just inform and confirm
+31 -25
View File
@@ -30,16 +30,16 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
For each selected change, collect:
a. **Artifact status** - Run `openspec status --change "<name>" --json`
- Parse `schemaName` and `artifacts` list
- Note which artifacts are `done` vs other states
- Parse `schemaName` and `artifacts` list
- Note which artifacts are `done` vs other states
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
- If no tasks file exists, note as "No tasks"
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
- If no tasks file exists, note as "No tasks"
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
- List which capability specs exist
- For each, extract requirement names (lines matching `### Requirement: <name>`)
- List which capability specs exist
- For each, extract requirement names (lines matching `### Requirement: <name>`)
4. **Detect spec conflicts**
@@ -59,18 +59,18 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
b. **Search the codebase** for implementation evidence:
- Look for code implementing requirements from each delta spec
- Check for related files, functions, or tests
- Look for code implementing requirements from each delta spec
- Check for related files, functions, or tests
c. **Determine resolution**:
- If only one change is actually implemented -> sync that one's specs
- If both implemented -> apply in chronological order (older first, newer overwrites)
- If neither implemented -> skip spec sync, warn user
- If only one change is actually implemented -> sync that one's specs
- If both implemented -> apply in chronological order (older first, newer overwrites)
- If neither implemented -> skip spec sync, warn user
d. **Record resolution** for each conflict:
- Which change's specs to apply
- In what order (if both)
- Rationale (what was found in codebase)
- Which change's specs to apply
- In what order (if both)
- Rationale (what was found in codebase)
6. **Show consolidated status table**
@@ -86,12 +86,14 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
```
For conflicts, show the resolution:
```
* Conflict resolution:
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
```
For incomplete changes, show warnings:
```
Warnings:
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
@@ -100,7 +102,6 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
7. **Confirm batch operation**
Use **AskUserQuestion tool** with a single confirmation:
- "Archive N changes?" with options based on status
- Options might include:
- "Archive all N changes"
@@ -114,20 +115,21 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
Process changes in the determined order (respecting conflict resolution):
a. **Sync specs** if delta specs exist:
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
- For conflicts, apply in resolved order
- Track if sync was done
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
- For conflicts, apply in resolved order
- Track if sync was done
b. **Perform the archive**:
```bash
mkdir -p openspec/changes/archive
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
```
```bash
mkdir -p openspec/changes/archive
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
```
c. **Track outcome** for each change:
- Success: archived successfully
- Failed: error during archive (record error)
- Skipped: user chose not to archive (if applicable)
- Success: archived successfully
- Failed: error during archive (record error)
- Skipped: user chose not to archive (if applicable)
9. **Display summary**
@@ -150,6 +152,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
```
If any failures:
```
Failed 1 change:
- some-change: Archive directory already exists
@@ -158,6 +161,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
**Conflict Resolution Examples**
Example 1: Only one implemented
```
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
@@ -173,6 +177,7 @@ Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
```
Example 2: Both implemented
```
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
@@ -226,6 +231,7 @@ No active changes found. Use `/opsx:new` to create a new change.
```
**Guardrails**
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
- Always prompt for selection, never auto-select
- Detect spec conflicts early and resolve by checking codebase
+8 -3
View File
@@ -23,9 +23,11 @@ Continue working on a change by creating the next artifact.
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check current status**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand current state. The response includes:
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
@@ -33,7 +35,7 @@ Continue working on a change by creating the next artifact.
3. **Act based on status**:
---
***
**If all artifacts are complete (`isComplete: true`)**:
- Congratulate the user
@@ -41,7 +43,7 @@ Continue working on a change by creating the next artifact.
- Suggest: "All artifacts created! You can now implement this change with `/opsx:apply` or archive it with `/opsx:archive`."
- STOP
---
***
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
- Pick the FIRST artifact with `status: "ready"` from the status output
@@ -64,7 +66,7 @@ Continue working on a change by creating the next artifact.
- Show what was created and what's now unlocked
- STOP after creating ONE artifact
---
***
**If no artifacts are ready (all blocked)**:
- This shouldn't happen with a valid schema
@@ -78,6 +80,7 @@ Continue working on a change by creating the next artifact.
**Output**
After each invocation, show:
- Which artifact was created
- Schema workflow being used
- Current progress (N/M complete)
@@ -91,6 +94,7 @@ The artifact types and their purpose depend on the schema. Use the `instruction`
Common artifact patterns:
**spec-driven schema** (proposal → specs → design → tasks):
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
- The Capabilities section is critical - each capability listed will need a spec file.
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
@@ -100,6 +104,7 @@ Common artifact patterns:
For other schemas, follow the `instruction` field from the CLI output.
**Guardrails**
- Create ONE artifact per invocation
- Always read dependency artifacts before creating a new one
- Never skip artifacts or create out of order
+15 -7
View File
@@ -9,6 +9,7 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
- A vague idea: "real-time collaboration"
- A specific problem: "the auth system is getting unwieldy"
- A change name: "add-dark-mode" (to explore in context of that change)
@@ -33,24 +34,28 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher
Depending on what the user brings, you might:
**Explore the problem space**
- Ask clarifying questions that emerge from what they said
- Challenge assumptions
- Reframe the problem
- Find analogies
**Investigate the codebase**
- Map existing architecture relevant to the discussion
- Find integration points
- Identify patterns already in use
- Surface hidden complexity
**Compare options**
- Brainstorm multiple approaches
- Build comparison tables
- Sketch tradeoffs
- Recommend a path (if asked)
**Visualize**
```
┌─────────────────────────────────────────┐
│ Use ASCII diagrams liberally │
@@ -69,6 +74,7 @@ Depending on what the user brings, you might:
```
**Surface risks and unknowns**
- Identify what could go wrong
- Find gaps in understanding
- Suggest spikes or investigations
@@ -82,11 +88,13 @@ You have full context of the OpenSpec system. Use it naturally, don't force it.
### Check for context
At the start, quickly check what exists:
```bash
openspec list --json
```
This tells you:
- If there are active changes
- Their names, schemas, and status
- What the user might be working on
@@ -117,14 +125,14 @@ If the user mentions a change or you detect one is relevant:
3. **Offer to capture when decisions are made**
| Insight Type | Where to Capture |
|--------------|------------------|
| Insight Type | Where to Capture |
| -------------------------- | ---------------------------- |
| New requirement discovered | `specs/<capability>/spec.md` |
| Requirement changed | `specs/<capability>/spec.md` |
| Design decision made | `design.md` |
| Scope changed | `proposal.md` |
| New work identified | `tasks.md` |
| Assumption invalidated | Relevant artifact |
| Requirement changed | `specs/<capability>/spec.md` |
| Design decision made | `design.md` |
| Scope changed | `proposal.md` |
| New work identified | `tasks.md` |
| Assumption invalidated | Relevant artifact |
Example offers:
- "That's a design decision. Capture it in design.md?"
+27 -20
View File
@@ -11,6 +11,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
1. **If no input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
@@ -18,15 +19,19 @@ Fast-forward through artifact creation - generate everything needed to start imp
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
```bash
openspec new change "<name>"
```
This creates a scaffolded change at `openspec/changes/<name>/`.
3. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
@@ -38,30 +43,30 @@ Fast-forward through artifact creation - generate everything needed to start imp
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "✓ Created <artifact-id>"
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "✓ Created <artifact-id>"
b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
5. **Show final status**
```bash
@@ -71,6 +76,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
@@ -84,6 +90,7 @@ After completing all artifacts, summarize:
- Use the `template` as a starting point, filling in based on context
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
+9
View File
@@ -11,6 +11,7 @@ Start a new change using the experimental artifact-driven approach.
1. **If no input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
@@ -28,23 +29,29 @@ Start a new change using the experimental artifact-driven approach.
**Otherwise**: Omit `--schema` to use the default.
3. **Create the change directory**
```bash
openspec new change "<name>"
```
Add `--schema <name>` only if the user requested a specific workflow.
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
4. **Show the artifact status**
```bash
openspec status --change "<name>"
```
This shows which artifacts need to be created and which are ready (dependencies satisfied).
5. **Get instructions for the first artifact**
The first artifact depends on the schema. Check the status output to find the first artifact with status "ready".
```bash
openspec instructions <first-artifact-id> --change "<name>"
```
This outputs the template and context for creating the first artifact.
6. **STOP and wait for user direction**
@@ -52,6 +59,7 @@ Start a new change using the experimental artifact-driven approach.
**Output**
After completing the steps, summarize:
- Change name and location
- Schema/workflow being used and its artifact sequence
- Current status (0/N artifacts complete)
@@ -59,6 +67,7 @@ After completing the steps, summarize:
- Prompt: "Ready to create the first artifact? Run `/opsx:continue` or just describe what this change is about and I'll draft it."
**Guardrails**
- Do NOT create any artifacts yet - just show the instructions
- Do NOT advance beyond showing the first artifact template
- If the name is invalid (not kebab-case), ask for a valid name
+24 -4
View File
@@ -15,6 +15,7 @@ openspec status --json 2>&1 || echo "NOT_INITIALIZED"
```
**If not initialized:**
> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`.
Stop here if not initialized.
@@ -59,6 +60,7 @@ Scan the codebase for small improvement opportunities. Look for:
6. **Missing validation** - User input handlers without validation
Also check recent git activity:
```bash
git log --oneline -10 2>/dev/null || echo "No git history"
```
@@ -94,6 +96,7 @@ Which task interests you? (Pick a number or describe your own)
```
**If nothing found:** Fall back to asking what the user wants to build:
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
### Scope Guardrail
@@ -126,6 +129,7 @@ Before we create a change, let me quickly show you **explore mode**—it's how y
```
Spend 1-2 minutes investigating the relevant code:
- Read the file(s) involved
- Draw a quick ASCII diagram if it helps
- Note any considerations
@@ -151,6 +155,7 @@ Now let's create a change to hold our work.
## Phase 4: Create the Change
**EXPLAIN:**
```
## Creating a Change
@@ -160,21 +165,25 @@ Let me create one for our task.
```
**DO:** Create the change with a derived kebab-case name:
```bash
openspec new change "<derived-name>"
```
**SHOW:**
```
Created: `openspec/changes/<name>/`
The folder structure:
```
openspec/changes/<name>/
├── proposal.md ← Why we're doing this (empty, we'll fill it)
├── design.md ← How we'll build it (empty)
├── specs/ ← Detailed requirements (empty)
└── tasks.md ← Implementation checklist (empty)
├── proposal.md ← Why we're doing this (empty, we'll fill it)
├── design.md ← How we'll build it (empty)
├── specs/ ← Detailed requirements (empty)
└── tasks.md ← Implementation checklist (empty)
```
Now let's fill in the first artifact—the proposal.
@@ -185,6 +194,7 @@ Now let's fill in the first artifact—the proposal.
## Phase 5: Proposal
**EXPLAIN:**
```
## The Proposal
@@ -229,9 +239,11 @@ Does this capture the intent? I can adjust before we save it.
**PAUSE** - Wait for user approval/feedback.
After approval, save the proposal:
```bash
openspec instructions proposal --change "<name>" --json
```
Then write the content to `openspec/changes/<name>/proposal.md`.
```
@@ -245,6 +257,7 @@ Next up: specs.
## Phase 6: Specs
**EXPLAIN:**
```
## Specs
@@ -254,6 +267,7 @@ For a small task like this, we might only need one spec file.
```
**DO:** Create the spec file:
```bash
mkdir -p openspec/changes/<name>/specs/<capability-name>
```
@@ -289,6 +303,7 @@ Save to `openspec/changes/<name>/specs/<capability>/spec.md`.
## Phase 7: Design
**EXPLAIN:**
```
## Design
@@ -334,6 +349,7 @@ Save to `openspec/changes/<name>/design.md`.
## Phase 8: Tasks
**EXPLAIN:**
```
## Tasks
@@ -372,6 +388,7 @@ Save to `openspec/changes/<name>/tasks.md`.
## Phase 9: Apply (Implementation)
**EXPLAIN:**
```
## Implementation
@@ -406,6 +423,7 @@ The change is implemented! One more step—let's archive it.
## Phase 10: Archive
**EXPLAIN:**
```
## Archiving
@@ -415,11 +433,13 @@ Archived changes become your project's decision history—you can always find th
```
**DO:**
```bash
openspec archive "<name>"
```
**SHOW:**
```
Archived to: `openspec/changes/archive/YYYY-MM-DD-<name>/`
+24 -18
View File
@@ -40,28 +40,28 @@ This is an **agent-driven** operation - you will read delta specs and directly e
c. **Apply changes intelligently**:
**ADDED Requirements:**
- If requirement doesn't exist in main spec → add it
- If requirement already exists → update it to match (treat as implicit MODIFIED)
**ADDED Requirements:**
- If requirement doesn't exist in main spec → add it
- If requirement already exists → update it to match (treat as implicit MODIFIED)
**MODIFIED Requirements:**
- Find the requirement in main spec
- Apply the changes - this can be:
- Adding new scenarios (don't need to copy existing ones)
- Modifying existing scenarios
- Changing the requirement description
- Preserve scenarios/content not mentioned in the delta
**MODIFIED Requirements:**
- Find the requirement in main spec
- Apply the changes - this can be:
- Adding new scenarios (don't need to copy existing ones)
- Modifying existing scenarios
- Changing the requirement description
- Preserve scenarios/content not mentioned in the delta
**REMOVED Requirements:**
- Remove the entire requirement block from main spec
**REMOVED Requirements:**
- Remove the entire requirement block from main spec
**RENAMED Requirements:**
- Find the FROM requirement, rename to TO
**RENAMED Requirements:**
- Find the FROM requirement, rename to TO
d. **Create new main spec** if capability doesn't exist yet:
- Create `openspec/specs/<capability>/spec.md`
- Add Purpose section (can be brief, mark as TBD)
- Add Requirements section with the ADDED requirements
- Create `openspec/specs/<capability>/spec.md`
- Add Purpose section (can be brief, mark as TBD)
- Add Requirements section with the ADDED requirements
4. **Show summary**
@@ -75,16 +75,20 @@ This is an **agent-driven** operation - you will read delta specs and directly e
## ADDED Requirements
### Requirement: New Feature
The system SHALL do something new.
#### Scenario: Basic case
- **WHEN** user does X
- **THEN** system does Y
## MODIFIED Requirements
### Requirement: Existing Feature
#### Scenario: New scenario to add
- **WHEN** user does A
- **THEN** system does B
@@ -101,8 +105,9 @@ The system SHALL do something new.
**Key Principle: Intelligent Merging**
Unlike programmatic merging, you can apply **partial updates**:
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
- The delta represents *intent*, not a wholesale replacement
- The delta represents _intent_, not a wholesale replacement
- Use your judgment to merge changes sensibly
**Output On Success**
@@ -124,6 +129,7 @@ Main specs are now updated. The change remains active - archive when implementat
```
**Guardrails**
- Read both delta and main specs before making changes
- Preserve existing content not mentioned in delta
- If something is unclear, ask for clarification
+4 -1
View File
@@ -19,9 +19,11 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifacts exist for this change
@@ -103,6 +105,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
8. **Generate Verification Report**
**Summary Scorecard**:
```
## Verification Report: <change-name>
@@ -115,7 +118,6 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
```
**Issues by Priority**:
1. **CRITICAL** (Must fix before archive):
- Incomplete tasks
- Missing requirement implementations
@@ -154,6 +156,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
**Output Format**
Use clear markdown with:
- Table for summary scorecard
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
- Code references in format: `file.ts:123`
@@ -0,0 +1,74 @@
# lodash-es-runtime-guards
Replace runtime `typeof` checks and ad-hoc empty-string checks with `lodash-es` predicates.
## When to Use
- You see runtime checks like `typeof value === 'string'`.
- You see object guards like `value && typeof value === 'object'`.
- You see emptiness checks like `value.trim() !== ''` or `value.trim() === ''`.
- You want consistent guard style across app, server, IPC, and tests.
## Do Not Replace
- Type-level `typeof` (TypeScript only), for example:
- `ReturnType<typeof fn>`
- `keyof typeof CONST_MAP`
- `z.infer<typeof Schema>`
These are compile-time types and must remain as-is.
## Guard Mapping
- `typeof x === 'string'` -> `isString(x)`
- `typeof x !== 'string'` -> `!isString(x)`
- `typeof x === 'number'` -> `isNumber(x)`
- `typeof x === 'boolean'` -> `isBoolean(x)`
- `typeof x === 'function'` -> `isFunction(x)`
- `typeof x === 'undefined'` -> `isUndefined(x)`
- `x && typeof x === 'object'` -> `isObjectLike(x)`
- `typeof x === 'object' && x !== null` -> `isObjectLike(x)`
- `typeof x === 'object' && !Array.isArray(x)` -> `isObjectLike(x) && !isArray(x)`
## String Emptiness Pattern
- Prefer:
- `isEmpty(value.trim())`
- Instead of:
- `value.trim() === ''`
- `value.trim() !== ''`
- `value.trim().length === 0`
## Import Style
- Use named imports from `lodash-es`:
```ts
import { isString, isNumber, isObjectLike, isEmpty } from 'lodash-es';
```
- Do not use full-package imports.
- Reuse existing imports in file; avoid duplicate imports.
## Refactor Checklist
1. Replace runtime checks first, preserving behavior.
2. Keep array/object branching semantics unchanged.
3. Re-scan:
```bash
rg -n "typeof\s+[^\n]+(?:===|!==)\s*'" src
```
4. Confirm only type-level `typeof` remains:
```bash
rg -n "\btypeof\b" src
```
5. Run verification:
```bash
npm run type-check
```
+4 -1
View File
@@ -1,5 +1,5 @@
---
name: "OPSX: Apply"
name: 'OPSX: Apply'
description: Implement tasks from an OpenSpec change (Experimental)
category: Workflow
tags: [workflow, artifacts, experimental]
@@ -21,9 +21,11 @@ Implement tasks from an OpenSpec change.
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
2. **Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
@@ -135,6 +137,7 @@ What would you like to do?
```
**Guardrails**
- Keep going through tasks until done or blocked
- Always read context files before starting (from the apply instructions output)
- If task is ambiguous, pause and ask before implementing
+3 -1
View File
@@ -1,5 +1,5 @@
---
name: "OPSX: Archive"
name: 'OPSX: Archive'
description: Archive a completed change in the experimental workflow
category: Workflow
tags: [workflow, archive, experimental]
@@ -64,6 +64,7 @@ Archive a completed change in the experimental workflow.
5. **Perform the archive**
Create the archive directory if it doesn't exist:
```bash
mkdir -p openspec/changes/archive
```
@@ -148,6 +149,7 @@ Target archive directory already exists.
```
**Guardrails**
- Always prompt for change selection if not provided
- Use artifact graph (openspec status --json) for completion checking
- Don't block archive on warnings - just inform and confirm
+32 -26
View File
@@ -1,5 +1,5 @@
---
name: "OPSX: Bulk Archive"
name: 'OPSX: Bulk Archive'
description: Archive multiple completed changes at once
category: Workflow
tags: [workflow, archive, experimental, bulk]
@@ -33,16 +33,16 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
For each selected change, collect:
a. **Artifact status** - Run `openspec status --change "<name>" --json`
- Parse `schemaName` and `artifacts` list
- Note which artifacts are `done` vs other states
- Parse `schemaName` and `artifacts` list
- Note which artifacts are `done` vs other states
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
- If no tasks file exists, note as "No tasks"
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
- If no tasks file exists, note as "No tasks"
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
- List which capability specs exist
- For each, extract requirement names (lines matching `### Requirement: <name>`)
- List which capability specs exist
- For each, extract requirement names (lines matching `### Requirement: <name>`)
4. **Detect spec conflicts**
@@ -62,18 +62,18 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
b. **Search the codebase** for implementation evidence:
- Look for code implementing requirements from each delta spec
- Check for related files, functions, or tests
- Look for code implementing requirements from each delta spec
- Check for related files, functions, or tests
c. **Determine resolution**:
- If only one change is actually implemented -> sync that one's specs
- If both implemented -> apply in chronological order (older first, newer overwrites)
- If neither implemented -> skip spec sync, warn user
- If only one change is actually implemented -> sync that one's specs
- If both implemented -> apply in chronological order (older first, newer overwrites)
- If neither implemented -> skip spec sync, warn user
d. **Record resolution** for each conflict:
- Which change's specs to apply
- In what order (if both)
- Rationale (what was found in codebase)
- Which change's specs to apply
- In what order (if both)
- Rationale (what was found in codebase)
6. **Show consolidated status table**
@@ -89,12 +89,14 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
```
For conflicts, show the resolution:
```
* Conflict resolution:
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
```
For incomplete changes, show warnings:
```
Warnings:
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
@@ -103,7 +105,6 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
7. **Confirm batch operation**
Use **AskUserQuestion tool** with a single confirmation:
- "Archive N changes?" with options based on status
- Options might include:
- "Archive all N changes"
@@ -117,20 +118,21 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
Process changes in the determined order (respecting conflict resolution):
a. **Sync specs** if delta specs exist:
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
- For conflicts, apply in resolved order
- Track if sync was done
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
- For conflicts, apply in resolved order
- Track if sync was done
b. **Perform the archive**:
```bash
mkdir -p openspec/changes/archive
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
```
```bash
mkdir -p openspec/changes/archive
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
```
c. **Track outcome** for each change:
- Success: archived successfully
- Failed: error during archive (record error)
- Skipped: user chose not to archive (if applicable)
- Success: archived successfully
- Failed: error during archive (record error)
- Skipped: user chose not to archive (if applicable)
9. **Display summary**
@@ -153,6 +155,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
```
If any failures:
```
Failed 1 change:
- some-change: Archive directory already exists
@@ -161,6 +164,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
**Conflict Resolution Examples**
Example 1: Only one implemented
```
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
@@ -176,6 +180,7 @@ Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
```
Example 2: Both implemented
```
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
@@ -229,6 +234,7 @@ No active changes found. Use `/opsx:new` to create a new change.
```
**Guardrails**
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
- Always prompt for selection, never auto-select
- Detect spec conflicts early and resolve by checking codebase
+9 -4
View File
@@ -1,5 +1,5 @@
---
name: "OPSX: Continue"
name: 'OPSX: Continue'
description: Continue working on a change - create the next artifact (Experimental)
category: Workflow
tags: [workflow, artifacts, experimental]
@@ -26,9 +26,11 @@ Continue working on a change by creating the next artifact.
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2. **Check current status**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand current state. The response includes:
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
@@ -36,7 +38,7 @@ Continue working on a change by creating the next artifact.
3. **Act based on status**:
---
***
**If all artifacts are complete (`isComplete: true`)**:
- Congratulate the user
@@ -44,7 +46,7 @@ Continue working on a change by creating the next artifact.
- Suggest: "All artifacts created! You can now implement this change with `/opsx:apply` or archive it with `/opsx:archive`."
- STOP
---
***
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
- Pick the FIRST artifact with `status: "ready"` from the status output
@@ -67,7 +69,7 @@ Continue working on a change by creating the next artifact.
- Show what was created and what's now unlocked
- STOP after creating ONE artifact
---
***
**If no artifacts are ready (all blocked)**:
- This shouldn't happen with a valid schema
@@ -81,6 +83,7 @@ Continue working on a change by creating the next artifact.
**Output**
After each invocation, show:
- Which artifact was created
- Schema workflow being used
- Current progress (N/M complete)
@@ -94,6 +97,7 @@ The artifact types and their purpose depend on the schema. Use the `instruction`
Common artifact patterns:
**spec-driven schema** (proposal → specs → design → tasks):
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
- The Capabilities section is critical - each capability listed will need a spec file.
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
@@ -103,6 +107,7 @@ Common artifact patterns:
For other schemas, follow the `instruction` field from the CLI output.
**Guardrails**
- Create ONE artifact per invocation
- Always read dependency artifacts before creating a new one
- Never skip artifacts or create out of order
+17 -9
View File
@@ -1,6 +1,6 @@
---
name: "OPSX: Explore"
description: "Enter explore mode - think through ideas, investigate problems, clarify requirements"
name: 'OPSX: Explore'
description: 'Enter explore mode - think through ideas, investigate problems, clarify requirements'
category: Workflow
tags: [workflow, explore, experimental, thinking]
---
@@ -12,6 +12,7 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
- A vague idea: "real-time collaboration"
- A specific problem: "the auth system is getting unwieldy"
- A change name: "add-dark-mode" (to explore in context of that change)
@@ -36,24 +37,28 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher
Depending on what the user brings, you might:
**Explore the problem space**
- Ask clarifying questions that emerge from what they said
- Challenge assumptions
- Reframe the problem
- Find analogies
**Investigate the codebase**
- Map existing architecture relevant to the discussion
- Find integration points
- Identify patterns already in use
- Surface hidden complexity
**Compare options**
- Brainstorm multiple approaches
- Build comparison tables
- Sketch tradeoffs
- Recommend a path (if asked)
**Visualize**
```
┌─────────────────────────────────────────┐
│ Use ASCII diagrams liberally │
@@ -72,6 +77,7 @@ Depending on what the user brings, you might:
```
**Surface risks and unknowns**
- Identify what could go wrong
- Find gaps in understanding
- Suggest spikes or investigations
@@ -85,11 +91,13 @@ You have full context of the OpenSpec system. Use it naturally, don't force it.
### Check for context
At the start, quickly check what exists:
```bash
openspec list --json
```
This tells you:
- If there are active changes
- Their names, schemas, and status
- What the user might be working on
@@ -120,14 +128,14 @@ If the user mentions a change or you detect one is relevant:
3. **Offer to capture when decisions are made**
| Insight Type | Where to Capture |
|--------------|------------------|
| Insight Type | Where to Capture |
| -------------------------- | ---------------------------- |
| New requirement discovered | `specs/<capability>/spec.md` |
| Requirement changed | `specs/<capability>/spec.md` |
| Design decision made | `design.md` |
| Scope changed | `proposal.md` |
| New work identified | `tasks.md` |
| Assumption invalidated | Relevant artifact |
| Requirement changed | `specs/<capability>/spec.md` |
| Design decision made | `design.md` |
| Scope changed | `proposal.md` |
| New work identified | `tasks.md` |
| Assumption invalidated | Relevant artifact |
Example offers:
- "That's a design decision. Capture it in design.md?"
+28 -21
View File
@@ -1,5 +1,5 @@
---
name: "OPSX: Fast Forward"
name: 'OPSX: Fast Forward'
description: Create a change and generate all artifacts needed for implementation in one go
category: Workflow
tags: [workflow, artifacts, experimental]
@@ -14,6 +14,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
1. **If no input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
@@ -21,15 +22,19 @@ Fast-forward through artifact creation - generate everything needed to start imp
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2. **Create the change directory**
```bash
openspec new change "<name>"
```
This creates a scaffolded change at `openspec/changes/<name>/`.
3. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
@@ -41,30 +46,30 @@ Fast-forward through artifact creation - generate everything needed to start imp
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "✓ Created <artifact-id>"
- Get instructions:
```bash
openspec instructions <artifact-id> --change "<name>" --json
```
- The instructions JSON includes:
- `context`: Project background (constraints for you - do NOT include in output)
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
- `template`: The structure to use for your output file
- `instruction`: Schema-specific guidance for this artifact type
- `outputPath`: Where to write the artifact
- `dependencies`: Completed artifacts to read for context
- Read any completed dependency files for context
- Create the artifact file using `template` as the structure
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
- Show brief progress: "✓ Created <artifact-id>"
b. **Continue until all `applyRequires` artifacts are complete**
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
- After creating each artifact, re-run `openspec status --change "<name>" --json`
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
- Stop when all `applyRequires` artifacts are done
c. **If an artifact requires user input** (unclear context):
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
- Use **AskUserQuestion tool** to clarify
- Then continue with creation
5. **Show final status**
```bash
@@ -74,6 +79,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
**Output**
After completing all artifacts, summarize:
- Change name and location
- List of artifacts created with brief descriptions
- What's ready: "All artifacts created! Ready for implementation."
@@ -87,6 +93,7 @@ After completing all artifacts, summarize:
- Use the `template` as a starting point, filling in based on context
**Guardrails**
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
- Always read dependency artifacts before creating a new one
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
+10 -1
View File
@@ -1,5 +1,5 @@
---
name: "OPSX: New"
name: 'OPSX: New'
description: Start a new change using the experimental artifact workflow (OPSX)
category: Workflow
tags: [workflow, artifacts, experimental]
@@ -14,6 +14,7 @@ Start a new change using the experimental artifact-driven approach.
1. **If no input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
@@ -31,23 +32,29 @@ Start a new change using the experimental artifact-driven approach.
**Otherwise**: Omit `--schema` to use the default.
3. **Create the change directory**
```bash
openspec new change "<name>"
```
Add `--schema <name>` only if the user requested a specific workflow.
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
4. **Show the artifact status**
```bash
openspec status --change "<name>"
```
This shows which artifacts need to be created and which are ready (dependencies satisfied).
5. **Get instructions for the first artifact**
The first artifact depends on the schema. Check the status output to find the first artifact with status "ready".
```bash
openspec instructions <first-artifact-id> --change "<name>"
```
This outputs the template and context for creating the first artifact.
6. **STOP and wait for user direction**
@@ -55,6 +62,7 @@ Start a new change using the experimental artifact-driven approach.
**Output**
After completing the steps, summarize:
- Change name and location
- Schema/workflow being used and its artifact sequence
- Current status (0/N artifacts complete)
@@ -62,6 +70,7 @@ After completing the steps, summarize:
- Prompt: "Ready to create the first artifact? Run `/opsx:continue` or just describe what this change is about and I'll draft it."
**Guardrails**
- Do NOT create any artifacts yet - just show the instructions
- Do NOT advance beyond showing the first artifact template
- If the name is invalid (not kebab-case), ask for a valid name
+25 -5
View File
@@ -1,5 +1,5 @@
---
name: "OPSX: Onboard"
name: 'OPSX: Onboard'
description: Guided onboarding - walk through a complete OpenSpec workflow cycle with narration
category: Workflow
tags: [workflow, onboarding, tutorial, learning]
@@ -18,6 +18,7 @@ openspec status --json 2>&1 || echo "NOT_INITIALIZED"
```
**If not initialized:**
> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`.
Stop here if not initialized.
@@ -62,6 +63,7 @@ Scan the codebase for small improvement opportunities. Look for:
6. **Missing validation** - User input handlers without validation
Also check recent git activity:
```bash
git log --oneline -10 2>/dev/null || echo "No git history"
```
@@ -97,6 +99,7 @@ Which task interests you? (Pick a number or describe your own)
```
**If nothing found:** Fall back to asking what the user wants to build:
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
### Scope Guardrail
@@ -129,6 +132,7 @@ Before we create a change, let me quickly show you **explore mode**—it's how y
```
Spend 1-2 minutes investigating the relevant code:
- Read the file(s) involved
- Draw a quick ASCII diagram if it helps
- Note any considerations
@@ -154,6 +158,7 @@ Now let's create a change to hold our work.
## Phase 4: Create the Change
**EXPLAIN:**
```
## Creating a Change
@@ -163,21 +168,25 @@ Let me create one for our task.
```
**DO:** Create the change with a derived kebab-case name:
```bash
openspec new change "<derived-name>"
```
**SHOW:**
```
Created: `openspec/changes/<name>/`
The folder structure:
```
openspec/changes/<name>/
├── proposal.md ← Why we're doing this (empty, we'll fill it)
├── design.md ← How we'll build it (empty)
├── specs/ ← Detailed requirements (empty)
└── tasks.md ← Implementation checklist (empty)
├── proposal.md ← Why we're doing this (empty, we'll fill it)
├── design.md ← How we'll build it (empty)
├── specs/ ← Detailed requirements (empty)
└── tasks.md ← Implementation checklist (empty)
```
Now let's fill in the first artifact—the proposal.
@@ -188,6 +197,7 @@ Now let's fill in the first artifact—the proposal.
## Phase 5: Proposal
**EXPLAIN:**
```
## The Proposal
@@ -232,9 +242,11 @@ Does this capture the intent? I can adjust before we save it.
**PAUSE** - Wait for user approval/feedback.
After approval, save the proposal:
```bash
openspec instructions proposal --change "<name>" --json
```
Then write the content to `openspec/changes/<name>/proposal.md`.
```
@@ -248,6 +260,7 @@ Next up: specs.
## Phase 6: Specs
**EXPLAIN:**
```
## Specs
@@ -257,6 +270,7 @@ For a small task like this, we might only need one spec file.
```
**DO:** Create the spec file:
```bash
mkdir -p openspec/changes/<name>/specs/<capability-name>
```
@@ -292,6 +306,7 @@ Save to `openspec/changes/<name>/specs/<capability>/spec.md`.
## Phase 7: Design
**EXPLAIN:**
```
## Design
@@ -337,6 +352,7 @@ Save to `openspec/changes/<name>/design.md`.
## Phase 8: Tasks
**EXPLAIN:**
```
## Tasks
@@ -375,6 +391,7 @@ Save to `openspec/changes/<name>/tasks.md`.
## Phase 9: Apply (Implementation)
**EXPLAIN:**
```
## Implementation
@@ -409,6 +426,7 @@ The change is implemented! One more step—let's archive it.
## Phase 10: Archive
**EXPLAIN:**
```
## Archiving
@@ -418,11 +436,13 @@ Archived changes become your project's decision history—you can always find th
```
**DO:**
```bash
openspec archive "<name>"
```
**SHOW:**
```
Archived to: `openspec/changes/archive/YYYY-MM-DD-<name>/`
+25 -19
View File
@@ -1,5 +1,5 @@
---
name: "OPSX: Sync"
name: 'OPSX: Sync'
description: Sync delta specs from a change to main specs
category: Workflow
tags: [workflow, specs, experimental]
@@ -43,28 +43,28 @@ This is an **agent-driven** operation - you will read delta specs and directly e
c. **Apply changes intelligently**:
**ADDED Requirements:**
- If requirement doesn't exist in main spec → add it
- If requirement already exists → update it to match (treat as implicit MODIFIED)
**ADDED Requirements:**
- If requirement doesn't exist in main spec → add it
- If requirement already exists → update it to match (treat as implicit MODIFIED)
**MODIFIED Requirements:**
- Find the requirement in main spec
- Apply the changes - this can be:
- Adding new scenarios (don't need to copy existing ones)
- Modifying existing scenarios
- Changing the requirement description
- Preserve scenarios/content not mentioned in the delta
**MODIFIED Requirements:**
- Find the requirement in main spec
- Apply the changes - this can be:
- Adding new scenarios (don't need to copy existing ones)
- Modifying existing scenarios
- Changing the requirement description
- Preserve scenarios/content not mentioned in the delta
**REMOVED Requirements:**
- Remove the entire requirement block from main spec
**REMOVED Requirements:**
- Remove the entire requirement block from main spec
**RENAMED Requirements:**
- Find the FROM requirement, rename to TO
**RENAMED Requirements:**
- Find the FROM requirement, rename to TO
d. **Create new main spec** if capability doesn't exist yet:
- Create `openspec/specs/<capability>/spec.md`
- Add Purpose section (can be brief, mark as TBD)
- Add Requirements section with the ADDED requirements
- Create `openspec/specs/<capability>/spec.md`
- Add Purpose section (can be brief, mark as TBD)
- Add Requirements section with the ADDED requirements
4. **Show summary**
@@ -78,16 +78,20 @@ This is an **agent-driven** operation - you will read delta specs and directly e
## ADDED Requirements
### Requirement: New Feature
The system SHALL do something new.
#### Scenario: Basic case
- **WHEN** user does X
- **THEN** system does Y
## MODIFIED Requirements
### Requirement: Existing Feature
#### Scenario: New scenario to add
- **WHEN** user does A
- **THEN** system does B
@@ -104,8 +108,9 @@ The system SHALL do something new.
**Key Principle: Intelligent Merging**
Unlike programmatic merging, you can apply **partial updates**:
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
- The delta represents *intent*, not a wholesale replacement
- The delta represents _intent_, not a wholesale replacement
- Use your judgment to merge changes sensibly
**Output On Success**
@@ -127,6 +132,7 @@ Main specs are now updated. The change remains active - archive when implementat
```
**Guardrails**
- Read both delta and main specs before making changes
- Preserve existing content not mentioned in delta
- If something is unclear, ask for clarification
Loaded 100 of 313 files, more files were not shown because too many files have changed in this diff. Show more