@nanmicoder/dsh-agent-teams 0.1.9 → 0.1.11

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/README.md CHANGED
@@ -22,6 +22,10 @@ Ask in natural language. The plugin provides the team protocol, ten coordination
22
22
  <img src="./assets/ui.png" width="100%" alt="DeepSeek Harness conversation with the AgentTeams live activity panel, members, tasks, dependencies, and reports">
23
23
  </p>
24
24
 
25
+ ## Releases
26
+
27
+ Read the [latest release notes](https://github.com/NanmiCoder/dsh-agent-teams/releases/latest) or browse the [complete release history](https://github.com/NanmiCoder/dsh-agent-teams/releases). The same Markdown notes are included in the npm package under `release-notes/`.
28
+
25
29
  ## Why AgentTeams?
26
30
 
27
31
  | Capability | What it changes |
@@ -91,10 +95,11 @@ command), describe the goal, and press Enter.
91
95
  /agent-teams research the pricing pages of three competitors
92
96
  ```
93
97
 
94
- The line is claimed by the command pipeline and never reaches the model as
95
- plain text the handler queues one deterministic activation message as an
96
- ordinary follow-up turn, so the captain protocol starts immediately. The
97
- invocation is durably logged (`command/run` / `command/done`).
98
+ The command pipeline claims the line, then preserves that exact input as an
99
+ ordinary user follow-up so it remains visible in the main chat. The gesture
100
+ boundary adds the deterministic activation directive at pre-step, so the
101
+ captain protocol still starts immediately. The invocation is also durably
102
+ logged (`command/run` / `command/done`).
98
103
 
99
104
  Surfaces without command adjudication (for example the headless CLI) get the
100
105
  same deterministic activation through a gesture boundary: any genuine user
package/README_ZH.md CHANGED
@@ -22,6 +22,10 @@
22
22
  <img src="./assets/ui.png" width="100%" alt="DeepSeek Harness 对话与 AgentTeams 实时活动面板,展示成员、任务依赖和回报">
23
23
  </p>
24
24
 
25
+ ## 版本更新
26
+
27
+ 查看[最新版本说明](https://github.com/NanmiCoder/dsh-agent-teams/releases/latest),或浏览[完整发布历史](https://github.com/NanmiCoder/dsh-agent-teams/releases)。同一份 Markdown 说明也会随 npm 包发布到 `release-notes/` 目录。
28
+
25
29
  ## 为什么需要 AgentTeams?
26
30
 
27
31
  | 能力 | 带来的变化 |
@@ -88,7 +92,7 @@ dsh web
88
92
  /agent-teams 调研三家竞品的定价页
89
93
  ```
90
94
 
91
- 这一行会被命令管线认领,**不会**以纯文本形式发给模型——处理器会以一条确定性激活消息排队一个普通跟进回合,队长协议立即启动。调用会持久化记录(`command/run` / `command/done`)。
95
+ 这一行被命令管线认领后,会按用户提交的原文作为普通用户消息送入主会话,因此聊天记录中仍能看到完整的 `/agent-teams …`。手势边界会在 pre-step 注入确定性激活指令,队长协议仍会立即启动。调用也会持久化记录(`command/run` / `command/done`)。
92
96
 
93
97
  没有命令裁决的表面(例如 headless CLI)也享有同等的确定性激活:任何以 `/agent-teams` 开头的真实用户消息,都会为其余文本激活该协议;句子中间出现的字样仍是普通文本。
94
98
 
package/assets/ui.png ADDED
Binary file
@@ -2,25 +2,28 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  /**
3
3
  * AgentTeams activity panel: the top-right floater monitoring every team.
4
4
  *
5
- * Modeled on the Claude Code desktop SessionActivityPanel: a fixed glass
6
- * panel at the top-right corner. On wide viewports it cooperatively makes the
7
- * conversation column yield space; narrow viewports keep overlay mode. It
5
+ * Modeled on the Claude Code desktop SessionActivityPanel: a shell-overlay
6
+ * panel that docks at the conversation's top-right edge by default, can be
7
+ * dragged into a floating window, resized, and folded into an activity badge.
8
+ * On wide viewports the docked panel makes the conversation column yield
9
+ * space; narrow viewports keep a simple inset overlay. It
8
10
  * polls the host `/plugins/dsh-agent-teams/state` route for
9
11
  * server-side snapshots (durable files + live subagent activity), with a
10
12
  * collapsed badge that auto-expands once when activity appears. Archived
11
13
  * teams stay available for the owning conversation after live work ends.
12
14
  *
13
- * The floater mounts through a body portal (no top-right slot exists in the
14
- * web shell); it is not a conversation node — the in-conversation panel was
15
- * removed in favor of this always-available monitor.
15
+ * The floater mounts in ui-layout's additive `shell.overlay`; it is not a
16
+ * conversation node — the in-conversation panel was removed in favor of this
17
+ * always-available monitor.
16
18
  * @module dsh-agent-teams/client/activity
17
19
  */
18
- import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
19
- import { IconBranchOutline16, IconCloseOutline16, } from '@deepseek-ai/dsh-client-ui-primitives';
20
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
21
+ import { IconBranchOutline16, IconChevronDownOutline14, IconPanelLeftOutline16, } from '@deepseek-ai/dsh-client-ui-primitives';
20
22
  import { activityPanelExpandedForSession, compactDagLayout, COMPACT_DAG_NODE_HEIGHT, COMPACT_DAG_NODE_WIDTH, dependencyFocusTaskId, relatedTaskIds, usesParallelTaskGrid, } from "./activity-model.js";
21
23
  import { getActivityMonitorTargetsSnapshot, getActivitySnapshotsSnapshot, startActivityPolling, subscribeActivityMonitorTargets, subscribeActivitySnapshots, } from "./activity-monitor.js";
22
24
  import { ACTION_ART, LEAD_ART, memberArtUrl } from "./artwork.js";
