Files
AgentHub/client/src/hooks/usePanePresence.ts
T
nntrivi2001 18e574a765 feat(server-mode): pane presence and typing audit (sharing phase 4)
A pane's tab shows the other people who have it open (pane-presence.js,
in memory, fed by the /ws-pty attach; GET /api/lanes/:id/presence and the
pane_presence broadcast to the owner and full collaborators) and says
"Billed to <name>" for a pane a collaborator opened (startedBy on the run).
The first real keystroke of each attach into a shared workspace's pane by
anyone but its owner is audited as `type into <run id>` (mouse, focus and
terminal replies to tmux do not count). The pty socket now pings, so a
vanished browser is ended at the next re-check and leaves the presence list.
2026-11-06 11:32:25 +07:00

55 lines
1.8 KiB
TypeScript

/**
* @file usePanePresence.ts
* @description Who has each of a workspace's panes open (server mode, workspace
* sharing phase 4): `GET /api/lanes/:id/presence` once, then every
* `pane_presence` frame for that workspace, and a re-read on a WebSocket
* reconnect (frames missed while away are never replayed). Local mode has
* one user and reads nothing. Returns `{runId: [{id, name}]}`.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useEffect, useState } from "react";
import { api } from "../lib/api";
import { eventBus } from "../lib/eventBus";
import { usePermissions } from "../components/AuthGate";
export type PanePeople = Record<string, { id: number; name: string }[]>;
export function usePanePresence(laneId: number | null | undefined): PanePeople {
const { local } = usePermissions();
const [panes, setPanes] = useState<PanePeople>({});
useEffect(() => {
setPanes({});
if (local || laneId == null) return;
let alive = true;
const load = () =>
Promise.resolve()
.then(() => api.lanes.presence(laneId))
.then((r) => alive && setPanes(r.panes))
.catch(() => {
/* presence is a nicety: no list, no chips */
});
void load();
const offMsg = eventBus.subscribe((msg) => {
if (msg.type !== "pane_presence") return;
const d = msg.data as { laneId?: number; runId?: string; users?: PanePeople[string] };
if (d.laneId !== laneId || !d.runId) return;
setPanes((prev) => {
const next = { ...prev };
if (d.users?.length) next[d.runId!] = d.users;
else delete next[d.runId!];
return next;
});
});
const offConn = eventBus.onConnection((up) => {
if (up) void load();
});
return () => {
alive = false;
offMsg();
offConn();
};
}, [laneId, local]);
return panes;
}