taskplane 0.6.1 → 0.7.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/README.md CHANGED
@@ -109,12 +109,13 @@ pi
109
109
  Inside the pi session:
110
110
 
111
111
  ```
112
- /orch-plan all # Preview waves, lanes, and dependencies
113
- /orch all # Execute all pending tasks in parallel
114
- /orch-status # Monitor batch progress
112
+ /orch # Detect project state guides onboarding or offers to start a batch
113
+ /orch-plan all # Preview waves, lanes, and dependencies
114
+ /orch all # Execute all pending tasks in parallel
115
+ /orch-status # Monitor batch progress
115
116
  ```
116
117
 
117
- The default scaffold includes two independent example tasks, so `/orch all` gives you an immediate orchestrator + dashboard experience.
118
+ `/orch` with no arguments is the universal entry point — it detects your project state and activates the supervisor for guided interaction (onboarding, batch planning, health checks, or retrospective). The default scaffold includes two independent example tasks, so `/orch all` gives you an immediate orchestrator + dashboard experience.
118
119
 
119
120
  ### 4. Optional: run one task directly
120
121
 
@@ -148,7 +149,7 @@ Orchestrator lanes execute tasks through task-runner under the hood, so `/task`
148
149
  | `/task-status` | Show current task progress |
149
150
  | `/task-pause` | Pause after current worker iteration finishes |
150
151
  | `/task-resume` | Resume a paused task |
151
- | `/orch <areas\|paths\|all>` | Execute tasks via isolated worktrees (recommended default) |
152
+ | `/orch [<areas\|paths\|all>]` | No args: detect state & guide (onboarding, batch planning, etc.); with args: execute tasks via isolated worktrees |
152
153
  | `/orch-plan <areas\|paths\|all>` | Preview execution plan without running |
153
154
  | `/orch-status` | Show batch progress |
154
155
  | `/orch-pause` | Pause batch after current tasks finish |
@@ -722,6 +722,231 @@ function ensureContentPanels() {
722
722
  }
723
723
  }
724
724
 
