agentic-comments 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -2
- package/SKILL.md +5 -0
- package/dist/account.d.ts +62 -0
- package/dist/account.js +114 -0
- package/dist/{chunk-VAVKBAHM.js → chunk-4G5BDTBA.js} +574 -574
- package/dist/{chunk-H6JK62SP.js → chunk-6UQ6CDLJ.js} +1 -1
- package/dist/{chunk-UNJL3N2L.js → chunk-J6JGWDUP.js} +3 -3
- package/dist/{chunk-6N53KQVU.js → chunk-LFYTCHXS.js} +1 -1
- package/dist/{chunk-T77DPNIV.js → chunk-TTNT27PG.js} +1 -1
- package/dist/chunk-UEQL27KF.js +148 -0
- package/dist/cli.js +7 -7
- package/dist/client.d.ts +3 -3
- package/dist/client.js +4 -143
- package/dist/{errors-C-xvp24-.d.ts → errors-Cin-719h.d.ts} +3 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/local.d.ts +2 -2
- package/dist/local.js +3 -3
- package/dist/mcp-IFQQHOA2.js +11 -0
- package/dist/mcp-bin.js +9 -9
- package/dist/{service-BvIE6t5n.d.ts → service-ZdZPWrwI.d.ts} +1 -1
- package/dist/{suggestions-CnkEcTuY.d.ts → suggestions-G1seGf5d.d.ts} +5 -2
- package/dist/ui.css +54 -0
- package/dist/ui.d.ts +46 -0
- package/dist/ui.js +332 -0
- package/dist/worker.d.ts +26 -2
- package/dist/worker.js +91 -3
- package/llms.txt +2 -0
- package/package.json +39 -7
- package/dist/mcp-WBAHKMF4.js +0 -11
package/dist/ui.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createClient
|
|
3
|
+
} from "./chunk-UEQL27KF.js";
|
|
4
|
+
|
|
5
|
+
// src/ui/panel.tsx
|
|
6
|
+
import { Component, createContext, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
|
|
7
|
+
import { AgentBadge, Avatar, Button, Input, Textarea } from "agentic-design/react";
|
|
8
|
+
|
|
9
|
+
// src/ui/rules.ts
|
|
10
|
+
var LEVELS = ["comments:read", "comments:comment", "comments:write", "comments:admin"];
|
|
11
|
+
var level = (scopes) => Math.max(-1, ...scopes.map((s) => LEVELS.indexOf(s)));
|
|
12
|
+
function holds(me, need) {
|
|
13
|
+
return me !== void 0 && level(me.scopes) >= LEVELS.indexOf(`comments:${need}`);
|
|
14
|
+
}
|
|
15
|
+
function mayDecide(me, suggestion) {
|
|
16
|
+
if (suggestion.status !== "pending") return { allowed: false };
|
|
17
|
+
if (!me) return { allowed: false, reason: "Sign in to decide this suggestion." };
|
|
18
|
+
const agent = me.kind !== "human";
|
|
19
|
+
if (agent ? level(me.scopes) < LEVELS.indexOf("comments:admin") && !me.scopes.includes("comments:accept") : !holds(me, "write")) {
|
|
20
|
+
return { allowed: false, reason: agent ? "An agent needs the accept right to decide a suggestion. A person can." : "Your access cannot accept or reject suggestions." };
|
|
21
|
+
}
|
|
22
|
+
const { author } = suggestion;
|
|
23
|
+
if (agent && author.kind !== "human" && author.name === me.name) {
|
|
24
|
+
return { allowed: false, reason: "An agent cannot accept or reject its own suggestion. A person or another agent decides." };
|
|
25
|
+
}
|
|
26
|
+
return { allowed: true };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/ui/panel.tsx
|
|
30
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
31
|
+
var VIEWS = [
|
|
32
|
+
{ id: "open", label: "Open" },
|
|
33
|
+
{ id: "resolved", label: "Resolved" },
|
|
34
|
+
{ id: "all", label: "All" }
|
|
35
|
+
];
|
|
36
|
+
var Ctx = createContext(void 0);
|
|
37
|
+
function useCtx() {
|
|
38
|
+
const value = useContext(Ctx);
|
|
39
|
+
if (!value) throw new Error("A panel card needs a CommentsPanel above it.");
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
var when = new Intl.DateTimeFormat("en", { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" });
|
|
43
|
+
var stamp = (iso) => {
|
|
44
|
+
const date = new Date(iso);
|
|
45
|
+
return Number.isNaN(date.getTime()) ? iso : `${when.format(date)} UTC`;
|
|
46
|
+
};
|
|
47
|
+
var only = (list) => Array.isArray(list) ? list.filter((x) => typeof x === "object" && x !== null) : [];
|
|
48
|
+
function failure(error) {
|
|
49
|
+
const code = typeof error?.code === "string" ? `${error.code}: ` : "";
|
|
50
|
+
return `${code}${error instanceof Error ? error.message : String(error)}`;
|
|
51
|
+
}
|
|
52
|
+
function Who({ author, agentLabel }) {
|
|
53
|
+
const { actingFor } = useContext(Ctx) ?? {};
|
|
54
|
+
if (author.kind === "human") {
|
|
55
|
+
return /* @__PURE__ */ jsxs("span", { className: "agd-comments__who", children: [
|
|
56
|
+
/* @__PURE__ */ jsx(Avatar, { entry: { id: `person-${author.name}`, name: author.name, kind: "person" } }),
|
|
57
|
+
/* @__PURE__ */ jsx("strong", { className: "agd-comments__name", children: author.name })
|
|
58
|
+
] });
|
|
59
|
+
}
|
|
60
|
+
return /* @__PURE__ */ jsx(AgentBadge, { name: author.name, label: agentLabel, actingFor: actingFor?.(author) });
|
|
61
|
+
}
|
|
62
|
+
function anchorLabel(anchor) {
|
|
63
|
+
switch (anchor.type) {
|
|
64
|
+
case "text":
|
|
65
|
+
return "Text";
|
|
66
|
+
case "cell":
|
|
67
|
+
return `Cell ${anchor.key}`;
|
|
68
|
+
case "slide":
|
|
69
|
+
return `Slide element ${anchor.elementId}`;
|
|
70
|
+
case "canvas":
|
|
71
|
+
return `Canvas element ${anchor.elementId}`;
|
|
72
|
+
default:
|
|
73
|
+
return "Anchor";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function AnchorNote({ state }) {
|
|
77
|
+
if (state.state === "attached") return null;
|
|
78
|
+
const text = state.state === "orphaned" ? "The anchored content is gone. You can still read and reply." : state.state === "edited" ? `The text changed since this comment. It now reads: \u201C${state.currentText}\u201D` : "The panel cannot tell if the content is still there.";
|
|
79
|
+
return /* @__PURE__ */ jsx("p", { className: `agd-comments__state agd-comments__state--${state.state}`, children: text });
|
|
80
|
+
}
|
|
81
|
+
function Quote({ anchor }) {
|
|
82
|
+
return /* @__PURE__ */ jsxs("figure", { className: "agd-comments__anchor", children: [
|
|
83
|
+
/* @__PURE__ */ jsx("figcaption", { className: "agd-comments__label", children: anchorLabel(anchor) }),
|
|
84
|
+
anchor.type === "text" && /* @__PURE__ */ jsx("blockquote", { className: "agd-comments__quote", children: anchor.text })
|
|
85
|
+
] });
|
|
86
|
+
}
|
|
87
|
+
function Body({ message }) {
|
|
88
|
+
return /* @__PURE__ */ jsxs("li", { className: "agd-comments__message", children: [
|
|
89
|
+
/* @__PURE__ */ jsxs("div", { className: "agd-comments__meta", children: [
|
|
90
|
+
/* @__PURE__ */ jsx(Who, { author: message.author, agentLabel: "Comment by an agent" }),
|
|
91
|
+
/* @__PURE__ */ jsx("time", { className: "agd-comments__time", dateTime: message.at, children: stamp(message.at) }),
|
|
92
|
+
message.editedAt && /* @__PURE__ */ jsx("span", { className: "agd-comments__time", children: "edited" })
|
|
93
|
+
] }),
|
|
94
|
+
/* @__PURE__ */ jsx("p", { className: "agd-comments__body", children: message.body })
|
|
95
|
+
] });
|
|
96
|
+
}
|
|
97
|
+
function changeText(change) {
|
|
98
|
+
if (typeof change === "object" && change !== null && "replace" in change && typeof change.replace === "string") return change.replace;
|
|
99
|
+
try {
|
|
100
|
+
return JSON.stringify(change) ?? "";
|
|
101
|
+
} catch {
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
var Status = ({ value }) => /* @__PURE__ */ jsx("span", { className: `agd-pill agd-comments__status agd-comments__status--${value}`, children: value[0].toUpperCase() + value.slice(1) });
|
|
106
|
+
function ThreadCard({ thread }) {
|
|
107
|
+
const { client, docId, me, busy, act } = useCtx();
|
|
108
|
+
const id = useId();
|
|
109
|
+
const [draft, setDraft] = useState("");
|
|
110
|
+
const resolved = thread.status === "resolved";
|
|
111
|
+
const mayComment = holds(me, "comment");
|
|
112
|
+
const submit = (event) => {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
const body = draft.trim();
|
|
115
|
+
if (!body) return;
|
|
116
|
+
void act(
|
|
117
|
+
`${thread.id}:reply`,
|
|
118
|
+
async () => {
|
|
119
|
+
await client.reply(docId, thread.id, { body });
|
|
120
|
+
setDraft("");
|
|
121
|
+
},
|
|
122
|
+
"Reply sent."
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
return /* @__PURE__ */ jsx("li", { className: "agd-comments__thread", children: /* @__PURE__ */ jsxs("article", { "aria-labelledby": `${id}-h`, children: [
|
|
126
|
+
/* @__PURE__ */ jsxs("header", { className: "agd-comments__item-top", children: [
|
|
127
|
+
/* @__PURE__ */ jsxs("h3", { className: "agd-comments__item-title", id: `${id}-h`, children: [
|
|
128
|
+
anchorLabel(thread.anchor),
|
|
129
|
+
" ",
|
|
130
|
+
/* @__PURE__ */ jsx(Status, { value: thread.status })
|
|
131
|
+
] }),
|
|
132
|
+
mayComment && /* @__PURE__ */ jsxs(Button, { size: "sm", loading: busy === `${thread.id}:status`, onClick: () => void act(`${thread.id}:status`, () => resolved ? client.reopen(docId, thread.id) : client.resolve(docId, thread.id), resolved ? "Thread reopened." : "Thread resolved.", "threads"), children: [
|
|
133
|
+
resolved ? "Reopen" : "Resolve",
|
|
134
|
+
/* @__PURE__ */ jsxs("span", { className: "agd-sr-only", children: [
|
|
135
|
+
" thread on ",
|
|
136
|
+
anchorLabel(thread.anchor)
|
|
137
|
+
] })
|
|
138
|
+
] })
|
|
139
|
+
] }),
|
|
140
|
+
/* @__PURE__ */ jsx(Quote, { anchor: thread.anchor }),
|
|
141
|
+
/* @__PURE__ */ jsx(AnchorNote, { state: thread.anchorState }),
|
|
142
|
+
/* @__PURE__ */ jsx("ul", { className: "agd-comments__messages", "aria-label": "Messages", children: thread.messages.map((m) => /* @__PURE__ */ jsx(Body, { message: m }, m.id)) }),
|
|
143
|
+
resolved ? /* @__PURE__ */ jsx("p", { className: "agd-comments__note", children: "Reopen this thread to reply." }) : mayComment ? /* @__PURE__ */ jsxs("form", { className: "agd-comments__reply", onSubmit: submit, children: [
|
|
144
|
+
/* @__PURE__ */ jsx(Textarea, { label: "Reply", rows: 2, value: draft, onChange: (e) => setDraft(e.target.value), maxLength: 1e4 }),
|
|
145
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", variant: "primary", size: "sm", loading: busy === `${thread.id}:reply`, children: "Reply" })
|
|
146
|
+
] }) : null
|
|
147
|
+
] }) });
|
|
148
|
+
}
|
|
149
|
+
function SuggestionCard({ suggestion }) {
|
|
150
|
+
const { client, docId, me, busy, act } = useCtx();
|
|
151
|
+
const decision = mayDecide(me, suggestion);
|
|
152
|
+
const usable = suggestion.anchorState.state === "attached";
|
|
153
|
+
const pending = suggestion.status === "pending";
|
|
154
|
+
return /* @__PURE__ */ jsxs("li", { className: "agd-comments__thread", children: [
|
|
155
|
+
/* @__PURE__ */ jsxs("div", { className: "agd-comments__meta", children: [
|
|
156
|
+
/* @__PURE__ */ jsx(Who, { author: suggestion.author, agentLabel: "Suggested by an agent" }),
|
|
157
|
+
/* @__PURE__ */ jsx("time", { className: "agd-comments__time", dateTime: suggestion.createdAt, children: stamp(suggestion.createdAt) }),
|
|
158
|
+
/* @__PURE__ */ jsx(Status, { value: suggestion.status })
|
|
159
|
+
] }),
|
|
160
|
+
/* @__PURE__ */ jsx(Quote, { anchor: suggestion.anchor }),
|
|
161
|
+
suggestion.summary && /* @__PURE__ */ jsx("p", { className: "agd-comments__body", children: suggestion.summary }),
|
|
162
|
+
/* @__PURE__ */ jsxs("p", { className: "agd-comments__change", children: [
|
|
163
|
+
/* @__PURE__ */ jsx("span", { className: "agd-comments__label", children: "Proposed change" }),
|
|
164
|
+
/* @__PURE__ */ jsx("code", { className: "agd-comments__code", children: changeText(suggestion.change) })
|
|
165
|
+
] }),
|
|
166
|
+
pending && /* @__PURE__ */ jsx(AnchorNote, { state: suggestion.anchorState }),
|
|
167
|
+
suggestion.decidedBy && /* @__PURE__ */ jsxs("p", { className: "agd-comments__note", children: [
|
|
168
|
+
suggestion.status === "accepted" ? "Accepted" : "Rejected",
|
|
169
|
+
" by ",
|
|
170
|
+
suggestion.decidedBy.kind === "human" ? "" : "an agent, ",
|
|
171
|
+
suggestion.decidedBy.name,
|
|
172
|
+
"."
|
|
173
|
+
] }),
|
|
174
|
+
pending && decision.allowed && /* @__PURE__ */ jsxs("div", { className: "agd-comments__actions", children: [
|
|
175
|
+
/* @__PURE__ */ jsxs(Button, { variant: "primary", size: "sm", loading: busy === `${suggestion.id}:accept`, disabled: !usable, onClick: () => void act(`${suggestion.id}:accept`, () => client.accept(docId, suggestion.id), "Suggestion accepted.", "suggestions"), children: [
|
|
176
|
+
"Accept",
|
|
177
|
+
/* @__PURE__ */ jsx("span", { className: "agd-sr-only", children: " suggestion" })
|
|
178
|
+
] }),
|
|
179
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", loading: busy === `${suggestion.id}:reject`, onClick: () => void act(`${suggestion.id}:reject`, () => client.reject(docId, suggestion.id), "Suggestion rejected.", "suggestions"), children: [
|
|
180
|
+
"Reject",
|
|
181
|
+
/* @__PURE__ */ jsx("span", { className: "agd-sr-only", children: " suggestion" })
|
|
182
|
+
] })
|
|
183
|
+
] }),
|
|
184
|
+
pending && decision.allowed && !usable && /* @__PURE__ */ jsx("p", { className: "agd-comments__note", children: suggestion.anchorState.state === "unknown" ? "The panel cannot tell if the anchored text is still there, so Accept is off. You can reject this suggestion." : "The anchored text changed or is gone. You can reject this suggestion, and you cannot accept it." }),
|
|
185
|
+
pending && !decision.allowed && decision.reason && /* @__PURE__ */ jsx("p", { className: "agd-comments__note", children: decision.reason })
|
|
186
|
+
] });
|
|
187
|
+
}
|
|
188
|
+
var CardBoundary = class extends Component {
|
|
189
|
+
state = { bad: false, seen: this.props.item };
|
|
190
|
+
static getDerivedStateFromError() {
|
|
191
|
+
return { bad: true };
|
|
192
|
+
}
|
|
193
|
+
// A read gives new objects. When the item is not the one that failed, the card gets another try.
|
|
194
|
+
static getDerivedStateFromProps(props, state) {
|
|
195
|
+
return props.item === state.seen ? null : { bad: false, seen: props.item };
|
|
196
|
+
}
|
|
197
|
+
render() {
|
|
198
|
+
return this.state.bad ? /* @__PURE__ */ jsx("li", { className: "agd-comments__thread agd-comments__note", children: "This item has bad data, so the panel does not show it." }) : this.props.children;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
function Section({ id, title, count, children }) {
|
|
202
|
+
return /* @__PURE__ */ jsxs("section", { className: "agd-comments__section", "aria-labelledby": id, children: [
|
|
203
|
+
/* @__PURE__ */ jsxs("h2", { className: "agd-comments__heading", id, tabIndex: -1, children: [
|
|
204
|
+
title,
|
|
205
|
+
" ",
|
|
206
|
+
/* @__PURE__ */ jsx("span", { className: "agd-comments__count", children: count })
|
|
207
|
+
] }),
|
|
208
|
+
children
|
|
209
|
+
] });
|
|
210
|
+
}
|
|
211
|
+
function CommentsPanel({ client, docId, title = "Comments", newThread = true, actingFor }) {
|
|
212
|
+
const base = useId();
|
|
213
|
+
const [notice, setNotice] = useState("");
|
|
214
|
+
const [me, setMe] = useState();
|
|
215
|
+
const [threads, setThreads] = useState();
|
|
216
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
217
|
+
const [view, setView] = useState("open");
|
|
218
|
+
const [error, setError] = useState();
|
|
219
|
+
const [busy, setBusy] = useState();
|
|
220
|
+
const [quote, setQuote] = useState("");
|
|
221
|
+
const [body, setBody] = useState("");
|
|
222
|
+
const run = useRef(0);
|
|
223
|
+
const writing = useRef(false);
|
|
224
|
+
const load = useCallback(
|
|
225
|
+
async (gen) => {
|
|
226
|
+
const [m, t, s] = await Promise.all([client.me(), client.listThreads(docId), client.listSuggestions(docId)]);
|
|
227
|
+
if (gen !== run.current) return;
|
|
228
|
+
if (!(typeof m === "object" && m !== null && typeof m.kind === "string" && typeof m.name === "string" && Array.isArray(m.scopes) && m.scopes.every((x) => typeof x === "string"))) {
|
|
229
|
+
throw new Error("the client returned a principal with no kind, name, or scopes");
|
|
230
|
+
}
|
|
231
|
+
setMe(m);
|
|
232
|
+
setThreads(only(t));
|
|
233
|
+
setSuggestions(only(s));
|
|
234
|
+
},
|
|
235
|
+
[client, docId]
|
|
236
|
+
);
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
const gen = ++run.current;
|
|
239
|
+
writing.current = false;
|
|
240
|
+
setMe(void 0);
|
|
241
|
+
setThreads(void 0);
|
|
242
|
+
setSuggestions([]);
|
|
243
|
+
setBusy(void 0);
|
|
244
|
+
setError(void 0);
|
|
245
|
+
setNotice("");
|
|
246
|
+
setQuote("");
|
|
247
|
+
setBody("");
|
|
248
|
+
setView("open");
|
|
249
|
+
load(gen).catch((e) => gen === run.current && setError(failure(e)));
|
|
250
|
+
return () => {
|
|
251
|
+
run.current += 1;
|
|
252
|
+
};
|
|
253
|
+
}, [load]);
|
|
254
|
+
const act = async (key, call, done, focus) => {
|
|
255
|
+
if (writing.current) {
|
|
256
|
+
setNotice("Wait for the current action to finish, then try again.");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
writing.current = true;
|
|
260
|
+
const gen = run.current;
|
|
261
|
+
const current = () => gen === run.current;
|
|
262
|
+
const refocus = () => focus && requestAnimationFrame(() => current() && document.getElementById(`${base}-${focus}`)?.focus());
|
|
263
|
+
setBusy(key);
|
|
264
|
+
setError(void 0);
|
|
265
|
+
setNotice("");
|
|
266
|
+
try {
|
|
267
|
+
await call();
|
|
268
|
+
await load(gen);
|
|
269
|
+
if (current()) {
|
|
270
|
+
if (done) setNotice(done);
|
|
271
|
+
refocus();
|
|
272
|
+
}
|
|
273
|
+
} catch (e) {
|
|
274
|
+
if (current()) setError(failure(e));
|
|
275
|
+
await load(gen).catch(() => void 0);
|
|
276
|
+
if (current()) refocus();
|
|
277
|
+
} finally {
|
|
278
|
+
if (current()) {
|
|
279
|
+
writing.current = false;
|
|
280
|
+
setBusy(void 0);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const start = (event) => {
|
|
285
|
+
event.preventDefault();
|
|
286
|
+
const q = quote.trim();
|
|
287
|
+
const b = body.trim();
|
|
288
|
+
if (!q || !b) return;
|
|
289
|
+
const gen = run.current;
|
|
290
|
+
void act(
|
|
291
|
+
"new",
|
|
292
|
+
async () => {
|
|
293
|
+
await client.addThread(docId, { anchor: { type: "text", quote: q }, body: b });
|
|
294
|
+
if (gen === run.current) {
|
|
295
|
+
setQuote("");
|
|
296
|
+
setBody("");
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
"Thread added."
|
|
300
|
+
);
|
|
301
|
+
};
|
|
302
|
+
const shown = (threads ?? []).filter((t) => view === "all" || t.status === view);
|
|
303
|
+
const pending = suggestions.filter((s) => s.status === "pending");
|
|
304
|
+
const decided = suggestions.filter((s) => s.status !== "pending");
|
|
305
|
+
return /* @__PURE__ */ jsx(Ctx.Provider, { value: { client, docId, me, busy, act, actingFor }, children: /* @__PURE__ */ jsxs("div", { className: "agd-comments", children: [
|
|
306
|
+
/* @__PURE__ */ jsxs("header", { className: "agd-comments__top", children: [
|
|
307
|
+
/* @__PURE__ */ jsx("h1", { className: "agd-comments__title", children: title }),
|
|
308
|
+
me && /* @__PURE__ */ jsxs("p", { className: "agd-comments__me", children: [
|
|
309
|
+
"Signed in as ",
|
|
310
|
+
/* @__PURE__ */ jsx(Who, { author: me, agentLabel: "Signed in as an agent" })
|
|
311
|
+
] })
|
|
312
|
+
] }),
|
|
313
|
+
error && /* @__PURE__ */ jsx("p", { className: "agd-comments__error", role: "alert", children: error }),
|
|
314
|
+
/* @__PURE__ */ jsx("p", { className: "agd-sr-only", role: "status", children: threads === void 0 && !error ? "Loading comments" : notice }),
|
|
315
|
+
newThread && holds(me, "comment") && /* @__PURE__ */ jsxs("form", { className: "agd-comments__new", onSubmit: start, children: [
|
|
316
|
+
/* @__PURE__ */ jsx(Input, { label: "Quote to comment on", value: quote, onChange: (e) => setQuote(e.target.value), hint: "Text that occurs once in the document.", maxLength: 2e3 }),
|
|
317
|
+
/* @__PURE__ */ jsx(Textarea, { label: "Comment", rows: 2, value: body, onChange: (e) => setBody(e.target.value), maxLength: 1e4 }),
|
|
318
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", variant: "primary", loading: busy === "new", children: "Add comment" })
|
|
319
|
+
] }),
|
|
320
|
+
/* @__PURE__ */ jsxs(Section, { id: `${base}-threads`, title: "Threads", count: shown.length, children: [
|
|
321
|
+
/* @__PURE__ */ jsx("div", { className: "agd-comments__views", role: "group", "aria-label": "Show threads", children: VIEWS.map((v) => /* @__PURE__ */ jsx(Button, { size: "sm", variant: view === v.id ? "primary" : "secondary", "aria-pressed": view === v.id, onClick: () => setView(v.id), children: v.label }, v.id)) }),
|
|
322
|
+
threads === void 0 ? null : shown.length === 0 ? /* @__PURE__ */ jsx("p", { className: "agd-comments__empty", children: view === "all" ? "No threads yet." : `No ${view} threads.` }) : /* @__PURE__ */ jsx("ul", { className: "agd-comments__list", children: shown.map((t) => /* @__PURE__ */ jsx(CardBoundary, { item: t, children: /* @__PURE__ */ jsx(ThreadCard, { thread: t }) }, t.id)) })
|
|
323
|
+
] }),
|
|
324
|
+
/* @__PURE__ */ jsx(Section, { id: `${base}-suggestions`, title: "Suggestions", count: pending.length, children: suggestions.length === 0 ? /* @__PURE__ */ jsx("p", { className: "agd-comments__empty", children: "No suggestions." }) : /* @__PURE__ */ jsx("ul", { className: "agd-comments__list", children: [...pending, ...decided].map((s) => /* @__PURE__ */ jsx(CardBoundary, { item: s, children: /* @__PURE__ */ jsx(SuggestionCard, { suggestion: s }) }, s.id)) }) })
|
|
325
|
+
] }) });
|
|
326
|
+
}
|
|
327
|
+
export {
|
|
328
|
+
CommentsPanel,
|
|
329
|
+
createClient,
|
|
330
|
+
holds,
|
|
331
|
+
mayDecide
|
|
332
|
+
};
|
package/dist/worker.d.ts
CHANGED
|
@@ -2,8 +2,9 @@ import { Hono } from 'hono';
|
|
|
2
2
|
import { Auth, Store, PrincipalContext, Region } from 'agentic-identity';
|
|
3
3
|
import { RequirePrincipalOptions } from 'agentic-identity/http';
|
|
4
4
|
import { D1Like } from 'agentic-identity/cloudflare';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { CommentsAccountDocument } from './account.js';
|
|
6
|
+
import { A as Actor, b as Author } from './suggestions-G1seGf5d.js';
|
|
7
|
+
import { C as CommentsService } from './service-ZdZPWrwI.js';
|
|
7
8
|
import 'yjs';
|
|
8
9
|
|
|
9
10
|
interface WorkerEnv {
|
|
@@ -36,6 +37,29 @@ declare class CommentsDocs {
|
|
|
36
37
|
create(id: string, text: string, actor: Actor): Promise<void>;
|
|
37
38
|
read<T>(id: string, actor: Actor, run: (service: CommentsService) => T): Promise<T>;
|
|
38
39
|
write<T>(id: string, actor: Actor, run: (service: CommentsService) => T): Promise<T>;
|
|
40
|
+
/** For the account plug-in: the id of each stored document. */
|
|
41
|
+
accountDocumentIds(): Promise<string[]>;
|
|
42
|
+
private ids;
|
|
43
|
+
/**
|
|
44
|
+
* For the account plug-in: runs `fn` on each stored document, with its `Comments` and `Suggestions`. With `save`,
|
|
45
|
+
* each document runs in one transaction and the state is written back there. A throw leaves it as it was. A document deleted since the list was read is skipped.
|
|
46
|
+
* A damaged document stops the run with CORRUPT.
|
|
47
|
+
*/
|
|
48
|
+
eachAccountDocument(fn: (doc: CommentsAccountDocument) => void, save: boolean, committed?: (id: string) => void): Promise<void>;
|
|
49
|
+
private scan;
|
|
50
|
+
/** The documents this subject created: the creator is kept on the row. For the account plug-in. */
|
|
51
|
+
exportCreated(author: Author): Promise<{
|
|
52
|
+
documents: {
|
|
53
|
+
documentId: string;
|
|
54
|
+
createdAt: unknown;
|
|
55
|
+
}[];
|
|
56
|
+
unread: string[];
|
|
57
|
+
}>;
|
|
58
|
+
/** Give the creator of each document this subject made the tombstone name. Returns how many, and the rows it could not read. A repeat finds none. */
|
|
59
|
+
eraseCreated(author: Author): Promise<{
|
|
60
|
+
count: number;
|
|
61
|
+
unread: string[];
|
|
62
|
+
}>;
|
|
39
63
|
}
|
|
40
64
|
/** The docs, or a function that makes them from the Worker bindings. */
|
|
41
65
|
type DocsSource = CommentsDocs | ((env: WorkerEnv) => Promise<CommentsDocs>);
|
package/dist/worker.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CommentsService,
|
|
3
|
-
TEXT_ROOT
|
|
4
|
-
|
|
3
|
+
TEXT_ROOT,
|
|
4
|
+
localHost
|
|
5
|
+
} from "./chunk-TTNT27PG.js";
|
|
5
6
|
import {
|
|
6
7
|
ACCEPT_SCOPE,
|
|
8
|
+
Comments,
|
|
7
9
|
CommentsError,
|
|
10
|
+
ERASED_NAME,
|
|
8
11
|
MODERATE_SCOPE,
|
|
12
|
+
Suggestions,
|
|
9
13
|
exact,
|
|
10
14
|
invalid,
|
|
15
|
+
readAuthor,
|
|
11
16
|
stamp
|
|
12
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-4G5BDTBA.js";
|
|
13
18
|
|
|
14
19
|
// src/worker.ts
|
|
15
20
|
import { Hono } from "hono";
|
|
@@ -150,6 +155,89 @@ var CommentsDocs = class {
|
|
|
150
155
|
return result;
|
|
151
156
|
});
|
|
152
157
|
}
|
|
158
|
+
/** For the account plug-in: the id of each stored document. */
|
|
159
|
+
async accountDocumentIds() {
|
|
160
|
+
return this.ids();
|
|
161
|
+
}
|
|
162
|
+
async ids() {
|
|
163
|
+
const ids = [];
|
|
164
|
+
let cursor;
|
|
165
|
+
do {
|
|
166
|
+
const page = await this.store.query(DOCS, { limit: 100, ...cursor ? { cursor } : {} });
|
|
167
|
+
for (const { id } of page.items) ids.push(id);
|
|
168
|
+
cursor = page.nextCursor;
|
|
169
|
+
} while (cursor);
|
|
170
|
+
return ids;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* For the account plug-in: runs `fn` on each stored document, with its `Comments` and `Suggestions`. With `save`,
|
|
174
|
+
* each document runs in one transaction and the state is written back there. A throw leaves it as it was. A document deleted since the list was read is skipped.
|
|
175
|
+
* A damaged document stops the run with CORRUPT.
|
|
176
|
+
*/
|
|
177
|
+
async eachAccountDocument(fn, save, committed) {
|
|
178
|
+
const open = (id, row) => {
|
|
179
|
+
const ydoc = this.load(row, id);
|
|
180
|
+
ydoc.getText(TEXT_ROOT);
|
|
181
|
+
const comments = new Comments(ydoc);
|
|
182
|
+
return { ydoc, doc: { id, comments, suggestions: new Suggestions(comments, localHost) } };
|
|
183
|
+
};
|
|
184
|
+
for (const id of await this.ids()) {
|
|
185
|
+
if (!save) {
|
|
186
|
+
const row = await this.store.get(DOCS, id);
|
|
187
|
+
if (row !== null) fn(open(id, row).doc);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const wrote = await this.store.transaction(async (tx) => {
|
|
191
|
+
const row = await tx.get(DOCS, id);
|
|
192
|
+
if (row === null) return false;
|
|
193
|
+
const { ydoc, doc } = open(id, row);
|
|
194
|
+
fn(doc);
|
|
195
|
+
const state = Y.encodeStateAsUpdate(ydoc);
|
|
196
|
+
if (state.length > MAX_STATE) throw new CommentsError("STORE_FULL", `document "${id}" would pass ${MAX_STATE} bytes of state`);
|
|
197
|
+
await tx.put(DOCS, id, { state: toBase64(state), createdBy: row?.createdBy ?? null, createdAt: row?.createdAt ?? null });
|
|
198
|
+
return true;
|
|
199
|
+
});
|
|
200
|
+
if (wrote) committed?.(id);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async scan(author, each, unread) {
|
|
204
|
+
let cursor;
|
|
205
|
+
do {
|
|
206
|
+
const page = await this.store.query(DOCS, { limit: 100, ...cursor ? { cursor } : {} });
|
|
207
|
+
for (const { id, data } of page.items) {
|
|
208
|
+
const raw = data?.createdBy;
|
|
209
|
+
const by = readAuthor(raw);
|
|
210
|
+
if (!by && raw !== null && raw !== void 0) unread?.push(id);
|
|
211
|
+
else if (by && by.kind === author.kind && by.name === author.name) await each(id, data);
|
|
212
|
+
}
|
|
213
|
+
cursor = page.nextCursor;
|
|
214
|
+
} while (cursor);
|
|
215
|
+
}
|
|
216
|
+
/** The documents this subject created: the creator is kept on the row. For the account plug-in. */
|
|
217
|
+
async exportCreated(author) {
|
|
218
|
+
const documents = [];
|
|
219
|
+
const unread = [];
|
|
220
|
+
await this.scan(author, async (id, row) => void documents.push({ documentId: id, createdAt: row.createdAt ?? null }), unread);
|
|
221
|
+
return { documents, unread };
|
|
222
|
+
}
|
|
223
|
+
/** Give the creator of each document this subject made the tombstone name. Returns how many, and the rows it could not read. A repeat finds none. */
|
|
224
|
+
async eraseCreated(author) {
|
|
225
|
+
const ids = [];
|
|
226
|
+
const unread = [];
|
|
227
|
+
await this.scan(author, async (id) => void ids.push(id), unread);
|
|
228
|
+
let count = 0;
|
|
229
|
+
for (const id of ids) {
|
|
230
|
+
const changed = await this.store.transaction(async (tx) => {
|
|
231
|
+
const row = await tx.get(DOCS, id);
|
|
232
|
+
const by = readAuthor(row?.createdBy);
|
|
233
|
+
if (!row || !by || by.kind !== author.kind || by.name !== author.name) return false;
|
|
234
|
+
await tx.put(DOCS, id, { ...row, createdBy: { kind: by.kind, name: ERASED_NAME } });
|
|
235
|
+
return true;
|
|
236
|
+
});
|
|
237
|
+
if (changed) count++;
|
|
238
|
+
}
|
|
239
|
+
return { count, unread };
|
|
240
|
+
}
|
|
153
241
|
};
|
|
154
242
|
var STATUS = {
|
|
155
243
|
INVALID_AUTHOR: 400,
|
package/llms.txt
CHANGED
|
@@ -6,8 +6,10 @@ Keywords: comments,threads,mentions,suggestions,annotations,collaboration,agents
|
|
|
6
6
|
|
|
7
7
|
- SDK: import { Comments, Suggestions, textAnchor } from "agentic-comments". The host calls stamp(); an agent never does.
|
|
8
8
|
- Plain text anchors: import { createAnchor, resolveAnchor, mapRange } from "agentic-comments/anchor". No Yjs. resolveAnchor maps through edits, then falls back to quote + context, else orphaned.
|
|
9
|
+
- Account export and erase: import { commentsAccount } from "agentic-comments/account". exportAccount(subject, { documents, actor }) returns JSON records of one subject (pass worker: docs to cover every stored worker document and its creator). eraseAccount tombstones them and returns counts. The actor is made with stamp() and names the subject.
|
|
9
10
|
- Threads live on a Yjs document and merge across replicas.
|
|
10
11
|
- Lifecycle: comments.editMessage, deleteMessage, deleteThread, exportPerson, erasePerson (and the same on the CLI, MCP, HTTP API, and client). Only the author may change their own messages; comments:moderate may change anyone's.
|
|
12
|
+
- React panel: import { CommentsPanel, createClient, mayDecide } from "agentic-comments/ui" and "agentic-comments/ui.css" (with agentic-design/styles.css). Threads, quotes, replies, resolve and reopen, agent badge, accept and reject. An agent never decides its own suggestion. Peer: react, react-dom.
|
|
11
13
|
- Events: new Notifications(comments, suggestions).subscribe(listener). Ids only, never text.
|
|
12
14
|
- CLI: agentic-comments <command>. MCP: agentic-comments mcp. Both use a local store file.
|
|
13
15
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-comments",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Comment threads and suggestions anchored to any content, from people and agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"comments",
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"types": "./dist/index.d.ts",
|
|
45
45
|
"import": "./dist/index.js"
|
|
46
46
|
},
|
|
47
|
+
"./account": {
|
|
48
|
+
"types": "./dist/account.d.ts",
|
|
49
|
+
"import": "./dist/account.js"
|
|
50
|
+
},
|
|
47
51
|
"./anchor": {
|
|
48
52
|
"types": "./dist/anchoring.d.ts",
|
|
49
53
|
"import": "./dist/anchoring.js"
|
|
@@ -59,36 +63,64 @@
|
|
|
59
63
|
"./worker": {
|
|
60
64
|
"types": "./dist/worker.d.ts",
|
|
61
65
|
"import": "./dist/worker.js"
|
|
62
|
-
}
|
|
66
|
+
},
|
|
67
|
+
"./ui": {
|
|
68
|
+
"types": "./dist/ui.d.ts",
|
|
69
|
+
"import": "./dist/ui.js"
|
|
70
|
+
},
|
|
71
|
+
"./ui.css": "./dist/ui.css"
|
|
63
72
|
},
|
|
64
73
|
"bin": {
|
|
65
74
|
"agentic-comments": "./dist/cli.js",
|
|
66
75
|
"agentic-comments-mcp": "./dist/mcp-bin.js"
|
|
67
76
|
},
|
|
68
77
|
"scripts": {
|
|
69
|
-
"build": "tsup",
|
|
70
|
-
"typecheck": "tsc --noEmit",
|
|
78
|
+
"build": "tsup && node scripts/build-css.mjs",
|
|
79
|
+
"typecheck": "tsc --noEmit && tsc -p ui --noEmit",
|
|
71
80
|
"test": "vitest run",
|
|
72
|
-
"deploy:demo": "npm run build && WRANGLER_SEND_METRICS=false wrangler deploy --config wrangler.demo.jsonc",
|
|
81
|
+
"deploy:demo": "npm run build && npm run build:ui && WRANGLER_SEND_METRICS=false wrangler deploy --config wrangler.demo.jsonc",
|
|
73
82
|
"clean": "rm -rf dist",
|
|
83
|
+
"dev": "npm run build && WRANGLER_SEND_METRICS=false wrangler dev --port 8794",
|
|
84
|
+
"dev:smoke": "npm run build && node scripts/dev-smoke.mjs 8794 /health",
|
|
74
85
|
"prepack": "npm run build",
|
|
75
|
-
"ci:local": "npm run build && npm run typecheck && npm test"
|
|
86
|
+
"ci:local": "npm run build && npm run typecheck && npm test && npm run test:ui",
|
|
87
|
+
"build:ui": "node scripts/build-ui.mjs",
|
|
88
|
+
"test:ui": "npm run build && npm run build:ui && playwright test"
|
|
76
89
|
},
|
|
77
90
|
"dependencies": {
|
|
78
91
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
92
|
+
"agentic-design": "^0.5.0",
|
|
79
93
|
"agentic-identity": "^0.8.0",
|
|
80
94
|
"hono": "^4.13.14",
|
|
81
95
|
"zod": "^4.6.7"
|
|
82
96
|
},
|
|
83
97
|
"peerDependencies": {
|
|
98
|
+
"react": "^19.0.0",
|
|
99
|
+
"react-dom": "^19.0.0",
|
|
84
100
|
"yjs": "^13.6.0"
|
|
85
101
|
},
|
|
86
102
|
"devDependencies": {
|
|
103
|
+
"@axe-core/playwright": "^4.14.0",
|
|
104
|
+
"@playwright/test": "^1.64.1",
|
|
87
105
|
"@types/node": "^22.0.0",
|
|
106
|
+
"@types/react": "^19.3.0",
|
|
107
|
+
"@types/react-dom": "^19.3.0",
|
|
108
|
+
"esbuild": "^0.28.2",
|
|
88
109
|
"tsup": "^8.0.0",
|
|
89
110
|
"typescript": "^5.6.0",
|
|
90
111
|
"vitest": "^3.0.0",
|
|
91
112
|
"wrangler": "^4.155.0",
|
|
92
113
|
"yjs": "^13.6.0"
|
|
93
|
-
}
|
|
114
|
+
},
|
|
115
|
+
"peerDependenciesMeta": {
|
|
116
|
+
"react": {
|
|
117
|
+
"optional": true
|
|
118
|
+
},
|
|
119
|
+
"react-dom": {
|
|
120
|
+
"optional": true
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"sideEffects": [
|
|
124
|
+
"**/*.css"
|
|
125
|
+
]
|
|
94
126
|
}
|