pi-crew 0.9.50 → 0.9.51
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 +115 -0
- package/dist/build-meta.json +42 -33
- package/dist/index.mjs +149 -54
- package/dist/index.mjs.map +4 -4
- package/docs/decisions/2026-07-26-c6-mascot-visibility-not-wired.md +84 -0
- package/package.json +1 -1
- package/skills/distill-software/SKILL.md +24 -1
- package/src/ui/live-run-sidebar.ts +5 -13
- package/src/ui/loaders.ts +6 -176
- package/src/ui/mascot.ts +14 -1
- package/src/ui/render-scheduler.ts +60 -6
- package/src/ui/run-dashboard.ts +12 -21
- package/src/ui/shared-overlay-scheduler.ts +96 -0
- package/src/ui/widget/index.ts +45 -11
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
2
|
+
import { RenderScheduler } from "./render-scheduler.ts";
|
|
3
|
+
import { runEventBusAsRenderScheduler } from "./run-event-bus.ts";
|
|
4
|
+
|
|
5
|
+
export interface OverlaySchedulerHandle {
|
|
6
|
+
/** Force a debounced re-render fan-out (e.g. theme change, dashboard interaction). */
|
|
7
|
+
schedule(): void;
|
|
8
|
+
/** Unregister this overlay. Disposes the shared scheduler when the last overlay leaves (ref-count). */
|
|
9
|
+
dispose(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* C3 (Option G): the 3 overlays (widget + sidebar + dashboard) each used to
|
|
14
|
+
* instantiate an identical RenderScheduler (same run-event-bus channels,
|
|
15
|
+
* debounce 75ms, fallback 750ms) → 9 subscriptions + 9 live timers + 3×
|
|
16
|
+
* per-event schedule() CPU during active runs. This module owns ONE shared
|
|
17
|
+
* RenderScheduler (module singleton) whose `render` and `onInvalidate`
|
|
18
|
+
* fan out to all registered overlays, with per-callback try/catch isolation
|
|
19
|
+
* + ref-counted disposal (the underlying scheduler is disposed when the last
|
|
20
|
+
* overlay leaves, and a future register recreates it).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const CHANNELS = ["run:state", "worker:lifecycle", "ui:invalidate"] as const;
|
|
24
|
+
|
|
25
|
+
interface SharedOverlayState {
|
|
26
|
+
readonly scheduler: RenderScheduler;
|
|
27
|
+
readonly renderers: Set<() => void>;
|
|
28
|
+
readonly invalidators: Set<(payload: unknown) => void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let shared: SharedOverlayState | undefined;
|
|
32
|
+
|
|
33
|
+
function ensureShared(): SharedOverlayState {
|
|
34
|
+
if (shared) return shared;
|
|
35
|
+
const renderers = new Set<() => void>();
|
|
36
|
+
const invalidators = new Set<(payload: unknown) => void>();
|
|
37
|
+
const scheduler = new RenderScheduler(
|
|
38
|
+
runEventBusAsRenderScheduler([...CHANNELS]),
|
|
39
|
+
() => {
|
|
40
|
+
// Fan-out: each registered overlay's render runs in its own
|
|
41
|
+
// try/catch so one failing component cannot break the others.
|
|
42
|
+
for (const render of renderers) {
|
|
43
|
+
try {
|
|
44
|
+
render();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logInternalError("shared-overlay-scheduler.render", error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
debounceMs: 75,
|
|
52
|
+
fallbackMs: 750,
|
|
53
|
+
events: [...CHANNELS],
|
|
54
|
+
onInvalidate: (payload) => {
|
|
55
|
+
for (const invalidate of invalidators) {
|
|
56
|
+
try {
|
|
57
|
+
invalidate(payload);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logInternalError("shared-overlay-scheduler.invalidate", error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
shared = { scheduler, renderers, invalidators };
|
|
66
|
+
return shared;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Register an overlay's render (+ optional onInvalidate) with the shared
|
|
71
|
+
* scheduler. Returns a handle that lets the overlay force a debounced
|
|
72
|
+
* fan-out (schedule) and unregister itself (dispose). The underlying shared
|
|
73
|
+
* scheduler is created lazily on first register and disposed when the last
|
|
74
|
+
* overlay disposes.
|
|
75
|
+
*/
|
|
76
|
+
export function registerOverlayScheduler(render: () => void, onInvalidate?: (payload: unknown) => void): OverlaySchedulerHandle {
|
|
77
|
+
const state = ensureShared();
|
|
78
|
+
state.renderers.add(render);
|
|
79
|
+
if (onInvalidate) state.invalidators.add(onInvalidate);
|
|
80
|
+
return {
|
|
81
|
+
schedule: () => {
|
|
82
|
+
state.scheduler.schedule();
|
|
83
|
+
},
|
|
84
|
+
dispose: () => {
|
|
85
|
+
state.renderers.delete(render);
|
|
86
|
+
if (onInvalidate) state.invalidators.delete(onInvalidate);
|
|
87
|
+
// Ref-count: when the last overlay leaves, dispose the underlying
|
|
88
|
+
// scheduler (clears subscriptions + timers) and reset the module
|
|
89
|
+
// variable so a future register recreates a fresh instance.
|
|
90
|
+
if (state.renderers.size === 0) {
|
|
91
|
+
state.scheduler.dispose();
|
|
92
|
+
if (shared === state) shared = undefined;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/ui/widget/index.ts
CHANGED
|
@@ -12,8 +12,8 @@ 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
14
|
import { requestRender, requestRenderTarget, setExtensionWidget } from "../pi-ui-compat.ts";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import type { OverlaySchedulerHandle } from "../shared-overlay-scheduler.ts";
|
|
16
|
+
import { registerOverlayScheduler } from "../shared-overlay-scheduler.ts";
|
|
17
17
|
import type { RunSnapshotCache } from "../snapshot-types.ts";
|
|
18
18
|
import { spinnerBucket, spinnerFrame } from "../spinner.ts";
|
|
19
19
|
import type { CrewTheme } from "../theme-adapter.ts";
|
|
@@ -64,6 +64,15 @@ const LEGACY_WIDGET_KEY = "pi-crew";
|
|
|
64
64
|
const WIDGET_KEY = "pi-crew-active";
|
|
65
65
|
const STATUS_KEY = "pi-crew";
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* C4 — short-TTL safety net for the buildSignature() result cache. Combined
|
|
69
|
+
* with invalidate-on-write (onInvalidate clears the cache on every real
|
|
70
|
+
* event), this prevents redundant O(runs×agents) signature recomputation
|
|
71
|
+
* during a burst of host-driven renders within one state snapshot while
|
|
72
|
+
* ensuring the signature is always recomputed after genuine data changes.
|
|
73
|
+
*/
|
|
74
|
+
const SIGNATURE_CACHE_TTL_MS = 100;
|
|
75
|
+
|
|
67
76
|
// ── Terminal resize handling (T-2) ────────────────────────────────────
|
|
68
77
|
// On a terminal resize the widget's cached render width goes stale until the
|
|
69
78
|
// next invalidate; a mid-run resize could briefly paint a frame at the old
|
|
@@ -104,13 +113,16 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
104
113
|
private readonly model: CrewWidgetModel;
|
|
105
114
|
private theme: CrewTheme;
|
|
106
115
|
private cacheSignature = "";
|
|
116
|
+
/** C4 — invalidate-on-write cache for the buildSignature() result. */
|
|
117
|
+
private cachedBuildSignature = "";
|
|
118
|
+
private cachedBuildSignatureAt = 0;
|
|
107
119
|
private cachedWidth = 0;
|
|
108
120
|
private cachedLines: string[] = [];
|
|
109
121
|
private cachedBaseLines: string[] = [];
|
|
110
122
|
private cachedTheme: CrewTheme;
|
|
111
123
|
private readonly tui: unknown;
|
|
112
124
|
private readonly unsubscribeTheme: () => void;
|
|
113
|
-
private readonly
|
|
125
|
+
private readonly schedulerHandle: OverlaySchedulerHandle;
|
|
114
126
|
|
|
115
127
|
constructor(model: CrewWidgetModel, themeLike: unknown, tui?: unknown) {
|
|
116
128
|
this.model = model;
|
|
@@ -130,13 +142,18 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
130
142
|
// subscribing independently a single event triggered up to 9 callbacks
|
|
131
143
|
// and ~150 invalidates/sec under load. The scheduler collapses bursts
|
|
132
144
|
// into one debounced invalidate.
|
|
133
|
-
this.
|
|
134
|
-
runEventBusAsRenderScheduler(["run:state", "worker:lifecycle", "ui:invalidate"]),
|
|
145
|
+
this.schedulerHandle = registerOverlayScheduler(
|
|
135
146
|
() => this.invalidate(),
|
|
136
|
-
{
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
147
|
+
() => {
|
|
148
|
+
// C4 invalidate-on-write: drop the cached buildSignature()
|
|
149
|
+
// result immediately when a real event arrives (run:state /
|
|
150
|
+
// worker:lifecycle / ui:invalidate). The prior blind-TTL
|
|
151
|
+
// attempt (reverted in 619a0cd) held a stale signature
|
|
152
|
+
// within the window and skipped re-render on genuine state
|
|
153
|
+
// changes. Clearing here + in invalidate() guarantees the
|
|
154
|
+
// next render tick always recomputes from fresh data.
|
|
155
|
+
this.cachedBuildSignature = "";
|
|
156
|
+
this.cachedBuildSignatureAt = 0;
|
|
140
157
|
},
|
|
141
158
|
);
|
|
142
159
|
}
|
|
@@ -190,6 +207,8 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
190
207
|
this.cacheSignature = "";
|
|
191
208
|
this.cachedBaseLines = [];
|
|
192
209
|
this.cachedLines = [];
|
|
210
|
+
this.cachedBuildSignature = "";
|
|
211
|
+
this.cachedBuildSignatureAt = 0;
|
|
193
212
|
}
|
|
194
213
|
|
|
195
214
|
/** Poke the host TUI to repaint immediately (defensive: no-op if unavailable). */
|
|
@@ -199,13 +218,28 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
199
218
|
|
|
200
219
|
dispose(): void {
|
|
201
220
|
this.unsubscribeTheme();
|
|
202
|
-
this.
|
|
221
|
+
this.schedulerHandle.dispose();
|
|
203
222
|
if (activeResizeTarget === this) activeResizeTarget = undefined;
|
|
204
223
|
}
|
|
205
224
|
|
|
206
225
|
render(width: number): string[] {
|
|
207
226
|
const runs = activeWidgetRuns(this.model.cwd, this.model.manifestCache, this.model.snapshotCache, this.model.preloadManifests);
|
|
208
|
-
|
|
227
|
+
// C4: invalidate-on-write signature cache. buildSignature() is
|
|
228
|
+
// O(runs×agents) string work called on every ~160ms host-driven render
|
|
229
|
+
// tick. Within one state snapshot (no event arrived → cache not cleared
|
|
230
|
+
// by onInvalidate) the result is identical, so we reuse the cached value
|
|
231
|
+
// to skip redundant recomputation. A short TTL acts as a safety net for
|
|
232
|
+
// the unlikely case where a real event is missed.
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
let sigBase: string;
|
|
235
|
+
if (this.cachedBuildSignature !== "" && now - this.cachedBuildSignatureAt < SIGNATURE_CACHE_TTL_MS) {
|
|
236
|
+
sigBase = this.cachedBuildSignature;
|
|
237
|
+
} else {
|
|
238
|
+
sigBase = this.buildSignature(runs);
|
|
239
|
+
this.cachedBuildSignature = sigBase;
|
|
240
|
+
this.cachedBuildSignatureAt = now;
|
|
241
|
+
}
|
|
242
|
+
const signature = `${sigBase}:${this.model.notificationCount ?? 0}`;
|
|
209
243
|
const runningGlyph = spinnerFrame("widget-header");
|
|
210
244
|
|
|
211
245
|
if (this.cacheSignature !== signature || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
|