@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.
Files changed (85) hide show
  1. package/AGENTS.md +5 -3
  2. package/BACKLOG.md +54 -29
  3. package/CHANGELOG.md +18 -2
  4. package/README.md +184 -300
  5. package/docs/actor-messages.md +6 -2
  6. package/docs/async-runs.md +3 -5
  7. package/docs/command-templates.md +2 -0
  8. package/docs/recipe-library.md +3 -0
  9. package/docs/task-first-recipes.md +29 -0
  10. package/docs/template-recipes.md +9 -14
  11. package/index.ts +158 -34
  12. package/lib/actor-inspector-tui.ts +374 -118
  13. package/lib/actor-rooms.ts +222 -24
  14. package/lib/async-runs.ts +59 -1
  15. package/lib/execution.ts +17 -0
  16. package/lib/file-state.ts +2 -1
  17. package/lib/observability.ts +82 -2
  18. package/lib/prompts.ts +2 -2
  19. package/lib/recipe-discovery.ts +86 -6
  20. package/lib/recipe-migration.ts +0 -2
  21. package/lib/recipe-references.ts +43 -10
  22. package/lib/temp.ts +55 -2
  23. package/lib/tools.ts +99 -11
  24. package/package.json +1 -1
  25. package/recipes/coordinator-locker.json +0 -1
  26. package/recipes/lens-swarm.json +0 -1
  27. package/recipes/music-player.json +0 -1
  28. package/recipes/pipeline-architect-coordinator.json +0 -1
  29. package/recipes/pipeline-artifact-bundle.json +0 -1
  30. package/recipes/pipeline-artifact-report.json +0 -1
  31. package/recipes/pipeline-artifact-write.json +0 -1
  32. package/recipes/pipeline-async-run-ops.json +0 -1
  33. package/recipes/pipeline-checkpoint-continuation.json +0 -1
  34. package/recipes/pipeline-development-tasking.json +0 -1
  35. package/recipes/pipeline-docs-maintenance.json +0 -1
  36. package/recipes/pipeline-media-library.json +0 -1
  37. package/recipes/pipeline-quorum-review.json +0 -1
  38. package/recipes/pipeline-release-readiness.json +0 -1
  39. package/recipes/pipeline-release-summary.json +0 -1
  40. package/recipes/pipeline-repo-health.json +0 -1
  41. package/recipes/pipeline-research-synthesis.json +0 -1
  42. package/recipes/pipeline-review-readiness.json +0 -1
  43. package/recipes/pipeline-room-swarm.json +48 -0
  44. package/recipes/subagent-artifact.json +0 -1
  45. package/recipes/subagent-checkpoint.json +0 -1
  46. package/recipes/subagent-conflict-report.json +0 -1
  47. package/recipes/subagent-contradiction-map.json +0 -1
  48. package/recipes/subagent-critic.json +0 -1
  49. package/recipes/subagent-evidence-map.json +0 -1
  50. package/recipes/subagent-followup.json +0 -1
  51. package/recipes/subagent-judge.json +0 -1
  52. package/recipes/subagent-merge.json +0 -1
  53. package/recipes/subagent-message.json +0 -1
  54. package/recipes/subagent-normalize.json +0 -1
  55. package/recipes/subagent-plan.json +0 -1
  56. package/recipes/subagent-prompt.json +0 -1
  57. package/recipes/subagent-quorum.json +0 -1
  58. package/recipes/subagent-review-coordinator.json +0 -1
  59. package/recipes/subagent-review.json +0 -1
  60. package/recipes/subagent-task-card.json +0 -1
  61. package/recipes/subagent-tools.json +0 -1
  62. package/recipes/subagent-verify.json +0 -1
  63. package/recipes/subagents-prompts.json +0 -1
  64. package/recipes/utility-actor-message.json +0 -1
  65. package/recipes/utility-artifact-manifest.json +0 -1
  66. package/recipes/utility-artifact-write.json +0 -1
  67. package/recipes/utility-changelog-head.json +0 -1
  68. package/recipes/utility-changelog-section.json +0 -1
  69. package/recipes/utility-coordinator-lock-snapshot.json +0 -1
  70. package/recipes/utility-git-log.json +0 -1
  71. package/recipes/utility-git-status.json +0 -1
  72. package/recipes/utility-jsonl-tail.json +0 -1
  73. package/recipes/utility-markdown-index.json +0 -1
  74. package/recipes/utility-package-summary.json +0 -1
  75. package/recipes/utility-playlist-build.json +0 -1
  76. package/recipes/utility-playlist-scan.json +0 -1
  77. package/recipes/utility-run-ops-snapshot.json +0 -1
  78. package/recipes/utility-run-state-files.json +0 -1
  79. package/recipes/utility-run-summary.json +0 -1
  80. package/recipes/utility-skill-summary.json +0 -1
  81. package/recipes/utility-validate-recipe.json +0 -1
  82. package/recipes/utility-validation-wrapper.json +0 -1
  83. package/scripts/room-swarm.mjs +243 -0
  84. package/skills/actors/SKILL.md +25 -12
  85. 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 type ActorInspectorVerbosity = "compact" | "verbose";
