live sessions default to agents only, and you ask by name for a commander
`?kind=` was opt-in, so a client that said nothing got everything — which is the wrong default for the one caller that cannot cope. The phone swipes between live chats one at a time, has no room for tabs and no way to skip a row, so a commander in that list is a commander you are stuck on. Defaulting to `agent` means forgetting the parameter can only ever under-report. The two callers that want both now say `kind=all` explicitly, and that is a fair price: they are few, they are on screens built to show both kinds, and they are the ones positioned to notice when the answer is wrong. `counts` still describes every live session whatever the filter, so a filtered caller renders both tab numbers from one request rather than two. Also corrects useLiveSessions' comment, which said `isCommander` was derived from the cwd. It stopped being true when a commander gained the right to choose its own working directory — it comes from the `command_hq_sessions` row, and the directory answers nothing. Unrelated but caught by docs:check on the way past: the registry gained a nineteenth entry (minesweeper, execution) and CLAUDE.md still said eighteen, which also left the generated platform skill two lines stale. Both corrected; the check is green again. The minesweeper feature itself is half-landed and not mine — `rfb.ts` fails typecheck and its registry test fails on a websocket that is not served. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -369,31 +369,37 @@ chatRouter.get('/live', async (ctx) => {
|
||||
const rows = [...sessions, ...openCodeSessions];
|
||||
|
||||
/*
|
||||
`?kind=agent|commander` — the split, done here rather than by each client.
|
||||
`?kind=` — the split, done here rather than by each client.
|
||||
|
||||
The classification was already server-side; what was not was the FILTER, so every client repeated it.
|
||||
That is fine for the web list, which has both tabs on screen, and wrong for the iPhone app, which swipes
|
||||
between live chats, has no room for tabs, and must simply never be handed a commander.
|
||||
That is fine for the web list, which has both tabs on screen, and wrong for a phone that swipes between
|
||||
live chats one at a time and must simply never be handed a commander.
|
||||
|
||||
Counts always describe the WHOLE set, filtered or not. The web tabs show `Agents (n) / Commanders (n)`
|
||||
side by side and both numbers have to be right whichever tab is open — a filter that also hid the other
|
||||
count would force a second request to render one screen, which is how a server-side filter usually ends
|
||||
up costing more than the client-side one it replaced.
|
||||
**The default is `agent`, not "everything".** A caller that says nothing gets ordinary chats, so the way
|
||||
to receive a commander is to ask for one by name. That is the whole point: forgetting the parameter can
|
||||
only ever under-report, never put a deploy commander into a swipe order that has no way to skip it. It
|
||||
costs the two callers that DO want both an explicit `kind=all`, which is the right trade — they are few,
|
||||
they are on screens built for it, and they are the ones in a position to notice if it is wrong.
|
||||
|
||||
An unrecognised `kind` is a 400, never "everything". The plural forms are accepted because the client's
|
||||
own type is already `'agents' | 'commanders'`, and the failure that silence would buy is precise: a
|
||||
mistyped `?kind=agents` returning commanders is exactly the swipe-order bug this exists to prevent.
|
||||
Counts always describe the WHOLE set, whatever the filter. The web tabs show `Agents (n) /
|
||||
Commanders (n)` side by side and both numbers have to be right whichever tab is open — a filter that
|
||||
also hid the other count would force a second request to render one screen, which is how a server-side
|
||||
filter ends up costing more than the client-side one it replaced.
|
||||
|
||||
An unrecognised `kind` is a 400, never a silent fallback. Plural forms are accepted because the client's
|
||||
own type is already `'agents' | 'commanders'`, and the failure silence would buy is precise: a mistyped
|
||||
`?kind=agents` resolving to something other than agents is the exact bug this exists to prevent.
|
||||
*/
|
||||
const kind = ctx.req.query('kind');
|
||||
const kind = ctx.req.query('kind') ?? 'agent';
|
||||
const counts = {
|
||||
agents: rows.filter((s) => !s.isCommander).length,
|
||||
commanders: rows.filter((s) => s.isCommander).length,
|
||||
};
|
||||
if (kind !== undefined && !['agent', 'agents', 'commander', 'commanders'].includes(kind)) {
|
||||
return ctx.json({ error: `Unknown kind '${kind}' — expected 'agent' or 'commander'` }, 400);
|
||||
if (!['agent', 'agents', 'commander', 'commanders', 'all'].includes(kind)) {
|
||||
return ctx.json({ error: `Unknown kind '${kind}' — expected 'agent', 'commander' or 'all'` }, 400);
|
||||
}
|
||||
const wantCommanders = kind === 'commander' || kind === 'commanders';
|
||||
const shown = kind === undefined ? rows : rows.filter((s) => s.isCommander === wantCommanders);
|
||||
const shown = kind === 'all' ? rows : rows.filter((s) => s.isCommander === wantCommanders);
|
||||
|
||||
return ctx.json({ sessions: shown, counts });
|
||||
});
|
||||
|
||||
@@ -27,7 +27,8 @@ export type LiveSession = {
|
||||
title: string | null;
|
||||
cwd: string | null;
|
||||
/**
|
||||
* A commander rather than an ordinary chat, derived server-side from the cwd.
|
||||
* A commander rather than an ordinary chat, derived server-side from its `command_hq_sessions` row.
|
||||
* NOT from the cwd — a commander chooses its own working directory, so the directory answers nothing.
|
||||
*
|
||||
* Every consumer of this list needs it, and one of them cannot work around its absence. The sessions
|
||||
* panel splits Live into two tabs; the iPhone app swipes between live chats, has no tabs, and so can only
|
||||
@@ -36,6 +37,12 @@ export type LiveSession = {
|
||||
isCommander: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Counts describe every live session, not just the ones returned — so a filtered request still renders
|
||||
* `Agents (n) / Commanders (n)` without a second round trip.
|
||||
*/
|
||||
type LivePayload = { sessions: LiveSession[]; counts: { agents: number; commanders: number } };
|
||||
|
||||
const LIVE_KEY = 'CHAT_LIVE_SESSIONS';
|
||||
|
||||
/**
|
||||
@@ -50,9 +57,17 @@ export function useLiveSessions() {
|
||||
const client = useClient();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const { data, isLoading, error, refetch } = useQuery<{ sessions: LiveSession[] }>({
|
||||
/*
|
||||
`kind=all` is required, not a default. `/chat/live` returns ONLY agents unless asked otherwise, so that
|
||||
a caller which forgets can never be handed a commander — a phone swiping between live chats one at a
|
||||
time has no way to skip one. This list is the other case: it renders both kinds as tabs, so it asks for
|
||||
both, explicitly, and the explicitness is what makes it reviewable.
|
||||
|
||||
`counts` describes the whole set either way, which is why both tab numbers are right from one request.
|
||||
*/
|
||||
const { data, isLoading, error, refetch } = useQuery<LivePayload>({
|
||||
queryKey: [LIVE_KEY],
|
||||
queryFn: () => client.get<{ sessions: LiveSession[] }>('/chat/live'),
|
||||
queryFn: () => client.get<LivePayload>('/chat/live?kind=all'),
|
||||
enabled: isAuthenticated,
|
||||
refetchInterval: 10_000,
|
||||
staleTime: 5_000,
|
||||
|
||||
Reference in New Issue
Block a user