thepopebot 1.2.46 → 1.2.47

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.
@@ -245,7 +245,7 @@ export async function getSwarmStatus(page = 1) {
245
245
  return await fetchStatus(page);
246
246
  } catch (err) {
247
247
  console.error('Failed to get swarm status:', err);
248
- return { error: 'Failed to get swarm status', active: [], completed: [], hasMore: false, counts: { running: 0, queued: 0 } };
248
+ return { error: 'Failed to get swarm status', runs: [], hasMore: false, counts: { running: 0, queued: 0 } };
249
249
  }
250
250
  }
251
251
 
@@ -29,7 +29,7 @@ function LoadingSkeleton() {
29
29
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
30
30
  /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-3", children: [...Array(2)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-20 animate-pulse rounded-md bg-border/50" }, i)) }),
31
31
  /* @__PURE__ */ jsx("div", { className: "h-8 w-32 animate-pulse rounded-md bg-border/50" }),
32
- [...Array(3)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-24 animate-pulse rounded-md bg-border/50" }, i))
32
+ [...Array(3)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-14 animate-pulse rounded-md bg-border/50" }, i))
33
33
  ] });
34
34
  }
35
35
  function SwarmSummaryCards({ counts }) {
@@ -49,119 +49,75 @@ function SwarmSummaryCards({ counts }) {
49
49
  card.label
50
50
  )) });
51
51
  }
