@agent-native/core 0.39.1 → 0.40.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 -1
- package/dist/action.js +12 -0
- package/dist/action.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +5 -1
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/skills.d.ts +6 -6
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +936 -1167
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +2 -5
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/NewWorkspaceAppFlow.js +1 -1
- package/dist/client/NewWorkspaceAppFlow.js.map +1 -1
- package/dist/client/blocks/AiEditableField.d.ts +8 -0
- package/dist/client/blocks/AiEditableField.d.ts.map +1 -0
- package/dist/client/blocks/AiEditableField.js +10 -0
- package/dist/client/blocks/AiEditableField.js.map +1 -0
- package/dist/client/blocks/BlockView.d.ts +3 -3
- package/dist/client/blocks/BlockView.d.ts.map +1 -1
- package/dist/client/blocks/BlockView.js +15 -3
- package/dist/client/blocks/BlockView.js.map +1 -1
- package/dist/client/blocks/SchemaBlockEditor.js +2 -2
- package/dist/client/blocks/SchemaBlockEditor.js.map +1 -1
- package/dist/client/blocks/index.d.ts +5 -2
- package/dist/client/blocks/index.d.ts.map +1 -1
- package/dist/client/blocks/index.js +6 -3
- package/dist/client/blocks/index.js.map +1 -1
- package/dist/client/blocks/library/ApiEndpointBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/ApiEndpointBlock.js +20 -6
- package/dist/client/blocks/library/ApiEndpointBlock.js.map +1 -1
- package/dist/client/blocks/library/DiffBlock.d.ts +29 -0
- package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/DiffBlock.js +190 -30
- package/dist/client/blocks/library/DiffBlock.js.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.js +46 -7
- package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
- package/dist/client/blocks/library/HighlightedCode.d.ts +10 -0
- package/dist/client/blocks/library/HighlightedCode.d.ts.map +1 -0
- package/dist/client/blocks/library/HighlightedCode.js +92 -0
- package/dist/client/blocks/library/HighlightedCode.js.map +1 -0
- package/dist/client/blocks/library/JsonExplorerBlock.d.ts +9 -4
- package/dist/client/blocks/library/JsonExplorerBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/JsonExplorerBlock.js +66 -30
- package/dist/client/blocks/library/JsonExplorerBlock.js.map +1 -1
- package/dist/client/blocks/library/MermaidBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/MermaidBlock.js +73 -44
- package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
- package/dist/client/blocks/library/OpenApiSpecBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/OpenApiSpecBlock.js +3 -2
- package/dist/client/blocks/library/OpenApiSpecBlock.js.map +1 -1
- package/dist/client/blocks/library/checklist.d.ts.map +1 -1
- package/dist/client/blocks/library/checklist.js +1 -0
- package/dist/client/blocks/library/checklist.js.map +1 -1
- package/dist/client/blocks/library/code-tabs.d.ts.map +1 -1
- package/dist/client/blocks/library/code-tabs.js +183 -102
- package/dist/client/blocks/library/code-tabs.js.map +1 -1
- package/dist/client/blocks/library/columns.config.d.ts +60 -0
- package/dist/client/blocks/library/columns.config.d.ts.map +1 -0
- package/dist/client/blocks/library/columns.config.js +37 -0
- package/dist/client/blocks/library/columns.config.js.map +1 -0
- package/dist/client/blocks/library/columns.d.ts +25 -0
- package/dist/client/blocks/library/columns.d.ts.map +1 -0
- package/dist/client/blocks/library/columns.js +199 -0
- package/dist/client/blocks/library/columns.js.map +1 -0
- package/dist/client/blocks/library/dev-doc-ui.d.ts +2 -1
- package/dist/client/blocks/library/dev-doc-ui.d.ts.map +1 -1
- package/dist/client/blocks/library/dev-doc-ui.js +2 -1
- package/dist/client/blocks/library/dev-doc-ui.js.map +1 -1
- package/dist/client/blocks/library/html.d.ts +1 -1
- package/dist/client/blocks/library/html.d.ts.map +1 -1
- package/dist/client/blocks/library/html.js +34 -4
- package/dist/client/blocks/library/html.js.map +1 -1
- package/dist/client/blocks/library/json-explorer.config.d.ts +3 -1
- package/dist/client/blocks/library/json-explorer.config.d.ts.map +1 -1
- package/dist/client/blocks/library/json-explorer.config.js +30 -1
- package/dist/client/blocks/library/json-explorer.config.js.map +1 -1
- package/dist/client/blocks/library/server-specs.d.ts.map +1 -1
- package/dist/client/blocks/library/server-specs.js +13 -3
- package/dist/client/blocks/library/server-specs.js.map +1 -1
- package/dist/client/blocks/library/specs.d.ts +4 -4
- package/dist/client/blocks/library/specs.d.ts.map +1 -1
- package/dist/client/blocks/library/specs.js +21 -16
- package/dist/client/blocks/library/specs.js.map +1 -1
- package/dist/client/blocks/library/table.config.d.ts +3 -0
- package/dist/client/blocks/library/table.config.d.ts.map +1 -1
- package/dist/client/blocks/library/table.config.js +13 -1
- package/dist/client/blocks/library/table.config.js.map +1 -1
- package/dist/client/blocks/library/table.d.ts.map +1 -1
- package/dist/client/blocks/library/table.js +90 -9
- package/dist/client/blocks/library/table.js.map +1 -1
- package/dist/client/blocks/library/tabs.config.d.ts +16 -8
- package/dist/client/blocks/library/tabs.config.d.ts.map +1 -1
- package/dist/client/blocks/library/tabs.config.js +10 -4
- package/dist/client/blocks/library/tabs.config.js.map +1 -1
- package/dist/client/blocks/library/tabs.d.ts.map +1 -1
- package/dist/client/blocks/library/tabs.js +146 -21
- package/dist/client/blocks/library/tabs.js.map +1 -1
- package/dist/client/blocks/server.d.ts +2 -1
- package/dist/client/blocks/server.d.ts.map +1 -1
- package/dist/client/blocks/server.js +1 -0
- package/dist/client/blocks/server.js.map +1 -1
- package/dist/client/blocks/types.d.ts +99 -9
- package/dist/client/blocks/types.d.ts.map +1 -1
- package/dist/client/blocks/types.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/rich-markdown-editor/BubbleToolbar.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/BubbleToolbar.js +13 -3
- package/dist/client/rich-markdown-editor/BubbleToolbar.js.map +1 -1
- package/dist/client/rich-markdown-editor/DragHandle.d.ts +49 -4
- package/dist/client/rich-markdown-editor/DragHandle.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/DragHandle.js +656 -88
- package/dist/client/rich-markdown-editor/DragHandle.js.map +1 -1
- package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts +10 -1
- package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/RegistryBlockNode.js +180 -15
- package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts +2 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.js +3 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.js.map +1 -1
- package/dist/client/rich-markdown-editor/SlashCommandMenu.d.ts +5 -0
- package/dist/client/rich-markdown-editor/SlashCommandMenu.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/SlashCommandMenu.js +33 -5
- package/dist/client/rich-markdown-editor/SlashCommandMenu.js.map +1 -1
- package/dist/client/rich-markdown-editor/index.d.ts +3 -3
- package/dist/client/rich-markdown-editor/index.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/index.js +2 -2
- package/dist/client/rich-markdown-editor/index.js.map +1 -1
- package/dist/client/rich-markdown-editor/registrySlashCommands.d.ts +14 -0
- package/dist/client/rich-markdown-editor/registrySlashCommands.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/registrySlashCommands.js +38 -0
- package/dist/client/rich-markdown-editor/registrySlashCommands.js.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts +1 -0
- package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.js +4 -0
- package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +11 -19
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/use-chat-models.d.ts.map +1 -1
- package/dist/client/use-chat-models.js +2 -5
- package/dist/client/use-chat-models.js.map +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +17 -1
- package/dist/db/client.js.map +1 -1
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +2 -1
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/route-discovery.d.ts +29 -0
- package/dist/deploy/route-discovery.d.ts.map +1 -1
- package/dist/deploy/route-discovery.js +158 -11
- package/dist/deploy/route-discovery.js.map +1 -1
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +9 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/sharing/access.d.ts +4 -2
- package/dist/sharing/access.d.ts.map +1 -1
- package/dist/sharing/access.js +8 -3
- package/dist/sharing/access.js.map +1 -1
- package/dist/sharing/actions/set-resource-visibility.d.ts.map +1 -1
- package/dist/sharing/actions/set-resource-visibility.js +2 -3
- package/dist/sharing/actions/set-resource-visibility.js.map +1 -1
- package/dist/sharing/registry.d.ts +13 -0
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js.map +1 -1
- package/dist/styles/rich-markdown-editor.css +15 -0
- package/dist/templates/default/.agents/skills/actions/SKILL.md +96 -11
- package/dist/templates/default/.agents/skills/adding-a-feature/SKILL.md +126 -26
- package/dist/templates/default/.agents/skills/capture-learnings/SKILL.md +56 -30
- package/dist/templates/default/.agents/skills/create-skill/SKILL.md +28 -0
- package/dist/templates/default/.agents/skills/delegate-to-agent/SKILL.md +75 -5
- package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +17 -0
- package/dist/templates/default/.agents/skills/real-time-collab/SKILL.md +99 -124
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +43 -10
- package/dist/templates/default/.agents/skills/security/SKILL.md +162 -144
- package/dist/templates/default/.agents/skills/self-modifying-code/SKILL.md +5 -3
- package/dist/templates/default/.agents/skills/shadcn-ui/SKILL.md +15 -0
- package/dist/templates/default/.agents/skills/storing-data/SKILL.md +116 -83
- package/dist/templates/default/DEVELOPING.md +10 -13
- package/dist/templates/workspace-core/.agents/skills/client-methods/references/legacy-client-fetch-audit-2026-06-03.md +9 -0
- package/dist/templates/workspace-core/.agents/skills/writing-agent-instructions/SKILL.md +27 -0
- package/docs/content/template-plan.md +5 -3
- package/docs/content/visual-plans.md +5 -2
- package/package.json +16 -1
- package/src/templates/default/.agents/skills/actions/SKILL.md +96 -11
- package/src/templates/default/.agents/skills/adding-a-feature/SKILL.md +126 -26
- package/src/templates/default/.agents/skills/capture-learnings/SKILL.md +56 -30
- package/src/templates/default/.agents/skills/create-skill/SKILL.md +28 -0
- package/src/templates/default/.agents/skills/delegate-to-agent/SKILL.md +75 -5
- package/src/templates/default/.agents/skills/frontend-design/SKILL.md +17 -0
- package/src/templates/default/.agents/skills/real-time-collab/SKILL.md +99 -124
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +43 -10
- package/src/templates/default/.agents/skills/security/SKILL.md +162 -144
- package/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +5 -3
- package/src/templates/default/.agents/skills/shadcn-ui/SKILL.md +15 -0
- package/src/templates/default/.agents/skills/storing-data/SKILL.md +116 -83
- package/src/templates/default/DEVELOPING.md +10 -13
- package/src/templates/workspace-core/.agents/skills/client-methods/references/legacy-client-fetch-audit-2026-06-03.md +9 -0
- package/src/templates/workspace-core/.agents/skills/writing-agent-instructions/SKILL.md +27 -0
|
@@ -1,183 +1,158 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: real-time-collab
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Multi-user collaborative editing with Yjs CRDT and live cursors. Use when
|
|
5
|
+
adding real-time collaborative editing to a template, debugging sync issues,
|
|
6
|
+
or understanding how the agent and humans edit documents simultaneously.
|
|
7
|
+
metadata:
|
|
8
|
+
internal: true
|
|
6
9
|
---
|
|
7
10
|
|
|
8
11
|
# Real-Time Collaboration
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
## Rule
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
Collaborative editing uses Yjs CRDT via TipTap. The agent and human users are equal participants — both edit the same Y.Doc and changes merge cleanly without conflicts.
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
User A (TipTap + Collaboration ext) ←→ Y.XmlFragment ←→ Server (_collab_docs table)
|
|
16
|
-
User B (TipTap + Collaboration ext) ←→ Y.XmlFragment ←→ ↑
|
|
17
|
-
Agent (edit-document action) ←→ search-replace endpoint ─┘
|
|
18
|
-
```
|
|
17
|
+
## How It Works
|
|
19
18
|
|
|
20
|
-
-
|
|
19
|
+
- **`Y.Doc`** stores the document as a `Y.XmlFragment` (ProseMirror node tree)
|
|
21
20
|
- **TipTap's Collaboration extension** binds the editor to the Y.XmlFragment via `ySyncPlugin`
|
|
22
21
|
- **CollaborationCaret extension** renders remote users' cursors with names and colors
|
|
23
22
|
- **Polling** (every 2s) syncs Y.Doc updates and awareness state between clients and server
|
|
24
|
-
- **SQL `_collab_docs` table** persists Yjs state
|
|
23
|
+
- **SQL `_collab_docs` table** persists Yjs state as base64-encoded binary (works across SQLite/Postgres)
|
|
25
24
|
|
|
26
|
-
##
|
|
25
|
+
## Agent + Human Editing
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
1. **Human edits** → TipTap → ySyncPlugin → Y.XmlFragment → `POST /_agent-native/collab/:docId/update`
|
|
28
|
+
2. **Agent edits** → action edits canonical SQL content + bumps `updatedAt` → change-sync refetch → the open editor reconciles the new content into the live Y.Doc (see below) → poll update → all clients
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
Both produce Yjs operations that merge cleanly. Agent edits appear without destroying cursor position, selection, or undo history.
|
|
31
|
+
|
|
32
|
+
This is how content (documents) and slides now work. The agent does **not** push edits into Yjs in-process, and it does **not** call any `findCollabOrigin()` / localhost probe — that approach silently no-op'd on serverless (the action runs in a different process), so agent edits didn't show up live until the user navigated away and back. Nor does it search-and-replace inside existing Y.XmlText nodes, which could never create new block structure (lists, headings, tables). The peer-editor model below replaces both.
|
|
33
|
+
|
|
34
|
+
## Agent Edits As A Real-Time Peer Editor
|
|
35
|
+
|
|
36
|
+
The agent edits documents the same way a human collaborator does: its change lands in the shared Y.Doc, propagates to every connected client, and persists. It gets there without any in-process Yjs push from the action.
|
|
37
|
+
|
|
38
|
+
**SQL is the durable source of truth for document body content.** The agent action edits the canonical content (e.g. `documents.content`) and bumps `updatedAt`. That's the whole server side — no localhost calls, no Yjs mutation from the action.
|
|
39
|
+
|
|
40
|
+
**The open editor reconciles authoritative external content into the live Y.Doc.** The action's `updatedAt` bump flows through the change-sync system (see `real-time-sync`), which refetches the record. The editor applies the new content through its real markdown/HTML pipeline via `setContent`, so new block structure (lists, headings, tables) renders correctly and merges with concurrent human edits through the Yjs CRDT diff. The result: the agent's edit propagates to every connected client and persists, exactly like a human collaborator's edit.
|
|
41
|
+
|
|
42
|
+
### The `updatedAt` gate
|
|
43
|
+
|
|
44
|
+
The editor only adopts content that is genuinely **newer** than what it already reflects. An older-or-equal `updatedAt` is a lagging poll or a stale snapshot and is **ignored**.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// Pseudocode in the editor's reconcile effect
|
|
48
|
+
if (loaded.updatedAt > lastAppliedUpdatedAt.current) {
|
|
49
|
+
applyAuthoritativeContent(loaded.content); // adopt
|
|
50
|
+
lastAppliedUpdatedAt.current = loaded.updatedAt;
|
|
51
|
+
}
|
|
52
|
+
// else: lagging poll / stale snapshot → ignore
|
|
32
53
|
```
|
|
33
54
|
|
|
34
|
-
|
|
55
|
+
**Why:** without the gate, a slightly-behind poll response re-applies old content right after the agent's edit, so the edit "reverts on the next poll" / "doesn't show until refresh" — the whack-a-mole we kept hitting. A **fresh mount or doc-switch has no baseline**, so it always adopts whatever content it loaded — which is why a manual refresh is always correct.
|
|
56
|
+
|
|
57
|
+
### Lead-client election
|
|
58
|
+
|
|
59
|
+
Exactly ONE connected client applies an authoritative snapshot into the shared Y.Doc; the rest receive it through normal Yjs sync. The lead is the present client with the lowest Yjs `clientID`, decided by the core helper:
|
|
35
60
|
|
|
36
|
-
In `vite.config.ts`:
|
|
37
61
|
```ts
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"@tiptap/y-tiptap",
|
|
47
|
-
],
|
|
48
|
-
},
|
|
49
|
-
});
|
|
62
|
+
import { isReconcileLeadClient } from "@agent-native/core/client";
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
loaded.updatedAt > lastAppliedUpdatedAt.current &&
|
|
66
|
+
isReconcileLeadClient(provider.awareness, ydoc.clientID)
|
|
67
|
+
) {
|
|
68
|
+
applyAuthoritativeContent(loaded.content);
|
|
69
|
+
}
|
|
50
70
|
```
|
|
51
71
|
|
|
52
|
-
|
|
72
|
+
**Why:** if every open editor independently diffed the same snapshot into the CRDT, each would insert the changed region at the same position, duplicating it N times (concurrent inserts → duplicated text). Electing one lead avoids that. The agent's awareness id (`AGENT_CLIENT_ID`, max int) can never win, and a client editing alone is always the lead. The election is deterministic across clients with no coordination round-trip.
|
|
53
73
|
|
|
54
|
-
###
|
|
74
|
+
### v1 limitation
|
|
75
|
+
|
|
76
|
+
A full-content reconcile is **last-writer-wins for the rare case** where a human has unsaved edits in the exact region the agent simultaneously rewrites — the agent's snapshot can clobber that in-flight human edit. Inline and structural edits in **different** regions merge fine through the CRDT; only same-region simultaneous rewrites are at risk.
|
|
77
|
+
|
|
78
|
+
## Enabling Collaboration
|
|
79
|
+
|
|
80
|
+
### 1. Install packages
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pnpm add @tiptap/extension-collaboration @tiptap/extension-collaboration-caret @tiptap/y-tiptap
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Add collab server plugin
|
|
55
87
|
|
|
56
|
-
Create `server/plugins/collab.ts`:
|
|
57
88
|
```ts
|
|
58
|
-
|
|
89
|
+
// server/plugins/collab.ts
|
|
90
|
+
import { createCollabPlugin } from "@agent-native/core/collab";
|
|
91
|
+
|
|
59
92
|
export default createCollabPlugin({
|
|
60
|
-
table: "
|
|
93
|
+
table: "documents",
|
|
61
94
|
contentColumn: "content",
|
|
62
95
|
idColumn: "id",
|
|
63
|
-
autoSeed: false, // Client-side seeding on first load
|
|
64
96
|
});
|
|
65
97
|
```
|
|
66
98
|
|
|
67
|
-
|
|
68
|
-
- `GET /:docId/state` — fetch Y.Doc state
|
|
69
|
-
- `POST /:docId/update` — apply client update
|
|
70
|
-
- `POST /:docId/text` — apply full text (diff-based)
|
|
71
|
-
- `POST /:docId/search-replace` — surgical text find/replace in Y.XmlFragment
|
|
72
|
-
- `POST /:docId/awareness` — sync cursor/presence state
|
|
73
|
-
|
|
74
|
-
### 4. Use the `useCollaborativeDoc` hook
|
|
99
|
+
### 3. Use the client hook
|
|
75
100
|
|
|
76
101
|
```ts
|
|
77
|
-
import { useCollaborativeDoc
|
|
102
|
+
import { useCollaborativeDoc } from "@agent-native/core/client";
|
|
78
103
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
const { ydoc, awareness, isLoading, activeUsers } = useCollaborativeDoc({
|
|
82
|
-
docId: documentId,
|
|
83
|
-
requestSource: TAB_ID,
|
|
84
|
-
user: { name: "Steve", email: "steve@example.com", color: "#60a5fa" },
|
|
85
|
-
});
|
|
104
|
+
const { ydoc, provider } = useCollaborativeDoc(documentId);
|
|
86
105
|
```
|
|
87
106
|
|
|
88
|
-
|
|
89
|
-
- Creates a stable `Y.Doc` per docId (never changes identity)
|
|
90
|
-
- Fetches server state and applies it
|
|
91
|
-
- Sends local updates to server
|
|
92
|
-
- Polls for remote updates (every 2s)
|
|
93
|
-
- Tracks active users via awareness
|
|
94
|
-
|
|
95
|
-
### 5. Add Collaboration extension to TipTap
|
|
107
|
+
### 4. Add TipTap extensions
|
|
96
108
|
|
|
97
109
|
```ts
|
|
98
|
-
import Collaboration from "@tiptap/extension-collaboration";
|
|
99
|
-
import CollaborationCaret from "@tiptap/extension-collaboration-caret";
|
|
100
|
-
import { Awareness } from "y-protocols/awareness";
|
|
101
|
-
|
|
102
|
-
// Create awareness locally (must use same y-protocols as the caret extension)
|
|
103
|
-
const awareness = new Awareness(ydoc);
|
|
104
|
-
awareness.setLocalStateField("user", { name, color });
|
|
110
|
+
import { Collaboration } from "@tiptap/extension-collaboration";
|
|
111
|
+
import { CollaborationCaret } from "@tiptap/extension-collaboration-caret";
|
|
105
112
|
|
|
106
113
|
const editor = useEditor({
|
|
107
114
|
extensions: [
|
|
108
|
-
StarterKit.configure({ history: false }), // Disable history — Yjs handles undo
|
|
109
115
|
Collaboration.configure({ document: ydoc }),
|
|
110
116
|
CollaborationCaret.configure({
|
|
111
|
-
provider
|
|
112
|
-
user: { name, color },
|
|
117
|
+
provider,
|
|
118
|
+
user: { name: session.email, color: "#6366f1" },
|
|
113
119
|
}),
|
|
114
|
-
// ... other extensions
|
|
115
120
|
],
|
|
116
|
-
content: initialContent, // Seeds Y.XmlFragment on first load
|
|
117
121
|
});
|
|
118
122
|
```
|
|
119
123
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
### 6. Seed the Y.XmlFragment
|
|
123
|
-
|
|
124
|
-
The Collaboration extension does NOT auto-seed from the `content` prop. You must seed manually:
|
|
124
|
+
### 5. Add to vite.config.ts optimizeDeps
|
|
125
125
|
|
|
126
126
|
```ts
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}, [editor, ydoc, content]);
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
**Critical:** Guard against saving empty content back to SQL when the editor is in collab mode but hasn't been seeded yet:
|
|
137
|
-
|
|
138
|
-
```ts
|
|
139
|
-
onUpdate: ({ editor }) => {
|
|
140
|
-
const md = editor.storage.markdown.getMarkdown();
|
|
141
|
-
if (!md.trim() && ydoc) return; // Don't save empty during seeding
|
|
142
|
-
onChange(md);
|
|
127
|
+
optimizeDeps: {
|
|
128
|
+
include: [
|
|
129
|
+
"@tiptap/extension-collaboration",
|
|
130
|
+
"@tiptap/extension-collaboration-caret",
|
|
131
|
+
"@tiptap/y-tiptap",
|
|
132
|
+
],
|
|
143
133
|
}
|
|
144
134
|
```
|
|
145
135
|
|
|
146
|
-
##
|
|
147
|
-
|
|
148
|
-
The `edit-document` action uses search-and-replace:
|
|
149
|
-
```bash
|
|
150
|
-
pnpm action edit-document --id <docId> --find "old text" --replace "new text"
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
When collab state exists, the action calls the server's `search-replace` endpoint which:
|
|
154
|
-
1. Walks the Y.XmlFragment tree
|
|
155
|
-
2. Finds the text in Y.XmlText nodes
|
|
156
|
-
3. Applies minimal delete/insert operations
|
|
157
|
-
4. Emits a Yjs update via the poll system
|
|
158
|
-
5. Client receives the update → ySyncPlugin applies a targeted ProseMirror transaction → cursor preserved
|
|
136
|
+
## Collab Routes (auto-mounted)
|
|
159
137
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
|
165
|
-
|
|
166
|
-
|
|
|
167
|
-
| `
|
|
168
|
-
| `createCollabPlugin` | `packages/core/src/server/collab-plugin.ts` | Route mounting |
|
|
169
|
-
| `searchAndReplace` | `packages/core/src/collab/ydoc-manager.ts` | Y.XmlFragment text mutation |
|
|
138
|
+
| Route | Purpose |
|
|
139
|
+
| ----- | ------- |
|
|
140
|
+
| `GET /_agent-native/collab/:docId/state` | Fetch full Y.Doc state |
|
|
141
|
+
| `POST /_agent-native/collab/:docId/update` | Apply client Yjs update |
|
|
142
|
+
| `POST /_agent-native/collab/:docId/text` | Apply full text (diff-based) |
|
|
143
|
+
| `POST /_agent-native/collab/:docId/search-replace` | Surgical find/replace in Y.XmlFragment |
|
|
144
|
+
| `POST /_agent-native/collab/:docId/awareness` | Sync cursor/presence state |
|
|
145
|
+
| `GET /_agent-native/collab/:docId/users` | List active users |
|
|
170
146
|
|
|
171
147
|
## Common Pitfalls
|
|
172
148
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
3. **Data loss from empty saves** — The `onUpdate` handler fires when the editor initializes with an empty Y.XmlFragment. If this empty content is saved to SQL, it overwrites the real content. Always guard against saving empty content in collab mode.
|
|
178
|
-
|
|
179
|
-
4. **Stale content on document switch** — Use `key={documentId}` on the editor component to force a full remount when switching documents. This ensures the Y.Doc, seeding, and editor state are all fresh.
|
|
149
|
+
- **Don't pass `content` as a TipTap prop** when Collaboration is enabled — Yjs owns the content. Set initial content via the Y.Doc instead.
|
|
150
|
+
- **Don't call `editor.setContent()` ad hoc for agent edits.** The only sanctioned `setContent` is the editor's reconcile path described above — gated by `updatedAt` and guarded by `isReconcileLeadClient`. Calling it from elsewhere (e.g. on every poll, or from every client) re-applies stale content or duplicates the changed region across the CRDT.
|
|
151
|
+
- **Add packages to `optimizeDeps`** — Vite won't pre-bundle Yjs packages correctly otherwise, causing runtime errors in dev.
|
|
152
|
+
- **One `Y.Doc` per document** — Don't create multiple Y.Doc instances for the same document ID. Use the `useCollaborativeDoc` hook which caches by ID.
|
|
180
153
|
|
|
181
|
-
|
|
154
|
+
## Related Skills
|
|
182
155
|
|
|
183
|
-
|
|
156
|
+
- `real-time-sync` — The change-sync system that delivers the `updatedAt` bump driving editor reconciliation; also `useReconciledState` for non-collaborative "copy a server value into local edit state" surfaces
|
|
157
|
+
- `storing-data` — The `_collab_docs` table where Yjs state is persisted; SQL holds the canonical document body that the editor reconciles from
|
|
158
|
+
- `self-modifying-code` — Agent edits to collaborative documents edit canonical SQL content, not raw Yjs
|
|
@@ -4,6 +4,8 @@ description: >-
|
|
|
4
4
|
How to keep the UI in sync with agent changes via SSE plus polling fallback.
|
|
5
5
|
Use when wiring query invalidation for new data models, debugging UI not
|
|
6
6
|
updating, or understanding jitter prevention.
|
|
7
|
+
metadata:
|
|
8
|
+
internal: true
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
# Real-Time Sync
|
|
@@ -20,7 +22,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
|
|
|
20
22
|
|
|
21
23
|
1. **Server** increments a version counter on every database write. In-process events stream through the authenticated `/_agent-native/events` endpoint.
|
|
22
24
|
|
|
23
|
-
2. **Client** listens for
|
|
25
|
+
2. **Client** listens for sync events and updates per-source change counters:
|
|
24
26
|
|
|
25
27
|
```ts
|
|
26
28
|
import { useDbSync } from "@agent-native/core";
|
|
@@ -47,9 +49,9 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
|
|
|
47
49
|
|
|
48
50
|
For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It runs every 2 seconds until SSE is connected, then relaxes to 15 seconds. If SSE is disabled or unavailable, polling continues at the normal cadence.
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
|
|
53
55
|
|
|
54
56
|
## Don't
|
|
55
57
|
|
|
@@ -132,9 +134,9 @@ The `use-navigation-state.ts` hook sends the same `TAB_ID` in the `X-Request-Sou
|
|
|
132
134
|
|
|
133
135
|
Without jitter prevention, a cycle occurs: the UI writes state, sync detects the change, the UI refetches and re-renders, potentially overwriting what the user is actively editing. With `ignoreSource`, the UI only reacts to changes from other sources (agent scripts, other browser tabs, other users).
|
|
134
136
|
|
|
135
|
-
## Action Routes and
|
|
137
|
+
## Action Routes and Live Sync
|
|
136
138
|
|
|
137
|
-
|
|
139
|
+
Actions work with the same sync system. When a mutating action writes to the database, the version counter increments and `useDbSync` picks up the change. Frontend mutations via `useActionMutation` automatically invalidate `["action"]` query keys on success, triggering refetches of `useActionQuery` hooks. Client components should call actions through those hooks, not with raw action-route fetches.
|
|
138
140
|
|
|
139
141
|
For custom apps, the best out-of-the-box path is:
|
|
140
142
|
|
|
@@ -146,14 +148,13 @@ This avoids duplicate `/api/*` JSON CRUD routes and makes agent-created records
|
|
|
146
148
|
|
|
147
149
|
### Auto-emit on mutating actions
|
|
148
150
|
|
|
149
|
-
The framework emits a
|
|
151
|
+
The framework emits a change event with `source: "action"` whenever any non-read-only action runs to completion — whether called via HTTP (`/_agent-native/actions/:name`) or as an agent tool call. Read-only actions (`http: { method: "GET" }` or explicit `readOnly: true`) are skipped.
|
|
150
152
|
|
|
151
153
|
This means UIs don't need the agent to remember to call `refresh-screen` after every mutation. A listener like this (used in the `macros` template) will refresh after any mutating agent call:
|
|
152
154
|
|
|
153
155
|
```ts
|
|
154
156
|
useDbSync({
|
|
155
157
|
queryClient,
|
|
156
|
-
queryKeys: [],
|
|
157
158
|
ignoreSource: TAB_ID,
|
|
158
159
|
onEvent: (data) => {
|
|
159
160
|
if (data.requestSource === TAB_ID) return;
|
|
@@ -165,9 +166,41 @@ useDbSync({
|
|
|
165
166
|
|
|
166
167
|
`refresh-screen` remains available for unusual cases — e.g. the agent mutated data via a path the framework can't see (external system the app mirrors), or the agent wants to pass a `scope` hint for narrower invalidation.
|
|
167
168
|
|
|
169
|
+
## Keeping Stateful Components In Sync
|
|
170
|
+
|
|
171
|
+
The `useChangeVersion` / `useActionQuery` pattern above keeps the **query layer** fresh. But components that copy a server value into local React state still go stale on agent edits — refetching the query updates the prop, yet the local copy never re-adopts it. This is a recurring bug.
|
|
172
|
+
|
|
173
|
+
**Never do this** for a value the agent can mutate:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
// BUG: `title` is captured once and never re-reads the prop.
|
|
177
|
+
const [title, setTitle] = useState(props.title);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
When the agent renames the record, the query refetches, `props.title` updates, but the input still shows the stale value until the component remounts.
|
|
181
|
+
|
|
182
|
+
**Derived-state surfaces (form fields, inline editors, popovers): use `useReconciledState`.** It re-adopts the authoritative external value when it changes, except while the user is actively editing that field — so agent mutations show up live without clobbering in-progress typing:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { useReconciledState } from "@agent-native/core/client";
|
|
186
|
+
|
|
187
|
+
// `active` = true while the user is editing this field (focused / dirty).
|
|
188
|
+
const [title, setTitle] = useReconciledState(props.title, { active: isEditing });
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Collaborative rich-text editors are different** — they don't copy a value into `useState`. They reconcile authoritative SQL content into a shared Y.Doc under an `updatedAt` gate with lead-client election. See `real-time-collab` → "Agent edits as a real-time peer editor". Don't reach for `useReconciledState` for a Yjs-backed editor.
|
|
192
|
+
|
|
193
|
+
| Surface | Keep it fresh with |
|
|
194
|
+
| ------- | ------------------ |
|
|
195
|
+
| React Query reads | `useChangeVersion` / `useActionQuery` (above) |
|
|
196
|
+
| Local edit state copied from a server value (inputs, popovers, inline editors) | `useReconciledState(externalValue, { active })` |
|
|
197
|
+
| Collaborative rich-text editor (Yjs) | `updatedAt`-gated reconcile + `isReconcileLeadClient` — see `real-time-collab` |
|
|
198
|
+
|
|
168
199
|
## Related Skills
|
|
169
200
|
|
|
170
|
-
- **storing-data** — Application-state and settings are
|
|
201
|
+
- **storing-data** — Application-state and settings are data stores that sync through change events
|
|
171
202
|
- **context-awareness** — Navigation state writes use jitter prevention to avoid overwriting active edits
|
|
172
|
-
- **actions** —
|
|
173
|
-
- **
|
|
203
|
+
- **actions** — Mutating actions trigger change events
|
|
204
|
+
- **client-methods** — Route details belong in helpers/hooks, not components
|
|
205
|
+
- **self-modifying-code** — Agent code edits trigger change events; rapid edits can cause event storms
|
|
206
|
+
- **real-time-collab** — Collaborative editors reconcile agent edits into a shared Y.Doc, driven by the same change-sync `updatedAt` bump
|