@llblab/pi-actors 0.17.1 → 0.19.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 (88) hide show
  1. package/AGENTS.md +6 -2
  2. package/BACKLOG.md +32 -26
  3. package/CHANGELOG.md +19 -3
  4. package/README.md +23 -8
  5. package/docs/actor-messages.md +5 -3
  6. package/docs/async-runs.md +3 -5
  7. package/docs/command-templates.md +2 -0
  8. package/docs/recipe-library.md +3 -1
  9. package/docs/task-first-recipes.md +29 -0
  10. package/docs/template-recipes.md +9 -14
  11. package/index.ts +111 -32
  12. package/lib/actor-inspector-tui.ts +192 -42
  13. package/lib/actor-rooms.ts +220 -26
  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 +1 -2
  26. package/recipes/lens-swarm.json +0 -1
  27. package/recipes/locker.json +45 -0
  28. package/recipes/music-player.json +0 -1
  29. package/recipes/pipeline-architect-coordinator.json +0 -1
  30. package/recipes/pipeline-artifact-bundle.json +0 -1
  31. package/recipes/pipeline-artifact-report.json +0 -1
  32. package/recipes/pipeline-artifact-write.json +0 -1
  33. package/recipes/pipeline-async-run-ops.json +0 -1
  34. package/recipes/pipeline-checkpoint-continuation.json +0 -1
  35. package/recipes/pipeline-development-tasking.json +0 -1
  36. package/recipes/pipeline-docs-maintenance.json +0 -1
  37. package/recipes/pipeline-media-library.json +0 -1
  38. package/recipes/pipeline-quorum-review.json +0 -1
  39. package/recipes/pipeline-release-readiness.json +0 -1
  40. package/recipes/pipeline-release-summary.json +0 -1
  41. package/recipes/pipeline-repo-health.json +0 -1
  42. package/recipes/pipeline-research-synthesis.json +0 -1
  43. package/recipes/pipeline-review-readiness.json +0 -1
  44. package/recipes/pipeline-room-swarm.json +3 -2
  45. package/recipes/subagent-artifact.json +0 -1
  46. package/recipes/subagent-checkpoint.json +0 -1
  47. package/recipes/subagent-conflict-report.json +0 -1
  48. package/recipes/subagent-contradiction-map.json +0 -1
  49. package/recipes/subagent-critic.json +0 -1
  50. package/recipes/subagent-evidence-map.json +0 -1
  51. package/recipes/subagent-followup.json +0 -1
  52. package/recipes/subagent-judge.json +0 -1
  53. package/recipes/subagent-merge.json +0 -1
  54. package/recipes/subagent-message.json +0 -1
  55. package/recipes/subagent-normalize.json +0 -1
  56. package/recipes/subagent-plan.json +0 -1
  57. package/recipes/subagent-prompt.json +0 -1
  58. package/recipes/subagent-quorum.json +0 -1
  59. package/recipes/subagent-review-coordinator.json +0 -1
  60. package/recipes/subagent-review.json +0 -1
  61. package/recipes/subagent-task-card.json +0 -1
  62. package/recipes/subagent-tools.json +0 -1
  63. package/recipes/subagent-verify.json +0 -1
  64. package/recipes/subagents-prompts.json +0 -1
  65. package/recipes/utility-actor-message.json +0 -1
  66. package/recipes/utility-artifact-manifest.json +0 -1
  67. package/recipes/utility-artifact-write.json +0 -1
  68. package/recipes/utility-changelog-head.json +0 -1
  69. package/recipes/utility-changelog-section.json +0 -1
  70. package/recipes/utility-coordinator-lock-snapshot.json +0 -1
  71. package/recipes/utility-git-log.json +0 -1
  72. package/recipes/utility-git-status.json +0 -1
  73. package/recipes/utility-jsonl-tail.json +0 -1
  74. package/recipes/utility-markdown-index.json +0 -1
  75. package/recipes/utility-package-summary.json +0 -1
  76. package/recipes/utility-playlist-build.json +0 -1
  77. package/recipes/utility-playlist-scan.json +0 -1
  78. package/recipes/utility-run-ops-snapshot.json +0 -1
  79. package/recipes/utility-run-state-files.json +0 -1
  80. package/recipes/utility-run-summary.json +0 -1
  81. package/recipes/utility-skill-summary.json +0 -1
  82. package/recipes/utility-validate-recipe.json +0 -1
  83. package/recipes/utility-validation-wrapper.json +0 -1
  84. package/scripts/coordinator.mjs +434 -0
  85. package/scripts/{coordinator-locker.mjs → locker.mjs} +23 -22
  86. package/skills/actors/SKILL.md +26 -12
  87. package/skills/swarm/SKILL.md +15 -1
  88. package/scripts/room-swarm.mjs +0 -244
