@neta-art/cohub 8.11.0 → 8.12.1
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/dist/board/animation.js +14 -2
- package/dist/board/core/file-preview.d.ts +2 -2
- package/dist/board/core/file-preview.js +2 -2
- package/dist/board/index.d.ts +2 -1
- package/dist/board/index.js +2 -1
- package/dist/board/mutation.js +2 -1
- package/dist/board/render/board-background.d.ts +16 -0
- package/dist/board/render/{themes/clean-theme.js → board-background.js} +28 -35
- package/dist/board/render/index.d.ts +3 -3
- package/dist/board/render/index.js +2 -2
- package/dist/board/render/renderers/file-card-renderer.js +3 -32
- package/dist/board/replay.d.ts +52 -0
- package/dist/board/replay.js +261 -0
- package/dist/board/semantic-document.d.ts +9 -2
- package/dist/board/semantic-document.js +1 -3
- package/dist/chunks/environment.d.ts +296 -9
- package/dist/chunks/environment.js +1 -0
- package/dist/chunks/http.d.ts +54 -4
- package/dist/chunks/http.js +67 -12
- package/dist/chunks/websocket.d.ts +1 -1
- package/dist/http.d.ts +3 -3
- package/dist/index.d.ts +11 -4
- package/dist/index.js +201 -51
- package/dist/protocol/dist/board-animation.d.ts +1 -0
- package/dist/protocol/dist/board-animation.js +34 -0
- package/dist/protocol/dist/board-authoring.d.ts +2 -1
- package/dist/protocol/dist/board-capability-registry.js +53 -3
- package/dist/protocol/dist/board-codec.js +149 -2
- package/dist/protocol/dist/board-constants.js +29 -7
- package/dist/protocol/dist/board-document.d.ts +30 -16
- package/dist/protocol/dist/board-document.js +15 -12
- package/dist/protocol/dist/board-effect.d.ts +4 -2
- package/dist/protocol/dist/board-effect.js +3 -2
- package/dist/protocol/dist/board.d.ts +148 -3
- package/dist/protocol/dist/board.js +7 -0
- package/dist/protocol/dist/index.d.ts +4 -2
- package/dist/protocol/dist/provenance.d.ts +21 -0
- package/dist/types.d.ts +1 -1
- package/docs/app-runtime-guide.md +7 -4
- package/package.json +1 -1
- package/dist/board/render/themes/board-theme-registry.d.ts +0 -22
- package/dist/board/render/themes/board-theme-registry.js +0 -13
- package/dist/board/render/themes/clean-theme.d.ts +0 -5
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { BOARD_DOCUMENT_KIND } from "../protocol/dist/board.js";
|
|
2
|
+
import { BoardAppearanceSchema, parseBoardDocument } from "../protocol/dist/board-document.js";
|
|
3
|
+
import { boardNodeToAuthoringItem } from "../protocol/dist/board-codec.js";
|
|
4
|
+
import { DEFAULT_BOARD_APPEARANCE, boardAuthoringItemToDocumentItem } from "./semantic-document.js";
|
|
5
|
+
//#region src/board/replay.ts
|
|
6
|
+
function boardReplayActorKind(source) {
|
|
7
|
+
if (!source) return "human";
|
|
8
|
+
if (source.toolCallId) return "agent";
|
|
9
|
+
return source.via === "cli" ? "cli" : "human";
|
|
10
|
+
}
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
/** Apply one stored operation (forward payload or inverse) to the mutable state. */
|
|
15
|
+
function applyOperation(state, type, payload) {
|
|
16
|
+
switch (type) {
|
|
17
|
+
case "board.patch": {
|
|
18
|
+
const patch = isRecord(payload.patch) ? payload.patch : {};
|
|
19
|
+
if (isRecord(patch.metadata)) state.metadata = patch.metadata;
|
|
20
|
+
if (isRecord(patch.metadataPatch)) state.metadata = {
|
|
21
|
+
...state.metadata,
|
|
22
|
+
...patch.metadataPatch
|
|
23
|
+
};
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
case "node.create": {
|
|
27
|
+
const node = payload.node;
|
|
28
|
+
if (node) state.nodes.set(node.nodeId, node);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
case "node.patch": {
|
|
32
|
+
const nodeId = payload.nodeId;
|
|
33
|
+
const current = state.nodes.get(nodeId);
|
|
34
|
+
if (current && isRecord(payload.patch)) state.nodes.set(nodeId, {
|
|
35
|
+
...current,
|
|
36
|
+
...payload.patch,
|
|
37
|
+
nodeId
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
case "node.delete":
|
|
42
|
+
state.nodes.delete(payload.nodeId);
|
|
43
|
+
return;
|
|
44
|
+
case "connection.create": {
|
|
45
|
+
const connection = payload.connection;
|
|
46
|
+
if (connection) state.connections.set(connection.id, connection);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
case "connection.patch": {
|
|
50
|
+
const connectionId = payload.connectionId;
|
|
51
|
+
const current = state.connections.get(connectionId);
|
|
52
|
+
if (current && isRecord(payload.patch)) state.connections.set(connectionId, {
|
|
53
|
+
...current,
|
|
54
|
+
...payload.patch,
|
|
55
|
+
id: connectionId
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
case "connection.delete":
|
|
60
|
+
state.connections.delete(payload.connectionId);
|
|
61
|
+
return;
|
|
62
|
+
default: return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Inverses are stored either as a full operation (`{ type, payload }`) or, for
|
|
67
|
+
* `board.patch`, as a bare `{ patch }`. Normalise both to `(type, payload)`.
|
|
68
|
+
*/
|
|
69
|
+
function inverseOf(operation) {
|
|
70
|
+
const inverse = operation.inverse;
|
|
71
|
+
if (!inverse) return null;
|
|
72
|
+
if (typeof inverse.type === "string" && isRecord(inverse.payload)) return {
|
|
73
|
+
type: inverse.type,
|
|
74
|
+
payload: inverse.payload
|
|
75
|
+
};
|
|
76
|
+
if (operation.type === "board.patch" && isRecord(inverse.patch)) return {
|
|
77
|
+
type: "board.patch",
|
|
78
|
+
payload: { patch: inverse.patch }
|
|
79
|
+
};
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
function operationItemIds(operation) {
|
|
83
|
+
const payload = operation.payload;
|
|
84
|
+
switch (operation.type) {
|
|
85
|
+
case "node.create": return isRecord(payload.node) && typeof payload.node.nodeId === "string" ? [payload.node.nodeId] : [];
|
|
86
|
+
case "node.patch":
|
|
87
|
+
case "node.delete": return typeof payload.nodeId === "string" ? [payload.nodeId] : [];
|
|
88
|
+
default: return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const VISUAL_OPERATION_TYPES = /* @__PURE__ */ new Set([
|
|
92
|
+
"board.patch",
|
|
93
|
+
"node.create",
|
|
94
|
+
"node.patch",
|
|
95
|
+
"node.delete",
|
|
96
|
+
"connection.create",
|
|
97
|
+
"connection.patch",
|
|
98
|
+
"connection.delete"
|
|
99
|
+
]);
|
|
100
|
+
function entryOf(transaction) {
|
|
101
|
+
return {
|
|
102
|
+
version: transaction.version,
|
|
103
|
+
actorId: transaction.actorId,
|
|
104
|
+
kind: boardReplayActorKind(transaction.source),
|
|
105
|
+
at: Date.parse(transaction.createdAt),
|
|
106
|
+
visual: transaction.operations.some((operation) => VISUAL_OPERATION_TYPES.has(operation.type))
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function appearanceOf(metadata) {
|
|
110
|
+
const parsed = BoardAppearanceSchema.safeParse(metadata.appearance);
|
|
111
|
+
return parsed.success ? parsed.data : DEFAULT_BOARD_APPEARANCE;
|
|
112
|
+
}
|
|
113
|
+
function project(state) {
|
|
114
|
+
const nodes = [...state.nodes.values()].sort((a, b) => (a.orderKey ?? "").localeCompare(b.orderKey ?? ""));
|
|
115
|
+
return parseBoardDocument({
|
|
116
|
+
kind: BOARD_DOCUMENT_KIND,
|
|
117
|
+
version: 1,
|
|
118
|
+
appearance: appearanceOf(state.metadata),
|
|
119
|
+
viewport: {
|
|
120
|
+
x: 0,
|
|
121
|
+
y: 0,
|
|
122
|
+
zoom: 1
|
|
123
|
+
},
|
|
124
|
+
items: nodes.map((node) => boardAuthoringItemToDocumentItem(boardNodeToAuthoringItem(node))),
|
|
125
|
+
connections: [...state.connections.values()]
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function stateFromSnapshot(snapshot) {
|
|
129
|
+
return {
|
|
130
|
+
nodes: new Map(snapshot.nodes.map(({ boardId: _boardId, version: _version, createdAt: _c, updatedAt: _u, ...node }) => [node.nodeId, node])),
|
|
131
|
+
connections: new Map(snapshot.connections.map(({ boardId: _boardId, revision: _revision, createdAt: _c, updatedAt: _u, ...connection }) => [connection.id, connection])),
|
|
132
|
+
metadata: snapshot.board.metadata
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Create a player positioned at the live version described by the first page.
|
|
137
|
+
*
|
|
138
|
+
* `transactions` are newest-first as served; the player keeps them oldest-first.
|
|
139
|
+
* Older pages can be added with `prepend`, and newer transactions that arrive
|
|
140
|
+
* live can be added with `append`, so the timeline grows in both directions
|
|
141
|
+
* without a reset.
|
|
142
|
+
*/
|
|
143
|
+
function createBoardReplayPlayer(page) {
|
|
144
|
+
if (!page.snapshot) throw new Error("Board replay needs the first transactions page (with snapshot).");
|
|
145
|
+
const liveVersion = page.board.version;
|
|
146
|
+
const newest = page.transactions.reduce((max, transaction) => Math.max(max, transaction.version), 0);
|
|
147
|
+
if (page.transactions.length > 0 && newest !== liveVersion) throw new Error(`Board replay page is inconsistent: board is at v${liveVersion}, newest transaction is v${newest}.`);
|
|
148
|
+
let transactions = [...page.transactions].sort((a, b) => a.version - b.version);
|
|
149
|
+
let entries = transactions.map(entryOf);
|
|
150
|
+
const state = stateFromSnapshot(page.snapshot);
|
|
151
|
+
let cursor = liveVersion;
|
|
152
|
+
const documents = /* @__PURE__ */ new Map();
|
|
153
|
+
/** Oldest version reachable with the transactions loaded so far. */
|
|
154
|
+
let floor = transactions[0]?.baseVersion ?? liveVersion;
|
|
155
|
+
/**
|
|
156
|
+
* Index of the first transaction whose version is `>= version`. Versions are
|
|
157
|
+
* strictly increasing but not dense (a no-op mutation leaves a gap), so
|
|
158
|
+
* binary-search by value rather than indexing by offset.
|
|
159
|
+
*/
|
|
160
|
+
function lowerBound(version) {
|
|
161
|
+
let low = 0;
|
|
162
|
+
let high = transactions.length;
|
|
163
|
+
while (low < high) {
|
|
164
|
+
const mid = low + high >> 1;
|
|
165
|
+
if (transactions[mid].version < version) low = mid + 1;
|
|
166
|
+
else high = mid;
|
|
167
|
+
}
|
|
168
|
+
return low;
|
|
169
|
+
}
|
|
170
|
+
function transactionProducing(version) {
|
|
171
|
+
const candidate = transactions[lowerBound(version)];
|
|
172
|
+
return candidate?.version === version ? candidate : void 0;
|
|
173
|
+
}
|
|
174
|
+
/** Undo the transaction that produced `cursor`. */
|
|
175
|
+
function stepBackward() {
|
|
176
|
+
const transaction = transactionProducing(cursor);
|
|
177
|
+
if (!transaction) return false;
|
|
178
|
+
for (let index = transaction.operations.length - 1; index >= 0; index -= 1) {
|
|
179
|
+
const inverse = inverseOf(transaction.operations[index]);
|
|
180
|
+
if (inverse) applyOperation(state, inverse.type, inverse.payload);
|
|
181
|
+
}
|
|
182
|
+
cursor = transaction.baseVersion;
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
function stepForward(next) {
|
|
186
|
+
for (const operation of next.operations) applyOperation(state, operation.type, operation.payload);
|
|
187
|
+
cursor = next.version;
|
|
188
|
+
}
|
|
189
|
+
function head() {
|
|
190
|
+
return transactions.at(-1)?.version ?? liveVersion;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Move to `version`, clamped to the loaded range. Returns the version reached,
|
|
194
|
+
* which is the nearest loaded version at or below the request.
|
|
195
|
+
*/
|
|
196
|
+
function seek(version) {
|
|
197
|
+
const target = Math.max(floor, Math.min(head(), version));
|
|
198
|
+
while (cursor > target && stepBackward());
|
|
199
|
+
for (let next = transactions[lowerBound(cursor + 1)]; next && next.version <= target; next = transactions[lowerBound(cursor + 1)]) stepForward(next);
|
|
200
|
+
return cursor;
|
|
201
|
+
}
|
|
202
|
+
function documentAt(version) {
|
|
203
|
+
const reached = seek(version);
|
|
204
|
+
const cached = documents.get(reached);
|
|
205
|
+
if (cached) return cached;
|
|
206
|
+
const document = project(state);
|
|
207
|
+
documents.set(reached, document);
|
|
208
|
+
return document;
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
/** Oldest version the loaded transactions can rewind to. */
|
|
212
|
+
get floor() {
|
|
213
|
+
return floor;
|
|
214
|
+
},
|
|
215
|
+
/** Newest version known to the player. */
|
|
216
|
+
get head() {
|
|
217
|
+
return head();
|
|
218
|
+
},
|
|
219
|
+
get entries() {
|
|
220
|
+
return entries;
|
|
221
|
+
},
|
|
222
|
+
/** Current position. */
|
|
223
|
+
get version() {
|
|
224
|
+
return cursor;
|
|
225
|
+
},
|
|
226
|
+
seek,
|
|
227
|
+
documentAt,
|
|
228
|
+
/** Item ids touched by the transaction that produced `version`, for camera follow. */
|
|
229
|
+
changedItemIds(version) {
|
|
230
|
+
return [...new Set(transactionProducing(version)?.operations.flatMap(operationItemIds))];
|
|
231
|
+
},
|
|
232
|
+
/** Add an older page (as served, newest-first). */
|
|
233
|
+
prepend(older) {
|
|
234
|
+
const current = floor;
|
|
235
|
+
const fresh = older.transactions.filter((transaction) => transaction.version <= current).sort((a, b) => a.version - b.version);
|
|
236
|
+
if (fresh.length === 0) return;
|
|
237
|
+
transactions = [...fresh, ...transactions];
|
|
238
|
+
entries = [...fresh.map(entryOf), ...entries];
|
|
239
|
+
floor = fresh[0].baseVersion;
|
|
240
|
+
},
|
|
241
|
+
/**
|
|
242
|
+
* Add transactions that landed after the current head. Forward payloads are
|
|
243
|
+
* enough to extend the timeline; the scrub position is untouched.
|
|
244
|
+
*
|
|
245
|
+
* Returns false when the page does not reach back to the head: the chain
|
|
246
|
+
* would have a hole, so nothing is appended and the caller must fetch an
|
|
247
|
+
* older page (`before` = the page's oldest version) and try again.
|
|
248
|
+
*/
|
|
249
|
+
append(latest) {
|
|
250
|
+
const current = head();
|
|
251
|
+
const fresh = latest.transactions.filter((transaction) => transaction.version > current).sort((a, b) => a.version - b.version);
|
|
252
|
+
if (fresh.length === 0) return true;
|
|
253
|
+
if (fresh[0].baseVersion > current) return false;
|
|
254
|
+
transactions = [...transactions, ...fresh];
|
|
255
|
+
entries = [...entries, ...fresh.map(entryOf)];
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
261
|
+
export { boardReplayActorKind, createBoardReplayPlayer };
|
|
@@ -4,7 +4,15 @@ import { BoardDocument, BoardItem } from "../protocol/dist/board-document.js";
|
|
|
4
4
|
import "../protocol/dist/index.js";
|
|
5
5
|
//#region src/board/semantic-document.d.ts
|
|
6
6
|
declare const DEFAULT_BOARD_APPEARANCE: {
|
|
7
|
-
theme
|
|
7
|
+
theme?: string | undefined;
|
|
8
|
+
motion?: {
|
|
9
|
+
enter?: {
|
|
10
|
+
kind: string;
|
|
11
|
+
kindVersion: number;
|
|
12
|
+
params: Record<string, unknown>;
|
|
13
|
+
} | undefined;
|
|
14
|
+
} | undefined;
|
|
15
|
+
mood?: "arcane" | "clean" | "cyber" | "natural" | "playful" | undefined;
|
|
8
16
|
background: {
|
|
9
17
|
kind: "custom" | "dots" | "grid" | "image" | "shader" | "solid";
|
|
10
18
|
color?: string | undefined;
|
|
@@ -18,7 +26,6 @@ declare const DEFAULT_BOARD_APPEARANCE: {
|
|
|
18
26
|
size: number;
|
|
19
27
|
opacity: number;
|
|
20
28
|
};
|
|
21
|
-
mood: "arcane" | "clean" | "cyber" | "natural" | "playful";
|
|
22
29
|
};
|
|
23
30
|
/** Convert a public authoring Item to the renderer/editor document shape. */
|
|
24
31
|
declare function boardAuthoringItemToDocumentItem(item: BoardAuthoringItem): BoardItem;
|
|
@@ -5,14 +5,12 @@ import { boardDrawPointsToWorld } from "../protocol/dist/board-geometry.js";
|
|
|
5
5
|
import { boardAuthoringItemToNode } from "../protocol/dist/board-codec.js";
|
|
6
6
|
//#region src/board/semantic-document.ts
|
|
7
7
|
const DEFAULT_BOARD_APPEARANCE = BoardAppearanceSchema.parse({
|
|
8
|
-
theme: "clean",
|
|
9
8
|
background: { kind: "solid" },
|
|
10
9
|
grid: {
|
|
11
10
|
visible: false,
|
|
12
11
|
size: 24,
|
|
13
12
|
opacity: .12
|
|
14
|
-
}
|
|
15
|
-
mood: "clean"
|
|
13
|
+
}
|
|
16
14
|
});
|
|
17
15
|
/** Convert a public authoring Item to the renderer/editor document shape. */
|
|
18
16
|
function boardAuthoringItemToDocumentItem(item) {
|