725
+ // ─── Supervisor Panel ───────────────────────────────────────────────────────
726
+
727
+ const $supervisorPanel = $("supervisor-panel");
728
+ const $supervisorStatusBadge = $("supervisor-status-badge");
729
+ const $supervisorCollapseBtn = $("supervisor-collapse-btn");
730
+ const $supervisorPanelBody = $("supervisor-panel-body");
731
+ const $supervisorStatusSection = $("supervisor-status-section");
732
+ const $supervisorConversationSection = $("supervisor-conversation-section");
733
+ const $supervisorActionsSection = $("supervisor-actions-section");
734
+ const $supervisorSummarySection = $("supervisor-summary-section");
735
+
736
+ let supervisorCollapsed = false;
737
+
738
+ // Toggle collapse on header click
739
+ $("supervisor-panel-toggle").addEventListener("click", (e) => {
740
+ // Don't toggle when clicking the collapse button itself (it has its own handler)
741
+ if (e.target.id === "supervisor-collapse-btn") return;
742
+ toggleSupervisorPanel();
743
+ });
744
+
745
+ $supervisorCollapseBtn.addEventListener("click", toggleSupervisorPanel);
746
+
747
+ function toggleSupervisorPanel() {
748
+ supervisorCollapsed = !supervisorCollapsed;
749
+ $supervisorPanelBody.style.display = supervisorCollapsed ? "none" : "";
750
+ $supervisorCollapseBtn.textContent = supervisorCollapsed ? "▸" : "▾";
751
+ }
752
+
753
+ /** Determine supervisor status from lock data. */
754
+ function supervisorStatusInfo(lock) {
755
+ if (!lock) return { status: "inactive", label: "Inactive", cls: "supervisor-inactive" };
756
+ if (lock.stale) return { status: "stale", label: "Stale", cls: "supervisor-stale" };
757
+ return { status: "active", label: "Active", cls: "supervisor-active" };
758
+ }
759
+
760
+ /** Format a timestamp for the supervisor timeline. */
761
+ function formatSupervisorTime(ts) {
762
+ if (!ts) return "";
763
+ const d = new Date(ts);
764
+ return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
765
+ }
766
+
767
+ /** Render the supervisor status indicator section. */
768
+ function renderSupervisorStatus(supervisor) {
769
+ const lock = supervisor.lock;
770
+ const info = supervisorStatusInfo(lock);
771
+
772
+ // Update the header badge
773
+ $supervisorStatusBadge.textContent = info.label;
774
+ $supervisorStatusBadge.className = `supervisor-status-badge ${info.cls}`;
775
+
776
+ let html = '<div class="supervisor-status-row">';
777
+ html += `<span class="supervisor-status-dot ${info.cls}"></span>`;
778
+ html += `<span class="supervisor-status-label">${info.label}</span>`;
779
+
780
+ if (lock) {
781
+ if (lock.autonomy) {
782
+ html += `<span class="supervisor-autonomy-badge">${escapeHtml(lock.autonomy)}</span>`;
783
+ }
784
+ if (lock.heartbeat) {
785
+ html += `<span class="supervisor-heartbeat" title="Last heartbeat">♡ ${relativeTime(lock.heartbeat)}</span>`;
786
+ }
787
+ if (lock.sessionId) {
788
+ html += `<span class="supervisor-session-id" title="Session: ${escapeHtml(lock.sessionId)}">${escapeHtml(lock.sessionId)}</span>`;
789
+ }
790
+ }
791
+
792
+ html += '</div>';
793
+ $supervisorStatusSection.innerHTML = html;
794
+ }
795
+
796
+ /** Render the conversation history section. */
797
+ function renderSupervisorConversation(supervisor) {
798
+ const conversation = supervisor.conversation || [];
799
+
800
+ if (conversation.length === 0) {
801
+ $supervisorConversationSection.innerHTML = '';
802
+ return;
803
+ }
804
+
805
+ let html = '<div class="supervisor-subsection-title">Conversation</div>';
806
+ html += '<div class="supervisor-conversation-list">';
807
+
808
+ for (const entry of conversation) {
809
+ const time = formatSupervisorTime(entry.ts || entry.timestamp);
810
+ const role = entry.role || "unknown";
811
+ const content = entry.content || entry.message || "";
812
+ const roleCls = role === "operator" ? "conv-role-operator" : "conv-role-supervisor";
813
+ const roleLabel = role === "operator" ? "Operator" : "Supervisor";
814
+
815
+ html += `<div class="supervisor-conv-entry ${roleCls}">`;
816
+ html += ` <div class="supervisor-conv-header">`;
817
+ html += ` <span class="supervisor-conv-role">${roleLabel}</span>`;
818
+ if (time) html += `<span class="supervisor-conv-time">${time}</span>`;
819
+ html += ` </div>`;
820
+ html += ` <div class="supervisor-conv-content">${escapeHtml(content)}</div>`;
821
+ html += `</div>`;
822
+ }
823
+
824
+ html += '</div>';
825
+ $supervisorConversationSection.innerHTML = html;
826
+ }
827
+
828
+ /** Merge supervisor actions and Tier 0 recovery events into a unified timeline.
829
+ * Actions from actions.jsonl and recovery events from events.jsonl are combined
830
+ * and sorted chronologically (per R002: show both Tier 0 and supervisor actions).
831
+ */
832
+ function buildRecoveryTimeline(supervisor) {
833
+ const actions = (supervisor.actions || []).map(a => ({
834
+ ts: a.ts || a.timestamp || 0,
835
+ tier: a.tier,
836
+ type: a.type || a.action || "unknown",
837
+ target: a.target || a.lane || a.taskId || "",
838
+ outcome: a.outcome || a.result || "",
839
+ reason: a.reason || "",
840
+ source: "action"
841
+ }));
842
+
843
+ // Include Tier 0 recovery events from events.jsonl
844
+ const events = (supervisor.events || [])
845
+ .filter(e => e.tier === 0 || e.type === "recovery" || e.type === "tier0_recovery")
846
+ .map(e => ({
847
+ ts: e.ts || e.timestamp || 0,
848
+ tier: e.tier != null ? e.tier : 0,
849
+ type: e.type || "event",
850
+ target: e.target || e.lane || e.taskId || "",
851
+ outcome: e.outcome || e.result || "",
852
+ reason: e.reason || e.message || "",
853
+ source: "event"
854
+ }));
855
+
856
+ const timeline = [...actions, ...events];
857
+ timeline.sort((a, b) => {
858
+ const tA = typeof a.ts === "string" ? new Date(a.ts).getTime() : a.ts;
859
+ const tB = typeof b.ts === "string" ? new Date(b.ts).getTime() : b.ts;
860
+ return tA - tB;
861
+ });
862
+
863
+ return timeline;
864
+ }
865
+
866
+ /** Render the recovery action timeline section. */
867
+ function renderSupervisorActions(supervisor) {
868
+ const timeline = buildRecoveryTimeline(supervisor);
869
+
870
+ if (timeline.length === 0) {
871
+ $supervisorActionsSection.innerHTML = '';
872
+ return;
873
+ }
874
+
875
+ let html = '<div class="supervisor-subsection-title">Recovery Actions</div>';
876
+ html += '<div class="supervisor-timeline">';
877
+
878
+ for (const entry of timeline) {
879
+ const time = formatSupervisorTime(entry.ts);
880
+ const tier = entry.tier != null ? `T${entry.tier}` : "";
881
+ const type = entry.type;
882
+ const target = entry.target;
883
+ const outcome = entry.outcome;
884
+ const reason = entry.reason;
885
+
886
+ const outcomeCls = outcome === "success" || outcome === "recovered"
887
+ ? "action-success"
888
+ : outcome === "failed" || outcome === "error"
889
+ ? "action-failed"
890
+ : "action-pending";
891
+
892
+ html += `<div class="supervisor-action-entry">`;
893
+ html += ` <div class="supervisor-action-left">`;
894
+ html += ` <span class="supervisor-action-time">${time}</span>`;
895
+ html += ` <span class="supervisor-action-dot ${outcomeCls}"></span>`;
896
+ html += ` </div>`;
897
+ html += ` <div class="supervisor-action-right">`;
898
+ html += ` <div class="supervisor-action-header">`;
899
+ if (tier) html += `<span class="supervisor-action-tier">${tier}</span>`;
900
+ html += ` <span class="supervisor-action-type">${escapeHtml(type)}</span>`;
901
+ if (target) html += `<span class="supervisor-action-target">${escapeHtml(target)}</span>`;
902
+ if (outcome) html += `<span class="supervisor-action-outcome ${outcomeCls}">${escapeHtml(outcome)}</span>`;
903
+ html += ` </div>`;
904
+ if (reason) {
905
+ html += `<div class="supervisor-action-reason">${escapeHtml(reason)}</div>`;
906
+ }
907
+ html += ` </div>`;
908
+ html += `</div>`;
909
+ }
910
+
911
+ html += '</div>';
912
+ $supervisorActionsSection.innerHTML = html;
913
+ }
914
+
915
+ /** Render the batch summary section (from summary.md). */
916
+ function renderSupervisorSummary(supervisor) {
917
+ const summary = supervisor.summary;
918
+
919
+ if (!summary) {
920
+ $supervisorSummarySection.innerHTML = '';
921
+ return;
922
+ }
923
+
924
+ let html = '<div class="supervisor-subsection-title">Batch Summary</div>';
925
+ html += '<div class="supervisor-summary-content">';
926
+ // Render the summary markdown using the STATUS.md renderer (reuse)
927
+ const { html: renderedMd } = renderStatusMd(summary);
928
+ html += renderedMd;
929
+ html += '</div>';
930
+ $supervisorSummarySection.innerHTML = html;
931
+ }
932
+
933
+ /** Main supervisor panel render function. */
934
+ function renderSupervisor(data) {
935
+ const supervisor = data.supervisor;
936
+
937
+ if (!supervisor) {
938
+ $supervisorPanel.style.display = "none";
939
+ return;
940
+ }
941
+
942
+ $supervisorPanel.style.display = "";
943
+
944
+ renderSupervisorStatus(supervisor);
945
+ renderSupervisorConversation(supervisor);
946
+ renderSupervisorActions(supervisor);
947
+ renderSupervisorSummary(supervisor);
948
+ }
949
+
725
950
  // ─── Full Render ────────────────────────────────────────────────────────────
