pi-crew 0.10.6 → 0.11.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 (44) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/index.mjs +34850 -34889
  3. package/package.json +170 -169
  4. package/scripts/watch-agent-transcript.mjs +120 -0
  5. package/src/extension/crew-input-router.ts +2 -0
  6. package/src/extension/crew-vibes/config.ts +1 -56
  7. package/src/extension/crew-vibes/figures.ts +5 -84
  8. package/src/extension/crew-vibes/font-detect.ts +1 -1
  9. package/src/extension/crew-vibes/index.ts +36 -328
  10. package/src/extension/crew-vibes/render.ts +2 -58
  11. package/src/extension/help.ts +1 -0
  12. package/src/extension/registration/command-registration.ts +5 -1
  13. package/src/extension/registration/commands/schedules.ts +212 -0
  14. package/src/extension/registration/commands/shared.ts +56 -1
  15. package/src/extension/registration/lifecycle-handlers.ts +16 -2
  16. package/src/extension/registration/schedule-toast-bridge.ts +109 -0
  17. package/src/extension/registration/viewers.ts +66 -1
  18. package/src/extension/team-tool/handle-schedule.ts +120 -1
  19. package/src/runtime/scheduling/scheduler.ts +120 -6
  20. package/src/runtime/settings-store.ts +28 -2
  21. package/src/state/coordination/locks.ts +17 -2
  22. package/src/ui/agents-jobs-browser.ts +785 -0
  23. package/src/ui/dashboard-panes/agents-pane.ts +7 -4
  24. package/src/ui/dashboard-panes/schedules-pane.ts +154 -0
  25. package/src/ui/inline-panel/crew-editor.ts +19 -6
  26. package/src/ui/inline-panel/index.ts +6 -0
  27. package/src/ui/key-utils.ts +3 -0
  28. package/src/ui/keybinding-map.ts +56 -2
  29. package/src/ui/mascot.ts +2 -1
  30. package/src/ui/overlays/agent-picker-overlay.ts +3 -3
  31. package/src/ui/overlays/confirm-overlay.ts +3 -2
  32. package/src/ui/overlays/mailbox-compose-overlay.ts +4 -3
  33. package/src/ui/overlays/mailbox-detail-overlay.ts +3 -3
  34. package/src/ui/run-dashboard.ts +257 -6
  35. package/src/ui/settings-overlay.ts +3 -3
  36. package/src/ui/transcript-viewer.ts +3 -2
  37. package/src/ui/widget/index.ts +61 -88
  38. package/src/ui/widget/widget-formatters.ts +7 -4
  39. package/src/ui/widget/widget-renderer.ts +134 -174
  40. package/src/ui/widget/widget-types.ts +0 -7
  41. package/src/utils/relative-time.ts +33 -0
  42. package/src/extension/crew-vibes/footer.ts +0 -313
  43. package/src/extension/crew-vibes/speed.ts +0 -286
  44. package/src/ui/dock-footer.ts +0 -49
@@ -11,7 +11,6 @@ import { DEFAULT_UI } from "../../config/defaults.ts";
11
11
  import type { ManifestCache } from "../../runtime/manifest-cache.ts";
12
12
  import type { TeamRunManifest } from "../../state/types.ts";
13
13
  import { truncate } from "../../utils/visual.ts";
14
- import { isFooterDockSinkActive, setFooterDockProvider } from "../dock-footer.ts";
15
14
  import { panelRowsFromRuns } from "../inline-panel/panel-rows.ts";
16
15
  import { panelDisplayState, setPanelRowsProvider, subscribePanelChange } from "../inline-panel/panel-store.ts";
17
16
  import { requestRender, requestRenderTarget, setExtensionWidget } from "../pi-ui-compat.ts";
@@ -23,7 +22,7 @@ import type { CrewTheme } from "../theme-adapter.ts";
23
22
  import { asCrewTheme, subscribeThemeChange } from "../theme-adapter.ts";
24
23
  import { buildTaskListLines } from "./task-list.ts";
