pi-fabric 0.28.13 → 0.29.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/dist/compaction/hook.d.ts +1 -0
- package/dist/compaction/hook.d.ts.map +1 -1
- package/dist/compaction/hook.js +13 -2
- package/dist/compaction/hook.js.map +1 -1
- package/dist/compaction/threshold.d.ts +5 -0
- package/dist/compaction/threshold.d.ts.map +1 -0
- package/dist/compaction/threshold.js +25 -0
- package/dist/compaction/threshold.js.map +1 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/ui/dashboard-fabric-graph.d.ts +22 -1
- package/dist/ui/dashboard-fabric-graph.d.ts.map +1 -1
- package/dist/ui/dashboard-fabric-graph.js +421 -71
- package/dist/ui/dashboard-fabric-graph.js.map +1 -1
- package/dist/ui/dashboard.d.ts +14 -0
- package/dist/ui/dashboard.d.ts.map +1 -1
- package/dist/ui/dashboard.js +156 -11
- package/dist/ui/dashboard.js.map +1 -1
- package/dist/ui/settings.d.ts +1 -0
- package/dist/ui/settings.d.ts.map +1 -1
- package/dist/ui/settings.js +27 -12
- package/dist/ui/settings.js.map +1 -1
- package/dist/ui/state-file-preview.d.ts +12 -0
- package/dist/ui/state-file-preview.d.ts.map +1 -0
- package/dist/ui/state-file-preview.js +94 -0
- package/dist/ui/state-file-preview.js.map +1 -0
- package/docs/compaction.md +22 -0
- package/docs/interface.md +30 -2
- package/package.json +1 -1
|
@@ -2,15 +2,17 @@ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
|
2
2
|
import { colorStatus, entityTail, statusGlyph } from "./dashboard-presentation.js";
|
|
3
3
|
import { padToWidth, safeText } from "./format.js";
|
|
4
4
|
import { isActiveStatus } from "./types.js";
|
|
5
|
+
import { loadStateFilePreview, renderStateFilePreview } from "./state-file-preview.js";
|
|
5
6
|
const kindRank = {
|
|
6
7
|
agent: 0,
|
|
7
8
|
actor: 1,
|
|
8
9
|
participant: 2,
|
|
9
10
|
peer: 3,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
group: 4,
|
|
12
|
+
topic: 5,
|
|
13
|
+
state: 6,
|
|
14
|
+
route: 7,
|
|
15
|
+
main: 8,
|
|
14
16
|
};
|
|
15
17
|
const activeRank = (status) => isActiveStatus(status) && status !== "blocked" ? 0 : status === "blocked" ? 1 : 2;
|
|
16
18
|
const nodeKind = (entity) => {
|
|
@@ -59,30 +61,124 @@ const graphLabel = (value, maxWidth) => {
|
|
|
59
61
|
}
|
|
60
62
|
return output;
|
|
61
63
|
};
|
|
62
|
-
const graphGlyph = (
|
|
63
|
-
if (
|
|
64
|
-
return "
|
|
65
|
-
|
|
64
|
+
const graphGlyph = (node, animation) => {
|
|
65
|
+
if (node.stale)
|
|
66
|
+
return animation.reducedMotion ? "·" : ["▧", "▫", "·", " "][Math.floor(animation.now / 240) % 4];
|
|
67
|
+
const frame = Math.floor(animation.now / 160);
|
|
68
|
+
const active = isActiveStatus(node.status);
|
|
69
|
+
if (node.kind === "main")
|
|
70
|
+
return active && !animation.reducedMotion ? ["◇", "◈", "◆", "◈"][frame % 4] : "◆";
|
|
71
|
+
if (node.kind === "actor") {
|
|
72
|
+
if ((node.queued ?? 0) > 0 && !animation.reducedMotion)
|
|
73
|
+
return ["◇", "◈", "◆", "◈"][frame % 4];
|
|
66
74
|
return "◇";
|
|
67
|
-
|
|
75
|
+
}
|
|
76
|
+
if (node.kind === "peer")
|
|
68
77
|
return "◈";
|
|
69
|
-
if (kind === "
|
|
70
|
-
return "
|
|
71
|
-
if (kind === "
|
|
78
|
+
if (node.kind === "group")
|
|
79
|
+
return "▱";
|
|
80
|
+
if (node.kind === "topic") {
|
|
81
|
+
const hot = node.activityAt !== undefined && animation.now - node.activityAt <= 10_000;
|
|
82
|
+
return hot && !animation.reducedMotion ? ["◎", "◉", "⦿", "◉"][frame % 4] : "◎";
|
|
83
|
+
}
|
|
84
|
+
if (node.kind === "state") {
|
|
85
|
+
if (/certified|committed|complete/.test(node.status) && !animation.reducedMotion) {
|
|
86
|
+
return ["◫", "▣", "✦", "▣"][Math.floor(animation.now / 280) % 4];
|
|
87
|
+
}
|
|
72
88
|
return "◫";
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (kind === "participant")
|
|
89
|
+
}
|
|
90
|
+
if (node.kind === "participant")
|
|
76
91
|
return "▧";
|
|
92
|
+
if (node.kind === "agent" && !animation.reducedMotion) {
|
|
93
|
+
const spawnAge = node.startedAt === undefined ? Number.POSITIVE_INFINITY : animation.now - node.startedAt;
|
|
94
|
+
if (spawnAge >= 0 && spawnAge <= 1_600) {
|
|
95
|
+
return ["·", "▪", "▫", "▣", "■"][Math.min(4, Math.floor(spawnAge / 320))];
|
|
96
|
+
}
|
|
97
|
+
if (active)
|
|
98
|
+
return ["■", "▣", "▪", "▣"][frame % 4];
|
|
99
|
+
}
|
|
77
100
|
return "■";
|
|
78
101
|
};
|
|
102
|
+
const PARTICIPANTS_GROUP_ID = "group:participants";
|
|
103
|
+
const MESH_GROUP_ID = "group:mesh";
|
|
104
|
+
const TOPICS_GROUP_ID = "group:mesh:topics";
|
|
105
|
+
const STATE_GROUP_ID = "group:mesh:state";
|
|
106
|
+
export const topologyParticipantGroup = (kind) => {
|
|
107
|
+
if (kind === "actor")
|
|
108
|
+
return { id: "actors", label: "Actors", order: 2 };
|
|
109
|
+
if (kind === "agent")
|
|
110
|
+
return { id: "agents", label: "Agents", order: 1 };
|
|
111
|
+
return { id: "sessions", label: "Sessions", order: 0 };
|
|
112
|
+
};
|
|
113
|
+
const titleSegment = (value) => value.length === 0 ? value : value[0].toUpperCase() + value.slice(1);
|
|
114
|
+
const namespaceParts = (value) => value.split(/[.:/]+/).map((part) => part.trim()).filter(Boolean);
|
|
115
|
+
export const topologyTopicGroupPath = (name) => {
|
|
116
|
+
const parts = namespaceParts(name);
|
|
117
|
+
if (parts[0] === "fabric") {
|
|
118
|
+
return [
|
|
119
|
+
{ id: "fabric", label: "Fabric" },
|
|
120
|
+
...(parts.length > 2 && parts[1]
|
|
121
|
+
? [{ id: `fabric:${parts[1]}`, label: titleSegment(parts[1]) }]
|
|
122
|
+
: []),
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
return [
|
|
126
|
+
{ id: "project", label: "Project topics" },
|
|
127
|
+
...(parts.length > 1 && parts[0]
|
|
128
|
+
? [{ id: `project:${parts[0]}`, label: parts[0] }]
|
|
129
|
+
: []),
|
|
130
|
+
];
|
|
131
|
+
};
|
|
132
|
+
export const topologyStateGroupPath = (key) => {
|
|
133
|
+
const parts = key.split("/").map((part) => part.trim()).filter(Boolean);
|
|
134
|
+
if (parts[0] === "state") {
|
|
135
|
+
const path = [{ id: "world", label: "World state" }];
|
|
136
|
+
if (parts[1] === "complexity") {
|
|
137
|
+
let prefix = "world:complexity";
|
|
138
|
+
path.push({ id: prefix, label: "Complexity" });
|
|
139
|
+
for (const directory of parts.slice(2, -1)) {
|
|
140
|
+
prefix += `:${directory}`;
|
|
141
|
+
path.push({ id: prefix, label: directory });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
let prefix = "world";
|
|
146
|
+
for (const directory of parts.slice(1, -1)) {
|
|
147
|
+
prefix += `:${directory}`;
|
|
148
|
+
path.push({ id: prefix, label: titleSegment(directory) });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return path;
|
|
152
|
+
}
|
|
153
|
+
if (parts[0] === "schema") {
|
|
154
|
+
const path = [{ id: "schema", label: "Schema" }];
|
|
155
|
+
const family = parts[1];
|
|
156
|
+
if (family === "hypothesis")
|
|
157
|
+
path.push({ id: "schema:hypotheses", label: "Hypotheses" });
|
|
158
|
+
else if (family === "certificate")
|
|
159
|
+
path.push({ id: "schema:certificates", label: "Certificates" });
|
|
160
|
+
else {
|
|
161
|
+
let prefix = "schema";
|
|
162
|
+
for (const directory of parts.slice(1, -1)) {
|
|
163
|
+
prefix += `:${directory}`;
|
|
164
|
+
path.push({ id: prefix, label: titleSegment(directory) });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return path;
|
|
168
|
+
}
|
|
169
|
+
const path = [{ id: "project", label: "Project state" }];
|
|
170
|
+
let prefix = "project";
|
|
171
|
+
for (const directory of parts.slice(0, -1)) {
|
|
172
|
+
prefix += `:${directory}`;
|
|
173
|
+
path.push({ id: prefix, label: directory });
|
|
174
|
+
}
|
|
175
|
+
return path;
|
|
176
|
+
};
|
|
79
177
|
const parentReference = (entity) => {
|
|
80
178
|
if (entity.kind === "agent")
|
|
81
179
|
return entity.value.parentId ?? entity.value.actorId;
|
|
82
180
|
if (entity.kind === "meshParticipant")
|
|
83
181
|
return entity.value.participant?.parentId;
|
|
84
|
-
if (entity.kind === "state")
|
|
85
|
-
return entity.value.owner;
|
|
86
182
|
if (entity.kind === "meshRoute")
|
|
87
183
|
return entity.value.fromId;
|
|
88
184
|
return undefined;
|
|
@@ -100,19 +196,94 @@ const buildLayout = (snapshot, entities, selectedRun, mesh) => {
|
|
|
100
196
|
aliases.set(snapshot.main.id, mainId);
|
|
101
197
|
aliases.set(snapshot.main.name, mainId);
|
|
102
198
|
aliases.set("main", mainId);
|
|
103
|
-
const
|
|
199
|
+
const groups = new Map();
|
|
200
|
+
const ensureGroup = (id, label, parentId, order) => {
|
|
201
|
+
if (!groups.has(id)) {
|
|
202
|
+
groups.set(id, {
|
|
203
|
+
id,
|
|
204
|
+
label,
|
|
205
|
+
status: "idle",
|
|
206
|
+
kind: "group",
|
|
207
|
+
parentId,
|
|
208
|
+
order,
|
|
209
|
+
x: 0,
|
|
210
|
+
y: 0,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return id;
|
|
214
|
+
};
|
|
215
|
+
const ensurePath = (rootId, path) => {
|
|
216
|
+
let parentId = rootId;
|
|
217
|
+
for (let index = 0; index < path.length; index++) {
|
|
218
|
+
const segment = path[index];
|
|
219
|
+
const id = `${rootId}:${segment.id}`;
|
|
220
|
+
parentId = ensureGroup(id, segment.label, parentId, index);
|
|
221
|
+
}
|
|
222
|
+
return parentId;
|
|
223
|
+
};
|
|
224
|
+
const participantCategory = (entity) => {
|
|
225
|
+
if (entity.kind === "actor")
|
|
226
|
+
return topologyParticipantGroup("actor");
|
|
227
|
+
if (entity.kind === "agent")
|
|
228
|
+
return topologyParticipantGroup("agent");
|
|
229
|
+
if (entity.kind === "peer")
|
|
230
|
+
return topologyParticipantGroup("peer");
|
|
231
|
+
return topologyParticipantGroup(entity.kind === "meshParticipant" ? entity.value.participant?.kind ?? "root" : "root");
|
|
232
|
+
};
|
|
233
|
+
const nodes = entities
|
|
234
|
+
.filter((entity) => entity.kind !== "meshRoute")
|
|
235
|
+
.map((entity) => {
|
|
104
236
|
const parentRef = parentReference(entity);
|
|
105
|
-
const
|
|
237
|
+
const explicitParentId = parentRef ? aliases.get(parentRef) : undefined;
|
|
238
|
+
let parentId = explicitParentId;
|
|
239
|
+
if (entity.kind !== "main") {
|
|
240
|
+
if (["agent", "actor", "peer", "meshParticipant"].includes(entity.kind) &&
|
|
241
|
+
(!parentId || parentId === mainId)) {
|
|
242
|
+
ensureGroup(PARTICIPANTS_GROUP_ID, "Participants", mainId, 0);
|
|
243
|
+
const category = participantCategory(entity);
|
|
244
|
+
parentId = ensureGroup(`${PARTICIPANTS_GROUP_ID}:${category.id}`, category.label, PARTICIPANTS_GROUP_ID, category.order);
|
|
245
|
+
}
|
|
246
|
+
else if (entity.kind === "meshTopic") {
|
|
247
|
+
ensureGroup(MESH_GROUP_ID, "Mesh", mainId, 1);
|
|
248
|
+
ensureGroup(TOPICS_GROUP_ID, "Topics", MESH_GROUP_ID, 0);
|
|
249
|
+
parentId = ensurePath(TOPICS_GROUP_ID, topologyTopicGroupPath(entity.value.name));
|
|
250
|
+
}
|
|
251
|
+
else if (entity.kind === "state") {
|
|
252
|
+
ensureGroup(MESH_GROUP_ID, "Mesh", mainId, 1);
|
|
253
|
+
ensureGroup(STATE_GROUP_ID, "State", MESH_GROUP_ID, 1);
|
|
254
|
+
parentId = ensurePath(STATE_GROUP_ID, topologyStateGroupPath(entity.value.key));
|
|
255
|
+
}
|
|
256
|
+
else if (!parentId) {
|
|
257
|
+
parentId = mainId;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const activityAt = entity.kind === "meshTopic" ? entity.value.lastEventAt : undefined;
|
|
261
|
+
const startedAt = entity.kind === "agent" ? entity.value.startedAt : undefined;
|
|
262
|
+
const queued = entity.kind === "actor" ? entity.value.queued : undefined;
|
|
263
|
+
const stale = entity.kind === "meshParticipant"
|
|
264
|
+
? entity.value.participant?.stale
|
|
265
|
+
: entity.kind === "agent"
|
|
266
|
+
? entity.value.stale
|
|
267
|
+
: undefined;
|
|
106
268
|
return {
|
|
107
269
|
id: entity.id,
|
|
108
|
-
label: entity.kind === "main"
|
|
270
|
+
label: entity.kind === "main"
|
|
271
|
+
? "Main"
|
|
272
|
+
: entity.kind === "state" && entity.value.label === entity.value.key
|
|
273
|
+
? entity.value.key.split("/").at(-1) ?? entity.value.label
|
|
274
|
+
: entity.label,
|
|
109
275
|
status: entity.status,
|
|
110
276
|
kind: nodeKind(entity),
|
|
111
|
-
...(
|
|
277
|
+
...(parentId ? { parentId } : {}),
|
|
278
|
+
...(activityAt !== undefined ? { activityAt } : {}),
|
|
279
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
280
|
+
...(queued !== undefined ? { queued } : {}),
|
|
281
|
+
...(stale !== undefined ? { stale } : {}),
|
|
112
282
|
x: 0,
|
|
113
283
|
y: 0,
|
|
114
284
|
};
|
|
115
285
|
});
|
|
286
|
+
nodes.push(...groups.values());
|
|
116
287
|
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
117
288
|
const children = new Map();
|
|
118
289
|
for (const node of nodes) {
|
|
@@ -129,6 +300,7 @@ const buildLayout = (snapshot, entities, selectedRun, mesh) => {
|
|
|
129
300
|
for (const bucket of children.values()) {
|
|
130
301
|
bucket.sort((left, right) => activeRank(left.status) - activeRank(right.status) ||
|
|
131
302
|
Number(inSelectedRun(right)) - Number(inSelectedRun(left)) ||
|
|
303
|
+
(left.order ?? 0) - (right.order ?? 0) ||
|
|
132
304
|
kindRank[left.kind] - kindRank[right.kind] ||
|
|
133
305
|
left.label.localeCompare(right.label));
|
|
134
306
|
}
|
|
@@ -142,7 +314,7 @@ const buildLayout = (snapshot, entities, selectedRun, mesh) => {
|
|
|
142
314
|
const descendants = (children.get(node.id) ?? []).filter((child) => !visited.has(child.id));
|
|
143
315
|
if (descendants.length === 0) {
|
|
144
316
|
node.y = nextLeafY;
|
|
145
|
-
nextLeafY +=
|
|
317
|
+
nextLeafY += 2;
|
|
146
318
|
return node.y;
|
|
147
319
|
}
|
|
148
320
|
const childRows = descendants.map((child) => place(child, depth + 1));
|
|
@@ -175,17 +347,36 @@ const buildLayout = (snapshot, entities, selectedRun, mesh) => {
|
|
|
175
347
|
edges.push({ from: source, to: target, kind: "subscription" });
|
|
176
348
|
}
|
|
177
349
|
}
|
|
350
|
+
for (const entity of entities) {
|
|
351
|
+
if (entity.kind !== "state" || !entity.value.owner)
|
|
352
|
+
continue;
|
|
353
|
+
const owner = aliases.get(entity.value.owner);
|
|
354
|
+
const target = aliases.get(entity.value.key) ?? aliases.get(entity.value.label);
|
|
355
|
+
if (owner && target && owner !== target) {
|
|
356
|
+
edges.push({ from: owner, to: target, kind: "subscription" });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
178
359
|
for (const route of mesh.routes) {
|
|
179
360
|
const source = aliases.get(route.fromId) ?? aliases.get(route.fromName);
|
|
180
361
|
const target = aliases.get(route.targetId) ?? aliases.get(route.targetName) ?? aliases.get(route.topic);
|
|
181
|
-
if (source && target && source !== target)
|
|
182
|
-
edges.push({ from: source, to: target, kind: "route" });
|
|
362
|
+
if (source && target && source !== target) {
|
|
363
|
+
edges.push({ from: source, to: target, kind: "route", route });
|
|
364
|
+
}
|
|
183
365
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
366
|
+
const positions = new Map(nodes.map((node) => [node.id, { x: node.x, y: node.y }]));
|
|
367
|
+
for (const edge of edges) {
|
|
368
|
+
if (!edge.route)
|
|
369
|
+
continue;
|
|
370
|
+
const from = nodeById.get(edge.from);
|
|
371
|
+
const to = nodeById.get(edge.to);
|
|
372
|
+
if (!from || !to)
|
|
373
|
+
continue;
|
|
374
|
+
positions.set(edge.route.id, {
|
|
375
|
+
x: Math.round((from.x + to.x) / 2),
|
|
376
|
+
y: Math.round((from.y + to.y) / 2),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return { nodes, edges, positions };
|
|
189
380
|
};
|
|
190
381
|
const lineChar = (mask) => {
|
|
191
382
|
const chars = {
|
|
@@ -203,12 +394,66 @@ const styleForStatus = (status) => {
|
|
|
203
394
|
return "success";
|
|
204
395
|
return "dim";
|
|
205
396
|
};
|
|
206
|
-
const
|
|
397
|
+
export const topologyTreeRouteNodeIds = (nodes, fromId, toId) => {
|
|
398
|
+
if (fromId === toId)
|
|
399
|
+
return [fromId];
|
|
400
|
+
const ancestry = (start) => {
|
|
401
|
+
const path = [];
|
|
402
|
+
const seen = new Set();
|
|
403
|
+
let current = start;
|
|
404
|
+
while (current && !seen.has(current)) {
|
|
405
|
+
path.push(current);
|
|
406
|
+
seen.add(current);
|
|
407
|
+
current = nodes.get(current)?.parentId;
|
|
408
|
+
}
|
|
409
|
+
return path;
|
|
410
|
+
};
|
|
411
|
+
const fromAncestors = ancestry(fromId);
|
|
412
|
+
const fromSet = new Set(fromAncestors);
|
|
413
|
+
const toAncestors = ancestry(toId);
|
|
414
|
+
const common = toAncestors.find((id) => fromSet.has(id));
|
|
415
|
+
if (!common)
|
|
416
|
+
return [fromId, toId];
|
|
417
|
+
return [
|
|
418
|
+
...fromAncestors.slice(0, fromAncestors.indexOf(common) + 1),
|
|
419
|
+
...toAncestors.slice(0, toAncestors.indexOf(common)).reverse(),
|
|
420
|
+
];
|
|
421
|
+
};
|
|
422
|
+
const routeGlyph = (route) => {
|
|
423
|
+
if (route.status === "failed")
|
|
424
|
+
return "!";
|
|
425
|
+
const kind = route.kind.toLowerCase();
|
|
426
|
+
if (kind.includes("certif"))
|
|
427
|
+
return "✦";
|
|
428
|
+
if (kind.includes("commit"))
|
|
429
|
+
return "◆";
|
|
430
|
+
if (kind.includes("control") || kind.includes("steer"))
|
|
431
|
+
return "↯";
|
|
432
|
+
if (kind.includes("message") || kind.includes("directive"))
|
|
433
|
+
return "✉";
|
|
434
|
+
return "•";
|
|
435
|
+
};
|
|
436
|
+
const routeStyle = (route) => route.status === "failed"
|
|
437
|
+
? "error"
|
|
438
|
+
: route.kind.toLowerCase().includes("certif")
|
|
439
|
+
? "warning"
|
|
440
|
+
: "accent";
|
|
441
|
+
const renderCanvas = (theme, layout, selectedEntityId, width, height, camera, animation) => {
|
|
207
442
|
const originX = Math.round(camera.x - width / 2);
|
|
208
443
|
const originY = Math.round(camera.y - height / 2);
|
|
209
444
|
const cells = Array.from({ length: height }, () => Array.from({ length: width }, () => ({ char: " ", style: "plain" })));
|
|
210
445
|
const masks = Array.from({ length: height }, () => Array(width).fill(0));
|
|
211
446
|
const nodeById = new Map(layout.nodes.map((node) => [node.id, node]));
|
|
447
|
+
const selectedRoute = layout.edges.find((edge) => edge.route?.id === selectedEntityId)?.route;
|
|
448
|
+
const related = new Set(selectedEntityId ? [selectedEntityId] : []);
|
|
449
|
+
for (const edge of layout.edges) {
|
|
450
|
+
if (edge.from === selectedEntityId || edge.to === selectedEntityId || edge.route?.id === selectedEntityId) {
|
|
451
|
+
related.add(edge.from);
|
|
452
|
+
related.add(edge.to);
|
|
453
|
+
if (edge.route)
|
|
454
|
+
related.add(edge.route.id);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
212
457
|
const setCell = (x, y, char, style) => {
|
|
213
458
|
const sx = x - originX;
|
|
214
459
|
const sy = y - originY;
|
|
@@ -245,18 +490,52 @@ const renderCanvas = (theme, layout, selectedEntityId, width, height, camera) =>
|
|
|
245
490
|
addMask(x, y, (y > worldStart ? 1 : 0) | (y < worldEnd ? 4 : 0));
|
|
246
491
|
}
|
|
247
492
|
};
|
|
248
|
-
const
|
|
493
|
+
const edgePathPoints = (from, to) => {
|
|
494
|
+
const fromEnd = from.x + Math.min(16, visibleWidth(safeText(from.label)) + 3);
|
|
495
|
+
const toStart = to.x - 2;
|
|
496
|
+
const bend = Math.max(fromEnd + 1, Math.floor((fromEnd + toStart) / 2));
|
|
497
|
+
const points = [];
|
|
498
|
+
const appendLine = (x1, y1, x2, y2) => {
|
|
499
|
+
const dx = Math.sign(x2 - x1);
|
|
500
|
+
const dy = Math.sign(y2 - y1);
|
|
501
|
+
const steps = Math.max(Math.abs(x2 - x1), Math.abs(y2 - y1));
|
|
502
|
+
for (let step = points.length === 0 ? 0 : 1; step <= steps; step++) {
|
|
503
|
+
points.push({ x: x1 + dx * step, y: y1 + dy * step });
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
appendLine(fromEnd, from.y, bend, from.y);
|
|
507
|
+
appendLine(bend, from.y, bend, to.y);
|
|
508
|
+
appendLine(bend, to.y, toStart, to.y);
|
|
509
|
+
return points;
|
|
510
|
+
};
|
|
511
|
+
const treePathPoints = (from, to) => {
|
|
512
|
+
const ids = topologyTreeRouteNodeIds(nodeById, from.id, to.id);
|
|
513
|
+
const points = [];
|
|
514
|
+
for (let index = 0; index < ids.length - 1; index++) {
|
|
515
|
+
const left = nodeById.get(ids[index]);
|
|
516
|
+
const right = nodeById.get(ids[index + 1]);
|
|
517
|
+
if (!left || !right)
|
|
518
|
+
continue;
|
|
519
|
+
const segment = right.parentId === left.id
|
|
520
|
+
? edgePathPoints(left, right)
|
|
521
|
+
: left.parentId === right.id
|
|
522
|
+
? edgePathPoints(right, left).reverse()
|
|
523
|
+
: edgePathPoints(left, right);
|
|
524
|
+
const previous = points.at(-1);
|
|
525
|
+
const first = segment[0];
|
|
526
|
+
if (previous && first && previous.x === first.x && previous.y === first.y)
|
|
527
|
+
segment.shift();
|
|
528
|
+
points.push(...segment);
|
|
529
|
+
}
|
|
530
|
+
return points;
|
|
531
|
+
};
|
|
249
532
|
for (const edge of layout.edges) {
|
|
250
|
-
if (edge.kind !== "structure"
|
|
533
|
+
if (edge.kind !== "structure")
|
|
251
534
|
continue;
|
|
252
535
|
const from = nodeById.get(edge.from);
|
|
253
536
|
const to = nodeById.get(edge.to);
|
|
254
537
|
if (!from || !to)
|
|
255
538
|
continue;
|
|
256
|
-
if (edge.kind !== "structure") {
|
|
257
|
-
meshPaths.push({ from, to });
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
539
|
const fromEnd = from.x + Math.min(16, visibleWidth(safeText(from.label)) + 3);
|
|
261
540
|
const toStart = to.x - 2;
|
|
262
541
|
const bend = Math.max(fromEnd + 1, Math.floor((fromEnd + toStart) / 2));
|
|
@@ -271,44 +550,104 @@ const renderCanvas = (theme, layout, selectedEntityId, width, height, camera) =>
|
|
|
271
550
|
cells[y][x] = { char: lineChar(mask), style: "edge" };
|
|
272
551
|
}
|
|
273
552
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
553
|
+
const traffic = new Map();
|
|
554
|
+
for (const edge of layout.edges) {
|
|
555
|
+
if (edge.kind === "structure")
|
|
556
|
+
continue;
|
|
557
|
+
const from = nodeById.get(edge.from);
|
|
558
|
+
const to = nodeById.get(edge.to);
|
|
559
|
+
if (!from || !to)
|
|
560
|
+
continue;
|
|
561
|
+
if (!edge.route) {
|
|
562
|
+
if (edge.from !== selectedEntityId && edge.to !== selectedEntityId)
|
|
563
|
+
continue;
|
|
564
|
+
const points = treePathPoints(from, to);
|
|
565
|
+
for (let index = 0; index < points.length; index += 2) {
|
|
566
|
+
const point = points[index];
|
|
567
|
+
setCell(point.x, point.y, "·", "accent");
|
|
289
568
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const key = `${edge.from}\u0000${edge.to}`;
|
|
572
|
+
const group = traffic.get(key) ?? { from, to, routes: [] };
|
|
573
|
+
group.routes.push(edge.route);
|
|
574
|
+
traffic.set(key, group);
|
|
575
|
+
}
|
|
576
|
+
for (const group of traffic.values()) {
|
|
577
|
+
const points = treePathPoints(group.from, group.to);
|
|
578
|
+
if (points.length === 0)
|
|
579
|
+
continue;
|
|
580
|
+
const selected = group.routes.find((route) => route.id === selectedEntityId);
|
|
581
|
+
const replay = group.routes.find((route) => route.id === animation.replayRouteId);
|
|
582
|
+
const newest = group.routes.reduce((latest, route) => route.lastAt > latest.lastAt ? route : latest);
|
|
583
|
+
const route = replay ?? selected ?? newest;
|
|
584
|
+
const age = Math.max(0, animation.now - route.lastAt);
|
|
585
|
+
const connected = group.from.id === selectedEntityId || group.to.id === selectedEntityId;
|
|
586
|
+
const visible = Boolean(replay || selected || connected || animation.showHistory || age <= 60_000);
|
|
587
|
+
if (!visible)
|
|
588
|
+
continue;
|
|
589
|
+
const fresh = Boolean(replay || age <= 10_000);
|
|
590
|
+
const style = selected || replay
|
|
591
|
+
? routeStyle(route)
|
|
592
|
+
: fresh
|
|
593
|
+
? "accent"
|
|
594
|
+
: "dim";
|
|
595
|
+
const stride = fresh ? 1 : 2;
|
|
596
|
+
for (let index = 0; index < points.length; index += stride) {
|
|
597
|
+
const point = points[index];
|
|
598
|
+
const trail = fresh && !animation.reducedMotion && index % 3 === 0 ? "━" : fresh ? "·" : "╌";
|
|
599
|
+
setCell(point.x, point.y, trail, style);
|
|
600
|
+
}
|
|
601
|
+
const packetActive = !animation.reducedMotion && Boolean(replay || age <= 2_000);
|
|
602
|
+
if (packetActive) {
|
|
603
|
+
const duration = 1_200;
|
|
604
|
+
const phase = replay
|
|
605
|
+
? (animation.now % duration) / duration
|
|
606
|
+
: Math.min(0.999, age / 2_000);
|
|
607
|
+
const packetIndex = Math.min(points.length - 1, Math.floor(phase * points.length));
|
|
608
|
+
const point = points[packetIndex];
|
|
609
|
+
setCell(point.x, point.y, routeGlyph(route), routeStyle(route));
|
|
610
|
+
if (route.kind.toLowerCase().includes("certif")) {
|
|
611
|
+
const ripple = points[Math.max(0, points.length - 1 - packetIndex)];
|
|
612
|
+
setCell(ripple.x, ripple.y, "✦", "warning");
|
|
613
|
+
}
|
|
614
|
+
else if (route.status === "failed") {
|
|
615
|
+
const impact = points.at(-1);
|
|
616
|
+
setCell(impact.x, impact.y, Math.floor(animation.now / 120) % 2 === 0 ? "╳" : "!", "error");
|
|
617
|
+
}
|
|
618
|
+
const label = graphLabel(animation.replayLabel ?? route.kind, 18);
|
|
619
|
+
let offset = 2;
|
|
620
|
+
for (const char of label) {
|
|
621
|
+
setCell(point.x + offset, point.y, char, routeStyle(route));
|
|
622
|
+
offset += visibleWidth(char);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
const count = group.routes.reduce((total, candidate) => total + candidate.count, 0);
|
|
627
|
+
if (count > 1) {
|
|
628
|
+
const point = points[Math.floor(points.length / 2)];
|
|
629
|
+
const label = `×${count}`;
|
|
630
|
+
for (let index = 0; index < label.length; index++) {
|
|
631
|
+
setCell(point.x + index, point.y, label[index], style);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
302
635
|
}
|
|
303
636
|
for (const node of layout.nodes) {
|
|
304
637
|
const selected = node.id === selectedEntityId;
|
|
305
|
-
const
|
|
638
|
+
const spotlighted = !selectedRoute || related.has(node.id);
|
|
639
|
+
const glyph = selected ? "▣" : graphGlyph(node, animation);
|
|
306
640
|
const label = graphLabel(safeText(node.label), 14);
|
|
307
|
-
|
|
641
|
+
const nodeStyle = selected
|
|
642
|
+
? "accent"
|
|
643
|
+
: !spotlighted
|
|
644
|
+
? "dim"
|
|
645
|
+
: styleForStatus(node.status);
|
|
646
|
+
setCell(node.x, node.y, glyph, nodeStyle);
|
|
308
647
|
setCell(node.x + 1, node.y, " ", "plain");
|
|
309
648
|
let offset = 0;
|
|
310
649
|
for (const char of label) {
|
|
311
|
-
setCell(node.x + 2 + offset, node.y, char, selected ? "accent" : "plain");
|
|
650
|
+
setCell(node.x + 2 + offset, node.y, char, selected ? "accent" : spotlighted ? "plain" : "dim");
|
|
312
651
|
offset += visibleWidth(char);
|
|
313
652
|
}
|
|
314
653
|
}
|
|
@@ -348,7 +687,7 @@ const wrapInspector = (theme, label, value, width) => {
|
|
|
348
687
|
const first = truncateToWidth(clean, Math.max(1, width - label.length - 1), "…");
|
|
349
688
|
return [`${label} ${theme.fg("dim", first)}`];
|
|
350
689
|
};
|
|
351
|
-
const inspectorLines = (theme, entity, snapshot, run, width, height) => {
|
|
690
|
+
const inspectorLines = (theme, entity, snapshot, run, width, height, invalidate) => {
|
|
352
691
|
const inner = Math.max(1, width - 2);
|
|
353
692
|
const border = (value) => theme.fg("borderMuted", value);
|
|
354
693
|
const content = [];
|
|
@@ -383,12 +722,20 @@ const inspectorLines = (theme, entity, snapshot, run, width, height) => {
|
|
|
383
722
|
}
|
|
384
723
|
else if (entity.kind === "meshRoute") {
|
|
385
724
|
content.push(`${safeText(entity.value.fromName)} → ${safeText(entity.value.targetName)}`);
|
|
386
|
-
content.push(
|
|
725
|
+
content.push(`kind ${safeText(entity.value.kind)}`);
|
|
726
|
+
content.push(`topic ${theme.fg("dim", safeText(entity.value.topic))}`);
|
|
727
|
+
content.push(`count ${entity.value.count}`);
|
|
387
728
|
}
|
|
388
729
|
else if (entity.kind === "state") {
|
|
389
730
|
content.push(`version ${entity.value.version}`);
|
|
390
731
|
if (entity.value.owner)
|
|
391
732
|
content.push(`owner ${safeText(entity.value.owner)}`);
|
|
733
|
+
const filePreview = loadStateFilePreview(entity.value, snapshot.main.cwd ?? process.cwd());
|
|
734
|
+
if (filePreview) {
|
|
735
|
+
content.push("");
|
|
736
|
+
content.push(`file ${safeText(filePreview.path)}`);
|
|
737
|
+
content.push(...renderStateFilePreview(filePreview, theme, Math.max(1, inner - 2), Math.max(0, height - content.length - 3), invalidate));
|
|
738
|
+
}
|
|
392
739
|
}
|
|
393
740
|
}
|
|
394
741
|
else {
|
|
@@ -425,16 +772,16 @@ const graphContextEntities = (allEntities, entities) => {
|
|
|
425
772
|
}
|
|
426
773
|
return allEntities.filter((entity) => visible.has(entity.id));
|
|
427
774
|
};
|
|
428
|
-
export const renderFabricTopologyPanel = ({ theme, filter, selectedEntityId, snapshot, run, mesh, allEntities, entities, width, height, camera, }) => {
|
|
775
|
+
export const renderFabricTopologyPanel = ({ theme, filter, selectedEntityId, snapshot, run, mesh, allEntities, entities, width, height, camera, animation, invalidate, }) => {
|
|
429
776
|
const graphEntities = graphContextEntities(allEntities, entities);
|
|
430
777
|
const layout = buildLayout(snapshot, graphEntities, run, mesh);
|
|
431
778
|
const selectableIds = new Set(entities.map((entity) => entity.id));
|
|
432
779
|
const selected = entities.find((entity) => entity.id === selectedEntityId) ?? entities[0];
|
|
433
780
|
const inspectorWidth = width >= 92 ? Math.min(36, Math.max(30, Math.floor(width * 0.3))) : 0;
|
|
434
781
|
const graphWidth = Math.max(1, width - inspectorWidth);
|
|
435
|
-
const graph = renderCanvas(theme, layout, selected?.id, graphWidth, height, camera);
|
|
782
|
+
const graph = renderCanvas(theme, layout, selected?.id, graphWidth, height, camera, animation);
|
|
436
783
|
const inspector = inspectorWidth > 0
|
|
437
|
-
? inspectorLines(theme, selected, snapshot, run, inspectorWidth, height)
|
|
784
|
+
? inspectorLines(theme, selected, snapshot, run, inspectorWidth, height, invalidate)
|
|
438
785
|
: [];
|
|
439
786
|
const lines = inspectorWidth > 0
|
|
440
787
|
? graph.map((line, index) => `${padToWidth(line, graphWidth)}${inspector[index] ?? ""}`)
|
|
@@ -464,6 +811,9 @@ export const renderFabricTopologyPanel = ({ theme, filter, selectedEntityId, sna
|
|
|
464
811
|
"■ agent",
|
|
465
812
|
"◇ actor",
|
|
466
813
|
"◎ topic",
|
|
814
|
+
"▱ group",
|
|
815
|
+
animation.replayRouteId ? "▶ replay" : animation.showHistory ? "history" : "live decay",
|
|
816
|
+
animation.reducedMotion ? "reduced motion" : undefined,
|
|
467
817
|
filter !== "all" ? `${entities.length}/${allEntities.length} ${filter}` : undefined,
|
|
468
818
|
].filter((value) => Boolean(value)).join(" · ");
|
|
469
819
|
const graphLegend = padToWidth(theme.fg("dim", truncateToWidth(legend, graphWidth, "")), graphWidth);
|