shipwright-cli 2.2.2 → 2.3.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.
Files changed (151) hide show
  1. package/README.md +12 -11
  2. package/dashboard/public/index.html +224 -8
  3. package/dashboard/public/styles.css +1078 -4
  4. package/dashboard/server.ts +1100 -15
  5. package/dashboard/src/canvas/interactions.ts +74 -0
  6. package/dashboard/src/canvas/layout.ts +85 -0
  7. package/dashboard/src/canvas/overlays.ts +117 -0
  8. package/dashboard/src/canvas/particles.ts +105 -0
  9. package/dashboard/src/canvas/renderer.ts +191 -0
  10. package/dashboard/src/components/charts/bar.ts +54 -0
  11. package/dashboard/src/components/charts/donut.ts +25 -0
  12. package/dashboard/src/components/charts/pipeline-rail.ts +105 -0
  13. package/dashboard/src/components/charts/sparkline.ts +82 -0
  14. package/dashboard/src/components/header.ts +616 -0
  15. package/dashboard/src/components/modal.ts +413 -0
  16. package/dashboard/src/components/terminal.ts +144 -0
  17. package/dashboard/src/core/api.test.ts +362 -0
  18. package/dashboard/src/core/api.ts +381 -0
  19. package/dashboard/src/core/helpers.ts +118 -0
  20. package/dashboard/src/core/router.test.ts +266 -0
  21. package/dashboard/src/core/router.ts +190 -0
  22. package/dashboard/src/core/sse.ts +38 -0
  23. package/dashboard/src/core/state.test.ts +235 -0
  24. package/dashboard/src/core/state.ts +150 -0
  25. package/dashboard/src/core/ws.test.ts +216 -0
  26. package/dashboard/src/core/ws.ts +143 -0
  27. package/dashboard/src/design/icons.test.ts +105 -0
  28. package/dashboard/src/design/icons.ts +131 -0
  29. package/dashboard/src/design/tokens.test.ts +204 -0
  30. package/dashboard/src/design/tokens.ts +160 -0
  31. package/dashboard/src/main.ts +68 -0
  32. package/dashboard/src/types/api.ts +337 -0
  33. package/dashboard/src/views/activity.ts +185 -0
  34. package/dashboard/src/views/agent-cockpit.ts +236 -0
  35. package/dashboard/src/views/agents.ts +72 -0
  36. package/dashboard/src/views/fleet-map.ts +299 -0
  37. package/dashboard/src/views/insights.ts +298 -0
  38. package/dashboard/src/views/machines.ts +162 -0
  39. package/dashboard/src/views/metrics.ts +420 -0
  40. package/dashboard/src/views/overview.ts +409 -0
  41. package/dashboard/src/views/pipeline-theater.ts +219 -0
  42. package/dashboard/src/views/pipelines.ts +595 -0
  43. package/dashboard/src/views/team.ts +362 -0
  44. package/dashboard/src/views/timeline.ts +389 -0
  45. package/dashboard/tsconfig.json +21 -0
  46. package/dashboard/vitest.config.ts +27 -0
  47. package/docs/AGI-WHATS-NEXT.md +15 -15
  48. package/package.json +16 -2
  49. package/scripts/lib/helpers.sh +30 -0
  50. package/scripts/lib/pipeline-quality-checks.sh +1 -1
  51. package/scripts/lib/pipeline-stages.sh +59 -0
  52. package/scripts/sw +86 -167
  53. package/scripts/sw-activity.sh +1 -1
  54. package/scripts/sw-adaptive.sh +1 -1
  55. package/scripts/sw-adversarial.sh +1 -1
  56. package/scripts/sw-architecture-enforcer.sh +1 -1
  57. package/scripts/sw-auth.sh +14 -6
  58. package/scripts/sw-autonomous.sh +230 -13
  59. package/scripts/sw-changelog.sh +2 -2
  60. package/scripts/sw-checkpoint.sh +1 -1
  61. package/scripts/sw-ci.sh +1 -1
  62. package/scripts/sw-cleanup.sh +1 -1
  63. package/scripts/sw-code-review.sh +1 -1
  64. package/scripts/sw-connect.sh +1 -1
  65. package/scripts/sw-context.sh +1 -1
  66. package/scripts/sw-cost.sh +1 -1
  67. package/scripts/sw-daemon.sh +2 -2
  68. package/scripts/sw-dashboard.sh +1 -1
  69. package/scripts/sw-db.sh +1 -1
  70. package/scripts/sw-decompose.sh +1 -1
  71. package/scripts/sw-deps.sh +1 -1
  72. package/scripts/sw-developer-simulation.sh +1 -1
  73. package/scripts/sw-discovery.sh +1 -1
  74. package/scripts/sw-doc-fleet.sh +1 -1
  75. package/scripts/sw-docs-agent.sh +1 -1
  76. package/scripts/sw-docs.sh +1 -1
  77. package/scripts/sw-doctor.sh +8 -1
  78. package/scripts/sw-dora.sh +1 -1
  79. package/scripts/sw-durable.sh +1 -1
  80. package/scripts/sw-e2e-orchestrator.sh +1 -1
  81. package/scripts/sw-eventbus.sh +1 -1
  82. package/scripts/sw-feedback.sh +1 -1
  83. package/scripts/sw-fix.sh +6 -5
  84. package/scripts/sw-fleet-discover.sh +1 -1
  85. package/scripts/sw-fleet-viz.sh +1 -1
  86. package/scripts/sw-fleet.sh +1 -1
  87. package/scripts/sw-github-app.sh +5 -2
  88. package/scripts/sw-github-checks.sh +1 -1
  89. package/scripts/sw-github-deploy.sh +1 -1
  90. package/scripts/sw-github-graphql.sh +1 -1
  91. package/scripts/sw-guild.sh +1 -1
  92. package/scripts/sw-heartbeat.sh +1 -1
  93. package/scripts/sw-hygiene.sh +1 -1
  94. package/scripts/sw-incident.sh +1 -1
  95. package/scripts/sw-init.sh +112 -9
  96. package/scripts/sw-instrument.sh +6 -1
  97. package/scripts/sw-intelligence.sh +5 -1
  98. package/scripts/sw-jira.sh +1 -1
  99. package/scripts/sw-launchd.sh +1 -1
  100. package/scripts/sw-linear.sh +20 -9
  101. package/scripts/sw-logs.sh +1 -1
  102. package/scripts/sw-loop.sh +2 -1
  103. package/scripts/sw-memory.sh +10 -1
  104. package/scripts/sw-mission-control.sh +1 -1
  105. package/scripts/sw-model-router.sh +4 -1
  106. package/scripts/sw-otel.sh +1 -1
  107. package/scripts/sw-oversight.sh +1 -1
  108. package/scripts/sw-pipeline-composer.sh +3 -1
  109. package/scripts/sw-pipeline-vitals.sh +4 -6
  110. package/scripts/sw-pipeline.sh +4 -1
  111. package/scripts/sw-pm.sh +5 -2
  112. package/scripts/sw-pr-lifecycle.sh +1 -1
  113. package/scripts/sw-predictive.sh +4 -1
  114. package/scripts/sw-prep.sh +3 -2
  115. package/scripts/sw-ps.sh +1 -1
  116. package/scripts/sw-public-dashboard.sh +10 -4
  117. package/scripts/sw-quality.sh +1 -1
  118. package/scripts/sw-reaper.sh +1 -1
  119. package/scripts/sw-recruit.sh +16 -0
  120. package/scripts/sw-regression.sh +2 -1
  121. package/scripts/sw-release-manager.sh +1 -1
  122. package/scripts/sw-release.sh +7 -5
  123. package/scripts/sw-remote.sh +1 -1
  124. package/scripts/sw-replay.sh +1 -1
  125. package/scripts/sw-retro.sh +4 -1
  126. package/scripts/sw-scale.sh +4 -1
  127. package/scripts/sw-security-audit.sh +1 -1
  128. package/scripts/sw-self-optimize.sh +113 -1
  129. package/scripts/sw-session.sh +1 -1
  130. package/scripts/sw-setup.sh +1 -1
  131. package/scripts/sw-standup.sh +2 -1
  132. package/scripts/sw-status.sh +1 -1
  133. package/scripts/sw-strategic.sh +2 -1
  134. package/scripts/sw-stream.sh +1 -1
  135. package/scripts/sw-swarm.sh +6 -1
  136. package/scripts/sw-team-stages.sh +1 -1
  137. package/scripts/sw-templates.sh +1 -1
  138. package/scripts/sw-testgen.sh +3 -2
  139. package/scripts/sw-tmux-pipeline.sh +2 -1
  140. package/scripts/sw-tmux.sh +1 -1
  141. package/scripts/sw-trace.sh +1 -1
  142. package/scripts/sw-tracker-jira.sh +1 -0
  143. package/scripts/sw-tracker-linear.sh +1 -0
  144. package/scripts/sw-tracker.sh +1 -1
  145. package/scripts/sw-triage.sh +198 -11
  146. package/scripts/sw-upgrade.sh +1 -1
  147. package/scripts/sw-ux.sh +1 -1
  148. package/scripts/sw-webhook.sh +1 -1
  149. package/scripts/sw-widgets.sh +2 -2
  150. package/scripts/sw-worktree.sh +1 -1
  151. package/dashboard/public/app.js +0 -4422