package/index.ts CHANGED
@@ -50,7 +50,13 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
50
50
  let runStatusFrame = 0;
51
51
  let communicationWidgetVisible = false;
52
52
  let actorInspectorRows = 12;
53
+ let actorInspectorChannels:
54
+ | ActorInspectorTui.ActorInspectorPreview["channel"][]
55
+ | undefined;
56
+ let actorInspectorMention: string | undefined;
57
+ const actorInspectorRoomLimitPerRun = 6;
53
58
  let selectedInspectorSequence: number | undefined;
59
+ let recipeWatcherFailureNotified = false;
54
60
  const getRunOwnerId = (ctx: ExtensionContext): string =>
55
61
  ctx.sessionManager.getSessionId();
56
62
  const updateRunUi = (ctx: ExtensionContext, notify = false): void => {
@@ -64,40 +70,59 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
64
70
  ctx.ui.setWidget(
65
71
  "zz-pi-actors-comms",
66
72
  communicationWidgetVisible
67
- ? () => ({
68
- invalidate() {},
69
- render(width: number) {
70
- const previews = ActorInspectorTui.readActorInspectorPreviews(
71
- RUN_STATE_ROOT,
72
- actorInspectorRows,
73
- { ownerId, currentRunOnly: true },
74
- );
75
- const style = {
76
- actor: (text: string) => ctx.ui.theme.fg("accent", text),
77
- muted: (text: string) => ctx.ui.theme.fg("dim", text),
78
- preview: (text: string) => ctx.ui.theme.fg("text", text),
79
- stripe: (text: string) => text,
80
- stripeAlt: (text: string) =>
81
- ctx.ui.theme.bg("customMessageBg", text),
82
- target: (text: string) => ctx.ui.theme.fg("success", text),
83
- type: (text: string) => ctx.ui.theme.fg("warning", text),
84
- };
85
- return (
86
- (selectedInspectorSequence !== undefined
87
- ? ActorInspectorTui.renderInspectorItemView(
88
- previews,
73
+ ? () => {
74
+ const style = {
75
+ actor: (text: string) => ctx.ui.theme.fg("accent", text),
76
+ muted: (text: string) => ctx.ui.theme.fg("dim", text),
77
+ preview: (text: string) => ctx.ui.theme.fg("text", text),
78
+ stripe: (text: string) => text,
79
+ stripeAlt: (text: string) =>
80
+ ctx.ui.theme.bg("customMessageBg", text),
81
+ target: (text: string) => ctx.ui.theme.fg("success", text),
82
+ type: (text: string) => ctx.ui.theme.fg("warning", text),
83
+ };
84
+ return {
85
+ invalidate() {},
86
+ render(width: number) {
87
+ const previews = ActorInspectorTui.readActorInspectorPreviews(
88
+ RUN_STATE_ROOT,
89
+ actorInspectorRows,
90
+ {
91
+ channels: actorInspectorChannels,
92
+ currentRunOnly: true,
93
+ mention: actorInspectorMention,
94
+ ownerId,
95
+ roomLimitPerRun: actorInspectorRoomLimitPerRun,
96
+ },
97
+ );
98
+ const rows =
99
+ (selectedInspectorSequence !== undefined
100
+ ? ActorInspectorTui.renderInspectorItemView(
101
+ previews,
102
+ width,
103
+ style,
104
+ { sequence: selectedInspectorSequence },
105
+ )
106
+ : ActorInspectorTui.renderInspectorWidget(
107
+ previews,
108
+ width,
109
+ style,
110
+ )) ?? [];
111
+ const run = previews[0]?.run;
112
+ const roster = run
113
+ ? ActorInspectorTui.renderInspectorRosterPanel(
114
+ ActorInspectorTui.readActorInspectorRoster(
115
+ RUN_STATE_ROOT,
116
+ run,
117
+ ),
89
118
  width,
90
119
  style,
91
- { sequence: selectedInspectorSequence },
92
120
  )
93
- : ActorInspectorTui.renderInspectorWidget(
94
- previews,
95
- width,
96
- style,
97
- )) ?? []
98
- );
99
- },
100
- })
121
+ : undefined;
122
+ return roster ? [...roster, ...rows] : rows;
123
+ },
124
+ };
125
+ }
101
126
  : undefined,
102
127
  { placement: "belowEditor" },
103
128
  );
@@ -127,6 +152,12 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
127
152
  { deliverAs: "followUp", triggerTurn: true },
128
153
  );
129
154
  }
