pilotswarm 0.5.20 → 0.5.21

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.
@@ -513,8 +513,16 @@ function canPinSessionRow(session) {
513
513
 
514
514
  function buildSelectedSessionMetaRuns(session, mode) {
515
515
  const runs = [];
516
+ // Live regeneration chip: the orchestration publishes regenStage in
517
+ // customStatus while the pipeline runs (archiving → distilling →
518
+ // flipping) and getSession spreads it onto the session view. Magenta to
519
+ // match the epoch divider; disappears when the flip lands.
520
+ if (typeof session?.regenStage === "string" && session.regenStage) {
521
+ runs.push({ text: `↻ regen:${session.regenStage}`, color: "magenta" });
522
+ }
516
523
  const statusLabel = getSessionRowStatusLabel(session);
517
524
  if (statusLabel) {
525
+ if (runs.length > 0) runs.push({ text: " · ", color: "gray" });
518
526
  runs.push({ text: statusLabel, color: sessionStatusColor(session, mode) });
519
527
  }
520
528
 
@@ -584,6 +592,10 @@ function buildSessionRowView(entry, session, state, totalDescendantCounts, visib
584
592
  prefixRuns.push({ text: "🗂 ", color: "cyan", bold: true });
585
593
  } else if (session?.isSystem) {
586
594
  prefixRuns.push({ text: "⚙ ", color: "yellow", bold: true });
595
+ } else if (session?.serviceKind) {
596
+ // Service session (tree-scoped machinery, e.g. the regen distiller):
597
+ // the alembic marks it as read-only distillation machinery.
598
+ prefixRuns.push({ text: "⚗ ", color: "magenta", bold: true });
587
599
  } else {
588
600
  const icon = sessionStatusIcon(session, mode);
589
601
  prefixRuns.push({
@@ -2287,8 +2299,14 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
2287
2299
  };
2288
2300
  const lines = [];
2289
2301
  for (const [index, message] of messages.entries()) {
2290
- const messageLines = buildChatMessageLines(message, maxWidth, buildOptions);
2291
- appendChatBlockLines(lines, messageLines);
2302
+ if (message?.kind === "epoch-divider") {
2303
+ lines.push(buildEpochDividerLine(message, maxWidth));
2304
+ } else if (message?.kind === "regen-refused") {
2305
+ lines.push(buildRegenRefusedLine(message, maxWidth));
2306
+ } else {
2307
+ const messageLines = buildChatMessageLines(message, maxWidth, buildOptions);
2308
+ appendChatBlockLines(lines, messageLines);
2309
+ }
2292
2310
  const nextMessage = messages[index + 1];
2293
2311
  if (
2294
2312
  nextMessage
@@ -2301,6 +2319,55 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
2301
2319
  return lines.length > 0 ? lines : [{ text: "No messages yet.", color: "gray" }];
2302
2320
  }
2303
2321
 
2322
+ // A centered inline rule ("──── label ────") for transcript markers. The dash
2323
+ // runs are CAPPED (not stretched to maxWidth): the web portal wraps by pixel
2324
+ // width, and a maxWidth-long "─" run overflows a narrower pane and wraps the
2325
+ // rule mid-label. A short symmetric rule reads as a divider on every width, and
2326
+ // when the label alone will not fit it is rendered bare (wraps as plain text,
2327
+ // never as dangling dash fragments).
2328
+ function buildRuleLine(label, color, maxWidth) {
2329
+ const safeWidth = Math.max(24, Number(maxWidth) || 80);
2330
+ const room = safeWidth - label.length;
2331
+ if (room < 2) return [{ text: label.trim(), color, bold: true }];
2332
+ const perSide = Math.min(6, Math.floor(room / 2));
2333
+ if (perSide < 1) return [{ text: label.trim(), color, bold: true }];
2334
+ return [
2335
+ { text: "─".repeat(perSide), color },
2336
+ { text: label, color, bold: true },
2337
+ { text: "─".repeat(perSide), color },
2338
+ ];
2339
+ }
2340
+
2341
+ // The inline transcript divider for a session-regeneration epoch flip — magenta,
2342
+ // with the new epoch and the count of archived turns (proposal M2).
2343
+ function buildEpochDividerLine(message, maxWidth) {
2344
+ const turns = Number.isFinite(message?.turnsArchived) ? message.turnsArchived : null;
2345
+ const label = ` ↻ context regenerated · epoch ${message?.epoch ?? "?"}`
2346
+ + `${turns != null ? ` · ${turns} turn${turns === 1 ? "" : "s"} archived` : ""} `;
2347
+ return buildRuleLine(label, "magenta", maxWidth);
2348
+ }
2349
+
2350
+ // Friendly (compact) text for the orchestration's regenerate_refused reasons
2351
+ // (lifecycle.ts). Kept short so the inline rule fits on one line.
2352
+ const REGEN_REFUSED_REASONS = {
2353
+ cooldown: "on cooldown (once per 6h)",
2354
+ too_young: "too soon (needs 5+ turns)",
2355
+ already_pending: "already in progress",
2356
+ is_system: "not allowed for system sessions",
2357
+ not_owner: "owner only",
2358
+ not_parent: "parent only",
2359
+ };
2360
+
2361
+ // The inline notice for a refused regeneration. Yellow (vs the magenta success
2362
+ // divider) so a no-op attempt reads as a warning, correcting the tool's
2363
+ // optimistic "regeneration accepted" acknowledgement.
2364
+ function buildRegenRefusedLine(message, maxWidth) {
2365
+ const reason = String(message?.reason || "unknown");
2366
+ const text = REGEN_REFUSED_REASONS[reason] || reason.replace(/_/g, " ");
2367
+ const label = ` ↻ regeneration refused · ${text} `;
2368
+ return buildRuleLine(label, "yellow", maxWidth);
2369
+ }
2370
+
2304
2371
  export function selectOutboxOverlayLines(state, maxWidth = 80, options = {}) {
2305
2372
  const messages = selectActiveOutboxMessages(state);
2306
2373
  if (!messages || messages.length === 0) return [];
@@ -3997,7 +4064,7 @@ function buildNodeMapCell(session, brandingTitle, width, active) {
3997
4064
  ? canonicalSystemTitle(session, brandingTitle)
3998
4065
  : (session?.title || shortSessionId(session?.sessionId)))
3999
4066
  : shortSessionId(session?.sessionId);
4000
- const prefix = session?.isSystem ? "⚙ " : `${sessionStatusIcon(session) || "."} `;
4067
+ const prefix = session?.isSystem ? "⚙ " : session?.serviceKind ? "⚗ " : `${sessionStatusIcon(session) || "."} `;
4001
4068
  const text = padDisplayText(`${prefix}${label}`, width);
4002
4069
 
4003
4070
  if (active) {
@@ -5156,10 +5223,28 @@ function buildSessionStatsLines(state, session, maxWidth) {
5156
5223
  }));
5157
5224
  lines.push(plainInspectorLine(""));
5158
5225
 
5159
- // Persistence card
5226
+ // Persistence card. Epoch (session-regeneration incarnation) is always
5227
+ // shown — 0 for a session that has never regenerated — so the current
5228
+ // epoch is visible at a glance; regen counters appear once it has.
5229
+ const regenCount = Number(summary.regenCount) || 0;
5230
+ const lastRegen = summary.lastRegenStats && typeof summary.lastRegenStats === "object" ? summary.lastRegenStats : null;
5231
+ const currentEpoch = Number.isFinite(lastRegen?.toEpoch) ? lastRegen.toEpoch : regenCount;
5232
+ // Distillation provenance: "fast" = deterministic package; otherwise the
5233
+ // distiller model label (strip the provider: prefix for width).
5234
+ const distillLabel = lastRegen
5235
+ ? (lastRegen.distillMode === "deterministic" || (!lastRegen.distillMode && !lastRegen.distillerModel)
5236
+ ? "fast"
5237
+ : String(lastRegen.distillerModel || "llm").replace(/^[^:]*:/, ""))
5238
+ : null;
5239
+ const lastRegenLabel = lastRegen
5240
+ ? `${lastRegen.turnsArchived ?? 0} turn${lastRegen.turnsArchived === 1 ? "" : "s"} · ${(Number(lastRegen.totalMs) / 1000).toFixed(1)}s${distillLabel ? ` · ${distillLabel}` : ""}`
5241
+ : null;
5160
5242
  lines.push(...buildMessageCardLines({
5161
5243
  title: "Persistence",
5162
5244
  body: formatKeyValueTable([
5245
+ ["Epoch", String(currentEpoch)],
5246
+ ["Regens", regenCount > 0 ? String(regenCount) : null],
5247
+ ["Last Regen", lastRegenLabel],
5163
5248
  ["Snapshot", formatCompactBytes(summary.snapshotSizeBytes)],
5164
5249
  ["Uncompressed", summary.rawSizeBytes ? formatCompactBytes(summary.rawSizeBytes) : null],
5165
5250
  ["Compression", formatCompressionRatio(summary.rawSizeBytes, summary.snapshotSizeBytes)],
@@ -2199,13 +2199,6 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
2199
2199
  return s && !s.isSystem && !s.isGroup;
2200
2200
  })
2201
2201
  : activeSession?.isGroup ? true : Boolean(activeSession);
2202
- const activeSessionActionLabel = activeSession?.isGroup
2203
- ? "Delete"
2204
- : isBulkSelection
2205
- ? `Terminate (${selectedCount})`
2206
- : activeSession?.isSystem
2207
- ? "Restart"
2208
- : "Terminate";
2209
2202
  const hasExplicitSelection = selectedCount > 0;
2210
2203
  const groupableIds = hasExplicitSelection
2211
2204
  ? viewState.selectedIds.filter((id) => {
@@ -2300,7 +2293,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
2300
2293
  }),
2301
2294
  React.createElement(IconButton, {
2302
2295
  className: "ps-mini-button",
2303
- icon: activeSession?.isSystem ? "↻" : "⊗",
2296
+ icon: activeSession?.isSystem ? "↻" : activeSession?.isGroup ? "⊗" : React.createElement(LifecycleGlyph),
2304
2297
  onClick: () => controller.handleCommand(activeSession?.isGroup ? UI_COMMANDS.DELETE_SESSION : UI_COMMANDS.OPEN_TERMINATE_PICKER).catch(() => {}),
2305
2298
  disabled: !canTerminate,
2306
2299
  label: isBulkSelection
@@ -2309,7 +2302,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
2309
2302
  ? (activeGroupCanDelete ? "Delete this empty group" : "This group cannot be deleted yet")
2310
2303
  : activeSession?.isSystem
2311
2304
  ? "Restart this system session (complete, terminate, or hard delete)"
2312
- : `${activeSessionActionLabel} — mark completed, cancel, or delete`,
2305
+ : "Lifecycleregenerate context, mark completed, cancel, or delete",
2313
2306
  }),
2314
2307
  actions);
2315
2308
 
@@ -2476,6 +2469,20 @@ function LinkGlyph() {
2476
2469
  React.createElement("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" }));
2477
2470
  }
2478
2471
 
2472
+ // The "lifecycle" glyph (two curved arrows forming a cycle). Fronts the session
2473
+ // Lifecycle menu — Regenerate (rebirth) plus the terminal dispositions.
2474
+ function LifecycleGlyph() {
2475
+ return React.createElement("svg", {
2476
+ className: "ps-share-glyph", viewBox: "0 0 24 24", fill: "none",
2477
+ stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round",
2478
+ "aria-hidden": "true",
2479
+ },
2480
+ React.createElement("path", { d: "M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
2481
+ React.createElement("path", { d: "M3 3v5h5" }),
2482
+ React.createElement("path", { d: "M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" }),
2483
+ React.createElement("path", { d: "M21 21v-5h-5" }));
2484
+ }
2485
+
2479
2486
  /**
2480
2487
  * Fetch the caller's effective access to the active session (security model).
2481
2488
  * Returns { access, loading, reload }. access is the getSessionAccess payload
@@ -2854,12 +2861,18 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
2854
2861
  );
2855
2862
  const navigationError = useControllerSelector(controller, selectNavigationError, shallowEqualObject);
2856
2863
  const composerBase = showComposer && !viewState.activeSessionIsGroup && viewState.chatViewMode !== "summary";
2857
- const readOnly = Boolean(access) && access.canWrite === false;
2864
+ // Service sessions ( tree-scoped machinery, e.g. the regen distiller) are
2865
+ // read-only BY KIND: their transcript is the trace of runtime machinery,
2866
+ // never a conversation surface — no prompt for anyone, owner included.
2867
+ const activeIsService = Boolean(viewState.sessionsById?.[viewState.activeSessionId]?.serviceKind);
2868
+ const readOnly = activeIsService || (Boolean(access) && access.canWrite === false);
2858
2869
  const composer = composerBase
2859
2870
  ? React.createElement("div", { className: "ps-chat-composer" },
2860
2871
  readOnly
2861
2872
  ? React.createElement("div", { className: "ps-composer-readonly" },
2862
- `You have view access to this session. Ask ${access.owner?.displayName || access.owner?.email || "the owner"} for write access to participate.`)
2873
+ activeIsService
2874
+ ? "⚗ Service session — runtime machinery. Its transcript is a read-only trace; it does not accept messages."
2875
+ : `You have view access to this session. Ask ${access.owner?.displayName || access.owner?.email || "the owner"} for write access to participate.`)
2863
2876
  : React.createElement(PromptComposer, { controller, mobile, active: true }))
2864
2877
  : null;
2865
2878
 
@@ -4499,6 +4512,10 @@ function ModalLayer({ controller }) {
4499
4512
  if (modal.type === "confirm" && modalState.confirm) {
4500
4513
  const isAlert = Boolean(modal.alert);
4501
4514
  const isDestructive = !isAlert && modal.action === "deleteSession";
4515
+ // The regenerate confirm carries distillation inputs: a mode select and
4516
+ // an optional distilling-instructions textarea, bound to modal.extras.
4517
+ const isRegen = modal.action === "regenerateSession";
4518
+ const extras = modal.extras || {};
4502
4519
  return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
4503
4520
  React.createElement("div", { className: "ps-modal is-narrow", onClick: (event) => event.stopPropagation() },
4504
4521
  React.createElement("div", { className: "ps-modal-header" },
@@ -4508,6 +4525,32 @@ function ModalLayer({ controller }) {
4508
4525
  React.createElement("div", { className: "ps-modal-body", style: { padding: "16px 20px" } },
4509
4526
  React.createElement("p", { style: { color: "#94a3b8", margin: 0 } }, modalState.confirm.message),
4510
4527
  ),
4528
+ isRegen
4529
+ ? React.createElement("div", { className: "ps-modal-body", style: { padding: "0 20px 12px", display: "flex", flexDirection: "column", gap: 10 } },
4530
+ React.createElement("label", { style: { color: "#94a3b8", fontSize: "12px", display: "flex", alignItems: "center", gap: 8 } },
4531
+ "Distillation",
4532
+ React.createElement("select", {
4533
+ className: "ps-modal-input",
4534
+ style: { flex: "1", padding: "4px 8px" },
4535
+ value: extras.distillMode || "llm",
4536
+ onChange: (event) => controller.updateConfirmExtras({ distillMode: event.currentTarget.value }),
4537
+ },
4538
+ React.createElement("option", { value: "llm" }, "Intelligent (LLM reads the whole transcript)"),
4539
+ React.createElement("option", { value: "deterministic" }, "Fast (no LLM — tail + pointers)"),
4540
+ ),
4541
+ ),
4542
+ (extras.distillMode || "llm") === "llm"
4543
+ ? React.createElement("textarea", {
4544
+ className: "ps-modal-input",
4545
+ style: { width: "100%", minHeight: "64px", resize: "vertical", fontFamily: "inherit", fontSize: "13px" },
4546
+ placeholder: "Distilling instructions (optional) — e.g. \"preserve every SQL snippet verbatim\"",
4547
+ value: extras.instructions || "",
4548
+ maxLength: 4000,
4549
+ onChange: (event) => controller.updateConfirmExtras({ instructions: event.currentTarget.value }),
4550
+ })
4551
+ : null,
4552
+ )
4553
+ : null,
4511
4554
  React.createElement("div", { className: "ps-modal-footer" },
4512
4555
  isAlert ? null : React.createElement("button", { type: "button", className: "ps-modal-button", onClick: close }, "Cancel"),
4513
4556
  React.createElement("button", {
@@ -4657,7 +4700,7 @@ function ModalLayer({ controller }) {
4657
4700
  return React.createElement("div", { className: "ps-modal-backdrop", onClick: close },
4658
4701
  React.createElement("div", { className: "ps-modal is-narrow", onClick: (event) => event.stopPropagation() },
4659
4702
  React.createElement("div", { className: "ps-modal-header" },
4660
- React.createElement("div", { className: "ps-modal-title" }, modal.title || "Terminate session"),
4703
+ React.createElement("div", { className: "ps-modal-title" }, modal.title || "Session Lifecycle"),
4661
4704
  React.createElement("button", { type: "button", className: "ps-modal-close", onClick: close }, "Close"),
4662
4705
  ),
4663
4706
  React.createElement("div", { className: "ps-modal-body", style: { padding: "12px 16px 4px" } },
@@ -4666,6 +4709,15 @@ function ModalLayer({ controller }) {
4666
4709
  className: "ps-modal-body",
4667
4710
  style: { padding: "10px 16px 16px", display: "flex", flexDirection: "column", gap: 8 },
4668
4711
  },
4712
+ modal.canRegenerate
4713
+ ? React.createElement("button", {
4714
+ type: "button",
4715
+ className: "ps-modal-button",
4716
+ style: { width: "100%", justifyContent: "flex-start" },
4717
+ title: "Archive and distill the transcript, then rebuild context fresh at the next turn boundary. Facts, artifacts, sub-agents, sharing, schedule, and chat history are preserved.",
4718
+ onClick: pick("regenerate"),
4719
+ }, "Regenerate Context")
4720
+ : null,
4669
4721
  React.createElement("button", {
4670
4722
  type: "button",
4671
4723
  className: "ps-modal-button is-primary",
@@ -4771,7 +4823,7 @@ function useKeyboardShortcuts(controller, mobile) {
4771
4823
  const handler = (event) => {
4772
4824
  const target = event.target;
4773
4825
  const editable = target instanceof HTMLElement
4774
- && (target.tagName === "TEXTAREA" || target.tagName === "INPUT" || target.isContentEditable);
4826
+ && (target.tagName === "TEXTAREA" || target.tagName === "INPUT" || target.tagName === "SELECT" || target.isContentEditable);
4775
4827
  const modal = controller.getState().ui.modal;
4776
4828
  const visibleInspectorTabs = getVisibleInspectorTabs(controller);
4777
4829
  const currentInspectorTab = controller.getState().ui.inspectorTab;