Skip to content

Commit aaca750

Browse files
v0.7.12: mcp servers ui/ux fixes, nuqs for query param management
2 parents 8df34a3 + d8da1e2 commit aaca750

94 files changed

Lines changed: 19623 additions & 1106 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/commands/cleanup.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
description: Run all code quality skills in sequence — effects, memo, callbacks, state, React Query, and emcn design review
2+
description: Run all code quality skills in sequence — effects, memo, callbacks, state, React Query, emcn design review, and url-state
33
argument-hint: [scope] [fix=true|false]
44
---
55

@@ -21,5 +21,6 @@ Run each of these skills in order on the specified scope, passing through the sc
2121
4. `/you-might-not-need-state $ARGUMENTS`
2222
5. `/react-query-best-practices $ARGUMENTS`
2323
6. `/emcn-design-review $ARGUMENTS`
24+
7. `/you-might-not-need-url-state $ARGUMENTS`
2425

25-
After all skills have run, output a summary of what was found and fixed (or proposed) across all six passes.
26+
After all skills have run, output a summary of what was found and fixed (or proposed) across all seven passes.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
description: Analyze and fix URL/query-param state anti-patterns — manual useSearchParams reads, hand-built query mutations, view-state trapped in useState, and objects in the URL
3+
argument-hint: [scope] [fix=true|false]
4+
---
5+
6+
# You Might Not Need URL State
7+
8+
Arguments:
9+
- scope: what to analyze (default: your current changes). Examples: "diff to main", "PR #123", "app/workspace/[workspaceId]/tables/", "whole codebase"
10+
- fix: whether to apply fixes (default: true). Set to false to only propose changes.
11+
12+
User arguments: $ARGUMENTS
13+
14+
## Context
15+
16+
Shareable client view-state (active tab/panel, filters, search query, sort, pagination, selected-entity id, an open "view" modal/drawer that is a destination) lives in the URL via [`nuqs`](https://nuqs.dev) — driven by a co-located `search-params.ts`, never read via `useSearchParams().get(...)` and never mutated by hand-built query strings. Remote data stays in React Query; high-frequency / large / ephemeral / socket-synced state stays in Zustand; purely local UI stays in `useState`.
17+
18+
`.claude/rules/sim-url-state.md` is the source of truth — read it first.
19+
20+
## References
21+
22+
Read these before analyzing:
23+
1. `.claude/rules/sim-url-state.md` — the decision framework, conventions, debounced-input pattern, sort convention, selected-entity deep-link pattern, and the workflow-editor carve-out
24+
2. https://nuqs.dev/docs/parsers — parsers (`parseAsString`/`parseAsInteger`/`parseAsBoolean`/`parseAsStringLiteral`/`parseAsArrayOf`/`createParser`)
25+
3. https://nuqs.dev/docs/options`withDefault`, `history`, `shallow`, `clearOnDefault`
26+
4. https://nuqs.dev/docs/server-side`createSearchParamsCache` for server reads
27+
28+
## Anti-patterns to detect
29+
30+
1. **Manual param reads for state**: `useSearchParams().get(...)` or `new URLSearchParams(window.location.search)` used to *read* view-state. Replace with `useQueryState`/`useQueryStates` bound to a `search-params.ts`. (Read-once auth/invite/redirect tokens — `token`, `callbackUrl`, `redirect`, `error`, `invite_flow`, `code` — are NOT view-state; leave them on `useSearchParams`.)
31+
2. **Hand-built query mutation**: constructing a query string + `router.replace`/`router.push` to change a param on the current path. Use a nuqs setter. (A `router.push` that changes the route *path* is fine; an outbound `new URLSearchParams` building an `href`/`window.open`/download/API URL is fine.)
32+
3. **`window.history.replaceState`/`pushState`** to mutate a param.
33+
4. **URL state duplicated into a store/useState + synced with an effect** (or a `popstate` listener). The URL is the single source of truth; derive from it, don't mirror it.
34+
5. **Objects in the URL**: serializing a `TableDefinition`/`SkillDefinition`/etc. Store the id and derive the object from the loaded list (`items.find(i => i.id === id)`).
35+
6. **High-frequency / large state in the URL**: cursor, pan/zoom, un-debounced keystrokes, big JSON blobs. Debounce text search (local `useState` mirror + reconcile effect); keep canvas/presence/resize state in Zustand.
36+
7. **Shareable view-state trapped in `useState`**: a tab/filter/sort/pagination/selected-entity that should be a link but lives in local state. Migrate it to the URL.
37+
8. **Missing Suspense boundary**: a component newly calling `useQueryState`/`useQueryStates` whose page entry has no `<Suspense>` wrapper (Next.js requires it for `useSearchParams`). Add one with a real-chrome fallback.
38+
9. **`import { z }` for param validation in client code**: use nuqs parsers instead.
39+
40+
## Steps
41+
42+
1. Read `.claude/rules/sim-url-state.md` and the nuqs docs above to understand the guidelines
43+
2. Analyze the specified scope for the anti-patterns listed above
44+
3. For each finding, decide the correct home using the decision table — do not force URL state onto ephemeral/high-frequency/socket-synced state
45+
4. If fix=true, apply the fixes (co-locate a `search-params.ts`, wire `useQueryState(s)`, add the Suspense boundary, delete the replaced state + sync effects). If fix=false, propose the fixes without applying.

.claude/rules/sim-hooks.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,19 @@ export function useFeature({ id, onSelect }: UseFeatureProps) {
4848
4. Wrap returned functions in useCallback
4949
5. Server data goes through React Query (`hooks/queries/`), never `useState` + `fetch`
5050
6. Keep only UI/orchestration state in these hooks
51+
52+
## State shape
53+
54+
Never mirror a prop into state with `useState(prop)` + a syncing `useEffect` — a prop change clobbers in-progress local edits. Use the prop directly, reset via a remount `key`, or — when you must seed local state from a prop only on a transition (e.g. a modal opening) — reset during render with the `prevX` ref idiom:
55+
56+
```typescript
57+
const prevOpenRef = useRef(open)
58+
if (prevOpenRef.current !== open) {
59+
prevOpenRef.current = open
60+
if (open) setName(initialName) // closed → open only
61+
}
62+
```
63+
64+
Model mutually-exclusive flags as ONE `status` enum, not several contradictory booleans. `isLoading`/`isVerified`/`isInvalidOtp` describing one machine collapse to `status: 'idle' | 'verifying' | 'verified' | 'error'` (+ `errorMessage`); derive any boolean a consumer still needs (`status === 'error'`).
65+
66+
Derive busy/success from the mutation object — never duplicate `mutation.isPending`/`mutation.isSuccess` into local `useState`. Read them directly (`mutation.isSuccess`) and reset with `mutation.reset()`. A distinct phase the mutation doesn't cover — e.g. a pre-submit captcha/Turnstile gate that runs before `mutate()` — is not a duplicate; keep that flag.

.claude/rules/sim-queries.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ paths:
77

88
All React Query hooks live in `hooks/queries/`. All server state must go through React Query — never use `useState` + `fetch` in components for data fetching or mutations.
99

10+
For *client* view-state that belongs in a shareable link (tabs, filters, search, pagination, selected entity id), use URL query params via nuqs — see `.claude/rules/sim-url-state.md`. React Query owns remote data; nuqs owns shareable client view-state.
11+
1012
## Query Key Factory
1113

1214
Every query file defines a hierarchical keys factory with an `all` root key and intermediate plural keys for prefix-level invalidation:
@@ -23,6 +25,8 @@ export const entityKeys = {
2325

2426
Never use inline query keys — always use the factory.
2527

28+
**Every identifier the `queryFn` forwards into the fetch MUST appear in the `queryKey`.** (Query-machinery identifiers — `signal`, `pageParam` — are exempt; they aren't fetch-scoping args.) If the fetch is scoped by `workspaceId`, `cursor`, `limit`, an org id, etc., those values must be part of the key — otherwise distinct fetch args share one cache entry (a cross-tenant / per-param cache collision). The lone exception is a globally-unique id used as the key while a second fetch arg is only an authz scope that cannot collide; annotate those with `// rq-lint-allow: <reason>`. Enforced by the `key-fetch-arg-drift` check in `scripts/check-react-query-patterns.ts`.
29+
2630
## File Structure
2731

2832
```typescript
@@ -140,4 +144,4 @@ const handler = useCallback(() => {
140144

141145
## Enforcement
142146

143-
`scripts/check-react-query-patterns.ts` (`bun run check:react-query`, run in CI) statically enforces these conventions: every `useQuery`/`useInfiniteQuery`/`useSuspenseQuery` declares an explicit `staleTime`, inline `queryFn`s destructure `signal`, `queryKey`s reference a colocated factory rather than an inline literal, and every `*Keys` factory in `hooks/queries/**` exposes an `all` root key. `hooks/queries/**` is a zero-tolerance zone; the rest of `apps/sim/**` is ratcheted against `scripts/check-react-query-patterns.baseline.json`. For a genuine exception, put `// rq-lint-allow: <reason>` on the line directly above the flagged construct.
147+
`scripts/check-react-query-patterns.ts` (`bun run check:react-query`, run in CI) statically enforces these conventions: every `useQuery`/`useInfiniteQuery`/`useSuspenseQuery` declares an explicit `staleTime`, inline `queryFn`s destructure `signal`, `queryKey`s reference a colocated factory rather than an inline literal, every `*Keys` factory in `hooks/queries/**` exposes an `all` root key, and every identifier the `queryFn` forwards into the fetch also appears in the `queryKey` (`key-fetch-arg-drift`). `hooks/queries/**` is a zero-tolerance zone; the rest of `apps/sim/**` is ratcheted against `scripts/check-react-query-patterns.baseline.json`. For a genuine exception, put `// rq-lint-allow: <reason>` on the line directly above the flagged construct.

.claude/rules/sim-stores.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const useFeatureStore = create<FeatureState>()(
5757

5858
1. Use `devtools` middleware (named stores)
5959
2. Use `persist` only when data should survive reload
60-
3. `partialize` to persist only necessary state
60+
3. `persist` MUST use `partialize` with an explicit whitelist of the durable fields. Exclude transient flags (`isResizing`, drag/hover state) and `_hasHydrated` from the whitelist, and never spread the whole state (`{ ...state }`) — it leaks actions and transient state into storage
6161
4. `_hasHydrated` pattern for persisted stores needing hydration tracking
6262
5. Immutable updates only
6363
6. `set((state) => ...)` when depending on previous state

0 commit comments

Comments
 (0)