38
+ export interface ActorInspectorRenderOptions {}
39
+
40
+ export interface ActorInspectorItemViewOptions {
41
+ sequence: number;
42
+ }
36
43
 
37
- export interface ActorInspectorRenderOptions {
38
- verbosity?: ActorInspectorVerbosity;
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(message: Pick<ActorMessage, "to">): ActorInspectorPreview["channel"] {
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:")) return "broadcast";
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 = typeof message.summary === "string" ? message.summary : undefined;
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) ? { body_preview: 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 readRoomPreviews(run: string, stateDir: string): ActorInspectorPreview[] {
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
- return fs
171
+ const previews = fs
113
172
  .readdirSync(roomsDir, { withFileTypes: true })
114
173
  .filter((entry) => entry.isDirectory())
115
- .flatMap((entry) =>
116
- readJsonLines(path.join(roomsDir, entry.name, "messages.jsonl"))
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 => Boolean(preview)),
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(run: string, stateDir: string): ActorInspectorPreview[] {
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(run: string, stateDir: string): ActorInspectorPreview[] {
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 ? previews.at(-1)?.run : undefined;
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 === 0,
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 = options.preserveSpaces === false
214
- ? value.replaceAll(/\s+/g, "_")
215
- : value.replaceAll(/\s+/g, " ").trim();
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 = actorName(preview.from);
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(styleFn: ((text: string) => string) | undefined, text: string): string {
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 detailText(preview: ActorInspectorPreview): string {
252
- return preview.body_preview || preview.summary || "-";
253
- }
254
-
255
- function stripAnsi(value: string): string {
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 Array.from(stripAnsi(value)).reduce((sum, char) => sum + charDisplayWidth(char), 0);
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 (displayWidth(value) <= width) return value;
286
- if (width <= 1) return "";
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
- for (const char of Array.from(value)) {
290
- const charWidth = charDisplayWidth(char);
291
- if (used + charWidth > width - 2) break;
292
- output += char;
293
- used += charWidth;
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(plain: string, rendered: string, width: number, styles: ActorInspectorWidgetStyle): string {
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 = boundedPlain === plain ? rendered : style(styles.preview, boundedPlain);
301
- const padding = Math.max(0, width - displayWidth(boundedPlain));
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 contentWidth = Math.max(8, width - prefix.length);
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(Math.max(0, routeWidth - route.length));
321
- const typePadding = " ".repeat(Math.max(0, typeWidth - preview.type.length));
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(headline, Math.max(0, contentWidth - lead.length));
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 renderVerboseInspectorEntry(
441
+ function renderInspectorEntry(
343
442
  preview: ActorInspectorPreview,
344
443
  width: number,
345
444
  sequenceWidth: number,
346
- labelWidth: number,
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 contentWidth = Math.max(8, width - prefix.length);
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 routePadding = " ".repeat(Math.max(0, labelWidth - route.length));
358
- const typePadding = " ".repeat(Math.max(0, labelWidth - preview.type.length));
359
- const headline = previewText(preview);
360
- const detail = detailText(preview);
361
- const headerLead = `${sequencePrefix}${route}${routePadding}${separator}`;
362
- const detailLead = `${detailSequencePrefix}${preview.type}${typePadding}${separator}`;
363
- const visibleHeadline = boundedLine(headline, Math.max(0, contentWidth - headerLead.length));
364
- const visibleDetail = boundedLine(detail, Math.max(0, contentWidth - detailLead.length));
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, route),
370
- routePadding,
467
+ style(styles.target, visibleRoute),
468
+ boundedRoutePadding,
371
469
  separator,
372
- style(styles.preview, visibleHeadline),
373
- ].join("");
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
- if (stripe && styles.stripe) return lines.map((line) => styles.stripe?.(line) ?? line);
386
- if (!stripe && styles.stripeAlt) return lines.map((line) => styles.stripeAlt?.(line) ?? line);
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
- const verbosity = options.verbosity ?? "verbose";
399
- const visibleLimit = verbosity === "compact" ? 12 : 6;
400
- const visible = previews
401
- .map((preview, index) => ({
402
- preview: { ...preview, sequence: preview.sequence ?? index + 1 },
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
- if (verbosity === "compact") {
412
- const routeWidth = Math.max(...visible.map(({ preview }) => routeText(preview).length));
413
- const typeWidth = Math.max(...visible.map(({ preview }) => preview.type.length));
414
- for (const { preview, stripe } of visible) {
415
- lines.push(...renderCompactInspectorEntry(preview, safeWidth, sequenceWidth, routeWidth, typeWidth, styles, stripe));
416
- }
417
- return lines;
418
- }
419
- const labelWidth = Math.max(
420
- ...visible.flatMap(({ preview }) => [routeText(preview).length, preview.type.length]),
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(...renderVerboseInspectorEntry(preview, safeWidth, sequenceWidth, labelWidth, styles, stripe));
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
  }