agentic-documents 0.5.0 → 0.6.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/CHANGELOG.md +21 -0
- package/README.md +51 -8
- package/dist/{chunk-7R7ZPPRF.js → chunk-34C47PDC.js} +1 -1
- package/dist/{chunk-UPEGODUV.js → chunk-5IQX2W52.js} +1 -1
- package/dist/{chunk-S6MQDWFK.js → chunk-6UYJV2EJ.js} +19 -6
- package/dist/{chunk-E7B6T5WN.js → chunk-CN6UAW7B.js} +5 -3
- package/dist/chunk-DWVJIKVT.js +8 -0
- package/dist/{chunk-6QOYS63L.js → chunk-GQ5E6T7Z.js} +4 -2
- package/dist/{chunk-YNHZCIGO.js → chunk-JBAQEIOH.js} +5 -3
- package/dist/chunk-KC5KFB5R.js +354 -0
- package/dist/{chunk-FX755I7V.js → chunk-V6TBTOHW.js} +11 -8
- package/dist/cli.js +16 -14
- package/dist/client.d.ts +5 -5
- package/dist/{doc-mJPajXEQ.d.ts → doc-BXG9qG5X.d.ts} +4 -2
- package/dist/editor.css +37 -0
- package/dist/embed.d.ts +86 -0
- package/dist/embed.js +431 -0
- package/dist/formats.d.ts +6 -3
- package/dist/formats.js +14 -224
- package/dist/index.d.ts +12 -11
- package/dist/index.js +14 -12
- package/dist/live-worker.d.ts +2 -2
- package/dist/live-worker.js +7 -6
- package/dist/live.d.ts +2 -2
- package/dist/live.js +5 -4
- package/dist/{mcp-ESSEG46C.js → mcp-ZTUTC4CS.js} +5 -4
- package/dist/{export-Cwx276UP.d.ts → render-463xHb-x.d.ts} +3 -4
- package/dist/{review-Hgq8OgvD.d.ts → review-Ds-i5Vnc.d.ts} +2 -2
- package/dist/{service-CqKE9DKz.d.ts → service-Ck6oX-Kq.d.ts} +2 -2
- package/dist/ui/app.css +1 -1
- package/dist/ui/app.js +65 -65
- package/dist/worker.d.ts +2 -2
- package/dist/worker.js +7 -6
- package/package.json +28 -11
- package/dist/chunk-L6W2AQUF.js +0 -258
package/dist/cli.js
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
VERSION
|
|
4
|
+
} from "./chunk-5Y66UCTE.js";
|
|
2
5
|
import {
|
|
3
6
|
createFileBlobs,
|
|
4
7
|
createFileStore
|
|
5
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-34C47PDC.js";
|
|
6
9
|
import {
|
|
7
10
|
LiveService,
|
|
8
11
|
whoAmI
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import {
|
|
11
|
-
DocsService
|
|
12
|
-
} from "./chunk-YNHZCIGO.js";
|
|
13
|
-
import {
|
|
14
|
-
VERSION
|
|
15
|
-
} from "./chunk-5Y66UCTE.js";
|
|
12
|
+
} from "./chunk-CN6UAW7B.js";
|
|
16
13
|
import {
|
|
17
14
|
commands
|
|
18
|
-
} from "./chunk-
|
|
19
|
-
import
|
|
20
|
-
|
|
15
|
+
} from "./chunk-6UYJV2EJ.js";
|
|
16
|
+
import {
|
|
17
|
+
DocsService
|
|
18
|
+
} from "./chunk-JBAQEIOH.js";
|
|
19
|
+
import "./chunk-5IQX2W52.js";
|
|
20
|
+
import "./chunk-KC5KFB5R.js";
|
|
21
21
|
import {
|
|
22
22
|
DocError,
|
|
23
23
|
parseAuthor
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-GQ5E6T7Z.js";
|
|
25
|
+
import "./chunk-DWVJIKVT.js";
|
|
25
26
|
|
|
26
27
|
// src/cli.ts
|
|
27
28
|
import { readFileSync, writeFileSync } from "fs";
|
|
@@ -120,7 +121,7 @@ async function main(argv, env = process.env) {
|
|
|
120
121
|
author = named && scopes?.length ? { ...named, scopes } : named;
|
|
121
122
|
}
|
|
122
123
|
if (name === "mcp") {
|
|
123
|
-
const { startMcp } = await import("./mcp-
|
|
124
|
+
const { startMcp } = await import("./mcp-ZTUTC4CS.js");
|
|
124
125
|
await startMcp(service, author);
|
|
125
126
|
return 0;
|
|
126
127
|
}
|
|
@@ -140,7 +141,8 @@ async function main(argv, env = process.env) {
|
|
|
140
141
|
}
|
|
141
142
|
const result = await command.run(service, input, author);
|
|
142
143
|
if (name === "export") {
|
|
143
|
-
const { data, encoding } = result;
|
|
144
|
+
const { data, encoding, warnings } = result;
|
|
145
|
+
for (const line of warnings) console.error(`warning: ${line}`);
|
|
144
146
|
const bytes = encoding === "base64" ? Buffer.from(data, "base64") : data;
|
|
145
147
|
if (out === void 0) process.stdout.write(bytes);
|
|
146
148
|
else writeFileSync(out, bytes);
|
package/dist/client.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
3
|
-
import { E as ExportFormat } from './
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { e as PrincipalKind, S as Section, E as EditEntry } from './doc-BXG9qG5X.js';
|
|
2
|
+
export { a as Anchor } from './doc-BXG9qG5X.js';
|
|
3
|
+
import { E as ExportFormat } from './render-463xHb-x.js';
|
|
4
|
+
import { a as DocErrorCode, S as Suggestion, T as Thread } from './review-Ds-i5Vnc.js';
|
|
5
|
+
import { d as DocSummary } from './service-Ck6oX-Kq.js';
|
|
6
6
|
import 'yjs';
|
|
7
7
|
import 'agentic-identity';
|
|
8
8
|
|
|
@@ -18,6 +18,7 @@ declare function blocksToMarkdown(nodes: PmNode[]): string;
|
|
|
18
18
|
|
|
19
19
|
declare const PRINCIPAL_KINDS: readonly ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
|
|
20
20
|
type PrincipalKind = (typeof PRINCIPAL_KINDS)[number];
|
|
21
|
+
|
|
21
22
|
/** Who made a change. Every mutation needs one, so machine-made work stays labeled. */
|
|
22
23
|
interface Principal {
|
|
23
24
|
kind: PrincipalKind;
|
|
@@ -51,7 +52,8 @@ interface Section {
|
|
|
51
52
|
title: string;
|
|
52
53
|
}
|
|
53
54
|
interface EditEntry {
|
|
54
|
-
|
|
55
|
+
/** `live` ties the editor client of an agent to that agent, so the text the client types is marked as its work. Its counts stay 0. */
|
|
56
|
+
op: "create" | "append" | "insert" | "replace" | "delete" | "live";
|
|
55
57
|
at: string;
|
|
56
58
|
author: Principal;
|
|
57
59
|
/** Counts of characters (text targets) or blocks (section targets). Never content. */
|
|
@@ -127,4 +129,4 @@ declare class AgenticDoc {
|
|
|
127
129
|
append(markdown: string, author: Principal): void;
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
export {
|
|
132
|
+
export { AgenticDoc as A, type DocOptions as D, type EditEntry as E, type Principal as P, type Section as S, type Target as T, type Anchor as a, type AnchorTarget as b, PRINCIPAL_KINDS as c, type PmNode as d, type PrincipalKind as e, type SectionTarget as f, type TextTarget as g, blocksToMarkdown as h, markdownToBlocks as m, parseAuthor as p };
|
package/dist/editor.css
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* The editor that `agentic-documents/ui` renders. Every rule sits under `.adoc`, so it does not touch the page around it.
|
|
2
|
+
Color, type, and space come from the agentic-design tokens: load `agentic-design/styles.css` first. */
|
|
3
|
+
.adoc.muted, .adoc .muted { color: var(--ad-color-text-muted); font-size: var(--ad-font-size-sm); margin: 0; }
|
|
4
|
+
.adoc.notice, .adoc .notice { max-width: var(--ad-size-content); }
|
|
5
|
+
.adoc.form { display: grid; gap: var(--ad-space-4); }
|
|
6
|
+
.adoc.form .form-actions { display: flex; gap: var(--ad-space-2); align-items: center; justify-content: flex-end; flex-wrap: wrap; }
|
|
7
|
+
.adoc .editor-bar { display: flex; align-items: center; justify-content: space-between; gap: var(--ad-space-4); flex-wrap: wrap; max-width: var(--ad-size-content); margin-block-end: var(--ad-space-3); }
|
|
8
|
+
.adoc .editor-meta { display: flex; align-items: center; gap: var(--ad-space-4); flex-wrap: wrap; }
|
|
9
|
+
.adoc .glyph { font-weight: var(--ad-font-weight-semibold); min-width: 1.25rem; text-align: center; }
|
|
10
|
+
.adoc .glyph--b { font-weight: 800; }
|
|
11
|
+
.adoc .glyph--i { font-style: italic; font-family: serif; }
|
|
12
|
+
.adoc .editor-host { max-width: var(--ad-size-content); background: var(--ad-color-surface); border: 1px solid var(--ad-color-border); border-radius: var(--ad-radius-md); }
|
|
13
|
+
.adoc .editor-host:focus-within { outline: 2px solid var(--ad-color-focus); outline-offset: 2px; }
|
|
14
|
+
.adoc .prose { min-height: 20rem; padding: var(--ad-space-6); outline: none; line-height: var(--ad-line-height-normal); overflow-wrap: anywhere; }
|
|
15
|
+
.adoc .prose > :first-child { margin-top: 0; }
|
|
16
|
+
.adoc .prose h1 { font-size: var(--ad-font-size-2xl); line-height: var(--ad-line-height-tight); }
|
|
17
|
+
.adoc .prose h2 { font-size: var(--ad-font-size-xl); line-height: var(--ad-line-height-tight); }
|
|
18
|
+
.adoc .prose h3 { font-size: var(--ad-font-size-lg); }
|
|
19
|
+
.adoc .prose a { color: var(--ad-color-accent-text); text-decoration: underline; }
|
|
20
|
+
.adoc .prose blockquote { margin-inline: 0; padding-inline-start: var(--ad-space-4); border-inline-start: 3px solid var(--ad-color-border-strong); }
|
|
21
|
+
.adoc .prose code { font-family: var(--ad-font-family-mono); background: var(--ad-color-surface-sunken); padding: 0 0.25em; border-radius: var(--ad-radius-sm); }
|
|
22
|
+
.adoc .prose pre { background: var(--ad-color-surface-sunken); padding: var(--ad-space-3); border-radius: var(--ad-radius-md); overflow-x: auto; }
|
|
23
|
+
.adoc .prose table { border-collapse: collapse; }
|
|
24
|
+
.adoc .prose th, .adoc .prose td { border: 1px solid var(--ad-color-border-strong); padding: var(--ad-space-1) var(--ad-space-2); }
|
|
25
|
+
.adoc .prose img { max-width: 100%; }
|
|
26
|
+
.adoc .prose ul[data-type="taskList"] { list-style: none; padding-inline-start: var(--ad-space-2); }
|
|
27
|
+
.adoc .prose ul[data-type="taskList"] li { display: flex; gap: var(--ad-space-2); }
|
|
28
|
+
|
|
29
|
+
/* Text an agent wrote: a dotted underline in the agent color and a tint. The legend and the title say it in words. */
|
|
30
|
+
.adoc .agent-text { text-decoration: underline dotted 2px var(--ad-color-agent); text-underline-offset: 3px; background: var(--ad-color-agent-subtle); border-radius: 2px; }
|
|
31
|
+
.adoc .legend { margin-block-start: var(--ad-space-3); }
|
|
32
|
+
|
|
33
|
+
/* The caret of another person or agent. */
|
|
34
|
+
.adoc .caret { position: relative; border-inline-start: 2px solid var(--ad-color-accent); margin-inline: -1px; pointer-events: none; word-break: normal; }
|
|
35
|
+
.adoc .caret--agent { border-inline-start: 2px dashed var(--ad-color-agent); }
|
|
36
|
+
.adoc .caret__tag { position: absolute; top: -1.45em; inset-inline-start: -2px; white-space: nowrap; font-size: var(--ad-font-size-xs); line-height: 1.3; padding: 0 var(--ad-space-1); border-radius: var(--ad-radius-sm) var(--ad-radius-sm) var(--ad-radius-sm) 0; background: var(--ad-color-accent); color: var(--ad-color-text-on-accent); user-select: none; }
|
|
37
|
+
.adoc .caret--agent .caret__tag { background: var(--ad-color-surface); color: var(--ad-color-agent-text); border: 1px solid var(--ad-color-agent); border-radius: 0; }
|
package/dist/embed.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { e as PrincipalKind } from './doc-BXG9qG5X.js';
|
|
3
|
+
import { SyncClient, Status } from 'agentic-realtime/client';
|
|
4
|
+
import * as Y from 'yjs';
|
|
5
|
+
import { DocsClient } from './client.js';
|
|
6
|
+
import './render-463xHb-x.js';
|
|
7
|
+
import './review-Ds-i5Vnc.js';
|
|
8
|
+
import './service-Ck6oX-Kq.js';
|
|
9
|
+
import 'agentic-identity';
|
|
10
|
+
|
|
11
|
+
/** The calls the editor makes. A `DocsClient` from `agentic-documents/client` has them. */
|
|
12
|
+
type EditorClient = Pick<DocsClient, "state" | "checkpoint">;
|
|
13
|
+
/** A live room of agentic-realtime for one document. */
|
|
14
|
+
interface EditorRoom {
|
|
15
|
+
/** The WebSocket address of the room, such as `wss://docs.example.com/api/v1/docs/plan/ws`. */
|
|
16
|
+
url: string;
|
|
17
|
+
/** A bearer token for the room. In a browser it goes in a WebSocket subprotocol. */
|
|
18
|
+
token?: string;
|
|
19
|
+
/** Your own WebSocket class, for a room that takes its token another way. */
|
|
20
|
+
WebSocket?: new (url: string, options?: {
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
}) => WebSocket;
|
|
23
|
+
}
|
|
24
|
+
interface LiveSession {
|
|
25
|
+
/** The room, or undefined when the document is open without one. */
|
|
26
|
+
sync?: SyncClient;
|
|
27
|
+
doc: Y.Doc;
|
|
28
|
+
/** The socket state now, and a way to hear when it changes. */
|
|
29
|
+
status: () => Status;
|
|
30
|
+
onStatus: (listener: (status: Status) => void) => () => void;
|
|
31
|
+
/** True when this tab changed the text since the last save that worked. */
|
|
32
|
+
readonly dirty: boolean;
|
|
33
|
+
/** Call it when this tab changes the text. */
|
|
34
|
+
touch(): void;
|
|
35
|
+
/** Merge the room into the stored document. It never makes a document: a removed one fails with NO_DOCUMENT. */
|
|
36
|
+
save(): Promise<void>;
|
|
37
|
+
close(): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Who uses the editor. The server still checks every write against its own token. */
|
|
41
|
+
interface EmbedPrincipal {
|
|
42
|
+
kind: PrincipalKind;
|
|
43
|
+
name: string;
|
|
44
|
+
/** Only `editor` can change the text. Any other value, or no value, opens it read only. */
|
|
45
|
+
role?: "editor" | "viewer";
|
|
46
|
+
}
|
|
47
|
+
interface DocumentEditorOptions {
|
|
48
|
+
/** The document id. */
|
|
49
|
+
id: string;
|
|
50
|
+
/** Loads and saves the document. A client from `createClient` in `agentic-documents/client` works. */
|
|
51
|
+
client: EditorClient;
|
|
52
|
+
principal: EmbedPrincipal;
|
|
53
|
+
/** The live room. With one, people and agents edit together and see each other. Without one, the editor saves alone. */
|
|
54
|
+
room?: EditorRoom;
|
|
55
|
+
/** Called when the document cannot open, with the error. */
|
|
56
|
+
onError?: (error: unknown) => void;
|
|
57
|
+
/** Called when the editor closes a document, with a promise that settles when the last save and the leave are done. It rejects when that save fails. */
|
|
58
|
+
onLeave?: (done: Promise<void>) => void;
|
|
59
|
+
}
|
|
60
|
+
type SessionState = {
|
|
61
|
+
state: "loading";
|
|
62
|
+
} | {
|
|
63
|
+
state: "error";
|
|
64
|
+
error: unknown;
|
|
65
|
+
} | {
|
|
66
|
+
state: "ready";
|
|
67
|
+
live: LiveSession;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Open a document for an editor, and close it when the component goes. On leave it saves first, but only when this tab
|
|
71
|
+
* changed something and may write. A save never makes a document, so a removed document stays removed. `onLeave` gets a
|
|
72
|
+
* promise that settles when the leave is done. It rejects when the save fails. With no `onLeave`, a failure is dropped.
|
|
73
|
+
*/
|
|
74
|
+
declare function useEditorSession(client: EditorClient, id: string, name: string, editable: boolean, room: EditorRoom | undefined, attempt: number, onLeave?: (done: Promise<void>) => void): SessionState;
|
|
75
|
+
/** The editor for one document, with its toolbar, presence, and save state. */
|
|
76
|
+
declare function DocumentEditor({ id, client, principal, room, onError, onLeave }: DocumentEditorOptions): react.JSX.Element;
|
|
77
|
+
interface MountedEditor {
|
|
78
|
+
/** Show another document, principal, or room in the same place. A bad principal throws and keeps the editor as it is. */
|
|
79
|
+
update(options: DocumentEditorOptions): void;
|
|
80
|
+
/** Remove the editor. It saves this tab's changes, then leaves the room. The promise rejects when that save fails. */
|
|
81
|
+
unmount(): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
/** Render the editor into an element, for an app that does not use React itself. */
|
|
84
|
+
declare function mountEditor(element: HTMLElement, options: DocumentEditorOptions): MountedEditor;
|
|
85
|
+
|
|
86
|
+
export { DocumentEditor, type DocumentEditorOptions, type EditorClient, type EditorRoom, type EmbedPrincipal, type LiveSession, type MountedEditor, type SessionState, mountEditor, useEditorSession };
|
package/dist/embed.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PRINCIPAL_KINDS,
|
|
3
|
+
isMachine
|
|
4
|
+
} from "./chunk-DWVJIKVT.js";
|
|
5
|
+
|
|
6
|
+
// ui/embed.tsx
|
|
7
|
+
import { Button as Button2, EmptyState as EmptyState2 } from "agentic-design/react";
|
|
8
|
+
import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
9
|
+
import { createRoot } from "react-dom/client";
|
|
10
|
+
|
|
11
|
+
// ui/live.ts
|
|
12
|
+
import { SyncClient } from "agentic-realtime/client";
|
|
13
|
+
import * as Y from "yjs";
|
|
14
|
+
var base64url = (text) => btoa(String.fromCharCode(...new TextEncoder().encode(text))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
15
|
+
var TokenSocket = class extends WebSocket {
|
|
16
|
+
constructor(url, options) {
|
|
17
|
+
const token = /^Bearer (.+)$/.exec(options?.headers?.authorization ?? "")?.[1];
|
|
18
|
+
super(url, token ? ["agentic-docs", `agentic-docs.bearer.${base64url(token)}`] : void 0);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
async function openSession(client, id, name, room) {
|
|
22
|
+
let status = room ? "connecting" : "connected";
|
|
23
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
24
|
+
const doc = new Y.Doc();
|
|
25
|
+
Y.applyUpdate(doc, await client.state(id));
|
|
26
|
+
const sync = room ? await SyncClient.connect({
|
|
27
|
+
url: room.url,
|
|
28
|
+
headers: room.token ? { authorization: `Bearer ${room.token}` } : void 0,
|
|
29
|
+
WebSocket: room.WebSocket ?? TokenSocket,
|
|
30
|
+
doc,
|
|
31
|
+
presence: { name },
|
|
32
|
+
reconnect: {},
|
|
33
|
+
onStatus: (next) => {
|
|
34
|
+
status = next;
|
|
35
|
+
for (const listener of listeners) listener(next);
|
|
36
|
+
}
|
|
37
|
+
}) : void 0;
|
|
38
|
+
await sync?.whenSynced().catch(() => void 0);
|
|
39
|
+
let changes = 0;
|
|
40
|
+
let saved = 0;
|
|
41
|
+
const session = {
|
|
42
|
+
sync,
|
|
43
|
+
doc,
|
|
44
|
+
status: () => status,
|
|
45
|
+
onStatus: (listener) => (listeners.add(listener), () => void listeners.delete(listener)),
|
|
46
|
+
get dirty() {
|
|
47
|
+
return changes !== saved;
|
|
48
|
+
},
|
|
49
|
+
touch: () => void changes++,
|
|
50
|
+
async save() {
|
|
51
|
+
const mark = changes;
|
|
52
|
+
await sync?.flush();
|
|
53
|
+
await client.checkpoint(id, Y.encodeStateAsUpdate(doc), { create: false });
|
|
54
|
+
saved = mark;
|
|
55
|
+
},
|
|
56
|
+
close: async () => await sync?.disconnect()
|
|
57
|
+
};
|
|
58
|
+
return session;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ui/editor.tsx
|
|
62
|
+
import { Editor, Extension as Extension2 } from "@tiptap/core";
|
|
63
|
+
import Collaboration from "@tiptap/extension-collaboration";
|
|
64
|
+
import Image from "@tiptap/extension-image";
|
|
65
|
+
import { TaskItem, TaskList } from "@tiptap/extension-list";
|
|
66
|
+
import { Table, TableCell, TableHeader, TableRow } from "@tiptap/extension-table";
|
|
67
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
68
|
+
import { ySyncPluginKey as ySyncPluginKey2, yCursorPlugin } from "@tiptap/y-tiptap";
|
|
69
|
+
import { Component, useEffect, useReducer, useRef, useState } from "react";
|
|
70
|
+
import { Button, Dialog, DialogClose, EmptyState, Input, PresenceStack, Toolbar, ToolbarButton, ToolbarSeparator, usableName } from "agentic-design/react";
|
|
71
|
+
|
|
72
|
+
// ui/marks.ts
|
|
73
|
+
import { Extension } from "@tiptap/core";
|
|
74
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
75
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
76
|
+
import { relativePositionToAbsolutePosition, ySyncPluginKey } from "@tiptap/y-tiptap";
|
|
77
|
+
import * as Y2 from "yjs";
|
|
78
|
+
function agentRuns(doc) {
|
|
79
|
+
const agents = /* @__PURE__ */ new Set();
|
|
80
|
+
for (const entry of doc.getArray("edits").toArray()) {
|
|
81
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
82
|
+
if (typeof entry.client === "number" && entry.author?.kind !== void 0 && entry.author.kind !== "human") agents.add(entry.client);
|
|
83
|
+
}
|
|
84
|
+
const runs = [];
|
|
85
|
+
if (agents.size === 0) return runs;
|
|
86
|
+
const walk = (node) => {
|
|
87
|
+
for (const child of node.toArray()) {
|
|
88
|
+
if (child instanceof Y2.XmlElement) walk(child);
|
|
89
|
+
else if (child instanceof Y2.XmlText) {
|
|
90
|
+
let index = 0;
|
|
91
|
+
for (let item = child._start; item; item = item.right) {
|
|
92
|
+
if (item.deleted || !item.countable) continue;
|
|
93
|
+
if (agents.has(item.id.client)) runs.push({ text: child, from: index, to: index + item.length });
|
|
94
|
+
index += item.length;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
walk(doc.getXmlFragment("default"));
|
|
100
|
+
return runs;
|
|
101
|
+
}
|
|
102
|
+
var key = new PluginKey("agentText");
|
|
103
|
+
var AgentText = Extension.create({
|
|
104
|
+
name: "agentText",
|
|
105
|
+
addProseMirrorPlugins() {
|
|
106
|
+
return [
|
|
107
|
+
new Plugin({
|
|
108
|
+
key,
|
|
109
|
+
state: {
|
|
110
|
+
init: () => DecorationSet.empty,
|
|
111
|
+
apply: (tr, old) => tr.getMeta(key) ?? old.map(tr.mapping, tr.doc)
|
|
112
|
+
},
|
|
113
|
+
props: { decorations: (state) => key.getState(state) },
|
|
114
|
+
view(view) {
|
|
115
|
+
const sync = ySyncPluginKey.getState(view.state);
|
|
116
|
+
if (!sync) return {};
|
|
117
|
+
let timer;
|
|
118
|
+
let drawn = "";
|
|
119
|
+
const refresh = () => {
|
|
120
|
+
timer = void 0;
|
|
121
|
+
try {
|
|
122
|
+
const mapping = ySyncPluginKey.getState(view.state).binding?.mapping;
|
|
123
|
+
if (!mapping) return;
|
|
124
|
+
const size = view.state.doc.content.size;
|
|
125
|
+
const marks = [];
|
|
126
|
+
for (const { text, from, to } of agentRuns(sync.doc)) {
|
|
127
|
+
const a = relativePositionToAbsolutePosition(sync.doc, sync.type, Y2.createRelativePositionFromTypeIndex(text, from), mapping);
|
|
128
|
+
const b = relativePositionToAbsolutePosition(sync.doc, sync.type, Y2.createRelativePositionFromTypeIndex(text, to), mapping);
|
|
129
|
+
if (a !== null && b !== null && a < b && b <= size) marks.push(Decoration.inline(a, b, { class: "agent-text", title: "Written by an agent" }));
|
|
130
|
+
}
|
|
131
|
+
const signature = marks.map((m) => `${m.from}-${m.to}`).join(",");
|
|
132
|
+
if (signature === drawn) return;
|
|
133
|
+
drawn = signature;
|
|
134
|
+
view.dispatch(view.state.tr.setMeta(key, DecorationSet.create(view.state.doc, marks)).setMeta("addToHistory", false));
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const schedule = () => {
|
|
139
|
+
timer ??= setTimeout(refresh, 60);
|
|
140
|
+
};
|
|
141
|
+
sync.doc.on("update", schedule);
|
|
142
|
+
schedule();
|
|
143
|
+
return {
|
|
144
|
+
destroy() {
|
|
145
|
+
sync.doc.off("update", schedule);
|
|
146
|
+
if (timer) clearTimeout(timer);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ui/editor.tsx
|
|
156
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
157
|
+
var SAVE_AFTER_MS = 1500;
|
|
158
|
+
var entryOf = (peer) => ({ id: String(peer.clientId), name: usableName(peer.name) ?? "Unnamed", kind: peer.machine ? "agent" : "person" });
|
|
159
|
+
var isRemoved = (error) => typeof error === "object" && error !== null && error.code === "NO_DOCUMENT";
|
|
160
|
+
function carets(sync) {
|
|
161
|
+
return Extension2.create({
|
|
162
|
+
name: "remoteCarets",
|
|
163
|
+
addProseMirrorPlugins: () => [
|
|
164
|
+
yCursorPlugin(sync.awareness, {
|
|
165
|
+
cursorBuilder: (_user, clientId) => {
|
|
166
|
+
const caret = document.createElement("span");
|
|
167
|
+
const peer = sync.peers.find((p) => p.clientId === clientId);
|
|
168
|
+
if (!peer) return caret;
|
|
169
|
+
caret.className = `caret${peer.machine ? " caret--agent" : ""}`;
|
|
170
|
+
const tag = document.createElement("span");
|
|
171
|
+
tag.className = "caret__tag";
|
|
172
|
+
tag.textContent = `${usableName(peer.name) ?? "Unnamed"}${peer.machine ? " \xB7 Agent" : ""}`;
|
|
173
|
+
caret.append(tag);
|
|
174
|
+
return caret;
|
|
175
|
+
},
|
|
176
|
+
selectionBuilder: () => ({})
|
|
177
|
+
})
|
|
178
|
+
]
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
var Boundary = class extends Component {
|
|
182
|
+
state = { failed: false };
|
|
183
|
+
static getDerivedStateFromError() {
|
|
184
|
+
return { failed: true };
|
|
185
|
+
}
|
|
186
|
+
render() {
|
|
187
|
+
return this.state.failed ? /* @__PURE__ */ jsx("div", { role: "alert", className: "adoc notice", children: /* @__PURE__ */ jsx(EmptyState, { title: "The editor stopped", description: "The document has content this page cannot show. Reload to try again." }) }) : this.props.children;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
function Surface({ live, editable, principal: me }) {
|
|
191
|
+
const host = useRef(null);
|
|
192
|
+
const [editor, setEditor] = useState();
|
|
193
|
+
const [, refresh] = useReducer((n) => n + 1, 0);
|
|
194
|
+
const [peers, setPeers] = useState([]);
|
|
195
|
+
const [saved, setSaved] = useState("saved");
|
|
196
|
+
const [connection, setConnection] = useState("connecting");
|
|
197
|
+
const [agentText, setAgentText] = useState(false);
|
|
198
|
+
const [linking, setLinking] = useState(false);
|
|
199
|
+
const [href, setHref] = useState("");
|
|
200
|
+
const [linkError, setLinkError] = useState();
|
|
201
|
+
const refocus = useRef(false);
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
const editor2 = new Editor({
|
|
204
|
+
element: host.current,
|
|
205
|
+
editable,
|
|
206
|
+
extensions: [
|
|
207
|
+
StarterKit.configure({ undoRedo: false, underline: false, link: { openOnClick: false, autolink: true, defaultProtocol: "https", isAllowedUri: (url) => /^(https?:\/\/|mailto:)\S+$/i.test(url) } }),
|
|
208
|
+
Image,
|
|
209
|
+
TaskList,
|
|
210
|
+
TaskItem,
|
|
211
|
+
Table,
|
|
212
|
+
TableRow,
|
|
213
|
+
TableCell,
|
|
214
|
+
TableHeader,
|
|
215
|
+
Collaboration.configure({ document: live.doc, field: "default" }),
|
|
216
|
+
...live.sync ? [carets(live.sync)] : [],
|
|
217
|
+
AgentText
|
|
218
|
+
],
|
|
219
|
+
editorProps: { attributes: { role: "textbox", "aria-multiline": "true", "aria-label": "Document text", class: "prose" } }
|
|
220
|
+
});
|
|
221
|
+
setEditor(editor2);
|
|
222
|
+
editor2.on("transaction", () => (setAgentText(host.current?.querySelector(".agent-text") != null), refresh()));
|
|
223
|
+
const offPeers = live.sync?.onPeers(setPeers);
|
|
224
|
+
setPeers(live.sync?.peers ?? []);
|
|
225
|
+
let timer;
|
|
226
|
+
let stopped = false;
|
|
227
|
+
let gone = false;
|
|
228
|
+
const save = () => {
|
|
229
|
+
timer = void 0;
|
|
230
|
+
setSaved("saving");
|
|
231
|
+
live.save().then(
|
|
232
|
+
() => !stopped && setSaved("saved"),
|
|
233
|
+
(error) => {
|
|
234
|
+
if (stopped) return;
|
|
235
|
+
if (isRemoved(error)) {
|
|
236
|
+
gone = true;
|
|
237
|
+
editor2.setEditable(false);
|
|
238
|
+
return setSaved("removed");
|
|
239
|
+
}
|
|
240
|
+
setSaved("failed");
|
|
241
|
+
timer = setTimeout(save, 5e3);
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
let labeled = false;
|
|
246
|
+
const changed = (_update, origin) => {
|
|
247
|
+
if (editable && !gone && origin === ySyncPluginKey2) {
|
|
248
|
+
if (!labeled && isMachine(me.kind)) {
|
|
249
|
+
labeled = true;
|
|
250
|
+
live.doc.getArray("edits").push([{ op: "live", at: (/* @__PURE__ */ new Date()).toISOString(), author: { kind: me.kind, name: me.name }, client: live.doc.clientID, inserted: 0, deleted: 0 }]);
|
|
251
|
+
}
|
|
252
|
+
live.touch();
|
|
253
|
+
setSaved("saving");
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
timer = setTimeout(save, SAVE_AFTER_MS);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
live.doc.on("update", changed);
|
|
259
|
+
setConnection(live.status());
|
|
260
|
+
const offStatus = live.onStatus(setConnection);
|
|
261
|
+
return () => {
|
|
262
|
+
stopped = true;
|
|
263
|
+
live.doc.off("update", changed);
|
|
264
|
+
offStatus();
|
|
265
|
+
if (timer) clearTimeout(timer);
|
|
266
|
+
offPeers?.();
|
|
267
|
+
editor2.destroy();
|
|
268
|
+
};
|
|
269
|
+
}, [live, editable, me.name, me.kind]);
|
|
270
|
+
const entries = [{ id: "me", name: me.name, kind: isMachine(me.kind) ? "agent" : "person" }, ...peers.map(entryOf)];
|
|
271
|
+
const pressed = (name, attrs) => editor?.isActive(name, attrs) ?? false;
|
|
272
|
+
const chain = () => (editor.view.focus(), editor.chain().focus());
|
|
273
|
+
const glyph = (text, style) => /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: `glyph glyph--${style ?? "plain"}`, children: text });
|
|
274
|
+
const listIcon = (numbered) => /* @__PURE__ */ jsxs("svg", { "aria-hidden": "true", focusable: "false", viewBox: "0 0 20 20", width: "1.25em", height: "1.25em", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", children: [
|
|
275
|
+
/* @__PURE__ */ jsx("path", { d: "M8 5h9M8 10h9M8 15h9" }),
|
|
276
|
+
numbered ? /* @__PURE__ */ jsx("path", { d: "M3 4.5l1-.5v4M3 11.5h2l-2 2.5h2.2", strokeLinejoin: "round" }) : /* @__PURE__ */ jsx("path", { d: "M3.5 5h.01M3.5 10h.01M3.5 15h.01", strokeWidth: "2.6" })
|
|
277
|
+
] });
|
|
278
|
+
const tool = (label, icon, active, run) => /* @__PURE__ */ jsx(ToolbarButton, { label, icon, pressed: active, onClick: run, disabled: !editable });
|
|
279
|
+
function openLink() {
|
|
280
|
+
setHref(editor?.getAttributes("link").href ?? "");
|
|
281
|
+
setLinkError(void 0);
|
|
282
|
+
setLinking(true);
|
|
283
|
+
}
|
|
284
|
+
function applyLink() {
|
|
285
|
+
const value = href.trim();
|
|
286
|
+
if (value === "") chain().extendMarkRange("link").unsetLink().run();
|
|
287
|
+
else if (!/^(https?:\/\/|mailto:)\S+$/i.test(value)) return setLinkError("Use a web address that starts with https:// or http://, or a mailto: address.");
|
|
288
|
+
else chain().extendMarkRange("link").setLink({ href: value }).run();
|
|
289
|
+
if (editor && !editor.state.selection.empty) editor.chain().setTextSelection(editor.state.selection.to).unsetMark("link").run();
|
|
290
|
+
refocus.current = true;
|
|
291
|
+
setLinking(false);
|
|
292
|
+
}
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
if (linking || !refocus.current) return;
|
|
295
|
+
refocus.current = false;
|
|
296
|
+
let tries = 0;
|
|
297
|
+
const timer = setInterval(() => {
|
|
298
|
+
if (editor?.isFocused || ++tries > 10) return clearInterval(timer);
|
|
299
|
+
editor?.view.focus();
|
|
300
|
+
}, 40);
|
|
301
|
+
return () => clearInterval(timer);
|
|
302
|
+
}, [linking, editor]);
|
|
303
|
+
return /* @__PURE__ */ jsxs("section", { "aria-label": "Editor", className: "adoc", children: [
|
|
304
|
+
/* @__PURE__ */ jsxs("div", { className: "editor-bar", children: [
|
|
305
|
+
/* @__PURE__ */ jsxs(Toolbar, { label: "Formatting", children: [
|
|
306
|
+
tool("Bold", glyph("B", "b"), pressed("bold"), () => chain().toggleBold().run()),
|
|
307
|
+
tool("Italic", glyph("I", "i"), pressed("italic"), () => chain().toggleItalic().run()),
|
|
308
|
+
/* @__PURE__ */ jsx(ToolbarSeparator, {}),
|
|
309
|
+
tool("H1 heading", glyph("H1"), pressed("heading", { level: 1 }), () => chain().toggleHeading({ level: 1 }).run()),
|
|
310
|
+
tool("H2 heading", glyph("H2"), pressed("heading", { level: 2 }), () => chain().toggleHeading({ level: 2 }).run()),
|
|
311
|
+
/* @__PURE__ */ jsx(ToolbarSeparator, {}),
|
|
312
|
+
tool("Bulleted list", listIcon(false), pressed("bulletList"), () => chain().toggleBulletList().run()),
|
|
313
|
+
tool("Numbered list", listIcon(true), pressed("orderedList"), () => chain().toggleOrderedList().run()),
|
|
314
|
+
/* @__PURE__ */ jsx(ToolbarSeparator, {}),
|
|
315
|
+
tool("Link", glyph("\u2197"), pressed("link"), openLink)
|
|
316
|
+
] }),
|
|
317
|
+
/* @__PURE__ */ jsxs("div", { className: "editor-meta", children: [
|
|
318
|
+
/* @__PURE__ */ jsx(PresenceStack, { entries, label: "People and agents here" }),
|
|
319
|
+
/* @__PURE__ */ jsxs("span", { role: "status", className: "muted", children: [
|
|
320
|
+
!editable ? "Read only. " : "",
|
|
321
|
+
connection === "disconnected" ? "Offline: changes are kept and sync when you are back. " : "",
|
|
322
|
+
saved === "saving" ? "Saving\u2026" : saved === "failed" ? "Not saved. Will try again." : saved === "removed" ? "This document was removed. Your changes are not saved." : "Saved"
|
|
323
|
+
] })
|
|
324
|
+
] })
|
|
325
|
+
] }),
|
|
326
|
+
/* @__PURE__ */ jsx("div", { ref: host, className: "editor-host" }),
|
|
327
|
+
/* @__PURE__ */ jsx("p", { className: "muted legend", children: agentText ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
328
|
+
/* @__PURE__ */ jsx("span", { className: "agent-text", children: "Dotted text" }),
|
|
329
|
+
" was written by an agent."
|
|
330
|
+
] }) : "Text that an agent writes shows with a dotted underline." }),
|
|
331
|
+
/* @__PURE__ */ jsx(Dialog, { open: linking, onOpenChange: setLinking, title: "Link", description: "Leave it empty to remove the link.", children: /* @__PURE__ */ jsxs("form", { className: "adoc form", onSubmit: (e) => (e.preventDefault(), applyLink()), children: [
|
|
332
|
+
/* @__PURE__ */ jsx(Input, { label: "Address", value: href, onChange: (e) => setHref(e.target.value), error: linkError, placeholder: "https://", autoComplete: "off" }),
|
|
333
|
+
/* @__PURE__ */ jsxs("div", { className: "form-actions", children: [
|
|
334
|
+
/* @__PURE__ */ jsx(DialogClose, { asChild: true, children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", children: "Cancel" }) }),
|
|
335
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", children: "Apply" })
|
|
336
|
+
] })
|
|
337
|
+
] }) })
|
|
338
|
+
] });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ui/embed.tsx
|
|
342
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
343
|
+
function checkPrincipal(principal) {
|
|
344
|
+
if (!principal || !PRINCIPAL_KINDS.includes(principal.kind) || typeof principal.name !== "string" || principal.name.trim() === "") {
|
|
345
|
+
throw new TypeError(`the editor needs a principal with a kind (${PRINCIPAL_KINDS.join(", ")}) and a name`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function useEditorSession(client, id, name, editable, room, attempt, onLeave) {
|
|
349
|
+
const [load, setLoad] = useState2({ state: "loading" });
|
|
350
|
+
const roomKey = room ? JSON.stringify([room.url, room.token ?? ""]) : "";
|
|
351
|
+
const roomRef = useRef2(room);
|
|
352
|
+
roomRef.current = room;
|
|
353
|
+
const leaveRef = useRef2(onLeave);
|
|
354
|
+
leaveRef.current = onLeave;
|
|
355
|
+
useEffect2(() => {
|
|
356
|
+
let cancelled = false;
|
|
357
|
+
let live;
|
|
358
|
+
setLoad({ state: "loading" });
|
|
359
|
+
openSession(client, id, name, roomRef.current).then(
|
|
360
|
+
(found) => {
|
|
361
|
+
live = found;
|
|
362
|
+
if (cancelled) return void found.close();
|
|
363
|
+
setLoad({ state: "ready", live: found });
|
|
364
|
+
},
|
|
365
|
+
(error) => !cancelled && setLoad({ state: "error", error })
|
|
366
|
+
);
|
|
367
|
+
return () => {
|
|
368
|
+
cancelled = true;
|
|
369
|
+
const open = live;
|
|
370
|
+
if (!open) return;
|
|
371
|
+
const done = (async () => {
|
|
372
|
+
try {
|
|
373
|
+
if (editable && open.dirty) await open.save();
|
|
374
|
+
} finally {
|
|
375
|
+
await open.close();
|
|
376
|
+
}
|
|
377
|
+
})();
|
|
378
|
+
if (leaveRef.current) leaveRef.current(done);
|
|
379
|
+
else done.catch(() => void 0);
|
|
380
|
+
};
|
|
381
|
+
}, [client, id, name, editable, roomKey, room?.WebSocket, attempt]);
|
|
382
|
+
return load;
|
|
383
|
+
}
|
|
384
|
+
var describe = (error) => {
|
|
385
|
+
if (isRemoved(error)) return "This document does not exist. It may have been removed.";
|
|
386
|
+
const code = typeof error === "object" && error !== null ? error.code : void 0;
|
|
387
|
+
if (code === "UNAUTHENTICATED") return "The server did not accept the token.";
|
|
388
|
+
if (code === "FORBIDDEN") return "You may not open this document.";
|
|
389
|
+
return error instanceof Error ? error.message : "Something went wrong.";
|
|
390
|
+
};
|
|
391
|
+
function DocumentEditor({ id, client, principal, room, onError, onLeave }) {
|
|
392
|
+
checkPrincipal(principal);
|
|
393
|
+
const editable = principal.role === "editor";
|
|
394
|
+
const [attempt, setAttempt] = useState2(0);
|
|
395
|
+
const load = useEditorSession(client, id, principal.name, editable, room, attempt, onLeave);
|
|
396
|
+
const reported = useRef2(void 0);
|
|
397
|
+
useEffect2(() => {
|
|
398
|
+
if (load.state === "error" && reported.current !== load.error) {
|
|
399
|
+
reported.current = load.error;
|
|
400
|
+
onError?.(load.error);
|
|
401
|
+
}
|
|
402
|
+
}, [load, onError]);
|
|
403
|
+
if (load.state === "loading") return /* @__PURE__ */ jsx2("p", { role: "status", className: "adoc muted", children: "Opening document\u2026" });
|
|
404
|
+
if (load.state === "error") {
|
|
405
|
+
return /* @__PURE__ */ jsx2("div", { role: "alert", className: "adoc notice", children: /* @__PURE__ */ jsx2(EmptyState2, { title: "Could not open this document", description: describe(load.error), action: /* @__PURE__ */ jsx2(Button2, { onClick: () => setAttempt(attempt + 1), children: "Try again" }) }) });
|
|
406
|
+
}
|
|
407
|
+
return /* @__PURE__ */ jsx2(Boundary, { children: /* @__PURE__ */ jsx2(Surface, { live: load.live, editable, principal }) });
|
|
408
|
+
}
|
|
409
|
+
function mountEditor(element, options) {
|
|
410
|
+
checkPrincipal(options.principal);
|
|
411
|
+
const root = createRoot(element);
|
|
412
|
+
const leaving = /* @__PURE__ */ new Set();
|
|
413
|
+
const track = (done) => {
|
|
414
|
+
leaving.add(done);
|
|
415
|
+
void done.catch(() => void 0).finally(() => leaving.delete(done));
|
|
416
|
+
};
|
|
417
|
+
const render = (next) => root.render(/* @__PURE__ */ jsx2(DocumentEditor, { ...next, onLeave: (done) => (track(done), next.onLeave?.(done)) }, next.id));
|
|
418
|
+
render(options);
|
|
419
|
+
return {
|
|
420
|
+
update: (next) => (checkPrincipal(next.principal), render(next)),
|
|
421
|
+
unmount: async () => {
|
|
422
|
+
root.unmount();
|
|
423
|
+
await Promise.all(leaving);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
export {
|
|
428
|
+
DocumentEditor,
|
|
429
|
+
mountEditor,
|
|
430
|
+
useEditorSession
|
|
431
|
+
};
|