agentic-comments 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +177 -2
- package/SKILL.md +34 -0
- package/dist/index.d.ts +332 -0
- package/dist/index.js +761 -0
- package/llms.txt +14 -0
- package/package.json +60 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pooria Arab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,178 @@
|
|
|
1
|
-
#
|
|
1
|
+
# agentic-comments
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://opensource.org/license/mit)
|
|
4
|
+
|
|
5
|
+
[Source](https://github.com/pooriaarab/agentic-comments) · [Support](https://github.com/pooriaarab/agentic-comments/issues)
|
|
6
|
+
|
|
7
|
+
Comment threads and suggestions anchored to any content, from people and agents.
|
|
8
|
+
|
|
9
|
+
> Status: first release, 0.1.0. Threads, suggestions, and anchor survival work. The CLI, the MCP
|
|
10
|
+
> server, and notifications come next.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Requires Node.js 20 or later. The quick start imports `yjs`, so install it with this package:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install agentic-comments yjs
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
To build from source, see Develop.
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import * as Y from "yjs";
|
|
26
|
+
import { Comments, stamp, textAnchor } from "agentic-comments";
|
|
27
|
+
|
|
28
|
+
const doc = new Y.Doc();
|
|
29
|
+
doc.getText("body").insert(0, "Hello brave world");
|
|
30
|
+
|
|
31
|
+
const comments = new Comments(doc);
|
|
32
|
+
const ada = stamp({ kind: "human", name: "ada" });
|
|
33
|
+
const scribe = stamp({ kind: "agent_of_human", name: "scribe" });
|
|
34
|
+
|
|
35
|
+
const thread = comments.add({ anchor: textAnchor(doc.getText("body"), 6, 11), body: "Word choice? @scribe" }, ada);
|
|
36
|
+
comments.reply(thread.id, { body: "I suggest a shorter word." }, scribe);
|
|
37
|
+
comments.resolve(thread.id, ada);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The `Comments` object lives on a Yjs document. Send the document updates with your
|
|
41
|
+
normal sync, and every replica sees the same threads.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- Threads anchored to a text range, a cell key, a slide element, or a canvas element.
|
|
46
|
+
You can add your own anchor type.
|
|
47
|
+
- A text anchor uses Yjs relative positions, so it follows its text through edits.
|
|
48
|
+
- Messages with mentions. Resolve and reopen a thread.
|
|
49
|
+
- Suggestions: a change your app proposes. A person, or an agent with the accept scope, accepts it.
|
|
50
|
+
An agent never accepts or rejects its own. Two replicas that accept at once apply it once.
|
|
51
|
+
- The host stamps every author. An operation never reads an author from its input.
|
|
52
|
+
- Every message keeps the author kind, so agent comments stay marked as machine-made.
|
|
53
|
+
- Replies and status merge across replicas. Two replicas that resolve at once agree.
|
|
54
|
+
- A malformed thread from a bad replica is skipped. A bad message is left out, and `skipped` on the
|
|
55
|
+
thread counts those. `problems()` names them.
|
|
56
|
+
|
|
57
|
+
## When the content changes
|
|
58
|
+
|
|
59
|
+
Every thread and suggestion has an `anchorState`:
|
|
60
|
+
|
|
61
|
+
| State | Meaning |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `attached` | The content is there, as it was. |
|
|
64
|
+
| `edited` | A text range is there, with other text. `currentText` shows it. |
|
|
65
|
+
| `orphaned` | The content is gone. The thread stays readable and takes replies. |
|
|
66
|
+
| `unknown` | Nothing can tell. A cell, slide, canvas, or custom anchor has no host answer. |
|
|
67
|
+
|
|
68
|
+
- A text anchor follows its text through edits by any replica. Deleting the whole range orphans it.
|
|
69
|
+
- For other anchors, pass `exists(anchor)` to `new Comments`. Return `true` or `false`. Any other answer,
|
|
70
|
+
or a throw, means `unknown`.
|
|
71
|
+
- `list({ anchorState: "orphaned" })` finds threads to review.
|
|
72
|
+
- `relocate(threadId, actor)` moves a text anchor to its quote when the quote occurs exactly once in the
|
|
73
|
+
same text. It never guesses. It throws `RELOCATE_FAILED` for no match or many.
|
|
74
|
+
- Accept refuses a suggestion whose anchor is orphaned or edited. The suggestion stays pending, and a
|
|
75
|
+
person can reject it.
|
|
76
|
+
- `currentText` is document text. It can hold any character, so a UI should treat it as untrusted.
|
|
77
|
+
|
|
78
|
+
## Suggestions
|
|
79
|
+
|
|
80
|
+
Your app owns the change. This package stores it as JSON, tracks the decision, and calls your `apply`.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { ACCEPT_SCOPE, Suggestions } from "agentic-comments";
|
|
84
|
+
|
|
85
|
+
const suggestions = new Suggestions(comments, {
|
|
86
|
+
apply(change, { anchor, by, ydoc }) {
|
|
87
|
+
// Edit ydoc here. Throw before you edit when the change cannot apply.
|
|
88
|
+
return { /* a receipt, JSON that lets revert undo this edit */ };
|
|
89
|
+
},
|
|
90
|
+
revert(receipt, { ydoc }) {
|
|
91
|
+
// Undo a duplicate edit. Every replica runs this at once, so use an edit that merges to one result.
|
|
92
|
+
// Throw before you change anything.
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const s = suggestions.add({ anchor, change: { replacement: "bold" }, summary: "Word choice" }, scribe);
|
|
97
|
+
suggestions.accept(s.id, ada);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- A signed-in person (`human`) accepts or rejects. Every other kind, including `user_token`, needs the
|
|
101
|
+
`comments:accept` scope, and never decides a suggestion it wrote. Names match exactly, in Unicode NFC form (a name in another form is refused), and two
|
|
102
|
+
principals that are not signed-in people with the same name count as one, whatever their kinds.
|
|
103
|
+
- Accept checks that a text anchor still matches the document, then runs the edit and writes the
|
|
104
|
+
record in one step. If `apply` throws before it edits, the suggestion stays pending. If it edits and
|
|
105
|
+
then fails, the decision is recorded without a receipt, and `problems()` reports it. A host that cannot undo a double apply must say `idempotent: true`.
|
|
106
|
+
- Every replica of a document must use the same host mode: the same `revert`, or the same `idempotent`
|
|
107
|
+
flag. A mix can leave a duplicate edit in the document.
|
|
108
|
+
- `problems()` entries about an edit that failed after it landed, or a duplicate with no receipt, live in
|
|
109
|
+
memory on the replica that saw them. Other replicas and a reload do not show them.
|
|
110
|
+
- A decision by an agent with the accept scope is final for everyone, including people.
|
|
111
|
+
- An accept on a replica that has not yet seen a reject wins, even when a person made the reject long before.
|
|
112
|
+
The window is any time the replicas were apart. An agent with the scope can override a person this way.
|
|
113
|
+
A host that wants the person to win can accept only while its replica is in sync.
|
|
114
|
+
- Two replicas that accept before they meet each apply the edit. After the merge, the application of
|
|
115
|
+
the lowest client id stays and `revert` undoes each other one. An accept and a reject at once end as
|
|
116
|
+
accepted.
|
|
117
|
+
- A receipt is stored data. A peer that can write comment data can choose one, and can rewrite the
|
|
118
|
+
`anchor` to match. Check the receipt in `revert` before you use it. The sync layer's write control is
|
|
119
|
+
the real protection. A record with no receipt (an `apply` that edited and
|
|
120
|
+
then failed) always stays, and the other application is undone. If two records have none, the extra
|
|
121
|
+
edit cannot be undone. `problems()` reports it. With `idempotent: true`, a duplicate is never undone.
|
|
122
|
+
- `apply` and `revert` must be synchronous, and must edit only the Yjs document. `accept` cannot run
|
|
123
|
+
inside another Yjs transaction, so not inside an observer or an `apply`.
|
|
124
|
+
- A peer that can write the document can write a decision record. Records with a bad author, time, or
|
|
125
|
+
receipt, or from a decider who wrote the suggestion as an agent, are not counted. A well-formed forged
|
|
126
|
+
record is, and it can win settle, so every replica undoes the real edit. The sync layer must decide who
|
|
127
|
+
may write.
|
|
128
|
+
|
|
129
|
+
## Author kinds
|
|
130
|
+
|
|
131
|
+
| Kind | Meaning | Label |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| `human` | A person signed in. | person |
|
|
134
|
+
| `user_token` | A person, through a token an agent holds. | person via agent |
|
|
135
|
+
| `agent_of_human` | An agent that acts for a person. | agent |
|
|
136
|
+
| `agent_of_agent` | An agent that acts for another agent. | agent |
|
|
137
|
+
| `agent` | An agent with its own identity. | agent |
|
|
138
|
+
|
|
139
|
+
## Limits
|
|
140
|
+
|
|
141
|
+
- No file store, CLI, MCP server, or HTTP API yet.
|
|
142
|
+
- `add` checks that a text anchor points into this document and matches its quote.
|
|
143
|
+
- The library trusts the host to call `stamp()` only for a principal it authenticated.
|
|
144
|
+
- A reply to a resolved thread is refused on the replica that sees the resolve. A reply and a
|
|
145
|
+
resolve made at once on two replicas both stay. The merged thread is resolved and holds the reply.
|
|
146
|
+
- Author and kind are only as trustworthy as every peer that can write the document. A peer can write
|
|
147
|
+
any author straight into the Yjs data. The sync layer must decide who may write.
|
|
148
|
+
- `relocate` and `exists` never touch a thread whose anchor is attached. `exists` runs for every thread
|
|
149
|
+
on every read, so keep it fast.
|
|
150
|
+
- A local reply stops at 1,000 messages. Replicas that reply at once can pass that. A read shows the first
|
|
151
|
+
2,000 valid messages. `skipped` and `problems()` report the rest.
|
|
152
|
+
- Every replica of a document must register the same custom anchor kinds.
|
|
153
|
+
- A repeated message id hides every copy. A peer that appends a copy of a thread's only message makes
|
|
154
|
+
the thread unreadable.
|
|
155
|
+
- The quoted text of a text anchor is not checked when a thread is read. A peer can store any text there.
|
|
156
|
+
`textAnchor` refuses a quote whose JSON form passes 4,096 characters. Quotes with many newlines or
|
|
157
|
+
quotation marks hit that before 2,000 characters.
|
|
158
|
+
- The first visible message decides when a thread started, so a peer that inserts an older message at
|
|
159
|
+
the front moves the thread in `list()`.
|
|
160
|
+
- A read scans every stored entry. A peer that writes millions of junk entries slows every read.
|
|
161
|
+
- A body mentions anyone named as `@handle`. Put `@` text in backticks to avoid that. Any two backticks in a body pair up, so a loose backtick
|
|
162
|
+
can hide a real mention that sits between two of them. A scoped
|
|
163
|
+
package name such as `@scope/pkg` is not a mention.
|
|
164
|
+
- Mentions are stored in lowercase, with no leading `@`. A handle holds up to 64 letters, digits, marks,
|
|
165
|
+
and `. _ -`. A longer handle in a body is refused.
|
|
166
|
+
|
|
167
|
+
## Develop
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
git clone https://github.com/pooriaarab/agentic-comments
|
|
171
|
+
cd agentic-comments
|
|
172
|
+
npm ci
|
|
173
|
+
npm run ci:local
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT.
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agentic-comments
|
|
3
|
+
description: Comment threads and suggestions anchored to any content, from people and agents. Keywords: comments,threads,mentions,suggestions,annotations,collaboration,agents,yjs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# agentic-comments
|
|
7
|
+
|
|
8
|
+
Comment threads and suggestions anchored to any content, from people and agents.
|
|
9
|
+
|
|
10
|
+
## Use the SDK
|
|
11
|
+
|
|
12
|
+
The host gives you an `Actor` that names you. Pass it to each call.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { Comments } from "agentic-comments";
|
|
16
|
+
|
|
17
|
+
const comments = new Comments(ydoc);
|
|
18
|
+
const thread = comments.add({ anchor: { type: "cell", key: "Sheet1!B4" }, body: "Check this total." }, actor);
|
|
19
|
+
comments.reply(thread.id, { body: "Fixed." }, actor);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Check `thread.anchorState`. An `orphaned` thread has lost its content. Read it, reply, or call
|
|
23
|
+
`comments.relocate(id, actor)`. Do not treat it as bad data.
|
|
24
|
+
|
|
25
|
+
Suggest a change with `suggestions.add({ anchor, change, summary }, actor)`. Accept needs a person or
|
|
26
|
+
the `comments:accept` scope. You never accept or reject your own suggestion.
|
|
27
|
+
|
|
28
|
+
The CLI and the MCP server are not built yet.
|
|
29
|
+
|
|
30
|
+
## Rules
|
|
31
|
+
|
|
32
|
+
- The host stamps the author. Never call `stamp()` yourself, and never pass an author in an input.
|
|
33
|
+
- Reopen a resolved thread before you reply.
|
|
34
|
+
- Report refusals and errors as failures.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
|
|
3
|
+
declare const VERSION: string;
|
|
4
|
+
|
|
5
|
+
/** A Yjs relative position as JSON. It names an item, not an index, so it follows its text. */
|
|
6
|
+
interface RelativePositionJson {
|
|
7
|
+
type?: {
|
|
8
|
+
client: number;
|
|
9
|
+
clock: number;
|
|
10
|
+
};
|
|
11
|
+
tname?: string;
|
|
12
|
+
item?: {
|
|
13
|
+
client: number;
|
|
14
|
+
clock: number;
|
|
15
|
+
};
|
|
16
|
+
assoc?: number;
|
|
17
|
+
}
|
|
18
|
+
interface TextAnchor {
|
|
19
|
+
type: "text";
|
|
20
|
+
/** The text under the range when the comment was made. `Comments.add` checks it against the document. */
|
|
21
|
+
text: string;
|
|
22
|
+
start: RelativePositionJson;
|
|
23
|
+
end: RelativePositionJson;
|
|
24
|
+
}
|
|
25
|
+
interface CellAnchor {
|
|
26
|
+
type: "cell";
|
|
27
|
+
/** A cell key the host app defines, such as `Sheet1!B4` or a stable cell id. */
|
|
28
|
+
key: string;
|
|
29
|
+
}
|
|
30
|
+
interface SlideAnchor {
|
|
31
|
+
type: "slide";
|
|
32
|
+
elementId: string;
|
|
33
|
+
}
|
|
34
|
+
interface CanvasAnchor {
|
|
35
|
+
type: "canvas";
|
|
36
|
+
elementId: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Where a thread points. A custom kind (see `AnchorKind`) returns its own shape, so narrow on `type`
|
|
40
|
+
* before you read its fields.
|
|
41
|
+
*/
|
|
42
|
+
type Anchor = TextAnchor | CellAnchor | SlideAnchor | CanvasAnchor;
|
|
43
|
+
/** Teach `Comments` a new anchor type. `parse` returns a clean copy or throws. */
|
|
44
|
+
interface AnchorKind {
|
|
45
|
+
type: string;
|
|
46
|
+
parse(raw: Record<string, unknown>): {
|
|
47
|
+
type: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether the content under an anchor is still there.
|
|
52
|
+
* `attached`: yes, as it was. `edited`: the range is there, with other text. `orphaned`: the content is
|
|
53
|
+
* gone. `unknown`: nothing can tell, as for a cell with no host answer.
|
|
54
|
+
*/
|
|
55
|
+
type AnchorState = {
|
|
56
|
+
state: "attached";
|
|
57
|
+
} | {
|
|
58
|
+
state: "edited";
|
|
59
|
+
currentText: string;
|
|
60
|
+
} | {
|
|
61
|
+
state: "orphaned";
|
|
62
|
+
} | {
|
|
63
|
+
state: "unknown";
|
|
64
|
+
};
|
|
65
|
+
/** The text of a Yjs text type. An embed counts as one character, so indexes match Yjs positions. */
|
|
66
|
+
declare function textOf(type: Y.Text | Y.XmlText): string;
|
|
67
|
+
/** Pin a range of a Yjs text. Later edits by any replica do not move the anchor off its text. */
|
|
68
|
+
declare function textAnchor(type: Y.Text | Y.XmlText, from: number, to: number): TextAnchor;
|
|
69
|
+
|
|
70
|
+
declare const PRINCIPAL_KINDS: readonly ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
|
|
71
|
+
type PrincipalKind = (typeof PRINCIPAL_KINDS)[number];
|
|
72
|
+
/** What a thread keeps about who wrote a message. Scopes never reach storage. */
|
|
73
|
+
interface Author {
|
|
74
|
+
kind: PrincipalKind;
|
|
75
|
+
name: string;
|
|
76
|
+
}
|
|
77
|
+
interface StampInput {
|
|
78
|
+
kind: PrincipalKind;
|
|
79
|
+
name: string;
|
|
80
|
+
/** Rights the host granted this principal, such as `comments:accept`. Used to decide, never stored. */
|
|
81
|
+
scopes?: string[];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The only author a write accepts. The host makes one from the principal it authenticated, and an
|
|
85
|
+
* operation never reads an author from its input. An object that `stamp()` did not make is refused,
|
|
86
|
+
* and a stamped one cannot change.
|
|
87
|
+
*/
|
|
88
|
+
declare class Actor implements Author {
|
|
89
|
+
readonly kind: PrincipalKind;
|
|
90
|
+
readonly name: string;
|
|
91
|
+
readonly scopes: readonly string[];
|
|
92
|
+
private constructor();
|
|
93
|
+
static of(input: StampInput): Actor;
|
|
94
|
+
static is(value: unknown): value is Actor;
|
|
95
|
+
}
|
|
96
|
+
/** Make the author for one authenticated principal. Throws when the kind or name is missing or wrong. */
|
|
97
|
+
declare const stamp: (input: StampInput) => Actor;
|
|
98
|
+
/** The label a UI shows. Agent work stays marked as machine-made. */
|
|
99
|
+
declare function authorLabel(author: Author): "person" | "person via agent" | "agent";
|
|
100
|
+
|
|
101
|
+
interface Message {
|
|
102
|
+
id: string;
|
|
103
|
+
at: string;
|
|
104
|
+
author: Author;
|
|
105
|
+
body: string;
|
|
106
|
+
/** Handles named in the body or listed on the input, without the `@`. */
|
|
107
|
+
mentions: string[];
|
|
108
|
+
}
|
|
109
|
+
interface Thread {
|
|
110
|
+
id: string;
|
|
111
|
+
anchor: Anchor;
|
|
112
|
+
status: "open" | "resolved";
|
|
113
|
+
/**
|
|
114
|
+
* The status change that won, and when. For a new thread, who opened it. Replicas that change the
|
|
115
|
+
* status at once agree on one change. Yjs picks it by client id, not by time.
|
|
116
|
+
*/
|
|
117
|
+
statusBy: Author;
|
|
118
|
+
statusAt: string;
|
|
119
|
+
messages: Message[];
|
|
120
|
+
/** Whether the anchored content is still there. A thread whose content is gone stays readable. */
|
|
121
|
+
anchorState: AnchorState;
|
|
122
|
+
/** How many stored entries this read did not show: invalid ones, repeated ids, and valid ones past the read limit. `problems()` names them. */
|
|
123
|
+
skipped: number;
|
|
124
|
+
}
|
|
125
|
+
interface CommentsOptions {
|
|
126
|
+
now?: () => Date;
|
|
127
|
+
/** More anchor types. A type that is already built in is refused. */
|
|
128
|
+
anchors?: AnchorKind[];
|
|
129
|
+
/**
|
|
130
|
+
* Tell whether the content a cell, slide, canvas, or custom anchor points at still exists. Return `true`
|
|
131
|
+
* or `false`. Any other answer, or a throw, means unknown. Text anchors do not need it.
|
|
132
|
+
*/
|
|
133
|
+
exists?: (anchor: Anchor) => boolean | undefined;
|
|
134
|
+
}
|
|
135
|
+
interface NewThread {
|
|
136
|
+
anchor: Anchor;
|
|
137
|
+
body: string;
|
|
138
|
+
mentions?: string[];
|
|
139
|
+
}
|
|
140
|
+
interface NewReply {
|
|
141
|
+
body: string;
|
|
142
|
+
mentions?: string[];
|
|
143
|
+
}
|
|
144
|
+
interface ThreadFilter {
|
|
145
|
+
status?: Thread["status"];
|
|
146
|
+
anchorType?: string;
|
|
147
|
+
/** A handle, with or without the `@`. */
|
|
148
|
+
mentioning?: string;
|
|
149
|
+
anchorState?: AnchorState["state"];
|
|
150
|
+
}
|
|
151
|
+
/** Comment threads on a Yjs document. Replies and status merge across replicas. */
|
|
152
|
+
declare class Comments {
|
|
153
|
+
readonly ydoc: Y.Doc;
|
|
154
|
+
private readonly kinds;
|
|
155
|
+
private readonly now;
|
|
156
|
+
private readonly exists?;
|
|
157
|
+
/** Every replica of a document must register the same custom anchor kinds. Others read those threads as malformed. */
|
|
158
|
+
constructor(ydoc: Y.Doc, options?: CommentsOptions);
|
|
159
|
+
private get map();
|
|
160
|
+
/** The clock, as an ISO time. Used by `Suggestions`. */
|
|
161
|
+
time(): string;
|
|
162
|
+
/** Whether the content under an anchor is still there. This never throws. */
|
|
163
|
+
anchorState(anchor: Anchor): AnchorState;
|
|
164
|
+
/** Read an anchor from stored data. This does not check the document. */
|
|
165
|
+
parseAnchor(raw: unknown): Anchor;
|
|
166
|
+
/** Parse an anchor from input and check it against this document. Throws `INVALID_ANCHOR` when it fails. */
|
|
167
|
+
checkAnchor(raw: unknown): Anchor;
|
|
168
|
+
/** A text anchor must name text in this document, in order, and match its quote. */
|
|
169
|
+
private checkInDocument;
|
|
170
|
+
/** Build a message, and refuse it unless a read would show it back. Nothing is written yet. */
|
|
171
|
+
private message;
|
|
172
|
+
private readMessage;
|
|
173
|
+
/** A thread with a bad anchor or status is malformed. A bad message is skipped and named in `issues`. */
|
|
174
|
+
private read;
|
|
175
|
+
private fields;
|
|
176
|
+
/** Start a thread with its first message. The anchor and body are checked before anything is written. */
|
|
177
|
+
add(input: NewThread, actor: Actor): Thread;
|
|
178
|
+
/**
|
|
179
|
+
* Add a message. The resolved check runs on this replica only. A reply and a resolve made at once
|
|
180
|
+
* on two replicas both stay, and the merged thread is resolved with the reply in it.
|
|
181
|
+
*/
|
|
182
|
+
reply(threadId: string, input: NewReply, actor: Actor): Message;
|
|
183
|
+
private setStatus;
|
|
184
|
+
/**
|
|
185
|
+
* Move a text anchor whose range is gone or edited to its quote, when the quote occurs exactly once in
|
|
186
|
+
* the same text. There is no fuzzy match. Throws `RELOCATE_FAILED` when it cannot say where. An attached
|
|
187
|
+
* thread is returned as it is.
|
|
188
|
+
*/
|
|
189
|
+
relocate(threadId: string, actor: Actor): Thread;
|
|
190
|
+
resolve(threadId: string, actor: Actor): Thread;
|
|
191
|
+
reopen(threadId: string, actor: Actor): Thread;
|
|
192
|
+
get(threadId: string): Thread;
|
|
193
|
+
/** Threads in the order they were started. A malformed thread is skipped; `problems()` names it. */
|
|
194
|
+
list(filter?: ThreadFilter): Thread[];
|
|
195
|
+
/** Threads or messages that could not be read, for example from a replica that wrote bad data. */
|
|
196
|
+
problems(): {
|
|
197
|
+
id: string;
|
|
198
|
+
reason: string;
|
|
199
|
+
}[];
|
|
200
|
+
private readAll;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
type CommentsErrorCode = "INVALID_AUTHOR" | "INVALID_INPUT" | "INVALID_ANCHOR" | "NO_THREAD" | "THREAD_RESOLVED" | "CORRUPT" | "NO_SUGGESTION" | "FORBIDDEN" | "ALREADY_DECIDED" | "APPLY_FAILED" | "RELOCATE_FAILED";
|
|
204
|
+
declare class CommentsError extends Error {
|
|
205
|
+
readonly code: CommentsErrorCode;
|
|
206
|
+
constructor(code: CommentsErrorCode, message: string);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The scope that lets an agent accept or reject a suggestion that another principal wrote. */
|
|
210
|
+
declare const ACCEPT_SCOPE = "comments:accept";
|
|
211
|
+
interface Suggestion {
|
|
212
|
+
id: string;
|
|
213
|
+
anchor: Anchor;
|
|
214
|
+
/** Whether the anchored content is still there. Accept refuses an orphaned or edited one. */
|
|
215
|
+
anchorState: AnchorState;
|
|
216
|
+
/** What the host app proposes. This package never reads it. The host's `apply` does. */
|
|
217
|
+
change: unknown;
|
|
218
|
+
summary?: string;
|
|
219
|
+
author: Author;
|
|
220
|
+
createdAt: string;
|
|
221
|
+
status: "pending" | "accepted" | "rejected";
|
|
222
|
+
decidedBy?: Author;
|
|
223
|
+
decidedAt?: string;
|
|
224
|
+
}
|
|
225
|
+
interface ApplyContext {
|
|
226
|
+
suggestionId: string;
|
|
227
|
+
anchor: Anchor;
|
|
228
|
+
/** Who wrote the suggestion. */
|
|
229
|
+
author: Author;
|
|
230
|
+
/** Who accepts it. Label the edit with this author. */
|
|
231
|
+
by: Author;
|
|
232
|
+
ydoc: Y.Doc;
|
|
233
|
+
}
|
|
234
|
+
interface RevertContext {
|
|
235
|
+
suggestionId: string;
|
|
236
|
+
anchor: Anchor;
|
|
237
|
+
/** Who wrote the suggestion. */
|
|
238
|
+
author: Author;
|
|
239
|
+
/** Who made the application being undone. */
|
|
240
|
+
by: Author;
|
|
241
|
+
ydoc: Y.Doc;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The host app's side. Each hook is synchronous, so the check and the edit stay in one step.
|
|
245
|
+
* Make every edit in `ydoc`, so it merges with the rest.
|
|
246
|
+
*/
|
|
247
|
+
interface SuggestionHost {
|
|
248
|
+
/** Check a change when it is suggested. Return the clean change, or throw. */
|
|
249
|
+
parse?(change: unknown): unknown;
|
|
250
|
+
/**
|
|
251
|
+
* Make the edit. Throw before you change anything when the edit cannot apply, for example when the
|
|
252
|
+
* anchored content changed. Yjs cannot undo a partial edit. If `apply` edits and then fails, the
|
|
253
|
+
* decision is recorded as accepted with no receipt, and `problems()` reports it. Return a receipt,
|
|
254
|
+
* JSON data that lets `revert` undo this edit.
|
|
255
|
+
*/
|
|
256
|
+
apply(change: unknown, context: ApplyContext): unknown;
|
|
257
|
+
/**
|
|
258
|
+
* Two replicas can accept one suggestion before they meet. Each applies the edit. After a merge one
|
|
259
|
+
* application stays and `revert` undoes each other. Every replica runs it at the same time, and the
|
|
260
|
+
* results must merge to one outcome, as a Yjs delete of the same range does. Throw before you change
|
|
261
|
+
* anything. A receipt comes from stored data. A peer that can write comment data can choose it, so
|
|
262
|
+
* check it before you use it. The anchor and the author are stored data too, so a check against them
|
|
263
|
+
* helps against a mistake and not against a peer that can write comment data.
|
|
264
|
+
*/
|
|
265
|
+
revert?(receipt: unknown, context: RevertContext): void;
|
|
266
|
+
/** Say that applying the same change twice is harmless. Then `revert` is not needed, and a duplicate is not undone. */
|
|
267
|
+
idempotent?: boolean;
|
|
268
|
+
}
|
|
269
|
+
interface NewSuggestion {
|
|
270
|
+
anchor: Anchor;
|
|
271
|
+
change: unknown;
|
|
272
|
+
summary?: string;
|
|
273
|
+
}
|
|
274
|
+
interface SuggestionFilter {
|
|
275
|
+
status?: Suggestion["status"];
|
|
276
|
+
anchorState?: AnchorState["state"];
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Suggested changes to content, from people and agents. Nothing changes until a person, or an agent
|
|
280
|
+
* with the accept scope that did not write it, accepts. Accept and reject are safe across replicas.
|
|
281
|
+
*/
|
|
282
|
+
declare class Suggestions {
|
|
283
|
+
private readonly comments;
|
|
284
|
+
private readonly host;
|
|
285
|
+
private readonly onTransaction;
|
|
286
|
+
private failures;
|
|
287
|
+
constructor(comments: Comments, host: SuggestionHost);
|
|
288
|
+
/** Stop listening to the document. */
|
|
289
|
+
close(): void;
|
|
290
|
+
private get ydoc();
|
|
291
|
+
private get map();
|
|
292
|
+
private get applied();
|
|
293
|
+
private get rejected();
|
|
294
|
+
private change;
|
|
295
|
+
/**
|
|
296
|
+
* Records of one kind for one suggestion, in client order. A record that is not valid is not counted:
|
|
297
|
+
* one with no author or time, a receipt that is not small JSON, or a decider who decided their own work.
|
|
298
|
+
*/
|
|
299
|
+
private records;
|
|
300
|
+
/** Group the entries of a decision map by suggestion id in one pass, so a read does not scan the map per suggestion. */
|
|
301
|
+
private index;
|
|
302
|
+
private read;
|
|
303
|
+
private fields;
|
|
304
|
+
/** Propose a change. Anyone can suggest. The host stamps the author. */
|
|
305
|
+
add(input: NewSuggestion, actor: Actor): Suggestion;
|
|
306
|
+
/** A person decides. An agent needs the accept scope, and never decides its own suggestion. */
|
|
307
|
+
private mayDecide;
|
|
308
|
+
/**
|
|
309
|
+
* Apply the change as the accepting author and record the decision, in one step. Accepting an
|
|
310
|
+
* accepted suggestion changes nothing. When `apply` throws, the suggestion stays pending.
|
|
311
|
+
*/
|
|
312
|
+
accept(suggestionId: string, actor: Actor): Suggestion;
|
|
313
|
+
/** Same right as accept. A person can withdraw their own suggestion. */
|
|
314
|
+
reject(suggestionId: string, actor: Actor): Suggestion;
|
|
315
|
+
get(suggestionId: string): Suggestion;
|
|
316
|
+
/** In the order they were made. A malformed suggestion is skipped, and `problems()` names it. */
|
|
317
|
+
list(filter?: SuggestionFilter): Suggestion[];
|
|
318
|
+
/** Suggestions that could not be read, and reverts that failed. */
|
|
319
|
+
problems(): {
|
|
320
|
+
id: string;
|
|
321
|
+
reason: string;
|
|
322
|
+
}[];
|
|
323
|
+
private readAll;
|
|
324
|
+
/**
|
|
325
|
+
* Two replicas can accept one suggestion before they meet. After a merge, the application of the
|
|
326
|
+
* lowest client id stays, and the host reverts each other one. Every replica reaches the same result.
|
|
327
|
+
* A revert that throws leaves its record, so the next merge tries again.
|
|
328
|
+
*/
|
|
329
|
+
settle(): void;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export { ACCEPT_SCOPE, Actor, type Anchor, type AnchorKind, type AnchorState, type ApplyContext, type Author, type CanvasAnchor, type CellAnchor, Comments, CommentsError, type CommentsErrorCode, type CommentsOptions, type Message, type NewReply, type NewSuggestion, type NewThread, PRINCIPAL_KINDS, type PrincipalKind, type RelativePositionJson, type RevertContext, type SlideAnchor, type StampInput, type Suggestion, type SuggestionFilter, type SuggestionHost, Suggestions, type TextAnchor, type Thread, type ThreadFilter, VERSION, authorLabel, stamp, textAnchor, textOf };
|