@llblab/pi-actors 0.17.0 → 0.18.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/AGENTS.md +5 -3
- package/BACKLOG.md +54 -29
- package/CHANGELOG.md +18 -2
- package/README.md +184 -300
- package/docs/actor-messages.md +6 -2
- package/docs/async-runs.md +3 -5
- package/docs/command-templates.md +2 -0
- package/docs/recipe-library.md +3 -0
- package/docs/task-first-recipes.md +29 -0
- package/docs/template-recipes.md +9 -14
- package/index.ts +158 -34
- package/lib/actor-inspector-tui.ts +374 -118
- package/lib/actor-rooms.ts +222 -24
- package/lib/async-runs.ts +59 -1
- package/lib/execution.ts +17 -0
- package/lib/file-state.ts +2 -1
- package/lib/observability.ts +82 -2
- package/lib/prompts.ts +2 -2
- package/lib/recipe-discovery.ts +86 -6
- package/lib/recipe-migration.ts +0 -2
- package/lib/recipe-references.ts +43 -10
- package/lib/temp.ts +55 -2
- package/lib/tools.ts +99 -11
- package/package.json +1 -1
- package/recipes/coordinator-locker.json +0 -1
- package/recipes/lens-swarm.json +0 -1
- package/recipes/music-player.json +0 -1
- package/recipes/pipeline-architect-coordinator.json +0 -1
- package/recipes/pipeline-artifact-bundle.json +0 -1
- package/recipes/pipeline-artifact-report.json +0 -1
- package/recipes/pipeline-artifact-write.json +0 -1
- package/recipes/pipeline-async-run-ops.json +0 -1
- package/recipes/pipeline-checkpoint-continuation.json +0 -1
- package/recipes/pipeline-development-tasking.json +0 -1
- package/recipes/pipeline-docs-maintenance.json +0 -1
- package/recipes/pipeline-media-library.json +0 -1
- package/recipes/pipeline-quorum-review.json +0 -1
- package/recipes/pipeline-release-readiness.json +0 -1
- package/recipes/pipeline-release-summary.json +0 -1
- package/recipes/pipeline-repo-health.json +0 -1
- package/recipes/pipeline-research-synthesis.json +0 -1
- package/recipes/pipeline-review-readiness.json +0 -1
- package/recipes/pipeline-room-swarm.json +48 -0
- package/recipes/subagent-artifact.json +0 -1
- package/recipes/subagent-checkpoint.json +0 -1
- package/recipes/subagent-conflict-report.json +0 -1
- package/recipes/subagent-contradiction-map.json +0 -1
- package/recipes/subagent-critic.json +0 -1
- package/recipes/subagent-evidence-map.json +0 -1
- package/recipes/subagent-followup.json +0 -1
- package/recipes/subagent-judge.json +0 -1
- package/recipes/subagent-merge.json +0 -1
- package/recipes/subagent-message.json +0 -1
- package/recipes/subagent-normalize.json +0 -1
- package/recipes/subagent-plan.json +0 -1
- package/recipes/subagent-prompt.json +0 -1
- package/recipes/subagent-quorum.json +0 -1
- package/recipes/subagent-review-coordinator.json +0 -1
- package/recipes/subagent-review.json +0 -1
- package/recipes/subagent-task-card.json +0 -1
- package/recipes/subagent-tools.json +0 -1
- package/recipes/subagent-verify.json +0 -1
- package/recipes/subagents-prompts.json +0 -1
- package/recipes/utility-actor-message.json +0 -1
- package/recipes/utility-artifact-manifest.json +0 -1
- package/recipes/utility-artifact-write.json +0 -1
- package/recipes/utility-changelog-head.json +0 -1
- package/recipes/utility-changelog-section.json +0 -1
- package/recipes/utility-coordinator-lock-snapshot.json +0 -1
- package/recipes/utility-git-log.json +0 -1
- package/recipes/utility-git-status.json +0 -1
- package/recipes/utility-jsonl-tail.json +0 -1
- package/recipes/utility-markdown-index.json +0 -1
- package/recipes/utility-package-summary.json +0 -1
- package/recipes/utility-playlist-build.json +0 -1
- package/recipes/utility-playlist-scan.json +0 -1
- package/recipes/utility-run-ops-snapshot.json +0 -1
- package/recipes/utility-run-state-files.json +0 -1
- package/recipes/utility-run-summary.json +0 -1
- package/recipes/utility-skill-summary.json +0 -1
- package/recipes/utility-validate-recipe.json +0 -1
- package/recipes/utility-validation-wrapper.json +0 -1
- package/scripts/room-swarm.mjs +243 -0
- package/skills/actors/SKILL.md +25 -12
- package/skills/swarm/SKILL.md +15 -1
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
|
|
9
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
10
|
+
|
|
9
11
|
import type { ActorMessage } from "./actor-messages.ts";
|
|
10
12
|
import * as Paths from "./paths.ts";
|
|
11
13
|
|
|
@@ -13,6 +15,7 @@ export interface ActorInspectorPreview {
|
|
|
13
15
|
body_preview?: string;
|
|
14
16
|
channel: "broadcast" | "direct" | "room";
|
|
15
17
|
from?: string;
|
|
18
|
+
from_display?: string;
|
|
16
19
|
run: string;
|
|
17
20
|
sequence?: number;
|
|
18
21
|
summary?: string;
|
|
@@ -32,15 +35,25 @@ export interface ActorInspectorWidgetStyle {
|
|
|
32
35
|
type?: (text: string) => string;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
export
|
|
38
|
+
export interface ActorInspectorRenderOptions {}
|
|
39
|
+
|
|
40
|
+
export interface ActorInspectorItemViewOptions {
|
|
41
|
+
sequence: number;
|
|
42
|
+
}
|
|
36
43
|
|
|
37
|
-
export interface
|
|
38
|
-
|
|
44
|
+
export interface ActorInspectorRosterMember {
|
|
45
|
+
address: string;
|
|
46
|
+
display?: string;
|
|
47
|
+
role?: string;
|
|
48
|
+
status?: string;
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
export interface ActorInspectorPreviewReadOptions {
|
|
42
52
|
ownerId?: string;
|
|
43
53
|
currentRunOnly?: boolean;
|
|
54
|
+
channels?: ActorInspectorPreview["channel"][];
|
|
55
|
+
mention?: string;
|
|
56
|
+
roomLimitPerRun?: number;
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
@@ -78,9 +91,12 @@ function previewValue(value: unknown, maxLength = 320): string | undefined {
|
|
|
78
91
|
: compact;
|
|
79
92
|
}
|
|
80
93
|
|
|
81
|
-
function channelFor(
|
|
94
|
+
function channelFor(
|
|
95
|
+
message: Pick<ActorMessage, "to">,
|
|
96
|
+
): ActorInspectorPreview["channel"] {
|
|
82
97
|
if (message.to.startsWith("room:")) return "room";
|
|
83
|
-
if (message.to === "coordinator" || message.to.startsWith("session:"))
|
|
98
|
+
if (message.to === "coordinator" || message.to.startsWith("session:"))
|
|
99
|
+
return "broadcast";
|
|
84
100
|
return "direct";
|
|
85
101
|
}
|
|
86
102
|
|
|
@@ -88,16 +104,27 @@ function previewFromMessage(
|
|
|
88
104
|
run: string,
|
|
89
105
|
message: Record<string, unknown>,
|
|
90
106
|
timestamp: string,
|
|
107
|
+
displayNames: Record<string, string> = {},
|
|
91
108
|
): ActorInspectorPreview | undefined {
|
|
92
109
|
const to = typeof message.to === "string" ? message.to : undefined;
|
|
93
110
|
const type = typeof message.type === "string" ? message.type : undefined;
|
|
94
111
|
if (!to || !type) return undefined;
|
|
95
112
|
const from = typeof message.from === "string" ? message.from : undefined;
|
|
96
|
-
const summary =
|
|
113
|
+
const summary =
|
|
114
|
+
typeof message.summary === "string" ? message.summary : undefined;
|
|
115
|
+
const body = asRecord(message.body);
|
|
116
|
+
const display = from
|
|
117
|
+
? typeof body.display === "string" && body.display.trim()
|
|
118
|
+
? body.display.trim()
|
|
119
|
+
: displayNames[from]
|
|
120
|
+
: undefined;
|
|
97
121
|
return {
|
|
98
|
-
...(previewValue(message.body)
|
|
122
|
+
...(previewValue(message.body)
|
|
123
|
+
? { body_preview: previewValue(message.body) }
|
|
124
|
+
: {}),
|
|
99
125
|
channel: channelFor({ to }),
|
|
100
126
|
...(from ? { from } : {}),
|
|
127
|
+
...(display ? { from_display: display } : {}),
|
|
101
128
|
run,
|
|
102
129
|
...(summary ? { summary } : {}),
|
|
103
130
|
timestamp,
|
|
@@ -106,30 +133,71 @@ function previewFromMessage(
|
|
|
106
133
|
};
|
|
107
134
|
}
|
|
108
135
|
|
|
109
|
-
function
|
|
136
|
+
function readRoomRosterRecords(
|
|
137
|
+
stateDir: string,
|
|
138
|
+
room: string,
|
|
139
|
+
): Record<string, Record<string, unknown>> {
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(
|
|
142
|
+
fs.readFileSync(path.join(stateDir, "rooms", room, "roster.json"), "utf8"),
|
|
143
|
+
) as Record<string, Record<string, unknown>>;
|
|
144
|
+
} catch {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function memberDisplay(_address: string, member: Record<string, unknown>): string | undefined {
|
|
150
|
+
const display = typeof member.display === "string" ? member.display.trim() : "";
|
|
151
|
+
return display || undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readRoomDisplayNames(stateDir: string, room: string): Record<string, string> {
|
|
155
|
+
const roster = readRoomRosterRecords(stateDir, room);
|
|
156
|
+
return Object.fromEntries(
|
|
157
|
+
Object.entries(roster).flatMap(([address, member]) => {
|
|
158
|
+
const display = memberDisplay(address, member);
|
|
159
|
+
return display ? [[address, display]] : [];
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readRoomPreviews(
|
|
165
|
+
run: string,
|
|
166
|
+
stateDir: string,
|
|
167
|
+
limitPerRun?: number,
|
|
168
|
+
): ActorInspectorPreview[] {
|
|
110
169
|
const roomsDir = path.join(stateDir, "rooms");
|
|
111
170
|
try {
|
|
112
|
-
|
|
171
|
+
const previews = fs
|
|
113
172
|
.readdirSync(roomsDir, { withFileTypes: true })
|
|
114
173
|
.filter((entry) => entry.isDirectory())
|
|
115
|
-
.flatMap((entry) =>
|
|
116
|
-
|
|
174
|
+
.flatMap((entry) => {
|
|
175
|
+
const displayNames = readRoomDisplayNames(stateDir, entry.name);
|
|
176
|
+
return readJsonLines(path.join(roomsDir, entry.name, "messages.jsonl"))
|
|
117
177
|
.map((message) =>
|
|
118
178
|
previewFromMessage(
|
|
119
179
|
run,
|
|
120
180
|
message,
|
|
121
181
|
String(message.received_at ?? message.timestamp ?? ""),
|
|
182
|
+
displayNames,
|
|
122
183
|
),
|
|
123
184
|
)
|
|
124
|
-
.filter((preview): preview is ActorInspectorPreview =>
|
|
125
|
-
|
|
185
|
+
.filter((preview): preview is ActorInspectorPreview =>
|
|
186
|
+
Boolean(preview),
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
const limit = Number.isFinite(limitPerRun) ? Math.max(0, Number(limitPerRun)) : undefined;
|
|
190
|
+
return limit === undefined ? previews : previews.slice(-limit);
|
|
126
191
|
} catch (error) {
|
|
127
192
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
128
193
|
return [];
|
|
129
194
|
}
|
|
130
195
|
}
|
|
131
196
|
|
|
132
|
-
function readInboxPreviews(
|
|
197
|
+
function readInboxPreviews(
|
|
198
|
+
run: string,
|
|
199
|
+
stateDir: string,
|
|
200
|
+
): ActorInspectorPreview[] {
|
|
133
201
|
return readJsonLines(path.join(stateDir, "inbox.jsonl"))
|
|
134
202
|
.map((message) =>
|
|
135
203
|
previewFromMessage(
|
|
@@ -141,7 +209,10 @@ function readInboxPreviews(run: string, stateDir: string): ActorInspectorPreview
|
|
|
141
209
|
.filter((preview): preview is ActorInspectorPreview => Boolean(preview));
|
|
142
210
|
}
|
|
143
211
|
|
|
144
|
-
function readOutboxPreviews(
|
|
212
|
+
function readOutboxPreviews(
|
|
213
|
+
run: string,
|
|
214
|
+
stateDir: string,
|
|
215
|
+
): ActorInspectorPreview[] {
|
|
145
216
|
return readJsonLines(path.join(stateDir, "outbox.jsonl"))
|
|
146
217
|
.map((event) => {
|
|
147
218
|
const message = asRecord(event.message ?? event);
|
|
@@ -169,6 +240,25 @@ function matchesOwner(stateDir: string, ownerId: string | undefined): boolean {
|
|
|
169
240
|
return ownerId === undefined || getRunOwnerId(stateDir) === ownerId;
|
|
170
241
|
}
|
|
171
242
|
|
|
243
|
+
function matchesPreviewFilter(
|
|
244
|
+
preview: ActorInspectorPreview,
|
|
245
|
+
options: ActorInspectorPreviewReadOptions,
|
|
246
|
+
): boolean {
|
|
247
|
+
if (options.channels?.length && !options.channels.includes(preview.channel)) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
const mention = options.mention?.trim().toLowerCase();
|
|
251
|
+
if (!mention) return true;
|
|
252
|
+
return [
|
|
253
|
+
preview.from,
|
|
254
|
+
preview.from_display,
|
|
255
|
+
preview.to,
|
|
256
|
+
preview.type,
|
|
257
|
+
preview.summary,
|
|
258
|
+
preview.body_preview,
|
|
259
|
+
].some((value) => value?.toLowerCase().includes(mention));
|
|
260
|
+
}
|
|
261
|
+
|
|
172
262
|
export function readActorInspectorPreviews(
|
|
173
263
|
stateRoot = Paths.getRunStateRoot(),
|
|
174
264
|
limit = 8,
|
|
@@ -182,20 +272,23 @@ export function readActorInspectorPreviews(
|
|
|
182
272
|
const stateDir = path.join(stateRoot, entry.name);
|
|
183
273
|
if (!matchesOwner(stateDir, options.ownerId)) return [];
|
|
184
274
|
return [
|
|
185
|
-
...readRoomPreviews(entry.name, stateDir),
|
|
275
|
+
...readRoomPreviews(entry.name, stateDir, options.roomLimitPerRun),
|
|
186
276
|
...readInboxPreviews(entry.name, stateDir),
|
|
187
277
|
...readOutboxPreviews(entry.name, stateDir),
|
|
188
278
|
];
|
|
189
279
|
})
|
|
190
280
|
.filter((preview) => preview.timestamp)
|
|
191
281
|
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
192
|
-
const currentRun = options.currentRunOnly
|
|
282
|
+
const currentRun = options.currentRunOnly
|
|
283
|
+
? previews.at(-1)?.run
|
|
284
|
+
: undefined;
|
|
193
285
|
return previews
|
|
194
286
|
.filter((preview) => !currentRun || preview.run === currentRun)
|
|
287
|
+
.filter((preview) => matchesPreviewFilter(preview, options))
|
|
195
288
|
.map((preview, index) => ({
|
|
196
289
|
...preview,
|
|
197
290
|
sequence: index + 1,
|
|
198
|
-
stripe: index % 2 ===
|
|
291
|
+
stripe: index % 2 === 1,
|
|
199
292
|
}))
|
|
200
293
|
.slice(-Math.max(1, limit));
|
|
201
294
|
} catch (error) {
|
|
@@ -210,9 +303,10 @@ function shorten(
|
|
|
210
303
|
options: { preserveSpaces?: boolean } = { preserveSpaces: true },
|
|
211
304
|
): string {
|
|
212
305
|
if (!value) return "-";
|
|
213
|
-
const compact =
|
|
214
|
-
|
|
215
|
-
|
|
306
|
+
const compact =
|
|
307
|
+
options.preserveSpaces === false
|
|
308
|
+
? value.replaceAll(/\s+/g, "_")
|
|
309
|
+
: value.replaceAll(/\s+/g, " ").trim();
|
|
216
310
|
if (maxLength <= 1) return compact.slice(0, Math.max(0, maxLength));
|
|
217
311
|
return compact.length > maxLength
|
|
218
312
|
? `${compact.slice(0, Math.max(0, maxLength - 1))}…`
|
|
@@ -233,14 +327,21 @@ function roomName(address: string): string | undefined {
|
|
|
233
327
|
return room ? room[1] : undefined;
|
|
234
328
|
}
|
|
235
329
|
|
|
330
|
+
function routeActorText(preview: ActorInspectorPreview): string {
|
|
331
|
+
return preview.from_display || actorName(preview.from);
|
|
332
|
+
}
|
|
333
|
+
|
|
236
334
|
function routeText(preview: ActorInspectorPreview): string {
|
|
237
|
-
const actor =
|
|
335
|
+
const actor = routeActorText(preview);
|
|
238
336
|
if (preview.channel === "room") return `${actor} # all`;
|
|
239
337
|
if (preview.channel === "broadcast") return `${actor} ⇢ ${preview.to}`;
|
|
240
338
|
return `${actor} → ${actorName(preview.to)}`;
|
|
241
339
|
}
|
|
242
340
|
|
|
243
|
-
function style(
|
|
341
|
+
function style(
|
|
342
|
+
styleFn: ((text: string) => string) | undefined,
|
|
343
|
+
text: string,
|
|
344
|
+
): string {
|
|
244
345
|
return styleFn ? styleFn(text) : text;
|
|
245
346
|
}
|
|
246
347
|
|
|
@@ -248,57 +349,47 @@ function previewText(preview: ActorInspectorPreview): string {
|
|
|
248
349
|
return preview.summary || preview.body_preview || "-";
|
|
249
350
|
}
|
|
250
351
|
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return value.replaceAll(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function charDisplayWidth(char: string): number {
|
|
260
|
-
const code = char.codePointAt(0) ?? 0;
|
|
261
|
-
if (code === 0) return 0;
|
|
262
|
-
if (code < 32 || (code >= 0x7f && code < 0xa0)) return 0;
|
|
263
|
-
if (
|
|
264
|
-
code >= 0x1100 &&
|
|
265
|
-
(code <= 0x115f ||
|
|
266
|
-
code === 0x2329 ||
|
|
267
|
-
code === 0x232a ||
|
|
268
|
-
(code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
|
|
269
|
-
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
270
|
-
(code >= 0xf900 && code <= 0xfaff) ||
|
|
271
|
-
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
272
|
-
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
273
|
-
(code >= 0xff00 && code <= 0xff60) ||
|
|
274
|
-
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
275
|
-
(code >= 0x1f300 && code <= 0x1faff))
|
|
276
|
-
) return 2;
|
|
277
|
-
return 1;
|
|
352
|
+
function propertyValue(value: unknown): string {
|
|
353
|
+
if (value === undefined) return "";
|
|
354
|
+
if (typeof value === "string") return value;
|
|
355
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
356
|
+
return JSON.stringify(value);
|
|
278
357
|
}
|
|
279
358
|
|
|
280
359
|
function displayWidth(value: string): number {
|
|
281
|
-
return
|
|
360
|
+
return visibleWidth(value);
|
|
282
361
|
}
|
|
283
362
|
|
|
363
|
+
const lineSegmenter = new Intl.Segmenter();
|
|
364
|
+
|
|
284
365
|
function boundedLine(value: string, width: number): string {
|
|
285
|
-
if (
|
|
286
|
-
if (
|
|
366
|
+
if (width <= 0) return "";
|
|
367
|
+
if (visibleWidth(value) <= width) return value;
|
|
368
|
+
const ellipsis = "…";
|
|
369
|
+
const ellipsisWidth = visibleWidth(ellipsis);
|
|
370
|
+
if (width <= ellipsisWidth) return ellipsis.slice(0, width);
|
|
287
371
|
let output = "";
|
|
288
372
|
let used = 0;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
373
|
+
const maxTextWidth = width - ellipsisWidth;
|
|
374
|
+
for (const { segment } of lineSegmenter.segment(value)) {
|
|
375
|
+
const segmentWidth = visibleWidth(segment);
|
|
376
|
+
if (used + segmentWidth > maxTextWidth) break;
|
|
377
|
+
output += segment;
|
|
378
|
+
used += segmentWidth;
|
|
294
379
|
}
|
|
295
|
-
return `${output}
|
|
380
|
+
return `${output}${ellipsis}`;
|
|
296
381
|
}
|
|
297
382
|
|
|
298
|
-
function padLine(
|
|
383
|
+
function padLine(
|
|
384
|
+
plain: string,
|
|
385
|
+
rendered: string,
|
|
386
|
+
width: number,
|
|
387
|
+
styles: ActorInspectorWidgetStyle,
|
|
388
|
+
): string {
|
|
299
389
|
const boundedPlain = boundedLine(plain, width);
|
|
300
|
-
const visible =
|
|
301
|
-
|
|
390
|
+
const visible =
|
|
391
|
+
boundedPlain === plain ? rendered : style(styles.preview, boundedPlain);
|
|
392
|
+
const padding = Math.max(0, width - visibleWidth(boundedPlain));
|
|
302
393
|
return `${visible}${" ".repeat(padding)}`;
|
|
303
394
|
}
|
|
304
395
|
|
|
@@ -313,15 +404,23 @@ function renderCompactInspectorEntry(
|
|
|
313
404
|
): string[] {
|
|
314
405
|
const separator = " ";
|
|
315
406
|
const prefix = " ";
|
|
316
|
-
const
|
|
407
|
+
const suffix = " ";
|
|
408
|
+
const contentWidth = Math.max(8, width - prefix.length - suffix.length);
|
|
317
409
|
const sequence = String(preview.sequence ?? 0).padStart(sequenceWidth, " ");
|
|
318
|
-
const sequencePrefix = `${sequence}
|
|
410
|
+
const sequencePrefix = `${sequence}${separator}`;
|
|
319
411
|
const route = routeText(preview);
|
|
320
|
-
const routePadding = " ".repeat(
|
|
321
|
-
|
|
412
|
+
const routePadding = " ".repeat(
|
|
413
|
+
Math.max(0, routeWidth - displayWidth(route)),
|
|
414
|
+
);
|
|
415
|
+
const typePadding = " ".repeat(
|
|
416
|
+
Math.max(0, typeWidth - displayWidth(preview.type)),
|
|
417
|
+
);
|
|
322
418
|
const headline = previewText(preview);
|
|
323
419
|
const lead = `${sequencePrefix}${route}${routePadding}${separator}${preview.type}${typePadding}${separator}`;
|
|
324
|
-
const visibleHeadline = boundedLine(
|
|
420
|
+
const visibleHeadline = boundedLine(
|
|
421
|
+
headline,
|
|
422
|
+
Math.max(0, contentWidth - displayWidth(lead)),
|
|
423
|
+
);
|
|
325
424
|
const plain = `${lead}${visibleHeadline}`;
|
|
326
425
|
const rendered = [
|
|
327
426
|
style(styles.muted, sequencePrefix),
|
|
@@ -333,57 +432,188 @@ function renderCompactInspectorEntry(
|
|
|
333
432
|
separator,
|
|
334
433
|
style(styles.preview, visibleHeadline),
|
|
335
434
|
].join("");
|
|
336
|
-
const line = `${prefix}${padLine(plain, rendered, contentWidth, styles)}`;
|
|
435
|
+
const line = `${prefix}${padLine(plain, rendered, contentWidth, styles)}${suffix}`;
|
|
337
436
|
if (stripe && styles.stripe) return [styles.stripe(line)];
|
|
338
437
|
if (!stripe && styles.stripeAlt) return [styles.stripeAlt(line)];
|
|
339
438
|
return [line];
|
|
340
439
|
}
|
|
341
440
|
|
|
342
|
-
function
|
|
441
|
+
function renderInspectorEntry(
|
|
343
442
|
preview: ActorInspectorPreview,
|
|
344
443
|
width: number,
|
|
345
444
|
sequenceWidth: number,
|
|
346
|
-
|
|
445
|
+
routeWidth: number,
|
|
446
|
+
typeWidth: number,
|
|
447
|
+
summaryWidth: number,
|
|
347
448
|
styles: ActorInspectorWidgetStyle,
|
|
348
449
|
stripe: boolean,
|
|
349
450
|
): string[] {
|
|
350
451
|
const separator = " ";
|
|
351
452
|
const prefix = " ";
|
|
352
|
-
const
|
|
453
|
+
const suffix = " ";
|
|
454
|
+
const contentWidth = Math.max(8, width - prefix.length - suffix.length);
|
|
353
455
|
const sequence = String(preview.sequence ?? 0).padStart(sequenceWidth, " ");
|
|
354
|
-
const sequencePrefix = `${sequence}
|
|
355
|
-
const detailSequencePrefix = `${" ".repeat(sequenceWidth)} `;
|
|
456
|
+
const sequencePrefix = `${sequence}${separator}`;
|
|
356
457
|
const route = routeText(preview);
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
const
|
|
365
|
-
const headerPlain = `${headerLead}${visibleHeadline}`;
|
|
366
|
-
const detailPlain = `${detailLead}${visibleDetail}`;
|
|
367
|
-
const header = [
|
|
458
|
+
const type = preview.type;
|
|
459
|
+
const summary = preview.summary?.trim() ?? "";
|
|
460
|
+
const body = preview.body_preview?.trim() || (!summary ? previewText(preview) : "-");
|
|
461
|
+
const visibleRoute = boundedLine(route, routeWidth);
|
|
462
|
+
const visibleType = boundedLine(type, typeWidth);
|
|
463
|
+
const boundedRoutePadding = " ".repeat(Math.max(0, routeWidth - displayWidth(visibleRoute)));
|
|
464
|
+
const boundedTypePadding = " ".repeat(Math.max(0, typeWidth - displayWidth(visibleType)));
|
|
465
|
+
const leadParts = [
|
|
368
466
|
style(styles.muted, sequencePrefix),
|
|
369
|
-
style(styles.target,
|
|
370
|
-
|
|
467
|
+
style(styles.target, visibleRoute),
|
|
468
|
+
boundedRoutePadding,
|
|
371
469
|
separator,
|
|
372
|
-
style(styles.
|
|
373
|
-
|
|
374
|
-
const detailLine = [
|
|
375
|
-
style(styles.muted, detailSequencePrefix),
|
|
376
|
-
style(styles.type, preview.type),
|
|
377
|
-
typePadding,
|
|
470
|
+
style(styles.type, visibleType),
|
|
471
|
+
boundedTypePadding,
|
|
378
472
|
separator,
|
|
379
|
-
style(styles.preview, visibleDetail),
|
|
380
|
-
].join("");
|
|
381
|
-
const lines = [
|
|
382
|
-
`${prefix}${padLine(headerPlain, header, contentWidth, styles)}`,
|
|
383
|
-
`${prefix}${padLine(detailPlain, detailLine, contentWidth, styles)}`,
|
|
384
473
|
];
|
|
385
|
-
|
|
386
|
-
if (
|
|
474
|
+
let lead = `${sequencePrefix}${visibleRoute}${boundedRoutePadding}${separator}${visibleType}${boundedTypePadding}${separator}`;
|
|
475
|
+
if (summary) {
|
|
476
|
+
const visibleSummary = boundedLine(summary, summaryWidth);
|
|
477
|
+
const summaryPadding = " ".repeat(Math.max(0, summaryWidth - displayWidth(visibleSummary)));
|
|
478
|
+
lead += `${visibleSummary}${summaryPadding}${separator}`;
|
|
479
|
+
leadParts.push(style(styles.preview, visibleSummary), summaryPadding, separator);
|
|
480
|
+
}
|
|
481
|
+
const visibleBody = boundedLine(body, Math.max(0, contentWidth - displayWidth(lead)));
|
|
482
|
+
const plain = `${lead}${visibleBody}`;
|
|
483
|
+
const rendered = `${leadParts.join("")}${style(styles.preview, visibleBody)}`;
|
|
484
|
+
const line = `${prefix}${padLine(plain, rendered, contentWidth, styles)}${suffix}`;
|
|
485
|
+
if (stripe && styles.stripe) return [styles.stripe(line)];
|
|
486
|
+
if (!stripe && styles.stripeAlt) return [styles.stripeAlt(line)];
|
|
487
|
+
return [line];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function readActorInspectorRoster(
|
|
491
|
+
stateRoot = Paths.getRunStateRoot(),
|
|
492
|
+
run: string,
|
|
493
|
+
room = "main",
|
|
494
|
+
): ActorInspectorRosterMember[] {
|
|
495
|
+
const stateDir = path.join(stateRoot, run);
|
|
496
|
+
const roster = readRoomRosterRecords(stateDir, room);
|
|
497
|
+
return Object.entries(roster).map(([address, member]) => ({
|
|
498
|
+
address,
|
|
499
|
+
...(memberDisplay(address, member)
|
|
500
|
+
? { display: memberDisplay(address, member) }
|
|
501
|
+
: {}),
|
|
502
|
+
...(typeof member.role === "string" ? { role: member.role } : {}),
|
|
503
|
+
...(typeof member.status === "string" ? { status: member.status } : {}),
|
|
504
|
+
}));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function rosterRoleText(role: string | undefined): string | undefined {
|
|
508
|
+
const cleaned = role?.replaceAll(/\s*\([^)]*\)\s*$/g, "").trim().toLowerCase();
|
|
509
|
+
if (!cleaned || cleaned === "actor") return undefined;
|
|
510
|
+
return cleaned.replaceAll(/\s+/g, "-");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function rosterMemberText(member: ActorInspectorRosterMember): string {
|
|
514
|
+
const name = member.display || actorName(member.address);
|
|
515
|
+
const role = rosterRoleText(member.role);
|
|
516
|
+
return role ? `${role}/${name}` : name;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function isRosterMemberActive(member: ActorInspectorRosterMember): boolean {
|
|
520
|
+
const status = member.status?.trim().toLowerCase();
|
|
521
|
+
return !status || status === "present" || status === "active" || status === "running";
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function renderInspectorRosterLine(
|
|
525
|
+
members: ActorInspectorRosterMember[],
|
|
526
|
+
width = 80,
|
|
527
|
+
styles: ActorInspectorWidgetStyle = {},
|
|
528
|
+
): string | undefined {
|
|
529
|
+
return renderInspectorRosterPanel(members, width, styles)?.[0];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function renderInspectorRosterPanel(
|
|
533
|
+
members: ActorInspectorRosterMember[],
|
|
534
|
+
width = 80,
|
|
535
|
+
styles: ActorInspectorWidgetStyle = {},
|
|
536
|
+
): string[] | undefined {
|
|
537
|
+
if (members.length === 0) return undefined;
|
|
538
|
+
const safeWidth = Math.max(1, width);
|
|
539
|
+
const innerWidth = Math.max(1, safeWidth - 2);
|
|
540
|
+
const prefix = `roster ${members.length}: `;
|
|
541
|
+
const tokens = members.map((member) => ({
|
|
542
|
+
active: isRosterMemberActive(member),
|
|
543
|
+
text: rosterMemberText(member),
|
|
544
|
+
}));
|
|
545
|
+
const lines: string[] = [];
|
|
546
|
+
let plain = prefix;
|
|
547
|
+
let rendered = style(styles.muted, prefix);
|
|
548
|
+
const flush = () => {
|
|
549
|
+
const visible = boundedLine(plain, innerWidth);
|
|
550
|
+
const line = visible === plain ? rendered : style(styles.muted, visible);
|
|
551
|
+
lines.push(` ${line}${" ".repeat(Math.max(0, innerWidth - displayWidth(visible)))} `);
|
|
552
|
+
};
|
|
553
|
+
for (const token of tokens) {
|
|
554
|
+
const separator = plain === prefix ? "" : ", ";
|
|
555
|
+
const nextPlain = `${plain}${separator}${token.text}`;
|
|
556
|
+
const renderedToken = style(token.active ? styles.target : styles.muted, token.text);
|
|
557
|
+
if (plain !== prefix && displayWidth(nextPlain) > innerWidth) {
|
|
558
|
+
flush();
|
|
559
|
+
plain = ` ${token.text}`;
|
|
560
|
+
rendered = `${style(styles.muted, " ")}${renderedToken}`;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
plain = nextPlain;
|
|
564
|
+
rendered = `${rendered}${style(styles.muted, separator)}${renderedToken}`;
|
|
565
|
+
}
|
|
566
|
+
flush();
|
|
567
|
+
return lines;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export function renderInspectorItemView(
|
|
571
|
+
previews: ActorInspectorPreview[],
|
|
572
|
+
width = 80,
|
|
573
|
+
styles: ActorInspectorWidgetStyle = {},
|
|
574
|
+
options: ActorInspectorItemViewOptions,
|
|
575
|
+
): string[] | undefined {
|
|
576
|
+
const preview = previews.find((item) => item.sequence === options.sequence);
|
|
577
|
+
if (!preview) return undefined;
|
|
578
|
+
const safeWidth = Math.max(1, width);
|
|
579
|
+
const orderedKeys = [
|
|
580
|
+
"channel",
|
|
581
|
+
"run",
|
|
582
|
+
"from",
|
|
583
|
+
"from_display",
|
|
584
|
+
"to",
|
|
585
|
+
"type",
|
|
586
|
+
"summary",
|
|
587
|
+
"body_preview",
|
|
588
|
+
"timestamp",
|
|
589
|
+
"stripe",
|
|
590
|
+
] as const;
|
|
591
|
+
const entries = orderedKeys
|
|
592
|
+
.filter((key) => preview[key] !== undefined)
|
|
593
|
+
.map((key) => [key, propertyValue(preview[key])] as const);
|
|
594
|
+
const keyWidth = Math.max(1, ...entries.map(([key]) => displayWidth(key)));
|
|
595
|
+
const sequenceText = String(preview.sequence ?? options.sequence);
|
|
596
|
+
const sequencePadding = " ".repeat(Math.max(0, keyWidth - displayWidth(sequenceText)));
|
|
597
|
+
const headerSeparator = " ";
|
|
598
|
+
const route = routeText(preview);
|
|
599
|
+
const visibleRoute = boundedLine(
|
|
600
|
+
route,
|
|
601
|
+
Math.max(0, safeWidth - keyWidth - headerSeparator.length),
|
|
602
|
+
);
|
|
603
|
+
const headerPlain = `${sequenceText}${sequencePadding}${headerSeparator}${visibleRoute}`;
|
|
604
|
+
const header = `${style(styles.muted, sequenceText)}${sequencePadding}${headerSeparator}${style(styles.target, visibleRoute)}`;
|
|
605
|
+
const headerPadding = Math.max(0, safeWidth - visibleWidth(headerPlain));
|
|
606
|
+
const lines = [`${header}${" ".repeat(headerPadding)}`, ""];
|
|
607
|
+
for (const [key, value] of entries) {
|
|
608
|
+
const keyPadding = " ".repeat(Math.max(0, keyWidth - displayWidth(key)));
|
|
609
|
+
const separator = " ";
|
|
610
|
+
const valueWidth = Math.max(0, safeWidth - keyWidth - separator.length);
|
|
611
|
+
const visibleValue = boundedLine(value, valueWidth);
|
|
612
|
+
const plain = `${key}${keyPadding}${separator}${visibleValue}`;
|
|
613
|
+
const rendered = `${style(styles.muted, key)}${keyPadding}${separator}${style(styles.preview, visibleValue)}`;
|
|
614
|
+
const padding = Math.max(0, safeWidth - visibleWidth(plain));
|
|
615
|
+
lines.push(`${rendered}${" ".repeat(padding)}`);
|
|
616
|
+
}
|
|
387
617
|
return lines;
|
|
388
618
|
}
|
|
389
619
|
|
|
@@ -395,32 +625,58 @@ export function renderInspectorWidget(
|
|
|
395
625
|
): string[] | undefined {
|
|
396
626
|
if (previews.length === 0) return undefined;
|
|
397
627
|
const safeWidth = Math.max(1, width);
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
.
|
|
402
|
-
|
|
403
|
-
stripe: preview.stripe ?? index % 2 === 0,
|
|
404
|
-
}))
|
|
405
|
-
.slice(-visibleLimit);
|
|
628
|
+
void options;
|
|
629
|
+
const visible = previews.map((preview, index) => ({
|
|
630
|
+
preview: { ...preview, sequence: preview.sequence ?? index + 1 },
|
|
631
|
+
stripe: preview.stripe ?? index % 2 === 1,
|
|
632
|
+
}));
|
|
406
633
|
const sequenceWidth = Math.max(
|
|
407
634
|
1,
|
|
408
635
|
...visible.map(({ preview }) => String(preview.sequence ?? 0).length),
|
|
409
636
|
);
|
|
410
637
|
const lines: string[] = [];
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
638
|
+
const separatorWidth = 2;
|
|
639
|
+
const sequencePrefixWidth = sequenceWidth + separatorWidth;
|
|
640
|
+
const fixedSeparatorsWidth = separatorWidth * 3;
|
|
641
|
+
const availableForColumns = Math.max(
|
|
642
|
+
0,
|
|
643
|
+
safeWidth - 1 - sequencePrefixWidth - fixedSeparatorsWidth,
|
|
644
|
+
);
|
|
645
|
+
const naturalRouteWidth = Math.max(
|
|
646
|
+
...visible.map(({ preview }) => displayWidth(routeText(preview))),
|
|
647
|
+
);
|
|
648
|
+
const naturalTypeWidth = Math.max(
|
|
649
|
+
...visible.map(({ preview }) => displayWidth(preview.type)),
|
|
421
650
|
);
|
|
651
|
+
const routeWidth = Math.min(
|
|
652
|
+
naturalRouteWidth,
|
|
653
|
+
Math.max(4, Math.floor(availableForColumns * 0.35)),
|
|
654
|
+
);
|
|
655
|
+
const typeWidth = Math.min(
|
|
656
|
+
naturalTypeWidth,
|
|
657
|
+
Math.max(4, Math.floor(availableForColumns * 0.25)),
|
|
658
|
+
);
|
|
659
|
+
const messageWidth = Math.max(0, availableForColumns - routeWidth - typeWidth);
|
|
660
|
+
const summaryWidths = visible
|
|
661
|
+
.map(({ preview }) => preview.summary?.trim())
|
|
662
|
+
.filter((summary): summary is string => Boolean(summary))
|
|
663
|
+
.map((summary) => displayWidth(summary));
|
|
664
|
+
const summaryWidth = summaryWidths.length
|
|
665
|
+
? Math.min(Math.max(...summaryWidths), Math.max(1, Math.floor(messageWidth * 0.5)))
|
|
666
|
+
: 0;
|
|
422
667
|
for (const { preview, stripe } of visible) {
|
|
423
|
-
lines.push(
|
|
668
|
+
lines.push(
|
|
669
|
+
...renderInspectorEntry(
|
|
670
|
+
preview,
|
|
671
|
+
safeWidth,
|
|
672
|
+
sequenceWidth,
|
|
673
|
+
routeWidth,
|
|
674
|
+
typeWidth,
|
|
675
|
+
summaryWidth,
|
|
676
|
+
styles,
|
|
677
|
+
stripe,
|
|
678
|
+
),
|
|
679
|
+
);
|
|
424
680
|
}
|
|
425
681
|
return lines;
|
|
426
682
|
}
|