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.
- package/CHANGELOG.md +99 -0
- package/dist/index.mjs +34850 -34889
- package/package.json +170 -169
- package/scripts/watch-agent-transcript.mjs +120 -0
- package/src/extension/crew-input-router.ts +2 -0
- package/src/extension/crew-vibes/config.ts +1 -56
- package/src/extension/crew-vibes/figures.ts +5 -84
- package/src/extension/crew-vibes/font-detect.ts +1 -1
- package/src/extension/crew-vibes/index.ts +36 -328
- package/src/extension/crew-vibes/render.ts +2 -58
- package/src/extension/help.ts +1 -0
- package/src/extension/registration/command-registration.ts +5 -1
- package/src/extension/registration/commands/schedules.ts +212 -0
- package/src/extension/registration/commands/shared.ts +56 -1
- package/src/extension/registration/lifecycle-handlers.ts +16 -2
- package/src/extension/registration/schedule-toast-bridge.ts +109 -0
- package/src/extension/registration/viewers.ts +66 -1
- package/src/extension/team-tool/handle-schedule.ts +120 -1
- package/src/runtime/scheduling/scheduler.ts +120 -6
- package/src/runtime/settings-store.ts +28 -2
- package/src/state/coordination/locks.ts +17 -2
- package/src/ui/agents-jobs-browser.ts +785 -0
- package/src/ui/dashboard-panes/agents-pane.ts +7 -4
- package/src/ui/dashboard-panes/schedules-pane.ts +154 -0
- package/src/ui/inline-panel/crew-editor.ts +19 -6
- package/src/ui/inline-panel/index.ts +6 -0
- package/src/ui/key-utils.ts +3 -0
- package/src/ui/keybinding-map.ts +56 -2
- package/src/ui/mascot.ts +2 -1
- package/src/ui/overlays/agent-picker-overlay.ts +3 -3
- package/src/ui/overlays/confirm-overlay.ts +3 -2
- package/src/ui/overlays/mailbox-compose-overlay.ts +4 -3
- package/src/ui/overlays/mailbox-detail-overlay.ts +3 -3
- package/src/ui/run-dashboard.ts +257 -6
- package/src/ui/settings-overlay.ts +3 -3
- package/src/ui/transcript-viewer.ts +3 -2
- package/src/ui/widget/index.ts +61 -88
- package/src/ui/widget/widget-formatters.ts +7 -4
- package/src/ui/widget/widget-renderer.ts +134 -174
- package/src/ui/widget/widget-types.ts +0 -7
- package/src/utils/relative-time.ts +33 -0
- package/src/extension/crew-vibes/footer.ts +0 -313
- package/src/extension/crew-vibes/speed.ts +0 -286
- package/src/ui/dock-footer.ts +0 -49
package/src/ui/widget/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
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:
|
|
472
|
-
// crew-vibes footer
|
|
473
|
-
//
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
state.
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
504
|
-
|
|
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 (
|
|
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));
|