Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b4b2306
Establish trusted Automerge reference backend
cursoragent Aug 7, 2026
072c26e
Expose the Automerge sync protocol
cursoragent Aug 7, 2026
b2e3945
Add persistent document collaboration
cursoragent Aug 7, 2026
af26887
Connect the document editor to Automerge
cursoragent Aug 7, 2026
3092be6
Regenerate the versioned Automerge module
cursoragent Aug 7, 2026
9e9a822
Materialize live collaboration edits
cursoragent Aug 7, 2026
b8b3208
Reconnect collaborative document sessions
cursoragent Aug 7, 2026
d67e117
Show collaborator cursors in documents
cursoragent Aug 7, 2026
63f1aea
Audit Automerge Rust dependencies
cursoragent Aug 7, 2026
cea43c1
Decode native Automerge binary containers
cursoragent Aug 7, 2026
16594d8
Materialize native Automerge operations
cursoragent Aug 7, 2026
9f22ebc
Encode native Automerge sync messages
cursoragent Aug 7, 2026
5a28b82
Decode Automerge binaries in pure Go
cursoragent Aug 7, 2026
20a7a25
Adopt the validated native history decoder
cursoragent Aug 7, 2026
b2c2ebf
Encode native Automerge changes
cursoragent Aug 7, 2026
83cd5bd
Expose the experimental pure-Go engine
cursoragent Aug 7, 2026
c531de1
Make the pure-Go Automerge engine default
cursoragent Aug 7, 2026
7e34174
Fix Go lint violations
cursoragent Aug 7, 2026
cc2902f
Wait for peer acknowledgements during sync
cursoragent Aug 8, 2026
5efbcc2
Topologically apply merged sync changes
cursoragent Aug 8, 2026
6b83b71
Preserve remote change hashes during sync
cursoragent Aug 8, 2026
93c9033
Restore collaborative block controls
cursoragent Aug 8, 2026
a8710e9
Preserve tables during collaboration
cursoragent Aug 8, 2026
73bff9b
Restore collaborative undo history
cursoragent Aug 8, 2026
b4b47dc
Encode dividers as structural blocks
cursoragent Aug 8, 2026
c1a5f69
Preserve Mermaid language through CRDT
cursoragent Aug 8, 2026
404ebeb
Send incremental Automerge sync changes
cursoragent Aug 8, 2026
5939003
Broadcast edits through in-memory rooms
cursoragent Aug 8, 2026
a9f05f5
Persist append-only collaboration changes
cursoragent Aug 8, 2026
f873804
Compact persisted collaboration changes
cursoragent Aug 8, 2026
9d67f44
Debounce persistence off the sync path
cursoragent Aug 8, 2026
981ca79
Fan out collaboration with PostgreSQL notify
cursoragent Aug 8, 2026
6afc786
Broadcast cursor presence through rooms
cursoragent Aug 8, 2026
0ccfeab
Close collaboration sockets during shutdown
cursoragent Aug 8, 2026
a79dfde
Project legacy dividers without content
cursoragent Aug 8, 2026
3b0055d
Require canonical structural dividers
cursoragent Aug 8, 2026
af91ee6
Store empty Automerge heads as bytes
cursoragent Aug 8, 2026
da804a1
Normalize text after structural dividers
cursoragent Aug 8, 2026
5f2342b
Cover edits during in-flight sync
cursoragent Aug 8, 2026
9cfec6c
Sync text after structural leaf blocks
cursoragent Aug 8, 2026
dc7314e
Add collaboration sync diagnostics
cursoragent Aug 8, 2026
525ba3f
Reconcile remote structural text edits
cursoragent Aug 8, 2026
66219c2
Keep slash commands out of documents
cursoragent Aug 8, 2026
c8a8b1b
Hide placeholder during slash commands
cursoragent Aug 8, 2026
1bd771e
Fix collaboration diagnostic import order
cursoragent Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
bin/
pkg/automerge/internal/reference/wasm/target/
node_modules/
.cache/
.turbo
Expand Down
23 changes: 23 additions & 0 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ NPROC ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || getconf _
MAKEFLAGS := --jobs=$(NPROC)

CAT ?= cat
CARGO ?= cargo
CP ?= cp
DOCKER ?= docker
GO ?= go
Expand All @@ -13,12 +14,14 @@ NPM ?= npm
NPX ?= npx
OPENSSL ?= openssl
SED ?= sed
SHA256SUM ?= sha256sum
SYFT ?= syft
TAIL ?= tail
ECHO ?= echo
GOLINTCMD ?= golangci-lint
SWIFTLINTCMD ?= swiftlint
SWIFTCMD ?= swift
RUST_TOOLCHAIN ?= 1.89.0
SWIFT_ENROLL_UI ?= cmd/probo-agent/installer/macos/enroll-ui
SWIFT_FORMAT_CONFIG ?= .swift-format
SWIFTLINT_CONFIG ?= .swiftlint.yml
Expand Down Expand Up @@ -104,6 +107,9 @@ EMBEDDED= apps/console/dist/index.html \
apps/compliance-portal/dist/index.html \
@probo/emails

AUTOMERGE_REFERENCE_DIR= pkg/automerge/internal/reference
AUTOMERGE_REFERENCE_WASM= $(AUTOMERGE_REFERENCE_DIR)/reference.wasm

PROBOD_BIN_EXTRA_DEPS=
PROBOD_BIN= bin/probod
PROBOD_SRC= cmd/probod/main.go
Expand Down Expand Up @@ -208,6 +214,11 @@ test-verbose: test ## Run tests with verbose output
test-short: TEST_FLAGS+=-short
test-short: test ## Run short tests only

