@nanmicoder/dsh-agent-teams 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -135,6 +135,183 @@ window.__ModuleLoader__.load({
135
135
  return related;
136
136
  }
137
137
  //#endregion
138
+ //#region lib/client/activity-monitor.js
139
+ /** Shared, demand-driven state for the AgentTeams browser monitor. */
140
+ const targets = /* @__PURE__ */ new Map();
141
+ const targetListeners = /* @__PURE__ */ new Set();
142
+ const snapshotListeners = /* @__PURE__ */ new Set();
143
+ let targetSnapshot = [];
144
+ let activitySnapshots = {
145
+ teams: [],
146
+ archivedTeams: []
147
+ };
148
+ function targetKey(sessionId, teamId) {
149
+ return `${sessionId}\u0000${teamId}`;
150
+ }
151
+ function publishTargets() {
152
+ targetSnapshot = [...targets.values()].filter((target) => target.active).map(({ key, sessionId, teamId }) => ({
153
+ key,
154
+ sessionId,
155
+ teamId
156
+ }));
157
+ for (const listener of targetListeners) listener();
158
+ }
159
+ /** Subscribe to the active monitor-target list (React external-store shape). */
160
+ function subscribeActivityMonitorTargets(listener) {
161
+ targetListeners.add(listener);
162
+ return () => {
163
+ targetListeners.delete(listener);
164
+ };
165
+ }
166
+ /** Read the stable active-target snapshot. */
167
+ function getActivityMonitorTargetsSnapshot() {
168
+ return targetSnapshot;
169
+ }
170
+ /**
171
+ * Register one successful AgentTeams card as a monitoring demand.
172
+ *
173
+ * The returned cleanup is reference-counted so multiple cards and React
174
+ * StrictMode remounts cannot stop another card's monitor.
175
+ */
176
+ function monitorAgentTeam(sessionId, teamId) {
177
+ const owner = sessionId.trim();
178
+ const id = teamId.trim();
179
+ if (owner === "" || id === "") return () => {};
180
+ const key = targetKey(owner, id);
181
+ const existing = targets.get(key);
182
+ if (existing === void 0) {
183
+ targets.set(key, {
184
+ key,
185
+ sessionId: owner,
186
+ teamId: id,
187
+ refs: 1,
188
+ active: true
189
+ });
190
+ publishTargets();
191
+ } else {
192
+ existing.refs += 1;
193
+ if (!existing.active) {
194
+ existing.active = true;
195
+ publishTargets();
196
+ }
197
+ }
198
+ let released = false;
199
+ return () => {
200
+ if (released) return;
201
+ released = true;
202
+ const current = targets.get(key);
203
+ if (current === void 0) return;
204
+ current.refs -= 1;
205
+ if (current.refs <= 0) {
206
+ targets.delete(key);
207
+ if (current.active) publishTargets();
208
+ }
209
+ };
210
+ }
211
+ /** Stop polling targets whose final archived snapshot has been captured. */
212
+ function settleActivityMonitorTargets(keys) {
213
+ let changed = false;
214
+ for (const key of keys) {
215
+ const target = targets.get(key);
216
+ if (target?.active !== true) continue;
217
+ target.active = false;
218
+ changed = true;
219
+ }
220
+ if (changed) publishTargets();
221
+ }
222
+ /** Subscribe to the shared live/archive snapshot. */
223
+ function subscribeActivitySnapshots(listener) {
224
+ snapshotListeners.add(listener);
225
+ return () => {
226
+ snapshotListeners.delete(listener);
227
+ };
228
+ }
229
+ /** Read the stable shared live/archive snapshot. */
230
+ function getActivitySnapshotsSnapshot() {
231
+ return activitySnapshots;
232
+ }
233
+ /** Publish one or both successful state-route responses. */
234
+ function updateActivitySnapshots(update) {
235
+ const next = {
236
+ teams: update.teams ?? activitySnapshots.teams,
237
+ archivedTeams: update.archivedTeams ?? activitySnapshots.archivedTeams
238
+ };
239
+ if (next.teams === activitySnapshots.teams && next.archivedTeams === activitySnapshots.archivedTeams) return;
240
+ activitySnapshots = next;
241
+ for (const listener of snapshotListeners) listener();
242
+ }
243
+ /** Poll cadence for the live host snapshot route. */
244
+ const ACTIVITY_POLL_MS = 1e3;
245
+ /** Host route serving live and archived team snapshots. */
246
+ const ACTIVITY_STATE_URL = "/plugins/dsh-agent-teams/state";
247
+ /**
248
+ * Start the single polling loop for the current session's requested targets.
249
+ *
250
+ * With no targets this is deliberately inert: installing the plugin must not
251
+ * touch the state route. Live state is polled at the normal cadence; archive
252
+ * state is fetched only as a one-time fallback for targets no longer live.
253
+ */
254
+ function startActivityPolling(monitorTargets, runtime = {}) {
255
+ if (monitorTargets.length === 0) return {
256
+ firstTick: Promise.resolve(),
257
+ stop: () => {}
258
+ };
259
+ const fetchState = runtime.fetchState ?? ((url, init) => fetch(url, init));
260
+ const schedule = runtime.schedule ?? ((callback, intervalMs) => setInterval(callback, intervalMs));
261
+ const cancel = runtime.cancel ?? ((timer) => {
262
+ clearInterval(timer);
263
+ });
264
+ const publishSnapshots = runtime.publishSnapshots ?? updateActivitySnapshots;
265
+ const settleTargets = runtime.settleTargets ?? settleActivityMonitorTargets;
266
+ let cancelled = false;
267
+ let inFlight = false;
268
+ let controller;
269
+ const tick = async () => {
270
+ if (inFlight || cancelled) return;
271
+ inFlight = true;
272
+ controller = new AbortController();
273
+ try {
274
+ const liveResponse = await fetchState(ACTIVITY_STATE_URL, {
275
+ cache: "no-store",
276
+ signal: controller.signal
277
+ });
278
+ if (!liveResponse.ok) return;
279
+ const body = await liveResponse.json();
280
+ if (cancelled || !Array.isArray(body.teams)) return;
281
+ const liveTeams = body.teams;
282
+ publishSnapshots({ teams: liveTeams });
283
+ const missing = monitorTargets.filter((target) => !liveTeams.some((team) => team.captainSessionId === target.sessionId && team.teamId === target.teamId));
284
+ if (missing.length === 0) return;
285
+ const archivedResponse = await fetchState(`${ACTIVITY_STATE_URL}?archived=1`, {
286
+ cache: "no-store",
287
+ signal: controller.signal
288
+ });
289
+ if (!archivedResponse.ok) return;
290
+ const archivedBody = await archivedResponse.json();
291
+ if (cancelled || !Array.isArray(archivedBody.teams)) return;
292
+ publishSnapshots({ archivedTeams: archivedBody.teams });
293
+ settleTargets(new Set(missing.map((target) => target.key)));
294
+ } catch (error) {
295
+ if (error?.name === "AbortError") return;
296
+ } finally {
297
+ inFlight = false;
298
+ }
299
+ };
300
+ const firstTick = tick();
301
+ const timer = schedule(() => {
302
+ tick();
303
+ }, ACTIVITY_POLL_MS);
304
+ return {
305
+ firstTick,
306
+ stop: () => {
307
+ if (cancelled) return;
308
+ cancelled = true;
309
+ controller?.abort();
310
+ cancel(timer);
311
+ }
312
+ };
313
+ }
314
+ //#endregion
138
315
  //#region lib/client/artwork.js
139
316
  /**
140
317
  * Shared whale artwork lookup for the activity panel and the conversation
@@ -224,33 +401,14 @@ window.__ModuleLoader__.load({
224
401
  } }));
225
402
  }
226
403
  /** Render one durable team as a compact conversation card. */
227
- function AgentTeamsCard({ node, openSession, currentSessionId }) {
404
+ function AgentTeamsCard({ node, openSession, sessionId }) {
228
405
  const data = node.data;
229
- const owner = data.captainSessionId || currentSessionId() || "";
230
- const [snapshot, setSnapshot] = (0, react.useState)();
406
+ const owner = data.captainSessionId || sessionId;
407
+ const { teams, archivedTeams } = (0, react.useSyncExternalStore)(subscribeActivitySnapshots, getActivitySnapshotsSnapshot);
231
408
  (0, react.useEffect)(() => {
232
- let cancelled = false;
233
- const tick = async () => {
234
- for (const url of ["/plugins/dsh-agent-teams/state", "/plugins/dsh-agent-teams/state?archived=1"]) try {
235
- const response = await fetch(url, { cache: "no-store" });
236
- if (!response.ok) continue;
237
- const body = await response.json();
238
- const found = Array.isArray(body.teams) ? body.teams.find((team) => team.teamId === data.teamId && (owner === "" || team.captainSessionId === owner)) : void 0;
239
- if (found !== void 0) {
240
- if (!cancelled) setSnapshot(found);
241
- return;
242
- }
243
- } catch {}
244
- };
245
- tick();
246
- const timer = setInterval(() => {
247
- tick();
248
- }, 1500);
249
- return () => {
250
- cancelled = true;
251
- clearInterval(timer);
252
- };
409
+ return monitorAgentTeam(owner, data.teamId);
253
410
  }, [data.teamId, owner]);
411
+ const snapshot = teams.find((team) => team.teamId === data.teamId && (owner === "" || team.captainSessionId === owner)) ?? archivedTeams.find((team) => team.teamId === data.teamId && (owner === "" || team.captainSessionId === owner));
254
412
  const resolved = (0, react.useMemo)(() => ({
255
413
  ...data,
256
414
  captainSessionId: snapshot?.captainSessionId ?? owner,
@@ -441,8 +599,6 @@ window.__ModuleLoader__.load({
441
599
  * removed in favor of this always-available monitor.
442
600
  * @module dsh-agent-teams/client/activity
443
601
  */
444
- /** Poll cadence for the host snapshot route. */
445
- const POLL_MS = 1e3;
446
602
  /** Grace before the panel collapses once no team remains. */
447
603
  const AUTOCLOSE_GRACE_MS = 2e3;
448
604
  /**
@@ -451,8 +607,6 @@ window.__ModuleLoader__.load({
451
607
  * right after load. New activity after this window auto-expands as usual.
452
608
  */
453
609
  const AUTO_OPEN_SETTLE_MS = 4e3;
454
- /** Host route serving team snapshots. */
455
- const STATE_URL = "/plugins/dsh-agent-teams/state";
456
610
  /** Root marker shared with the panel CSS while the portal is expanded. */
457
611
  const PANEL_OPEN_ATTRIBUTE = "data-agent-teams-panel-open";
458
612
  /** Initial-letter fallback for unmatched roles. */
@@ -1056,14 +1210,15 @@ window.__ModuleLoader__.load({
1056
1210
  setWasActive(false);
1057
1211
  openSession(id);
1058
1212
  };
1059
- const [teams, setTeams] = (0, react.useState)([]);
1060
- const [archivedTeams, setArchivedTeams] = (0, react.useState)([]);
1061
1213
  const [open, setOpen] = (0, react.useState)(false);
1062
1214
  const [openOwner, setOpenOwner] = (0, react.useState)();
1063
1215
  const [autoOpened, setAutoOpened] = (0, react.useState)(false);
1064
1216
  const [wasActive, setWasActive] = (0, react.useState)(false);
1065
1217
  const [historic, setHistoric] = (0, react.useState)(/* @__PURE__ */ new Map());
1066
1218
  const current = (0, react.useSyncExternalStore)(sessionsList.subscribe, sessionsList.getSnapshot).current;
1219
+ const monitorTargets = (0, react.useSyncExternalStore)(subscribeActivityMonitorTargets, getActivityMonitorTargetsSnapshot);
1220
+ const { teams, archivedTeams } = (0, react.useSyncExternalStore)(subscribeActivitySnapshots, getActivitySnapshotsSnapshot);
1221
+ const currentTargets = (0, react.useMemo)(() => current === void 0 ? [] : monitorTargets.filter((target) => target.sessionId === current), [current, monitorTargets]);
1067
1222
  const currentRef = (0, react.useRef)(current);
1068
1223
  (0, react.useEffect)(() => {
1069
1224
  currentRef.current = current;
@@ -1086,34 +1241,12 @@ window.__ModuleLoader__.load({
1086
1241
  };
1087
1242
  }, [expanded]);
1088
1243
  (0, react.useEffect)(() => {
1089
- let cancelled = false;
1090
- let inFlight = false;
1091
- const tick = async () => {
1092
- if (inFlight || cancelled) return;
1093
- inFlight = true;
1094
- try {
1095
- const [liveResponse, archivedResponse] = await Promise.all([fetch(STATE_URL, { cache: "no-store" }), fetch(`${STATE_URL}?archived=1`, { cache: "no-store" })]);
1096
- if (liveResponse.ok) {
1097
- const body = await liveResponse.json();
1098
- if (!cancelled && Array.isArray(body.teams)) setTeams(body.teams);
1099
- }
1100
- if (archivedResponse.ok) {
1101
- const body = await archivedResponse.json();
1102
- if (!cancelled && Array.isArray(body.teams)) setArchivedTeams(body.teams);
1103
- }
1104
- } catch {} finally {
1105
- inFlight = false;
1106
- }
1107
- };
1108
- tick();
1109
- const timer = setInterval(() => {
1110
- tick();
1111
- }, POLL_MS);
1244
+ if (currentTargets.length === 0) return;
1245
+ const controller = startActivityPolling(currentTargets);
1112
1246
  return () => {
1113
- cancelled = true;
1114
- clearInterval(timer);
1247
+ controller.stop();
1115
1248
  };
1116
- }, []);
1249
+ }, [currentTargets]);
1117
1250
  (0, react.useEffect)(() => {
1118
1251
  const onOpenPanel = (event) => {
1119
1252
  const activeSession = currentRef.current;
@@ -1358,12 +1491,9 @@ window.__ModuleLoader__.load({
1358
1491
  ctx.slots.inject("conversation.chat.node", () => ctx.slots.register({
1359
1492
  name: "conversation.chat.node",
1360
1493
  key: "agent-teams",
1361
- inject: () => ({
1362
- openSession: (id) => {
1363
- ctx.sessions.open(id);
1364
- },
1365
- currentSessionId: () => ctx.sessions.list.getSnapshot().current
1366
- })
1494
+ inject: () => ({ openSession: (id) => {
1495
+ ctx.sessions.open(id);
1496
+ } })
1367
1497
  }, AgentTeamsCard));
1368
1498
  }
1369
1499
  //#endregion