23
25
  import { OPEN_PANEL_EVENT } from "./AgentTeamsCard.js";
26
+ import { DEFAULT_PANEL_LAYOUT, PANEL_LAYOUT_STORAGE_KEY, compactPanelForBounds, dockPanelLayout, floatPanelLayout, movePanelLayout, panelMaximumHeight, panelUsesAutoHeight, parsePanelLayout, resizePanelLayout, resolvePanelGeometry, } from "./panel-geometry.js";
24
27
  import css from './ActivityPanel.module.css';
25
28
  /** Grace before the panel collapses once no team remains. */
26
29
  const AUTOCLOSE_GRACE_MS = 2000;
@@ -30,8 +33,22 @@ const AUTOCLOSE_GRACE_MS = 2000;
30
33
  * right after load. New activity after this window auto-expands as usual.
31
34
  */
32
35
  const AUTO_OPEN_SETTLE_MS = 4000;
33
- /** Root marker shared with the panel CSS while the portal is expanded. */
36
+ /** Root marker shared with the panel CSS while the shell overlay is expanded. */
34
37
  const PANEL_OPEN_ATTRIBUTE = 'data-agent-teams-panel-open';
38
+ /** Shared width concession consumed by the conversation root CSS. */
39
+ const PANEL_SHIFT_PROPERTY = '--agent-teams-panel-shift';
40
+ const PANEL_CONVERSATION_GAP = 14;
41
+ const MOVE_THRESHOLD = 4;
42
+ function initialPanelLayout() {
43
+ if (typeof window === 'undefined')
44
+ return DEFAULT_PANEL_LAYOUT;
45
+ return parsePanelLayout(window.localStorage.getItem(PANEL_LAYOUT_STORAGE_KEY));
46
+ }
47
+ function initialPanelBounds() {
48
+ if (typeof window === 'undefined')
49
+ return { width: 1440, height: 900, anchorRight: 1440 };
50
+ return { width: window.innerWidth, height: window.innerHeight, anchorRight: window.innerWidth };
51
+ }
35
52
  /** Initial-letter fallback for unmatched roles. */
36
53
  function memberInitial(name) {
37
54
  return name.trim().slice(0, 1).toUpperCase() || '?';
@@ -82,7 +99,7 @@ function WorkGlyph({ active }) {
82
99
  }
83
100
  /** Collapsed badge: an always-visible corner pill while any team exists. */
84
101
  function CollapsedBadge({ count, busy, onClick }) {
85
- return (_jsxs("button", { type: "button", className: css.badge, "data-busy": busy, onClick: onClick, "aria-label": `AgentTeams 活动,${count} 个团队`, children: [_jsx("span", { className: css.badgeDot, "data-busy": busy, "aria-hidden": true }), _jsx("span", { className: css.badgeCount, children: count })] }));
102
+ return (_jsxs("button", { type: "button", className: css.badge, "data-agent-teams-collapsed": true, "data-busy": busy, onClick: onClick, "aria-label": `AgentTeams 活动,${count} 个团队`, children: [_jsx("span", { className: css.badgeDot, "data-busy": busy, "aria-hidden": true }), _jsx("span", { className: css.badgeCount, children: count })] }));
86
103
  }
87
104
  function memberStateLabel(member, tasks, historic) {
88
105
  const owned = tasks.filter((task) => task.assignee === member.name);
@@ -219,8 +236,11 @@ function TeamSection({ team, onNavigate, historic = false }) {
219
236
  const allCompleted = team.tasks.length > 0 && completedCount === team.tasks.length;
220
237
  return (_jsxs("section", { className: css.team, "data-team-id": team.teamId, children: [_jsxs("header", { className: css.teamHead, children: [_jsx("span", { className: css.teamName, title: team.name, children: team.name }), historic && _jsx("span", { className: css.historicPill, children: "\u5DF2\u7ED3\u675F" }), _jsxs("span", { className: css.teamStats, children: [_jsxs("span", { "data-stat": "members", children: [team.members.length, " \u6210\u5458"] }), _jsxs("span", { "data-stat": "tasks", children: [completedCount, "/", team.tasks.length, " \u5B8C\u6210"] }), _jsxs("span", { "data-stat": "messages", children: [team.messageCount, " \u6D88\u606F"] })] })] }), _jsxs("section", { className: css.delegationSection, "aria-label": "\u961F\u957F\u6D3E\u5DE5\u5173\u7CFB", "data-delegation-map": true, children: [_jsxs("div", { className: css.captainNode, children: [_jsx("span", { className: css.captainAvatar, children: _jsx("img", { className: css.leadAvatar, src: LEAD_ART, alt: "", "aria-hidden": true }) }), _jsxs("span", { className: css.captainInfo, children: [_jsxs("span", { className: css.captainLine, children: [_jsx("span", { className: css.captainName, children: "\u961F\u957F" }), _jsx("span", { className: css.captainRole, children: "\u62C6\u89E3 \u00B7 \u6D3E\u53D1 \u00B7 \u6C47\u603B" })] }), _jsxs("span", { className: css.captainSummary, children: ["\u5DF2\u6D3E\u53D1 ", assignedCount, " \u9879\u4EFB\u52A1\u7ED9 ", team.members.length, " \u540D\u6210\u5458"] })] }), _jsxs("span", { className: css.captainState, "data-busy": busyCount > 0, children: [_jsx(WorkGlyph, { active: busyCount > 0 }), busyCount > 0 ? `${busyCount} 人执行中` : allCompleted ? '已收齐' : '等待回报'] })] }), _jsx(ProgressOverview, { team: team }), _jsxs("button", { type: "button", className: css.membersToggle, onClick: () => { setMembersOpen((current) => !current); }, "aria-expanded": membersOpen, "data-members-toggle": true, children: [_jsxs("span", { children: [_jsx(Chevron, { open: membersOpen }), "\u6210\u5458 ", team.members.length] }), _jsx("span", { children: membersOpen ? '收起' : '展开' })] }), membersOpen && _jsxs("div", { className: css.delegationTree, children: [team.members.length === 0 && _jsx("span", { className: css.emptyHint, children: "\u6682\u65E0\u6210\u5458\uFF0C\u7B49\u5F85\u961F\u957F\u7EC4\u5EFA\u56E2\u961F" }), team.members.map((member) => {
221
238
  const owned = team.tasks.filter((task) => task.assignee === member.name);
222
- return (_jsxs("div", { className: css.memberBlock, "data-activity": member.activity, children: [_jsx("span", { className: css.memberBranch, "aria-hidden": true, children: _jsx("span", {}) }), _jsxs("button", { type: "button", className: css.memberRow, "data-activity": member.activity, onClick: () => { if (member.id !== '')
223
- onNavigate(member.id); }, children: [_jsxs("span", { className: css.memberAvatar, "data-unread": member.unread > 0, children: [memberArtUrl(member.name, member.role) !== null ? (_jsx("img", { className: css.memberArt, src: memberArtUrl(member.name, member.role) ?? '', alt: "", "aria-hidden": true })) : (_jsx("span", { className: css.memberInitial, style: { background: accentOf(member.id) }, children: memberInitial(member.name) })), _jsx("img", { className: css.stateArt, "data-activity": member.activity, src: ACTION_ART[member.activity], alt: "", "aria-hidden": true })] }), _jsxs("span", { className: css.memberInfo, children: [_jsxs("span", { className: css.memberLine, children: [_jsx("span", { className: css.memberName, children: member.name }), member.role !== '' && _jsx("span", { className: css.memberRole, children: member.role }), _jsxs("span", { className: css.memberState, "data-activity": member.activity, children: [_jsx(WorkGlyph, { active: member.activity === 'working' }), memberStateLabel(member, team.tasks, historic)] })] }), _jsx("span", { className: css.memberStatusLine, children: memberStatusText(member, team.tasks) })] }), _jsxs("span", { className: css.memberCount, children: [member.done, "/", member.total] })] }), _jsxs("div", { className: css.assignmentLine, children: [_jsx("span", { className: css.assignmentLabel, children: "\u961F\u957F\u6D3E\u53D1" }), _jsx("span", { className: css.assignmentTasks, children: owned.length === 0
239
+ return (_jsxs("div", { className: css.memberBlock, "data-activity": member.activity, children: [_jsx("span", { className: css.memberBranch, "aria-hidden": true, children: _jsx("span", {}) }), _jsxs("button", { type: "button", className: css.memberRow, "data-activity": member.activity, onClick: () => {
240
+ if (member.id !== '') {
241
+ onNavigate(team.captainSessionId, member.id);
242
+ }
243
+ }, children: [_jsxs("span", { className: css.memberAvatar, "data-unread": member.unread > 0, children: [memberArtUrl(member.name, member.role) !== null ? (_jsx("img", { className: css.memberArt, src: memberArtUrl(member.name, member.role) ?? '', alt: "", "aria-hidden": true })) : (_jsx("span", { className: css.memberInitial, style: { background: accentOf(member.id) }, children: memberInitial(member.name) })), _jsx("img", { className: css.stateArt, "data-activity": member.activity, src: ACTION_ART[member.activity], alt: "", "aria-hidden": true })] }), _jsxs("span", { className: css.memberInfo, children: [_jsxs("span", { className: css.memberLine, children: [_jsx("span", { className: css.memberName, children: member.name }), member.role !== '' && _jsx("span", { className: css.memberRole, children: member.role }), _jsxs("span", { className: css.memberState, "data-activity": member.activity, children: [_jsx(WorkGlyph, { active: member.activity === 'working' }), memberStateLabel(member, team.tasks, historic)] })] }), _jsx("span", { className: css.memberStatusLine, children: memberStatusText(member, team.tasks) })] }), _jsxs("span", { className: css.memberCount, children: [member.done, "/", member.total] })] }), _jsxs("div", { className: css.assignmentLine, children: [_jsx("span", { className: css.assignmentLabel, children: "\u961F\u957F\u6D3E\u53D1" }), _jsx("span", { className: css.assignmentTasks, children: owned.length === 0
224
244
  ? _jsx("span", { className: css.taskEmpty, children: "\u6682\u65E0\u4EFB\u52A1" })
225
245
  : owned.map((task) => (_jsx("span", { className: css.assignmentChip, "data-state": taskTone(task.state, task.status), title: task.subject, children: task.id }, task.id))) })] })] }, member.id));
226
246
  })] })] }), _jsx(DependencyMap, { tasks: team.tasks })] }));
@@ -251,20 +271,28 @@ function historicCardTeam(data, owner) {
251
271
  /** The top-right activity floater. Teams follow the current session: live
252
272
  * snapshots and historic card summaries are only shown while their captain
253
273
  * session is the one currently open. */
254
- export function ActivityPanel({ sessionsList, openSession }) {
274
+ export function ActivityPanel({ sessionsList, openMember }) {
255
275
  // Navigating to a member's subagent transcript is an explicit departure:
256
276
  // hide the floater immediately instead of waiting out the autocollapse
257
277
  // grace, so the panel never lingers over the member session.
258
- const navigateToSession = (id) => {
278
+ const navigateToSession = (parentId, childId) => {
259
279
  setOpen(false);
260
280
  setWasActive(false);
261
- openSession(id);
281
+ openMember(parentId, childId);
262
282
  };
263
283
  const [open, setOpen] = useState(false);
264
284
  const [openOwner, setOpenOwner] = useState();
265
285
  const [autoOpened, setAutoOpened] = useState(false);
266
286
  const [wasActive, setWasActive] = useState(false);
267
287
  const [historic, setHistoric] = useState(new Map());
288
+ const [layout, setLayout] = useState(initialPanelLayout);
289
+ const [bounds, setBounds] = useState(initialPanelBounds);
290
+ const [interaction, setInteraction] = useState(null);
291
+ const panelRef = useRef(null);
292
+ const boundsRef = useRef(bounds);
293
+ const gestureRef = useRef(null);
294
+ const frameRef = useRef(null);
295
+ const pendingLayoutRef = useRef(null);
268
296
  const current = useSyncExternalStore(sessionsList.subscribe, sessionsList.getSnapshot).current;
269
297
  const monitorTargets = useSyncExternalStore(subscribeActivityMonitorTargets, getActivityMonitorTargetsSnapshot);
270
298
  const { teams, archivedTeams } = useSyncExternalStore(subscribeActivitySnapshots, getActivitySnapshotsSnapshot);
@@ -273,7 +301,60 @@ export function ActivityPanel({ sessionsList, openSession }) {
273
301
  useEffect(() => { currentRef.current = current; }, [current]);
274
302
  const mountedAtRef = useRef(performance.now());
275
303
  const expanded = activityPanelExpandedForSession(open, openOwner, current);
276
- // This portal survives conversation route changes. Gate expansion by its
304
+ const geometry = useMemo(() => resolvePanelGeometry(layout, bounds), [layout, bounds]);
305
+ const compact = compactPanelForBounds(bounds);
306
+ const commitLayout = useCallback((next) => {
307
+ setLayout(next);
308
+ }, []);
309
+ useEffect(() => {
310
+ window.localStorage.setItem(PANEL_LAYOUT_STORAGE_KEY, JSON.stringify(layout));
311
+ }, [layout]);
312
+ // The slot sits inside AppFrame, so all geometry is measured against the
313
+ // shell overlay rather than the browser viewport. The conversation's real
314
+ // right edge is the dock anchor and naturally follows sidebar/details
315
+ // concessions without importing their hashed implementation classes.
316
+ useLayoutEffect(() => {
317
+ const overlay = document.querySelector('[data-shell-overlay]');
318
+ if (overlay === null)
319
+ return;
320
+ const conversation = document.querySelector("[data-phase='active']");
321
+ let frame = null;
322
+ const measure = () => {
323
+ frame = null;
324
+ const overlayRect = overlay.getBoundingClientRect();
325
+ const conversationRect = conversation?.getBoundingClientRect();
326
+ const next = {
327
+ width: overlayRect.width,
328
+ height: overlayRect.height,
329
+ anchorRight: conversationRect === undefined
330
+ ? overlayRect.width
331
+ : Math.min(Math.max(conversationRect.right - overlayRect.left, 0), overlayRect.width),
332
+ };
333
+ const previous = boundsRef.current;
334
+ if (previous.width === next.width
335
+ && previous.height === next.height
336
+ && previous.anchorRight === next.anchorRight)
337
+ return;
338
+ boundsRef.current = next;
339
+ setBounds(next);
340
+ };
341
+ const scheduleMeasure = () => {
342
+ frame ??= requestAnimationFrame(measure);
343
+ };
344
+ measure();
345
+ const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(scheduleMeasure);
346
+ observer?.observe(overlay);
347
+ if (conversation !== null)
348
+ observer?.observe(conversation);
349
+ window.addEventListener('resize', scheduleMeasure);
350
+ return () => {
351
+ if (frame !== null)
352
+ cancelAnimationFrame(frame);
353
+ observer?.disconnect();
354
+ window.removeEventListener('resize', scheduleMeasure);
355
+ };
356
+ }, [current]);
357
+ // This shell overlay survives conversation route changes. Gate expansion by its
277
358
  // owning session during render, then clear stale state before paint. This
278
359
  // removes the old panel immediately instead of waiting for the no-team
279
360
  // autoclose grace period on the destination page.
@@ -285,25 +366,34 @@ export function ActivityPanel({ sessionsList, openSession }) {
285
366
  setWasActive(false);
286
367
  setAutoOpened(false);
287
368
  }, [current, openOwner]);
288
- // The activity panel is a body portal, so announce its open state on body.
289
- // CSS can then make the conversation column yield space without knowing the
290
- // host shell's hashed module class names. Narrow viewports keep overlay mode.
369
+ // Only the wide docked mode asks the conversation column to yield. Floating
370
+ // and compact modes are intentionally true overlays. The width is written as
371
+ // one shared variable so the panel and the concession cannot drift apart.
291
372
  useLayoutEffect(() => {
292
373
  const root = document.documentElement;
293
- if (expanded)
374
+ const shouldYield = expanded && geometry.mode === 'docked' && !compact;
375
+ if (shouldYield) {
294
376
  root.setAttribute(PANEL_OPEN_ATTRIBUTE, '');
295
- else
377
+ root.style.setProperty(PANEL_SHIFT_PROPERTY, `${geometry.width + PANEL_CONVERSATION_GAP + 18}px`);
378
+ }
379
+ else {
296
380
  root.removeAttribute(PANEL_OPEN_ATTRIBUTE);
297
- return () => { root.removeAttribute(PANEL_OPEN_ATTRIBUTE); };
298
- }, [expanded]);
381
+ root.style.removeProperty(PANEL_SHIFT_PROPERTY);
382
+ }
383
+ return () => {
384
+ root.removeAttribute(PANEL_OPEN_ATTRIBUTE);
385
+ root.style.removeProperty(PANEL_SHIFT_PROPERTY);
386
+ };
387
+ }, [compact, expanded, geometry.mode, geometry.width]);
299
388
  useEffect(() => {
300
- // Installing the plugin alone must not touch the state route. A successful
301
- // AgentTeams conversation card registers the demand that reaches here.
302
- if (currentTargets.length === 0)
389
+ if (current === undefined)
303
390
  return;
304
- const controller = startActivityPolling(currentTargets);
391
+ // Cards keep live teams on the normal cadence. The current-session scope
392
+ // also performs one cold-start discovery pass so archived/cardless teams
393
+ // survive a browser or `dsh web` restart.
394
+ const controller = startActivityPolling(currentTargets, { discoverySessionId: current });
305
395
  return () => { controller.stop(); };
306
- }, [currentTargets]);
396
+ }, [current, currentTargets]);
307
397
  useEffect(() => {
308
398
  const onOpenPanel = (event) => {
309
399
  const activeSession = currentRef.current;
@@ -365,6 +455,123 @@ export function ActivityPanel({ sessionsList, openSession }) {
365
455
  }, [visibleCount, autoOpened, wasActive]);
366
456
  const busy = useMemo(() => visibleTeams.some((team) => team.members.some((member) => member.activity === 'working')), [visibleTeams]);
367
457
  const hasTeams = visibleCount > 0;
458
+ // Auto-height panels do not store their live content height. Capture the
459
+ // rendered box when a pointer gesture starts so movement and a first manual
460
+ // resize clamp against what the user actually sees.
461
+ const panelGeometryForGesture = useCallback(() => {
462
+ const measuredHeight = panelRef.current?.getBoundingClientRect().height;
463
+ if (measuredHeight === undefined || measuredHeight <= 0)
464
+ return geometry;
465
+ return { ...geometry, height: measuredHeight };
466
+ }, [geometry]);
467
+ const flushScheduledLayout = useCallback(() => {
468
+ if (frameRef.current !== null) {
469
+ cancelAnimationFrame(frameRef.current);
470
+ frameRef.current = null;
471
+ }
472
+ const pending = pendingLayoutRef.current;
473
+ pendingLayoutRef.current = null;
474
+ if (pending !== null)
475
+ commitLayout(pending);
476
+ }, [commitLayout]);
477
+ const scheduleLayout = useCallback((next) => {
478
+ pendingLayoutRef.current = next;
479
+ frameRef.current ??= requestAnimationFrame(() => {
480
+ frameRef.current = null;
481
+ const pending = pendingLayoutRef.current;
482
+ pendingLayoutRef.current = null;
483
+ if (pending !== null)
484
+ commitLayout(pending);
485
+ });
486
+ }, [commitLayout]);
487
+ useEffect(() => () => {
488
+ if (frameRef.current !== null)
489
+ cancelAnimationFrame(frameRef.current);
490
+ }, []);
491
+ const beginMove = useCallback((event) => {
492
+ if (compact || event.button !== 0 || event.target.closest('button') !== null)
493
+ return;
494
+ event.preventDefault();
495
+ event.currentTarget.setPointerCapture(event.pointerId);
496
+ gestureRef.current = {
497
+ kind: 'move',
498
+ pointerId: event.pointerId,
499
+ originX: event.clientX,
500
+ originY: event.clientY,
501
+ start: panelGeometryForGesture(),
502
+ activated: false,
503
+ };
504
+ }, [compact, panelGeometryForGesture]);
505
+ const beginResize = useCallback((edge, event) => {
506
+ if (compact || event.button !== 0 || (geometry.mode === 'docked' && edge !== 'left'))
507
+ return;
508
+ event.preventDefault();
509
+ event.stopPropagation();
510
+ event.currentTarget.setPointerCapture(event.pointerId);
511
+ gestureRef.current = {
512
+ kind: 'resize',
513
+ edge,
514
+ pointerId: event.pointerId,
515
+ originX: event.clientX,
516
+ originY: event.clientY,
517
+ start: panelGeometryForGesture(),
518
+ activated: true,
519
+ };
520
+ setInteraction('resizing');
521
+ }, [compact, geometry.mode, panelGeometryForGesture]);
522
+ const updateGesture = useCallback((event) => {
523
+ const gesture = gestureRef.current;
524
+ if (gesture === null || gesture.pointerId !== event.pointerId
525
+ || !event.currentTarget.hasPointerCapture(event.pointerId))
526
+ return;
527
+ const dx = event.clientX - gesture.originX;
528
+ const dy = event.clientY - gesture.originY;
529
+ const activeBounds = boundsRef.current;
530
+ if (gesture.kind === 'move') {
531
+ if (!gesture.activated && Math.hypot(dx, dy) < MOVE_THRESHOLD)
532
+ return;
533
+ if (!gesture.activated) {
534
+ gesture.activated = true;
535
+ setInteraction('dragging');
536
+ }
537
+ scheduleLayout(movePanelLayout(floatPanelLayout(gesture.start, activeBounds), dx, dy, activeBounds));
538
+ return;
539
+ }
540
+ scheduleLayout(resizePanelLayout(gesture.start, gesture.edge ?? 'left', dx, dy, activeBounds));
541
+ }, [scheduleLayout]);
542
+ const endGesture = useCallback((event) => {
543
+ const gesture = gestureRef.current;
544
+ if (gesture === null || gesture.pointerId !== event.pointerId)
545
+ return;
546
+ updateGesture(event);
547
+ flushScheduledLayout();
548
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
549
+ event.currentTarget.releasePointerCapture(event.pointerId);
550
+ }
551
+ gestureRef.current = null;
552
+ setInteraction(null);
553
+ }, [flushScheduledLayout, updateGesture]);
554
+ const cancelGesture = useCallback((event) => {
555
+ const gesture = gestureRef.current;
556
+ if (gesture === null || gesture.pointerId !== event.pointerId)
557
+ return;
558
+ flushScheduledLayout();
559
+ gestureRef.current = null;
560
+ setInteraction(null);
561
+ }, [flushScheduledLayout]);
562
+ const toggleDock = useCallback(() => {
563
+ const liveGeometry = panelGeometryForGesture();
564
+ commitLayout(liveGeometry.mode === 'docked'
565
+ ? floatPanelLayout(liveGeometry, boundsRef.current)
566
+ : dockPanelLayout(liveGeometry, boundsRef.current));
567
+ }, [commitLayout, panelGeometryForGesture]);
568
+ const autoHeight = panelUsesAutoHeight(geometry, bounds);
569
+ const panelStyle = {
570
+ width: geometry.width,
571
+ height: autoHeight ? 'auto' : geometry.height,
572
+ maxHeight: panelMaximumHeight(geometry, bounds),
573
+ transform: `translate3d(${geometry.x}px, ${geometry.y}px, 0)`,
574
+ };
368
575
  if (!hasTeams && !expanded)
369
576
  return null;
370
577
  return (_jsxs(_Fragment, { children: [!expanded && (_jsx(CollapsedBadge, { count: visibleCount, busy: busy, onClick: () => {
@@ -372,13 +579,13 @@ export function ActivityPanel({ sessionsList, openSession }) {
372
579
  return;
373
580
  setOpenOwner(current);
374
581
  setOpen(true);
375
- } })), expanded && (_jsxs("aside", { className: css.panel, "data-agent-teams-activity": true, children: [_jsxs("header", { className: css.panelHead, children: [_jsxs("span", { className: css.panelTitle, children: ["AgentTeams \u6D3B\u52A8", _jsx("span", { className: css.panelDot, "data-busy": busy, "aria-hidden": true })] }), _jsx("button", { type: "button", className: css.closeButton, onClick: () => {
376
- setOpen(false);
377
- setOpenOwner(undefined);
378
- }, "aria-label": "\u5173\u95ED", children: _jsx(IconCloseOutline16, {}) })] }), _jsx("div", { className: css.teams, children: visibleCount === 0
582
+ } })), expanded && (_jsxs("aside", { ref: panelRef, className: css.panel, style: panelStyle, "data-agent-teams-activity": true, "data-panel-mode": geometry.mode, "data-height-mode": autoHeight ? 'auto' : 'manual', "data-compact": compact || undefined, "data-dragging": interaction === 'dragging' || undefined, "data-resizing": interaction === 'resizing' || undefined, "aria-label": "AgentTeams \u6D3B\u52A8\u9762\u677F", children: [_jsxs("header", { className: css.panelHead, onPointerDown: beginMove, onPointerMove: updateGesture, onPointerUp: endGesture, onPointerCancel: cancelGesture, "data-drag-handle": !compact || undefined, children: [_jsxs("span", { className: css.panelTitle, children: ["AgentTeams \u6D3B\u52A8", _jsx("span", { className: css.panelDot, "data-busy": busy, "aria-hidden": true })] }), _jsxs("span", { className: css.panelControls, children: [!compact && (_jsx("button", { type: "button", className: css.iconButton, "data-control": "dock", "data-mode": geometry.mode, onClick: toggleDock, "aria-label": geometry.mode === 'docked' ? '切换为浮动面板' : '停靠到右侧', title: geometry.mode === 'docked' ? '切换为浮动面板' : '停靠到右侧', children: _jsx(IconPanelLeftOutline16, {}) })), _jsx("button", { type: "button", className: css.iconButton, "data-control": "collapse", onClick: () => {
583
+ setOpen(false);
584
+ setOpenOwner(undefined);
585
+ }, "aria-label": "\u6536\u8D77\u6D3B\u52A8\u9762\u677F", title: "\u6536\u8D77\u6D3B\u52A8\u9762\u677F", children: _jsx(IconChevronDownOutline14, {}) })] })] }), _jsx("div", { className: css.teams, children: visibleCount === 0
379
586
  ? _jsx("span", { className: css.emptyHint, children: "\u6682\u65E0\u56E2\u961F\u6D3B\u52A8" })
380
587
  : (_jsxs(_Fragment, { children: [visibleTeams.map((team) => (_jsx(TeamSection, { team: team, onNavigate: navigateToSession }, team.teamId))), visibleArchived.map((team) => (_jsx("div", { "data-team-id": team.teamId, "data-historic": true, className: css.archivedWrap, children: _jsx(TeamSection, { team: team, onNavigate: navigateToSession, historic: true }) }, `${team.captainSessionId}:${team.teamId}`))), visibleHistoric.map(({ data: team, owner }) => {
381
588
  const teamKey = `${owner}:${team.teamId}`;
382
589
  return (_jsx(TeamSection, { team: historicCardTeam(team, owner), onNavigate: navigateToSession, historic: true }, teamKey));
383
- })] })) })] }))] }));
590
+ })] })) }), !compact && (_jsx("div", { className: css.resizeHandle, "data-resize-edge": "left", onPointerDown: (event) => { beginResize('left', event); }, onPointerMove: updateGesture, onPointerUp: endGesture, onPointerCancel: cancelGesture, "aria-hidden": true })), !compact && geometry.mode === 'floating' && (_jsxs(_Fragment, { children: [_jsx("div", { className: css.resizeHandle, "data-resize-edge": "bottom", onPointerDown: (event) => { beginResize('bottom', event); }, onPointerMove: updateGesture, onPointerUp: endGesture, onPointerCancel: cancelGesture, "aria-hidden": true }), _jsx("div", { className: css.resizeHandle, "data-resize-edge": "corner", onPointerDown: (event) => { beginResize('corner', event); }, onPointerMove: updateGesture, onPointerUp: endGesture, onPointerCancel: cancelGesture, "aria-hidden": true })] }))] }))] }));
384
591
  }
@@ -30,7 +30,7 @@ function openActivityPanel(data) {
30
30
  }));
31
31
  }
32
32
  /** Render one durable team as a compact conversation card. */
33
- export function AgentTeamsCard({ node, openSession, sessionId }) {
33
+ export function AgentTeamsCard({ node, openMember, sessionId }) {
34
34
  const data = node.data;
35
35
  // `conversation.chat.node` is session-scoped, so its framework-owned id is
36
36
  // a stable owner even while another conversation becomes current.
@@ -47,6 +47,8 @@ export function AgentTeamsCard({ node, openSession, sessionId }) {
47
47
  teamName: snapshot?.name ?? data.teamName,
48
48
  members: snapshot?.members.map((member) => ({ id: member.id, name: member.name, role: member.role })) ?? data.members,
49
49
  }), [data, owner, snapshot]);
50
- return (_jsxs("section", { className: css.root, "data-agent-teams-card": true, "data-team-id": resolved.teamId, children: [_jsxs("header", { className: css.head, children: [_jsx("img", { className: css.leadAvatar, src: LEAD_ART, alt: "", "aria-hidden": true }), _jsx("span", { className: css.teamName, title: resolved.teamName, children: resolved.teamName }), _jsxs("span", { className: css.memberCount, children: [resolved.members.length, " \u540D\u6210\u5458"] }), _jsx("button", { type: "button", className: css.panelButton, onClick: () => { openActivityPanel(resolved); }, "aria-label": "\u6253\u5F00\u6D3B\u52A8\u9762\u677F", title: "\u6253\u5F00\u6D3B\u52A8\u9762\u677F", children: "\u6D3B\u52A8\u9762\u677F" })] }), resolved.members.length > 0 && (_jsx("div", { className: css.members, children: resolved.members.map((member) => (_jsxs("button", { type: "button", className: css.member, onClick: () => { if (member.id !== '')
51
- openSession(member.id); }, title: member.role === '' ? member.name : `${member.name} · ${member.role}`, children: [memberArtUrl(member.name, member.role) !== null ? (_jsx("img", { className: css.memberArt, src: memberArtUrl(member.name, member.role) ?? '', alt: "", "aria-hidden": true })) : (_jsx("span", { className: css.memberInitial, children: member.name.trim().slice(0, 1).toUpperCase() || '?' })), _jsx("span", { className: css.memberName, children: member.name })] }, member.id))) }))] }));
50
+ return (_jsxs("section", { className: css.root, "data-agent-teams-card": true, "data-team-id": resolved.teamId, children: [_jsxs("header", { className: css.head, children: [_jsx("img", { className: css.leadAvatar, src: LEAD_ART, alt: "", "aria-hidden": true }), _jsx("span", { className: css.teamName, title: resolved.teamName, children: resolved.teamName }), _jsxs("span", { className: css.memberCount, children: [resolved.members.length, " \u540D\u6210\u5458"] }), _jsx("button", { type: "button", className: css.panelButton, onClick: () => { openActivityPanel(resolved); }, "aria-label": "\u6253\u5F00\u6D3B\u52A8\u9762\u677F", title: "\u6253\u5F00\u6D3B\u52A8\u9762\u677F", children: "\u6D3B\u52A8\u9762\u677F" })] }), resolved.members.length > 0 && (_jsx("div", { className: css.members, children: resolved.members.map((member) => (_jsxs("button", { type: "button", className: css.member, onClick: () => {
51
+ if (member.id !== '')
52
+ openMember(owner, member.id);
53
+ }, title: member.role === '' ? member.name : `${member.name} · ${member.role}`, children: [memberArtUrl(member.name, member.role) !== null ? (_jsx("img", { className: css.memberArt, src: memberArtUrl(member.name, member.role) ?? '', alt: "", "aria-hidden": true })) : (_jsx("span", { className: css.memberInitial, children: member.name.trim().slice(0, 1).toUpperCase() || '?' })), _jsx("span", { className: css.memberName, children: member.name })] }, member.id))) }))] }));
52
54
  }