@@ -0,0 +1,185 @@
1
+ // Activity tab - filterable event feed with pagination
2
+
3
+ import { store } from "../core/state";
4
+ import {
5
+ escapeHtml,
6
+ formatDuration,
7
+ formatTime,
8
+ getBadgeClass,
9
+ getTypeShort,
10
+ } from "../core/helpers";
11
+ import { icon } from "../design/icons";
12
+ import * as api from "../core/api";
13
+ import type { FleetState, View } from "../types/api";
14
+
15
+ function setupActivityFilters(): void {
16
+ const chips = document.querySelectorAll("#activity-filters .filter-chip");
17
+ chips.forEach((chip) => {
18
+ chip.addEventListener("click", () => {
19
+ store.set("activityFilter", chip.getAttribute("data-filter") || "all");
20
+ const siblings = document.querySelectorAll(
21
+ "#activity-filters .filter-chip",
22
+ );
23
+ siblings.forEach((s) => s.classList.remove("active"));
24
+ chip.classList.add("active");
25
+ renderActivityTimeline();
26
+ });
27
+ });
28
+
29
+ const issueFilter = document.getElementById(
30
+ "activity-issue-filter",
31
+ ) as HTMLInputElement;
32
+ if (issueFilter) {
33
+ issueFilter.addEventListener("input", () => {
34
+ store.set(
35
+ "activityIssueFilter",
36
+ issueFilter.value.replace(/[^0-9]/g, ""),
37
+ );
38
+ renderActivityTimeline();
39
+ });
40
+ }
41
+
42
+ const loadMoreBtn = document.getElementById("load-more-btn");
43
+ if (loadMoreBtn) loadMoreBtn.addEventListener("click", loadMoreActivity);
44
+ }
45
+
46
+ function loadActivity(): void {
47
+ store.update({ activityOffset: 0, activityEvents: [] });
48
+
49
+ api
50
+ .fetchActivity({ limit: 50, offset: 0 })
51
+ .then((result) => {
52
+ store.update({
53
+ activityEvents: result.events || [],
54
+ activityHasMore: result.hasMore || false,
55
+ activityOffset: (result.events || []).length,
56
+ });
57
+ renderActivityTimeline();
58
+ })
59
+ .catch((err) => {
60
+ const el = document.getElementById("activity-timeline");
61
+ if (el)
62
+ el.innerHTML = `<div class="empty-state"><p>Failed to load: ${escapeHtml(String(err))}</p></div>`;
63
+ });
64
+ }
65
+
66
+ function loadMoreActivity(): void {
67
+ const btn = document.getElementById("load-more-btn") as HTMLButtonElement;
68
+ if (btn) {
69
+ btn.disabled = true;
70
+ btn.textContent = "Loading...";
71
+ }
72
+
73
+ const offset = store.get("activityOffset");
74
+ api
75
+ .fetchActivity({ limit: 50, offset })
76
+ .then((result) => {
77
+ const existing = store.get("activityEvents");
78
+ const newEvents = result.events || [];
79
+ store.update({
80
+ activityEvents: [...existing, ...newEvents],
81
+ activityHasMore: result.hasMore || false,
82
+ activityOffset: existing.length + newEvents.length,
83
+ });
84
+ renderActivityTimeline();
85
+ if (btn) {
86
+ btn.disabled = false;
87
+ btn.textContent = "Load more";
88
+ }
89
+ })
90
+ .catch(() => {
91
+ if (btn) {
92
+ btn.disabled = false;
93
+ btn.textContent = "Load more";
94
+ }
95
+ });
96
+ }
97
+
98
+ function renderActivityTimeline(): void {
99
+ const container = document.getElementById("activity-timeline");
100
+ const loadMoreWrap = document.getElementById("activity-load-more");
101
+ if (!container) return;
102
+
103
+ const activityEvents = store.get("activityEvents");
104
+ const activityFilter = store.get("activityFilter");
105
+ const activityIssueFilter = store.get("activityIssueFilter");
106
+ const activityHasMore = store.get("activityHasMore");
107
+
108
+ const filtered = activityEvents.filter((ev) => {
109
+ const typeRaw = String(ev.type || "");
110
+ const badge = getBadgeClass(typeRaw);
111
+ if (
112
+ activityFilter !== "all" &&
113
+ badge !== activityFilter &&
114
+ !typeRaw.includes(activityFilter)
115
+ )
116
+ return false;
117
+ if (activityIssueFilter && String(ev.issue || "") !== activityIssueFilter)
118
+ return false;
119
+ return true;
120
+ });
121
+
122
+ if (filtered.length === 0) {
123
+ container.innerHTML =
124
+ '<div class="empty-state"><p>No matching events</p></div>';
125
+ if (loadMoreWrap)
126
+ loadMoreWrap.style.display = activityHasMore ? "" : "none";
127
+ return;
128
+ }
129
+
130
+ let html = "";
131
+ for (const ev of filtered) {
132
+ const typeRaw = String(ev.type || "unknown");
133
+ const typeShort = getTypeShort(typeRaw);
134
+ const badgeClass = getBadgeClass(typeRaw);
135
+
136
+ let detail = "";
137
+ if (ev.stage) detail += "stage=" + ev.stage + " ";
138
+ if (ev.issueTitle) detail += ev.issueTitle;
139
+ else if (ev.title) detail += ev.title;
140
+ detail = detail.trim();
141
+
142
+ if (!detail) {
143
+ const skip: Record<string, boolean> = {
144
+ ts: true,
145
+ type: true,
146
+ timestamp: true,
147
+ issue: true,
148
+ stage: true,
149
+ duration_s: true,
150
+ issueTitle: true,
151
+ title: true,
152
+ };
153
+ const dparts: string[] = [];
154
+ for (const [key, val] of Object.entries(ev)) {
155
+ if (!skip[key]) dparts.push(key + "=" + val);
156
+ }
157
+ detail = dparts.join(" ");
158
+ }
159
+
160
+ html +=
161
+ `<div class="timeline-row">` +
162
+ `<span class="timeline-ts">${formatTime(String(ev.ts || ev.timestamp || ""))}</span>` +
163
+ `<span class="activity-badge ${badgeClass}">${escapeHtml(typeShort)}</span>` +
164
+ (ev.issue ? `<span class="timeline-issue">#${ev.issue}</span>` : "") +
165
+ `<span class="timeline-detail">${escapeHtml(detail)}</span>` +
166
+ (ev.duration_s != null
167
+ ? `<span class="timeline-duration">${formatDuration(Number(ev.duration_s))}</span>`
168
+ : "") +
169
+ `</div>`;
170
+ }
171
+
172
+ container.innerHTML = html;
173
+ if (loadMoreWrap) loadMoreWrap.style.display = activityHasMore ? "" : "none";
174
+ }
175
+
176
+ export const activityView: View = {
177
+ init() {
178
+ setupActivityFilters();
179
+ if (store.get("activityEvents").length === 0) loadActivity();
180
+ },
181
+ render(_data: FleetState) {
182
+ renderActivityTimeline();
183
+ },
184
+ destroy() {},
185
+ };
@@ -0,0 +1,236 @@
1
+ // Agent Cockpit - full-screen agent view with live terminal, CPU/memory sparklines, self-healing ring
2
+
3
+ import { store } from "../core/state";
4
+ import { escapeHtml, formatDuration, fmtNum } from "../core/helpers";
5
+ import { icon } from "../design/icons";
6
+ import { colors } from "../design/tokens";
7
+ import { renderSparkline } from "../components/charts/sparkline";
8
+ import { renderSVGDonut } from "../components/charts/donut";
9
+ import { LiveTerminal } from "../components/terminal";
10
+ import { SSEClient } from "../core/sse";
11
+ import * as api from "../core/api";
12
+ import type { FleetState, View, PipelineInfo } from "../types/api";
13
+
14
+ let terminal: LiveTerminal | null = null;
15
+ let sseClient: SSEClient | null = null;
16
+ let selectedAgent: number | null = null;
17
+ let cpuHistory: number[] = [];
18
+ let memHistory: number[] = [];
19
+
20
+ function renderCockpit(data: FleetState): void {
21
+ const container = document.getElementById("panel-agent-cockpit");
22
+ if (!container) return;
23
+
24
+ const pipelines = data.pipelines || [];
25
+ if (pipelines.length === 0 && !selectedAgent) {
26
+ container.innerHTML = `<div class="empty-state">${icon("cpu", 48)}<p>No active agents</p></div>`;
27
+ return;
28
+ }
29
+
30
+ // Agent selector bar
31
+ let html = '<div class="cockpit-layout">';
32
+ html += '<div class="cockpit-agent-bar">';
33
+ for (const p of pipelines) {
34
+ const isSelected = selectedAgent === p.issue;
35
+ const statusDot = p.status === "failed" ? "offline" : "online";
36
+ html +=
37
+ `<button class="cockpit-agent-btn${isSelected ? " selected" : ""}" data-issue="${p.issue}">` +
38
+ `<span class="presence-dot ${statusDot}"></span>#${p.issue}</button>`;
39
+ }
40
+ html += "</div>";
41
+
42
+ if (selectedAgent) {
43
+ const pipeline = pipelines.find((p) => p.issue === selectedAgent);
44
+ if (pipeline) {
45
+ html += '<div class="cockpit-main">';
46
+
47
+ // Top metrics row
48
+ html += '<div class="cockpit-metrics">';
49
+
50
+ // CPU sparkline
51
+ html += '<div class="cockpit-metric-card">';
52
+ html += `<div class="cockpit-metric-header">${icon("cpu", 16)} CPU</div>`;
53
+ html += `<div class="cockpit-metric-chart">${cpuHistory.length > 1 ? renderSparkline(cpuHistory, colors.accent.cyan, 160, 40) : '<span class="text-muted">\u2014</span>'}</div>`;
54
+ html += "</div>";
55
+
56
+ // Memory sparkline
57
+ html += '<div class="cockpit-metric-card">';
58
+ html += `<div class="cockpit-metric-header">${icon("memory-stick", 16)} Memory</div>`;
59
+ html += `<div class="cockpit-metric-chart">${memHistory.length > 1 ? renderSparkline(memHistory, colors.accent.purple, 160, 40) : '<span class="text-muted">\u2014</span>'}</div>`;
60
+ html += "</div>";
61
+
62
+ // Self-healing ring
63
+ const healthPct =
64
+ pipeline.status === "failed"
65
+ ? 0
66
+ : Math.min(
67
+ 100,
68
+ ((pipeline.iteration || 0) / (pipeline.maxIterations || 20)) *
69
+ 100,
70
+ );
71
+ html += '<div class="cockpit-metric-card">';
72
+ html += `<div class="cockpit-metric-header">${icon("shield-alert", 16)} Health</div>`;
73
+ html += `<div class="cockpit-metric-chart">${renderSVGDonut(100 - healthPct)}</div>`;
74
+ html += "</div>";
75
+
76
+ // Stage + status
77
+ html += '<div class="cockpit-metric-card">';
78
+ html += `<div class="cockpit-metric-header">${icon("activity", 16)} Status</div>`;
79
+ html += '<div class="cockpit-status-info">';
80
+ html += `<div>Stage: <strong>${escapeHtml(pipeline.stage)}</strong></div>`;
81
+ html += `<div>Iteration: ${pipeline.iteration || 0}/${pipeline.maxIterations || 20}</div>`;
82
+ html += `<div>Elapsed: ${formatDuration(pipeline.elapsed_s)}</div>`;
83
+ if (pipeline.linesWritten != null)
84
+ html += `<div>Lines: ${fmtNum(pipeline.linesWritten)}</div>`;
85
+ html += "</div></div>";
86
+
87
+ html += "</div>"; // cockpit-metrics
88
+
89
+ // Live Changes panel
90
+ html += '<div class="cockpit-changes" id="cockpit-changes">';
91
+ html += `<div class="cockpit-changes-header">${icon("file-diff", 16)} Files Changed <button class="btn-sm" id="cockpit-refresh-diff">Refresh</button></div>`;
92
+ html +=
93
+ '<div class="cockpit-changes-body" id="cockpit-changes-body"></div>';
94
+ html += "</div>";
95
+
96
+ // Live terminal
97
+ html +=
98
+ '<div class="cockpit-terminal" id="cockpit-terminal-container"></div>';
99
+
100
+ html += "</div>"; // cockpit-main
101
+ } else {
102
+ html += `<div class="empty-state"><p>Agent #${selectedAgent} no longer active</p></div>`;
103
+ }
104
+ } else {
105
+ html += `<div class="empty-state">${icon("terminal", 32)}<p>Select an agent to monitor</p></div>`;
106
+ }
107
+
108
+ html += "</div>";
109
+ container.innerHTML = html;
110
+
111
+ // Wire up agent selector
112
+ container.querySelectorAll(".cockpit-agent-btn").forEach((btn) => {
113
+ btn.addEventListener("click", () => {
114
+ const issue = parseInt(btn.getAttribute("data-issue") || "0", 10);
115
+ if (issue) selectAgent(issue, data);
116
+ });
117
+ });
118
+
119
+ // Initialize terminal
120
+ if (selectedAgent) {
121
+ const termContainer = document.getElementById("cockpit-terminal-container");
122
+ if (termContainer) {
123
+ terminal = new LiveTerminal(termContainer);
124
+ connectAgentStream(selectedAgent);
125
+ }
126
+
127
+ // Load live changes
128
+ loadCockpitChanges(selectedAgent);
129
+ const refreshBtn = document.getElementById("cockpit-refresh-diff");
130
+ if (refreshBtn) {
131
+ refreshBtn.addEventListener("click", () => {
132
+ if (selectedAgent) loadCockpitChanges(selectedAgent);
133
+ });
134
+ }
135
+
136
+ // Update resource histories from agent heartbeat data
137
+ const agents = data.agents || [];
138
+ const agentInfo = agents.find((a) => a.issue === selectedAgent);
139
+ if (agentInfo) {
140
+ if (agentInfo.cpu_pct != null) {
141
+ cpuHistory.push(agentInfo.cpu_pct);
142
+ if (cpuHistory.length > 60) cpuHistory.shift();
143
+ }
144
+ if (agentInfo.memory_mb != null) {
145
+ memHistory.push(agentInfo.memory_mb);
146
+ if (memHistory.length > 60) memHistory.shift();
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ function selectAgent(issue: number, data: FleetState): void {
153
+ if (sseClient) sseClient.close();
154
+ if (terminal) terminal.destroy();
155
+ selectedAgent = issue;
156
+ cpuHistory = [];
157
+ memHistory = [];
158
+ renderCockpit(data);
159
+ }
160
+
161
+ function loadCockpitChanges(issue: number): void {
162
+ const body = document.getElementById("cockpit-changes-body");
163
+ if (!body) return;
164
+ body.innerHTML = '<div class="empty-state"><p>Loading...</p></div>';
165
+
166
+ api
167
+ .fetchPipelineFiles(issue)
168
+ .then((data) => {
169
+ const files = data.files || [];
170
+ if (files.length === 0) {
171
+ body.innerHTML = '<div class="empty-state"><p>No changes yet</p></div>';
172
+ return;
173
+ }
174
+ let html = "";
175
+ for (const f of files) {
176
+ const statusCls =
177
+ f.status === "added"
178
+ ? "file-added"
179
+ : f.status === "deleted"
180
+ ? "file-deleted"
181
+ : "file-modified";
182
+ const statusChar =
183
+ f.status === "added" ? "A" : f.status === "deleted" ? "D" : "M";
184
+ html += `<div class="changes-file-item ${statusCls}"><span class="file-status">${statusChar}</span><span class="file-path">${escapeHtml(f.path)}</span></div>`;
185
+ }
186
+ body.innerHTML = html;
187
+ })
188
+ .catch(() => {
189
+ body.innerHTML =
190
+ '<div class="empty-state"><p>Could not load changes</p></div>';
191
+ });
192
+ }
193
+
194
+ function connectAgentStream(issue: number): void {
195
+ if (sseClient) sseClient.close();
196
+
197
+ sseClient = new SSEClient(
198
+ `/api/logs/${issue}/stream`,
199
+ (data) => {
200
+ if (terminal) terminal.append(data);
201
+ },
202
+ () => {
203
+ api
204
+ .fetchLogs(issue)
205
+ .then((data) => {
206
+ if (terminal) terminal.append(data.content || "No logs available");
207
+ })
208
+ .catch(() => {
209
+ if (terminal) terminal.append("Failed to load logs");
210
+ });
211
+ },
212
+ );
213
+ sseClient.connect();
214
+ }
215
+
216
+ export const agentCockpitView: View = {
217
+ init() {},
218
+
219
+ render(data: FleetState) {
220
+ renderCockpit(data);
221
+ },
222
+
223
+ destroy() {
224
+ if (sseClient) {
225
+ sseClient.close();
226
+ sseClient = null;
227
+ }
228
+ if (terminal) {
229
+ terminal.destroy();
230
+ terminal = null;
231
+ }
232
+ selectedAgent = null;
233
+ cpuHistory = [];
234
+ memHistory = [];
235
+ },
236
+ };
@@ -0,0 +1,72 @@
1
+ // Agents tab - agent cards grid with intervention controls
2
+
3
+ import { escapeHtml, formatDuration } from "../core/helpers";
4
+ import { icon } from "../design/icons";
5
+ import { openInterventionModal, confirmAbort } from "../components/modal";
6
+ import * as api from "../core/api";
7
+ import type { FleetState, View, PipelineInfo } from "../types/api";
8
+
9
+ function renderAgentsTab(data: FleetState): void {
10
+ const container = document.getElementById("agents-grid");
11
+ if (!container) return;
12
+
13
+ const pipelines = data.pipelines || [];
14
+ if (pipelines.length === 0) {
15
+ container.innerHTML = `<div class="empty-state">${icon("users", 32)}<p>No active agents</p></div>`;
16
+ return;
17
+ }
18
+
19
+ let html = "";
20
+ for (const p of pipelines) {
21
+ const statusClass = p.status === "failed" ? "agent-failed" : "agent-active";
22
+ html +=
23
+ `<div class="agent-card ${statusClass}">` +
24
+ `<div class="agent-card-header">` +
25
+ `<span class="agent-issue">#${p.issue}</span>` +
26
+ `<span class="agent-title">${escapeHtml(p.title)}</span></div>` +
27
+ `<div class="agent-card-body">` +
28
+ `<div class="agent-info-row"><span class="agent-info-label">${icon("activity", 14)} Stage</span>` +
29
+ `<span class="agent-info-value">${escapeHtml(p.stage)}</span></div>` +
30
+ `<div class="agent-info-row"><span class="agent-info-label">${icon("timer", 14)} Elapsed</span>` +
31
+ `<span class="agent-info-value">${formatDuration(p.elapsed_s)}</span></div>` +
32
+ `<div class="agent-info-row"><span class="agent-info-label">${icon("refresh-cw", 14)} Iteration</span>` +
33
+ `<span class="agent-info-value">${p.iteration || 0}/${p.maxIterations || 20}</span></div>` +
34
+ `</div>` +
35
+ `<div class="agent-card-actions">` +
36
+ `<button class="agent-action-btn" data-action="message" data-issue="${p.issue}">${icon("message-square", 14)} Message</button>` +
37
+ `<button class="agent-action-btn" data-action="pause" data-issue="${p.issue}">${icon("pause", 14)} Pause</button>` +
38
+ `<button class="agent-action-btn danger" data-action="abort" data-issue="${p.issue}">${icon("square", 14)} Abort</button>` +
39
+ `</div></div>`;
40
+ }
41
+ container.innerHTML = html;
42
+
43
+ // Wire up action buttons
44
+ container.querySelectorAll(".agent-action-btn").forEach((btn) => {
45
+ btn.addEventListener("click", (e) => {
46
+ e.stopPropagation();
47
+ const action = btn.getAttribute("data-action");
48
+ const issue = parseInt(btn.getAttribute("data-issue") || "0", 10);
49
+ if (!issue) return;
50
+
51
+ switch (action) {
52
+ case "message":
53
+ openInterventionModal(issue);
54
+ break;
55
+ case "pause":
56
+ api.sendIntervention(issue, "pause");
57
+ break;
58
+ case "abort":
59
+ confirmAbort(issue);
60
+ break;
61
+ }
62
+ });
63
+ });
64
+ }
65
+
66
+ export const agentsView: View = {
67
+ init() {},
68
+ render(data: FleetState) {
69
+ renderAgentsTab(data);
70
+ },
71
+ destroy() {},
72
+ };