@agent-native/core 0.45.1 → 0.46.0
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 +1 -0
- package/dist/client/components/LiveCursorOverlay.d.ts +46 -0
- package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -0
- package/dist/client/components/LiveCursorOverlay.js +137 -0
- package/dist/client/components/LiveCursorOverlay.js.map +1 -0
- package/dist/client/components/PresenceBar.d.ts +11 -1
- package/dist/client/components/PresenceBar.d.ts.map +1 -1
- package/dist/client/components/PresenceBar.js +39 -7
- package/dist/client/components/PresenceBar.js.map +1 -1
- package/dist/client/components/RemoteSelectionRings.d.ts +43 -0
- package/dist/client/components/RemoteSelectionRings.d.ts.map +1 -0
- package/dist/client/components/RemoteSelectionRings.js +116 -0
- package/dist/client/components/RemoteSelectionRings.js.map +1 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -1
- package/dist/collab/awareness.d.ts +25 -0
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/awareness.js +42 -5
- package/dist/collab/awareness.js.map +1 -1
- package/dist/collab/client.d.ts +19 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +362 -57
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/follow-mode.d.ts +56 -0
- package/dist/collab/follow-mode.d.ts.map +1 -0
- package/dist/collab/follow-mode.js +54 -0
- package/dist/collab/follow-mode.js.map +1 -0
- package/dist/collab/index.d.ts +3 -1
- package/dist/collab/index.d.ts.map +1 -1
- package/dist/collab/index.js +5 -1
- package/dist/collab/index.js.map +1 -1
- package/dist/collab/presence.d.ts +56 -0
- package/dist/collab/presence.d.ts.map +1 -0
- package/dist/collab/presence.js +98 -0
- package/dist/collab/presence.js.map +1 -0
- package/dist/collab/routes.d.ts.map +1 -1
- package/dist/collab/routes.js +33 -6
- package/dist/collab/routes.js.map +1 -1
- package/dist/collab/struct-routes.d.ts.map +1 -1
- package/dist/collab/struct-routes.js +24 -4
- package/dist/collab/struct-routes.js.map +1 -1
- package/dist/collab/ydoc-manager.d.ts +13 -0
- package/dist/collab/ydoc-manager.d.ts.map +1 -1
- package/dist/collab/ydoc-manager.js +51 -15
- package/dist/collab/ydoc-manager.js.map +1 -1
- package/dist/server/collab-plugin.d.ts +6 -0
- package/dist/server/collab-plugin.d.ts.map +1 -1
- package/dist/server/collab-plugin.js +105 -5
- package/dist/server/collab-plugin.js.map +1 -1
- package/dist/server/poll-events.d.ts +5 -0
- package/dist/server/poll-events.d.ts.map +1 -1
- package/dist/server/poll-events.js +27 -4
- package/dist/server/poll-events.js.map +1 -1
- package/dist/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
- package/dist/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
- package/docs/content/real-time-collaboration.md +481 -97
- package/package.json +1 -1
- package/src/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
- package/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ The agent and the UI are equal citizens of the same system. Every action works b
|
|
|
11
11
|

