clay-server 4.0.0-beta.1 → 4.0.0-beta.11
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/lib/background-task-timing.js +105 -0
- package/lib/config.js +25 -1
- package/lib/daemon.js +22 -0
- package/lib/git-cli.js +94 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/project-connection.js +17 -0
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-mcp-server.js +314 -0
- package/lib/project-logs-query.js +167 -0
- package/lib/project-logs-root.js +67 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +357 -0
- package/lib/project-logs-snapshot.js +202 -0
- package/lib/project-logs-store.js +498 -0
- package/lib/project-logs-versioning.js +128 -0
- package/lib/project-logs.js +348 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-pair-lifecycle.js +461 -0
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +124 -111
- package/lib/project-sessions.js +46 -6
- package/lib/project-user-message.js +41 -4
- package/lib/project-vendor-login.js +414 -0
- package/lib/project-worker-permission.js +429 -0
- package/lib/project-worker-proposal.js +16 -1
- package/lib/project.js +164 -4
- package/lib/public/app.js +49 -20
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/input.css +79 -25
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +52 -0
- package/lib/public/css/project-logs.css +234 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/sticky-notes.css +231 -226
- package/lib/public/index.html +12 -0
- package/lib/public/modules/app-connection.js +90 -4
- package/lib/public/modules/app-messages.js +54 -19
- package/lib/public/modules/app-notifications.js +15 -124
- package/lib/public/modules/app-panels.js +43 -2
- package/lib/public/modules/app-projects.js +2 -2
- package/lib/public/modules/background-tasks-ui.js +139 -16
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-sidebar.js +7 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/pane-bridge.js +9 -0
- package/lib/public/modules/project-logs-ambient.js +238 -0
- package/lib/public/modules/project-logs-render.js +359 -0
- package/lib/public/modules/project-logs.js +329 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/sidebar-mobile.js +5 -9
- package/lib/public/modules/sidebar-sessions.js +25 -1
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-view.js +7 -0
- package/lib/public/modules/sticky-notes-browser.js +324 -0
- package/lib/public/modules/sticky-notes-card.js +358 -0
- package/lib/public/modules/sticky-notes-editor.js +288 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +168 -946
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tui-attention.js +10 -0
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/vendor-login.js +287 -0
- package/lib/public/modules/worker-pane-lock.js +174 -0
- package/lib/public/style.css +2 -0
- package/lib/sdk-bridge.js +77 -10
- package/lib/sdk-message-processor.js +24 -7
- package/lib/server.js +61 -6
- package/lib/session-driver-eligibility.js +183 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +233 -0
- package/lib/session-pair-mcp-server.js +45 -5
- package/lib/session-pair-prompts.js +60 -0
- package/lib/sessions.js +4 -1
- package/lib/tools-registry.js +11 -1
- package/lib/update-snooze.js +437 -0
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/ws-schema.js +28 -3
- package/lib/yoke/adapters/claude.js +69 -4
- package/lib/yoke/adapters/codex.js +42 -5
- package/lib/yoke/codex-app-server.js +20 -2
- package/lib/yoke/codex-background-tasks.js +8 -2
- package/package.json +3 -3
package/lib/notes.js
CHANGED
|
@@ -3,6 +3,7 @@ var path = require("path");
|
|
|
3
3
|
var crypto = require("crypto");
|
|
4
4
|
var config = require("./config");
|
|
5
5
|
var utils = require("./utils");
|
|
6
|
+
var lifecycle = require("./notes-lifecycle");
|
|
6
7
|
|
|
7
8
|
function createNotesManager(opts) {
|
|
8
9
|
var cwd = opts.cwd;
|
|
@@ -19,11 +20,14 @@ function createNotesManager(opts) {
|
|
|
19
20
|
return "n_" + Date.now() + "_" + crypto.randomBytes(3).toString("hex");
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
// Legacy files are normalized on load, never on read. Nothing is rewritten
|
|
24
|
+
// just because a note was looked at; the projected fields reach disk the next
|
|
25
|
+
// time some other write happens.
|
|
22
26
|
function loadFromDisk() {
|
|
23
27
|
try {
|
|
24
28
|
var data = fs.readFileSync(notesFile, "utf8");
|
|
25
29
|
var parsed = JSON.parse(data);
|
|
26
|
-
return parsed.notes || [];
|
|
30
|
+
return lifecycle.normalizeAll(parsed.notes || []).notes;
|
|
27
31
|
} catch (e) {
|
|
28
32
|
return [];
|
|
29
33
|
}
|
|
@@ -56,6 +60,10 @@ function createNotesManager(opts) {
|
|
|
56
60
|
color: data.color || "purple",
|
|
57
61
|
opacity: typeof data.opacity === "number" ? data.opacity : 0.64,
|
|
58
62
|
minimized: false,
|
|
63
|
+
// A new note is always an open attention item.
|
|
64
|
+
state: lifecycle.STATE_OPEN,
|
|
65
|
+
closedAt: null,
|
|
66
|
+
closedBy: null,
|
|
59
67
|
zIndex: notes.length + 1,
|
|
60
68
|
createdAt: now,
|
|
61
69
|
updatedAt: now,
|
|
@@ -74,13 +82,20 @@ function createNotesManager(opts) {
|
|
|
74
82
|
function update(id, changes) {
|
|
75
83
|
for (var i = 0; i < notes.length; i++) {
|
|
76
84
|
if (notes[i].id === id) {
|
|
77
|
-
var allowed = ["text", "x", "y", "w", "h", "color", "minimized", "
|
|
85
|
+
var allowed = ["text", "x", "y", "w", "h", "color", "minimized", "zIndex", "opacity"];
|
|
78
86
|
for (var j = 0; j < allowed.length; j++) {
|
|
79
87
|
var key = allowed[j];
|
|
80
88
|
if (changes[key] !== undefined) {
|
|
81
89
|
notes[i][key] = changes[key];
|
|
82
90
|
}
|
|
83
91
|
}
|
|
92
|
+
// `hidden` is the legacy spelling of the lifecycle. Route it through the
|
|
93
|
+
// transitions so state, hidden, and the close provenance can never
|
|
94
|
+
// disagree, and so an old client cannot half-apply a close.
|
|
95
|
+
if (changes.hidden !== undefined) {
|
|
96
|
+
if (changes.hidden === true) lifecycle.applyClose(notes[i], changes.actor || null, Date.now());
|
|
97
|
+
else lifecycle.applyReopen(notes[i]);
|
|
98
|
+
}
|
|
84
99
|
notes[i].updatedAt = Date.now();
|
|
85
100
|
saveToDisk();
|
|
86
101
|
return notes[i];
|
|
@@ -89,6 +104,44 @@ function createNotesManager(opts) {
|
|
|
89
104
|
return null;
|
|
90
105
|
}
|
|
91
106
|
|
|
107
|
+
// Close a note. This is what completion means: the record stays and only its
|
|
108
|
+
// place on the active canvas changes. Idempotent.
|
|
109
|
+
function close(id, actor) {
|
|
110
|
+
for (var i = 0; i < notes.length; i++) {
|
|
111
|
+
if (notes[i].id !== id) continue;
|
|
112
|
+
if (lifecycle.applyClose(notes[i], actor || null, Date.now())) {
|
|
113
|
+
notes[i].updatedAt = Date.now();
|
|
114
|
+
saveToDisk();
|
|
115
|
+
}
|
|
116
|
+
return notes[i];
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Reopen a closed note. Idempotent.
|
|
122
|
+
function reopen(id) {
|
|
123
|
+
for (var i = 0; i < notes.length; i++) {
|
|
124
|
+
if (notes[i].id !== id) continue;
|
|
125
|
+
if (lifecycle.applyReopen(notes[i])) {
|
|
126
|
+
notes[i].updatedAt = Date.now();
|
|
127
|
+
saveToDisk();
|
|
128
|
+
}
|
|
129
|
+
return notes[i];
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function openList() {
|
|
135
|
+
return lifecycle.openNotes(notes);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function closedList() {
|
|
139
|
+
return lifecycle.closedNotes(notes);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Permanent deletion. Deliberately not reachable from any WebSocket message,
|
|
143
|
+
// MCP tool, or UI control: completion closes, it never erases. Retained only
|
|
144
|
+
// for maintenance callers that must truly discard a record.
|
|
92
145
|
function remove(id) {
|
|
93
146
|
for (var i = 0; i < notes.length; i++) {
|
|
94
147
|
if (notes[i].id === id) {
|
|
@@ -123,7 +176,7 @@ function createNotesManager(opts) {
|
|
|
123
176
|
function getActiveNotesText() {
|
|
124
177
|
var active = [];
|
|
125
178
|
for (var i = 0; i < notes.length; i++) {
|
|
126
|
-
if (
|
|
179
|
+
if (lifecycle.isOpen(notes[i]) && notes[i].text) active.push(notes[i]);
|
|
127
180
|
}
|
|
128
181
|
if (active.length === 0) return "";
|
|
129
182
|
var lines = [];
|
|
@@ -137,8 +190,12 @@ function createNotesManager(opts) {
|
|
|
137
190
|
|
|
138
191
|
return {
|
|
139
192
|
list: list,
|
|
193
|
+
openList: openList,
|
|
194
|
+
closedList: closedList,
|
|
140
195
|
create: create,
|
|
141
196
|
update: update,
|
|
197
|
+
close: close,
|
|
198
|
+
reopen: reopen,
|
|
142
199
|
remove: remove,
|
|
143
200
|
bringToFront: bringToFront,
|
|
144
201
|
getActiveNotesText: getActiveNotesText,
|
|
@@ -78,6 +78,7 @@ function attachConnection(ctx) {
|
|
|
78
78
|
var _mcp = ctx._mcp;
|
|
79
79
|
var _notifications = ctx._notifications;
|
|
80
80
|
var _splitGroups = ctx._splitGroups;
|
|
81
|
+
var _vendorLogin = ctx._vendorLogin;
|
|
81
82
|
var hydrateImageRefs = ctx.hydrateImageRefs;
|
|
82
83
|
var resolveSessionForView = ctx.resolveSessionForView;
|
|
83
84
|
var broadcastClientCount = ctx.broadcastClientCount;
|
|
@@ -191,6 +192,9 @@ function attachConnection(ctx) {
|
|
|
191
192
|
sendTo(ws, { type: "last_vendor", vendor: sm.lastVendor || "" });
|
|
192
193
|
sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
|
|
193
194
|
sendTo(ws, { type: "term_list", terminals: tm.list() });
|
|
195
|
+
// Any login flow already running in this project, so a reconnecting client
|
|
196
|
+
// re-attaches to the existing login terminal instead of starting another.
|
|
197
|
+
if (_vendorLogin) _vendorLogin.sendStateTo(ws);
|
|
194
198
|
// Context sources sent after session is resolved (per-session storage)
|
|
195
199
|
// Send email accounts list for context sources picker
|
|
196
200
|
var emailUserId = (wsUser && wsUser.id) || "default";
|
|
@@ -346,9 +350,22 @@ function attachConnection(ctx) {
|
|
|
346
350
|
broadcastPresence();
|
|
347
351
|
}
|
|
348
352
|
|
|
353
|
+
// Keep-alive for the project socket, mirroring the `/ws` global handler.
|
|
354
|
+
//
|
|
355
|
+
// A reverse proxy resets its upstream read timeout on data flowing back from
|
|
356
|
+
// the server, so a client ping is only useful if the server answers it. This
|
|
357
|
+
// reads nothing, mutates nothing, and reveals nothing about the connection:
|
|
358
|
+
// it is a fixed reply on a fixed message type.
|
|
359
|
+
function handleKeepaliveMessage(ws, msg) {
|
|
360
|
+
if (!msg || typeof msg !== "object" || msg.type !== "ping") return false;
|
|
361
|
+
sendTo(ws, { type: "pong" });
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
|
|
349
365
|
return {
|
|
350
366
|
handleConnection: handleConnection,
|
|
351
367
|
handleDisconnection: handleDisconnection,
|
|
368
|
+
handleKeepaliveMessage: handleKeepaliveMessage,
|
|
352
369
|
};
|
|
353
370
|
}
|
|
354
371
|
|
package/lib/project-http.js
CHANGED
|
@@ -730,7 +730,7 @@ function attachHTTP(ctx) {
|
|
|
730
730
|
}
|
|
731
731
|
|
|
732
732
|
// Git panel status and actions
|
|
733
|
-
var isGitPanelRequest = urlPath === "/api/git/status" ||
|
|
733
|
+
var isGitPanelRequest = urlPath === "/api/git/status" || urlPath === "/api/git/summary" ||
|
|
734
734
|
urlPath === "/api/git/action" || urlPath.indexOf("/api/git/file-diff?") === 0 ||
|
|
735
735
|
urlPath.indexOf("/api/git/session-diff?") === 0;
|
|
736
736
|
if (isGitPanelRequest && usersModule.isMultiUser()) {
|
|
@@ -768,6 +768,20 @@ function attachHTTP(ctx) {
|
|
|
768
768
|
return true;
|
|
769
769
|
}
|
|
770
770
|
|
|
771
|
+
// Compact repository placard data for the always-visible sidebar block.
|
|
772
|
+
// Same permission gate as the panel, but no file payload and no path.
|
|
773
|
+
if (req.method === "GET" && urlPath === "/api/git/summary") {
|
|
774
|
+
try {
|
|
775
|
+
var gitSummary = gitCli.getSummary(cwd, getOsUserInfoForReq(req));
|
|
776
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
777
|
+
res.end(JSON.stringify(gitSummary));
|
|
778
|
+
} catch (e) {
|
|
779
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
780
|
+
res.end(JSON.stringify({ error: e.message || "Unable to read Git summary" }));
|
|
781
|
+
}
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
|
|
771
785
|
if (req.method === "GET" && urlPath.indexOf("/api/git/file-diff?") === 0) {
|
|
772
786
|
var gitDiffParams = new URLSearchParams(urlPath.slice(urlPath.indexOf("?")));
|
|
773
787
|
var gitDiffPath = gitDiffParams.get("path");
|
package/lib/project-knowledge.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
var fs = require("fs");
|
|
2
2
|
var path = require("path");
|
|
3
|
+
var mateSync = require("./mate-knowledge-sync");
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Attach knowledge file management to a project context.
|
|
@@ -75,6 +76,23 @@ function attachKnowledge(ctx) {
|
|
|
75
76
|
return files;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
// Mirror a Mate-owned knowledge file into the Clay-wide Knowledge backend
|
|
80
|
+
// after the legacy write has already succeeded. The legacy file stays
|
|
81
|
+
// authoritative; this only keeps the new scope from going stale.
|
|
82
|
+
function mirrorMateSource(ws, fileName) {
|
|
83
|
+
if (!isMate) return;
|
|
84
|
+
var user = (ws && ws._clayUser) || null;
|
|
85
|
+
mateSync.syncQuietly({
|
|
86
|
+
mateDir: cwd,
|
|
87
|
+
fileName: fileName,
|
|
88
|
+
actor: {
|
|
89
|
+
type: user ? "user" : "system",
|
|
90
|
+
userId: user ? user.id : null,
|
|
91
|
+
displayName: user ? (user.displayName || user.username) : null,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
function handleKnowledgeMessage(ws, msg) {
|
|
79
97
|
if (msg.type === "knowledge_list") {
|
|
80
98
|
sendTo(ws, { type: "knowledge_list", files: listKnowledgeFiles() });
|
|
@@ -111,6 +129,7 @@ function attachKnowledge(ctx) {
|
|
|
111
129
|
var knowledgeDir = path.join(cwd, "knowledge");
|
|
112
130
|
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
113
131
|
fs.writeFileSync(path.join(knowledgeDir, safeName), msg.content);
|
|
132
|
+
mirrorMateSource(ws, safeName);
|
|
114
133
|
sendTo(ws, { type: "knowledge_saved", name: safeName });
|
|
115
134
|
sendTo(ws, { type: "knowledge_list", files: listKnowledgeFilesBasic() });
|
|
116
135
|
return true;
|
|
@@ -121,6 +140,9 @@ function attachKnowledge(ctx) {
|
|
|
121
140
|
var safeName = path.basename(msg.name);
|
|
122
141
|
var filePath = path.join(cwd, "knowledge", safeName);
|
|
123
142
|
try { fs.unlinkSync(filePath); } catch (e) {}
|
|
143
|
+
// Reconciling after the unlink appends a tombstone, so a deleted file
|
|
144
|
+
// does not stay live in the new backend.
|
|
145
|
+
mirrorMateSource(ws, safeName);
|
|
124
146
|
sendTo(ws, { type: "knowledge_deleted", name: safeName });
|
|
125
147
|
sendTo(ws, { type: "knowledge_list", files: listKnowledgeFilesBasic() });
|
|
126
148
|
return true;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Comment and review projection for Project Logs.
|
|
2
|
+
//
|
|
3
|
+
// Split from project-logs-store.js so the store stays inside the module size
|
|
4
|
+
// limit.
|
|
5
|
+
//
|
|
6
|
+
// A user comment is a revision proposal, never an automatic canonical
|
|
7
|
+
// mutation. It starts `pending` and stays that way until the project's own
|
|
8
|
+
// Driver session reviews it. Reviews are append-only records that reference a
|
|
9
|
+
// comment id; nothing is ever edited in place, and a comment written before
|
|
10
|
+
// reviews existed simply projects as `pending`.
|
|
11
|
+
|
|
12
|
+
var STATUS_PENDING = "pending";
|
|
13
|
+
var STATUS_CLARIFICATION = "clarification-needed";
|
|
14
|
+
var STATUS_INCORPORATED = "incorporated";
|
|
15
|
+
var STATUS_DECLINED = "declined";
|
|
16
|
+
|
|
17
|
+
var STATUSES = [STATUS_PENDING, STATUS_CLARIFICATION, STATUS_INCORPORATED, STATUS_DECLINED];
|
|
18
|
+
|
|
19
|
+
// Actions the Driver may take. `incorporate` is the only one that produces a
|
|
20
|
+
// canonical revision.
|
|
21
|
+
var ACTIONS = ["incorporate", "clarify", "decline"];
|
|
22
|
+
|
|
23
|
+
var ACTION_STATUS = {
|
|
24
|
+
incorporate: STATUS_INCORPORATED,
|
|
25
|
+
clarify: STATUS_CLARIFICATION,
|
|
26
|
+
decline: STATUS_DECLINED,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Waiting on the Driver. Only an unreviewed comment qualifies.
|
|
30
|
+
//
|
|
31
|
+
// A clarification is deliberately NOT Driver work: the Driver has already
|
|
32
|
+
// answered, and the ball is with the user. Because a comment is settled once
|
|
33
|
+
// and never re-reviewed, counting a clarification as pending would leave it in
|
|
34
|
+
// the queue forever with no way out. The user's reply is a new comment, which
|
|
35
|
+
// is pending in its own right and can be incorporated or declined.
|
|
36
|
+
function isPendingReview(status) {
|
|
37
|
+
return status === STATUS_PENDING;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Answered by the Driver and now waiting on the person who commented. Shown
|
|
41
|
+
// permanently on the original comment; never counted as Driver work.
|
|
42
|
+
function isAwaitingUser(status) {
|
|
43
|
+
return status === STATUS_CLARIFICATION;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isAction(value) {
|
|
47
|
+
return ACTIONS.indexOf(value) !== -1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Project the comment thread of one entry, folding review records onto the
|
|
51
|
+
// comments they resolve. `revisionForRecordId` maps an incorporating review
|
|
52
|
+
// record id to the canonical revision number it produced, so an incorporated
|
|
53
|
+
// comment can point at its result.
|
|
54
|
+
function comments(chain, options) {
|
|
55
|
+
var settings = options || {};
|
|
56
|
+
var maxComments = settings.maxComments || 200;
|
|
57
|
+
var revisionForRecordId = settings.revisionForRecordId || {};
|
|
58
|
+
var byId = {};
|
|
59
|
+
var order = [];
|
|
60
|
+
|
|
61
|
+
var i;
|
|
62
|
+
for (i = 0; i < chain.length; i++) {
|
|
63
|
+
var record = chain[i];
|
|
64
|
+
if (record.op !== "comment") continue;
|
|
65
|
+
if (order.length >= maxComments) break;
|
|
66
|
+
var comment = {
|
|
67
|
+
id: record.id,
|
|
68
|
+
body: record.body || "",
|
|
69
|
+
at: record.at || 0,
|
|
70
|
+
author: record.author || null,
|
|
71
|
+
// A comment with no review yet is pending, which is also how every
|
|
72
|
+
// comment written before reviews existed projects.
|
|
73
|
+
status: STATUS_PENDING,
|
|
74
|
+
review: null,
|
|
75
|
+
};
|
|
76
|
+
byId[record.id] = comment;
|
|
77
|
+
order.push(comment);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (i = 0; i < chain.length; i++) {
|
|
81
|
+
var review = chain[i];
|
|
82
|
+
if (review.op !== "review") continue;
|
|
83
|
+
var target = byId[review.commentId];
|
|
84
|
+
if (!target) continue;
|
|
85
|
+
if (!isAction(review.action)) continue;
|
|
86
|
+
// Append-only: the first review of a comment settles it. A later review of
|
|
87
|
+
// the same comment is ignored rather than silently overwriting the record
|
|
88
|
+
// of what was decided.
|
|
89
|
+
if (target.review) continue;
|
|
90
|
+
target.status = ACTION_STATUS[review.action];
|
|
91
|
+
target.review = {
|
|
92
|
+
action: review.action,
|
|
93
|
+
response: typeof review.response === "string" ? review.response : "",
|
|
94
|
+
at: review.at || 0,
|
|
95
|
+
reviewer: review.author || null,
|
|
96
|
+
revision: review.action === "incorporate"
|
|
97
|
+
? (revisionForRecordId[review.id] || null)
|
|
98
|
+
: null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return order;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// How many comments are still waiting on the Driver. Used for the ledger row
|
|
106
|
+
// count and for the system-prompt signal, neither of which carries any body.
|
|
107
|
+
function pendingReviewCount(list) {
|
|
108
|
+
var count = 0;
|
|
109
|
+
for (var i = 0; i < (list || []).length; i++) {
|
|
110
|
+
if (isPendingReview(list[i].status)) count++;
|
|
111
|
+
}
|
|
112
|
+
return count;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function pendingReviewComments(list) {
|
|
116
|
+
var out = [];
|
|
117
|
+
for (var i = 0; i < (list || []).length; i++) {
|
|
118
|
+
if (isPendingReview(list[i].status)) out.push(list[i]);
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
STATUS_PENDING: STATUS_PENDING,
|
|
125
|
+
STATUS_CLARIFICATION: STATUS_CLARIFICATION,
|
|
126
|
+
STATUS_INCORPORATED: STATUS_INCORPORATED,
|
|
127
|
+
STATUS_DECLINED: STATUS_DECLINED,
|
|
128
|
+
STATUSES: STATUSES,
|
|
129
|
+
ACTIONS: ACTIONS,
|
|
130
|
+
ACTION_STATUS: ACTION_STATUS,
|
|
131
|
+
isPendingReview: isPendingReview,
|
|
132
|
+
isAwaitingUser: isAwaitingUser,
|
|
133
|
+
isAction: isAction,
|
|
134
|
+
comments: comments,
|
|
135
|
+
pendingReviewCount: pendingReviewCount,
|
|
136
|
+
pendingReviewComments: pendingReviewComments,
|
|
137
|
+
};
|