726
951
 
727
952
  // ─── Current data (stored for conversation viewer) ──────────────────────────
@@ -738,6 +963,7 @@ function render(data) {
738
963
  if (!batch) {
739
964
  renderHeader(null);
740
965
  renderSummary(null);
966
+ renderSupervisor(data);
741
967
  // Refresh history list (batch may have just finished)
742
968
  if (!noBatchRendered) loadHistoryList();
743
969
  renderNoBatch();
@@ -763,6 +989,7 @@ function render(data) {
763
989
  const repos = buildRepoSet(batch);
764
990
  updateRepoFilter(repos);
765
991
 
992
+ renderSupervisor(data);
766
993
  renderLanesTasks(batch, tmux);
767
994
  renderMergeAgents(batch, tmux);
768
995
  renderErrors(batch);
@@ -51,6 +51,25 @@
51
51
  </div>
52
52
  </div>
53
53
 
54
+ <!-- Supervisor Panel (hidden when no supervisor data) -->
55
+ <div class="panel supervisor-panel" id="supervisor-panel" style="display:none;">
56
+ <div class="panel-header supervisor-panel-header" id="supervisor-panel-toggle">
57
+ <span>Supervisor</span>
58
+ <span class="supervisor-status-badge" id="supervisor-status-badge"></span>
59
+ <button class="supervisor-collapse-btn" id="supervisor-collapse-btn" title="Toggle supervisor panel">▾</button>
60
+ </div>
61
+ <div class="panel-body supervisor-panel-body" id="supervisor-panel-body">
62
+ <!-- Status Section -->
63
+ <div class="supervisor-section" id="supervisor-status-section"></div>
64
+ <!-- Conversation Section -->
65
+ <div class="supervisor-section" id="supervisor-conversation-section"></div>
66
+ <!-- Actions Timeline Section -->
67
+ <div class="supervisor-section" id="supervisor-actions-section"></div>
68
+ <!-- Batch Summary Section -->
69
+ <div class="supervisor-section" id="supervisor-summary-section"></div>
70
+ </div>
71
+ </div>
72
+
54
73
  <!-- Lanes + Tasks (integrated) -->
55
74
  <div class="panel" id="lanes-tasks-panel">
56
75
  <div class="panel-header">Lanes &amp; Tasks</div>
@@ -1038,6 +1038,325 @@ body {
1038
1038
  border-bottom: none;
1039
1039
  }
1040
1040
 
1041
+ /* ─── Supervisor Panel ──────────────────────────────────────────────────── */
1042
+
1043
+ .supervisor-panel {
1044
+ border-color: var(--border);
1045
+ }
1046
+
1047
+ .supervisor-panel-header {
1048
+ display: flex;
1049
+ align-items: center;
1050
+ gap: 10px;
1051
+ cursor: pointer;
1052
+ user-select: none;
1053
+ }
1054
+
1055
+ .supervisor-panel-header:hover {
1056
+ background: var(--bg-surface-hover);
1057
+ }
1058
+
1059
+ .supervisor-collapse-btn {
1060
+ margin-left: auto;
1061
+ background: none;
1062
+ border: none;
1063
+ color: var(--text-muted);
1064
+ font-size: 0.9rem;
1065
+ cursor: pointer;
1066
+ padding: 0 4px;
1067
+ line-height: 1;
1068
+ }
1069
+
1070
+ .supervisor-collapse-btn:hover {
1071
+ color: var(--text);
1072
+ }
1073
+
1074
+ .supervisor-panel-body {
1075
+ padding: 0;
1076
+ }
1077
+
1078
+ .supervisor-section {
1079
+ padding: 0;
1080
+ }
1081
+
1082
+ .supervisor-section:empty {
1083
+ display: none;
1084
+ }
1085
+
1086
+ /* Supervisor status badge (in panel header) */
1087
+ .supervisor-status-badge {
1088
+ font-family: var(--font-mono);
1089
+ font-size: 0.7rem;
1090
+ font-weight: 600;
1091
+ padding: 2px 8px;
1092
+ border-radius: 10px;
1093
+ text-transform: uppercase;
1094
+ letter-spacing: 0.04em;
1095
+ }
1096
+
1097
+ .supervisor-status-badge.supervisor-active {
1098
+ background: rgba(63,185,80,0.15);
1099
+ color: var(--green);
1100
+ }
1101
+
1102
+ .supervisor-status-badge.supervisor-stale {
1103
+ background: rgba(210,153,34,0.15);
1104
+ color: var(--yellow);
1105
+ }
1106
+
1107
+ .supervisor-status-badge.supervisor-inactive {
1108
+ background: rgba(139,148,158,0.1);
1109
+ color: var(--text-muted);
1110
+ }
1111
+
1112
+ /* Status row inside the panel */
1113
+ .supervisor-status-row {
1114
+ display: flex;
1115
+ align-items: center;
1116
+ gap: 10px;
1117
+ padding: 10px 14px;
1118
+ border-bottom: 1px solid var(--border-subtle);
1119
+ flex-wrap: wrap;
1120
+ }
1121
+
1122
+ .supervisor-status-dot {
1123
+ width: 8px;
1124
+ height: 8px;
1125
+ border-radius: 50%;
1126
+ flex-shrink: 0;
1127
+ }
1128
+
1129
+ .supervisor-status-dot.supervisor-active {
1130
+ background: var(--green);
1131
+ animation: pulse 2s infinite;
1132
+ }
1133
+
1134
+ .supervisor-status-dot.supervisor-stale {
1135
+ background: var(--yellow);
1136
+ }
1137
+
1138
+ .supervisor-status-dot.supervisor-inactive {
1139
+ background: var(--text-faint);
1140
+ }
1141
+
1142
+ .supervisor-status-label {
1143
+ font-weight: 600;
1144
+ font-size: 0.85rem;
1145
+ }
1146
+
1147
+ .supervisor-autonomy-badge {
1148
+ font-family: var(--font-mono);
1149
+ font-size: 0.7rem;
1150
+ font-weight: 500;
1151
+ padding: 1px 7px;
1152
+ border-radius: 8px;
1153
+ background: rgba(188,140,255,0.12);
1154
+ color: var(--magenta);
1155
+ }
1156
+
1157
+ .supervisor-heartbeat {
1158
+ font-family: var(--font-mono);
1159
+ font-size: 0.75rem;
1160
+ color: var(--text-muted);
1161
+ }
1162
+
1163
+ .supervisor-session-id {
1164
+ font-family: var(--font-mono);
1165
+ font-size: 0.72rem;
1166
+ color: var(--text-faint);
1167
+ overflow: hidden;
1168
+ text-overflow: ellipsis;
1169
+ max-width: 200px;
1170
+ white-space: nowrap;
1171
+ }
1172
+
1173
+ /* Subsection titles */
1174
+ .supervisor-subsection-title {
1175
+ font-size: 0.72rem;
1176
+ font-weight: 600;
1177
+ text-transform: uppercase;
1178
+ letter-spacing: 0.05em;
1179
+ color: var(--text-faint);
1180
+ padding: 8px 14px 4px;
1181
+ border-top: 1px solid var(--border-subtle);
1182
+ }
1183
+
1184
+ /* ── Conversation List ─────────────────────────────────────────────────── */
1185
+
1186
+ .supervisor-conversation-list {
1187
+ max-height: 250px;
1188
+ overflow-y: auto;
1189
+ padding: 4px 14px 8px;
1190
+ }
1191
+
1192
+ .supervisor-conv-entry {
1193
+ padding: 6px 10px;
1194
+ margin-bottom: 4px;
1195
+ border-radius: var(--radius-sm);
1196
+ font-size: 0.82rem;
1197
+ }
1198
+
1199
+ .supervisor-conv-entry.conv-role-operator {
1200
+ background: rgba(88,166,255,0.06);
1201
+ border-left: 3px solid var(--accent-dim);
1202
+ }
1203
+
1204
+ .supervisor-conv-entry.conv-role-supervisor {
1205
+ background: rgba(63,185,80,0.06);
1206
+ border-left: 3px solid var(--green-dim);
1207
+ }
1208
+
1209
+ .supervisor-conv-header {
1210
+ display: flex;
1211
+ align-items: center;
1212
+ gap: 8px;
1213
+ margin-bottom: 2px;
1214
+ }
1215
+
1216
+ .supervisor-conv-role {
1217
+ font-weight: 600;
1218
+ font-size: 0.75rem;
1219
+ text-transform: uppercase;
1220
+ letter-spacing: 0.04em;
1221
+ }
1222
+
1223
+ .conv-role-operator .supervisor-conv-role { color: var(--accent); }
1224
+ .conv-role-supervisor .supervisor-conv-role { color: var(--green); }
1225
+
1226
+ .supervisor-conv-time {
1227
+ font-family: var(--font-mono);
1228
+ font-size: 0.68rem;
1229
+ color: var(--text-faint);
1230
+ }
1231
+
1232
+ .supervisor-conv-content {
1233
+ color: var(--text);
1234
+ line-height: 1.5;
1235
+ white-space: pre-wrap;
1236
+ word-break: break-word;
1237
+ }
1238
+
1239
+ /* ── Recovery Actions Timeline ─────────────────────────────────────────── */
1240
+
1241
+ .supervisor-timeline {
1242
+ max-height: 300px;
1243
+ overflow-y: auto;
1244
+ padding: 4px 14px 8px;
1245
+ }
1246
+
1247
+ .supervisor-action-entry {
1248
+ display: flex;
1249
+ gap: 10px;
1250
+ padding: 6px 0;
1251
+ border-bottom: 1px solid var(--border-subtle);
1252
+ }
1253
+
1254
+ .supervisor-action-entry:last-child {
1255
+ border-bottom: none;
1256
+ }
1257
+
1258
+ .supervisor-action-left {
1259
+ display: flex;
1260
+ flex-direction: column;
1261
+ align-items: center;
1262
+ gap: 4px;
1263
+ min-width: 60px;
1264
+ flex-shrink: 0;
1265
+ }
1266
+
1267
+ .supervisor-action-time {
1268
+ font-family: var(--font-mono);
1269
+ font-size: 0.68rem;
1270
+ color: var(--text-faint);
1271
+ white-space: nowrap;
1272
+ }
1273
+
1274
+ .supervisor-action-dot {
1275
+ width: 8px;
1276
+ height: 8px;
1277
+ border-radius: 50%;
1278
+ }
1279
+
1280
+ .supervisor-action-dot.action-success { background: var(--green); }
1281
+ .supervisor-action-dot.action-failed { background: var(--red); }
1282
+ .supervisor-action-dot.action-pending { background: var(--yellow); }
1283
+
1284
+ .supervisor-action-right {
1285
+ flex: 1;
1286
+ min-width: 0;
1287
+ }
1288
+
1289
+ .supervisor-action-header {
1290
+ display: flex;
1291
+ align-items: center;
1292
+ gap: 6px;
1293
+ flex-wrap: wrap;
1294
+ }
1295
+
1296
+ .supervisor-action-tier {
1297
+ font-family: var(--font-mono);
1298
+ font-size: 0.68rem;
1299
+ font-weight: 700;
1300
+ padding: 1px 5px;
1301
+ border-radius: 4px;
1302
+ background: rgba(188,140,255,0.12);
1303
+ color: var(--magenta);
1304
+ }
1305
+
1306
+ .supervisor-action-type {
1307
+ font-weight: 600;
1308
+ font-size: 0.82rem;
1309
+ color: var(--text);
1310
+ }
1311
+
1312
+ .supervisor-action-target {
1313
+ font-family: var(--font-mono);
1314
+ font-size: 0.75rem;
1315
+ color: var(--text-muted);
1316
+ }
1317
+
1318
+ .supervisor-action-outcome {
1319
+ font-family: var(--font-mono);
1320
+ font-size: 0.7rem;
1321
+ font-weight: 600;
1322
+ padding: 1px 6px;
1323
+ border-radius: 8px;
1324
+ }
1325
+
1326
+ .supervisor-action-outcome.action-success {
1327
+ background: rgba(63,185,80,0.15);
1328
+ color: var(--green);
1329
+ }
1330
+
1331
+ .supervisor-action-outcome.action-failed {
1332
+ background: rgba(248,81,73,0.15);
1333
+ color: var(--red);
1334
+ }
1335
+
1336
+ .supervisor-action-outcome.action-pending {
1337
+ background: rgba(210,153,34,0.15);
1338
+ color: var(--yellow);
1339
+ }
1340
+
1341
+ .supervisor-action-reason {
1342
+ font-size: 0.78rem;
1343
+ color: var(--text-muted);
1344
+ margin-top: 2px;
1345
+ line-height: 1.4;
1346
+ }
1347
+
1348
+ /* ── Batch Summary ─────────────────────────────────────────────────────── */
1349
+
1350
+ .supervisor-summary-content {
1351
+ padding: 0 14px 8px;
1352
+ max-height: 300px;
1353
+ overflow-y: auto;
1354
+ }
1355
+
1356
+ .supervisor-summary-content .status-md-content {
1357
+ padding: 4px 0;
1358
+ }
1359
+
1041
1360
  /* ─── Scrollbar ────────────────────────────────────────────────────────── */
1042
1361
 
1043
1362
  ::-webkit-scrollbar { width: 6px; }