import { useCallback, useRef } from 'react'; import { defineChannel } from 'hooks/usePanelChannel'; /** * "Something at NPM changed — anyone reading it should read it again." * * Two publishers, and naming it after the second one would have been wrong the other way round. The * connection being saved or removed is one. **A chat turn that ran tools is the other**, and it is the * bigger one: an agent that deletes a proxy host leaves every list on the screen describing a world that * no longer exists, and nothing else would ever tell them. * * ── Why a channel and not the URL ── * * `docs/navigation-audit.md` draws the line: anything ADDRESSABLE goes in the URL, and a channel is only * for a signal — "something changed, re-read it". This is the second kind. Nobody wants to link to "the * moment after the connection was saved", and there is nothing to select. * * ── Why it is needed at all ── * * The panels are siblings in a workspace layout. None of them can call into another and none should try — * they are mounted independently and no one owns the rest. Before this, saving a connection left every * panel showing whatever it had failed to load a minute earlier, and the only fix a person had was a * manual refresh. Reported on 2026-08-25 the first time a connection was saved through the form; the chat * publisher was added the same day, after an agent deleted two hosts that stayed on screen. * * ── The nonce is a plain counter, and that is not an accident ── * * Copied deliberately from `officerdev/src/channels.ts`, which records why. `setSignal(n => n + 1)` is * WRONG here: the functional setter applies against the value captured at render, so two bumps inside one * render window both compute `snapshot + 1` and the second silently overwrites the first — nobody * re-reads. `Date.now()` has the same flaw in miniature, two bumps in one millisecond being one number. * A module counter never reads React state, so it cannot be wrong however often it is called. */ const useNpmChangedChannel = defineChannel('proxy-manager:npm-changed', 0); let nonce = 0; export const useNpmChanged = () => { const [signal, setSignal] = useNpmChangedChannel(); // The setter is a fresh closure every render and this `bump` goes into dependency lists, so it is read // through a ref — stable identity without freezing the first render's setter. const setRef = useRef(setSignal); setRef.current = setSignal; const bump = useCallback(() => setRef.current(++nonce), []); return [signal, bump] as const; };