Files
proxy-manager/web/DetailPanel.tsx
T
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

76 lines
2.9 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import { useSearchParams } from 'react-router';
import { useClient } from 'hooks/useClient';
import { DEFAULT_SECTION, isSectionKey, type SectionKey } from './sections';
import { HostsPanel } from './HostsPanel';
import { CertificatesPanel } from './CertificatesPanel';
import { ConnectionPanel } from './ConnectionPanel';
import { ListSection, REDIRECTIONS, STREAMS, DEAD_HOSTS, ACCESS_LISTS } from './ListSection';
import { useNpmChanged } from './channel';
/**
* The right column: whichever section the nav has selected.
*
* It reads `?section=` from the URL rather than being handed it — the two panels are siblings in a
* workspace and neither owns the other, and `docs/navigation-audit.md` puts addressable state in the URL
* for exactly this reason. A bogus or missing value falls back rather than rendering nothing.
*
* ── The unconfigured check lives HERE, once ──
*
* Every section needs a connection, so every section would otherwise have to handle its absence, and each
* one would say something slightly different. Asking `/status` once and rendering the connection form
* instead of any section is the same answer for all seven, and it means "no connection yet" reads as a
* setup step rather than as six broken lists.
*/
type Status = { configured: boolean };
export const DetailPanel = () => {
const [params] = useSearchParams();
const raw = params.get('section');
const section: SectionKey = isSectionKey(raw) ? raw : DEFAULT_SECTION;
const client = useClient();
const clientRef = useRef(client);
clientRef.current = client;
const [configured, setConfigured] = useState<boolean | null>(null);
const [connectionSignal] = useNpmChanged();
const check = useCallback(async () => {
try {
const status = await clientRef.current.get<Status>('/proxy-manager/status');
setConfigured(status.configured);
} catch {
// Cannot tell. Render the section rather than the setup form — a transient failure must not look
// like "you have not set this up", which is a much more alarming thing to be told.
setConfigured(true);
}
}, []);
useEffect(() => {
void check();
}, [check, connectionSignal]);
if (configured === null) {
return <div className="flex size-full items-center justify-center p-6 text-sm text-muted-foreground">Loading…</div>;
}
if (!configured || section === 'connection') return <ConnectionPanel />;
switch (section) {
case 'proxy-hosts':
return <HostsPanel />;
case 'certificates':
return <CertificatesPanel />;
case 'redirections':
return <ListSection {...REDIRECTIONS} />;
case 'streams':
return <ListSection {...STREAMS} />;
case 'dead-hosts':
return <ListSection {...DEAD_HOSTS} />;
case 'access-lists':
return <ListSection {...ACCESS_LISTS} />;
}
};