25
24
  import { activeWidgetRuns, statusSummary } from "./widget-model.ts";
26
- import { buildWidgetLines, colorWidgetLine, DEFAULT_WIDGET_WIDTH, renderLines } from "./widget-renderer.ts";
25
+ import { buildWidgetLines, colorWidgetLine, DEFAULT_WIDGET_WIDTH, renderLines, schedulesWidgetLine } from "./widget-renderer.ts";
27
26
  import type { CrewWidgetModel, CrewWidgetState, WidgetRun } from "./widget-types.ts";
28
27
 
29
28
  export { activeWidgetRuns, statusSummary } from "./widget-model.ts";
@@ -360,7 +359,18 @@ class CrewWidgetComponent implements WidgetComponent {
360
359
  // Panel cursor/pane state is part of the rendered output, so it belongs in
361
360
  // the cache key — otherwise moving the cursor would not repaint.
362
361
  const panel = panelDisplayState();
363
- const signatureWithPanel = `${signature}|panel:${panel.selectedTaskId ?? ""}/${panel.viewedTaskId ?? ""}/${panel.focused ? 1 : 0}`;
362
+ // Tier C: the schedules line is painted output too — its content (enabled
363
+ // count + next-run minute bucket) joins the cache key so a job
364
+ // add/remove/toggle repaints the dock without waiting for any other
365
+ // event. The reader behind schedulesWidgetLine is scheduler-backed (no
366
+ // disk on the paint path), so this stays cheap per tick.
367
+ // ONE clock read per render (review round 1 minor-2): the same Date feeds
368
+ // BOTH this signature fragment and buildWidgetLines (via options.now), so
369
+ // the painted line can never straddle a minute bucket and disagree with
370
+ // the cache key for a tick.
371
+ const schedNow = new Date();
372
+ const schedLine = schedulesWidgetLine(this.model.cwd, schedNow);
373
+ const signatureWithPanel = `${signature}|panel:${panel.selectedTaskId ?? ""}/${panel.viewedTaskId ?? ""}/${panel.focused ? 1 : 0}|sched:${schedLine ?? ""}`;
364
374
 
365
375
  // The spinner-frame swap only belongs on the LEGACY header, whose line 0
366
376
  // already starts with a glyph position (`<frame> Crew agents …`). The
@@ -375,7 +385,7 @@ class CrewWidgetComponent implements WidgetComponent {
375
385
  runs,
376
386
  this.model.notificationCount ?? 0,
377
387
  width,
378
- { rowStyle: this.model.rowStyle, ...panel },
388
+ { rowStyle: this.model.rowStyle, now: schedNow, ...panel },
379
389
  ).map((line, index) => {
380
390
  if (!compactDock && index === 0 && line.length > 0) return `${runningGlyph}${line.slice(1)}`;
381
391
  return line;
@@ -390,11 +400,20 @@ class CrewWidgetComponent implements WidgetComponent {
390
400
  if (runs.length === 0) {
391
401
  this.invalidate();
392
402
  // P0-6: render from snapshots only — never read disk on every render tick.
393
- // When the snapshot cache is provided but hasn't populated yet, paint a
394
- // single "(loading…)" line so the pre-load frame is well-formed instead
395
- // of an empty panel. Without a cache (legacy/tests) keep the empty result.
396
- if (this.model.snapshotCache) return ["(loading…)"];
397
- return [];
403
+ // Tier C live-fix #2 (2026-09-13): the "(loading…)" placeholder was
404
+ // dead code at zero runs until the keep-alive fix mounted this component
405
+ // in quiet sessions — and then it painted FOREVER next to the schedules
406
+ // line (the cache only populates per-run; with no runs there is nothing
407
+ // to load, so "loading" can never resolve). The placeholder is only
408
+ // meaningful on run-transition frames, which always have runs>0 and take
409
+ // a different branch. At zero runs the only paintable content is the
410
+ // schedules line (or nothing).
411
+ // Single-line contract (round 3): the keep-alive row carries the
412
+ // ↓·enter interaction hint + the focused ❯ marker — identical to
413
+ // buildWidgetLines' zero-runs branch.
414
+ const zeroBase = `${schedLine} — ↓·enter`;
415
+ const focused = panel.focused === true;
416
+ return [truncate(focused ? `❯ ${zeroBase}` : zeroBase, width)];
398
417
  }
399
418
 
400
419
  this.ensureTruncated(width);
@@ -407,37 +426,7 @@ class CrewWidgetComponent implements WidgetComponent {
407
426
  }
408
427
  }
409
428
 
410
- // ── Footer dock host (widgetPlacement: "bottom") ──────────────────────
411
-
412
- /**
413
- * Dock host for `widgetPlacement: "bottom"`. Keeps a single CrewWidgetComponent
414
- * (theme-less → raw lines, no ANSI) per session and feeds its render output to
415
- * the crew-vibes footer through the dock-footer registry. The footer colors the
416
- * lines with ITS OWN theme so the dock matches the footer context. All caching,
417
- * event wiring (panel changes, run events, resize) lives in the wrapped
418
- * component; the footer is re-rendered by pi on every host repaint.
419
- */
420
- class FooterDockHost {
421
- private component: CrewWidgetComponent | undefined;
422
- private readonly model: CrewWidgetModel;
423
-
424
- constructor(model: CrewWidgetModel) {
425
- this.model = model;
426
- }
427
-
428
- render(width: number): string[] {
429
- if (!this.component) this.component = new CrewWidgetComponent(this.model, undefined, undefined);
430
- return this.component.render(width);
431
- }
432
-
433
- dispose(): void {
434
- this.component?.dispose();
435
- this.component = undefined;
436
- }
437
- }
438
-
439
429
  // ── Re-export listLiveAgents for buildSignature ───────────────────────
440
-
441
430
  import { listLiveAgents } from "../../runtime/live-session/live-agent-manager.ts";
442
431
 
443
432
  // ── Public API ────────────────────────────────────────────────────────
@@ -468,11 +457,11 @@ export function updateCrewWidget(
468
457
  // projection is registered here instead of re-reading state on every keypress.
469
458
  setPanelRowsProvider(() => panelRowsFromRuns(activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache, preloadedManifests, workspaceId)));
470
459
  const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
471
- // `bottom` is not a pi widget slot: the dock then renders inside the
472
- // crew-vibes footer (dock-footer registry). pi's slot calls always use a
473
- // real slot so legacy-clear/installs stay on maps pi understands.
460
+ // `bottom` is not a pi widget slot name: it maps to the belowEditor slot.
461
+ // (The crew-vibes footer-dock variant was retired 2026-09-13 — option 1 of
462
+ // the UI review follow-up: pi's NATIVE footer is restored; the crew widget
463
+ // always renders through pi's widget slots.)
474
464
  const bottomMode = placement === "bottom";
475
- const dockInFooter = bottomMode && isFooterDockSinkActive();
476
465
  const piPlacement: "aboveEditor" | "belowEditor" = bottomMode ? "belowEditor" : placement;
477
466
 
478
467
  // PERF (2026-08-24): the persistent CrewWidgetComponent renders itself from
@@ -486,22 +475,35 @@ export function updateCrewWidget(
486
475
  }
487
476
 
488
477
  if (runs.length === 0) {
489
- if (state.lastVisibility !== "hidden" || state.lastPlacement !== placement) {
490
- setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement: piPlacement });
491
- setExtensionWidget(ctx, TASKS_WIDGET_KEY, undefined, { placement: "aboveEditor" });
492
- state.lastTasksVisibility = "hidden";
493
- state.footerDock?.dispose();
494
- state.footerDock = undefined;
495
- setFooterDockProvider(undefined);
496
- state.lastVisibility = "hidden";
497
- state.lastPlacement = placement;
498
- state.lastKey = WIDGET_KEY;
499
- state.lastMaxLines = maxLines;
500
- state.lastCwd = ctx.cwd;
501
- state.model = undefined;
478
+ // Tier C live-fix (caught 2026-09-13, live TUI proof): scheduled jobs are
479
+ // exactly what runs while nothing interactive is active. When a schedules
480
+ // line would paint, do NOT take the hide path — fall through to the
481
+ // install path below so the widget/footer dock stays mounted and its
482
+ // render() paints the schedules-only line. The old unconditional hide
483
+ // UNMOUNTED CrewWidgetComponent in every quiet session, so the no-runs
484
+ // render branch and buildWidgetLines' empty-render gate were dead code:
485
+ // `⏰ 1 sched · next Xm` never appeared outside a run.
486
+ const schedKeepAlive = Boolean(schedulesWidgetLine(ctx.cwd, new Date()));
487
+ if (!schedKeepAlive) {
488
+ if (state.lastVisibility !== "hidden" || state.lastPlacement !== placement) {
489
+ setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement: piPlacement });
490
+ setExtensionWidget(ctx, TASKS_WIDGET_KEY, undefined, { placement: "aboveEditor" });
491
+ state.lastTasksVisibility = "hidden";
492
+ state.lastVisibility = "hidden";
493
+ state.lastPlacement = placement;
494
+ state.lastKey = WIDGET_KEY;
495
+ state.lastMaxLines = maxLines;
496
+ state.lastCwd = ctx.cwd;
497
+ state.model = undefined;
498
+ }
499
+ requestRender(ctx);
500
+ return;
502
501
  }
503
- requestRender(ctx);
504
- return;
502
+ // schedKeepAlive: fall through — model + install/footer-dock below;
503
+ // tasksVisible stays false (no runs), so the task-list widget stays
504
+ // hidden exactly as before. One clock read per update event (this is
505
+ // the event path, not the per-render paint path) is fine; the component
506
+ // keeps its own single-read signature discipline per render.
505
507
  }