155
+ Observability.pruneRunObservationState(
156
+ observedRuns,
157
+ observedRunEventLines,
158
+ summary,
159
+ transitions.map((transition) => transition.run),
160
+ );
130
161
  for (const event of outboxEvents) {
131
162
  if (!Observability.shouldNotifyRunOutboxEvent(event)) continue;
132
163
  const text = Observability.formatRunOutboxMessage(event);
@@ -200,7 +231,16 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
200
231
  if (recipeReloadTimeout) clearTimeout(recipeReloadTimeout);
201
232
  recipeReloadTimeout = undefined;
202
233
  };
234
+ const notifyRecipeWatcherFailure = (ctx: ExtensionContext): void => {
235
+ if (recipeWatcherFailureNotified) return;
236
+ recipeWatcherFailureNotified = true;
237
+ ctx.ui.notify(
238
+ "Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.",
239
+ "warning",
240
+ );
241
+ };
203
242
  const scheduleRecipeReload = (ctx: ExtensionContext): void => {
243
+ recipeWatcherFailureNotified = false;
204
244
  if (recipeReloadTimeout) clearTimeout(recipeReloadTimeout);
205
245
  recipeReloadTimeout = setTimeout(() => {
206
246
  runtime.loadTools(ctx);
@@ -216,9 +256,10 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
216
256
  recipeRootWatcher.on("error", () => {
217
257
  recipeRootWatcher?.close();
218
258
  recipeRootWatcher = undefined;
259
+ notifyRecipeWatcherFailure(ctx);
219
260
  });
220
261
  } catch {
221
- // Watching is best-effort; restarting the session reloads recipe tools.
262
+ notifyRecipeWatcherFailure(ctx);
222
263
  }
223
264
  };
224
265
  const actorToolDefinitions = new Map<string, any>();
@@ -292,6 +333,44 @@ export default function toolRegistryExtension(pi: ExtensionAPI) {
292
333
  );
293
334
  },
294
335
  });