.PHONY: test-automerge-conformance
test-automerge-conformance: ## Test Go binary compatibility with official Automerge JS
AUTOMERGE_JS_ORACLE=$(CURDIR)/packages/automerge-conformance/oracle.mjs \
$(GO_BASE) test -count=1 ./pkg/automerge

.PHONY: coverage-report
coverage-report: test ## Generate HTML coverage report
$(GO) tool cover -html=coverage.out -o coverage.html
Expand Down Expand Up @@ -414,6 +425,18 @@ pkg/server/api/complianceportal/v1/schema.graphql: pkg/server/api/complianceport
.PHONY: generate
generate: $(GENERATED)

.PHONY: generate-automerge-reference
generate-automerge-reference: ## Rebuild the embedded official Automerge WASM backend
cd $(AUTOMERGE_REFERENCE_DIR)/wasm && \
$(CARGO) +$(RUST_TOOLCHAIN) build --locked --release --target wasm32-wasip1
$(CP) $(AUTOMERGE_REFERENCE_DIR)/wasm/target/wasm32-wasip1/release/probo_automerge_reference.wasm $(AUTOMERGE_REFERENCE_WASM)
cd $(AUTOMERGE_REFERENCE_DIR) && $(SHA256SUM) reference.wasm > reference.wasm.sha256

.PHONY: audit-automerge-reference
audit-automerge-reference: ## Audit Automerge Rust advisories, licenses, bans, and sources
cd $(AUTOMERGE_REFERENCE_DIR)/wasm && \
$(CARGO) +$(RUST_TOOLCHAIN) deny check

.PHONY: embed
embed: $(EMBEDDED)

Expand Down
2 changes: 2 additions & 0 deletions apps/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
"@automerge/automerge": "^3.4.0",
"@automerge/prosemirror": "^0.2.0",
"@hookform/resolvers": "^5.7.1",
"@phosphor-icons/react": "^2.1.10",
"@probo/coredata": "^1.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@
// SOFTWARE.

import { formatError } from "@probo/helpers";
import { RichEditor, useToast } from "@probo/ui";
import { useCallback, useState } from "react";
import {
createRichEditorAutomergeDocument,
RichEditor,
supportsRichEditorCollaboration,
useToast,
} from "@probo/ui";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
import { useOutletContext } from "react-router";
Expand All @@ -30,8 +35,20 @@ import { useDebounceCallback } from "usehooks-ts";
import type { DocumentDescriptionPage_updateContentMutation } from "#/__generated__/core/DocumentDescriptionPage_updateContentMutation.graphql";
import type { DocumentDescriptionPageQuery } from "#/__generated__/core/DocumentDescriptionPageQuery.graphql";

import {
type AutomergeDocumentHandle,
connectAutomergeDocument,
} from "./_lib/AutomergeDocumentHandle";

const autoSaveIntervalMs = 1000;

type CollaborationState = {
versionID: string;
handle?: AutomergeDocumentHandle;
failed?: boolean;
reconnecting?: boolean;
};

export const documentDescriptionPageQuery = graphql`
query DocumentDescriptionPageQuery($documentId: ID! $versionId: ID! $versionSpecified: Boolean!) {
# We use this on /documents/:documentId/versions/:versionId/description
Expand Down Expand Up @@ -158,6 +175,86 @@ export function DocumentDescriptionPage(props: {
&& document.canUpdate
&& document.status !== "ARCHIVED"
&& document.writeMode !== "GENERATED";
const collaborationSupported = canEdit
&& supportsRichEditorCollaboration(currentVersion.content);
const [collaboration, setCollaboration] = useState<CollaborationState>();

useEffect(() => {
if (!collaborationSupported) return;

let cancelled = false;
let activeHandle: AutomergeDocumentHandle | undefined;

void connectAutomergeDocument(
currentVersion.id,
content => createRichEditorAutomergeDocument(content),
(connected) => {
if (cancelled) return;

setCollaboration({
versionID: currentVersion.id,
handle: activeHandle,
reconnecting: !connected,
});
if (!connected) {
toast({
title: t("documentDescriptionPage.errors.title"),
description: t("documentDescriptionPage.errors.save"),
variant: "error",
});
}
},
).then((handle) => {
if (cancelled) {
handle.close();
return;
}

activeHandle = handle;
setCollaboration({
versionID: currentVersion.id,
handle,
});
}).catch((error) => {
if (cancelled) return;

setCollaboration({
versionID: currentVersion.id,
failed: true,
});
toast({
title: t("documentDescriptionPage.errors.title"),
description: error instanceof Error
? error.message
: t("documentDescriptionPage.errors.save"),
variant: "error",
});
});

return () => {
cancelled = true;
activeHandle?.close();
};
}, [
collaborationSupported,
currentVersion.id,
t,
toast,
]);

const collaborationConnecting = collaborationSupported
&& (
collaboration?.versionID !== currentVersion.id
|| (!collaboration?.handle && !collaboration?.failed)
);
const collaborationHandle = collaborationSupported
&& collaboration?.versionID === currentVersion.id
&& !collaboration.failed
? collaboration.handle
: undefined;
const collaborationReconnecting = collaborationSupported
&& collaboration?.versionID === currentVersion.id
&& collaboration.reconnecting;

// The editor key must change on explicit actions (delete draft, edit
// title/type) but NOT on auto-save side effects (cursor preservation).
Expand Down Expand Up @@ -199,7 +296,8 @@ export function DocumentDescriptionPage(props: {
className="flex-1"
content={currentVersion.content}
data-theme="document"
disabled={!canEdit}
disabled={!canEdit || collaborationConnecting || collaborationReconnecting}
collaborationHandle={collaborationHandle}
onChangeContent={handleUpdate}
/>
);
Expand Down
Loading
Loading