506
508
 
507
509
  const needsWidgetInstall =
@@ -536,27 +538,7 @@ export function updateCrewWidget(
536
538
  state.model.rowStyle = rowStyle;
537
539
  }
538
540
 
539
- if (dockInFooter) {
540
- // Keep pi's widget slot free: the crew-vibes footer paints the dock at
541
- // the very bottom, below the quota/meter lines. A widget-slot install
542
- // from a PREVIOUS placement (or a sink that was just enabled) must be
543
- // removed first.
544
- if (needsWidgetInstall && state.lastKey === WIDGET_KEY) {
545
- setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement: piPlacement });
546
- }
547
- if (!state.footerDock) state.footerDock = new FooterDockHost(state.model);
548
- setFooterDockProvider((width) => state.footerDock!.render(width));
549
- } else {
550
- // Widget-slot path (aboveEditor/belowEditor, or no footer sink for
551
- // "bottom"): ensure any stale footer dock is detached first.
552
- if (state.footerDock) {
553
- state.footerDock.dispose();
554
- state.footerDock = undefined;
555
- }
556
- setFooterDockProvider(undefined);
557
- }
558
-
559
- if (needsWidgetInstall && !dockInFooter) {
541
+ if (needsWidgetInstall) {
560
542
  const model = state.model;
561
543
  setExtensionWidget(ctx, WIDGET_KEY, ((_tui: unknown, theme: unknown) => new CrewWidgetComponent(model, theme, _tui)) as never, {
562
544
  placement: piPlacement,
@@ -567,12 +549,6 @@ export function updateCrewWidget(
567
549
  state.lastKey = WIDGET_KEY;
568
550
  state.lastMaxLines = maxLines;
569
551
  state.lastCwd = ctx.cwd;
570
- } else if (dockInFooter) {
571
- state.lastVisibility = "visible";
572
- state.lastPlacement = placement;
573
- state.lastKey = WIDGET_KEY;
574
- state.lastMaxLines = maxLines;
575
- state.lastCwd = ctx.cwd;
576
552
  }
577
553
 
578
554
  // Task list (aboveEditor): the run's plan progress — Claude Code / droid
@@ -614,9 +590,6 @@ export function stopCrewWidget(
614
590
  setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement: piPlacement });
615
591
  setExtensionWidget(ctx, TASKS_WIDGET_KEY, undefined, { placement: "aboveEditor" });
616
592
  state.lastTasksVisibility = "hidden";
617
- state.footerDock?.dispose();
618
- state.footerDock = undefined;
619
- setFooterDockProvider(undefined);
620
593
  state.lastVisibility = "hidden";
621
594
  state.lastPlacement = placement;
622
595
  state.lastKey = WIDGET_KEY;
@@ -274,6 +274,9 @@ export function tokenCountShort(count: number): string {
274
274
  }
275
275
 
276
276
  export interface DockUsageOptions {
277
+ /** Pinned render clock (D6-T4): callers that already hold ONE Date per
278
+ * paint pass it so every duration/age in a frame agrees to the tick. */
279
+ nowMs?: number;
277
280
  /** While the agent's pane is open: add tok/s + context % like pi-subtask. */
278
281
  viewed?: boolean;
279
282
  /** Model context window for the `P% / N` gauge; omitted when unknown. */
@@ -304,7 +307,7 @@ export function dockUsageText(agent: CrewAgentRecord, liveHandle?: LiveAgentHand
304
307
  if (typeof cost === "number" && Number.isFinite(cost) && cost > 0) parts.push(`$${cost.toFixed(4)}`);
305
308
  if (options.viewed) {
306
309
  const act = liveHandle.activity;
307
- const ms = computeLiveDurationMs(act);
310
+ const ms = computeLiveDurationMs(act, options.nowMs);
308
311
  const totalTokens = input + output + cacheWrite;
309
312
  if (totalTokens > 0 && ms > 1000) {
310
313
  const tps = Math.round(totalTokens / (ms / 1000));
@@ -403,7 +406,7 @@ function fitNameOnly(lead: string, name: string, suffix: string, width: number):
403
406
 
404
407
  // ── Agent stats line ──────────────────────────────────────────────────
405
408
 
406
- export function agentStats(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle): string {
409
+ export function agentStats(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle, nowMs?: number): string {
407
410
  const parts: string[] = [];
408
411
  if (liveHandle) {
409
412
  const act = liveHandle.activity;
@@ -422,7 +425,7 @@ export function agentStats(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle)
422
425
  } catch {
423
426
  /* ignore */
424
427
  }
425
- const ms = computeLiveDurationMs(act);
428
+ const ms = computeLiveDurationMs(act, nowMs);
426
429
  if (total > 0 && ms > 1000) {
427
430
  const tps = Math.round(total / (ms / 1000));
428
431
  if (tps > 0) parts.push(alignMetric(`${formatTokensCompact(tps)}/s`, TPS_METRIC_WIDTH));
@@ -438,7 +441,7 @@ export function agentStats(agent: CrewAgentRecord, liveHandle?: LiveAgentHandle)
438
441
  if (tokenCount && tokenCount > 0) parts.push(alignMetric(formatTokensCompact(tokenCount), TOKENS_METRIC_WIDTH));
439
442
  const cost = agentCost(agent);
440
443
  if (cost) parts.push(alignMetric(cost, COST_METRIC_WIDTH));
441
- const ageMs = agent.startedAt ? Math.max(0, Date.now() - new Date(agent.startedAt).getTime()) : 0;
444
+ const ageMs = agent.startedAt ? Math.max(0, (nowMs ?? Date.now()) - new Date(agent.startedAt).getTime()) : 0;
442
445
  if (tokenCount && tokenCount > 0 && ageMs > 1000) {
443
446
  const tps = Math.round(tokenCount / (ageMs / 1000));
444
447
  if (tps > 0) parts.push(alignMetric(`${formatTokensCompact(tps)}/s`, TPS_METRIC_WIDTH));