pi-crew 0.10.6 β 0.11.1
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 +215 -0
- package/README.md +0 -1
- package/dist/index.mjs +30623 -29844
- package/package.json +170 -169
- package/schema.json +10 -0
- package/scripts/watch-agent-transcript.mjs +120 -0
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
- package/skills/real-test-pi-crew/SKILL.md +181 -1
- package/src/config/config-validation.ts +0 -2
- package/src/config/defaults.ts +0 -1
- package/src/config/types.ts +0 -5
- package/src/extension/crew-input-router.ts +2 -0
- package/src/extension/crew-vibes/config.ts +1 -77
- package/src/extension/crew-vibes/index.ts +36 -330
- package/src/extension/crew-vibes/render.ts +1 -106
- package/src/extension/help.ts +1 -2
- package/src/extension/register.ts +7 -2
- package/src/extension/registration/command-registration.ts +5 -1
- package/src/extension/registration/commands/manage.ts +5 -4
- package/src/extension/registration/commands/schedules.ts +212 -0
- package/src/extension/registration/commands/shared.ts +56 -2
- package/src/extension/registration/lifecycle-handlers.ts +16 -2
- package/src/extension/registration/schedule-toast-bridge.ts +109 -0
- package/src/extension/registration/subagent-tools.ts +16 -6
- package/src/extension/registration/team-tool.ts +10 -3
- package/src/extension/registration/terminal-status-wiring.ts +172 -0
- package/src/extension/registration/viewers.ts +72 -1
- package/src/extension/team-tool/handle-schedule.ts +120 -1
- package/src/extension/team-tool/handle-settings.ts +14 -2
- package/src/runtime/scheduling/scheduler.ts +120 -6
- package/src/runtime/settings-store.ts +28 -2
- package/src/runtime/task-display.ts +1 -1
- package/src/schema/config-schema.ts +4 -0
- package/src/state/coordination/locks.ts +17 -2
- package/src/ui/adaptive-card.ts +65 -0
- package/src/ui/agents-jobs-browser.ts +791 -0
- package/src/ui/card-colors.ts +36 -7
- package/src/ui/dashboard-panes/agents-pane.ts +52 -17
- package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
- package/src/ui/dashboard-panes/health-pane.ts +7 -5
- package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
- package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
- package/src/ui/dashboard-panes/pane-theme.ts +21 -0
- package/src/ui/dashboard-panes/plan-pane.ts +63 -30
- package/src/ui/dashboard-panes/progress-pane.ts +3 -2
- package/src/ui/dashboard-panes/schedules-pane.ts +177 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
- package/src/ui/dwf-phase-display.ts +3 -20
- package/src/ui/format-helpers.ts +22 -0
- package/src/ui/inline-panel/crew-editor.ts +32 -9
- package/src/ui/inline-panel/index.ts +66 -4
- package/src/ui/key-utils.ts +3 -0
- package/src/ui/keybinding-map.ts +286 -34
- package/src/ui/live-conversation-overlay.ts +176 -46
- package/src/ui/live-run-sidebar.ts +134 -55
- package/src/ui/mascot.ts +34 -17
- package/src/ui/overlays/agent-picker-overlay.ts +81 -26
- package/src/ui/overlays/confirm-overlay.ts +55 -28
- package/src/ui/overlays/help-overlay.ts +108 -53
- package/src/ui/overlays/mailbox-compose-overlay.ts +89 -49
- package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
- package/src/ui/powerbar-publisher.ts +0 -1
- package/src/ui/rail.ts +333 -0
- package/src/ui/run-dashboard.ts +344 -72
- package/src/ui/settings-overlay.ts +84 -42
- package/src/ui/terminal-status.ts +7 -1
- package/src/ui/theme-adapter.ts +0 -45
- package/src/ui/theme-discovery.ts +12 -6
- package/src/ui/tool-progress-formatter.ts +126 -8
- package/src/ui/tool-renderers/brief-mode.ts +8 -66
- package/src/ui/tool-renderers/index.ts +372 -521
- package/src/ui/transcript-viewer.ts +33 -14
- package/src/ui/widget/index.ts +86 -133
- package/src/ui/widget/task-list.ts +64 -32
- package/src/ui/widget/widget-formatters.ts +3 -399
- package/src/ui/widget/widget-model.ts +28 -7
- package/src/ui/widget/widget-renderer.ts +298 -265
- package/src/ui/widget/widget-types.ts +0 -9
- package/src/utils/relative-time.ts +33 -0
- package/src/utils/visual.ts +0 -4
- package/src/extension/crew-vibes/figures.ts +0 -101
- package/src/extension/crew-vibes/font-detect.ts +0 -71
- 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/dynamic-border.ts +0 -35
- package/src/ui/loaders.ts +0 -6
- package/src/ui/overlay-stack.ts +0 -148
|
@@ -4,30 +4,22 @@
|
|
|
4
4
|
* Extracted from crew-widget.ts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { getCrewScheduler, getScheduledJobs, getScheduledJobsHiddenCountView } from "../../extension/team-tool/handle-schedule.ts";
|
|
7
8
|
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
8
|
-
import { listLiveAgents } from "../../runtime/live-session/live-agent-manager.ts";
|
|
9
9
|
import { isPlanApprovalStatePending } from "../../runtime/plan-approval.ts";
|
|
10
10
|
import { isFinishedRunStatus } from "../../runtime/process-status.ts";
|
|
11
|
-
import type {
|
|
12
|
-
import {
|
|
11
|
+
import type { ScheduledJob } from "../../runtime/scheduling/scheduler.ts";
|
|
12
|
+
import { formatRelativeTime } from "../../utils/relative-time.ts";
|
|
13
|
+
import { truncate, visibleWidth } from "../../utils/visual.ts";
|
|
13
14
|
import { Box, Text } from "../layout-primitives.ts";
|
|
15
|
+
import { ACTIVE, RAIL, type RailSlot, railLeaders, railRaw, shortId, statusSlot } from "../rail.ts";
|
|
14
16
|
import { spinnerFrame } from "../spinner.ts";
|
|
15
|
-
import { colorizeStatusGlyphs
|
|
16
|
-
import type
|
|
17
|
-
import {
|
|
18
|
-
agentActivity,
|
|
19
|
-
agentStats,
|
|
20
|
-
budgetedRow,
|
|
21
|
-
dockElapsed,
|
|
22
|
-
dockStatusIcon,
|
|
23
|
-
dockStatusLabel,
|
|
24
|
-
dockUsageText,
|
|
25
|
-
notificationBadge,
|
|
26
|
-
} from "./widget-formatters.ts";
|
|
17
|
+
import { colorizeStatusGlyphs } from "../status-colors.ts";
|
|
18
|
+
import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
|
|
19
|
+
import { notificationBadge } from "./widget-formatters.ts";
|
|
27
20
|
import { activeWidgetRuns, shortRunLabel } from "./widget-model.ts";
|
|
28
21
|
import type { WidgetRun } from "./widget-types.ts";
|
|
29
22
|
|
|
30
|
-
export const MAX_AGENTS_DISPLAY = 3;
|
|
31
23
|
const FINISHED_LINGER_MAX_AGE = 1;
|
|
32
24
|
/** Default terminal width when caller doesn't pass one explicitly. Keep <= 116
|
|
33
25
|
* (the same default used elsewhere in pi-crew tool renderers) so we never paint
|
|
@@ -39,19 +31,158 @@ export const TASK_DESC_MAX = 60;
|
|
|
39
31
|
const ERROR_LINGER_MAX_AGE = 2;
|
|
40
32
|
const ERROR_STATUSES = new Set(["failed", "cancelled", "stopped", "needs_attention"]);
|
|
41
33
|
|
|
34
|
+
// ββ RAIL dock composition (design system Β§2.B) βββββββββββββββββββββββββ
|
|
35
|
+
//
|
|
36
|
+
// The dock is the ONE surface that must never grow a second row: it speaks the
|
|
37
|
+
// rail grammar with the BODY glyph (`β`) only β never `β`/`β`, which imply a
|
|
38
|
+
// multi-line card. The builders below return PLAIN text (that is the exported
|
|
39
|
+
// contract: `buildWidgetLines` is the raw row, `colorWidgetLine` the paint
|
|
40
|
+
// pass), but they COMPOSE through the shared rail helpers with a no-op theme
|
|
41
|
+
// so the glyph/leader vocabulary lives in `rail.ts` alone.
|
|
42
|
+
const PLAIN_THEME = asCrewTheme(undefined);
|
|
43
|
+
|
|
44
|
+
/** Identity word of the dock + status bar. */
|
|
45
|
+
const DOCK_WORD = "CREW";
|
|
46
|
+
|
|
47
|
+
/** The dock's tail: four leaders + the one-key entry hint. */
|
|
48
|
+
const DOCK_HINT_RIGHT = "βΒ·enter";
|
|
49
|
+
const DOCK_HINT = `Β·Β·Β·Β· ${DOCK_HINT_RIGHT}`;
|
|
50
|
+
|
|
51
|
+
/** The dock's FOCUS marker (RAIL Β§2.B: "focused row keeps the `β― ` prefix").
|
|
52
|
+
* It is deliberately not `rail.ts`'s `CURSOR` (`βΊ`) β that glyph marks the
|
|
53
|
+
* selected row of a LIST; the dock is a single line, and `β―` is the marker
|
|
54
|
+
* the prompt area has always acknowledged the β keystroke with. */
|
|
55
|
+
const FOCUS_MARKER = "β―";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `Β·Β·Β·Β· βΒ·enter` appended to a dock row. Composed through `railLeaders` with a
|
|
59
|
+
* budget that pins the leader run at exactly four dots β the dock is a
|
|
60
|
+
* left-aligned single row, so the leaders must NOT stretch to the terminal
|
|
61
|
+
* width the way a card's metricsβelapsed leaders do.
|
|
62
|
+
*/
|
|
63
|
+
function dockTail(left: string, theme: CrewTheme, maxWidth?: number): string {
|
|
64
|
+
const pinned = visibleWidth(left) + visibleWidth(DOCK_HINT_RIGHT) + 6;
|
|
65
|
+
// The pin gives a fixed four-dot leader on a wide terminal. On a NARROW one
|
|
66
|
+
// the budget shrinks instead, so `railLeaders` trims the LEFT segment (with
|
|
67
|
+
// `β¦`) and the actionable `βΒ·enter` hint never gets clipped away β the live
|
|
68
|
+
// row used to end in `ββ¦` at 50 columns.
|
|
69
|
+
const budget = maxWidth === undefined ? pinned : Math.min(pinned, maxWidth - 2);
|
|
70
|
+
return railLeaders(left, DOCK_HINT_RIGHT, budget, theme);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** `CREW βΈ <subject>` β the dock identity (colour is applied on the paint pass). */
|
|
74
|
+
function dockIdentity(subject: string): string {
|
|
75
|
+
return subject ? `${DOCK_WORD} ${ACTIVE} ${subject}` : DOCK_WORD;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `β <content>` β the dock rail, unpadded (`truncate` stays the single clip). */
|
|
79
|
+
function dockLine(content: string, theme: CrewTheme, slot: RailSlot): string {
|
|
80
|
+
return railRaw(RAIL.body, slot, content, theme);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Aggregate run status β the dock's rail colour (Β§2.B: rail colour = state).
|
|
85
|
+
* A failure outranks live work; live work outranks idle. Zero runs is idle.
|
|
86
|
+
*/
|
|
87
|
+
export function widgetRailSlot(runs: readonly WidgetRun[]): RailSlot {
|
|
88
|
+
if (runs.length === 0) return "border";
|
|
89
|
+
const slots = runs.map((entry) => statusSlot(entry.run.status));
|
|
90
|
+
if (slots.includes("error")) return "error";
|
|
91
|
+
if (slots.includes("borderAccent")) return "borderAccent";
|
|
92
|
+
return slots[0] ?? "border";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** `<team>` (or `team/workflow` when they differ); several live runs collapse
|
|
96
|
+
* to `<n> runs` β the counts on the row stay aggregate. */
|
|
97
|
+
function dockSubject(runs: WidgetRun[]): string {
|
|
98
|
+
if (runs.length > 1) return `${runs.length} runs`;
|
|
99
|
+
const first = runs[0];
|
|
100
|
+
return first ? shortRunLabel(first.run) : "idle";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The zero-run dock row: `β CREW βΈ idle Β· β° 1 sched Β·Β·Β·Β· βΒ·enter`.
|
|
105
|
+
*
|
|
106
|
+
* Returns `undefined` when nothing schedules-related paints. The keep-alive
|
|
107
|
+
* path MUST then render NOTHING (`[]`) β a bare hint, and above all the
|
|
108
|
+
* literal `undefined β βΒ·enter`, is the live regression this guards against.
|
|
109
|
+
*/
|
|
110
|
+
export function idleWidgetLine(schedLine: string | undefined, focused = false, maxWidth?: number): string | undefined {
|
|
111
|
+
if (!schedLine) return undefined;
|
|
112
|
+
const left = `${dockIdentity("idle")} Β· ${schedLine}`;
|
|
113
|
+
const line = dockLine(dockTail(left, PLAIN_THEME, maxWidth), PLAIN_THEME, "border");
|
|
114
|
+
return focused ? `${FOCUS_MARKER} ${line}` : line;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The dock's leading activity glyph: a braille spinner ONLY while something is
|
|
119
|
+
* actually running, otherwise the outcome glyph of the aggregate state.
|
|
120
|
+
*
|
|
121
|
+
* Live-run bug (2026-09-16): the dock spun forever because `buildWidgetLines`
|
|
122
|
+
* always passed `spinnerFrame("widget-header")` β a finished run painted
|
|
123
|
+
* `β β Ή CREW βΈ fast-fix Β· 0 running Β· 3/3 done`, i.e. a spinner with nothing to
|
|
124
|
+
* spin for. `β`/`β` are used (not `β`/`β`) because they are in
|
|
125
|
+
* `STATUS_GLYPH_CHARS`, so the shared colorizer paints them.
|
|
126
|
+
*/
|
|
127
|
+
export function widgetActivityGlyph(runs: readonly WidgetRun[]): string {
|
|
128
|
+
const anyAgentRunning = runs.some((entry) => entry.agents.some((agent) => agent.status === "running"));
|
|
129
|
+
const anyRunRunning = runs.some((entry) => entry.run.status === "running");
|
|
130
|
+
if (anyAgentRunning || anyRunRunning) return spinnerFrame("widget-header");
|
|
131
|
+
const slot = widgetRailSlot(runs);
|
|
132
|
+
if (slot === "error") return "β";
|
|
133
|
+
if (slot === "success") return "β";
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
|
|
42
137
|
// ββ Header ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
43
138
|
|
|
44
|
-
|
|
139
|
+
/**
|
|
140
|
+
* The dock's ONE row (RAIL Β§2.B):
|
|
141
|
+
*
|
|
142
|
+
* `β β § CREW βΈ fast-fix Β· 2 running Β· 3/5 done Β· β° 1 sched Β·Β·Β·Β· βΒ·enter`
|
|
143
|
+
*
|
|
144
|
+
* Zero runs paint `β CREW βΈ idle Β· β¦` (see `idleWidgetLine`) instead. Returns
|
|
145
|
+
* PLAIN text β the component colorizes index 0 with `colorWidgetLine`.
|
|
146
|
+
*/
|
|
147
|
+
export function widgetHeader(
|
|
148
|
+
runs: WidgetRun[],
|
|
149
|
+
runningGlyph: string,
|
|
150
|
+
maxLines = 20,
|
|
151
|
+
notificationCount = 0,
|
|
152
|
+
schedSegment?: string,
|
|
153
|
+
maxWidth?: number,
|
|
154
|
+
): string {
|
|
45
155
|
const agents = runs.flatMap((item) => item.agents);
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
156
|
+
const segments: string[] = [];
|
|
157
|
+
if (runs.length > 0) {
|
|
158
|
+
const runningAgents = agents.filter((a) => a.status === "running").length;
|
|
159
|
+
const queuedAgents = agents.filter((a) => a.status === "queued").length;
|
|
160
|
+
const waitingAgents = agents.filter((a) => a.status === "waiting").length;
|
|
161
|
+
const completedAgents = agents.filter((a) => a.status === "completed").length;
|
|
162
|
+
// Zero counts are noise on a one-line dock: a finished run reads
|
|
163
|
+
// `3/3 done`, not `0 running Β· 3/3 done`.
|
|
164
|
+
if (runningAgents) segments.push(`${runningAgents} running`);
|
|
165
|
+
if (queuedAgents) segments.push(`${queuedAgents} queued`);
|
|
166
|
+
if (waitingAgents) segments.push(`${waitingAgents} waiting`);
|
|
167
|
+
if (completedAgents) segments.push(`${completedAgents}/${agents.length} done`);
|
|
168
|
+
// WP-3 on the single line (2026-09-14 round 2): a run parked awaiting plan
|
|
169
|
+
// approval surfaces as a `β plan:<run8>` segment β the row-level badge is
|
|
170
|
+
// gone with the run tree, so the count row carries the signal.
|
|
171
|
+
const planPending = runs.find((item) => isPlanApprovalStatePending(item.run.planApproval));
|
|
172
|
+
if (planPending) segments.push(`β plan:${shortId(planPending.run.runId)}`);
|
|
173
|
+
}
|
|
174
|
+
// Tier C: `schedSegment` is the ALREADY-BUILT `β° β¦` string (jobs, hidden
|
|
175
|
+
// count and clock are all injected upstream β buildWidgetLines); undefined
|
|
176
|
+
// means nothing schedules-related paints.
|
|
177
|
+
if (schedSegment) segments.push(schedSegment);
|
|
178
|
+
// Bug 021: the alerts badge is one more segment (no π, capped at 99+).
|
|
179
|
+
const badge = notificationBadge(notificationCount)
|
|
180
|
+
.replace(/^\s*Β·\s*/, "")
|
|
181
|
+
.trim();
|
|
182
|
+
if (badge) segments.push(badge);
|
|
183
|
+
const head = runningGlyph ? `${runningGlyph} ${dockIdentity(dockSubject(runs))}` : dockIdentity(dockSubject(runs));
|
|
184
|
+
const left = segments.length > 0 ? `${head} Β· ${segments.join(" Β· ")}` : head;
|
|
185
|
+
return dockLine(dockTail(left, PLAIN_THEME, maxWidth), PLAIN_THEME, "border");
|
|
55
186
|
}
|
|
56
187
|
|
|
57
188
|
// ββ Agent ordering (shared with the inline panel) ββββββββββββββββββββββ
|
|
@@ -98,146 +229,114 @@ export function orderWidgetAgents(entry: WidgetRun, now = Date.now()): { active:
|
|
|
98
229
|
|
|
99
230
|
// ββ Line builder ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
100
231
|
|
|
101
|
-
/**
|
|
102
|
-
* Row layout for the per-agent lines.
|
|
103
|
-
*
|
|
104
|
-
* - `detailed` β the historical two-line tree (name row + `βΆ activity` row).
|
|
105
|
-
* - `compact` β one width-budgeted line per agent, so a wide terminal shows the
|
|
106
|
-
* full description instead of the same clip a narrow one gets.
|
|
107
|
-
*/
|
|
108
|
-
export type WidgetRowStyle = "compact" | "detailed";
|
|
109
|
-
|
|
110
232
|
export interface WidgetRenderOptions {
|
|
111
|
-
rowStyle?: WidgetRowStyle;
|
|
112
233
|
/** Task id under the inline panel cursor, if any. */
|
|
113
234
|
selectedTaskId?: string;
|
|
114
235
|
/** Task id whose transcript pane is open, if any. */
|
|
115
236
|
viewedTaskId?: string;
|
|
116
237
|
/**
|
|
117
|
-
* True while the inline panel holds the cursor.
|
|
118
|
-
*
|
|
119
|
-
*
|
|
238
|
+
* True while the inline panel holds the cursor. The inline panel then
|
|
239
|
+
* gets a visible cursor acknowledgement; the idle widget stays compact
|
|
240
|
+
* to keep the prompt area small.
|
|
120
241
|
*/
|
|
121
242
|
focused?: boolean;
|
|
243
|
+
/**
|
|
244
|
+
* Injected clock (dialect D6-T4) for the Tier-C schedules line β pinned by
|
|
245
|
+
* tests; defaults to "now" at the top of buildWidgetLines. The pure
|
|
246
|
+
* schedules builder never reads the clock itself.
|
|
247
|
+
*/
|
|
248
|
+
now?: Date;
|
|
122
249
|
}
|
|
123
250
|
|
|
124
|
-
|
|
125
|
-
function shortModelLabel(agent: CrewAgentRecord, run: TeamRunManifest): string | undefined {
|
|
126
|
-
const model = agent.model ?? run.modelContext?.parentModel ?? run.modelContext?.override;
|
|
127
|
-
if (typeof model !== "string" || !model) return undefined;
|
|
128
|
-
return model.split("/").at(-1) ?? model;
|
|
129
|
-
}
|
|
251
|
+
// ββ Schedules line (Tier C) βββββββββββββββββββββββββββββββββββββββββββ
|
|
130
252
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// the
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const activity =
|
|
159
|
-
!finished && liveHandle?.status === "running" && liveLine
|
|
160
|
-
? liveLine.length > 60
|
|
161
|
-
? `${liveLine.slice(0, 60)}β¦`
|
|
162
|
-
: liveLine
|
|
163
|
-
: finished
|
|
164
|
-
? dockStatusLabel(agent.status)
|
|
165
|
-
: agentActivity(agent, liveHandle);
|
|
166
|
-
const usage = dockUsageText(agent, liveHandle, { viewed: options.viewedTaskId === agent.taskId });
|
|
167
|
-
const ageText = dockElapsed(agent.completedAt ?? agent.startedAt);
|
|
168
|
-
const model = shortModelLabel(agent, run);
|
|
169
|
-
// Stats tail: `Β· glm-5.3 Β· β1.2k β350 Β· 41s` β the model the worker is
|
|
170
|
-
// actually on first, then usage, then elapsed.
|
|
171
|
-
const suffix = `${model ? ` Β· ${model}` : ""}${usage ? ` Β· ${usage}` : ""}${ageText ? ` Β· ${ageText}` : ""}`;
|
|
172
|
-
return budgetedRow({ lead: `${marker} ${dockGlyph} `, name: nameText, activity, suffix }, width);
|
|
253
|
+
/**
|
|
254
|
+
* Tier C (schedules UI): the ONE low-priority schedules line for the crew
|
|
255
|
+
* widget β `β° N sched Β· next Xm`. Painted when β₯1 ENABLED job exists, always
|
|
256
|
+
* as the LAST row (below active-run info, per the widget priority rules),
|
|
257
|
+
* with an optional compact `Β· N hidden` segment (P2-1) when the B2 gate is
|
|
258
|
+
* hiding project-tier jobs. Pure (dialect D6-T4): jobs, the clock, AND the
|
|
259
|
+
* hidden count are injected β no Date.now()/settings read happens here.
|
|
260
|
+
* Returns undefined when the line must not paint (0 enabled jobs AND nothing
|
|
261
|
+
* hidden), so callers skip the row entirely. With ZERO enabled jobs but a
|
|
262
|
+
* hidden count > 0, the hidden-only line still paints β that is exactly the
|
|
263
|
+
* all-gated case where an invisible gate used to show nothing at all.
|
|
264
|
+
*/
|
|
265
|
+
export function buildSchedulesWidgetLine(jobs: readonly ScheduledJob[], now: Date, hiddenCount = 0): string | undefined {
|
|
266
|
+
const enabled = jobs.filter((job) => job.enabled);
|
|
267
|
+
const hidden = hiddenCount > 0 ? ` Β· ${hiddenCount} hidden` : "";
|
|
268
|
+
if (enabled.length === 0) return hiddenCount > 0 ? `β° 0 sched${hidden}` : undefined;
|
|
269
|
+
const nextTargets = enabled
|
|
270
|
+
// new Date(iso) here is a STORED-ISO parse, not a clock read.
|
|
271
|
+
.map((job) => (job.nextRun ? new Date(job.nextRun).getTime() : Number.NaN))
|
|
272
|
+
.filter((ms) => Number.isFinite(ms));
|
|
273
|
+
if (nextTargets.length === 0) return `β° ${enabled.length} sched${hidden}`;
|
|
274
|
+
const next = Math.min(...nextTargets);
|
|
275
|
+
// "in 84m" β "next 84m": the future prefix is redundant right after
|
|
276
|
+
// "next"; overdue targets keep their "Xm ago" tail so a stale nextRun
|
|
277
|
+
// stays legible instead of silently reading as future work.
|
|
278
|
+
const relative = formatRelativeTime(now, new Date(next)).replace(/^in /, "");
|
|
279
|
+
return `β° ${enabled.length} sched Β· next ${relative}${hidden}`;
|
|
173
280
|
}
|
|
174
281
|
|
|
175
282
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* the
|
|
183
|
-
*
|
|
283
|
+
* Injectable scheduled-jobs reader for the widget (single source of truth,
|
|
284
|
+
* G17): the default reads through getScheduledJobs(); tests swap in a stub
|
|
285
|
+
* so the render path never touches real scheduler/settings state.
|
|
286
|
+
*
|
|
287
|
+
* The default is gated on a REGISTERED scheduler: without one (unit tests,
|
|
288
|
+
* pre-registration, post-cleanup) there are no armed jobs to report β and we
|
|
289
|
+
* must never hit the settings store from a paint path (P0-6: no disk per
|
|
290
|
+
* render tick).
|
|
184
291
|
*/
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const flat: Array<{
|
|
195
|
-
run: TeamRunManifest;
|
|
196
|
-
agent: CrewAgentRecord;
|
|
197
|
-
finished: boolean;
|
|
198
|
-
liveHandle: ReturnType<typeof listLiveAgents>[number] | undefined;
|
|
199
|
-
}> = [];
|
|
200
|
-
for (const entry of runs) {
|
|
201
|
-
const { active, finished } = orderWidgetAgents(entry, now);
|
|
202
|
-
const liveForRun = listLiveAgents().filter((a) => a.runId === entry.run.runId);
|
|
203
|
-
for (const agent of active) {
|
|
204
|
-
flat.push({ run: entry.run, agent, finished: false, liveHandle: liveForRun.find((h) => h.taskId === agent.taskId) });
|
|
205
|
-
}
|
|
206
|
-
for (const agent of finished) {
|
|
207
|
-
flat.push({ run: entry.run, agent, finished: true, liveHandle: liveForRun.find((h) => h.taskId === agent.taskId) });
|
|
208
|
-
}
|
|
292
|
+
export type WidgetScheduledJobsReader = (cwd: string) => ScheduledJob[];
|
|
293
|
+
let scheduledJobsReader: WidgetScheduledJobsReader = defaultScheduledJobsReader;
|
|
294
|
+
|
|
295
|
+
function defaultScheduledJobsReader(cwd: string): ScheduledJob[] {
|
|
296
|
+
if (!getCrewScheduler()) return [];
|
|
297
|
+
try {
|
|
298
|
+
return getScheduledJobs(cwd);
|
|
299
|
+
} catch {
|
|
300
|
+
return [];
|
|
209
301
|
}
|
|
302
|
+
}
|
|
210
303
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
304
|
+
/** @internal β test seam: inject a deterministic jobs view. */
|
|
305
|
+
export function setWidgetScheduledJobsReader(reader: WidgetScheduledJobsReader): void {
|
|
306
|
+
scheduledJobsReader = reader;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** @internal β test seam: restore the provider-backed default. */
|
|
310
|
+
export function resetWidgetScheduledJobsReader(): void {
|
|
311
|
+
scheduledJobsReader = defaultScheduledJobsReader;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Injectable hidden-jobs reader (P2-1) β mirrors the jobs reader seam: the
|
|
315
|
+
* default is gated on the REGISTERED scheduler exactly like
|
|
316
|
+
* defaultScheduledJobsReader (P0-6 β a paint path must never hit the settings
|
|
317
|
+
* store), and with a registered scheduler it only ever serves the in-memory
|
|
318
|
+
* registration-time stash from handle-schedule.ts. */
|
|
319
|
+
export type WidgetHiddenJobsReader = (cwd: string) => number;
|
|
320
|
+
let hiddenJobsReader: WidgetHiddenJobsReader = defaultHiddenJobsReader;
|
|
321
|
+
|
|
322
|
+
function defaultHiddenJobsReader(_cwd: string): number {
|
|
323
|
+
if (!getCrewScheduler()) return 0;
|
|
324
|
+
return getScheduledJobsHiddenCountView(); // stash-only when registered: no disk
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** @internal β test seam: inject a deterministic hidden count. */
|
|
328
|
+
export function setWidgetHiddenJobsReader(reader: WidgetHiddenJobsReader): void {
|
|
329
|
+
hiddenJobsReader = reader;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** @internal β test seam: restore the stash-backed default. */
|
|
333
|
+
export function resetWidgetHiddenJobsReader(): void {
|
|
334
|
+
hiddenJobsReader = defaultHiddenJobsReader;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Reader-backed schedules line β the widget's live data path. */
|
|
338
|
+
export function schedulesWidgetLine(cwd: string, now: Date): string | undefined {
|
|
339
|
+
return buildSchedulesWidgetLine(scheduledJobsReader(cwd), now, hiddenJobsReader(cwd));
|
|
241
340
|
}
|
|
242
341
|
|
|
243
342
|
export function buildWidgetLines(
|
|
@@ -249,133 +348,67 @@ export function buildWidgetLines(
|
|
|
249
348
|
width = DEFAULT_WIDGET_WIDTH,
|
|
250
349
|
options: WidgetRenderOptions = {},
|
|
251
350
|
): string[] {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
351
|
+
// SINGLE-LINE DOCK (design system Β§2.B, maintainer design 2026-09-14): the
|
|
352
|
+
// dock paints EXACTLY ONE row β `β <identity> Β· counts Β· schedules Β·Β·Β·Β·
|
|
353
|
+
// βΒ·enter`. No per-agent rows, no run tree, no scroll window; ALL browsing
|
|
354
|
+
// lives in the Agents & Jobs browser, opened by βΒ·enter from THIS line
|
|
355
|
+
// (crew-editor: idle enter at the line target = browser). The rail glyph is
|
|
356
|
+
// always `β` β `β`/`β` would imply a multi-line card.
|
|
357
|
+
//
|
|
358
|
+
// Focused (the β cursor sits ON this line): prefix a β― marker so the
|
|
359
|
+
// keystroke is visibly acknowledged β the line itself is the cursor target;
|
|
360
|
+
// there is no second row to land on. maxLines/frame remain in the signature
|
|
361
|
+
// for call-site compatibility; a single line is always within budget.
|
|
362
|
+
const schedLine = schedulesWidgetLine(cwd, options.now ?? new Date());
|
|
258
363
|
const runs = providedRuns ?? activeWidgetRuns(cwd);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return focused ? lines : lines.slice(0, maxLines);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const lines: string[] = [widgetHeader(runs, runningGlyph, maxLines, notificationCount)];
|
|
271
|
-
|
|
272
|
-
for (const entry of runs) {
|
|
273
|
-
const { run, agents } = entry;
|
|
274
|
-
const now = Date.now();
|
|
275
|
-
const { active: activeAgents, finished: finishedAgents } = orderWidgetAgents(entry, now);
|
|
276
|
-
const completed = agents.filter((a) => a.status === "completed").length;
|
|
277
|
-
// WP-3 (H4): while a run is parked awaiting plan approval, the spinner
|
|
278
|
-
// glyph is replaced by a `β plan:<last-8 runId>` badge. Plain-unicode β
|
|
279
|
-
// (the same glyph needs_attention already uses) stays legible in no-color
|
|
280
|
-
// mode and is colorized by the shared colorizeStatusGlyphs pass. In-place
|
|
281
|
-
// glyph swap only β no extra line, so the MAX_AGENTS_DISPLAY / maxLines
|
|
282
|
-
// budget and the truncate(width) rule are untouched.
|
|
283
|
-
const planPending = isPlanApprovalStatePending(run.planApproval);
|
|
284
|
-
const runGlyph = planPending ? `β plan:${run.runId.slice(-8)}` : iconForStatus(run.status, { runningGlyph });
|
|
285
|
-
const isTerminal = isFinishedRunStatus(run.status);
|
|
286
|
-
// Run progress line. v1βv3 flickered on snapshot.tasks state, v4 was
|
|
287
|
-
// too minimal (`0/1 agents` only), v5 duplicated the worker activity
|
|
288
|
-
// line (tools/tokens/duration already shown one row below). v6 (this)
|
|
289
|
-
// shows only data that is RUN-level (not already in the per-agent
|
|
290
|
-
// activity line) and is GUARANTEED stable across ticks:
|
|
291
|
-
// - agents count β from `agents` array, always populated, never empty.
|
|
292
|
-
// - run elapsed β from `run.createdAt`, always set on manifest.
|
|
293
|
-
//
|
|
294
|
-
// Bug 022 (timer-fix + label): for TERMINAL runs (failed/cancelled/
|
|
295
|
-
// completed) the elapsed counter previously kept ticking up forever
|
|
296
|
-
// from createdAt (a failed run showed `2028s` and climbing, read as
|
|
297
|
-
// "still running"). Now it FREEZES at updatedAt (when the run
|
|
298
|
-
// reached its terminal status). The status label is also surfaced
|
|
299
|
-
// explicitly so the row cannot be misread as an active run.
|
|
300
|
-
const agentCountText = `${completed}/${agents.length} agents`;
|
|
301
|
-
const runEndMs = isTerminal ? new Date(run.updatedAt).getTime() : now;
|
|
302
|
-
const runElapsedMs = Math.max(0, Number.isFinite(runEndMs) ? runEndMs - new Date(run.createdAt).getTime() : 0);
|
|
303
|
-
const runElapsedText = `${Math.floor(runElapsedMs / 1000)}s`;
|
|
304
|
-
const statusLabel = isTerminal ? ` Β· ${run.status}` : "";
|
|
305
|
-
const progressPart = `${agentCountText} Β· ${runElapsedText}${statusLabel}`;
|
|
306
|
-
lines.push(truncate(`ββ ${runGlyph} ${shortRunLabel(run)} Β· ${progressPart} Β· ${run.runId.slice(-8)}`, width));
|
|
307
|
-
|
|
308
|
-
const liveForRun = listLiveAgents().filter((a) => a.runId === run.runId);
|
|
309
|
-
|
|
310
|
-
// Focused: list every agent so the keyboard cursor can reach it. Idle: keep
|
|
311
|
-
// the historical cap so the prompt area stays small.
|
|
312
|
-
const activeCap = focused ? activeAgents.length : MAX_AGENTS_DISPLAY;
|
|
313
|
-
// Finished rows only appear in slots not used by active agents (max 2). When
|
|
314
|
-
// there are >= MAX_AGENTS_DISPLAY live workers, finished rows are suppressed
|
|
315
|
-
// entirely so they cannot push a live agent's activity line off-screen.
|
|
316
|
-
const finishedSlots = focused ? finishedAgents.length : Math.max(0, Math.min(2, MAX_AGENTS_DISPLAY - activeAgents.length));
|
|
317
|
-
|
|
318
|
-
/** Cursor marker column, mirroring the inline panel's selection. */
|
|
319
|
-
const markerFor = (taskId: string): string => (options.selectedTaskId === taskId ? "β―" : " ");
|
|
320
|
-
|
|
321
|
-
const visibleAgents = activeAgents.slice(0, activeCap);
|
|
322
|
-
for (const [index, agent] of visibleAgents.entries()) {
|
|
323
|
-
const last = index === visibleAgents.length - 1 && activeAgents.length <= activeCap && finishedSlots === 0;
|
|
324
|
-
const branch = last ? "ββ" : "ββ";
|
|
325
|
-
const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
|
|
326
|
-
const legacyGlyph = options.viewedTaskId === agent.taskId ? "β" : iconForStatus(agent.status, { runningGlyph });
|
|
327
|
-
const stats = agentStats(agent, liveHandle);
|
|
328
|
-
const name = liveHandle?.agent ?? agent.agent;
|
|
329
|
-
const activity = agentActivity(agent, liveHandle);
|
|
330
|
-
const desc = truncate(liveHandle?.description ?? agent.role ?? "", TASK_DESC_MAX);
|
|
331
|
-
const _activeMain = truncate(`β ${branch} ${legacyGlyph} ${name}${desc ? ` Β· ${desc}` : ` Β· ${agent.role}`}`, width);
|
|
332
|
-
lines.push(_activeMain);
|
|
333
|
-
const _activity = truncate(`β βΆ ${activity}${stats ? ` Β· ${stats}` : ""}`, width);
|
|
334
|
-
lines.push(_activity);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (activeAgents.length > activeCap) {
|
|
338
|
-
lines.push(truncate(`β ββ β¦ +${activeAgents.length - activeCap} more agents`, width));
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
for (const [index, agent] of finishedAgents.slice(0, finishedSlots).entries()) {
|
|
342
|
-
const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
|
|
343
|
-
const name = liveHandle?.agent ?? agent.agent;
|
|
344
|
-
const legacyIcon =
|
|
345
|
-
agent.status === "completed" ? "β" : agent.status === "failed" ? "β" : agent.status === "needs_attention" ? "β " : "βͺ";
|
|
346
|
-
const stats = agentStats(agent, liveHandle);
|
|
347
|
-
const desc = truncate(liveHandle?.description ?? agent.role ?? "", TASK_DESC_MAX);
|
|
348
|
-
const isLastFinished = index === Math.min(finishedAgents.length, finishedSlots) - 1;
|
|
349
|
-
const branch = isLastFinished ? "ββ" : "ββ";
|
|
350
|
-
const _finished = truncate(`β ${branch} ${legacyIcon} ${name} Β· ${desc}${stats ? ` Β· ${stats}` : ""}`, width);
|
|
351
|
-
lines.push(_finished);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Focused (inline panel cursor active): the keyboard can reach every
|
|
355
|
-
// agent, so the renderer must list them ALL β clipping here would put
|
|
356
|
-
// the cursor marker (β―) on rows that are never painted. The idle
|
|
357
|
-
// widget keeps its historical cap to hold the prompt area small.
|
|
358
|
-
if (lines.length >= maxLines && !focused) break;
|
|
364
|
+
const focused = options.focused === true;
|
|
365
|
+
if (!runs.length) {
|
|
366
|
+
// Zero runs keep-alive (Tier C): jobs are exactly what run while no
|
|
367
|
+
// interactive run is active. The β° segment stands alone as the row; with
|
|
368
|
+
// no schedules either there is NOTHING to paint β `[]`, never a bare
|
|
369
|
+
// hint and never the literal `undefined β βΒ·enter` (live bug 2026-09-16).
|
|
370
|
+
const idle = idleWidgetLine(schedLine, focused);
|
|
371
|
+
return idle ? [truncate(idle, width)] : [];
|
|
359
372
|
}
|
|
360
|
-
|
|
361
|
-
return focused ?
|
|
373
|
+
const base = widgetHeader(runs, widgetActivityGlyph(runs), maxLines, notificationCount, schedLine, width);
|
|
374
|
+
return [truncate(focused ? `${FOCUS_MARKER} ${base}` : base, width)];
|
|
362
375
|
}
|
|
363
376
|
|
|
364
377
|
// ββ Colorization ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
365
378
|
|
|
366
|
-
|
|
379
|
+
/**
|
|
380
|
+
* Paint pass for a PLAIN dock/plan line (`index 0` = the identity row).
|
|
381
|
+
*
|
|
382
|
+
* The builders above return plain text, so this adds the identity/rail/chrome
|
|
383
|
+
* colours: the leading `β` (or plan `β`) takes the rail slot β `statusSlot` of
|
|
384
|
+
* the aggregate run status, injected by the component β the identity word goes
|
|
385
|
+
* accent+bold, and the tail hint is dimmed. Lines that were ALREADY built with
|
|
386
|
+
* a real theme (the task-list variant builds through `rail.ts` directly) carry
|
|
387
|
+
* escapes and are passed through untouched, apart from the shared glyph
|
|
388
|
+
* colorizer below.
|
|
389
|
+
*/
|
|
390
|
+
export function colorWidgetLine(line: string, index: number, theme: CrewTheme, slot: RailSlot = "border"): string {
|
|
367
391
|
let result = line;
|
|
368
|
-
if (index === 0) {
|
|
369
|
-
|
|
392
|
+
if (index === 0 && !result.includes("\u001b")) {
|
|
393
|
+
// `β|β <WORD> βΈ <subject>`: rail glyph takes the state slot, the identity
|
|
394
|
+
// word accent+bold, the subject toolTitle+bold. The subject is bounded by
|
|
395
|
+
// the first ` Β·` so a count segment can never be swallowed.
|
|
396
|
+
result = result.replace(
|
|
397
|
+
/^([β―] )?([ββ]) (?:(\S+) )?((?:CREW|PLAN)(?: βΈ [^Β·]+?)?)(?= Β·|$)/,
|
|
398
|
+
(_match, cursor: string | undefined, glyph: string, spinner: string | undefined, identity: string) => {
|
|
399
|
+
const [word = "", subject] = identity.split(" βΈ ");
|
|
400
|
+
const tail = subject ? ` ${theme.fg("dim", ACTIVE)} ${theme.fg("toolTitle", theme.bold(subject))}` : "";
|
|
401
|
+
// The spinner is re-emitted raw: the shared glyph colorizer below
|
|
402
|
+
// paints the braille range accent.
|
|
403
|
+
return `${cursor ?? ""}${theme.fg(slot, glyph)} ${spinner ? `${spinner} ` : ""}${theme.fg("accent", theme.bold(word))}${tail}`;
|
|
404
|
+
},
|
|
405
|
+
);
|
|
406
|
+
result = result.replace(DOCK_HINT, theme.fg("dim", DOCK_HINT));
|
|
370
407
|
}
|
|
371
408
|
// Shared glyph colorizer covers ALL status glyphs β including β³ (waiting),
|
|
372
409
|
// β (needs_attention), and the braille spinner range β -β£Ώ (running) β which the
|
|
373
410
|
// previous local statusGlyphColor map + regex omitted (F-1, V-3).
|
|
374
|
-
|
|
375
|
-
if (index === 0) {
|
|
376
|
-
result = theme.fg("accent", result);
|
|
377
|
-
}
|
|
378
|
-
return result;
|
|
411
|
+
return colorizeStatusGlyphs(result, theme);
|
|
379
412
|
}
|
|
380
413
|
|
|
381
414
|
export function renderLines(lines: string[], width: number): string[] {
|