52
- function SwarmActiveJobs({ jobs, onCancel }) {
53
- if (!jobs || jobs.length === 0) {
54
- return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground py-4 text-center", children: "No active jobs." });
52
+ const conclusionBadgeStyles = {
53
+ success: "bg-green-500/10 text-green-500",
54
+ failure: "bg-red-500/10 text-red-500",
55
+ cancelled: "bg-yellow-500/10 text-yellow-500",
56
+ skipped: "bg-muted text-muted-foreground"
57
+ };
58
+ function SwarmWorkflowList({ runs, onCancel, onRerun }) {
59
+ if (!runs || runs.length === 0) {
60
+ return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground py-4 text-center", children: "No workflow runs." });
55
61
  }
56
- return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: jobs.map((job) => {
57
- const isRunning = job.status === "in_progress";
58
- const progress = job.steps_total > 0 ? Math.round(job.steps_completed / job.steps_total * 100) : 0;
59
- return /* @__PURE__ */ jsxs("div", { className: "rounded-md border bg-card p-4", children: [
60
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
61
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
62
- /* @__PURE__ */ jsx(
63
- "span",
64
- {
65
- className: `inline-block h-2.5 w-2.5 rounded-full ${isRunning ? "bg-green-500 animate-pulse" : "bg-yellow-500"}`
66
- }
67
- ),
68
- /* @__PURE__ */ jsx("span", { className: "font-mono text-sm font-medium", children: job.job_id.slice(0, 8) }),
69
- job.workflow_name && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: job.workflow_name })
70
- ] }),
71
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
72
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: formatDuration(job.duration_seconds) }),
73
- job.html_url && /* @__PURE__ */ jsx(
74
- "a",
75
- {
76
- href: job.html_url,
77
- target: "_blank",
78
- rel: "noopener noreferrer",
79
- className: "text-xs text-blue-500 hover:underline",
80
- children: "View"
81
- }
82
- ),
83
- /* @__PURE__ */ jsx(
84
- "button",
85
- {
86
- onClick: () => onCancel(job.run_id),
87
- className: "inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive",
88
- title: "Cancel job",
89
- children: /* @__PURE__ */ jsx(StopIcon, { size: 14 })
90
- }
91
- )
92
- ] })
93
- ] }),
94
- job.current_step && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-2 truncate", children: job.current_step }),
95
- job.steps_total > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
96
- /* @__PURE__ */ jsx("div", { className: "flex-1 h-1.5 rounded-full bg-border overflow-hidden", children: /* @__PURE__ */ jsx(
97
- "div",
62
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-col divide-y divide-border", children: runs.map((run) => {
63
+ const isActive = run.status === "in_progress" || run.status === "queued";
64
+ const isRunning = run.status === "in_progress";
65
+ const isQueued = run.status === "queued";
66
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-3 px-1", children: [
67
+ isRunning && /* @__PURE__ */ jsx("span", { className: "inline-block h-2.5 w-2.5 shrink-0 rounded-full bg-green-500 animate-pulse" }),
68
+ isQueued && /* @__PURE__ */ jsx("span", { className: "inline-block h-2.5 w-2.5 shrink-0 rounded-full bg-yellow-500" }),
69
+ !isActive && /* @__PURE__ */ jsx(
70
+ "span",
71
+ {
72
+ className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium uppercase shrink-0 ${conclusionBadgeStyles[run.conclusion] || "bg-muted text-muted-foreground"}`,
73
+ children: run.conclusion || "unknown"
74
+ }
75
+ ),
76
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium truncate", children: run.workflow_name || run.branch }),
77
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground shrink-0", children: isActive ? formatDuration(run.duration_seconds) : timeAgo(run.updated_at || run.started_at) }),
78
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
79
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
80
+ run.html_url && /* @__PURE__ */ jsx(
81
+ "a",
82
+ {
83
+ href: run.html_url,
84
+ target: "_blank",
85
+ rel: "noopener noreferrer",
86
+ className: "text-xs text-blue-500 hover:underline",
87
+ children: "View"
88
+ }
89
+ ),
90
+ isActive && /* @__PURE__ */ jsx(
91
+ "button",
98
92
  {
99
- className: "h-full rounded-full bg-green-500 transition-all duration-500",
100
- style: { width: `${progress}%` }
93
+ onClick: () => onCancel(run.run_id),
94
+ className: "inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive",
95
+ title: "Cancel",
96
+ children: /* @__PURE__ */ jsx(StopIcon, { size: 14 })
101
97
  }
102
- ) }),
103
- /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground shrink-0", children: [
104
- job.steps_completed,
105
- "/",
106
- job.steps_total
107
- ] })
98
+ ),
99
+ !isActive && /* @__PURE__ */ jsx(
100
+ "button",
101
+ {
102
+ onClick: () => onRerun(run.run_id, false),
103
+ className: "text-xs rounded-md px-2 py-1 border hover:bg-accent",
104
+ children: "Rerun"
105
+ }
106
+ ),
107
+ !isActive && run.conclusion === "failure" && /* @__PURE__ */ jsx(
108
+ "button",
109
+ {
110
+ onClick: () => onRerun(run.run_id, true),
111
+ className: "text-xs rounded-md px-2 py-1 border text-red-500 hover:bg-red-500/10",
112
+ children: "Rerun failed"
113
+ }
114
+ )
108
115
  ] })
109
- ] }, job.run_id);
116
+ ] }, run.run_id);
110
117
  }) });
111
118
  }
112
- function SwarmJobHistory({ jobs, onRerun }) {
113
- if (!jobs || jobs.length === 0) {
114
- return /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground py-4 text-center", children: "No completed jobs." });
115
- }
116
- const badgeStyles = {
117
- success: "bg-green-500/10 text-green-500",
118
- failure: "bg-red-500/10 text-red-500",
119
- cancelled: "bg-yellow-500/10 text-yellow-500"
120
- };
121
- return /* @__PURE__ */ jsx("div", { className: "flex flex-col divide-y divide-border", children: jobs.map((job) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-3 px-1", children: [
122
- /* @__PURE__ */ jsx(
123
- "span",
124
- {
125
- className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium uppercase ${badgeStyles[job.conclusion] || "bg-muted text-muted-foreground"}`,
126
- children: job.conclusion || "unknown"
127
- }
128
- ),
129
- /* @__PURE__ */ jsx("span", { className: "font-mono text-sm", children: job.job_id.slice(0, 8) }),
130
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: timeAgo(job.updated_at || job.started_at) }),
131
- /* @__PURE__ */ jsx("div", { className: "flex-1" }),
132
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
133
- job.html_url && /* @__PURE__ */ jsx(
134
- "a",
135
- {
136
- href: job.html_url,
137
- target: "_blank",
138
- rel: "noopener noreferrer",
139
- className: "text-xs text-blue-500 hover:underline",
140
- children: "View"
141
- }
142
- ),
143
- /* @__PURE__ */ jsx(
144
- "button",
145
- {
146
- onClick: () => onRerun(job.run_id, false),
147
- className: "text-xs rounded-md px-2 py-1 border hover:bg-accent",
148
- children: "Rerun"
149
- }
150
- ),
151
- job.conclusion === "failure" && /* @__PURE__ */ jsx(
152
- "button",
153
- {
154
- onClick: () => onRerun(job.run_id, true),
155
- className: "text-xs rounded-md px-2 py-1 border text-red-500 hover:bg-red-500/10",
156
- children: "Rerun failed"
157
- }
158
- )
159
- ] })
160
- ] }, job.run_id)) });
161
- }
162
119
  function SwarmPage({ session }) {
163
- const [active, setActive] = useState([]);
164
- const [completed, setCompleted] = useState([]);
120
+ const [runs, setRuns] = useState([]);
165
121
  const [counts, setCounts] = useState({ running: 0, queued: 0 });
166
122
  const [hasMore, setHasMore] = useState(false);
167
123
  const [page, setPage] = useState(1);
@@ -171,11 +127,10 @@ function SwarmPage({ session }) {
171
127
  const fetchStatus = useCallback(async () => {
172
128
  try {
173
129
  const data = await getSwarmStatus();
174
- setActive(data.active || []);
175
130
  setCounts(data.counts || { running: 0, queued: 0 });
176
131
  setHasMore(data.hasMore || false);
177
- setCompleted((prev) => {
178
- const firstPage = data.completed || [];
132
+ setRuns((prev) => {
133
+ const firstPage = data.runs || [];
179
134
  if (page <= 1) return firstPage;
180
135
  const beyondPage1 = prev.slice(firstPage.length);
181
136
  return [...firstPage, ...beyondPage1];
@@ -197,9 +152,8 @@ function SwarmPage({ session }) {
197
152
  setPage(1);
198
153
  try {
199
154
  const data = await getSwarmStatus();
200
- setActive(data.active || []);
201
155
  setCounts(data.counts || { running: 0, queued: 0 });
202
- setCompleted(data.completed || []);
156
+ setRuns(data.runs || []);
203
157
  setHasMore(data.hasMore || false);
204
158
  } catch (err) {
205
159
  console.error("Failed to fetch swarm status:", err);
@@ -211,7 +165,7 @@ function SwarmPage({ session }) {
211
165
  setLoadingMore(true);
212
166
  try {
213
167
  const data = await getSwarmStatus(nextPage);
214
- setCompleted((prev) => [...prev, ...data.completed || []]);
168
+ setRuns((prev) => [...prev, ...data.runs || []]);
215
169
  setHasMore(data.hasMore || false);
216
170
  setPage(nextPage);
217
171
  } catch (err) {
@@ -257,21 +211,12 @@ function SwarmPage({ session }) {
257
211
  loading ? /* @__PURE__ */ jsx(LoadingSkeleton, {}) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-6", children: [
258
212
  /* @__PURE__ */ jsx(SwarmSummaryCards, { counts }),
259
213
  /* @__PURE__ */ jsxs("div", { children: [
260
- /* @__PURE__ */ jsx("h2", { className: "text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3", children: "Active Jobs" }),
261
- /* @__PURE__ */ jsx(
262
- SwarmActiveJobs,
263
- {
264
- jobs: active,
265
- onCancel: handleCancel
266
- }
267
- )
268
- ] }),
269
- /* @__PURE__ */ jsxs("div", { children: [
270
- /* @__PURE__ */ jsx("h2", { className: "text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3", children: "Job History" }),
214
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3", children: "Workflow Runs" }),
271
215
  /* @__PURE__ */ jsx(
272
- SwarmJobHistory,
216
+ SwarmWorkflowList,
273
217
  {
274
- jobs: completed,
218
+ runs,
219
+ onCancel: handleCancel,
275
220
  onRerun: handleRerun
276
221
  }
277
222
  ),
@@ -42,7 +42,7 @@ function LoadingSkeleton() {
42
42
  </div>
43
43
  <div className="h-8 w-32 animate-pulse rounded-md bg-border/50" />
44
44
  {[...Array(3)].map((_, i) => (
45
- <div key={i} className="h-24 animate-pulse rounded-md bg-border/50" />
45
+ <div key={i} className="h-14 animate-pulse rounded-md bg-border/50" />
46
46
  ))}
47
47
  </div>
48
48
  );
@@ -74,90 +74,104 @@ function SwarmSummaryCards({ counts }) {
74
74
  }
75
75
 
76
76
  // ─────────────────────────────────────────────────────────────────────────────
77
- // Active Jobs
77
+ // Unified Workflow List
78
78
  // ─────────────────────────────────────────────────────────────────────────────
79
79
 
80
- function SwarmActiveJobs({ jobs, onCancel }) {
81
- if (!jobs || jobs.length === 0) {
80
+ const conclusionBadgeStyles = {
81
+ success: 'bg-green-500/10 text-green-500',
82
+ failure: 'bg-red-500/10 text-red-500',
83
+ cancelled: 'bg-yellow-500/10 text-yellow-500',
84
+ skipped: 'bg-muted text-muted-foreground',
85
+ };
86
+
87
+ function SwarmWorkflowList({ runs, onCancel, onRerun }) {
88
+ if (!runs || runs.length === 0) {
82
89
  return (
83
90
  <div className="text-sm text-muted-foreground py-4 text-center">
84
- No active jobs.
91
+ No workflow runs.
85
92
  </div>
86
93
  );
87
94
  }
88
95
 
89
96
  return (
90
- <div className="flex flex-col gap-3">
91
- {jobs.map((job) => {
92
- const isRunning = job.status === 'in_progress';
93
- const progress = job.steps_total > 0
94
- ? Math.round((job.steps_completed / job.steps_total) * 100)
95
- : 0;
97
+ <div className="flex flex-col divide-y divide-border">
98
+ {runs.map((run) => {
99
+ const isActive = run.status === 'in_progress' || run.status === 'queued';
100
+ const isRunning = run.status === 'in_progress';
101
+ const isQueued = run.status === 'queued';
96
102
 
97
103
  return (
98
- <div key={job.run_id} className="rounded-md border bg-card p-4">
99
- <div className="flex items-center justify-between mb-2">
100
- <div className="flex items-center gap-2">
101
- {/* Status dot */}
102
- <span
103
- className={`inline-block h-2.5 w-2.5 rounded-full ${
104
- isRunning ? 'bg-green-500 animate-pulse' : 'bg-yellow-500'
105
- }`}
106
- />
107
- <span className="font-mono text-sm font-medium">
108
- {job.job_id.slice(0, 8)}
109
- </span>
110
- {job.workflow_name && (
111
- <span className="text-xs text-muted-foreground">
112
- {job.workflow_name}
113
- </span>
114
- )}
115
- </div>
116
- <div className="flex items-center gap-2">
117
- <span className="text-xs text-muted-foreground">
118
- {formatDuration(job.duration_seconds)}
119
- </span>
120
- {job.html_url && (
121
- <a
122
- href={job.html_url}
123
- target="_blank"
124
- rel="noopener noreferrer"
125
- className="text-xs text-blue-500 hover:underline"
126
- >
127
- View
128
- </a>
129
- )}
104
+ <div key={run.run_id} className="flex items-center gap-3 py-3 px-1">
105
+ {/* Status indicator */}
106
+ {isRunning && (
107
+ <span className="inline-block h-2.5 w-2.5 shrink-0 rounded-full bg-green-500 animate-pulse" />
108
+ )}
109
+ {isQueued && (
110
+ <span className="inline-block h-2.5 w-2.5 shrink-0 rounded-full bg-yellow-500" />
111
+ )}
112
+ {!isActive && (
113
+ <span
114
+ className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium uppercase shrink-0 ${
115
+ conclusionBadgeStyles[run.conclusion] || 'bg-muted text-muted-foreground'
116
+ }`}
117
+ >
118
+ {run.conclusion || 'unknown'}
119
+ </span>
120
+ )}
121
+
122
+ {/* Workflow name */}
123
+ <span className="text-sm font-medium truncate">
124
+ {run.workflow_name || run.branch}
125
+ </span>
126
+
127
+ {/* Duration or time ago */}
128
+ <span className="text-xs text-muted-foreground shrink-0">
129
+ {isActive
130
+ ? formatDuration(run.duration_seconds)
131
+ : timeAgo(run.updated_at || run.started_at)}
132
+ </span>
133
+
134
+ {/* Spacer */}
135
+ <div className="flex-1" />
136
+
137
+ {/* Actions */}
138
+ <div className="flex items-center gap-2 shrink-0">
139
+ {run.html_url && (
140
+ <a
141
+ href={run.html_url}
142
+ target="_blank"
143
+ rel="noopener noreferrer"
144
+ className="text-xs text-blue-500 hover:underline"
145
+ >
146
+ View
147
+ </a>
148
+ )}
149
+ {isActive && (
130
150
  <button
131
- onClick={() => onCancel(job.run_id)}
151
+ onClick={() => onCancel(run.run_id)}
132
152
  className="inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
133
- title="Cancel job"
153
+ title="Cancel"
134
154
  >
135
155
  <StopIcon size={14} />
136
156
  </button>
137
- </div>
157
+ )}
158
+ {!isActive && (
159
+ <button
160
+ onClick={() => onRerun(run.run_id, false)}
161
+ className="text-xs rounded-md px-2 py-1 border hover:bg-accent"
162
+ >
163
+ Rerun
164
+ </button>
165
+ )}
166
+ {!isActive && run.conclusion === 'failure' && (
167
+ <button
168
+ onClick={() => onRerun(run.run_id, true)}
169
+ className="text-xs rounded-md px-2 py-1 border text-red-500 hover:bg-red-500/10"
170
+ >
171
+ Rerun failed
172
+ </button>
173
+ )}
138
174
  </div>
139
-
140
- {/* Current step */}
141
- {job.current_step && (
142
- <p className="text-xs text-muted-foreground mb-2 truncate">
143
- {job.current_step}
144
- </p>
145
- )}
146
-
147
- {/* Progress bar */}
148
- {job.steps_total > 0 && (
149
- <div className="flex items-center gap-2">
150
- <div className="flex-1 h-1.5 rounded-full bg-border overflow-hidden">
151
- <div
152
- className="h-full rounded-full bg-green-500 transition-all duration-500"
153
- style={{ width: `${progress}%` }}
154
- />
155
- </div>
156
- <span className="text-xs text-muted-foreground shrink-0">
157
- {job.steps_completed}/{job.steps_total}
158
- </span>
159
- </div>
160
- )}
161
175
  </div>
162
176
  );
163
177
  })}
@@ -165,89 +179,12 @@ function SwarmActiveJobs({ jobs, onCancel }) {
165
179
  );
166
180
  }
167
181
 
168
- // ─────────────────────────────────────────────────────────────────────────────
169
- // Job History
170
- // ─────────────────────────────────────────────────────────────────────────────
171
-
172
- function SwarmJobHistory({ jobs, onRerun }) {
173
- if (!jobs || jobs.length === 0) {
174
- return (
175
- <div className="text-sm text-muted-foreground py-4 text-center">
176
- No completed jobs.
177
- </div>
178
- );
179
- }
180
-
181
- const badgeStyles = {
182
- success: 'bg-green-500/10 text-green-500',
183
- failure: 'bg-red-500/10 text-red-500',
184
- cancelled: 'bg-yellow-500/10 text-yellow-500',
185
- };
186
-
187
- return (
188
- <div className="flex flex-col divide-y divide-border">
189
- {jobs.map((job) => (
190
- <div key={job.run_id} className="flex items-center gap-3 py-3 px-1">
191
- {/* Conclusion badge */}
192
- <span
193
- className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium uppercase ${
194
- badgeStyles[job.conclusion] || 'bg-muted text-muted-foreground'
195
- }`}
196
- >
197
- {job.conclusion || 'unknown'}
198
- </span>
199
-
200
- {/* Job ID */}
201
- <span className="font-mono text-sm">{job.job_id.slice(0, 8)}</span>
202
-
203
- {/* Time ago */}
204
- <span className="text-xs text-muted-foreground">
205
- {timeAgo(job.updated_at || job.started_at)}
206
- </span>
207
-
208
- {/* Spacer */}
209
- <div className="flex-1" />
210
-
211
- {/* Actions */}
212
- <div className="flex items-center gap-2">
213
- {job.html_url && (
214
- <a
215
- href={job.html_url}
216
- target="_blank"
217
- rel="noopener noreferrer"
218
- className="text-xs text-blue-500 hover:underline"
219
- >
220
- View
221
- </a>
222
- )}
223
- <button
224
- onClick={() => onRerun(job.run_id, false)}
225
- className="text-xs rounded-md px-2 py-1 border hover:bg-accent"
226
- >
227
- Rerun
228
- </button>
229
- {job.conclusion === 'failure' && (
230
- <button
231
- onClick={() => onRerun(job.run_id, true)}
232
- className="text-xs rounded-md px-2 py-1 border text-red-500 hover:bg-red-500/10"
233
- >
234
- Rerun failed
235
- </button>
236
- )}
237
- </div>
238
- </div>
239
- ))}
240
- </div>
241
- );
242
- }
243
-
244
182
  // ─────────────────────────────────────────────────────────────────────────────
245
183
  // Main Page
246
184
  // ─────────────────────────────────────────────────────────────────────────────
247
185
 
248
186
  export function SwarmPage({ session }) {
249
- const [active, setActive] = useState([]);
250
- const [completed, setCompleted] = useState([]);
187
+ const [runs, setRuns] = useState([]);
251
188
  const [counts, setCounts] = useState({ running: 0, queued: 0 });
252
189
  const [hasMore, setHasMore] = useState(false);
253
190
  const [page, setPage] = useState(1);
@@ -258,15 +195,12 @@ export function SwarmPage({ session }) {
258
195
  const fetchStatus = useCallback(async () => {
259
196
  try {
260
197
  const data = await getSwarmStatus();
261
- setActive(data.active || []);
262
198
  setCounts(data.counts || { running: 0, queued: 0 });
263
199
  setHasMore(data.hasMore || false);
264
- // On auto-refresh, replace page 1 completed jobs but keep any loaded beyond page 1
265
- setCompleted((prev) => {
266
- const firstPage = data.completed || [];
267
- // If user hasn't loaded beyond page 1, just use page 1 results
200
+ // On auto-refresh, replace page 1 runs but keep any loaded beyond page 1
201
+ setRuns((prev) => {
202
+ const firstPage = data.runs || [];
268
203
  if (page <= 1) return firstPage;
269
- // Keep accumulated jobs beyond page 1
270
204
  const beyondPage1 = prev.slice(firstPage.length);
271
205
  return [...firstPage, ...beyondPage1];
272
206
  });
@@ -286,13 +220,11 @@ export function SwarmPage({ session }) {
286
220
 
287
221
  const handleRefresh = async () => {
288
222
  setRefreshing(true);
289
- // Reset to page 1
290
223
  setPage(1);
291
224
  try {
292
225
  const data = await getSwarmStatus();
293
- setActive(data.active || []);
294
226
  setCounts(data.counts || { running: 0, queued: 0 });
295
- setCompleted(data.completed || []);
227
+ setRuns(data.runs || []);
296
228
  setHasMore(data.hasMore || false);
297
229
  } catch (err) {
298
230
  console.error('Failed to fetch swarm status:', err);
@@ -305,7 +237,7 @@ export function SwarmPage({ session }) {
305
237
  setLoadingMore(true);
306
238
  try {
307
239
  const data = await getSwarmStatus(nextPage);
308
- setCompleted((prev) => [...prev, ...(data.completed || [])]);
240
+ setRuns((prev) => [...prev, ...(data.runs || [])]);
309
241
  setHasMore(data.hasMore || false);
310
242
  setPage(nextPage);
311
243
  } catch (err) {
@@ -365,24 +297,14 @@ export function SwarmPage({ session }) {
365
297
  {/* Summary Cards */}
366
298
  <SwarmSummaryCards counts={counts} />
367
299
 
368
- {/* Active Jobs */}
300
+ {/* Workflow Runs */}
369
301
  <div>
370
302
  <h2 className="text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3">
371
- Active Jobs
303
+ Workflow Runs
372
304
  </h2>
373
- <SwarmActiveJobs
374
- jobs={active}
305
+ <SwarmWorkflowList
306
+ runs={runs}
375
307
  onCancel={handleCancel}
376
- />
377
- </div>
378
-
379
- {/* Job History */}
380
- <div>
381
- <h2 className="text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3">
382
- Job History
383
- </h2>
384
- <SwarmJobHistory
385
- jobs={completed}
386
308
  onRerun={handleRerun}
387
309
  />
388
310
  {hasMore && (
@@ -30,7 +30,7 @@ async function githubApi(endpoint, options = {}) {
30
30
  * @param {string} [workflow] - Workflow filename to scope to (e.g., 'run-job.yml')
31
31
  * @returns {Promise<object>} - Workflow runs response
32
32
  */
33
- async function getWorkflowRuns(status, workflow, { page = 1, perPage = 100 } = {}) {
33
+ async function getWorkflowRuns(status, { workflow, page = 1, perPage = 100 } = {}) {
34
34
  const { GH_OWNER, GH_REPO } = process.env;
35
35
  const params = new URLSearchParams();
36
36
  if (status) params.set('status', status);
@@ -62,8 +62,8 @@ async function getWorkflowRunJobs(runId) {
62
62
  async function getJobStatus(jobId) {
63
63
  // Fetch both in_progress and queued runs (scoped to run-job.yml)
64
64
  const [inProgress, queued] = await Promise.all([
65
- getWorkflowRuns('in_progress', 'run-job.yml'),
66
- getWorkflowRuns('queued', 'run-job.yml'),
65
+ getWorkflowRuns('in_progress', { workflow: 'run-job.yml' }),
66
+ getWorkflowRuns('queued', { workflow: 'run-job.yml' }),
67
67
  ]);
68
68
 
69
69
  const allRuns = [...(inProgress.workflow_runs || []), ...(queued.workflow_runs || [])];
@@ -125,85 +125,35 @@ async function getJobStatus(jobId) {
125
125
  }
126
126
 
127
127
  /**
128
- * Get full swarm status: active + completed jobs with counts
129
- * @returns {Promise<object>} - { active, completed, counts }
128
+ * Get full swarm status: unified list of all workflow runs with counts
129
+ * @param {number} [page=1] - Page number for pagination
130
+ * @returns {Promise<object>} - { runs, hasMore, counts }
130
131
  */
131
132
  async function getSwarmStatus(page = 1) {
132
- const [inProgress, queued, completed] = await Promise.all([
133
- getWorkflowRuns('in_progress', 'run-job.yml'),
134
- getWorkflowRuns('queued', 'run-job.yml'),
135
- getWorkflowRuns('completed', 'run-job.yml', { page }),
133
+ const [inProgressData, queuedData, allRuns] = await Promise.all([
134
+ getWorkflowRuns('in_progress', { perPage: 1 }),
135
+ getWorkflowRuns('queued', { perPage: 1 }),
136
+ getWorkflowRuns(null, { page, perPage: 25 }),
136
137
  ]);
137
138
 
138
- const activeRuns = [
139
- ...(inProgress.workflow_runs || []),
140
- ...(queued.workflow_runs || []),
141
- ].filter(run => run.head_branch?.startsWith('job/'));
142
-
143
- const completedRuns = (completed.workflow_runs || [])
144
- .filter(run => run.head_branch?.startsWith('job/'));
145
-
146
- // Get step details for active jobs
147
- const active = await Promise.all(
148
- activeRuns.map(async (run) => {
149
- const jobId = run.head_branch.slice(4);
150
- const startedAt = new Date(run.created_at);
151
- const durationSeconds = Math.round((Date.now() - startedAt.getTime()) / 1000);
152
-
153
- let currentStep = null;
154
- let stepsCompleted = 0;
155
- let stepsTotal = 0;
156
-
157
- try {
158
- const jobsData = await getWorkflowRunJobs(run.id);
159
- if (jobsData.jobs?.length > 0) {
160
- const job = jobsData.jobs[0];
161
- stepsTotal = job.steps?.length || 0;
162
- stepsCompleted = job.steps?.filter(s => s.status === 'completed').length || 0;
163
- currentStep = job.steps?.find(s => s.status === 'in_progress')?.name || null;
164
- }
165
- } catch (err) {
166
- // Jobs endpoint may fail if run hasn't started yet
167
- }
168
-
169
- return {
170
- job_id: jobId,
171
- branch: run.head_branch,
172
- status: run.status,
173
- workflow_name: run.name,
174
- started_at: run.created_at,
175
- duration_seconds: durationSeconds,
176
- current_step: currentStep,
177
- steps_completed: stepsCompleted,
178
- steps_total: stepsTotal,
179
- run_id: run.id,
180
- html_url: run.html_url,
181
- };
182
- })
183
- );
184
-
185
- // Completed jobs are lighter — no step detail needed
186
- const completedJobs = completedRuns.map((run) => ({
187
- job_id: run.head_branch.slice(4),
139
+ const runs = (allRuns.workflow_runs || []).map(run => ({
140
+ run_id: run.id,
188
141
  branch: run.head_branch,
189
142
  status: run.status,
190
143
  conclusion: run.conclusion,
191
144
  workflow_name: run.name,
192
145
  started_at: run.created_at,
193
146
  updated_at: run.updated_at,
194
- run_id: run.id,
147
+ duration_seconds: Math.round((Date.now() - new Date(run.created_at).getTime()) / 1000),
195
148
  html_url: run.html_url,
196
149
  }));
197
150
 
198
- const totalCompleted = completed.total_count || 0;
199
-
200
151
  return {
201
- active,
202
- completed: completedJobs,
203
- hasMore: page * 100 < totalCompleted,
152
+ runs,
153
+ hasMore: page * 25 < (allRuns.total_count || 0),
204
154
  counts: {
205
- running: active.filter(j => j.status === 'in_progress').length,
206
- queued: active.filter(j => j.status === 'queued').length,
155
+ running: inProgressData.total_count || 0,
156
+ queued: queuedData.total_count || 0,
207
157
  },
208
158
  };
209
159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thepopebot",
3
- "version": "1.2.46",
3
+ "version": "1.2.47",
4
4
  "type": "module",
5
5
  "description": "Create autonomous AI agents with a two-layer architecture: Next.js Event Handler + Docker Agent.",
6
6
  "bin": {
@@ -13,6 +13,7 @@ jobs:
13
13
  steps:
14
14
  - name: Upgrade thepopebot
15
15
  run: |
16
+ # Phase 1: Update package and build (but don't swap yet)
16
17
  docker exec thepopebot-event-handler bash -c '
17
18
  export GH_TOKEN=$(grep "^GH_TOKEN=" /app/.env | cut -d= -f2-)
18
19
  echo "${GH_TOKEN}" | gh auth login --with-token
@@ -21,19 +22,23 @@ jobs:
21
22
  git fetch origin main
22
23
  git reset --hard origin/main
23
24
 
24
- # Update thepopebot package
25
25
  npm update thepopebot
26
+ npx thepopebot init
26
27
  npm install --omit=dev
27
28
 
28
- # Atomic swap build (same pattern as rebuild-event-handler)
29
- rm -rf .next-new .next-old
29
+ rm -rf .next-new
30
30
  NEXT_BUILD_DIR=.next-new npm run build
31
+ '
32
+
33
+ # Phase 2: Pull new image and restart container
34
+ cd /project
35
+ docker compose pull event-handler
36
+ docker compose up -d event-handler
31
37
 
38
+ # Phase 3: Swap build and reload inside new container
39
+ docker exec thepopebot-event-handler bash -c '
32
40
  mv .next .next-old 2>/dev/null || true
33
41
  mv .next-new .next
34
-
35
- echo "Upgrade complete, reloading..."
36
42
  npx pm2 reload all
37
-
38
43
  rm -rf .next-old
39
44
  '
@@ -54,6 +54,7 @@ services:
54
54
  LABELS: self-hosted
55
55
  volumes:
56
56
  - /var/run/docker.sock:/var/run/docker.sock
57
+ - .:/project:ro
57
58
  restart: unless-stopped
58
59
 
59
60
  volumes: