@nanmicoder/dsh-agent-teams 0.1.9 → 0.1.10
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 +9 -4
- package/README_ZH.md +5 -1
- package/assets/ui.png +0 -0
- package/lib/client/ActivityPanel.js +226 -23
- package/lib/client/activity-model.js +4 -4
- package/lib/client/index.js +21 -14
- package/lib/client/panel-geometry.js +143 -0
- package/lib/client.js +492 -80
- package/lib/client.js.map +1 -1
- package/lib/command.js +14 -15
- package/lib/types/client/ActivityPanel.d.ts +8 -6
- package/lib/types/client/activity-model.d.ts +4 -4
- package/lib/types/client/index.d.ts +3 -4
- package/lib/types/client/panel-geometry.d.ts +49 -0
- package/lib/types/command.d.ts +14 -15
- package/package.json +23 -23
- package/release-notes/v0.1.10.md +48 -0
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
|
6
|
-
* panel at the top-right
|
|
7
|
-
*
|
|
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
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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,
|
|
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
|
|
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);
|
|
@@ -265,6 +282,14 @@ export function ActivityPanel({ sessionsList, openSession }) {
|
|
|
265
282
|
const [autoOpened, setAutoOpened] = useState(false);
|
|
266
283
|
const [wasActive, setWasActive] = useState(false);
|
|
267
284
|
const [historic, setHistoric] = useState(new Map());
|
|
285
|
+
const [layout, setLayout] = useState(initialPanelLayout);
|
|
286
|
+
const [bounds, setBounds] = useState(initialPanelBounds);
|
|
287
|
+
const [interaction, setInteraction] = useState(null);
|
|
288
|
+
const panelRef = useRef(null);
|
|
289
|
+
const boundsRef = useRef(bounds);
|
|
290
|
+
const gestureRef = useRef(null);
|
|
291
|
+
const frameRef = useRef(null);
|
|
292
|
+
const pendingLayoutRef = useRef(null);
|
|
268
293
|
const current = useSyncExternalStore(sessionsList.subscribe, sessionsList.getSnapshot).current;
|
|
269
294
|
const monitorTargets = useSyncExternalStore(subscribeActivityMonitorTargets, getActivityMonitorTargetsSnapshot);
|
|
270
295
|
const { teams, archivedTeams } = useSyncExternalStore(subscribeActivitySnapshots, getActivitySnapshotsSnapshot);
|
|
@@ -273,7 +298,60 @@ export function ActivityPanel({ sessionsList, openSession }) {
|
|
|
273
298
|
useEffect(() => { currentRef.current = current; }, [current]);
|
|
274
299
|
const mountedAtRef = useRef(performance.now());
|
|
275
300
|
const expanded = activityPanelExpandedForSession(open, openOwner, current);
|
|
276
|
-
|
|
301
|
+
const geometry = useMemo(() => resolvePanelGeometry(layout, bounds), [layout, bounds]);
|
|
302
|
+
const compact = compactPanelForBounds(bounds);
|
|
303
|
+
const commitLayout = useCallback((next) => {
|
|
304
|
+
setLayout(next);
|
|
305
|
+
}, []);
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
window.localStorage.setItem(PANEL_LAYOUT_STORAGE_KEY, JSON.stringify(layout));
|
|
308
|
+
}, [layout]);
|
|
309
|
+
// The slot sits inside AppFrame, so all geometry is measured against the
|
|
310
|
+
// shell overlay rather than the browser viewport. The conversation's real
|
|
311
|
+
// right edge is the dock anchor and naturally follows sidebar/details
|
|
312
|
+
// concessions without importing their hashed implementation classes.
|
|
313
|
+
useLayoutEffect(() => {
|
|
314
|
+
const overlay = document.querySelector('[data-shell-overlay]');
|
|
315
|
+
if (overlay === null)
|
|
316
|
+
return;
|
|
317
|
+
const conversation = document.querySelector("[data-phase='active']");
|
|
318
|
+
let frame = null;
|
|
319
|
+
const measure = () => {
|
|
320
|
+
frame = null;
|
|
321
|
+
const overlayRect = overlay.getBoundingClientRect();
|
|
322
|
+
const conversationRect = conversation?.getBoundingClientRect();
|
|
323
|
+
const next = {
|
|
324
|
+
width: overlayRect.width,
|
|
325
|
+
height: overlayRect.height,
|
|
326
|
+
anchorRight: conversationRect === undefined
|
|
327
|
+
? overlayRect.width
|
|
328
|
+
: Math.min(Math.max(conversationRect.right - overlayRect.left, 0), overlayRect.width),
|
|
329
|
+
};
|
|
330
|
+
const previous = boundsRef.current;
|
|
331
|
+
if (previous.width === next.width
|
|
332
|
+
&& previous.height === next.height
|
|
333
|
+
&& previous.anchorRight === next.anchorRight)
|
|
334
|
+
return;
|
|
335
|
+
boundsRef.current = next;
|
|
336
|
+
setBounds(next);
|
|
337
|
+
};
|
|
338
|
+
const scheduleMeasure = () => {
|
|
339
|
+
frame ??= requestAnimationFrame(measure);
|
|
340
|
+
};
|
|
341
|
+
measure();
|
|
342
|
+
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(scheduleMeasure);
|
|
343
|
+
observer?.observe(overlay);
|
|
344
|
+
if (conversation !== null)
|
|
345
|
+
observer?.observe(conversation);
|
|
346
|
+
window.addEventListener('resize', scheduleMeasure);
|
|
347
|
+
return () => {
|
|
348
|
+
if (frame !== null)
|
|
349
|
+
cancelAnimationFrame(frame);
|
|
350
|
+
observer?.disconnect();
|
|
351
|
+
window.removeEventListener('resize', scheduleMeasure);
|
|
352
|
+
};
|
|
353
|
+
}, [current]);
|
|
354
|
+
// This shell overlay survives conversation route changes. Gate expansion by its
|
|
277
355
|
// owning session during render, then clear stale state before paint. This
|
|
278
356
|
// removes the old panel immediately instead of waiting for the no-team
|
|
279
357
|
// autoclose grace period on the destination page.
|
|
@@ -285,17 +363,25 @@ export function ActivityPanel({ sessionsList, openSession }) {
|
|
|
285
363
|
setWasActive(false);
|
|
286
364
|
setAutoOpened(false);
|
|
287
365
|
}, [current, openOwner]);
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
366
|
+
// Only the wide docked mode asks the conversation column to yield. Floating
|
|
367
|
+
// and compact modes are intentionally true overlays. The width is written as
|
|
368
|
+
// one shared variable so the panel and the concession cannot drift apart.
|
|
291
369
|
useLayoutEffect(() => {
|
|
292
370
|
const root = document.documentElement;
|
|
293
|
-
|
|
371
|
+
const shouldYield = expanded && geometry.mode === 'docked' && !compact;
|
|
372
|
+
if (shouldYield) {
|
|
294
373
|
root.setAttribute(PANEL_OPEN_ATTRIBUTE, '');
|
|
295
|
-
|
|
374
|
+
root.style.setProperty(PANEL_SHIFT_PROPERTY, `${geometry.width + PANEL_CONVERSATION_GAP + 18}px`);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
296
377
|
root.removeAttribute(PANEL_OPEN_ATTRIBUTE);
|
|
297
|
-
|
|
298
|
-
|
|
378
|
+
root.style.removeProperty(PANEL_SHIFT_PROPERTY);
|
|
379
|
+
}
|
|
380
|
+
return () => {
|
|
381
|
+
root.removeAttribute(PANEL_OPEN_ATTRIBUTE);
|
|
382
|
+
root.style.removeProperty(PANEL_SHIFT_PROPERTY);
|
|
383
|
+
};
|
|
384
|
+
}, [compact, expanded, geometry.mode, geometry.width]);
|
|
299
385
|
useEffect(() => {
|
|
300
386
|
// Installing the plugin alone must not touch the state route. A successful
|
|
301
387
|
// AgentTeams conversation card registers the demand that reaches here.
|
|
@@ -365,6 +451,123 @@ export function ActivityPanel({ sessionsList, openSession }) {
|
|
|
365
451
|
}, [visibleCount, autoOpened, wasActive]);
|
|
366
452
|
const busy = useMemo(() => visibleTeams.some((team) => team.members.some((member) => member.activity === 'working')), [visibleTeams]);
|
|
367
453
|
const hasTeams = visibleCount > 0;
|
|
454
|
+
// Auto-height panels do not store their live content height. Capture the
|
|
455
|
+
// rendered box when a pointer gesture starts so movement and a first manual
|
|
456
|
+
// resize clamp against what the user actually sees.
|
|
457
|
+
const panelGeometryForGesture = useCallback(() => {
|
|
458
|
+
const measuredHeight = panelRef.current?.getBoundingClientRect().height;
|
|
459
|
+
if (measuredHeight === undefined || measuredHeight <= 0)
|
|
460
|
+
return geometry;
|
|
461
|
+
return { ...geometry, height: measuredHeight };
|
|
462
|
+
}, [geometry]);
|
|
463
|
+
const flushScheduledLayout = useCallback(() => {
|
|
464
|
+
if (frameRef.current !== null) {
|
|
465
|
+
cancelAnimationFrame(frameRef.current);
|
|
466
|
+
frameRef.current = null;
|
|
467
|
+
}
|
|
468
|
+
const pending = pendingLayoutRef.current;
|
|
469
|
+
pendingLayoutRef.current = null;
|
|
470
|
+
if (pending !== null)
|
|
471
|
+
commitLayout(pending);
|
|
472
|
+
}, [commitLayout]);
|
|
473
|
+
const scheduleLayout = useCallback((next) => {
|
|
474
|
+
pendingLayoutRef.current = next;
|
|
475
|
+
frameRef.current ??= requestAnimationFrame(() => {
|
|
476
|
+
frameRef.current = null;
|
|
477
|
+
const pending = pendingLayoutRef.current;
|
|
478
|
+
pendingLayoutRef.current = null;
|
|
479
|
+
if (pending !== null)
|
|
480
|
+
commitLayout(pending);
|
|
481
|
+
});
|
|
482
|
+
}, [commitLayout]);
|
|
483
|
+
useEffect(() => () => {
|
|
484
|
+
if (frameRef.current !== null)
|
|
485
|
+
cancelAnimationFrame(frameRef.current);
|
|
486
|
+
}, []);
|
|
487
|
+
const beginMove = useCallback((event) => {
|
|
488
|
+
if (compact || event.button !== 0 || event.target.closest('button') !== null)
|
|
489
|
+
return;
|
|
490
|
+
event.preventDefault();
|
|
491
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
492
|
+
gestureRef.current = {
|
|
493
|
+
kind: 'move',
|
|
494
|
+
pointerId: event.pointerId,
|
|
495
|
+
originX: event.clientX,
|
|
496
|
+
originY: event.clientY,
|
|
497
|
+
start: panelGeometryForGesture(),
|
|
498
|
+
activated: false,
|
|
499
|
+
};
|
|
500
|
+
}, [compact, panelGeometryForGesture]);
|
|
501
|
+
const beginResize = useCallback((edge, event) => {
|
|
502
|
+
if (compact || event.button !== 0 || (geometry.mode === 'docked' && edge !== 'left'))
|
|
503
|
+
return;
|
|
504
|
+
event.preventDefault();
|
|
505
|
+
event.stopPropagation();
|
|
506
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
507
|
+
gestureRef.current = {
|
|
508
|
+
kind: 'resize',
|
|
509
|
+
edge,
|
|
510
|
+
pointerId: event.pointerId,
|
|
511
|
+
originX: event.clientX,
|
|
512
|
+
originY: event.clientY,
|
|
513
|
+
start: panelGeometryForGesture(),
|
|
514
|
+
activated: true,
|
|
515
|
+
};
|
|
516
|
+
setInteraction('resizing');
|
|
517
|
+
}, [compact, geometry.mode, panelGeometryForGesture]);
|
|
518
|
+
const updateGesture = useCallback((event) => {
|
|
519
|
+
const gesture = gestureRef.current;
|
|
520
|
+
if (gesture === null || gesture.pointerId !== event.pointerId
|
|
521
|
+
|| !event.currentTarget.hasPointerCapture(event.pointerId))
|
|
522
|
+
return;
|
|
523
|
+
const dx = event.clientX - gesture.originX;
|
|
524
|
+
const dy = event.clientY - gesture.originY;
|
|
525
|
+
const activeBounds = boundsRef.current;
|
|
526
|
+
if (gesture.kind === 'move') {
|
|
527
|
+
if (!gesture.activated && Math.hypot(dx, dy) < MOVE_THRESHOLD)
|
|
528
|
+
return;
|
|
529
|
+
if (!gesture.activated) {
|
|
530
|
+
gesture.activated = true;
|
|
531
|
+
setInteraction('dragging');
|
|
532
|
+
}
|
|
533
|
+
scheduleLayout(movePanelLayout(floatPanelLayout(gesture.start, activeBounds), dx, dy, activeBounds));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
scheduleLayout(resizePanelLayout(gesture.start, gesture.edge ?? 'left', dx, dy, activeBounds));
|
|
537
|
+
}, [scheduleLayout]);
|
|
538
|
+
const endGesture = useCallback((event) => {
|
|
539
|
+
const gesture = gestureRef.current;
|
|
540
|
+
if (gesture === null || gesture.pointerId !== event.pointerId)
|
|
541
|
+
return;
|
|
542
|
+
updateGesture(event);
|
|
543
|
+
flushScheduledLayout();
|
|
544
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
545
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
546
|
+
}
|
|
547
|
+
gestureRef.current = null;
|
|
548
|
+
setInteraction(null);
|
|
549
|
+
}, [flushScheduledLayout, updateGesture]);
|
|
550
|
+
const cancelGesture = useCallback((event) => {
|
|
551
|
+
const gesture = gestureRef.current;
|
|
552
|
+
if (gesture === null || gesture.pointerId !== event.pointerId)
|
|
553
|
+
return;
|
|
554
|
+
flushScheduledLayout();
|
|
555
|
+
gestureRef.current = null;
|
|
556
|
+
setInteraction(null);
|
|
557
|
+
}, [flushScheduledLayout]);
|
|
558
|
+
const toggleDock = useCallback(() => {
|
|
559
|
+
const liveGeometry = panelGeometryForGesture();
|
|
560
|
+
commitLayout(liveGeometry.mode === 'docked'
|
|
561
|
+
? floatPanelLayout(liveGeometry, boundsRef.current)
|
|
562
|
+
: dockPanelLayout(liveGeometry, boundsRef.current));
|
|
563
|
+
}, [commitLayout, panelGeometryForGesture]);
|
|
564
|
+
const autoHeight = panelUsesAutoHeight(geometry, bounds);
|
|
565
|
+
const panelStyle = {
|
|
566
|
+
width: geometry.width,
|
|
567
|
+
height: autoHeight ? 'auto' : geometry.height,
|
|
568
|
+
maxHeight: panelMaximumHeight(geometry, bounds),
|
|
569
|
+
transform: `translate3d(${geometry.x}px, ${geometry.y}px, 0)`,
|
|
570
|
+
};
|
|
368
571
|
if (!hasTeams && !expanded)
|
|
369
572
|
return null;
|
|
370
573
|
return (_jsxs(_Fragment, { children: [!expanded && (_jsx(CollapsedBadge, { count: visibleCount, busy: busy, onClick: () => {
|
|
@@ -372,13 +575,13 @@ export function ActivityPanel({ sessionsList, openSession }) {
|
|
|
372
575
|
return;
|
|
373
576
|
setOpenOwner(current);
|
|
374
577
|
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.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
578
|
+
} })), 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: () => {
|
|
579
|
+
setOpen(false);
|
|
580
|
+
setOpenOwner(undefined);
|
|
581
|
+
}, "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
582
|
? _jsx("span", { className: css.emptyHint, children: "\u6682\u65E0\u56E2\u961F\u6D3B\u52A8" })
|
|
380
583
|
: (_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
584
|
const teamKey = `${owner}:${team.teamId}`;
|
|
382
585
|
return (_jsx(TeamSection, { team: historicCardTeam(team, owner), onNavigate: navigateToSession, historic: true }, teamKey));
|
|
383
|
-
})] })) })] }))] }));
|
|
586
|
+
})] })) }), !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
587
|
}
|
|
@@ -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
|
|
18
|
-
* when the conversation route changes. Ownership keeps an expanded
|
|
19
|
-
* from leaking onto the new-session screen (or another conversation)
|
|
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;
|
package/lib/client/index.js
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
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";
|
|
6
5
|
/** Required services: conversation nodes, slots, and sessions navigation. */
|
|
7
6
|
export const inject = ['conversationEvents', 'slots', 'sessions'];
|
|
7
|
+
/** The replayed user message is the canonical transcript entry. */
|
|
8
|
+
function HiddenAgentTeamsCommand() {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
8
11
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* for a closed floater or a re-opened session.
|
|
12
|
+
* Register the activity monitor in the shell's additive overlay and the
|
|
13
|
+
* in-conversation team card. The card's activity button re-opens a folded
|
|
14
|
+
* monitor via a window event — the recovery path for an old session.
|
|
13
15
|
*/
|
|
14
16
|
export function apply(ctx) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
const Panel = () => _jsx(ActivityPanel, { sessionsList: ctx.sessions.list, openSession: (id) => { ctx.sessions.open(id); } });
|
|
18
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
19
|
+
name: 'shell.overlay',
|
|
20
|
+
id: 'agent-teams-activity',
|
|
21
|
+
order: 80,
|
|
22
|
+
label: 'AgentTeams activity',
|
|
23
|
+
}, Panel));
|
|
24
|
+
// The host command is only the slash-menu/admission surface. Its input is
|
|
25
|
+
// replayed as the visible user message, so the generic result row would be
|
|
26
|
+
// a duplicate placed before that message by command lifecycle ordering.
|
|
27
|
+
ctx.slots.inject('conversation.chat.commandview', () => ctx.slots.register({
|
|
28
|
+
name: 'conversation.chat.commandview',
|
|
29
|
+
key: 'agent-teams',
|
|
30
|
+
}, HiddenAgentTeamsCommand));
|
|
24
31
|
ctx.conversationEvents.register(agentTeamsCardDefinition);
|
|
25
32
|
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
|
|
26
33
|
name: 'conversation.chat.node',
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** Pure persisted geometry rules for the AgentTeams shell-overlay panel. */
|
|
2
|
+
export const PANEL_LAYOUT_STORAGE_KEY = 'dsh-agent-teams:activity-panel:v1';
|
|
3
|
+
export const PANEL_COMPACT_BREAKPOINT = 960;
|
|
4
|
+
export const PANEL_DEFAULT_WIDTH = 388;
|
|
5
|
+
export const PANEL_DEFAULT_HEIGHT = 640;
|
|
6
|
+
export const PANEL_MIN_WIDTH = 320;
|
|
7
|
+
export const PANEL_MAX_WIDTH = 640;
|
|
8
|
+
export const PANEL_MIN_HEIGHT = 360;
|
|
9
|
+
export const PANEL_DOCK_TOP = 64;
|
|
10
|
+
export const PANEL_DOCK_RIGHT = 18;
|
|
11
|
+
export const PANEL_DOCK_BOTTOM = 48;
|
|
12
|
+
export const PANEL_FLOAT_MARGIN = 12;
|
|
13
|
+
export const DEFAULT_PANEL_LAYOUT = Object.freeze({
|
|
14
|
+
mode: 'docked',
|
|
15
|
+
x: 0,
|
|
16
|
+
y: PANEL_DOCK_TOP,
|
|
17
|
+
width: PANEL_DEFAULT_WIDTH,
|
|
18
|
+
height: PANEL_DEFAULT_HEIGHT,
|
|
19
|
+
heightMode: 'auto',
|
|
20
|
+
});
|
|
21
|
+
function clamp(value, minimum, maximum) {
|
|
22
|
+
return Math.min(Math.max(value, minimum), maximum);
|
|
23
|
+
}
|
|
24
|
+
function finite(value) {
|
|
25
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
26
|
+
}
|
|
27
|
+
/** Decode one versioned localStorage value, rejecting partial/corrupt state. */
|
|
28
|
+
export function parsePanelLayout(value) {
|
|
29
|
+
if (value === null)
|
|
30
|
+
return DEFAULT_PANEL_LAYOUT;
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(value);
|
|
33
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
34
|
+
return DEFAULT_PANEL_LAYOUT;
|
|
35
|
+
const record = parsed;
|
|
36
|
+
if ((record.mode !== 'docked' && record.mode !== 'floating')
|
|
37
|
+
|| !finite(record.x) || !finite(record.y)
|
|
38
|
+
|| !finite(record.width) || !finite(record.height)) {
|
|
39
|
+
return DEFAULT_PANEL_LAYOUT;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
mode: record.mode,
|
|
43
|
+
x: record.x,
|
|
44
|
+
y: record.y,
|
|
45
|
+
width: record.width,
|
|
46
|
+
height: record.height,
|
|
47
|
+
// v1 values written before content-fit height existed have no mode.
|
|
48
|
+
// Treat them as automatic so the upgrade removes legacy blank space.
|
|
49
|
+
heightMode: record.mode === 'floating' && record.heightMode === 'manual' ? 'manual' : 'auto',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return DEFAULT_PANEL_LAYOUT;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Whether the panel should become a simple inset overlay with no gestures. */
|
|
57
|
+
export function compactPanelForBounds(bounds) {
|
|
58
|
+
return bounds.width <= PANEL_COMPACT_BREAKPOINT;
|
|
59
|
+
}
|
|
60
|
+
/** Docked and compact panels always fit content; floating panels may be user-sized. */
|
|
61
|
+
export function panelUsesAutoHeight(layout, bounds) {
|
|
62
|
+
return compactPanelForBounds(bounds) || layout.mode === 'docked' || layout.heightMode === 'auto';
|
|
63
|
+
}
|
|
64
|
+
/** CSS max-height ceiling that keeps an auto-height panel inside its shell. */
|
|
65
|
+
export function panelMaximumHeight(layout, bounds) {
|
|
66
|
+
const bottomInset = compactPanelForBounds(bounds) || layout.mode === 'floating'
|
|
67
|
+
? PANEL_FLOAT_MARGIN
|
|
68
|
+
: PANEL_DOCK_BOTTOM;
|
|
69
|
+
return Math.max(1, bounds.height - layout.y - bottomInset);
|
|
70
|
+
}
|
|
71
|
+
/** Resolve persisted state into a visible rectangle inside the current shell. */
|
|
72
|
+
export function resolvePanelGeometry(layout, bounds) {
|
|
73
|
+
const boundsWidth = Math.max(1, bounds.width);
|
|
74
|
+
const boundsHeight = Math.max(1, bounds.height);
|
|
75
|
+
if (compactPanelForBounds(bounds)) {
|
|
76
|
+
return {
|
|
77
|
+
...layout,
|
|
78
|
+
x: PANEL_FLOAT_MARGIN,
|
|
79
|
+
y: PANEL_FLOAT_MARGIN,
|
|
80
|
+
width: Math.max(1, boundsWidth - PANEL_FLOAT_MARGIN * 2),
|
|
81
|
+
height: Math.max(1, boundsHeight - PANEL_FLOAT_MARGIN * 2),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const maximumWidth = Math.max(1, Math.min(PANEL_MAX_WIDTH, boundsWidth - PANEL_FLOAT_MARGIN * 2));
|
|
85
|
+
const minimumWidth = Math.min(PANEL_MIN_WIDTH, maximumWidth);
|
|
86
|
+
const width = clamp(layout.width, minimumWidth, maximumWidth);
|
|
87
|
+
const maximumHeight = Math.max(1, boundsHeight - PANEL_FLOAT_MARGIN * 2);
|
|
88
|
+
const minimumHeight = Math.min(PANEL_MIN_HEIGHT, maximumHeight);
|
|
89
|
+
if (layout.mode === 'docked') {
|
|
90
|
+
const y = clamp(PANEL_DOCK_TOP, PANEL_FLOAT_MARGIN, Math.max(PANEL_FLOAT_MARGIN, boundsHeight - minimumHeight - PANEL_FLOAT_MARGIN));
|
|
91
|
+
const availableHeight = Math.max(1, boundsHeight - y - PANEL_DOCK_BOTTOM);
|
|
92
|
+
const height = clamp(availableHeight, Math.min(minimumHeight, availableHeight), maximumHeight);
|
|
93
|
+
const anchorRight = clamp(bounds.anchorRight, 0, boundsWidth);
|
|
94
|
+
const maximumX = Math.max(PANEL_FLOAT_MARGIN, boundsWidth - width - PANEL_FLOAT_MARGIN);
|
|
95
|
+
const x = clamp(anchorRight - PANEL_DOCK_RIGHT - width, PANEL_FLOAT_MARGIN, maximumX);
|
|
96
|
+
return { mode: 'docked', x, y, width, height, heightMode: layout.heightMode };
|
|
97
|
+
}
|
|
98
|
+
const height = clamp(layout.height, minimumHeight, maximumHeight);
|
|
99
|
+
return {
|
|
100
|
+
mode: 'floating',
|
|
101
|
+
x: clamp(layout.x, PANEL_FLOAT_MARGIN, Math.max(PANEL_FLOAT_MARGIN, boundsWidth - width - PANEL_FLOAT_MARGIN)),
|
|
102
|
+
y: clamp(layout.y, PANEL_FLOAT_MARGIN, Math.max(PANEL_FLOAT_MARGIN, boundsHeight - height - PANEL_FLOAT_MARGIN)),
|
|
103
|
+
width,
|
|
104
|
+
height,
|
|
105
|
+
heightMode: layout.heightMode,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Undock without a visual jump by adopting the panel's resolved rectangle. */
|
|
109
|
+
export function floatPanelLayout(geometry, bounds) {
|
|
110
|
+
return resolvePanelGeometry({ ...geometry, mode: 'floating' }, bounds);
|
|
111
|
+
}
|
|
112
|
+
/** Return to the right dock, preserving width and restoring content-fit height. */
|
|
113
|
+
export function dockPanelLayout(layout, bounds) {
|
|
114
|
+
return resolvePanelGeometry({ ...layout, mode: 'docked', heightMode: 'auto' }, bounds);
|
|
115
|
+
}
|
|
116
|
+
/** Translate a floating panel and clamp it back into the visible shell. */
|
|
117
|
+
export function movePanelLayout(start, dx, dy, bounds) {
|
|
118
|
+
return resolvePanelGeometry({ ...start, mode: 'floating', x: start.x + dx, y: start.y + dy }, bounds);
|
|
119
|
+
}
|
|
120
|
+
/** Resize while keeping the edge opposite the active handle stationary. */
|
|
121
|
+
export function resizePanelLayout(start, edge, dx, dy, bounds) {
|
|
122
|
+
if (start.mode === 'docked') {
|
|
123
|
+
if (edge !== 'left')
|
|
124
|
+
return resolvePanelGeometry(start, bounds);
|
|
125
|
+
return resolvePanelGeometry({ ...start, width: start.width - dx }, bounds);
|
|
126
|
+
}
|
|
127
|
+
const resolved = resolvePanelGeometry(start, bounds);
|
|
128
|
+
const minimumWidth = Math.min(PANEL_MIN_WIDTH, resolved.x + resolved.width - PANEL_FLOAT_MARGIN);
|
|
129
|
+
const minimumHeight = Math.min(PANEL_MIN_HEIGHT, bounds.height - resolved.y - PANEL_FLOAT_MARGIN);
|
|
130
|
+
if (edge === 'left') {
|
|
131
|
+
const right = resolved.x + resolved.width;
|
|
132
|
+
const maximumWidth = Math.max(1, Math.min(PANEL_MAX_WIDTH, right - PANEL_FLOAT_MARGIN));
|
|
133
|
+
const width = clamp(resolved.width - dx, Math.min(minimumWidth, maximumWidth), maximumWidth);
|
|
134
|
+
return { ...resolved, x: right - width, width };
|
|
135
|
+
}
|
|
136
|
+
const maximumHeight = Math.max(1, bounds.height - resolved.y - PANEL_FLOAT_MARGIN);
|
|
137
|
+
const height = clamp(resolved.height + dy, Math.min(minimumHeight, maximumHeight), maximumHeight);
|
|
138
|
+
if (edge === 'bottom')
|
|
139
|
+
return { ...resolved, height, heightMode: 'manual' };
|
|
140
|
+
const maximumWidth = Math.max(1, Math.min(PANEL_MAX_WIDTH, bounds.width - resolved.x - PANEL_FLOAT_MARGIN));
|
|
141
|
+
const width = clamp(resolved.width + dx, Math.min(minimumWidth, maximumWidth), maximumWidth);
|
|
142
|
+
return { ...resolved, width, height, heightMode: 'manual' };
|
|
143
|
+
}
|