Files
pastilhasandClaude Opus 5 66383aa9a4 the lists re-read when the agent changes something
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
2026-08-25 19:03:35 +00:00

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;
};