Merge branch 'v2/main' into v2/chore/2163-agent-skills
Three conflicts, all in the files this PR refactored. The code from #1955 and #2161 (core/react's useSyncExternalStore conversion) merged cleanly. - `.github/copilot-instructions.md` — modify/delete. This PR deletes it, and the four lines v2/main added there are a distillation of AGENTS.md rules that arrive in AGENTS.md anyway. Kept deleted. - `AGENTS.md` — the incoming change expanded the `core/react/` entry in the project tree by eleven lines; this PR compacted that tree to one line per entry on purpose. Took the compact tree and carried the substance onto its one line. The other incoming hunk — the store-subscription rules under React instructions — merged cleanly, which is the split working: it is a RULE, so it lands in the section that stayed. - `README.md` — same shape, one tree line. This README no longer enumerates `core/`'s subdirectories at all, so the substance went where that content now lives: `docs/architecture.md` and the `project-structure` skill's core table. Nothing from v2/main was dropped; every incoming statement is now in exactly one place rather than the three it used to occupy. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
commit
67abdf96b4
1 changed files with 1126 additions and 803 deletions
11
AGENTS.md
11
AGENTS.md
|
|
@ -49,7 +49,7 @@ inspector/
|
|||
│ ├── logging/ Silent pino logger singleton
|
||||
│ ├── mcp/ InspectorClient, transports, state stores, config import
|
||||
│ ├── node/ Node-only helpers (version reader, host normalization)
|
||||
│ ├── react/ React hooks over the state stores
|
||||
│ ├── react/ React hooks over the state stores (read during render — see React instructions)
|
||||
│ └── storage/ File I/O helpers for the OAuth persist backends
|
||||
├── test-servers/ Composable MCP test servers + JSON configs
|
||||
├── scripts/ Root build/verify tooling: install cascade, smokes, verify:* guards
|
||||
|
|
@ -383,6 +383,15 @@ Nothing **enforces** the boundary: no path alias keys off it, and the coverage `
|
|||
- Use **`useValueChange(value, onChange)`** (`clients/web/src/hooks/useValueChange.ts`) instead. It is React's documented ["adjusting state during render"](https://react.dev/reference/react/useState#storing-information-from-previous-renders) pattern: it compares `value` against the previous render's with `Object.is` and calls `onChange(next)` during render, so React discards the in-progress output and re-runs the component before anything reaches the DOM. It does **not** fire on the first render — seed the dependent state with `useState` instead. Because the comparison is `Object.is`, the value you pass **must be referentially stable** across renders that mean "no change": prefer a primitive key derived from the data (an id, a name, a URI), and otherwise a memoized value. A fresh object/array literal would compare unequal every render and loop.
|
||||
- The `onChange` you pass runs **during render**, so it must be pure — `setState` calls and nothing else. No fetches, DOM writes, logging, ref mutation, or parent callbacks: a render can be replayed (StrictMode) or abandoned (concurrent React), so external work would run an unpredictable number of times.
|
||||
- An effect is still the right tool for genuine synchronization with an external system (DOM measurement, `requestAnimationFrame`, subscriptions, timers). The rule is about deriving React state from React props, not about effects in general. `NetworkEntry` shows the split: the reveal's force-open is a state update and uses `useValueChange`, while its `requestAnimationFrame` scroll stays a `useEffect`.
|
||||
- **Subscribing to an `@inspector/core` state store is `useSyncExternalStore`, never `useState` + a subscribing `useEffect`.** That second shape looks like the legitimate "synchronize with an external system" case above and is not, because it also *seeds and re-seeds local state from the store prop* — so it carries the same stale frame (switching servers paints the previous server's tools for one frame) plus a window where an event dispatched between the render and the effect is lost outright. Every hook in `core/react/` was converted away from it in #1955.
|
||||
- Reach for **`useStoreSnapshot(store, event, read, whenAbsent)`** (`core/react/useStoreSnapshot.ts`) when the getter returns a **fresh value per read** — a defensive copy (`getTools()` is `[...this.items]`) or a freshly built object (`getPagination()`). That is nearly all of them, and the caching it adds is what keeps `useSyncExternalStore` from looping. Call it once per value.
|
||||
- When a getter's value is **already referentially stable**, subscribe with `useSyncExternalStore` directly and skip the helper — `useListError` does, because its snapshot is the stored `Error` instance itself (or `null`). The rule is read-during-render, not "always use the helper".
|
||||
- Either way `useValueChange` is *not* the tool here — it lives in `clients/web/src`, and `core/react/` is consumed by the CLI and TUI too.
|
||||
- `read` and `whenAbsent` must be **referentially stable across renders** — they are part of the snapshot's cache key. `read` is a function, so declare it at module scope. `whenAbsent` only needs a module-scope constant when it is an **object or array** (`NO_TOOLS`, `NO_PAGINATION`); a primitive fallback (`false`, `undefined`, `"disconnected"`) is already stable under `Object.is` and is passed inline throughout these hooks.
|
||||
- An unstable one **fails quietly, so don't expect to be told**: measured on React 19, an inline `read` throws nothing, logs nothing — not even React's "getSnapshot should be cached" dev warning, whose double-call happens within a single render where the closure is unchanged — and forces no extra render. It simply returns a fresh value every render, defeating every downstream `useMemo` / `React.memo` / effect dep that keys on it.
|
||||
- The snapshot is cached against the store's **per-event dispatch counter** (`TypedEventTarget.getEventRevision`), which every dispatch advances automatically — not against the snapshot's contents. That is deliberate and load-bearing: these getters return a defensive copy, so contents are the only alternative, and a contents comparison cannot see a dispatch that mutated an entry the list already holds (`MessageLogState` folding a response into its request entry does exactly that). Don't "optimize" it into a shallow compare.
|
||||
- Consequently the **store is the source of truth and the event is only the signal** — the hook re-reads the store rather than taking the event's `detail`. A test that fires a store event must put the value on the store first; dispatching alone announces a change that isn't there. A hand-rolled fake must extend the real `TypedEventTarget` (a bare `EventTarget` has no `getEventRevision` and fails at runtime).
|
||||
- Genuinely local state accumulated from an event stream, with no getter to read it back from, stays `useState` — `useInspectorClient`'s `lastError` is the one such case. Reset it on a store swap **during render**, not in an effect.
|
||||
- Theme files vs. Storybook element components
|
||||
- **Theme files** (`src/theme/<Component>.ts`) and **element components** (`src/components/elements/`) serve different purposes and both are needed.
|
||||
- Theme files customize every instance of a Mantine component app-wide — defaults (size, radius), custom variants, and global style overrides. They are applied automatically by `MantineProvider`.
|
||||
|
|
|
|||
Loading…
Reference in a new issue