|
|
12
12
|
|
|
13
13
|
- **Everything syncs** — Agent and UI share one database and one state. Changes from either side show up instantly on the other.
|
|
14
|
+
- **Real-time multiplayer** — Humans and agents collaborate in the same document simultaneously: CRDT merging, live presence (cursors, selection rings, who's on which slide), and the agent as a first-class peer editor. Works on any SQL database and any host, including serverless.
|
|
14
15
|
- **Context-aware** — The agent knows what you're looking at. Select text, hit Cmd+I, and tell it what to do.
|
|
15
16
|
- **Per-user workspace** — Skills, memory, instructions, sub-agents, and MCP servers — SQL-backed, customizable per user. Claude-Code-level flexibility, SaaS-grade economics.
|
|
16
17
|
- **Agents call agents** — Tag another agent from any app. They discover each other over A2A and take action across your stack.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiveCursorOverlay — renders remote users' cursors over an absolutely-
|
|
3
|
+
* positioned container.
|
|
4
|
+
*
|
|
5
|
+
* Cursor positions are expected as normalized coordinates (0–1 relative to
|
|
6
|
+
* the container's content size) so different zoom/scroll positions map
|
|
7
|
+
* correctly. Pass a `mapCoords` prop to handle non-identity transforms
|
|
8
|
+
* (e.g. a zoomed canvas).
|
|
9
|
+
*
|
|
10
|
+
* The agent's cursor is styled distinctly with a sparkle variant + "AI"
|
|
11
|
+
* label, consistent with AgentPresenceChip.
|
|
12
|
+
*
|
|
13
|
+
* Cursors fade out after 10 seconds of no movement.
|
|
14
|
+
*/
|
|
15
|
+
import type { OtherPresence, NormalizedPoint } from "../../collab/presence.js";
|
|
16
|
+
export interface CursorMapFn {
|
|
17
|
+
/** Convert normalized coords to pixel offsets within the overlay container. */
|
|
18
|
+
(norm: NormalizedPoint): {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface LiveCursorOverlayProps {
|
|
24
|
+
/** Remote participants with presence payload. */
|
|
25
|
+
others: OtherPresence[];
|
|
26
|
+
/**
|
|
27
|
+
* Key inside presence payload that carries the cursor position.
|
|
28
|
+
* Default: "cursor"
|
|
29
|
+
* Expected shape: { x: number; y: number } (normalized 0–1).
|
|
30
|
+
*/
|
|
31
|
+
cursorKey?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Override coordinate mapping. Default: scale by container clientWidth/Height.
|
|
34
|
+
* Pass this when the container uses transform: scale() or has virtual scroll.
|
|
35
|
+
*/
|
|
36
|
+
mapCoords?: CursorMapFn;
|
|
37
|
+
/**
|
|
38
|
+
* Container element ref. Required when mapCoords is not provided —
|
|
39
|
+
* used to compute pixel positions from normalized coords.
|
|
40
|
+
*/
|
|
41
|
+
containerRef?: React.RefObject<HTMLElement | null>;
|
|
42
|
+
/** Additional CSS class for the overlay div. */
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare function LiveCursorOverlay({ others, cursorKey, mapCoords, containerRef, className, }: LiveCursorOverlayProps): import("react/jsx-runtime").JSX.Element;
|
|
46
|
+
//# sourceMappingURL=LiveCursorOverlay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LiveCursorOverlay.d.ts","sourceRoot":"","sources":["../../../src/client/components/LiveCursorOverlay.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAG/E,MAAM,WAAW,WAAW;IAC1B,+EAA+E;IAC/E,CAAC,IAAI,EAAE,eAAe,GAAG;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAED,MAAM,WAAW,sBAAsB;IACrC,iDAAiD;IACjD,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACnD,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAsHD,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,SAAoB,EACpB,SAAS,EACT,YAAY,EACZ,SAAS,GACV,EAAE,sBAAsB,2CAuFxB"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* LiveCursorOverlay — renders remote users' cursors over an absolutely-
|
|
4
|
+
* positioned container.
|
|
5
|
+
*
|
|
6
|
+
* Cursor positions are expected as normalized coordinates (0–1 relative to
|
|
7
|
+
* the container's content size) so different zoom/scroll positions map
|
|
8
|
+
* correctly. Pass a `mapCoords` prop to handle non-identity transforms
|
|
9
|
+
* (e.g. a zoomed canvas).
|
|
10
|
+
*
|
|
11
|
+
* The agent's cursor is styled distinctly with a sparkle variant + "AI"
|
|
12
|
+
* label, consistent with AgentPresenceChip.
|
|
13
|
+
*
|
|
14
|
+
* Cursors fade out after 10 seconds of no movement.
|
|
15
|
+
*/
|
|
16
|
+
import { useState, useEffect, useRef, memo } from "react";
|
|
17
|
+
import { IconSparkles } from "@tabler/icons-react";
|
|
18
|
+
const STALE_MS = 10_000; // Fade out cursors older than 10s
|
|
19
|
+
const CURSOR_SVG = `
|
|
20
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="22" viewBox="0 0 16 22" fill="none">
|
|
21
|
+
<path d="M0 0L0 18L4.5 13.5L7.5 20L9.5 19.5L6.5 13L12 13L0 0Z" fill="__COLOR__" stroke="white" stroke-width="1"/>
|
|
22
|
+
</svg>
|
|
23
|
+
`.trim();
|
|
24
|
+
function CursorPointer({ color, isAgent, }) {
|
|
25
|
+
if (isAgent) {
|
|
26
|
+
return (_jsx("div", { style: {
|
|
27
|
+
display: "flex",
|
|
28
|
+
alignItems: "center",
|
|
29
|
+
justifyContent: "center",
|
|
30
|
+
width: 22,
|
|
31
|
+
height: 22,
|
|
32
|
+
borderRadius: "50%",
|
|
33
|
+
backgroundColor: color,
|
|
34
|
+
color: "#fff",
|
|
35
|
+
boxShadow: `0 0 0 2px #fff`,
|
|
36
|
+
}, children: _jsx(IconSparkles, { size: 12, stroke: 2 }) }));
|
|
37
|
+
}
|
|
38
|
+
const svgSrc = "data:image/svg+xml;charset=utf-8," +
|
|
39
|
+
encodeURIComponent(CURSOR_SVG.replace("__COLOR__", color));
|
|
40
|
+
return (_jsx("img", { src: svgSrc, alt: "", "aria-hidden": true, style: { width: 16, height: 22, display: "block", flexShrink: 0 } }));
|
|
41
|
+
}
|
|
42
|
+
const CursorLabel = memo(function CursorLabel({ entry, }) {
|
|
43
|
+
const { other, x, y } = entry;
|
|
44
|
+
const color = other.user.color || "#94a3b8";
|
|
45
|
+
const label = other.isAgent ? "AI" : other.user.name || other.user.email;
|
|
46
|
+
return (_jsxs("div", { "aria-label": `${label} cursor`, style: {
|
|
47
|
+
position: "absolute",
|
|
48
|
+
left: x,
|
|
49
|
+
top: y,
|
|
50
|
+
pointerEvents: "none",
|
|
51
|
+
userSelect: "none",
|
|
52
|
+
transform: "translate(-2px, -2px)",
|
|
53
|
+
zIndex: 9999,
|
|
54
|
+
transition: "left 120ms linear, top 120ms linear",
|
|
55
|
+
display: "flex",
|
|
56
|
+
flexDirection: "column",
|
|
57
|
+
alignItems: "flex-start",
|
|
58
|
+
gap: 2,
|
|
59
|
+
}, children: [_jsx(CursorPointer, { color: color, isAgent: other.isAgent }), _jsxs("div", { style: {
|
|
60
|
+
backgroundColor: color,
|
|
61
|
+
color: "#fff",
|
|
62
|
+
fontSize: 11,
|
|
63
|
+
fontWeight: 600,
|
|
64
|
+
padding: "1px 5px",
|
|
65
|
+
borderRadius: 4,
|
|
66
|
+
whiteSpace: "nowrap",
|
|
67
|
+
marginLeft: other.isAgent ? 0 : 4,
|
|
68
|
+
boxShadow: "0 1px 3px rgba(0,0,0,0.25)",
|
|
69
|
+
maxWidth: 120,
|
|
70
|
+
overflow: "hidden",
|
|
71
|
+
textOverflow: "ellipsis",
|
|
72
|
+
display: "flex",
|
|
73
|
+
alignItems: "center",
|
|
74
|
+
gap: 3,
|
|
75
|
+
}, children: [other.isAgent && (_jsx(IconSparkles, { size: 9, stroke: 2, style: { flexShrink: 0, opacity: 0.85 } })), label] })] }));
|
|
76
|
+
});
|
|
77
|
+
export function LiveCursorOverlay({ others, cursorKey = "cursor", mapCoords, containerRef, className, }) {
|
|
78
|
+
const overlayRef = useRef(null);
|
|
79
|
+
const [, tick] = useState(0); // Force re-render to prune stale cursors
|
|
80
|
+
const entriesRef = useRef(new Map());
|
|
81
|
+
// Tick every 5s to prune stale cursors (no re-render storm).
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const id = setInterval(() => tick((n) => n + 1), 5_000);
|
|
84
|
+
return () => clearInterval(id);
|
|
85
|
+
}, []);
|
|
86
|
+
// Build entries from others, computing pixel positions.
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const visible = [];
|
|
89
|
+
for (const other of others) {
|
|
90
|
+
const pos = other.presence[cursorKey];
|
|
91
|
+
if (!pos || typeof pos.x !== "number" || typeof pos.y !== "number")
|
|
92
|
+
continue;
|
|
93
|
+
// Compute pixel position.
|
|
94
|
+
let px;
|
|
95
|
+
let py;
|
|
96
|
+
if (mapCoords) {
|
|
97
|
+
const mapped = mapCoords(pos);
|
|
98
|
+
px = mapped.x;
|
|
99
|
+
py = mapped.y;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const container = containerRef?.current ?? overlayRef.current?.parentElement;
|
|
103
|
+
const w = container ? container.clientWidth : 0;
|
|
104
|
+
const h = container ? container.clientHeight : 0;
|
|
105
|
+
px = pos.x * w;
|
|
106
|
+
py = pos.y * h;
|
|
107
|
+
}
|
|
108
|
+
const prev = entriesRef.current.get(other.clientId);
|
|
109
|
+
const lastSeen = prev && prev.x === px && prev.y === py ? prev.lastSeen : now;
|
|
110
|
+
const entry = { other, x: px, y: py, lastSeen };
|
|
111
|
+
entriesRef.current.set(other.clientId, entry);
|
|
112
|
+
if (now - lastSeen < STALE_MS) {
|
|
113
|
+
visible.push(entry);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Remove entries for participants who left.
|
|
117
|
+
for (const clientId of entriesRef.current.keys()) {
|
|
118
|
+
if (!others.find((o) => o.clientId === clientId)) {
|
|
119
|
+
entriesRef.current.delete(clientId);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (visible.length === 0) {
|
|
123
|
+
return (_jsx("div", { ref: overlayRef, "aria-hidden": true, style: {
|
|
124
|
+
position: "absolute",
|
|
125
|
+
inset: 0,
|
|
126
|
+
pointerEvents: "none",
|
|
127
|
+
overflow: "hidden",
|
|
128
|
+
}, className: className }));
|
|
129
|
+
}
|
|
130
|
+
return (_jsx("div", { ref: overlayRef, "aria-hidden": true, style: {
|
|
131
|
+
position: "absolute",
|
|
132
|
+
inset: 0,
|
|
133
|
+
pointerEvents: "none",
|
|
134
|
+
overflow: "hidden",
|
|
135
|
+
}, className: className, children: visible.map((entry) => (_jsx(CursorLabel, { entry: entry }, entry.other.clientId))) }));
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=LiveCursorOverlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LiveCursorOverlay.js","sourceRoot":"","sources":["../../../src/client/components/LiveCursorOverlay.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * LiveCursorOverlay — renders remote users' cursors over an absolutely-\n * positioned container.\n *\n * Cursor positions are expected as normalized coordinates (0–1 relative to\n * the container's content size) so different zoom/scroll positions map\n * correctly. Pass a `mapCoords` prop to handle non-identity transforms\n * (e.g. a zoomed canvas).\n *\n * The agent's cursor is styled distinctly with a sparkle variant + \"AI\"\n * label, consistent with AgentPresenceChip.\n *\n * Cursors fade out after 10 seconds of no movement.\n */\n\nimport { useState, useEffect, useRef, memo } from \"react\";\nimport { IconSparkles } from \"@tabler/icons-react\";\nimport type { OtherPresence, NormalizedPoint } from \"../../collab/presence.js\";\nimport { AGENT_CLIENT_ID } from \"../../collab/agent-identity.js\";\n\nexport interface CursorMapFn {\n /** Convert normalized coords to pixel offsets within the overlay container. */\n (norm: NormalizedPoint): { x: number; y: number };\n}\n\nexport interface LiveCursorOverlayProps {\n /** Remote participants with presence payload. */\n others: OtherPresence[];\n /**\n * Key inside presence payload that carries the cursor position.\n * Default: \"cursor\"\n * Expected shape: { x: number; y: number } (normalized 0–1).\n */\n cursorKey?: string;\n /**\n * Override coordinate mapping. Default: scale by container clientWidth/Height.\n * Pass this when the container uses transform: scale() or has virtual scroll.\n */\n mapCoords?: CursorMapFn;\n /**\n * Container element ref. Required when mapCoords is not provided —\n * used to compute pixel positions from normalized coords.\n */\n containerRef?: React.RefObject<HTMLElement | null>;\n /** Additional CSS class for the overlay div. */\n className?: string;\n}\n\nconst STALE_MS = 10_000; // Fade out cursors older than 10s\n\nconst CURSOR_SVG = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"22\" viewBox=\"0 0 16 22\" fill=\"none\">\n <path d=\"M0 0L0 18L4.5 13.5L7.5 20L9.5 19.5L6.5 13L12 13L0 0Z\" fill=\"__COLOR__\" stroke=\"white\" stroke-width=\"1\"/>\n </svg>\n`.trim();\n\nfunction CursorPointer({\n color,\n isAgent,\n}: {\n color: string;\n isAgent: boolean;\n}) {\n if (isAgent) {\n return (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 22,\n height: 22,\n borderRadius: \"50%\",\n backgroundColor: color,\n color: \"#fff\",\n boxShadow: `0 0 0 2px #fff`,\n }}\n >\n <IconSparkles size={12} stroke={2} />\n </div>\n );\n }\n\n const svgSrc =\n \"data:image/svg+xml;charset=utf-8,\" +\n encodeURIComponent(CURSOR_SVG.replace(\"__COLOR__\", color));\n\n return (\n <img\n src={svgSrc}\n alt=\"\"\n aria-hidden\n style={{ width: 16, height: 22, display: \"block\", flexShrink: 0 }}\n />\n );\n}\n\ninterface CursorEntry {\n other: OtherPresence;\n x: number;\n y: number;\n lastSeen: number;\n}\n\nconst CursorLabel = memo(function CursorLabel({\n entry,\n}: {\n entry: CursorEntry;\n}) {\n const { other, x, y } = entry;\n const color = other.user.color || \"#94a3b8\";\n const label = other.isAgent ? \"AI\" : other.user.name || other.user.email;\n\n return (\n <div\n aria-label={`${label} cursor`}\n style={{\n position: \"absolute\",\n left: x,\n top: y,\n pointerEvents: \"none\",\n userSelect: \"none\",\n transform: \"translate(-2px, -2px)\",\n zIndex: 9999,\n transition: \"left 120ms linear, top 120ms linear\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"flex-start\",\n gap: 2,\n }}\n >\n <CursorPointer color={color} isAgent={other.isAgent} />\n <div\n style={{\n backgroundColor: color,\n color: \"#fff\",\n fontSize: 11,\n fontWeight: 600,\n padding: \"1px 5px\",\n borderRadius: 4,\n whiteSpace: \"nowrap\",\n marginLeft: other.isAgent ? 0 : 4,\n boxShadow: \"0 1px 3px rgba(0,0,0,0.25)\",\n maxWidth: 120,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n display: \"flex\",\n alignItems: \"center\",\n gap: 3,\n }}\n >\n {other.isAgent && (\n <IconSparkles\n size={9}\n stroke={2}\n style={{ flexShrink: 0, opacity: 0.85 }}\n />\n )}\n {label}\n </div>\n </div>\n );\n});\n\nexport function LiveCursorOverlay({\n others,\n cursorKey = \"cursor\",\n mapCoords,\n containerRef,\n className,\n}: LiveCursorOverlayProps) {\n const overlayRef = useRef<HTMLDivElement>(null);\n const [, tick] = useState(0); // Force re-render to prune stale cursors\n const entriesRef = useRef<Map<number, CursorEntry>>(new Map());\n\n // Tick every 5s to prune stale cursors (no re-render storm).\n useEffect(() => {\n const id = setInterval(() => tick((n) => n + 1), 5_000);\n return () => clearInterval(id);\n }, []);\n\n // Build entries from others, computing pixel positions.\n const now = Date.now();\n const visible: CursorEntry[] = [];\n\n for (const other of others) {\n const pos = other.presence[cursorKey] as NormalizedPoint | undefined;\n if (!pos || typeof pos.x !== \"number\" || typeof pos.y !== \"number\")\n continue;\n\n // Compute pixel position.\n let px: number;\n let py: number;\n if (mapCoords) {\n const mapped = mapCoords(pos);\n px = mapped.x;\n py = mapped.y;\n } else {\n const container =\n containerRef?.current ?? overlayRef.current?.parentElement;\n const w = container ? container.clientWidth : 0;\n const h = container ? container.clientHeight : 0;\n px = pos.x * w;\n py = pos.y * h;\n }\n\n const prev = entriesRef.current.get(other.clientId);\n const lastSeen =\n prev && prev.x === px && prev.y === py ? prev.lastSeen : now;\n const entry: CursorEntry = { other, x: px, y: py, lastSeen };\n entriesRef.current.set(other.clientId, entry);\n\n if (now - lastSeen < STALE_MS) {\n visible.push(entry);\n }\n }\n\n // Remove entries for participants who left.\n for (const clientId of entriesRef.current.keys()) {\n if (!others.find((o) => o.clientId === clientId)) {\n entriesRef.current.delete(clientId);\n }\n }\n\n if (visible.length === 0) {\n return (\n <div\n ref={overlayRef}\n aria-hidden\n style={{\n position: \"absolute\",\n inset: 0,\n pointerEvents: \"none\",\n overflow: \"hidden\",\n }}\n className={className}\n />\n );\n }\n\n return (\n <div\n ref={overlayRef}\n aria-hidden\n style={{\n position: \"absolute\",\n inset: 0,\n pointerEvents: \"none\",\n overflow: \"hidden\",\n }}\n className={className}\n >\n {visible.map((entry) => (\n <CursorLabel key={entry.other.clientId} entry={entry} />\n ))}\n </div>\n );\n}\n"]}
|
|
@@ -12,6 +12,16 @@ export interface PresenceBarProps {
|
|
|
12
12
|
maxVisible?: number;
|
|
13
13
|
/** Additional CSS classes. */
|
|
14
14
|
className?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Called when an avatar is clicked. Receives the user being clicked
|
|
17
|
+
* (or null for the agent avatar). Use this to start/stop follow mode.
|
|
18
|
+
*/
|
|
19
|
+
onAvatarClick?: (user: CollabUser | null) => void;
|
|
20
|
+
/**
|
|
21
|
+
* The email of the user currently being followed. Highlighted with a
|
|
22
|
+
* blue ring to indicate active follow mode.
|
|
23
|
+
*/
|
|
24
|
+
followingEmail?: string | null;
|
|
15
25
|
}
|
|
16
|
-
export declare function PresenceBar({ activeUsers, agentPresent, agentActive, currentUserEmail, maxVisible, className, }: PresenceBarProps): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export declare function PresenceBar({ activeUsers, agentPresent, agentActive, currentUserEmail, maxVisible, className, onAvatarClick, followingEmail, }: PresenceBarProps): import("react/jsx-runtime").JSX.Element;
|
|
17
27
|
//# sourceMappingURL=PresenceBar.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PresenceBar.d.ts","sourceRoot":"","sources":["../../../src/client/components/PresenceBar.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,UAAU,EAIhB,MAAM,wBAAwB,CAAC;AAQhC,MAAM,WAAW,gBAAgB;IAC/B,6CAA6C;IAC7C,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,sDAAsD;IACtD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8BAA8B;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"PresenceBar.d.ts","sourceRoot":"","sources":["../../../src/client/components/PresenceBar.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,UAAU,EAIhB,MAAM,wBAAwB,CAAC;AAQhC,MAAM,WAAW,gBAAgB;IAC/B,6CAA6C;IAC7C,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,sDAAsD;IACtD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8BAA8B;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAyND,wBAAgB,WAAW,CAAC,EAC1B,WAAW,EACX,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,UAAc,EACd,SAAS,EACT,aAAa,EACb,cAAc,GACf,EAAE,gBAAgB,2CA+DlB"}
|
|
@@ -43,7 +43,7 @@ function injectStyles() {
|
|
|
43
43
|
document.head.appendChild(style);
|
|
44
44
|
styleInjected = true;
|
|
45
45
|
}
|
|
46
|
-
function UserAvatar({ user, isFirst }) {
|
|
46
|
+
function UserAvatar({ user, isFirst, onClick, isFollowing, }) {
|
|
47
47
|
const color = user.color || emailToColor(user.email);
|
|
48
48
|
const name = user.name || emailToName(user.email);
|
|
49
49
|
const initial = name.charAt(0).toUpperCase();
|
|
@@ -51,9 +51,16 @@ function UserAvatar({ user, isFirst }) {
|
|
|
51
51
|
...baseAvatarStyle,
|
|
52
52
|
backgroundColor: color,
|
|
53
53
|
marginLeft: isFirst ? 0 : OVERLAP,
|
|
54
|
-
|
|
54
|
+
cursor: onClick ? "pointer" : "default",
|
|
55
|
+
boxShadow: isFollowing
|
|
56
|
+
? `0 0 0 2px #3b82f6, 0 0 0 4px #fff`
|
|
57
|
+
: `0 0 0 2px #fff`,
|
|
58
|
+
}, "aria-label": `${name} (${user.email})${isFollowing ? " — following" : ""}`, tabIndex: 0, onClick: onClick, onKeyDown: (e) => {
|
|
59
|
+
if (e.key === "Enter" || e.key === " ")
|
|
60
|
+
onClick?.();
|
|
61
|
+
}, children: initial }) }), _jsx(TooltipContent, { side: "bottom", children: isFollowing ? `Following ${name} — click to stop` : user.email })] }));
|
|
55
62
|
}
|
|
56
|
-
function AgentAvatar({ active }) {
|
|
63
|
+
function AgentAvatar({ active, onClick, isFollowing, }) {
|
|
57
64
|
injectStyles();
|
|
58
65
|
return (_jsxs("div", { style: {
|
|
59
66
|
display: "flex",
|
|
@@ -64,7 +71,29 @@ function AgentAvatar({ active }) {
|
|
|
64
71
|
backgroundColor: AGENT_COLOR,
|
|
65
72
|
marginLeft: 0,
|
|
66
73
|
animation: active ? "_anPresencePulse 2s infinite" : undefined,
|
|
67
|
-
|
|
74
|
+
cursor: onClick ? "pointer" : "default",
|
|
75
|
+
boxShadow: isFollowing
|
|
76
|
+
? `0 0 0 2px #3b82f6, 0 0 0 4px #fff`
|
|
77
|
+
: undefined,
|
|
78
|
+
}, title: isFollowing
|
|
79
|
+
? "Following AI — click to stop"
|
|
80
|
+
: active
|
|
81
|
+
? "AI is editing"
|
|
82
|
+
: "AI agent", onClick: onClick, tabIndex: onClick ? 0 : undefined, onKeyDown: (e) => {
|
|
83
|
+
if (e.key === "Enter" || e.key === " ")
|
|
84
|
+
onClick?.();
|
|
85
|
+
}, role: onClick ? "button" : undefined, children: "A" }), active && !isFollowing && _jsx(AgentEditingChip, {}), isFollowing && (_jsx("span", { style: {
|
|
86
|
+
display: "inline-flex",
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
height: 20,
|
|
89
|
+
padding: "0 8px",
|
|
90
|
+
borderRadius: 9999,
|
|
91
|
+
backgroundColor: `#3b82f620`,
|
|
92
|
+
color: "#3b82f6",
|
|
93
|
+
fontSize: 11,
|
|
94
|
+
fontWeight: 600,
|
|
95
|
+
whiteSpace: "nowrap",
|
|
96
|
+
}, children: "Following AI" }))] }));
|
|
68
97
|
}
|
|
69
98
|
function AgentEditingChip() {
|
|
70
99
|
return (_jsxs("span", { style: {
|
|
@@ -97,7 +126,7 @@ function OverflowBadge({ count, isFirst, }) {
|
|
|
97
126
|
fontSize: 10,
|
|
98
127
|
}, title: `${count} more collaborator${count === 1 ? "" : "s"}`, children: ["+", count] }));
|
|
99
128
|
}
|
|
100
|
-
export function PresenceBar({ activeUsers, agentPresent, agentActive, currentUserEmail, maxVisible = 5, className, }) {
|
|
129
|
+
export function PresenceBar({ activeUsers, agentPresent, agentActive, currentUserEmail, maxVisible = 5, className, onAvatarClick, followingEmail, }) {
|
|
101
130
|
const { humanUsers, showAgent } = useMemo(() => {
|
|
102
131
|
const currentEmail = currentUserEmail?.trim().toLowerCase();
|
|
103
132
|
const uniqueUsers = dedupeCollabUsersByEmail(activeUsers);
|
|
@@ -115,10 +144,13 @@ export function PresenceBar({ activeUsers, agentPresent, agentActive, currentUse
|
|
|
115
144
|
const overflowCount = humanUsers.length - visibleUsers.length;
|
|
116
145
|
if (!showAgent && humanUsers.length === 0)
|
|
117
146
|
return null;
|
|
118
|
-
|
|
147
|
+
const followingLower = followingEmail?.trim().toLowerCase() ?? null;
|
|
148
|
+
const isFollowingAgent = followingLower === "agent@system";
|
|
149
|
+
return (_jsx(TooltipProvider, { delayDuration: 150, children: _jsxs("div", { style: containerStyle, className: className, children: [showAgent && (_jsx(AgentAvatar, { active: !!agentActive, onClick: onAvatarClick ? () => onAvatarClick(null) : undefined, isFollowing: isFollowingAgent })), visibleUsers.length > 0 && (_jsxs("div", { style: {
|
|
119
150
|
display: "flex",
|
|
120
151
|
alignItems: "center",
|
|
121
152
|
marginLeft: showAgent ? 6 : 0,
|
|
122
|
-
}, children: [visibleUsers.map((u, i) => (_jsx(UserAvatar, { user: u, isFirst: i === 0
|
|
153
|
+
}, children: [visibleUsers.map((u, i) => (_jsx(UserAvatar, { user: u, isFirst: i === 0, onClick: onAvatarClick ? () => onAvatarClick(u) : undefined, isFollowing: followingLower != null &&
|
|
154
|
+
u.email.trim().toLowerCase() === followingLower }, u.email))), overflowCount > 0 && (_jsx(OverflowBadge, { count: overflowCount, isFirst: false }))] }))] }) }));
|
|
123
155
|
}
|
|
124
156
|
//# sourceMappingURL=PresenceBar.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PresenceBar.js","sourceRoot":"","sources":["../../../src/client/components/PresenceBar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from \"react\";\nimport {\n type CollabUser,\n dedupeCollabUsersByEmail,\n emailToColor,\n emailToName,\n} from \"../../collab/client.js\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"./ui/tooltip.js\";\n\nexport interface PresenceBarProps {\n /** Active collaborators on this document. */\n activeUsers: CollabUser[];\n /** Whether the agent has a durable presence entry. */\n agentPresent?: boolean;\n /** Whether the agent is actively making edits right now. */\n agentActive?: boolean;\n /** Current user's email (to exclude from the list). */\n currentUserEmail?: string;\n /** Max visible avatars before \"+N\" overflow. Default: 5 */\n maxVisible?: number;\n /** Additional CSS classes. */\n className?: string;\n}\n\nconst AVATAR_SIZE = 28;\nconst OVERLAP = -8;\nconst BORDER_WIDTH = 2;\nconst FONT_SIZE = 12;\nconst AGENT_COLOR = \"#00B5FF\";\n\nconst baseAvatarStyle: React.CSSProperties = {\n width: AVATAR_SIZE,\n height: AVATAR_SIZE,\n borderRadius: \"50%\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n fontSize: FONT_SIZE,\n fontWeight: 700,\n color: \"#fff\",\n border: `${BORDER_WIDTH}px solid #fff`,\n flexShrink: 0,\n position: \"relative\",\n cursor: \"default\",\n boxSizing: \"border-box\",\n};\n\nconst containerStyle: React.CSSProperties = {\n display: \"flex\",\n alignItems: \"center\",\n flexDirection: \"row\",\n};\n\nconst pulseKeyframes = `\n@keyframes _anPresencePulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.6; }\n}\n`;\n\nlet styleInjected = false;\n\nfunction injectStyles() {\n if (styleInjected || typeof document === \"undefined\") return;\n const style = document.createElement(\"style\");\n style.textContent = pulseKeyframes;\n document.head.appendChild(style);\n styleInjected = true;\n}\n\nfunction UserAvatar({ user, isFirst }: { user: CollabUser; isFirst: boolean }) {\n const color = user.color || emailToColor(user.email);\n const name = user.name || emailToName(user.email);\n const initial = name.charAt(0).toUpperCase();\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <div\n style={{\n ...baseAvatarStyle,\n backgroundColor: color,\n marginLeft: isFirst ? 0 : OVERLAP,\n }}\n aria-label={`${name} (${user.email})`}\n tabIndex={0}\n >\n {initial}\n </div>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">{user.email}</TooltipContent>\n </Tooltip>\n );\n}\n\nfunction AgentAvatar({ active }: { active: boolean }) {\n injectStyles();\n\n return (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 4,\n }}\n >\n <div\n style={{\n ...baseAvatarStyle,\n backgroundColor: AGENT_COLOR,\n marginLeft: 0,\n animation: active ? \"_anPresencePulse 2s infinite\" : undefined,\n }}\n title={active ? \"AI is editing\" : \"AI agent\"}\n >\n A\n </div>\n {active && <AgentEditingChip />}\n </div>\n );\n}\n\nfunction AgentEditingChip() {\n return (\n <span\n style={{\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 4,\n height: 20,\n padding: \"0 8px\",\n borderRadius: 9999,\n backgroundColor: `${AGENT_COLOR}20`,\n color: AGENT_COLOR,\n fontSize: 11,\n fontWeight: 600,\n whiteSpace: \"nowrap\",\n }}\n >\n <span\n style={{\n width: 6,\n height: 6,\n borderRadius: \"50%\",\n backgroundColor: AGENT_COLOR,\n animation: \"_anPresencePulse 2s infinite\",\n flexShrink: 0,\n }}\n />\n AI editing\n </span>\n );\n}\n\nfunction OverflowBadge({\n count,\n isFirst,\n}: {\n count: number;\n isFirst: boolean;\n}) {\n return (\n <div\n style={{\n ...baseAvatarStyle,\n backgroundColor: \"rgba(255,255,255,0.1)\",\n color: \"rgba(255,255,255,0.5)\",\n marginLeft: isFirst ? 0 : OVERLAP,\n fontSize: 10,\n }}\n title={`${count} more collaborator${count === 1 ? \"\" : \"s\"}`}\n >\n +{count}\n </div>\n );\n}\n\nexport function PresenceBar({\n activeUsers,\n agentPresent,\n agentActive,\n currentUserEmail,\n maxVisible = 5,\n className,\n}: PresenceBarProps) {\n const { humanUsers, showAgent } = useMemo(() => {\n const currentEmail = currentUserEmail?.trim().toLowerCase();\n const uniqueUsers = dedupeCollabUsersByEmail(activeUsers);\n const humans = uniqueUsers.filter((u) => {\n const email = u.email.trim().toLowerCase();\n return email !== currentEmail && email !== \"agent@system\";\n });\n const hasAgentUser = uniqueUsers.some(\n (u) => u.email.trim().toLowerCase() === \"agent@system\",\n );\n return {\n humanUsers: humans,\n showAgent: agentPresent || agentActive || hasAgentUser,\n };\n }, [activeUsers, currentUserEmail, agentPresent, agentActive]);\n\n const visibleUsers = humanUsers.slice(0, maxVisible);\n const overflowCount = humanUsers.length - visibleUsers.length;\n\n if (!showAgent && humanUsers.length === 0) return null;\n\n return (\n <TooltipProvider delayDuration={150}>\n <div style={containerStyle} className={className}>\n {showAgent && <AgentAvatar active={!!agentActive} />}\n {visibleUsers.length > 0 && (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n marginLeft: showAgent ? 6 : 0,\n }}\n >\n {visibleUsers.map((u, i) => (\n <UserAvatar key={u.email} user={u} isFirst={i === 0} />\n ))}\n {overflowCount > 0 && (\n <OverflowBadge count={overflowCount} isFirst={false} />\n )}\n </div>\n )}\n </div>\n </TooltipProvider>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"PresenceBar.js","sourceRoot":"","sources":["../../../src/client/components/PresenceBar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from \"react\";\nimport {\n type CollabUser,\n dedupeCollabUsersByEmail,\n emailToColor,\n emailToName,\n} from \"../../collab/client.js\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"./ui/tooltip.js\";\n\nexport interface PresenceBarProps {\n /** Active collaborators on this document. */\n activeUsers: CollabUser[];\n /** Whether the agent has a durable presence entry. */\n agentPresent?: boolean;\n /** Whether the agent is actively making edits right now. */\n agentActive?: boolean;\n /** Current user's email (to exclude from the list). */\n currentUserEmail?: string;\n /** Max visible avatars before \"+N\" overflow. Default: 5 */\n maxVisible?: number;\n /** Additional CSS classes. */\n className?: string;\n /**\n * Called when an avatar is clicked. Receives the user being clicked\n * (or null for the agent avatar). Use this to start/stop follow mode.\n */\n onAvatarClick?: (user: CollabUser | null) => void;\n /**\n * The email of the user currently being followed. Highlighted with a\n * blue ring to indicate active follow mode.\n */\n followingEmail?: string | null;\n}\n\nconst AVATAR_SIZE = 28;\nconst OVERLAP = -8;\nconst BORDER_WIDTH = 2;\nconst FONT_SIZE = 12;\nconst AGENT_COLOR = \"#00B5FF\";\n\nconst baseAvatarStyle: React.CSSProperties = {\n width: AVATAR_SIZE,\n height: AVATAR_SIZE,\n borderRadius: \"50%\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n fontSize: FONT_SIZE,\n fontWeight: 700,\n color: \"#fff\",\n border: `${BORDER_WIDTH}px solid #fff`,\n flexShrink: 0,\n position: \"relative\",\n cursor: \"default\",\n boxSizing: \"border-box\",\n};\n\nconst containerStyle: React.CSSProperties = {\n display: \"flex\",\n alignItems: \"center\",\n flexDirection: \"row\",\n};\n\nconst pulseKeyframes = `\n@keyframes _anPresencePulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.6; }\n}\n`;\n\nlet styleInjected = false;\n\nfunction injectStyles() {\n if (styleInjected || typeof document === \"undefined\") return;\n const style = document.createElement(\"style\");\n style.textContent = pulseKeyframes;\n document.head.appendChild(style);\n styleInjected = true;\n}\n\nfunction UserAvatar({\n user,\n isFirst,\n onClick,\n isFollowing,\n}: {\n user: CollabUser;\n isFirst: boolean;\n onClick?: () => void;\n isFollowing?: boolean;\n}) {\n const color = user.color || emailToColor(user.email);\n const name = user.name || emailToName(user.email);\n const initial = name.charAt(0).toUpperCase();\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <div\n style={{\n ...baseAvatarStyle,\n backgroundColor: color,\n marginLeft: isFirst ? 0 : OVERLAP,\n cursor: onClick ? \"pointer\" : \"default\",\n boxShadow: isFollowing\n ? `0 0 0 2px #3b82f6, 0 0 0 4px #fff`\n : `0 0 0 2px #fff`,\n }}\n aria-label={`${name} (${user.email})${isFollowing ? \" — following\" : \"\"}`}\n tabIndex={0}\n onClick={onClick}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") onClick?.();\n }}\n >\n {initial}\n </div>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">\n {isFollowing ? `Following ${name} — click to stop` : user.email}\n </TooltipContent>\n </Tooltip>\n );\n}\n\nfunction AgentAvatar({\n active,\n onClick,\n isFollowing,\n}: {\n active: boolean;\n onClick?: () => void;\n isFollowing?: boolean;\n}) {\n injectStyles();\n\n return (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 4,\n }}\n >\n <div\n style={{\n ...baseAvatarStyle,\n backgroundColor: AGENT_COLOR,\n marginLeft: 0,\n animation: active ? \"_anPresencePulse 2s infinite\" : undefined,\n cursor: onClick ? \"pointer\" : \"default\",\n boxShadow: isFollowing\n ? `0 0 0 2px #3b82f6, 0 0 0 4px #fff`\n : undefined,\n }}\n title={\n isFollowing\n ? \"Following AI — click to stop\"\n : active\n ? \"AI is editing\"\n : \"AI agent\"\n }\n onClick={onClick}\n tabIndex={onClick ? 0 : undefined}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") onClick?.();\n }}\n role={onClick ? \"button\" : undefined}\n >\n A\n </div>\n {active && !isFollowing && <AgentEditingChip />}\n {isFollowing && (\n <span\n style={{\n display: \"inline-flex\",\n alignItems: \"center\",\n height: 20,\n padding: \"0 8px\",\n borderRadius: 9999,\n backgroundColor: `#3b82f620`,\n color: \"#3b82f6\",\n fontSize: 11,\n fontWeight: 600,\n whiteSpace: \"nowrap\",\n }}\n >\n Following AI\n </span>\n )}\n </div>\n );\n}\n\nfunction AgentEditingChip() {\n return (\n <span\n style={{\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 4,\n height: 20,\n padding: \"0 8px\",\n borderRadius: 9999,\n backgroundColor: `${AGENT_COLOR}20`,\n color: AGENT_COLOR,\n fontSize: 11,\n fontWeight: 600,\n whiteSpace: \"nowrap\",\n }}\n >\n <span\n style={{\n width: 6,\n height: 6,\n borderRadius: \"50%\",\n backgroundColor: AGENT_COLOR,\n animation: \"_anPresencePulse 2s infinite\",\n flexShrink: 0,\n }}\n />\n AI editing\n </span>\n );\n}\n\nfunction OverflowBadge({\n count,\n isFirst,\n}: {\n count: number;\n isFirst: boolean;\n}) {\n return (\n <div\n style={{\n ...baseAvatarStyle,\n backgroundColor: \"rgba(255,255,255,0.1)\",\n color: \"rgba(255,255,255,0.5)\",\n marginLeft: isFirst ? 0 : OVERLAP,\n fontSize: 10,\n }}\n title={`${count} more collaborator${count === 1 ? \"\" : \"s\"}`}\n >\n +{count}\n </div>\n );\n}\n\nexport function PresenceBar({\n activeUsers,\n agentPresent,\n agentActive,\n currentUserEmail,\n maxVisible = 5,\n className,\n onAvatarClick,\n followingEmail,\n}: PresenceBarProps) {\n const { humanUsers, showAgent } = useMemo(() => {\n const currentEmail = currentUserEmail?.trim().toLowerCase();\n const uniqueUsers = dedupeCollabUsersByEmail(activeUsers);\n const humans = uniqueUsers.filter((u) => {\n const email = u.email.trim().toLowerCase();\n return email !== currentEmail && email !== \"agent@system\";\n });\n const hasAgentUser = uniqueUsers.some(\n (u) => u.email.trim().toLowerCase() === \"agent@system\",\n );\n return {\n humanUsers: humans,\n showAgent: agentPresent || agentActive || hasAgentUser,\n };\n }, [activeUsers, currentUserEmail, agentPresent, agentActive]);\n\n const visibleUsers = humanUsers.slice(0, maxVisible);\n const overflowCount = humanUsers.length - visibleUsers.length;\n\n if (!showAgent && humanUsers.length === 0) return null;\n\n const followingLower = followingEmail?.trim().toLowerCase() ?? null;\n const isFollowingAgent = followingLower === \"agent@system\";\n\n return (\n <TooltipProvider delayDuration={150}>\n <div style={containerStyle} className={className}>\n {showAgent && (\n <AgentAvatar\n active={!!agentActive}\n onClick={onAvatarClick ? () => onAvatarClick(null) : undefined}\n isFollowing={isFollowingAgent}\n />\n )}\n {visibleUsers.length > 0 && (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n marginLeft: showAgent ? 6 : 0,\n }}\n >\n {visibleUsers.map((u, i) => (\n <UserAvatar\n key={u.email}\n user={u}\n isFirst={i === 0}\n onClick={onAvatarClick ? () => onAvatarClick(u) : undefined}\n isFollowing={\n followingLower != null &&\n u.email.trim().toLowerCase() === followingLower\n }\n />\n ))}\n {overflowCount > 0 && (\n <OverflowBadge count={overflowCount} isFirst={false} />\n )}\n </div>\n )}\n </div>\n </TooltipProvider>\n );\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RemoteSelectionRings — renders colored outline rings + name tags over
|
|
3
|
+
* elements selected by remote participants.
|
|
4
|
+
*
|
|
5
|
+
* Each participant's presence payload may contain a `selection` key with an
|
|
6
|
+
* opaque descriptor (e.g. a CSS selector). The `resolveRect` callback maps
|
|
7
|
+
* a descriptor to a DOMRect (or null when the element isn't found). Rings
|
|
8
|
+
* are rendered as absolutely-positioned outlines anchored to the container.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* <div style={{ position: "relative" }}>
|
|
12
|
+
* {content}
|
|
13
|
+
* <RemoteSelectionRings
|
|
14
|
+
* others={others}
|
|
15
|
+
* resolveRect={(selector) => document.querySelector(selector)?.getBoundingClientRect() ?? null}
|
|
16
|
+
* containerRef={containerRef}
|
|
17
|
+
* />
|
|
18
|
+
* </div>
|
|
19
|
+
*/
|
|
20
|
+
import type { OtherPresence } from "../../collab/presence.js";
|
|
21
|
+
export interface RemoteSelectionRingsProps {
|
|
22
|
+
/** Remote participants. */
|
|
23
|
+
others: OtherPresence[];
|
|
24
|
+
/**
|
|
25
|
+
* Key inside presence payload that carries the selection descriptor.
|
|
26
|
+
* Default: "selection"
|
|
27
|
+
*/
|
|
28
|
+
selectionKey?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Resolver: maps a selection descriptor to a DOMRect relative to the
|
|
31
|
+
* viewport. Return null when the element is not found.
|
|
32
|
+
*/
|
|
33
|
+
resolveRect: (descriptor: string) => DOMRect | null;
|
|
34
|
+
/**
|
|
35
|
+
* Container element ref. Rings are positioned relative to this element's
|
|
36
|
+
* bounding box.
|
|
37
|
+
*/
|
|
38
|
+
containerRef: React.RefObject<HTMLElement | null>;
|
|
39
|
+
/** Additional CSS class for the overlay div. */
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function RemoteSelectionRings({ others, selectionKey, resolveRect, containerRef, className, }: RemoteSelectionRingsProps): import("react/jsx-runtime").JSX.Element;
|
|
43
|
+
//# sourceMappingURL=RemoteSelectionRings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RemoteSelectionRings.d.ts","sourceRoot":"","sources":["../../../src/client/components/RemoteSelectionRings.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,MAAM,WAAW,yBAAyB;IACxC,2BAA2B;IAC3B,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,WAAW,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IACpD;;;OAGG;IACH,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAClD,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAoDD,wBAAgB,oBAAoB,CAAC,EACnC,MAAM,EACN,YAA0B,EAC1B,WAAW,EACX,YAAY,EACZ,SAAS,GACV,EAAE,yBAAyB,2CAmF3B"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* RemoteSelectionRings — renders colored outline rings + name tags over
|
|
4
|
+
* elements selected by remote participants.
|
|
5
|
+
*
|
|
6
|
+
* Each participant's presence payload may contain a `selection` key with an
|
|
7
|
+
* opaque descriptor (e.g. a CSS selector). The `resolveRect` callback maps
|
|
8
|
+
* a descriptor to a DOMRect (or null when the element isn't found). Rings
|
|
9
|
+
* are rendered as absolutely-positioned outlines anchored to the container.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* <div style={{ position: "relative" }}>
|
|
13
|
+
* {content}
|
|
14
|
+
* <RemoteSelectionRings
|
|
15
|
+
* others={others}
|
|
16
|
+
* resolveRect={(selector) => document.querySelector(selector)?.getBoundingClientRect() ?? null}
|
|
17
|
+
* containerRef={containerRef}
|
|
18
|
+
* />
|
|
19
|
+
* </div>
|
|
20
|
+
*/
|
|
21
|
+
import { useState, useEffect, useRef, useLayoutEffect, memo } from "react";
|
|
22
|
+
const RingItem = memo(function RingItem({ ring }) {
|
|
23
|
+
return (_jsx("div", { "aria-label": `${ring.label} selection`, style: {
|
|
24
|
+
position: "absolute",
|
|
25
|
+
top: ring.rect.top,
|
|
26
|
+
left: ring.rect.left,
|
|
27
|
+
width: ring.rect.width,
|
|
28
|
+
height: ring.rect.height,
|
|
29
|
+
outline: `2px solid ${ring.color}`,
|
|
30
|
+
outlineOffset: 2,
|
|
31
|
+
borderRadius: 3,
|
|
32
|
+
pointerEvents: "none",
|
|
33
|
+
boxShadow: `0 0 0 1px ${ring.color}40`,
|
|
34
|
+
zIndex: 9998,
|
|
35
|
+
}, children: _jsx("div", { style: {
|
|
36
|
+
position: "absolute",
|
|
37
|
+
top: -20,
|
|
38
|
+
left: 0,
|
|
39
|
+
backgroundColor: ring.color,
|
|
40
|
+
color: "#fff",
|
|
41
|
+
fontSize: 10,
|
|
42
|
+
fontWeight: 600,
|
|
43
|
+
padding: "1px 5px",
|
|
44
|
+
borderRadius: 3,
|
|
45
|
+
whiteSpace: "nowrap",
|
|
46
|
+
maxWidth: 120,
|
|
47
|
+
overflow: "hidden",
|
|
48
|
+
textOverflow: "ellipsis",
|
|
49
|
+
}, children: ring.isAgent ? `AI — ${ring.label}` : ring.label }) }));
|
|
50
|
+
});
|
|
51
|
+
export function RemoteSelectionRings({ others, selectionKey = "selection", resolveRect, containerRef, className, }) {
|
|
52
|
+
const overlayRef = useRef(null);
|
|
53
|
+
const [rings, setRings] = useState([]);
|
|
54
|
+
// Recompute rings whenever others change or on animation frame.
|
|
55
|
+
const recompute = () => {
|
|
56
|
+
const container = containerRef.current;
|
|
57
|
+
if (!container) {
|
|
58
|
+
setRings([]);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const containerRect = container.getBoundingClientRect();
|
|
62
|
+
const next = [];
|
|
63
|
+
for (const other of others) {
|
|
64
|
+
const descriptor = other.presence[selectionKey];
|
|
65
|
+
if (!descriptor || typeof descriptor !== "string")
|
|
66
|
+
continue;
|
|
67
|
+
const domRect = resolveRect(descriptor);
|
|
68
|
+
if (!domRect)
|
|
69
|
+
continue;
|
|
70
|
+
// Convert viewport-relative rect to container-relative.
|
|
71
|
+
const top = domRect.top - containerRect.top;
|
|
72
|
+
const left = domRect.left - containerRect.left;
|
|
73
|
+
if (left + domRect.width < 0 ||
|
|
74
|
+
top + domRect.height < 0 ||
|
|
75
|
+
left > containerRect.width ||
|
|
76
|
+
top > containerRect.height) {
|
|
77
|
+
continue; // Out of container bounds — skip.
|
|
78
|
+
}
|
|
79
|
+
next.push({
|
|
80
|
+
clientId: other.clientId,
|
|
81
|
+
color: other.user.color || "#94a3b8",
|
|
82
|
+
label: other.isAgent ? "AI" : other.user.name || other.user.email,
|
|
83
|
+
isAgent: other.isAgent,
|
|
84
|
+
rect: { top, left, width: domRect.width, height: domRect.height },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
setRings(next);
|
|
88
|
+
};
|
|
89
|
+
// Recompute on scroll/resize of the container to keep rings in sync.
|
|
90
|
+
useLayoutEffect(() => {
|
|
91
|
+
recompute();
|
|
92
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
93
|
+
}, [others, selectionKey]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const container = containerRef.current;
|
|
96
|
+
if (!container)
|
|
97
|
+
return;
|
|
98
|
+
const observer = new ResizeObserver(() => recompute());
|
|
99
|
+
observer.observe(container);
|
|
100
|
+
container.addEventListener("scroll", recompute, { passive: true });
|
|
101
|
+
window.addEventListener("scroll", recompute, { passive: true });
|
|
102
|
+
return () => {
|
|
103
|
+
observer.disconnect();
|
|
104
|
+
container.removeEventListener("scroll", recompute);
|
|
105
|
+
window.removeEventListener("scroll", recompute);
|
|
106
|
+
};
|
|
107
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
108
|
+
}, [containerRef]);
|
|
109
|
+
return (_jsx("div", { ref: overlayRef, "aria-hidden": true, style: {
|
|
110
|
+
position: "absolute",
|
|
111
|
+
inset: 0,
|
|
112
|
+
pointerEvents: "none",
|
|
113
|
+
overflow: "hidden",
|
|
114
|
+
}, className: className, children: rings.map((ring) => (_jsx(RingItem, { ring: ring }, ring.clientId))) }));
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=RemoteSelectionRings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RemoteSelectionRings.js","sourceRoot":"","sources":["../../../src/client/components/RemoteSelectionRings.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * RemoteSelectionRings — renders colored outline rings + name tags over\n * elements selected by remote participants.\n *\n * Each participant's presence payload may contain a `selection` key with an\n * opaque descriptor (e.g. a CSS selector). The `resolveRect` callback maps\n * a descriptor to a DOMRect (or null when the element isn't found). Rings\n * are rendered as absolutely-positioned outlines anchored to the container.\n *\n * Usage:\n * <div style={{ position: \"relative\" }}>\n * {content}\n * <RemoteSelectionRings\n * others={others}\n * resolveRect={(selector) => document.querySelector(selector)?.getBoundingClientRect() ?? null}\n * containerRef={containerRef}\n * />\n * </div>\n */\n\nimport { useState, useEffect, useRef, useLayoutEffect, memo } from \"react\";\nimport type { OtherPresence } from \"../../collab/presence.js\";\n\nexport interface RemoteSelectionRingsProps {\n /** Remote participants. */\n others: OtherPresence[];\n /**\n * Key inside presence payload that carries the selection descriptor.\n * Default: \"selection\"\n */\n selectionKey?: string;\n /**\n * Resolver: maps a selection descriptor to a DOMRect relative to the\n * viewport. Return null when the element is not found.\n */\n resolveRect: (descriptor: string) => DOMRect | null;\n /**\n * Container element ref. Rings are positioned relative to this element's\n * bounding box.\n */\n containerRef: React.RefObject<HTMLElement | null>;\n /** Additional CSS class for the overlay div. */\n className?: string;\n}\n\ninterface Ring {\n clientId: number;\n color: string;\n label: string;\n isAgent: boolean;\n rect: { top: number; left: number; width: number; height: number };\n}\n\nconst RingItem = memo(function RingItem({ ring }: { ring: Ring }) {\n return (\n <div\n aria-label={`${ring.label} selection`}\n style={{\n position: \"absolute\",\n top: ring.rect.top,\n left: ring.rect.left,\n width: ring.rect.width,\n height: ring.rect.height,\n outline: `2px solid ${ring.color}`,\n outlineOffset: 2,\n borderRadius: 3,\n pointerEvents: \"none\",\n boxShadow: `0 0 0 1px ${ring.color}40`,\n zIndex: 9998,\n }}\n >\n {/* Name tag in top-left corner of the ring */}\n <div\n style={{\n position: \"absolute\",\n top: -20,\n left: 0,\n backgroundColor: ring.color,\n color: \"#fff\",\n fontSize: 10,\n fontWeight: 600,\n padding: \"1px 5px\",\n borderRadius: 3,\n whiteSpace: \"nowrap\",\n maxWidth: 120,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n }}\n >\n {ring.isAgent ? `AI — ${ring.label}` : ring.label}\n </div>\n </div>\n );\n});\n\nexport function RemoteSelectionRings({\n others,\n selectionKey = \"selection\",\n resolveRect,\n containerRef,\n className,\n}: RemoteSelectionRingsProps) {\n const overlayRef = useRef<HTMLDivElement>(null);\n const [rings, setRings] = useState<Ring[]>([]);\n\n // Recompute rings whenever others change or on animation frame.\n const recompute = () => {\n const container = containerRef.current;\n if (!container) {\n setRings([]);\n return;\n }\n const containerRect = container.getBoundingClientRect();\n const next: Ring[] = [];\n\n for (const other of others) {\n const descriptor = other.presence[selectionKey] as string | undefined;\n if (!descriptor || typeof descriptor !== \"string\") continue;\n\n const domRect = resolveRect(descriptor);\n if (!domRect) continue;\n\n // Convert viewport-relative rect to container-relative.\n const top = domRect.top - containerRect.top;\n const left = domRect.left - containerRect.left;\n if (\n left + domRect.width < 0 ||\n top + domRect.height < 0 ||\n left > containerRect.width ||\n top > containerRect.height\n ) {\n continue; // Out of container bounds — skip.\n }\n\n next.push({\n clientId: other.clientId,\n color: other.user.color || \"#94a3b8\",\n label: other.isAgent ? \"AI\" : other.user.name || other.user.email,\n isAgent: other.isAgent,\n rect: { top, left, width: domRect.width, height: domRect.height },\n });\n }\n\n setRings(next);\n };\n\n // Recompute on scroll/resize of the container to keep rings in sync.\n useLayoutEffect(() => {\n recompute();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [others, selectionKey]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n const observer = new ResizeObserver(() => recompute());\n observer.observe(container);\n container.addEventListener(\"scroll\", recompute, { passive: true });\n window.addEventListener(\"scroll\", recompute, { passive: true });\n return () => {\n observer.disconnect();\n container.removeEventListener(\"scroll\", recompute);\n window.removeEventListener(\"scroll\", recompute);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [containerRef]);\n\n return (\n <div\n ref={overlayRef}\n aria-hidden\n style={{\n position: \"absolute\",\n inset: 0,\n pointerEvents: \"none\",\n overflow: \"hidden\",\n }}\n className={className}\n >\n {rings.map((ring) => (\n <RingItem key={ring.clientId} ring={ring} />\n ))}\n </div>\n );\n}\n"]}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -74,6 +74,8 @@ export { trackEvent, trackSessionStatus, configureTracking, setSentryUser, captu
|
|
|
74
74
|
export { track } from "./track.js";
|
|
75
75
|
export { useCollaborativeDoc, isReconcileLeadClient, emailToColor, emailToName, type UseCollaborativeDocOptions, type UseCollaborativeDocResult, type CollabUser, } from "../collab/client.js";
|
|
76
76
|
export { AGENT_CLIENT_ID } from "../collab/agent-identity.js";
|
|
77
|
+
export { usePresence, toNormalized, fromNormalized, type OtherPresence, type PresencePayload, type UsePresenceResult, type NormalizedPoint, } from "../collab/presence.js";
|
|
78
|
+
export { useFollowUser, type UseFollowUserOptions, type UseFollowUserResult, type ViewportDescriptor, } from "../collab/follow-mode.js";
|
|
77
79
|
export { ResourcesPanel, ResourceTree, ResourceEditor, useResources, useResourceTree, useResource, useCreateResource, useUpdateResource, useDeleteResource, useUploadResource, type Resource, type ResourceMeta, type TreeNode, type ResourceScope, type ResourceTreeProps, type ResourceEditorProps, } from "./resources/index.js";
|
|
78
80
|
export type { AppToFrameMessage, FrameToAppMessage, FrameMessage, CodeCompleteMessage, ChatRunningMessage, } from "./frame-protocol.js";
|
|
79
81
|
export { CommandMenu, useCommandMenuShortcut, openAgentSidebar, submitToAgent, type CommandMenuProps, type CommandGroupProps, type CommandItemProps, type CommandShortcutProps, } from "./CommandMenu.js";
|
|
@@ -87,6 +89,8 @@ export { useAvatarUrl, uploadAvatar, invalidateAvatarCache, } from "./use-avatar
|
|
|
87
89
|
export { ObservabilityDashboard, ThumbsFeedback, } from "./observability/index.js";
|
|
88
90
|
export { PresenceBar, type PresenceBarProps, } from "./components/PresenceBar.js";
|
|
89
91
|
export { AgentPresenceChip, type AgentPresenceChipProps, } from "./components/AgentPresenceChip.js";
|
|
92
|
+
export { LiveCursorOverlay, type LiveCursorOverlayProps, type CursorMapFn, } from "./components/LiveCursorOverlay.js";
|
|
93
|
+
export { RemoteSelectionRings, type RemoteSelectionRingsProps, } from "./components/RemoteSelectionRings.js";
|
|
90
94
|
export { useCollaborativeMap, useCollaborativeArray, type UseCollaborativeMapOptions, type UseCollaborativeMapResult, type UseCollaborativeArrayOptions, type UseCollaborativeArrayResult, } from "../collab/client-struct.js";
|
|
91
95
|
export { NotificationsBell } from "./notifications/index.js";
|
|
92
96
|
export { defineBlock, BlockRegistry, registerBlocks, BlockRegistryProvider, useBlockRegistry, useOptionalBlockRegistry, BlockView, SchemaBlockEditor, markdown, richtext, introspect, serializeSpecBlock, parseSpecBlock, createAttrReader, describeBlocksForAgent, renderBlockVocabularyReference, type BlockSpec, type BlockPlacement, type BlockMdxConfig, type BlockAttrReader, type BlockRenderContext, type BlockReadProps, type BlockEditProps, type MdxAttrValue, type FieldKind, type FieldDescriptor, type MdxJsxNode, type MdxAttrNode, type SerializableBlock, type ParsedBlockBase, type BlockAgentDoc, } from "./blocks/index.js";
|