bullswarm 0.22.0 → 0.22.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 +12 -0
- package/package.json +1 -1
- package/src/workflow/dashboard.js +187 -57
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# bullswarm changelog
|
|
2
2
|
|
|
3
|
+
## 0.22.1 — unified workflow dashboard navigation
|
|
4
|
+
|
|
5
|
+
- The workflow dashboard now keeps V2 runs in the unified list and timeline
|
|
6
|
+
shell on both desktop and narrow terminals, with stable mobile borders and
|
|
7
|
+
the same phase-segmented timeline for current and historical runs.
|
|
8
|
+
|
|
9
|
+
- Timeline phases are numbered and presented in declared program order, so
|
|
10
|
+
parallel workers finishing out of order cannot place Phase 2 above Phase 1.
|
|
11
|
+
Preflight is selectable and opens the Workflow Planner; Up and Down move
|
|
12
|
+
through Preflight, Phase 1, Phase 2, and later phases, while Enter or Right
|
|
13
|
+
opens the selected planner or phase agents.
|
|
14
|
+
|
|
3
15
|
## 0.22.0 — autonomous Dynamic Workflow V2
|
|
4
16
|
|
|
5
17
|
- Autonomous goals now run on the V2 kernel: agents propose bounded programs
|
package/package.json
CHANGED
|
@@ -40,7 +40,10 @@ function keyPressed(name, key) {
|
|
|
40
40
|
return DASHBOARD_KEYS[name].bindings?.includes(key) ?? false;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function navigationFooter({
|
|
43
|
+
function navigationFooter({
|
|
44
|
+
list = false, depth = 0, narrow = false, filterEditing = false, mobileTimeline = true,
|
|
45
|
+
timelineSelection = null,
|
|
46
|
+
} = {}) {
|
|
44
47
|
if (filterEditing) return 'Filter: {query}█ · Enter apply · Esc clear';
|
|
45
48
|
if (list) {
|
|
46
49
|
return narrow
|
|
@@ -48,8 +51,13 @@ function navigationFooter({ list = false, depth = 0, narrow = false, filterEditi
|
|
|
48
51
|
: `${keyHint('up')} · ${keyHint('in')} · / filter · a active/all · ${keyHint('detach')} · ${keyHint('out')} · ${keyHint('nextWorkflow')} · ${keyHint('previousWorkflow')} · r refresh · c stop`;
|
|
49
52
|
}
|
|
50
53
|
const extras = depth >= 4 ? ' · PgUp/PgDn scroll' : '';
|
|
51
|
-
const
|
|
52
|
-
|
|
54
|
+
const phaseNavigation = narrow && depth <= 2 && mobileTimeline;
|
|
55
|
+
const phaseToggle = narrow && depth <= 2
|
|
56
|
+
? ` · t ${mobileTimeline ? 'phases' : 'timeline'}`
|
|
57
|
+
: '';
|
|
58
|
+
const movement = phaseNavigation ? '↑ previous phase · ↓ next phase' : `${keyHint('up')} · ${keyHint('down')}`;
|
|
59
|
+
const open = phaseNavigation ? `Enter ${timelineSelection === 0 ? 'planner' : 'agents'}` : keyHint('in');
|
|
60
|
+
return `${movement}${phaseToggle} · ${open} · ${keyHint('out')} · ${keyHint('nextWorkflow')} · ${keyHint('previousWorkflow')} · o planner · v technical · c stop · ${keyHint('detach')}${extras}`;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
function breadcrumbSegments(row, { depth = 0, phase = null, agent = null } = {}) {
|
|
@@ -146,8 +154,8 @@ export function dashboardRows(bullswarmDir, { all = false } = {}) {
|
|
|
146
154
|
.filter((r) => all || r.ongoing)
|
|
147
155
|
.sort((a, b) => {
|
|
148
156
|
if (a.ongoing !== b.ongoing) return a.ongoing ? -1 : 1;
|
|
149
|
-
return String(b.state
|
|
150
|
-
.localeCompare(String(a.state
|
|
157
|
+
return String(stateStartedAt(b.state) ?? b.report?.startedAt ?? '')
|
|
158
|
+
.localeCompare(String(stateStartedAt(a.state) ?? a.report?.startedAt ?? ''));
|
|
151
159
|
})
|
|
152
160
|
.map((r) => {
|
|
153
161
|
const state = r.state ?? {};
|
|
@@ -779,7 +787,7 @@ export function renderWorkflowTui(row, {
|
|
|
779
787
|
width = 120, height = 36, focus = 0, phaseIndex = null, agentIndex = null,
|
|
780
788
|
detailScroll = 0, message = null, confirmCancel = false,
|
|
781
789
|
controlSelected = false, orchestratorDetail = false, orchestratorVerbose = false,
|
|
782
|
-
workflowVerbose = false, mobileTimeline = true,
|
|
790
|
+
workflowVerbose = false, mobileTimeline = true, timelineSelection = null,
|
|
783
791
|
spinnerFrame = 0,
|
|
784
792
|
} = {}) {
|
|
785
793
|
width = Math.max(20, Number(width) || 120);
|
|
@@ -809,7 +817,12 @@ export function renderWorkflowTui(row, {
|
|
|
809
817
|
];
|
|
810
818
|
const footer = confirmCancel
|
|
811
819
|
? ' Stop this workflow? y confirm · n/Esc keep running'
|
|
812
|
-
: navigationFooter({
|
|
820
|
+
: navigationFooter({
|
|
821
|
+
depth: breadcrumbDepth,
|
|
822
|
+
narrow,
|
|
823
|
+
mobileTimeline: mobileTimeline && !orchestratorDetail && !workflowVerbose && focus === 0,
|
|
824
|
+
timelineSelection,
|
|
825
|
+
});
|
|
813
826
|
const rawMessageLine = message
|
|
814
827
|
? ` ${truncate(message, width - 2)}`
|
|
815
828
|
: ` ${orchestratorDetail ? `Workflow Planner ${orchestratorVerbose ? 'technical details' : 'overview'}` : workflowVerbose ? 'Workflow technical details' : focus === 0 ? (narrow && !mobileTimeline ? 'Phases' : 'Timeline · auto-following newest event') : focus === 1 ? 'Agents' : 'Agent activity'} · r refresh · workflow continues after detach`;
|
|
@@ -907,7 +920,13 @@ export function renderWorkflowTui(row, {
|
|
|
907
920
|
} else if (workflowVerbose) {
|
|
908
921
|
body = renderPanel('Workflow technical details', technical.slice(scroll, scroll + contentHeight), width, bodyHeight);
|
|
909
922
|
} else if (focus === 0 && mobileTimeline) {
|
|
910
|
-
|
|
923
|
+
const selectedTimelineSegment = timelineSelection === 0
|
|
924
|
+
? 'Preflight'
|
|
925
|
+
: timelineSelection > 0 ? model.phases[timelineSelection - 1]?.label : null;
|
|
926
|
+
body = renderWorkflowOverviewPanel(
|
|
927
|
+
model, width, bodyHeight, spinnerFrame, detailScroll,
|
|
928
|
+
selectedTimelineSegment,
|
|
929
|
+
);
|
|
911
930
|
} else if (focus === 0) {
|
|
912
931
|
body = renderPanel(phaseTitle, visiblePhases, width, bodyHeight);
|
|
913
932
|
} else if (focus === 1) {
|
|
@@ -971,7 +990,7 @@ function joinPanels(left, right) {
|
|
|
971
990
|
return left.map((line, index) => `${line}${right[index] ?? ''}`);
|
|
972
991
|
}
|
|
973
992
|
|
|
974
|
-
function renderWorkflowOverviewPanel(model, width, height, spinnerFrame, timelineScroll = 0) {
|
|
993
|
+
function renderWorkflowOverviewPanel(model, width, height, spinnerFrame, timelineScroll = 0, selectedTimelineSegment = null) {
|
|
975
994
|
const inner = Math.max(1, width - 2);
|
|
976
995
|
const timeline = workflowTimelineLines(model, inner);
|
|
977
996
|
const live = workflowLiveLines(model, inner, spinnerFrame);
|
|
@@ -981,28 +1000,37 @@ function renderWorkflowOverviewPanel(model, width, height, spinnerFrame, timelin
|
|
|
981
1000
|
const liveRows = Math.min(live.lines.length, Math.max(2, Math.floor(contentRows * 0.42)));
|
|
982
1001
|
const timelineRows = Math.max(1, contentRows - liveRows - nextRows);
|
|
983
1002
|
const maxTimelineScroll = Math.max(0, timeline.lines.length - timelineRows);
|
|
1003
|
+
const selectedHeader = selectedTimelineSegment
|
|
1004
|
+
? timeline.lines.findIndex((line) => line?.header && line.segment === selectedTimelineSegment)
|
|
1005
|
+
: -1;
|
|
984
1006
|
const scroll = clamp(timelineScroll, 0, maxTimelineScroll);
|
|
985
|
-
const end =
|
|
986
|
-
|
|
987
|
-
|
|
1007
|
+
const end = selectedHeader >= 0
|
|
1008
|
+
? Math.min(timeline.lines.length, selectedHeader + timelineRows)
|
|
1009
|
+
: Math.max(0, timeline.lines.length - scroll);
|
|
1010
|
+
let start = selectedHeader >= 0
|
|
1011
|
+
? selectedHeader
|
|
1012
|
+
: Math.max(0, end - timelineRows);
|
|
1013
|
+
if (start > 0 && selectedHeader < 0) {
|
|
988
1014
|
// Reserve one row for the continuation header while keeping the newest
|
|
989
1015
|
// timestamped milestone in view.
|
|
990
1016
|
start = Math.max(0, end - Math.max(0, timelineRows - 1));
|
|
991
1017
|
while (start < end && !/^\d{2}:\d{2}\s/.test(timelineText(timeline.lines[start]))) start += 1;
|
|
992
1018
|
}
|
|
993
1019
|
let visibleTimeline = timeline.lines.slice(start, end);
|
|
994
|
-
if (start > 0) {
|
|
1020
|
+
if (start > 0 && selectedHeader < 0) {
|
|
995
1021
|
visibleTimeline.unshift(dimText(`↑ ${start} earlier timeline rows`, inner));
|
|
996
1022
|
const continuation = visibleTimeline.find((line) => line?.segment)?.segment
|
|
997
1023
|
?? currentTimelineSegment(model);
|
|
998
1024
|
if (continuation) {
|
|
999
1025
|
const priorHeader = timeline.lines.find((line) => line?.header && line.segment === continuation);
|
|
1000
|
-
|
|
1001
|
-
continuation,
|
|
1026
|
+
const header = continuationHeader(
|
|
1027
|
+
timelineSegmentDisplayName(continuation, model),
|
|
1002
1028
|
priorHeader?.elapsed ?? 'running',
|
|
1003
1029
|
inner,
|
|
1004
1030
|
visibleTimeline.find((line) => line?.segment === continuation)?.at,
|
|
1005
|
-
)
|
|
1031
|
+
);
|
|
1032
|
+
header.segment = continuation;
|
|
1033
|
+
visibleTimeline.splice(1, 0, header);
|
|
1006
1034
|
}
|
|
1007
1035
|
// Scrolled views already carry the upward marker and continuation header;
|
|
1008
1036
|
// omit inter-segment spacer rows so the viewport retains the latest event.
|
|
@@ -1013,7 +1041,7 @@ function renderWorkflowOverviewPanel(model, width, height, spinnerFrame, timelin
|
|
|
1013
1041
|
if (visibleTimeline.length >= timelineRows) visibleTimeline[visibleTimeline.length - 1] = marker;
|
|
1014
1042
|
else visibleTimeline.push(marker);
|
|
1015
1043
|
}
|
|
1016
|
-
if (start > 0 && visibleTimeline.length > timelineRows) {
|
|
1044
|
+
if (start > 0 && selectedHeader < 0 && visibleTimeline.length > timelineRows) {
|
|
1017
1045
|
// The continuation marker and header are structural context, not
|
|
1018
1046
|
// expendable event rows. Keep both and trim the oldest visible events.
|
|
1019
1047
|
visibleTimeline = [visibleTimeline[0], visibleTimeline[1],
|
|
@@ -1021,6 +1049,11 @@ function renderWorkflowOverviewPanel(model, width, height, spinnerFrame, timelin
|
|
|
1021
1049
|
} else {
|
|
1022
1050
|
visibleTimeline = visibleTimeline.slice(0, timelineRows);
|
|
1023
1051
|
}
|
|
1052
|
+
if (selectedTimelineSegment) {
|
|
1053
|
+
visibleTimeline = visibleTimeline.map((line) => line?.header && line.segment === selectedTimelineSegment
|
|
1054
|
+
? { ...line, text: `\x1b[7m${timelineText(line)}\x1b[0m` }
|
|
1055
|
+
: line);
|
|
1056
|
+
}
|
|
1024
1057
|
const visibleLive = live.lines.slice(0, liveRows);
|
|
1025
1058
|
const visibleNext = next.slice(0, nextRows);
|
|
1026
1059
|
const title = ` Workflow timeline · ${timeline.milestoneCount} milestone${timeline.milestoneCount === 1 ? '' : 's'} `;
|
|
@@ -1148,57 +1181,92 @@ function workflowTimelineLines(model, width) {
|
|
|
1148
1181
|
if (detail) add(event.committedAt, detail, Number(event.sequence), event.type.startsWith('decision.') ? 'Planner' : 'Preflight');
|
|
1149
1182
|
}
|
|
1150
1183
|
|
|
1151
|
-
events
|
|
1184
|
+
return groupedTimeline(events, model, width, state.finishedAt);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function groupedTimeline(events, model, width, workflowFinishedAt) {
|
|
1188
|
+
const chronological = (a, b) => Date.parse(a.at) - Date.parse(b.at)
|
|
1189
|
+
|| Number(a.sequence ?? Number.MAX_SAFE_INTEGER) - Number(b.sequence ?? Number.MAX_SAFE_INTEGER);
|
|
1190
|
+
events.sort(chronological);
|
|
1191
|
+
// Phase blocks are a navigable program, so present adjacent phase activity in
|
|
1192
|
+
// the declared order even when parallel workers finish out of order. Planner
|
|
1193
|
+
// checkpoints remain chronological boundaries between separate programs.
|
|
1194
|
+
const phaseRank = new Map(model.phases.map((phase, index) => [phase.label, index]));
|
|
1195
|
+
const orderedEvents = [];
|
|
1196
|
+
for (let index = 0; index < events.length;) {
|
|
1197
|
+
if (!phaseRank.has(events[index].segment)) {
|
|
1198
|
+
orderedEvents.push(events[index]);
|
|
1199
|
+
index += 1;
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
let end = index;
|
|
1203
|
+
while (end < events.length && phaseRank.has(events[end].segment)) end += 1;
|
|
1204
|
+
orderedEvents.push(...events.slice(index, end).sort((a, b) =>
|
|
1205
|
+
phaseRank.get(a.segment) - phaseRank.get(b.segment) || chronological(a, b)));
|
|
1206
|
+
index = end;
|
|
1207
|
+
}
|
|
1152
1208
|
const segments = new Map();
|
|
1153
|
-
for (const event of
|
|
1209
|
+
for (const event of orderedEvents) {
|
|
1154
1210
|
const name = event.segment ?? 'Workflow';
|
|
1155
|
-
|
|
1211
|
+
const firstAt = event.startedAt ?? event.at;
|
|
1212
|
+
if (!segments.has(name)) segments.set(name, { name, events: [], first: firstAt, last: event.at });
|
|
1156
1213
|
const segment = segments.get(name);
|
|
1157
1214
|
segment.events.push(event);
|
|
1158
|
-
if (Date.parse(
|
|
1215
|
+
if (Date.parse(firstAt) < Date.parse(segment.first)) segment.first = firstAt;
|
|
1159
1216
|
if (Date.parse(event.at) > Date.parse(segment.last)) segment.last = event.at;
|
|
1160
1217
|
}
|
|
1161
1218
|
const lines = [];
|
|
1162
1219
|
let previousSegment = null;
|
|
1163
1220
|
const openedSegments = new Set();
|
|
1164
|
-
for (const event of
|
|
1221
|
+
for (const event of orderedEvents) {
|
|
1165
1222
|
if (event.segment !== previousSegment) {
|
|
1166
1223
|
if (lines.length) lines.push('');
|
|
1167
1224
|
const segment = segments.get(event.segment ?? 'Workflow');
|
|
1168
|
-
const
|
|
1225
|
+
const segmentFinishedAt = segment.last;
|
|
1169
1226
|
const currentSegment = currentTimelineSegment(model);
|
|
1170
|
-
const running = !
|
|
1171
|
-
const elapsed = running ? 'running' : durationText(segment.first,
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1227
|
+
const running = !workflowFinishedAt && segment.name === currentSegment;
|
|
1228
|
+
const elapsed = running ? 'running' : durationText(segment.first, segmentFinishedAt);
|
|
1229
|
+
const displayName = timelineSegmentDisplayName(segment.name, model);
|
|
1230
|
+
const header = openedSegments.has(segment.name)
|
|
1231
|
+
? continuationHeader(displayName, elapsed, width)
|
|
1232
|
+
: segmentHeader(displayName, elapsed, width);
|
|
1233
|
+
header.segment = segment.name;
|
|
1234
|
+
lines.push(header);
|
|
1175
1235
|
openedSegments.add(segment.name);
|
|
1176
1236
|
previousSegment = event.segment;
|
|
1177
1237
|
}
|
|
1178
1238
|
lines.push(...event.lines.map((line) => ({ text: line, segment: event.segment, at: event.at })));
|
|
1179
1239
|
}
|
|
1180
1240
|
if (!lines.length) lines.push({ text: 'Waiting for the first durable workflow milestone', segment: null });
|
|
1181
|
-
return { lines, milestoneCount:
|
|
1241
|
+
return { lines, milestoneCount: orderedEvents.length };
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function timelineSegmentDisplayName(name, model) {
|
|
1245
|
+
const phaseIndex = model.phases.findIndex((phase) => phase.label === name);
|
|
1246
|
+
return phaseIndex >= 0 ? `Phase ${phaseIndex + 1} · ${name}` : name;
|
|
1182
1247
|
}
|
|
1183
1248
|
|
|
1184
1249
|
function workflowTimelineLinesV2(model, width) {
|
|
1185
1250
|
const { state } = model;
|
|
1186
1251
|
const rows = [];
|
|
1187
|
-
const add = (at, label, right = '', detail = null, segment = 'Workflow') => {
|
|
1252
|
+
const add = (at, label, right = '', detail = null, segment = 'Workflow', startedAt = null) => {
|
|
1188
1253
|
if (!at) return;
|
|
1189
|
-
rows.push({
|
|
1254
|
+
rows.push({
|
|
1255
|
+
at, startedAt, segment,
|
|
1256
|
+
lines: [timelineRow(at, label, right, width), ...(detail ? [timelineDetail(detail, width)] : [])],
|
|
1257
|
+
});
|
|
1190
1258
|
};
|
|
1191
|
-
add(state.lifecycle.startedAt, '● Workflow initiated', '', 'Goal accepted; preparing repository reconnaissance', '
|
|
1259
|
+
add(state.lifecycle.startedAt, '● Workflow initiated', '', 'Goal accepted; preparing repository reconnaissance', 'Preflight');
|
|
1192
1260
|
const eventByType = new Map();
|
|
1193
1261
|
for (const event of model.events) {
|
|
1194
1262
|
if (!eventByType.has(event.type)) eventByType.set(event.type, []);
|
|
1195
1263
|
eventByType.get(event.type).push(event);
|
|
1196
1264
|
}
|
|
1197
|
-
for (const event of eventByType.get('preflight.scout_started') ?? []) add(event.committedAt, '●
|
|
1265
|
+
for (const event of eventByType.get('preflight.scout_started') ?? []) add(event.committedAt, '● Scout started', '', event.payload?.purpose, 'Preflight');
|
|
1198
1266
|
for (const event of eventByType.get('preflight.scout_finished') ?? []) {
|
|
1199
1267
|
const attempt = state.preflight.scout.attempts.at(-1);
|
|
1200
1268
|
const detail = [attempt?.pool, attempt?.model, tokenText(attempt?.usage)].filter(Boolean).join(' · ');
|
|
1201
|
-
add(event.committedAt, `${event.payload?.status === 'succeeded' ? '✓' : '×'}
|
|
1269
|
+
add(event.committedAt, `${event.payload?.status === 'succeeded' ? '✓' : '×'} Scout ${event.payload?.status === 'succeeded' ? 'completed' : 'could not complete'}`, durationText(state.preflight.scout.startedAt, state.preflight.scout.finishedAt), detail, 'Preflight');
|
|
1202
1270
|
}
|
|
1203
1271
|
for (const event of eventByType.get('planner.finished') ?? []) {
|
|
1204
1272
|
const turn = Number(event.payload?.turn ?? 1);
|
|
@@ -1206,12 +1274,19 @@ function workflowTimelineLinesV2(model, width) {
|
|
|
1206
1274
|
? turn === 1 ? '[Workflow Planner] plan created' : `[Workflow Planner] plan updated #${turn}`
|
|
1207
1275
|
: '[Workflow Planner] planning attempt rejected';
|
|
1208
1276
|
const attempt = state.planner.attempts.findLast((item) => item.turn === turn);
|
|
1209
|
-
add(
|
|
1277
|
+
add(
|
|
1278
|
+
event.committedAt,
|
|
1279
|
+
`${event.payload?.ok ? '◇' : '×'} ${label}`,
|
|
1280
|
+
attempt ? durationText(attempt.startedAt, attempt.finishedAt) : '',
|
|
1281
|
+
event.payload?.summary ?? event.payload?.why,
|
|
1282
|
+
turn === 1 ? 'Preflight' : 'Planner',
|
|
1283
|
+
attempt?.startedAt,
|
|
1284
|
+
);
|
|
1210
1285
|
}
|
|
1211
1286
|
const stageById = new Map((state.presentation?.stages ?? []).map((stage) => [stage.id, stage]));
|
|
1212
1287
|
for (const event of model.events) {
|
|
1213
1288
|
if (event.type === 'presentation.stage_started') {
|
|
1214
|
-
add(event.committedAt,
|
|
1289
|
+
add(event.committedAt, '├─ started', '', null, event.payload.label);
|
|
1215
1290
|
}
|
|
1216
1291
|
if (event.type === 'action.finished' || event.type === 'evidence.recorded') {
|
|
1217
1292
|
const actionId = event.payload?.actionId;
|
|
@@ -1223,17 +1298,15 @@ function workflowTimelineLinesV2(model, width) {
|
|
|
1223
1298
|
if (event.type === 'presentation.stage_completed') {
|
|
1224
1299
|
const stage = stageById.get(event.payload?.stageId);
|
|
1225
1300
|
const ok = event.payload?.status === 'completed';
|
|
1226
|
-
add(event.committedAt, `└─${ok ? '✓' : '×'}
|
|
1301
|
+
add(event.committedAt, `└─${ok ? '✓' : '×'} completed`, `${event.payload.completed}/${event.payload.total}`, null, stage?.label ?? event.payload.label);
|
|
1227
1302
|
}
|
|
1228
1303
|
}
|
|
1229
1304
|
if (state.lifecycle.finishedAt) {
|
|
1230
1305
|
const status = state.lifecycle.status;
|
|
1231
|
-
|
|
1306
|
+
const finalSegment = state.presentation?.stages?.findLast((stage) => stage.startedAt)?.label ?? 'Workflow';
|
|
1307
|
+
add(state.lifecycle.finishedAt, `${status === 'completed' ? '✓' : status === 'partial' ? '!' : '×'} Workflow ${status === 'completed' ? 'complete - result is ready' : `${status} - result is ready`}`, durationText(state.lifecycle.startedAt, state.lifecycle.finishedAt), null, finalSegment);
|
|
1232
1308
|
}
|
|
1233
|
-
rows
|
|
1234
|
-
const lines = rows.flatMap((row) => row.lines.map((line) => ({ text: line, segment: row.segment, at: row.at })));
|
|
1235
|
-
if (!lines.length) lines.push({ text: 'Waiting for the first durable workflow milestone', segment: null });
|
|
1236
|
-
return { lines, milestoneCount: rows.length };
|
|
1309
|
+
return groupedTimeline(rows, model, width, state.lifecycle.finishedAt);
|
|
1237
1310
|
}
|
|
1238
1311
|
|
|
1239
1312
|
function segmentHeader(name, elapsed, width) {
|
|
@@ -1271,7 +1344,10 @@ function currentTimelineSegment(model) {
|
|
|
1271
1344
|
if (model.v2) {
|
|
1272
1345
|
const activeAction = state.actions.find((action) => action.status === 'running');
|
|
1273
1346
|
return state.presentation.stages.find((stage) => stage.actionIds.includes(activeAction?.id))?.label
|
|
1274
|
-
?? (state.
|
|
1347
|
+
?? (state.preflight?.scout?.status === 'running' ? 'Preflight'
|
|
1348
|
+
: state.planner.status === 'running'
|
|
1349
|
+
? (state.actions.length ? 'Planner' : 'Preflight')
|
|
1350
|
+
: 'Workflow');
|
|
1275
1351
|
}
|
|
1276
1352
|
if (orchestrator.active || (orchestrator.autonomous && !state.currentStep?.phase)) return 'Planner';
|
|
1277
1353
|
const phase = state.currentStep?.phase ?? state.currentPhase?.name;
|
|
@@ -2019,15 +2095,21 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2019
2095
|
return 0;
|
|
2020
2096
|
}
|
|
2021
2097
|
let selected = 0;
|
|
2022
|
-
|
|
2098
|
+
const directRow = token ? detailRow(bullswarmDir, token) : null;
|
|
2099
|
+
const directV2 = isV2State(directRow?.state);
|
|
2100
|
+
let detail = Boolean(token) && !directV2;
|
|
2023
2101
|
let message = null;
|
|
2024
2102
|
let lastGoodRow = null;
|
|
2025
|
-
let dashboardFilter = 'active';
|
|
2103
|
+
let dashboardFilter = directV2 ? 'all' : 'active';
|
|
2026
2104
|
let query = '';
|
|
2027
2105
|
let filterEditing = false;
|
|
2028
2106
|
let allRows = dashboardRows(bullswarmDir, { all: true });
|
|
2029
2107
|
let rows = filterDashboardRows(allRows, dashboardFilter, query);
|
|
2030
|
-
|
|
2108
|
+
if (directV2) {
|
|
2109
|
+
const directIndex = rows.findIndex((row) => row.runId === directRow.runId);
|
|
2110
|
+
if (directIndex >= 0) selected = directIndex;
|
|
2111
|
+
}
|
|
2112
|
+
let selectedRunId = token ? directRow.runId : (rows[selected]?.runId ?? null);
|
|
2031
2113
|
let lastPaintedFrame = null;
|
|
2032
2114
|
const ui = {
|
|
2033
2115
|
focus: 0,
|
|
@@ -2042,6 +2124,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2042
2124
|
orchestratorVerbose: false,
|
|
2043
2125
|
workflowVerbose: false,
|
|
2044
2126
|
mobileTimeline: true,
|
|
2127
|
+
timelineSelection: null,
|
|
2045
2128
|
spinnerFrame: 0,
|
|
2046
2129
|
};
|
|
2047
2130
|
// Clearing the entire alternate screen for every spinner frame produces a
|
|
@@ -2063,6 +2146,12 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2063
2146
|
lastPaintedFrame = frame;
|
|
2064
2147
|
output.write(frame);
|
|
2065
2148
|
};
|
|
2149
|
+
// Several mobile terminals auto-wrap when the final column is painted.
|
|
2150
|
+
// Leave one narrow-screen column unused so the right border remains stable.
|
|
2151
|
+
const frameWidth = () => {
|
|
2152
|
+
const columns = Math.max(20, Number(output.columns) || 120);
|
|
2153
|
+
return columns < 100 ? Math.max(20, columns - 1) : columns;
|
|
2154
|
+
};
|
|
2066
2155
|
const paintUnsafe = () => {
|
|
2067
2156
|
if (selected >= rows.length) selected = Math.max(0, rows.length - 1);
|
|
2068
2157
|
if (detail && selectedRunId) {
|
|
@@ -2078,7 +2167,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2078
2167
|
ui.phaseIndex = model.phaseIndex;
|
|
2079
2168
|
ui.agentIndex = model.agentIndex;
|
|
2080
2169
|
writeFrame(renderWorkflowTui(row, {
|
|
2081
|
-
width:
|
|
2170
|
+
width: frameWidth(),
|
|
2082
2171
|
height: output.rows,
|
|
2083
2172
|
...ui,
|
|
2084
2173
|
message,
|
|
@@ -2098,7 +2187,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2098
2187
|
allRows,
|
|
2099
2188
|
selected,
|
|
2100
2189
|
message,
|
|
2101
|
-
width:
|
|
2190
|
+
width: frameWidth(),
|
|
2102
2191
|
height: output.rows,
|
|
2103
2192
|
filter: dashboardFilter,
|
|
2104
2193
|
query,
|
|
@@ -2115,7 +2204,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2115
2204
|
message = `display error: ${err.message}`;
|
|
2116
2205
|
try {
|
|
2117
2206
|
writeFrame(renderDashboard({
|
|
2118
|
-
rows, allRows, selected, message, width:
|
|
2207
|
+
rows, allRows, selected, message, width: frameWidth(), height: output.rows,
|
|
2119
2208
|
filter: dashboardFilter, query, filterEditing, spinnerFrame: ui.spinnerFrame,
|
|
2120
2209
|
}));
|
|
2121
2210
|
} catch { /* keep the loop alive */ }
|
|
@@ -2173,6 +2262,7 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2173
2262
|
ui.agentIndex = ui.focus < 2 ? nextModel.agentIndex
|
|
2174
2263
|
: clamp(ui.agentIndex ?? nextModel.agentIndex, 0, Math.max(0, agents.length - 1));
|
|
2175
2264
|
ui.detailScroll = 0;
|
|
2265
|
+
ui.timelineSelection = null;
|
|
2176
2266
|
}
|
|
2177
2267
|
message = null;
|
|
2178
2268
|
paint();
|
|
@@ -2207,8 +2297,30 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2207
2297
|
const model = workflowPanelModel(row, { phaseIndex: ui.phaseIndex, agentIndex: ui.agentIndex });
|
|
2208
2298
|
const narrowTimeline = output.columns < 100 && ui.mobileTimeline && ui.focus === 0;
|
|
2209
2299
|
if (ui.orchestratorDetail || ui.workflowVerbose || narrowTimeline) {
|
|
2210
|
-
if (narrowTimeline)
|
|
2211
|
-
|
|
2300
|
+
if (narrowTimeline) {
|
|
2301
|
+
const visibleSegments = new Set(workflowTimelineLines(model, Math.max(20, frameWidth() - 2)).lines
|
|
2302
|
+
.filter((line) => line?.header)
|
|
2303
|
+
.map((line) => line.segment));
|
|
2304
|
+
const navigable = [
|
|
2305
|
+
...(visibleSegments.has('Preflight') ? [{ selection: 0, phaseIndex: null }] : []),
|
|
2306
|
+
...model.phases
|
|
2307
|
+
.map((phase, index) => ({ phase, selection: index + 1, phaseIndex: index }))
|
|
2308
|
+
.filter(({ phase }) => visibleSegments.has(phase.label)),
|
|
2309
|
+
];
|
|
2310
|
+
if (navigable.length) {
|
|
2311
|
+
const current = navigable.findIndex((target) => target.selection === ui.timelineSelection);
|
|
2312
|
+
const base = current >= 0 ? current : (delta < 0 ? navigable.length : -1);
|
|
2313
|
+
const target = navigable[clamp(base + delta, 0, navigable.length - 1)];
|
|
2314
|
+
ui.timelineSelection = target.selection;
|
|
2315
|
+
if (target.phaseIndex != null) {
|
|
2316
|
+
ui.followActivePhase = false;
|
|
2317
|
+
ui.phaseIndex = target.phaseIndex;
|
|
2318
|
+
ui.agentIndex = null;
|
|
2319
|
+
ui.followActiveAgent = true;
|
|
2320
|
+
}
|
|
2321
|
+
ui.detailScroll = 0;
|
|
2322
|
+
}
|
|
2323
|
+
} else ui.detailScroll = Math.max(0, ui.detailScroll + delta);
|
|
2212
2324
|
return paint();
|
|
2213
2325
|
}
|
|
2214
2326
|
if (ui.focus === 0) {
|
|
@@ -2302,12 +2414,16 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2302
2414
|
ui.orchestratorDetail = false;
|
|
2303
2415
|
ui.orchestratorVerbose = false;
|
|
2304
2416
|
ui.focus = 0;
|
|
2305
|
-
ui.controlSelected =
|
|
2417
|
+
ui.controlSelected = output.columns >= 100;
|
|
2418
|
+
if (output.columns < 100 && ui.mobileTimeline) ui.timelineSelection = 0;
|
|
2306
2419
|
ui.detailScroll = 0;
|
|
2307
2420
|
} else if (ui.workflowVerbose) {
|
|
2308
2421
|
ui.workflowVerbose = false;
|
|
2309
2422
|
ui.detailScroll = 0;
|
|
2310
|
-
} else if (detail && ui.focus > 0)
|
|
2423
|
+
} else if (detail && ui.focus > 0) {
|
|
2424
|
+
ui.focus -= 1;
|
|
2425
|
+
if (ui.focus === 0 && output.columns < 100 && ui.mobileTimeline) ui.timelineSelection = (ui.phaseIndex ?? 0) + 1;
|
|
2426
|
+
}
|
|
2311
2427
|
else if (detail) detail = false;
|
|
2312
2428
|
return paint();
|
|
2313
2429
|
}
|
|
@@ -2320,14 +2436,25 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2320
2436
|
detail = true;
|
|
2321
2437
|
ui.followActivePhase = true;
|
|
2322
2438
|
ui.followActiveAgent = true;
|
|
2439
|
+
ui.timelineSelection = null;
|
|
2323
2440
|
}
|
|
2324
2441
|
} else if (ui.orchestratorDetail) {
|
|
2325
2442
|
message = 'Planner detail is the deepest level.';
|
|
2326
2443
|
} else if (ui.workflowVerbose) {
|
|
2327
2444
|
message = 'Technical details are the deepest level.';
|
|
2328
2445
|
} else if (ui.focus === 0) {
|
|
2329
|
-
ui.
|
|
2330
|
-
|
|
2446
|
+
if (output.columns < 100 && ui.mobileTimeline && ui.timelineSelection === 0) {
|
|
2447
|
+
const row = detailRow(bullswarmDir, selectedRunId);
|
|
2448
|
+
const model = workflowPanelModel(row, { phaseIndex: ui.phaseIndex, agentIndex: ui.agentIndex });
|
|
2449
|
+
if (model.orchestrator.autonomous) {
|
|
2450
|
+
ui.orchestratorDetail = true;
|
|
2451
|
+
ui.orchestratorVerbose = false;
|
|
2452
|
+
ui.controlSelected = true;
|
|
2453
|
+
} else message = 'This workflow has no autonomous Workflow Planner.';
|
|
2454
|
+
} else {
|
|
2455
|
+
ui.focus = 1;
|
|
2456
|
+
ui.followActiveAgent = true;
|
|
2457
|
+
}
|
|
2331
2458
|
} else if (ui.focus === 1) {
|
|
2332
2459
|
const row = detailRow(bullswarmDir, selectedRunId);
|
|
2333
2460
|
if (workflowPanelModel(row, { phaseIndex: ui.phaseIndex }).agents.length) ui.focus = 2;
|
|
@@ -2341,10 +2468,13 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2341
2468
|
ui.orchestratorDetail = false;
|
|
2342
2469
|
ui.orchestratorVerbose = false;
|
|
2343
2470
|
ui.focus = 0;
|
|
2471
|
+
ui.controlSelected = output.columns >= 100;
|
|
2472
|
+
if (output.columns < 100 && ui.mobileTimeline) ui.timelineSelection = 0;
|
|
2344
2473
|
} else if (ui.workflowVerbose) {
|
|
2345
2474
|
ui.workflowVerbose = false;
|
|
2346
2475
|
} else if (detail && ui.focus > 0) {
|
|
2347
2476
|
ui.focus -= 1;
|
|
2477
|
+
if (ui.focus === 0 && output.columns < 100 && ui.mobileTimeline) ui.timelineSelection = (ui.phaseIndex ?? 0) + 1;
|
|
2348
2478
|
} else if (detail) {
|
|
2349
2479
|
detail = false;
|
|
2350
2480
|
}
|
|
@@ -2354,8 +2484,8 @@ export async function runDashboard(bullswarmDir, {
|
|
|
2354
2484
|
if (keyPressed('up', key)) return moveVertical(-1);
|
|
2355
2485
|
if (keyPressed('down', key)) return moveVertical(1);
|
|
2356
2486
|
const timelineScroll = detail && ui.focus === 0 && !ui.orchestratorDetail && !ui.workflowVerbose;
|
|
2357
|
-
if (key === '\u001b[5~') { ui.detailScroll = timelineScroll ? ui.detailScroll + 8 : Math.max(0, ui.detailScroll - 8); return paint(); }
|
|
2358
|
-
if (key === '\u001b[6~') { ui.detailScroll = timelineScroll ? Math.max(0, ui.detailScroll - 8) : ui.detailScroll + 8; return paint(); }
|
|
2487
|
+
if (key === '\u001b[5~') { if (timelineScroll) ui.timelineSelection = null; ui.detailScroll = timelineScroll ? ui.detailScroll + 8 : Math.max(0, ui.detailScroll - 8); return paint(); }
|
|
2488
|
+
if (key === '\u001b[6~') { if (timelineScroll) ui.timelineSelection = null; ui.detailScroll = timelineScroll ? Math.max(0, ui.detailScroll - 8) : ui.detailScroll + 8; return paint(); }
|
|
2359
2489
|
if (key === 'o' && detail) {
|
|
2360
2490
|
const row = detailRow(bullswarmDir, selectedRunId);
|
|
2361
2491
|
const model = workflowPanelModel(row, { phaseIndex: ui.phaseIndex, agentIndex: ui.agentIndex });
|