@@ -14,10 +14,10 @@ export function usesParallelTaskGrid(tasks) {
14
14
  /**
15
15
  * Whether an expanded activity panel still belongs to the current session.
16
16
  *
17
- * The panel is mounted through a body portal, so React does not remount it
18
- * when the conversation route changes. Ownership keeps an expanded panel
19
- * from leaking onto the new-session screen (or another conversation) while
20
- * its local open state is being reset.
17
+ * The panel is mounted in the root-scoped shell overlay, so React does not
18
+ * remount it when the conversation route changes. Ownership keeps an expanded
19
+ * panel from leaking onto the new-session screen (or another conversation)
20
+ * while its local open state is being reset.
21
21
  */
22
22
  export function activityPanelExpandedForSession(open, owner, current) {
23
23
  return open && owner !== undefined && owner === current;
@@ -104,12 +104,17 @@ export const ACTIVITY_STATE_URL = '/plugins/dsh-agent-teams/state';
104
104
  /**
105
105
  * Start the single polling loop for the current session's requested targets.
106
106
  *
107
- * With no targets this is deliberately inert: installing the plugin must not
108
- * touch the state route. Live state is polled at the normal cadence; archive
109
- * state is fetched only as a one-time fallback for targets no longer live.
107
+ * With neither targets nor a discovery session this is deliberately inert.
108
+ * A discovery session performs one live+archive pass after selection/restart;
109
+ * it keeps polling only while that captain still owns a live team. This
110
+ * restores legacy/cardless history without turning every ordinary session
111
+ * into a permanent one-second filesystem scan. Explicit card targets retain
112
+ * the normal cadence, and archive state is refreshed when a target or a
113
+ * previously discovered live team disappears.
110
114
  */
111
115
  export function startActivityPolling(monitorTargets, runtime = {}) {
112
- if (monitorTargets.length === 0) {
116
+ const discoverySessionId = runtime.discoverySessionId?.trim();
117
+ if (monitorTargets.length === 0 && (discoverySessionId === undefined || discoverySessionId === '')) {
113
118
  return { firstTick: Promise.resolve(), stop: () => { } };
114
119
  }
115
120
  const fetchState = runtime.fetchState ?? ((url, init) => fetch(url, init));
@@ -119,10 +124,16 @@ export function startActivityPolling(monitorTargets, runtime = {}) {
119
124
  const settleTargets = runtime.settleTargets ?? settleActivityMonitorTargets;
120
125
  let cancelled = false;
121
126
  let inFlight = false;
127
+ let discoveryComplete = false;
128
+ let discoveredLiveKeys = new Set();
122
129
  let controller;
123
130
  const tick = async () => {
124
131
  if (inFlight || cancelled)
125
132
  return;
133
+ // A cardless ordinary or archive-only session needs one recovery pass,
134
+ // then stays dormant until the component is recreated for another session.
135
+ if (discoveryComplete && monitorTargets.length === 0 && discoveredLiveKeys.size === 0)
136
+ return;
126
137
  inFlight = true;
127
138
  controller = new AbortController();
128
139
  try {
@@ -137,11 +148,24 @@ export function startActivityPolling(monitorTargets, runtime = {}) {
137
148
  return;
138
149
  const liveTeams = body.teams;
139
150
  publishSnapshots({ teams: liveTeams });
151
+ const previousDiscoveredKeys = discoveredLiveKeys;
152
+ discoveredLiveKeys = new Set(discoverySessionId === undefined || discoverySessionId === ''
153
+ ? []
154
+ : liveTeams
155
+ .filter((team) => team.captainSessionId === discoverySessionId)
156
+ .map((team) => team.teamId));
157
+ const discoveredTeamArchived = [...previousDiscoveredKeys]
158
+ .some((teamId) => !discoveredLiveKeys.has(teamId));
140
159
  const missing = monitorTargets.filter((target) => !liveTeams.some((team) => team.captainSessionId === target.sessionId && team.teamId === target.teamId));
141
- if (missing.length === 0)
160
+ const needsDiscoveryArchive = discoverySessionId !== undefined
161
+ && discoverySessionId !== ''
162
+ && !discoveryComplete;
163
+ if (missing.length === 0 && !needsDiscoveryArchive && !discoveredTeamArchived)
142
164
  return;
143
- // Archives are immutable. A successful fallback retires every missing
144
- // target, including legacy cards whose host archive no longer exists.
165
+ // Archives are immutable per team generation. A successful fallback
166
+ // retires every missing explicit target, including legacy cards whose
167
+ // host archive no longer exists; discovery remains available from the
168
+ // shared snapshot after this controller becomes dormant.
145
169
  const archivedResponse = await fetchState(`${ACTIVITY_STATE_URL}?archived=1`, {
146
170
  cache: 'no-store',
147
171
  signal: controller.signal,
@@ -152,6 +176,7 @@ export function startActivityPolling(monitorTargets, runtime = {}) {
152
176
  if (cancelled || !Array.isArray(archivedBody.teams))
153
177
  return;
154
178
  publishSnapshots({ archivedTeams: archivedBody.teams });
179
+ discoveryComplete = true;
155
180
  settleTargets(new Set(missing.map((target) => target.key)));
156
181
  }
157
182
  catch (error) {
@@ -1,32 +1,45 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createRoot } from 'react-dom/client';
3
2
  import { ActivityPanel } from "./ActivityPanel.js";
4
3
  import { AgentTeamsCard } from "./AgentTeamsCard.js";
5
4
  import { agentTeamsCardDefinition } from "./agent-teams-card-definition.js";
5
+ import { openAgentTeamMember } from "./session-navigation.js";
6
6
  /** Required services: conversation nodes, slots, and sessions navigation. */
7
7
  export const inject = ['conversationEvents', 'slots', 'sessions'];
8
+ /** The replayed user message is the canonical transcript entry. */
9
+ function HiddenAgentTeamsCommand() {
10
+ return null;
11
+ }
8
12
  /**
9
- * Mount the floater through a body portal (the web shell has no top-right
10
- * slot) and register the in-conversation team card, whose "activity panel"
11
- * button re-activates the floater via a window event — the recovery path
12
- * for a closed floater or a re-opened session.
13
+ * Register the activity monitor in the shell's additive overlay and the
14
+ * in-conversation team card. The card's activity button re-opens a folded
15
+ * monitor via a window event — the recovery path for an old session.
13
16
  */
14
17
  export function apply(ctx) {
15
- const host = document.createElement('div');
16
- host.dataset.agentTeamsHost = '';
17
- document.body.appendChild(host);
18
- const root = createRoot(host);
19
- root.render(_jsx(ActivityPanel, { sessionsList: ctx.sessions.list, openSession: (id) => { ctx.sessions.open(id); } }));
20
- ctx.effect(() => () => {
21
- root.unmount();
22
- host.remove();
23
- }, 'agent-teams: activity panel');
18
+ const openMember = (parentId, childId) => {
19
+ void openAgentTeamMember(ctx.sessions, parentId, childId).catch((error) => {
20
+ console.warn(`agent-teams: failed to open member transcript ${childId}: ${String(error)}`);
21
+ });
22
+ };
23
+ const Panel = () => _jsx(ActivityPanel, { sessionsList: ctx.sessions.list, openMember: openMember });
24
+ ctx.slots.inject('shell.overlay', () => ctx.slots.register({
25
+ name: 'shell.overlay',
26
+ id: 'agent-teams-activity',
27
+ order: 80,
28
+ label: 'AgentTeams activity',
29
+ }, Panel));
30
+ // The host command is only the slash-menu/admission surface. Its input is
31
+ // replayed as the visible user message, so the generic result row would be
32
+ // a duplicate placed before that message by command lifecycle ordering.
33
+ ctx.slots.inject('conversation.chat.commandview', () => ctx.slots.register({
34
+ name: 'conversation.chat.commandview',
35
+ key: 'agent-teams',
36
+ }, HiddenAgentTeamsCommand));
24
37
  ctx.conversationEvents.register(agentTeamsCardDefinition);
25
38
  ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
26
39
  name: 'conversation.chat.node',
27
40
  key: 'agent-teams',
28
41
  inject: () => ({
29
- openSession: (id) => { ctx.sessions.open(id); },
42
+ openMember,
30
43
  }),
31
44
  }, AgentTeamsCard));
32
45
  }