336
+ pi.registerCommand("actors-inspector-filter", {
337
+ description:
338
+ "Filter actor inspector rows: all, room, direct, broadcast, mention <text>",
339
+ handler: async (args, ctx) => {
340
+ const parts = Array.isArray(args)
341
+ ? args.map(String)
342
+ : String(args ?? "").split(/\s+/);
343
+ const mode = (parts[0] ?? "").trim().toLowerCase();
344
+ if (!mode || mode === "all" || mode === "clear") {
345
+ actorInspectorChannels = undefined;
346
+ actorInspectorMention = undefined;
347
+ } else if (mode === "room" || mode === "direct" || mode === "broadcast") {
348
+ actorInspectorChannels = [mode];
349
+ actorInspectorMention = undefined;
350
+ } else if (mode === "mention") {
351
+ const mention = parts.slice(1).join(" ").trim();
352
+ if (!mention) {
353
+ ctx.ui.notify(
354
+ "Usage: /actors-inspector-filter mention <text>",
355
+ "warning",
356
+ );
357
+ return;
358
+ }
359
+ actorInspectorChannels = undefined;
360
+ actorInspectorMention = mention;
361
+ } else {
362
+ ctx.ui.notify(
363
+ "Usage: /actors-inspector-filter all|room|direct|broadcast|mention <text>",
364
+ "warning",
365
+ );
366
+ return;
367
+ }
368
+ selectedInspectorSequence = undefined;
369
+ communicationWidgetVisible = true;
370
+ updateRunUi(ctx);
371
+ ctx.ui.notify(`Actor inspector filter ${mode || "all"}`, "info");
372
+ },
373
+ });
295
374
  pi.registerCommand("actors-inspect", {
296
375
  description: "Inspect actor message by visible number",
297
376
  handler: async (args, ctx) => {
@@ -41,9 +41,19 @@ export interface ActorInspectorItemViewOptions {
41
41
  sequence: number;
42
42
  }
43
43
 
44
+ export interface ActorInspectorRosterMember {
45
+ address: string;
46
+ display?: string;
47
+ role?: string;
48
+ status?: string;
49
+ }
50
+
44
51
  export interface ActorInspectorPreviewReadOptions {
45
52
  ownerId?: string;
46
53
  currentRunOnly?: boolean;
54
+ channels?: ActorInspectorPreview["channel"][];
55
+ mention?: string;
56
+ roomLimitPerRun?: number;
47
57
  }
48
58
 
49
59
  function asRecord(value: unknown): Record<string, unknown> {
@@ -123,32 +133,42 @@ function previewFromMessage(
123
133
  };
124
134
  }
125
135
 
126
- function readRoomDisplayNames(stateDir: string, room: string): Record<string, string> {
136
+ function readRoomRosterRecords(
137
+ stateDir: string,
138
+ room: string,
139
+ ): Record<string, Record<string, unknown>> {
127
140
  try {
128
- const roster = JSON.parse(
141
+ return JSON.parse(
129
142
  fs.readFileSync(path.join(stateDir, "rooms", room, "roster.json"), "utf8"),
130
143
  ) as Record<string, Record<string, unknown>>;
131
- return Object.fromEntries(
132
- Object.entries(roster).flatMap(([address, member]) => {
133
- const glyph = typeof member.glyph === "string" ? member.glyph.trim() : "";
134
- const display = typeof member.display === "string" ? member.display.trim() : "";
135
- if (display) return [[address, display]];
136
- if (!glyph) return [];
137
- return [[address, `${glyph} ${actorName(address)}`]];
138
- }),
139
- );
140
144
  } catch {
141
145
  return {};
142
146
  }
143
147
  }
144
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
+
145
164
  function readRoomPreviews(
146
165
  run: string,
147
166
  stateDir: string,
167
+ limitPerRun?: number,
148
168
  ): ActorInspectorPreview[] {
149
169
  const roomsDir = path.join(stateDir, "rooms");
150
170
  try {
151
- return fs
171
+ const previews = fs
152
172
  .readdirSync(roomsDir, { withFileTypes: true })
153
173
  .filter((entry) => entry.isDirectory())
154
174
  .flatMap((entry) => {
@@ -166,6 +186,8 @@ function readRoomPreviews(
166
186
  Boolean(preview),
167
187
  );
168
188
  });
189
+ const limit = Number.isFinite(limitPerRun) ? Math.max(0, Number(limitPerRun)) : undefined;
190
+ return limit === undefined ? previews : previews.slice(-limit);
169
191
  } catch (error) {
170
192
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
171
193
  return [];
@@ -218,6 +240,25 @@ function matchesOwner(stateDir: string, ownerId: string | undefined): boolean {
218
240
  return ownerId === undefined || getRunOwnerId(stateDir) === ownerId;
219
241
  }
220
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
+
221
262
  export function readActorInspectorPreviews(
222
263
  stateRoot = Paths.getRunStateRoot(),
223
264
  limit = 8,
@@ -231,7 +272,7 @@ export function readActorInspectorPreviews(
231
272
  const stateDir = path.join(stateRoot, entry.name);
232
273
  if (!matchesOwner(stateDir, options.ownerId)) return [];
233
274
  return [
234
- ...readRoomPreviews(entry.name, stateDir),
275
+ ...readRoomPreviews(entry.name, stateDir, options.roomLimitPerRun),
235
276
  ...readInboxPreviews(entry.name, stateDir),
236
277
  ...readOutboxPreviews(entry.name, stateDir),
237
278
  ];
@@ -243,10 +284,11 @@ export function readActorInspectorPreviews(
243
284
  : undefined;
244
285
  return previews
245
286
  .filter((preview) => !currentRun || preview.run === currentRun)
287
+ .filter((preview) => matchesPreviewFilter(preview, options))
246
288
  .map((preview, index) => ({
247
289
  ...preview,
248
290
  sequence: index + 1,
249
- stripe: index % 2 === 0,
291
+ stripe: index % 2 === 1,
250
292
  }))
251
293
  .slice(-Math.max(1, limit));
252
294
  } catch (error) {
@@ -285,8 +327,12 @@ function roomName(address: string): string | undefined {
285
327
  return room ? room[1] : undefined;
286
328
  }
287
329
 
330
+ function routeActorText(preview: ActorInspectorPreview): string {
331
+ return preview.from_display || actorName(preview.from);
332
+ }
333
+
288
334
  function routeText(preview: ActorInspectorPreview): string {
289
- const actor = preview.from_display || actorName(preview.from);
335
+ const actor = routeActorText(preview);
290
336
  if (preview.channel === "room") return `${actor} # all`;
291
337
  if (preview.channel === "broadcast") return `${actor} ⇢ ${preview.to}`;
292
338
  return `${actor} → ${actorName(preview.to)}`;
@@ -314,17 +360,18 @@ function displayWidth(value: string): number {
314
360
  return visibleWidth(value);
315
361
  }
316
362
 
363
+ const lineSegmenter = new Intl.Segmenter();
364
+
317
365
  function boundedLine(value: string, width: number): string {
318
366
  if (width <= 0) return "";
319
367
  if (visibleWidth(value) <= width) return value;
320
- const ellipsis = "...";
368
+ const ellipsis = "";
321
369
  const ellipsisWidth = visibleWidth(ellipsis);
322
370
  if (width <= ellipsisWidth) return ellipsis.slice(0, width);
323
371
  let output = "";
324
372
  let used = 0;
325
373
  const maxTextWidth = width - ellipsisWidth;
326
- const segmenter = new Intl.Segmenter();
327
- for (const { segment } of segmenter.segment(value)) {
374
+ for (const { segment } of lineSegmenter.segment(value)) {
328
375
  const segmentWidth = visibleWidth(segment);
329
376
  if (used + segmentWidth > maxTextWidth) break;
330
377
  output += segment;
@@ -357,9 +404,10 @@ function renderCompactInspectorEntry(
357
404
  ): string[] {
358
405
  const separator = " ";
359
406
  const prefix = " ";
360
- const contentWidth = Math.max(8, width - prefix.length);
407
+ const suffix = " ";
408
+ const contentWidth = Math.max(8, width - prefix.length - suffix.length);
361
409
  const sequence = String(preview.sequence ?? 0).padStart(sequenceWidth, " ");
362
- const sequencePrefix = `${sequence} `;
410
+ const sequencePrefix = `${sequence}${separator}`;
363
411
  const route = routeText(preview);
364
412
  const routePadding = " ".repeat(
365
413
  Math.max(0, routeWidth - displayWidth(route)),
@@ -384,7 +432,7 @@ function renderCompactInspectorEntry(
384
432
  separator,
385
433
  style(styles.preview, visibleHeadline),
386
434
  ].join("");
387
- const line = `${prefix}${padLine(plain, rendered, contentWidth, styles)}`;
435
+ const line = `${prefix}${padLine(plain, rendered, contentWidth, styles)}${suffix}`;
388
436
  if (stripe && styles.stripe) return [styles.stripe(line)];
389
437
  if (!stripe && styles.stripeAlt) return [styles.stripeAlt(line)];
390
438
  return [line];
@@ -402,39 +450,123 @@ function renderInspectorEntry(
402
450
  ): string[] {
403
451
  const separator = " ";
404
452
  const prefix = " ";
405
- const contentWidth = Math.max(8, width - prefix.length);
453
+ const suffix = " ";
454
+ const contentWidth = Math.max(8, width - prefix.length - suffix.length);
406
455
  const sequence = String(preview.sequence ?? 0).padStart(sequenceWidth, " ");
407
456
  const sequencePrefix = `${sequence}${separator}`;
408
457
  const route = routeText(preview);
409
458
  const type = preview.type;
410
459
  const summary = preview.summary?.trim() ?? "";
411
460
  const body = preview.body_preview?.trim() || (!summary ? previewText(preview) : "-");
412
- const routePadding = " ".repeat(Math.max(0, routeWidth - displayWidth(route)));
413
- const typePadding = " ".repeat(Math.max(0, typeWidth - displayWidth(type)));
414
- const visibleSummary = boundedLine(summary, summaryWidth);
415
- const summaryPadding = " ".repeat(Math.max(0, summaryWidth - displayWidth(visibleSummary)));
416
- const lead = `${sequencePrefix}${route}${routePadding}${separator}${type}${typePadding}${separator}${visibleSummary}${summaryPadding}${separator}`;
417
- const renderedLead = [
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 = [
418
466
  style(styles.muted, sequencePrefix),
419
- style(styles.target, route),
420
- routePadding,
421
- separator,
422
- style(styles.type, type),
423
- typePadding,
467
+ style(styles.target, visibleRoute),
468
+ boundedRoutePadding,
424
469
  separator,
425
- style(styles.preview, visibleSummary),
426
- summaryPadding,
470
+ style(styles.type, visibleType),
471
+ boundedTypePadding,
427
472
  separator,
428
- ].join("");
473
+ ];
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
+ }
429
481
  const visibleBody = boundedLine(body, Math.max(0, contentWidth - displayWidth(lead)));
430
482
  const plain = `${lead}${visibleBody}`;
431
- const rendered = `${renderedLead}${style(styles.preview, visibleBody)}`;
432
- const line = `${prefix}${padLine(plain, rendered, contentWidth, styles)}`;
483
+ const rendered = `${leadParts.join("")}${style(styles.preview, visibleBody)}`;
484
+ const line = `${prefix}${padLine(plain, rendered, contentWidth, styles)}${suffix}`;
433
485
  if (stripe && styles.stripe) return [styles.stripe(line)];
434
486
  if (!stripe && styles.stripeAlt) return [styles.stripeAlt(line)];
435
487
  return [line];
436
488
  }
437
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
+
438
570
  export function renderInspectorItemView(
439
571
  previews: ActorInspectorPreview[],
440
572
  width = 80,
@@ -496,24 +628,42 @@ export function renderInspectorWidget(
496
628
  void options;
497
629
  const visible = previews.map((preview, index) => ({
498
630
  preview: { ...preview, sequence: preview.sequence ?? index + 1 },
499
- stripe: preview.stripe ?? index % 2 === 0,
631
+ stripe: preview.stripe ?? index % 2 === 1,
500
632
  }));
501
633
  const sequenceWidth = Math.max(
502
634
  1,
503
635
  ...visible.map(({ preview }) => String(preview.sequence ?? 0).length),
504
636
  );
505
637
  const lines: string[] = [];
506
- const routeWidth = Math.max(
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(
507
646
  ...visible.map(({ preview }) => displayWidth(routeText(preview))),
508
647
  );
509
- const typeWidth = Math.max(
648
+ const naturalTypeWidth = Math.max(
510
649
  ...visible.map(({ preview }) => displayWidth(preview.type)),
511
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);
512
660
  const summaryWidths = visible
513
661
  .map(({ preview }) => preview.summary?.trim())
514
662
  .filter((summary): summary is string => Boolean(summary))
515
663
  .map((summary) => displayWidth(summary));
516
- const summaryWidth = summaryWidths.length ? Math.max(...summaryWidths) : 0;
664
+ const summaryWidth = summaryWidths.length
665
+ ? Math.min(Math.max(...summaryWidths), Math.max(1, Math.floor(messageWidth * 0.5)))
666
+ : 0;
517
667
  for (const { preview, stripe } of visible) {
518
668
  lines.push(
519
669
  ...renderInspectorEntry(