an agent deleted two proxy hosts and both stayed on screen. the person had the answer in the transcript and a stale list six inches above it — which is the one thing a chat beside a list has to get right, and the reason it is beside the list at all. so the chat panel bumps the refresh channel when a turn ran tools, and every section re-reads. gated on hadToolCalls: a turn that only talked changed nothing, and re-reading after every sentence would put six requests behind every reply. the channel is renamed with it. it was proxy-manager:connection-changed, named after its only publisher; it has two now and the second is the bigger one, so it says what it MEANS — something at NPM changed, read it again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
49 lines
2.5 KiB
TypeScript
49 lines
2.5 KiB
TypeScript
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<number>('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;
|
|
};
|