@agent-native/core 0.39.0 → 0.39.2
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/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/skills.d.ts +5 -5
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +458 -615
- 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/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/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/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 +1 -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
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
name: delegate-to-agent
|
|
3
3
|
description: >-
|
|
4
4
|
How to delegate all AI work to the agent chat. Use when delegating AI work
|
|
5
|
-
from UI or scripts to the agent, when
|
|
6
|
-
when
|
|
5
|
+
from UI or scripts to the agent, when a user asks for agent behavior or
|
|
6
|
+
LLM-powered features, when tempted to add inline LLM calls, or when sending
|
|
7
|
+
messages to the agent from application code.
|
|
8
|
+
metadata:
|
|
9
|
+
internal: true
|
|
7
10
|
---
|
|
8
11
|
|
|
9
12
|
# Delegate All AI to the Agent
|
|
@@ -14,7 +17,7 @@ The UI and server never call an LLM directly. All AI work is delegated to the ag
|
|
|
14
17
|
|
|
15
18
|
## Why
|
|
16
19
|
|
|
17
|
-
The agent is the single AI interface. It has context about the full project, can read/write
|
|
20
|
+
The agent is the single AI interface. It has context about the full project, can read/write any file, and can run scripts. Inline LLM calls bypass this — they create a shadow AI that doesn't know what the agent knows and can't coordinate with it.
|
|
18
21
|
|
|
19
22
|
## How
|
|
20
23
|
|
|
@@ -123,6 +126,55 @@ Buttons that produce new content ("New Design", "Create Dashboard", "Make Deck",
|
|
|
123
126
|
|
|
124
127
|
If you find yourself writing `submit: true` with a hardcoded creative verb (`"design a..."`, `"write a..."`, `"build a..."`), stop and add a Popover.
|
|
125
128
|
|
|
129
|
+
## Delegating to a Sub-Agent (Agent Teams)
|
|
130
|
+
|
|
131
|
+
`sendToAgentChat()` delegates from app code _to_ the agent. The other axis of
|
|
132
|
+
delegation is the agent handing work _to a sub-agent_ through the Agent Teams
|
|
133
|
+
run-manager. The main chat stays the orchestrator: it spawns sub-agents, then
|
|
134
|
+
reads and integrates their results.
|
|
135
|
+
|
|
136
|
+
### When to spawn a sub-agent vs do it yourself
|
|
137
|
+
|
|
138
|
+
- **Do it yourself** when the work is small, on the critical path, or tightly
|
|
139
|
+
coupled to what you're already doing. Sub-agent overhead and coordination risk
|
|
140
|
+
outweigh the benefit.
|
|
141
|
+
- **Spawn a sub-agent** for a self-contained unit of work that can run
|
|
142
|
+
independently — a disjoint investigation, an isolated implementation slice, a
|
|
143
|
+
long-running search — especially when it frees the main thread to keep
|
|
144
|
+
orchestrating.
|
|
145
|
+
|
|
146
|
+
### Briefing contract
|
|
147
|
+
|
|
148
|
+
Every sub-agent brief must specify four things, or the sub-agent will guess:
|
|
149
|
+
|
|
150
|
+
- **Objective** — the one concrete outcome it owns, in a sentence.
|
|
151
|
+
- **Context** — the facts it needs (paths, prior findings, constraints) so it
|
|
152
|
+
doesn't re-derive them.
|
|
153
|
+
- **Output** — the exact shape you want back (a summary, a file edited, a list
|
|
154
|
+
of paths, a yes/no with rationale).
|
|
155
|
+
- **Boundaries** — what it must NOT touch (files, branches, side effects) and
|
|
156
|
+
when to stop and report rather than push forward.
|
|
157
|
+
|
|
158
|
+
### Fan-out discipline
|
|
159
|
+
|
|
160
|
+
- **Default to a single sub-agent.** Most delegation is one focused task.
|
|
161
|
+
- **Spawn multiple only for genuinely independent units** that don't share state
|
|
162
|
+
or files. Never parallelize coupled work — if B needs A's output, run them in
|
|
163
|
+
sequence.
|
|
164
|
+
- **Cap parallel fan-out at ~3.** More sub-agents means more synthesis cost and
|
|
165
|
+
more chance of conflicting edits to the same area.
|
|
166
|
+
|
|
167
|
+
### Synthesis discipline
|
|
168
|
+
|
|
169
|
+
- **Read every result** before concluding — don't act on the first one back.
|
|
170
|
+
- **Reconcile conflicts** between sub-agent findings explicitly; decide which is
|
|
171
|
+
right rather than averaging or ignoring.
|
|
172
|
+
- **Integrate into one answer.** The main thread produces the single coherent
|
|
173
|
+
result; it never just forwards raw sub-agent transcripts to the user.
|
|
174
|
+
|
|
175
|
+
Background sub-agents must use the core run-manager / Agent Teams infrastructure
|
|
176
|
+
rather than ad-hoc LLM calls.
|
|
177
|
+
|
|
126
178
|
## Don't
|
|
127
179
|
|
|
128
180
|
- Don't `import Anthropic from "@anthropic-ai/sdk"` in client or server code
|
|
@@ -136,9 +188,27 @@ If you find yourself writing `submit: true` with a hardcoded creative verb (`"de
|
|
|
136
188
|
|
|
137
189
|
Scripts may call external APIs (image generation, search, etc.) — but the AI reasoning and orchestration still goes through the agent. A script is a tool the agent uses, not a replacement for the agent.
|
|
138
190
|
|
|
191
|
+
## When to Use A2A Instead
|
|
192
|
+
|
|
193
|
+
`sendToAgentChat()` delegates work to the **local** agent — the one running alongside your app. When the work should go to a **different** agent entirely (e.g., asking an analytics agent for data, or a calendar agent for availability), use the A2A (agent-to-agent) protocol instead.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { callAgent } from "@agent-native/core/a2a";
|
|
197
|
+
|
|
198
|
+
// Call a different agent — not the local agent chat
|
|
199
|
+
const stats = await callAgent(
|
|
200
|
+
"https://analytics.example.com",
|
|
201
|
+
"What were last week's signups?",
|
|
202
|
+
{ apiKey: process.env.ANALYTICS_A2A_KEY },
|
|
203
|
+
);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
See the **a2a-protocol** skill for the full pattern.
|
|
207
|
+
|
|
139
208
|
## Related Skills
|
|
140
209
|
|
|
141
|
-
- **
|
|
210
|
+
- **a2a-protocol** — When the work goes to a different agent, not the local one
|
|
211
|
+
- **actions** — The agent invokes actions via `pnpm action <name>` to perform complex operations
|
|
142
212
|
- **self-modifying-code** — The agent operates through the chat bridge to make code changes
|
|
143
213
|
- **storing-data** — The agent writes results to the database after processing requests
|
|
144
|
-
- **real-time-sync** — The UI updates automatically when the agent writes
|
|
214
|
+
- **real-time-sync** — The UI updates automatically when the agent writes data
|
|
@@ -8,6 +8,8 @@ description: >-
|
|
|
8
8
|
creative, polished UI that avoids generic AI aesthetics.
|
|
9
9
|
license: Complete terms in LICENSE.txt
|
|
10
10
|
source: https://github.com/anthropics/skills/blob/main/skills/frontend-design/SKILL.md
|
|
11
|
+
metadata:
|
|
12
|
+
internal: true
|
|
11
13
|
---
|
|
12
14
|
|
|
13
15
|
# Frontend Design
|
|
@@ -28,6 +30,19 @@ Before coding, decide:
|
|
|
28
30
|
|
|
29
31
|
Then implement working code that is cohesive, accessible, responsive, and polished in small details: typography, spacing, copy, motion, empty states, loading states, focus states, and error states.
|
|
30
32
|
|
|
33
|
+
## Minimalism And Progressive Disclosure
|
|
34
|
+
|
|
35
|
+
Default to Apple/Linear-level restraint: make the primary workflow obvious, then remove everything that does not help that workflow right now. A polished UI often has fewer visible controls, fewer borders, fewer labels, and fewer explanatory surfaces than the first reasonable implementation.
|
|
36
|
+
|
|
37
|
+
- **Start by subtracting**: Before adding a visible control, banner, toolbar row, card, or explanatory block, ask what can be removed, merged, renamed, or moved into an existing affordance.
|
|
38
|
+
- **One primary action**: Each surface should have one dominant next action. Secondary actions belong in menus, popovers, command palettes, disclosure rows, or contextual hover/focus states unless they are used constantly.
|
|
39
|
+
- **Progressively disclose rare work**: Advanced options, diagnostics, metadata, settings, import/export, destructive actions, and inspection tools should stay tucked away until requested. Prefer small icon triggers with tooltips, popovers, drawers, or detail panels over permanent chrome.
|
|
40
|
+
- **Keep chrome quiet**: Avoid new always-visible bars, badges, callouts, helper text, and counters unless they prevent mistakes or are central to repeated use. Status can often be a dot, ring, muted count, or tooltip.
|
|
41
|
+
- **Favor content over containers**: Do not wrap every section in a card. Use whitespace, alignment, typography, dividers, and full-width bands before adding boxes.
|
|
42
|
+
- **Design for repeated use**: Production app UI should feel calm after the hundredth use. If a control shouts, animates, explains itself, or occupies a full row for an occasional action, hide or compress it.
|
|
43
|
+
- **Make absence intentional**: Empty states should be sparse and action-oriented. Do not fill blank space with marketing copy, decorative art, or lists of features just because the screen feels empty.
|
|
44
|
+
- **Use familiar primitives**: Icon buttons need clear tooltips. Menus, popovers, tabs, switches, and segmented controls should carry complexity instead of exposing every option at once.
|
|
45
|
+
|
|
31
46
|
## Aesthetic Guidelines
|
|
32
47
|
|
|
33
48
|
- **Typography**: Use the product's existing type system first. For net-new public pages, choose characterful but readable type and keep sizing appropriate to the surface.
|
|
@@ -72,6 +87,8 @@ Avoid:
|
|
|
72
87
|
- Custom reimplementations of shadcn primitives.
|
|
73
88
|
- Raw color overrides on shared components when semantic tokens or variants would work.
|
|
74
89
|
- New always-visible controls for rare actions. Prefer menus, popovers, sheets, tabs, collapsibles, or advanced sections.
|
|
90
|
+
- Full-width banners, persistent helper rows, decorative cards, or explanatory chrome for status that could be a compact affordance.
|
|
91
|
+
- Treating progressive disclosure as optional. If a control is not part of the main daily workflow, hide it until context, hover, focus, or explicit user intent makes it relevant.
|
|
75
92
|
- UI cards nested inside other cards.
|
|
76
93
|
- Text or icons that resize or shift fixed-format UI on hover/loading.
|
|
77
94
|
|
|
@@ -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
|