@agent-native/core 0.45.0 → 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/action.d.ts +8 -1
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +20 -10
- package/dist/action.js.map +1 -1
- package/dist/cli/app-skill.d.ts +3 -1
- package/dist/cli/app-skill.d.ts.map +1 -1
- package/dist/cli/app-skill.js +50 -8
- package/dist/cli/app-skill.js.map +1 -1
- package/dist/cli/connect.d.ts.map +1 -1
- package/dist/cli/connect.js +39 -5
- package/dist/cli/connect.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +9 -7
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.js +42 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-config-writers.d.ts +10 -0
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +60 -6
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +4 -6
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/plan-local.d.ts.map +1 -1
- package/dist/cli/plan-local.js +15 -2
- package/dist/cli/plan-local.js.map +1 -1
- package/dist/cli/plan-publish-store.d.ts +17 -7
- package/dist/cli/plan-publish-store.d.ts.map +1 -1
- package/dist/cli/plan-publish-store.js +33 -8
- package/dist/cli/plan-publish-store.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts +63 -5
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +641 -48
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/skills.d.ts +26 -11
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +644 -972
- package/dist/cli/skills.js.map +1 -1
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +3 -2
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.js +37 -9
- package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
- package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/DiffBlock.js +44 -12
- package/dist/client/blocks/library/DiffBlock.js.map +1 -1
- package/dist/client/blocks/library/annotation-rail.d.ts +12 -3
- package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -1
- package/dist/client/blocks/library/annotation-rail.js +29 -3
- package/dist/client/blocks/library/annotation-rail.js.map +1 -1
- package/dist/client/blocks/library/html.d.ts.map +1 -1
- package/dist/client/blocks/library/html.js +3 -1
- package/dist/client/blocks/library/html.js.map +1 -1
- package/dist/client/blocks/library/question-form.d.ts.map +1 -1
- package/dist/client/blocks/library/question-form.js +4 -1
- package/dist/client/blocks/library/question-form.js.map +1 -1
- 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/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +2 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/extensions/routes.d.ts +18 -0
- package/dist/extensions/routes.d.ts.map +1 -1
- package/dist/extensions/routes.js +30 -8
- package/dist/extensions/routes.js.map +1 -1
- package/dist/oauth-tokens/store.d.ts.map +1 -1
- package/dist/oauth-tokens/store.js +42 -5
- package/dist/oauth-tokens/store.js.map +1 -1
- package/dist/scripts/db/index.d.ts.map +1 -1
- package/dist/scripts/db/index.js +1 -0
- package/dist/scripts/db/index.js.map +1 -1
- package/dist/scripts/db/migrate-encrypt-oauth-tokens.d.ts +28 -0
- package/dist/scripts/db/migrate-encrypt-oauth-tokens.d.ts.map +1 -0
- package/dist/scripts/db/migrate-encrypt-oauth-tokens.js +164 -0
- package/dist/scripts/db/migrate-encrypt-oauth-tokens.js.map +1 -0
- package/dist/scripts/db/scoping.d.ts.map +1 -1
- package/dist/scripts/db/scoping.js +7 -5
- package/dist/scripts/db/scoping.js.map +1 -1
- package/dist/secrets/index.d.ts +1 -0
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +4 -0
- package/dist/secrets/index.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/sharing/actions/set-resource-visibility.d.ts.map +1 -1
- package/dist/sharing/actions/set-resource-visibility.js +4 -1
- package/dist/sharing/actions/set-resource-visibility.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/plan-plugin.md +21 -6
- package/docs/content/pr-visual-recap.md +52 -3
- package/docs/content/real-time-collaboration.md +481 -97
- package/docs/content/skills-guide.md +13 -0
- package/docs/content/template-plan.md +18 -7
- package/package.json +5 -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
|
@@ -1,55 +1,117 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Real-Time Collaboration"
|
|
3
|
-
description: "Multi-user collaborative editing
|
|
3
|
+
description: "Multi-user collaborative editing where the AI agent is a first-class peer: CRDT merging, live presence, SSE fast-path, and granular server-side merge — on any SQL database and any host."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Real-Time Collaboration
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Imagine opening a document and seeing a peer's cursor scroll to a paragraph,
|
|
9
|
+
then the text rewrite itself — surgically, without losing your place. That
|
|
10
|
+
peer might be a teammate. It might be the agent. From the framework's
|
|
11
|
+
perspective they are identical: both produce Yjs operations that merge
|
|
12
|
+
conflict-free into the shared document. This is the keystone of the
|
|
13
|
+
agent-native collaboration model.
|
|
14
|
+
|
|
15
|
+
## Vision {#vision}
|
|
16
|
+
|
|
17
|
+
Editing alongside the agent feels like working in Google Docs or Figma with
|
|
18
|
+
a coworker who is both instant and tireless:
|
|
19
|
+
|
|
20
|
+
- **CRDT merging** — Concurrent edits from humans and agents merge without
|
|
21
|
+
conflicts. You type in one paragraph; the agent rewrites another; both
|
|
22
|
+
land cleanly.
|
|
23
|
+
- **Presence** — A `PresenceBar` shows who is in the document right now,
|
|
24
|
+
including an agent presence indicator when the agent is actively editing.
|
|
25
|
+
- **The agent as a peer editor** — Agent edits flow through the same Yjs
|
|
26
|
+
infrastructure as human edits. They appear live, without disrupting cursor
|
|
27
|
+
positions, selections, or the undo stack.
|
|
28
|
+
- **Works everywhere** — Any SQL database Drizzle supports (SQLite, Postgres).
|
|
29
|
+
Any hosting target Nitro supports, including serverless and edge.
|
|
30
|
+
|
|
31
|
+
## Architecture {#architecture}
|
|
32
|
+
|
|
33
|
+
The collaboration system has five interlocking layers.
|
|
34
|
+
|
|
35
|
+
### 1. Yjs Y.Doc (CRDT layer)
|
|
36
|
+
|
|
37
|
+
Each collaborative document is a `Y.Doc` containing shared types — usually a
|
|
38
|
+
`Y.XmlFragment` for rich text (the ProseMirror node tree that TipTap reads) or
|
|
39
|
+
`Y.Map` / `Y.Array` for structured JSON data. Yjs merges concurrent updates
|
|
40
|
+
with no central coordinator; any two clients that exchange their state reach
|
|
41
|
+
the same result regardless of order.
|
|
42
|
+
|
|
43
|
+
### 2. SQL canonical content (durable source of truth)
|
|
44
|
+
|
|
45
|
+
Yjs state is persisted in a `_collab_docs` table as base64-encoded binary.
|
|
46
|
+
The table is framework-managed and provider-agnostic (SQLite and Postgres use
|
|
47
|
+
identical schemas). Each row carries an optimistic-concurrency version column
|
|
48
|
+
to prevent concurrent write races. Tombstone compaction runs opportunistically
|
|
49
|
+
when the stored blob exceeds 4× the freshly encoded state — no background job
|
|
50
|
+
required.
|
|
51
|
+
|
|
52
|
+
### 3. `updatedAt`-gated reconcile (agent-edit propagation)
|
|
53
|
+
|
|
54
|
+
Agent actions do not push into Yjs in-process. Instead, the action edits the
|
|
55
|
+
canonical SQL content column and bumps `updatedAt`. The change-sync system
|
|
56
|
+
detects the bump, the open editor refetches the record, and the lead client
|
|
57
|
+
applies the new content into the shared Y.Doc via `setContent`. An `updatedAt`
|
|
58
|
+
gate ensures only genuinely newer content is adopted — lagging poll responses
|
|
59
|
+
cannot revert the edit.
|
|
60
|
+
|
|
61
|
+
### 4. Lead-client election (deduplication)
|
|
62
|
+
|
|
63
|
+
When multiple tabs are open, exactly one applies an authoritative SQL snapshot
|
|
64
|
+
into the shared Y.Doc. The lead is the tab with the lowest Yjs `clientID`
|
|
65
|
+
among currently visible peers. The agent's awareness entry uses
|
|
66
|
+
`AGENT_CLIENT_ID` (max int) so it can never be the lead. A client editing
|
|
67
|
+
alone is always the lead. The election is deterministic with no coordination
|
|
68
|
+
round-trip (`isReconcileLeadClient` from `@agent-native/core/client`).
|
|
69
|
+
|
|
70
|
+
### 5. SSE fast-path + polling fallback (transport)
|
|
71
|
+
|
|
72
|
+
Collab update events travel via two paths:
|
|
73
|
+
|
|
74
|
+
- **SSE fast-path** — The client subscribes to `/_agent-native/poll-events`
|
|
75
|
+
(the same `EventSource` used by `useDbSync`). Collab update events arrive
|
|
76
|
+
push-style, typically in tens of milliseconds. While SSE is healthy the
|
|
77
|
+
poll loop relaxes to a slow cadence (~12 s by default).
|
|
78
|
+
- **Polling fallback** — `/_agent-native/poll?since=N` is polled every 2 s
|
|
79
|
+
when SSE is unavailable. This makes collaboration work on any deployment
|
|
80
|
+
target — including serverless functions where persistent connections are
|
|
81
|
+
impossible and different invocations can handle different requests.
|
|
82
|
+
|
|
83
|
+
Local Yjs updates are debounced and coalesced with `Y.mergeUpdates` (~80 ms)
|
|
84
|
+
before being sent to the server, reducing keystroke-level network traffic.
|
|
85
|
+
The batch is flushed immediately on `visibilitychange` or `pagehide`. A
|
|
86
|
+
state-vector diff (`GET /:docId/state?stateVector=…`) is fetched only on
|
|
87
|
+
reconnect, ring-buffer overflow, or every 15th poll cycle — not on every
|
|
88
|
+
cycle.
|
|
89
|
+
|
|
90
|
+
Network errors use exponential backoff with jitter, capped at ~15 s.
|
|
9
91
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
The agent and human users are equal participants in collaborative editing. The key insight is that both produce Yjs operations that merge cleanly:
|
|
29
|
-
|
|
30
|
-
- **Human edits** flow through TipTap → ySyncPlugin → Y.XmlFragment → server via HTTP
|
|
31
|
-
- **Agent edits** flow through the `edit-document` action → server search-replace endpoint → Y.XmlFragment mutation → poll update → all clients
|
|
32
|
-
|
|
33
|
-
The agent's `edit-document` action uses surgical search-and-replace on Y.XmlText nodes within the Y.XmlFragment tree. This produces the smallest possible Yjs update — only the changed text is modified, not the entire document. The result: the user sees the agent's change appear in their editor without losing their place.
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
# Agent makes a surgical edit — user sees it appear live
|
|
37
|
-
pnpm action edit-document --id doc123 --find "Big Projects" --replace "Proyectos Grandes"
|
|
38
|
-
|
|
39
|
-
# The action:
|
|
40
|
-
# 1. Updates SQL content column (for search/API compat)
|
|
41
|
-
# 2. Calls POST /_agent-native/collab/doc123/search-replace
|
|
42
|
-
# 3. Server walks Y.XmlFragment, finds text, modifies Y.XmlText node
|
|
43
|
-
# 4. Minimal Yjs update emitted via poll system
|
|
44
|
-
# 5. Client receives update → ySyncPlugin applies targeted PM transaction
|
|
45
|
-
# 6. User's cursor stays in place ✓
|
|
92
|
+
```
|
|
93
|
+
Browser Server SQL
|
|
94
|
+
────── ────── ───
|
|
95
|
+
[Human edits]
|
|
96
|
+
→ Y.Doc update
|
|
97
|
+
→ debounce ~80ms
|
|
98
|
+
→ POST /collab/:docId/update ──────→ apply + persist
|
|
99
|
+
emitCollabUpdate
|
|
100
|
+
← SSE push ──── poll-events stream
|
|
101
|
+
← Y.applyUpdate
|
|
102
|
+
|
|
103
|
+
[Agent action]
|
|
104
|
+
action writes SQL content + bumps updatedAt
|
|
105
|
+
← change-sync detects updatedAt bump
|
|
106
|
+
← lead client calls setContent
|
|
107
|
+
→ Y.Doc update
|
|
108
|
+
→ POST /collab/:docId/update ──────→ apply + persist
|
|
109
|
+
← SSE push to all other clients
|
|
46
110
|
```
|
|
47
111
|
|
|
48
|
-
##
|
|
49
|
-
|
|
50
|
-
Templates opt into collaboration with five steps:
|
|
112
|
+
## Quickstart {#quickstart}
|
|
51
113
|
|
|
52
|
-
### 1. Install
|
|
114
|
+
### 1. Install packages
|
|
53
115
|
|
|
54
116
|
```bash
|
|
55
117
|
pnpm add @tiptap/extension-collaboration @tiptap/extension-collaboration-caret @tiptap/y-tiptap @tiptap/core
|
|
@@ -78,6 +140,11 @@ export default defineConfig({
|
|
|
78
140
|
|
|
79
141
|
### 3. Add the collab server plugin
|
|
80
142
|
|
|
143
|
+
Always set `resourceType` to the name of the shareable resource registered
|
|
144
|
+
via `registerShareableResource`. Without it, collab push events are delivered
|
|
145
|
+
to all authenticated users without document-level scoping, and the server
|
|
146
|
+
logs a one-time warning.
|
|
147
|
+
|
|
81
148
|
```typescript
|
|
82
149
|
// server/plugins/collab.ts
|
|
83
150
|
import { createCollabPlugin } from "@agent-native/core/server";
|
|
@@ -86,22 +153,31 @@ export default createCollabPlugin({
|
|
|
86
153
|
table: "documents",
|
|
87
154
|
contentColumn: "content",
|
|
88
155
|
idColumn: "id",
|
|
89
|
-
|
|
156
|
+
resourceType: "document", // required for access-scoped event delivery
|
|
90
157
|
});
|
|
91
158
|
```
|
|
92
159
|
|
|
93
160
|
### 4. Use the client hook
|
|
94
161
|
|
|
95
162
|
```typescript
|
|
96
|
-
import {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
163
|
+
import {
|
|
164
|
+
useCollaborativeDoc,
|
|
165
|
+
emailToColor,
|
|
166
|
+
emailToName,
|
|
167
|
+
} from "@agent-native/core/client";
|
|
168
|
+
|
|
169
|
+
const TAB_ID = generateTabId(); // or Math.random().toString(36)
|
|
170
|
+
|
|
171
|
+
const { ydoc, awareness, isLoading, activeUsers, agentActive, agentPresent } =
|
|
172
|
+
useCollaborativeDoc({
|
|
173
|
+
docId: documentId,
|
|
174
|
+
requestSource: TAB_ID,
|
|
175
|
+
user: {
|
|
176
|
+
name: emailToName(session.email),
|
|
177
|
+
email: session.email,
|
|
178
|
+
color: emailToColor(session.email),
|
|
179
|
+
},
|
|
180
|
+
});
|
|
105
181
|
```
|
|
106
182
|
|
|
107
183
|
### 5. Add TipTap extensions
|
|
@@ -109,77 +185,385 @@ const { ydoc, awareness, isLoading, activeUsers } = useCollaborativeDoc({
|
|
|
109
185
|
```typescript
|
|
110
186
|
import Collaboration from "@tiptap/extension-collaboration";
|
|
111
187
|
import CollaborationCaret from "@tiptap/extension-collaboration-caret";
|
|
112
|
-
import { Awareness } from "y-protocols/awareness";
|
|
113
|
-
|
|
114
|
-
// Create awareness for cursor sync
|
|
115
|
-
const awareness = new Awareness(ydoc);
|
|
116
|
-
awareness.setLocalStateField("user", { name, color });
|
|
117
188
|
|
|
118
189
|
const editor = useEditor({
|
|
119
190
|
extensions: [
|
|
120
|
-
StarterKit.configure({ history: false }), // Yjs
|
|
191
|
+
StarterKit.configure({ history: false }), // Yjs owns undo
|
|
121
192
|
Collaboration.configure({ document: ydoc }),
|
|
122
193
|
CollaborationCaret.configure({
|
|
123
194
|
provider: { awareness },
|
|
124
195
|
user: { name, color },
|
|
125
196
|
}),
|
|
126
197
|
],
|
|
127
|
-
content
|
|
198
|
+
// Do NOT pass content here — Yjs owns the content
|
|
128
199
|
});
|
|
129
200
|
```
|
|
130
201
|
|
|
131
|
-
|
|
202
|
+
### 6. Seed on first load (if content exists)
|
|
132
203
|
|
|
133
|
-
The
|
|
204
|
+
The Collaboration extension does not auto-seed from a `content` prop. If the
|
|
205
|
+
Y.Doc is empty and the document has existing content, seed it:
|
|
134
206
|
|
|
135
|
-
|
|
207
|
+
```typescript
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!ydoc || !editor || !isLoaded) return;
|
|
210
|
+
const fragment = ydoc.getXmlFragment("default");
|
|
211
|
+
if (fragment.length === 0 && initialContent) {
|
|
212
|
+
editor.commands.setContent(initialContent);
|
|
213
|
+
}
|
|
214
|
+
}, [ydoc, editor, isLoaded]);
|
|
215
|
+
```
|
|
136
216
|
|
|
137
|
-
|
|
217
|
+
---
|
|
138
218
|
|
|
139
|
-
|
|
219
|
+
## Presence Kit {#presence-kit}
|
|
140
220
|
|
|
141
|
-
-
|
|
142
|
-
- REST routes for update/delete at `/api/comments/:id`; create and list run through the `add-comment` / `list-comments` actions
|
|
143
|
-
- Comments sidebar with threaded view and reply UI
|
|
144
|
-
- Resolve/unresolve threads
|
|
145
|
-
- **Send to AI** button — sends the comment thread context to the agent chat via `sendToAgentChat()`
|
|
146
|
-
- Agent actions: `list-comments`, `add-comment`
|
|
147
|
-
- Notion comment sync: `sync-notion-comments` action for bidirectional pull/push
|
|
221
|
+
The presence kit provides Liveblocks/Figma-grade live-cursor and selection primitives on top of the existing awareness layer.
|
|
148
222
|
|
|
149
|
-
|
|
223
|
+
### Fast awareness {#fast-awareness}
|
|
150
224
|
|
|
151
|
-
|
|
225
|
+
Awareness state changes now propagate at ~150ms instead of the 2s poll cycle:
|
|
152
226
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
| `GET /:docId/state` | Fetch full Y.Doc state (base64) |
|
|
156
|
-
| `POST /:docId/update` | Apply client Yjs update |
|
|
157
|
-
| `POST /:docId/text` | Apply full text replacement (diff-based) |
|
|
158
|
-
| `POST /:docId/search-replace` | Surgical find/replace in Y.XmlFragment |
|
|
159
|
-
| `POST /:docId/awareness` | Sync cursor/presence state |
|
|
160
|
-
| `GET /:docId/users` | List active users on a document |
|
|
227
|
+
- **Client → server**: any call to `setPresence()` or `awareness.setLocalStateField()` triggers a throttled POST to `/_agent-native/collab/:docId/awareness` within 150ms, coalescing rapid changes into one request.
|
|
228
|
+
- **Server → clients**: the `postAwareness` handler emits an `AWARENESS_CHANGE_EVENT` after storing. The `/_agent-native/poll-events` SSE stream forwards these events push-style to connected peers. Polling-only deployments continue to work — cursors degrade to poll cadence without errors.
|
|
161
229
|
|
|
162
|
-
|
|
230
|
+
### `usePresence(awareness, localClientId)` {#use-presence}
|
|
163
231
|
|
|
164
|
-
|
|
232
|
+
Returns a reactive list of remote participants and a setter for the local presence payload:
|
|
165
233
|
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
|
|
234
|
+
```typescript
|
|
235
|
+
import { usePresence } from "@agent-native/core/client";
|
|
236
|
+
|
|
237
|
+
const { others, setPresence } = usePresence(awareness, ydoc?.clientID);
|
|
238
|
+
|
|
239
|
+
// Publish cursor position (normalized 0–1)
|
|
240
|
+
setPresence({ cursor: { x: 0.4, y: 0.7 }, selection: "#hero" });
|
|
241
|
+
|
|
242
|
+
// others: OtherPresence[]
|
|
243
|
+
// {
|
|
244
|
+
// clientId: number
|
|
245
|
+
// user: { name, email, color }
|
|
246
|
+
// presence: { cursor?, selection?, viewport?, ... }
|
|
247
|
+
// isAgent: boolean ← true for AGENT_CLIENT_ID
|
|
248
|
+
// }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The agent (AGENT_CLIENT_ID) appears as a first-class participant with `isAgent: true`. When `agentUpdateSelection()` is called server-side, its selection metadata flows through `usePresence` like any other participant.
|
|
252
|
+
|
|
253
|
+
### `LiveCursorOverlay` {#live-cursor-overlay}
|
|
254
|
+
|
|
255
|
+
Renders remote cursors as absolutely-positioned labels over a container element:
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
import { LiveCursorOverlay } from "@agent-native/core/client";
|
|
259
|
+
|
|
260
|
+
// cursor positions stored as { x, y } normalized 0–1 under presence.cursor
|
|
261
|
+
<div ref={containerRef} style={{ position: "relative" }}>
|
|
262
|
+
{content}
|
|
263
|
+
<LiveCursorOverlay
|
|
264
|
+
others={others} // from usePresence
|
|
265
|
+
containerRef={containerRef}
|
|
266
|
+
cursorKey="cursor" // key in presence payload (default: "cursor")
|
|
267
|
+
/>
|
|
268
|
+
</div>;
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The agent's cursor renders distinctly with a sparkle icon. Cursors fade out after 10s of inactivity with smooth CSS transitions at 120ms.
|
|
272
|
+
|
|
273
|
+
### `RemoteSelectionRings` {#remote-selection-rings}
|
|
274
|
+
|
|
275
|
+
Renders colored outline rings + name tags over remotely-selected elements:
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
import { RemoteSelectionRings } from "@agent-native/core/client";
|
|
279
|
+
|
|
280
|
+
<div ref={containerRef} style={{ position: "relative" }}>
|
|
281
|
+
{content}
|
|
282
|
+
<RemoteSelectionRings
|
|
283
|
+
others={others}
|
|
284
|
+
selectionKey="selection" // key in presence payload (default: "selection")
|
|
285
|
+
resolveRect={(descriptor) =>
|
|
286
|
+
document.querySelector(descriptor)?.getBoundingClientRect() ?? null
|
|
287
|
+
}
|
|
288
|
+
containerRef={containerRef}
|
|
289
|
+
/>
|
|
290
|
+
</div>;
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### `useFollowUser` {#follow-user}
|
|
294
|
+
|
|
295
|
+
Invoke a callback whenever the followed participant's viewport changes:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { useFollowUser } from "@agent-native/core/client";
|
|
299
|
+
|
|
300
|
+
const { isFollowing, stopFollowing } = useFollowUser({
|
|
301
|
+
others,
|
|
302
|
+
followingId, // null to stop following
|
|
303
|
+
viewportKey: "viewport",
|
|
304
|
+
onViewport: (vp) => {
|
|
305
|
+
if (vp.fileId) setActiveFileId(vp.fileId);
|
|
306
|
+
if (vp.zoom) setZoom(vp.zoom);
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Participants publish their viewport with `setPresence({ viewport: { fileId, zoom } })`.
|
|
312
|
+
|
|
313
|
+
### `PresenceBar` follow-mode props {#presence-bar-follow}
|
|
314
|
+
|
|
315
|
+
The `PresenceBar` component now accepts optional follow-mode props:
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
<PresenceBar
|
|
319
|
+
activeUsers={activeUsers}
|
|
320
|
+
agentActive={agentActive}
|
|
321
|
+
onAvatarClick={(user) => {
|
|
322
|
+
// user is null for the agent avatar
|
|
323
|
+
const email = user?.email ?? "agent@system";
|
|
324
|
+
setFollowing((prev) => (prev === email ? null : email));
|
|
325
|
+
}}
|
|
326
|
+
followingEmail={followingEmail} // highlighted avatar + "Following X" chip
|
|
327
|
+
/>
|
|
328
|
+
```
|
|
169
329
|
|
|
170
|
-
|
|
171
|
-
pnpm action edit-document --id doc123 --edits '[{"find":"old","replace":"new"}]'
|
|
330
|
+
### Normalized coordinate helpers {#norm-coords}
|
|
172
331
|
|
|
173
|
-
|
|
174
|
-
|
|
332
|
+
```typescript
|
|
333
|
+
import { toNormalized, fromNormalized } from "@agent-native/core/client";
|
|
334
|
+
|
|
335
|
+
// In a pointer event handler:
|
|
336
|
+
const norm = toNormalized(
|
|
337
|
+
e.clientX,
|
|
338
|
+
e.clientY,
|
|
339
|
+
container.getBoundingClientRect(),
|
|
340
|
+
);
|
|
341
|
+
setPresence({ cursor: norm });
|
|
342
|
+
|
|
343
|
+
// In a cursor renderer:
|
|
344
|
+
const px = fromNormalized(norm, container.getBoundingClientRect());
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Agent cursor plumbing {#agent-cursor}
|
|
348
|
+
|
|
349
|
+
Server-side actions call `agentUpdateSelection()` to publish where the agent is working. The design template's `edit-design` and `generate-design` actions call this automatically. Other templates can do the same:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import {
|
|
353
|
+
agentEnterDocument,
|
|
354
|
+
agentLeaveDocument,
|
|
355
|
+
agentUpdateSelection,
|
|
356
|
+
} from "@agent-native/core/collab";
|
|
357
|
+
|
|
358
|
+
agentEnterDocument(docId);
|
|
359
|
+
agentUpdateSelection(docId, {
|
|
360
|
+
selection: "#target-element",
|
|
361
|
+
editingFile: "index.html",
|
|
362
|
+
});
|
|
363
|
+
try {
|
|
364
|
+
// ... perform edits ...
|
|
365
|
+
} finally {
|
|
366
|
+
agentLeaveDocument(docId);
|
|
367
|
+
}
|
|
175
368
|
```
|
|
176
369
|
|
|
177
|
-
|
|
370
|
+
The selection metadata flows through `usePresence` on connected clients as `other.presence.selection`.
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Route table {#routes}
|
|
375
|
+
|
|
376
|
+
All routes are auto-mounted under `/_agent-native/collab/` by the collab
|
|
377
|
+
plugin:
|
|
378
|
+
|
|
379
|
+
| Route | Purpose |
|
|
380
|
+
| ----------------------------- | ----------------------------------------------------------- |
|
|
381
|
+
| `GET /:docId/state` | Full Y.Doc state (base64). Accepts `?stateVector=` for diff |
|
|
382
|
+
| `POST /:docId/update` | Apply client Yjs update (base64). Max 2 MB by default |
|
|
383
|
+
| `POST /:docId/text` | Apply full text replacement (diff-based) |
|
|
384
|
+
| `POST /:docId/search-replace` | Surgical find/replace in Y.XmlFragment |
|
|
385
|
+
| `POST /:docId/json` | Apply full JSON diff to Y.Map/Y.Array |
|
|
386
|
+
| `GET /:docId/json` | Read current JSON state |
|
|
387
|
+
| `POST /:docId/patch` | Apply surgical JSON patch ops (upsert/remove/reorder) |
|
|
388
|
+
| `POST /:docId/awareness` | Sync cursor/presence state |
|
|
389
|
+
| `GET /:docId/users` | List active users on a document |
|
|
390
|
+
|
|
391
|
+
## Transport and performance {#transport}
|
|
392
|
+
|
|
393
|
+
| Property | Value |
|
|
394
|
+
| ---------------------------- | ---------------------------------------------------------- |
|
|
395
|
+
| Update debounce | ~80 ms (coalesces rapid keystrokes via `Y.mergeUpdates`) |
|
|
396
|
+
| Poll interval (no SSE) | 2 s (configurable via `pollInterval`) |
|
|
397
|
+
| Poll interval (SSE healthy) | ~12 s (configurable via `pollIntervalWithSse`) |
|
|
398
|
+
| State-vector fetch frequency | On reconnect, ring-buffer gap, or every 15th poll cycle |
|
|
399
|
+
| Backoff on error | Exponential with jitter, cap ~15 s |
|
|
400
|
+
| Max payload (writes) | 2 MB default, configurable via `maxPayloadBytes` |
|
|
401
|
+
| Compaction threshold | Stored blob > 4× fresh encoding triggers tombstone compact |
|
|
402
|
+
| Per-write DB reads | 1 (CAS version read inside `persistMergedState` only) |
|
|
178
403
|
|
|
179
|
-
##
|
|
404
|
+
## Security {#security}
|
|
405
|
+
|
|
406
|
+
### Always set `resourceType`
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
createCollabPlugin({
|
|
410
|
+
resourceType: "document", // the name passed to registerShareableResource
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Without `resourceType` the plugin logs a warning and broadcasts collab push
|
|
415
|
+
events to all authenticated users on the deployment without document-level
|
|
416
|
+
scoping. Non-owners fall back to state-vector catch-up (safe but higher
|
|
417
|
+
latency) regardless of whether `resourceType` is set.
|
|
418
|
+
|
|
419
|
+
### Access checks
|
|
420
|
+
|
|
421
|
+
All collab routes require authentication. When `resourceType` is set, reads
|
|
422
|
+
require at least viewer access and writes require editor access, using the
|
|
423
|
+
same `resolveAccess` / `assertAccess` helpers as the sharing system. A 404
|
|
424
|
+
(not 403) is returned on access failures to avoid leaking document existence.
|
|
425
|
+
|
|
426
|
+
### Payload limits
|
|
427
|
+
|
|
428
|
+
Write routes (`update`, `text`, `json`, `patch`, `search-replace`) reject
|
|
429
|
+
payloads exceeding the configured limit with HTTP 413. The default is 2 MB.
|
|
430
|
+
Override per-plugin:
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
createCollabPlugin({
|
|
434
|
+
resourceType: "document",
|
|
435
|
+
maxPayloadBytes: 512 * 1024, // 512 KB
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Awareness scoping
|
|
440
|
+
|
|
441
|
+
Awareness routes (`POST /awareness`, `GET /users`) are gated by the same
|
|
442
|
+
access check as reads — a user who lacks viewer access cannot learn who else
|
|
443
|
+
is editing a document.
|
|
444
|
+
|
|
445
|
+
## Patterns {#patterns}
|
|
446
|
+
|
|
447
|
+
### Granular server-side merge for structured data
|
|
448
|
+
|
|
449
|
+
For structured documents (slide decks, form builders, design files) the Yjs
|
|
450
|
+
body collab model can conflict when two agents or users rewrite the same
|
|
451
|
+
top-level record simultaneously. The safer pattern is **granular server-side
|
|
452
|
+
merge**: define an action that accepts a set of targeted operations and
|
|
453
|
+
applies them atomically, so concurrent edits to different items both survive.
|
|
454
|
+
|
|
455
|
+
**Slides (`patch-deck`)** — Instead of replacing the entire deck JSON on every
|
|
456
|
+
change, the action accepts per-slide operations:
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
// Conceptual patch-deck action shape
|
|
460
|
+
type PatchDeckOp =
|
|
461
|
+
| { type: "patch"; slideId: string; fields: Partial<SlideFields> }
|
|
462
|
+
| { type: "add"; position: number; slide: SlideData }
|
|
463
|
+
| { type: "delete"; slideId: string }
|
|
464
|
+
| { type: "reorder"; slideId: string; newIndex: number };
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Two users editing different slides both succeed; there is no LWW clobber at
|
|
468
|
+
the deck level.
|
|
469
|
+
|
|
470
|
+
**Forms (`patch-form-fields`)** — Field-level merge with upsert/remove/reorder
|
|
471
|
+
ops so concurrent edits to different form fields both survive.
|
|
472
|
+
|
|
473
|
+
Use this pattern when:
|
|
474
|
+
|
|
475
|
+
- The document is structured (items inside a container).
|
|
476
|
+
- Concurrent edits target different items.
|
|
477
|
+
- Body collab (Yjs `Y.XmlFragment`) is overkill or inapplicable.
|
|
478
|
+
|
|
479
|
+
Use body collab (Y.XmlFragment + TipTap) when:
|
|
480
|
+
|
|
481
|
+
- The document is free-form rich text where any region can be edited.
|
|
482
|
+
- Cursor-level CRDT merge matters.
|
|
483
|
+
|
|
484
|
+
### Collaborative undo scoping (Y.UndoManager)
|
|
485
|
+
|
|
486
|
+
The Design template uses `Y.UndoManager` to scope undo/redo to the local
|
|
487
|
+
user's own edits. Remote peer edits and agent edits are never undone by a
|
|
488
|
+
user's Cmd+Z.
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
import * as Y from "yjs";
|
|
492
|
+
|
|
493
|
+
const LOCAL_EDIT_ORIGIN = "local";
|
|
494
|
+
|
|
495
|
+
const undoManager = new Y.UndoManager(ydoc.getText("content"), {
|
|
496
|
+
trackedOrigins: new Set([LOCAL_EDIT_ORIGIN]),
|
|
497
|
+
captureTimeout: 800, // coalesce rapid slider drags into one undo step
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Wrap local edits with the tracked origin
|
|
501
|
+
ydoc.transact(() => {
|
|
502
|
+
// apply local style change
|
|
503
|
+
}, LOCAL_EDIT_ORIGIN);
|
|
504
|
+
|
|
505
|
+
// Undo/redo — only reverses LOCAL_EDIT_ORIGIN transactions
|
|
506
|
+
undoManager.undo(); // Cmd+Z
|
|
507
|
+
undoManager.redo(); // Shift+Cmd+Z
|
|
508
|
+
```
|
|
180
509
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
-
|
|
184
|
-
|
|
185
|
-
-
|
|
510
|
+
Key properties:
|
|
511
|
+
|
|
512
|
+
- `trackedOrigins` must be a `Set`. Only transactions with a matching origin
|
|
513
|
+
are captured in the undo stack.
|
|
514
|
+
- Remote updates (origin `"remote"`) and agent updates (origin `"agent"`) are
|
|
515
|
+
never captured.
|
|
516
|
+
- Recreate and dispose the manager when the active document changes; stale
|
|
517
|
+
managers hold references that can grow unboundedly.
|
|
518
|
+
|
|
519
|
+
## Known limitations {#limitations}
|
|
520
|
+
|
|
521
|
+
- **Same-region simultaneous rewrite is LWW** — If the agent rewrites a
|
|
522
|
+
passage and a human has unsaved edits in the exact same region, the
|
|
523
|
+
lead-client snapshot can overwrite the human's in-flight changes. Edits in
|
|
524
|
+
different regions merge correctly via the CRDT. Granular server-side merge
|
|
525
|
+
(see above) avoids this for structured documents.
|
|
526
|
+
- **In-process write locks on serverless** — The `_writeLocks` map is
|
|
527
|
+
process-local. Concurrent requests landing on different serverless
|
|
528
|
+
invocations serialize at the SQL CAS layer (optimistic concurrency) rather
|
|
529
|
+
than the in-memory lock. This is safe but means high-throughput scenarios on
|
|
530
|
+
serverless may see more CAS retries.
|
|
531
|
+
- **Awareness is per-process** — The awareness in-memory store is
|
|
532
|
+
process-local. Serverless / multi-process deployments see partial awareness
|
|
533
|
+
state per invocation. Clients still receive full awareness snapshots on each
|
|
534
|
+
poll cycle, so presence indicators update within one poll interval.
|
|
535
|
+
|
|
536
|
+
## Presence {#presence}
|
|
537
|
+
|
|
538
|
+
The `useCollaborativeDoc` hook returns:
|
|
539
|
+
|
|
540
|
+
- `activeUsers` — array of `CollabUser` (name, email, color) for all peers
|
|
541
|
+
currently in the document (sourced from awareness).
|
|
542
|
+
- `agentActive` — `true` briefly after the agent makes an edit (use for a
|
|
543
|
+
transient visual indicator).
|
|
544
|
+
- `agentPresent` — `true` while the agent has an active awareness entry
|
|
545
|
+
(durable presence heartbeat).
|
|
546
|
+
|
|
547
|
+
Use `emailToColor(email)` and `emailToName(email)` from
|
|
548
|
+
`@agent-native/core/client` to generate consistent cursor colors and display
|
|
549
|
+
names from email addresses.
|
|
550
|
+
|
|
551
|
+
A `PresenceBar` rendered with `activeUsers` shows live human and agent
|
|
552
|
+
collaborators. Per-slide presence (which users are viewing a given slide)
|
|
553
|
+
layers on top of the same awareness state.
|
|
554
|
+
|
|
555
|
+
## Related docs {#related}
|
|
556
|
+
|
|
557
|
+
- [Real-Time Sync](/docs/real-time-sync) — the `useDbSync` + `useChangeVersion`
|
|
558
|
+
system that delivers the `updatedAt` bump driving editor reconciliation.
|
|
559
|
+
- [Security](/docs/security) — `registerShareableResource`, `resolveAccess`,
|
|
560
|
+
and `assertAccess` for the access model referenced by `resourceType`.
|
|
561
|
+
- [Sharing](/docs/sharing) — how documents are shared and how access is granted.
|
|
562
|
+
- [Template: Content](/docs/template-content) — reference implementation of
|
|
563
|
+
collaborative rich-text editing.
|
|
564
|
+
- [Template: Slides](/docs/template-slides) — granular `patch-deck` action for
|
|
565
|
+
structured concurrent editing.
|
|
566
|
+
- [Template: Forms](/docs/template-forms) — field-level `patch-form-fields`
|
|
567
|
+
server-side merge.
|
|
568
|
+
- [Template: Design](/docs/template-design) — `Y.UndoManager` undo/redo scoped
|
|
569
|
+
to local user edits.
|
|
@@ -132,6 +132,19 @@ Plan app is published this way as a ready-to-add marketplace at the repo root
|
|
|
132
132
|
see [Plan plugin & marketplace](/docs/plan-plugin) for the end-to-end install
|
|
133
133
|
and auto-update flow.
|
|
134
134
|
|
|
135
|
+
For users who install copied skills through the universal CLI instead of a
|
|
136
|
+
plugin marketplace, use the CLI freshness commands:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npx @agent-native/core@latest skills status visual-plan
|
|
140
|
+
npx @agent-native/core@latest skills update visual-plan
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`skills update` scans known Codex/Claude project and user skill folders, compares
|
|
144
|
+
the copied folder hash to the latest bundled skill, and rewrites stale folders in
|
|
145
|
+
place. Newly copied Agent Native skills include an `agent-native-skill.json`
|
|
146
|
+
marker so future status output can identify the source and hash.
|
|
147
|
+
|
|
135
148
|
## Creating custom skills {#creating-skills}
|
|
136
149
|
|
|
137
150
|
Create a skill when:
|