pi-crew 0.10.1 → 0.10.2
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 +98 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- package/agents/executor.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/dist/index.mjs +2781 -949
- package/package.json +1 -1
- package/schema.json +492 -94
- package/src/config/config-validation.ts +7 -1
- package/src/config/defaults.ts +8 -1
- package/src/config/role-tools.ts +4 -2
- package/src/config/types.ts +19 -1
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands/run.ts +15 -1
- package/src/extension/registration/commands/shared.ts +8 -0
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +52 -2
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/team-tool.ts +5 -1
- package/src/extension/registration/ui.ts +5 -4
- package/src/extension/team-tool/run-deadline.ts +20 -3
- package/src/extension/team-tool/run.ts +26 -2
- package/src/runtime/child-pi/child-pi-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-streams.ts +21 -1
- package/src/runtime/detached-run-results.ts +90 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +30 -3
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +5 -1
- package/src/runtime/merge-gate.ts +25 -9
- package/src/runtime/model/model-fallback.ts +7 -3
- package/src/runtime/process-status.ts +16 -1
- package/src/runtime/recovery/crash-recovery.ts +16 -0
- package/src/runtime/run-tracker.ts +77 -10
- package/src/runtime/team-runner.ts +3 -2
- package/src/schema/config-schema.ts +1 -1
- package/src/state/stores/plan-store.ts +1 -1
- package/src/state/stores/state-store.ts +73 -3
- package/src/state/types.ts +2 -0
- package/src/ui/dock-footer.ts +49 -0
- package/src/ui/inline-panel/agent-pane.ts +375 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/pi-ui-compat.ts +9 -0
- package/src/ui/widget/index.ts +176 -22
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +234 -37
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- package/src/utils/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/workflows/discover-workflows.ts +1 -0
- package/src/workflows/workflow-config.ts +7 -0
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* panel-store.ts — process-wide state shared by the inline panel's two halves.
|
|
3
|
+
*
|
|
4
|
+
* The editor wrapper mutates the cursor; the widget renderer and the transcript
|
|
5
|
+
* pane read it. Neither can own the state (the editor is created by pi's
|
|
6
|
+
* factory, the widget by another), so it lives here as a small observable
|
|
7
|
+
* singleton — the same shape pi-subtask keeps in its closure.
|
|
8
|
+
*
|
|
9
|
+
* Row projection is injected rather than computed here: the widget already owns
|
|
10
|
+
* the manifest/snapshot caches that produce the run list, and duplicating that
|
|
11
|
+
* read on every keypress would put disk I/O on the input path.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { PanelRow, PanelSelection, PanelTarget } from "./panel-selection.ts";
|
|
15
|
+
import { isAgentSelection } from "./panel-selection.ts";
|
|
16
|
+
|
|
17
|
+
let selection: PanelSelection = null;
|
|
18
|
+
let viewed: PanelTarget | undefined;
|
|
19
|
+
let rowsProvider: (() => PanelRow[]) | undefined;
|
|
20
|
+
const listeners = new Set<() => void>();
|
|
21
|
+
|
|
22
|
+
/** Install the row projection. Called once during panel wiring. */
|
|
23
|
+
export function setPanelRowsProvider(provider: (() => PanelRow[]) | undefined): void {
|
|
24
|
+
rowsProvider = provider;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Navigable rows, freshly projected on each call.
|
|
29
|
+
*
|
|
30
|
+
* A provider throw must never break input handling — a transient manifest read
|
|
31
|
+
* failure would otherwise make the panel swallow keys — so it degrades to an
|
|
32
|
+
* empty list, which the editor treats as "nothing to navigate".
|
|
33
|
+
*/
|
|
34
|
+
export function panelRows(): PanelRow[] {
|
|
35
|
+
if (!rowsProvider) return [];
|
|
36
|
+
try {
|
|
37
|
+
return rowsProvider();
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getPanelSelection(): PanelSelection {
|
|
44
|
+
return selection;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function setPanelSelection(next: PanelSelection): void {
|
|
48
|
+
if (next === selection) return;
|
|
49
|
+
if (isAgentSelection(selection) && isAgentSelection(next) && selection.runId === next.runId && selection.taskId === next.taskId) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
selection = next;
|
|
53
|
+
notifyPanelChange();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getViewedAgent(): PanelTarget | undefined {
|
|
57
|
+
return viewed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function setViewedAgent(next: PanelTarget | undefined): void {
|
|
61
|
+
if (viewed === next) return;
|
|
62
|
+
if (viewed && next && viewed.runId === next.runId && viewed.taskId === next.taskId) return;
|
|
63
|
+
viewed = next;
|
|
64
|
+
notifyPanelChange();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** True while the panel holds the cursor, i.e. keys are not going to the editor. */
|
|
68
|
+
export function isPanelFocused(): boolean {
|
|
69
|
+
return selection !== null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** What the widget renderer needs, in one read. */
|
|
73
|
+
export function panelDisplayState(): {
|
|
74
|
+
selectedTaskId: string | undefined;
|
|
75
|
+
viewedTaskId: string | undefined;
|
|
76
|
+
focused: boolean;
|
|
77
|
+
} {
|
|
78
|
+
return {
|
|
79
|
+
selectedTaskId: isAgentSelection(selection) ? selection.taskId : undefined,
|
|
80
|
+
viewedTaskId: viewed?.taskId,
|
|
81
|
+
// Cursor-driven: uncapping the row budget while the user is typing
|
|
82
|
+
// into the pane (viewed set, selection null) would jerk the layout on
|
|
83
|
+
// every pane open. The cursor entry is when the full list is needed.
|
|
84
|
+
focused: selection !== null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function subscribePanelChange(listener: () => void): () => void {
|
|
89
|
+
listeners.add(listener);
|
|
90
|
+
return () => {
|
|
91
|
+
listeners.delete(listener);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function notifyPanelChange(): void {
|
|
96
|
+
for (const listener of [...listeners]) {
|
|
97
|
+
try {
|
|
98
|
+
listener();
|
|
99
|
+
} catch {
|
|
100
|
+
// A failing repaint listener must not stop the others.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Session teardown / test isolation. */
|
|
106
|
+
export function resetPanelStore(): void {
|
|
107
|
+
selection = null;
|
|
108
|
+
viewed = undefined;
|
|
109
|
+
rowsProvider = undefined;
|
|
110
|
+
listeners.clear();
|
|
111
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* view-session-store.ts — session-switch guard for run protection.
|
|
3
|
+
*
|
|
4
|
+
* Agent views are in-document panes (see agent-pane.ts / inline-panel's
|
|
5
|
+
* openPane): viewing an agent NEVER switches, resumes, or tears down a
|
|
6
|
+
* session. This store is the one piece of session-switch machinery that
|
|
7
|
+
* remains, and it protects runs from switches the USER makes (/resume, /new,
|
|
8
|
+
* /fork): a switch tears the current session down via session.abort(), which
|
|
9
|
+
* fires the abort signal of any tool call still in flight — a foreground
|
|
10
|
+
* team run created by that tool is linked to the turn's abort, so ANY switch
|
|
11
|
+
* seconds after a run starts used to kill the run's workers ("Child Pi
|
|
12
|
+
* exited with 143" → run cancelled). While the guard is set, the
|
|
13
|
+
* caller-abort propagation is suppressed (run-deadline.ts).
|
|
14
|
+
*
|
|
15
|
+
* Set by the session_before_switch handler (runs before teardown's abort),
|
|
16
|
+
* cleared on the next session_start (the switch landed) and on reconcile.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
let sessionSwitchInFlight = false;
|
|
20
|
+
|
|
21
|
+
export function markSessionSwitchInFlight(): void {
|
|
22
|
+
sessionSwitchInFlight = true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function clearSessionSwitchInFlight(): void {
|
|
26
|
+
sessionSwitchInFlight = false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isSessionSwitchInFlight(): boolean {
|
|
30
|
+
return sessionSwitchInFlight;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Test isolation. */
|
|
34
|
+
export function resetCrewViewSessionState(): void {
|
|
35
|
+
sessionSwitchInFlight = false;
|
|
36
|
+
}
|
package/src/ui/pi-ui-compat.ts
CHANGED
|
@@ -46,6 +46,15 @@ export function setExtensionWidget(
|
|
|
46
46
|
ctx.ui.setWidget(key, content as never, widgetOptions as WidgetOptions);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Map the crew widget placement onto pi's `WidgetPlacement`. `"bottom"` is a
|
|
51
|
+
* crew-only placement (rendered inside the crew-vibes footer, below the quota
|
|
52
|
+
* lines); when a raw widget slot is involved it falls back to `belowEditor`.
|
|
53
|
+
*/
|
|
54
|
+
export function toPiWidgetPlacement(placement: "aboveEditor" | "belowEditor" | "bottom"): "aboveEditor" | "belowEditor" {
|
|
55
|
+
return placement === "bottom" ? "belowEditor" : placement;
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
type FooterFactory = (tui: unknown, theme: unknown, footerData: unknown) => unknown;
|
|
50
59
|
|
|
51
60
|
/** Install a custom footer component, or pass `undefined` to restore pi's built-in footer.
|
package/src/ui/widget/index.ts
CHANGED
|
@@ -11,6 +11,9 @@ 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
|
+
import { panelRowsFromRuns } from "../inline-panel/panel-rows.ts";
|
|
16
|
+
import { panelDisplayState, setPanelRowsProvider, subscribePanelChange } from "../inline-panel/panel-store.ts";
|
|
14
17
|
import { requestRender, requestRenderTarget, setExtensionWidget } from "../pi-ui-compat.ts";
|
|
15
18
|
import type { OverlaySchedulerHandle } from "../shared-overlay-scheduler.ts";
|
|
16
19
|
import { registerOverlayScheduler } from "../shared-overlay-scheduler.ts";
|
|
@@ -18,6 +21,7 @@ import type { RunSnapshotCache } from "../snapshot-types.ts";
|
|
|
18
21
|
import { spinnerBucket, spinnerFrame } from "../spinner.ts";
|
|
19
22
|
import type { CrewTheme } from "../theme-adapter.ts";
|
|
20
23
|
import { asCrewTheme, subscribeThemeChange } from "../theme-adapter.ts";
|
|
24
|
+
import { buildTaskListLines } from "./task-list.ts";
|
|
21
25
|
import { activeWidgetRuns, statusSummary } from "./widget-model.ts";
|
|
22
26
|
import { buildWidgetLines, colorWidgetLine, DEFAULT_WIDGET_WIDTH, renderLines } from "./widget-renderer.ts";
|
|
23
27
|
import type { CrewWidgetModel, CrewWidgetState, WidgetRun } from "./widget-types.ts";
|
|
@@ -62,6 +66,8 @@ export {
|
|
|
62
66
|
const MAX_LINES_DEFAULT = DEFAULT_UI.widgetMaxLines;
|
|
63
67
|
const LEGACY_WIDGET_KEY = "pi-crew";
|
|
64
68
|
const WIDGET_KEY = "pi-crew-active";
|
|
69
|
+
/** The run's plan progress, painted ABOVE the editor (task-list.ts). */
|
|
70
|
+
const TASKS_WIDGET_KEY = "pi-crew-tasks";
|
|
65
71
|
const STATUS_KEY = "pi-crew";
|
|
66
72
|
|
|
67
73
|
/**
|
|
@@ -78,11 +84,12 @@ const SIGNATURE_CACHE_TTL_MS = 100;
|
|
|
78
84
|
// next invalidate; a mid-run resize could briefly paint a frame at the old
|
|
79
85
|
// width. We register ONE debounced process-level listener (guarded so it never
|
|
80
86
|
// accumulates across widget reinstalls) that busts the active widget's cache
|
|
81
|
-
// and pokes Pi to repaint at the new width. The listener references
|
|
82
|
-
// module-level `
|
|
83
|
-
//
|
|
87
|
+
// and pokes Pi to repaint at the new width. The listener references the
|
|
88
|
+
// module-level `activeResizeTargets` set (every mounted widget) rather than
|
|
89
|
+
// specific instances, so replaced widgets do not leak listeners.
|
|
84
90
|
let resizeListenerInstalled = false;
|
|
85
|
-
|
|
91
|
+
/** All mounted widgets (dock + task list) — every one gets resize-busted. */
|
|
92
|
+
const activeResizeTargets = new Set<{ invalidate(): void; requestRepaint(): void }>();
|
|
86
93
|
let resizeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
87
94
|
|
|
88
95
|
/**
|
|
@@ -95,8 +102,10 @@ const onResize = (): void => {
|
|
|
95
102
|
// Debounce (~120ms) so a drag-resize doesn't thrash renders.
|
|
96
103
|
resizeTimer = setTimeout(() => {
|
|
97
104
|
resizeTimer = undefined;
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
for (const target of activeResizeTargets) {
|
|
106
|
+
target.invalidate();
|
|
107
|
+
target.requestRepaint();
|
|
108
|
+
}
|
|
100
109
|
}, 120);
|
|
101
110
|
};
|
|
102
111
|
|
|
@@ -134,7 +143,7 @@ export function uninstallResizeListener(): void {
|
|
|
134
143
|
clearTimeout(resizeTimer);
|
|
135
144
|
resizeTimer = undefined;
|
|
136
145
|
}
|
|
137
|
-
|
|
146
|
+
activeResizeTargets.clear();
|
|
138
147
|
process.off("SIGWINCH", onResize);
|
|
139
148
|
// Windows has no SIGWINCH; Node emits "resize" on stdout instead.
|
|
140
149
|
// Remove via whichever API is present. The listener was only registered
|
|
@@ -159,6 +168,8 @@ interface WidgetComponent extends CrewComponent {}
|
|
|
159
168
|
class CrewWidgetComponent implements WidgetComponent {
|
|
160
169
|
private readonly model: CrewWidgetModel;
|
|
161
170
|
private theme: CrewTheme;
|
|
171
|
+
/** Which surface this instance paints: the agent dock, or the task list. */
|
|
172
|
+
private readonly variant: "dock" | "tasks";
|
|
162
173
|
private cacheSignature = "";
|
|
163
174
|
/** C4 — invalidate-on-write cache for the buildSignature() result. */
|
|
164
175
|
private cachedBuildSignature = "";
|
|
@@ -169,10 +180,12 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
169
180
|
private cachedTheme: CrewTheme;
|
|
170
181
|
private readonly tui: unknown;
|
|
171
182
|
private readonly unsubscribeTheme: () => void;
|
|
183
|
+
private readonly unsubscribePanel: () => void;
|
|
172
184
|
private readonly schedulerHandle: OverlaySchedulerHandle;
|
|
173
185
|
|
|
174
|
-
constructor(model: CrewWidgetModel, themeLike: unknown, tui?: unknown) {
|
|
186
|
+
constructor(model: CrewWidgetModel, themeLike: unknown, tui?: unknown, variant: "dock" | "tasks" = "dock") {
|
|
175
187
|
this.model = model;
|
|
188
|
+
this.variant = variant;
|
|
176
189
|
this.theme = asCrewTheme(themeLike);
|
|
177
190
|
this.cachedTheme = this.theme;
|
|
178
191
|
this.tui = tui;
|
|
@@ -180,9 +193,16 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
180
193
|
// terminal-resize listener is installed. On a resize the cached width
|
|
181
194
|
// goes stale; busting the cache + requesting a repaint refreshes the
|
|
182
195
|
// widget at the new width without waiting for the next event tick (T-2).
|
|
183
|
-
|
|
196
|
+
activeResizeTargets.add(this);
|
|
184
197
|
installResizeListener();
|
|
185
198
|
this.unsubscribeTheme = subscribeThemeChange(themeLike, () => this.invalidate());
|
|
199
|
+
// Cursor movement is a keypress, not a run event, so it never reaches the
|
|
200
|
+
// shared scheduler. Repaint directly instead of waiting for the next host
|
|
201
|
+
// tick — a lagging cursor reads as a dropped keystroke.
|
|
202
|
+
this.unsubscribePanel = subscribePanelChange(() => {
|
|
203
|
+
this.invalidate();
|
|
204
|
+
this.requestRepaint();
|
|
205
|
+
});
|
|
186
206
|
// 1.10 (UI-P1-1): route run:state / worker:lifecycle / ui:invalidate
|
|
187
207
|
// through a RenderScheduler (debounce + fallback) instead of three
|
|
188
208
|
// direct runEventBus.onChannel subscriptions. With 3 overlays
|
|
@@ -265,8 +285,9 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
265
285
|
|
|
266
286
|
dispose(): void {
|
|
267
287
|
this.unsubscribeTheme();
|
|
288
|
+
this.unsubscribePanel();
|
|
268
289
|
this.schedulerHandle.dispose();
|
|
269
|
-
|
|
290
|
+
activeResizeTargets.delete(this);
|
|
270
291
|
}
|
|
271
292
|
|
|
272
293
|
render(width: number): string[] {
|
|
@@ -295,7 +316,35 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
295
316
|
const signature = `${sigBase}:${this.model.notificationCount ?? 0}`;
|
|
296
317
|
const runningGlyph = spinnerFrame("widget-header");
|
|
297
318
|
|
|
298
|
-
|
|
319
|
+
// Task-list variant: the run's plan above the editor (task-list.ts).
|
|
320
|
+
// No panel state, no spinner glyph — the list changes only on task
|
|
321
|
+
// transitions, which the run signature already covers.
|
|
322
|
+
if (this.variant === "tasks") {
|
|
323
|
+
if (this.cacheSignature !== signature || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
|
|
324
|
+
this.cachedBaseLines = buildTaskListLines(runs, width);
|
|
325
|
+
this.cachedLines = this.colorize(this.cachedBaseLines, width);
|
|
326
|
+
this.cachedWidth = width;
|
|
327
|
+
this.cachedTheme = this.theme;
|
|
328
|
+
this.cacheSignature = signature;
|
|
329
|
+
}
|
|
330
|
+
if (runs.length === 0) {
|
|
331
|
+
this.invalidate();
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
return this.cachedLines.map((line) => truncate(line, width));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Panel cursor/pane state is part of the rendered output, so it belongs in
|
|
338
|
+
// the cache key — otherwise moving the cursor would not repaint.
|
|
339
|
+
const panel = panelDisplayState();
|
|
340
|
+
const signatureWithPanel = `${signature}|panel:${panel.selectedTaskId ?? ""}/${panel.viewedTaskId ?? ""}/${panel.focused ? 1 : 0}`;
|
|
341
|
+
|
|
342
|
+
// The spinner-frame swap only belongs on the LEGACY header, whose line 0
|
|
343
|
+
// already starts with a glyph position (`<frame> Crew agents …`). The
|
|
344
|
+
// compact dock's line 0 is the HINT text ("agents (N) — ↓ to select"):
|
|
345
|
+
// swapping would visibly eat its first character on every frame.
|
|
346
|
+
const compactDock = this.model.rowStyle === "compact";
|
|
347
|
+
if (this.cacheSignature !== signatureWithPanel || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
|
|
299
348
|
this.cachedBaseLines = buildWidgetLines(
|
|
300
349
|
this.model.cwd,
|
|
301
350
|
0,
|
|
@@ -303,14 +352,15 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
303
352
|
runs,
|
|
304
353
|
this.model.notificationCount ?? 0,
|
|
305
354
|
width,
|
|
355
|
+
{ rowStyle: this.model.rowStyle, ...panel },
|
|
306
356
|
).map((line, index) => {
|
|
307
|
-
if (index === 0 && line.length > 0) return `${runningGlyph}${line.slice(1)}`;
|
|
357
|
+
if (!compactDock && index === 0 && line.length > 0) return `${runningGlyph}${line.slice(1)}`;
|
|
308
358
|
return line;
|
|
309
359
|
});
|
|
310
360
|
this.cachedLines = this.colorize(this.cachedBaseLines, width);
|
|
311
361
|
this.cachedWidth = width;
|
|
312
362
|
this.cachedTheme = this.theme;
|
|
313
|
-
this.cacheSignature =
|
|
363
|
+
this.cacheSignature = signatureWithPanel;
|
|
314
364
|
}
|
|
315
365
|
|
|
316
366
|
if (runs.length === 0) {
|
|
@@ -323,12 +373,43 @@ class CrewWidgetComponent implements WidgetComponent {
|
|
|
323
373
|
return [];
|
|
324
374
|
}
|
|
325
375
|
|
|
326
|
-
|
|
327
|
-
|
|
376
|
+
if (!compactDock) {
|
|
377
|
+
const updatedHeader = `${runningGlyph}${this.cachedBaseLines[0]?.slice(1) ?? ""}`;
|
|
378
|
+
this.cachedLines[0] = truncate(colorWidgetLine(updatedHeader, 0, this.theme), width);
|
|
379
|
+
}
|
|
328
380
|
return this.cachedLines.map((line) => truncate(line, width));
|
|
329
381
|
}
|
|
330
382
|
}
|
|
331
383
|
|
|
384
|
+
// ── Footer dock host (widgetPlacement: "bottom") ──────────────────────
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Dock host for `widgetPlacement: "bottom"`. Keeps a single CrewWidgetComponent
|
|
388
|
+
* (theme-less → raw lines, no ANSI) per session and feeds its render output to
|
|
389
|
+
* the crew-vibes footer through the dock-footer registry. The footer colors the
|
|
390
|
+
* lines with ITS OWN theme so the dock matches the footer context. All caching,
|
|
391
|
+
* event wiring (panel changes, run events, resize) lives in the wrapped
|
|
392
|
+
* component; the footer is re-rendered by pi on every host repaint.
|
|
393
|
+
*/
|
|
394
|
+
class FooterDockHost {
|
|
395
|
+
private component: CrewWidgetComponent | undefined;
|
|
396
|
+
private readonly model: CrewWidgetModel;
|
|
397
|
+
|
|
398
|
+
constructor(model: CrewWidgetModel) {
|
|
399
|
+
this.model = model;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
render(width: number): string[] {
|
|
403
|
+
if (!this.component) this.component = new CrewWidgetComponent(this.model, undefined, undefined);
|
|
404
|
+
return this.component.render(width);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
dispose(): void {
|
|
408
|
+
this.component?.dispose();
|
|
409
|
+
this.component = undefined;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
332
413
|
// ── Re-export listLiveAgents for buildSignature ───────────────────────
|
|
333
414
|
|
|
334
415
|
import { listLiveAgents } from "../../runtime/live-session/live-agent-manager.ts";
|
|
@@ -355,20 +436,39 @@ export function updateCrewWidget(
|
|
|
355
436
|
}
|
|
356
437
|
|
|
357
438
|
const runs = activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache, preloadedManifests, workspaceId);
|
|
358
|
-
const
|
|
439
|
+
const rowStyle = config?.widgetRowStyle ?? DEFAULT_UI.widgetRowStyle;
|
|
440
|
+
// The inline panel navigates the same run list the widget paints, and this is
|
|
441
|
+
// the only place already holding the manifest/snapshot caches — so the row
|
|
442
|
+
// projection is registered here instead of re-reading state on every keypress.
|
|
443
|
+
setPanelRowsProvider(() => panelRowsFromRuns(activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache, preloadedManifests, workspaceId)));
|
|
444
|
+
const lines = buildWidgetLines(ctx.cwd, state.frame, maxLines, runs, state.notificationCount ?? 0, getRenderWidth(), {
|
|
445
|
+
rowStyle,
|
|
446
|
+
...panelDisplayState(),
|
|
447
|
+
});
|
|
359
448
|
const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
|
|
449
|
+
// `bottom` is not a pi widget slot: the dock then renders inside the
|
|
450
|
+
// crew-vibes footer (dock-footer registry). pi's slot calls always use a
|
|
451
|
+
// real slot so legacy-clear/installs stay on maps pi understands.
|
|
452
|
+
const bottomMode = placement === "bottom";
|
|
453
|
+
const dockInFooter = bottomMode && isFooterDockSinkActive();
|
|
454
|
+
const piPlacement: "aboveEditor" | "belowEditor" = bottomMode ? "belowEditor" : placement;
|
|
360
455
|
|
|
361
456
|
ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(runs) : undefined);
|
|
362
457
|
|
|
363
458
|
const shouldClearLegacy = state.legacyCleared !== true || state.lastPlacement !== placement;
|
|
364
459
|
if (shouldClearLegacy) {
|
|
365
|
-
setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement });
|
|
460
|
+
setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement: piPlacement });
|
|
366
461
|
state.legacyCleared = true;
|
|
367
462
|
}
|
|
368
463
|
|
|
369
464
|
if (!lines.length) {
|
|
370
465
|
if (state.lastVisibility !== "hidden" || state.lastPlacement !== placement) {
|
|
371
|
-
setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement });
|
|
466
|
+
setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement: piPlacement });
|
|
467
|
+
setExtensionWidget(ctx, TASKS_WIDGET_KEY, undefined, { placement: "aboveEditor" });
|
|
468
|
+
state.lastTasksVisibility = "hidden";
|
|
469
|
+
state.footerDock?.dispose();
|
|
470
|
+
state.footerDock = undefined;
|
|
471
|
+
setFooterDockProvider(undefined);
|
|
372
472
|
state.lastVisibility = "hidden";
|
|
373
473
|
state.lastPlacement = placement;
|
|
374
474
|
state.lastKey = WIDGET_KEY;
|
|
@@ -398,6 +498,7 @@ export function updateCrewWidget(
|
|
|
398
498
|
snapshotCache,
|
|
399
499
|
preloadManifests: preloadedManifests,
|
|
400
500
|
workspaceId,
|
|
501
|
+
rowStyle,
|
|
401
502
|
};
|
|
402
503
|
else {
|
|
403
504
|
state.model.cwd = ctx.cwd;
|
|
@@ -408,12 +509,33 @@ export function updateCrewWidget(
|
|
|
408
509
|
state.model.snapshotCache = snapshotCache;
|
|
409
510
|
state.model.preloadManifests = preloadedManifests;
|
|
410
511
|
state.model.workspaceId = workspaceId;
|
|
512
|
+
state.model.rowStyle = rowStyle;
|
|
411
513
|
}
|
|
412
514
|
|
|
413
|
-
if (
|
|
515
|
+
if (dockInFooter) {
|
|
516
|
+
// Keep pi's widget slot free: the crew-vibes footer paints the dock at
|
|
517
|
+
// the very bottom, below the quota/meter lines. A widget-slot install
|
|
518
|
+
// from a PREVIOUS placement (or a sink that was just enabled) must be
|
|
519
|
+
// removed first.
|
|
520
|
+
if (needsWidgetInstall && state.lastKey === WIDGET_KEY) {
|
|
521
|
+
setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement: piPlacement });
|
|
522
|
+
}
|
|
523
|
+
if (!state.footerDock) state.footerDock = new FooterDockHost(state.model);
|
|
524
|
+
setFooterDockProvider((width) => state.footerDock!.render(width));
|
|
525
|
+
} else {
|
|
526
|
+
// Widget-slot path (aboveEditor/belowEditor, or no footer sink for
|
|
527
|
+
// "bottom"): ensure any stale footer dock is detached first.
|
|
528
|
+
if (state.footerDock) {
|
|
529
|
+
state.footerDock.dispose();
|
|
530
|
+
state.footerDock = undefined;
|
|
531
|
+
}
|
|
532
|
+
setFooterDockProvider(undefined);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (needsWidgetInstall && !dockInFooter) {
|
|
414
536
|
const model = state.model;
|
|
415
537
|
setExtensionWidget(ctx, WIDGET_KEY, ((_tui: unknown, theme: unknown) => new CrewWidgetComponent(model, theme, _tui)) as never, {
|
|
416
|
-
placement,
|
|
538
|
+
placement: piPlacement,
|
|
417
539
|
persist: true,
|
|
418
540
|
});
|
|
419
541
|
state.lastVisibility = "visible";
|
|
@@ -421,6 +543,32 @@ export function updateCrewWidget(
|
|
|
421
543
|
state.lastKey = WIDGET_KEY;
|
|
422
544
|
state.lastMaxLines = maxLines;
|
|
423
545
|
state.lastCwd = ctx.cwd;
|
|
546
|
+
} else if (dockInFooter) {
|
|
547
|
+
state.lastVisibility = "visible";
|
|
548
|
+
state.lastPlacement = placement;
|
|
549
|
+
state.lastKey = WIDGET_KEY;
|
|
550
|
+
state.lastMaxLines = maxLines;
|
|
551
|
+
state.lastCwd = ctx.cwd;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Task list (aboveEditor): the run's plan progress — Claude Code / droid
|
|
555
|
+
// style, independent of the dock's placement (so it also shows when the
|
|
556
|
+
// dock renders in the crew-vibes footer). Installed once while any
|
|
557
|
+
// display-active run exists; the component paints nothing until a run
|
|
558
|
+
// carries a tasks slice.
|
|
559
|
+
const tasksVisible = runs.length > 0 && Boolean(state.model);
|
|
560
|
+
if (tasksVisible && state.lastTasksVisibility !== "visible") {
|
|
561
|
+
const model = state.model;
|
|
562
|
+
setExtensionWidget(
|
|
563
|
+
ctx,
|
|
564
|
+
TASKS_WIDGET_KEY,
|
|
565
|
+
((_tui: unknown, theme: unknown) => new CrewWidgetComponent(model, theme, _tui, "tasks")) as never,
|
|
566
|
+
{ placement: "aboveEditor" },
|
|
567
|
+
);
|
|
568
|
+
state.lastTasksVisibility = "visible";
|
|
569
|
+
} else if (!tasksVisible && state.lastTasksVisibility === "visible") {
|
|
570
|
+
setExtensionWidget(ctx, TASKS_WIDGET_KEY, undefined, { placement: "aboveEditor" });
|
|
571
|
+
state.lastTasksVisibility = "hidden";
|
|
424
572
|
}
|
|
425
573
|
|
|
426
574
|
requestRender(ctx);
|
|
@@ -436,9 +584,15 @@ export function stopCrewWidget(
|
|
|
436
584
|
uninstallResizeListener();
|
|
437
585
|
if (ctx?.hasUI) {
|
|
438
586
|
const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
|
|
587
|
+
const piPlacement: "aboveEditor" | "belowEditor" = placement === "bottom" ? "belowEditor" : placement;
|
|
439
588
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
440
|
-
setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement });
|
|
441
|
-
setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement });
|
|
589
|
+
setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement: piPlacement });
|
|
590
|
+
setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement: piPlacement });
|
|
591
|
+
setExtensionWidget(ctx, TASKS_WIDGET_KEY, undefined, { placement: "aboveEditor" });
|
|
592
|
+
state.lastTasksVisibility = "hidden";
|
|
593
|
+
state.footerDock?.dispose();
|
|
594
|
+
state.footerDock = undefined;
|
|
595
|
+
setFooterDockProvider(undefined);
|
|
442
596
|
state.lastVisibility = "hidden";
|
|
443
597
|
state.lastPlacement = placement;
|
|
444
598
|
state.lastKey = WIDGET_KEY;
|