shipwright-cli 2.2.1 → 2.3.0
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 +19 -19
- package/dashboard/public/index.html +224 -8
- package/dashboard/public/styles.css +1078 -4
- package/dashboard/server.ts +1100 -15
- package/dashboard/src/canvas/interactions.ts +74 -0
- package/dashboard/src/canvas/layout.ts +85 -0
- package/dashboard/src/canvas/overlays.ts +117 -0
- package/dashboard/src/canvas/particles.ts +105 -0
- package/dashboard/src/canvas/renderer.ts +191 -0
- package/dashboard/src/components/charts/bar.ts +54 -0
- package/dashboard/src/components/charts/donut.ts +25 -0
- package/dashboard/src/components/charts/pipeline-rail.ts +105 -0
- package/dashboard/src/components/charts/sparkline.ts +82 -0
- package/dashboard/src/components/header.ts +616 -0
- package/dashboard/src/components/modal.ts +413 -0
- package/dashboard/src/components/terminal.ts +144 -0
- package/dashboard/src/core/api.ts +381 -0
- package/dashboard/src/core/helpers.ts +118 -0
- package/dashboard/src/core/router.ts +190 -0
- package/dashboard/src/core/sse.ts +38 -0
- package/dashboard/src/core/state.ts +150 -0
- package/dashboard/src/core/ws.ts +143 -0
- package/dashboard/src/design/icons.ts +131 -0
- package/dashboard/src/design/tokens.ts +160 -0
- package/dashboard/src/main.ts +68 -0
- package/dashboard/src/types/api.ts +337 -0
- package/dashboard/src/views/activity.ts +185 -0
- package/dashboard/src/views/agent-cockpit.ts +236 -0
- package/dashboard/src/views/agents.ts +72 -0
- package/dashboard/src/views/fleet-map.ts +299 -0
- package/dashboard/src/views/insights.ts +298 -0
- package/dashboard/src/views/machines.ts +162 -0
- package/dashboard/src/views/metrics.ts +420 -0
- package/dashboard/src/views/overview.ts +409 -0
- package/dashboard/src/views/pipeline-theater.ts +219 -0
- package/dashboard/src/views/pipelines.ts +595 -0
- package/dashboard/src/views/team.ts +362 -0
- package/dashboard/src/views/timeline.ts +389 -0
- package/dashboard/tsconfig.json +21 -0
- package/docs/AGI-PLATFORM-PLAN.md +5 -5
- package/docs/AGI-WHATS-NEXT.md +19 -16
- package/docs/README.md +2 -0
- package/package.json +8 -1
- package/scripts/check-version-consistency.sh +72 -0
- package/scripts/lib/daemon-adaptive.sh +610 -0
- package/scripts/lib/daemon-dispatch.sh +489 -0
- package/scripts/lib/daemon-failure.sh +387 -0
- package/scripts/lib/daemon-patrol.sh +1113 -0
- package/scripts/lib/daemon-poll.sh +1202 -0
- package/scripts/lib/daemon-state.sh +550 -0
- package/scripts/lib/daemon-triage.sh +490 -0
- package/scripts/lib/helpers.sh +81 -0
- package/scripts/lib/pipeline-intelligence.sh +0 -6
- package/scripts/lib/pipeline-quality-checks.sh +3 -1
- package/scripts/lib/pipeline-stages.sh +20 -0
- package/scripts/sw +109 -168
- package/scripts/sw-activity.sh +1 -1
- package/scripts/sw-adaptive.sh +2 -2
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +14 -6
- package/scripts/sw-autonomous.sh +1 -1
- package/scripts/sw-changelog.sh +2 -2
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +1 -1
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +1 -1
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +1 -1
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +53 -4817
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +1 -1
- package/scripts/sw-decompose.sh +1 -1
- package/scripts/sw-deps.sh +1 -1
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +1 -1
- package/scripts/sw-doc-fleet.sh +1 -1
- package/scripts/sw-docs-agent.sh +1 -1
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +49 -1
- package/scripts/sw-dora.sh +1 -1
- package/scripts/sw-durable.sh +1 -1
- package/scripts/sw-e2e-orchestrator.sh +1 -1
- package/scripts/sw-eventbus.sh +1 -1
- package/scripts/sw-feedback.sh +1 -1
- package/scripts/sw-fix.sh +6 -5
- package/scripts/sw-fleet-discover.sh +1 -1
- package/scripts/sw-fleet-viz.sh +3 -3
- package/scripts/sw-fleet.sh +1 -1
- package/scripts/sw-github-app.sh +5 -2
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +1 -1
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +1 -1
- package/scripts/sw-incident.sh +1 -1
- package/scripts/sw-init.sh +112 -9
- package/scripts/sw-instrument.sh +6 -1
- package/scripts/sw-intelligence.sh +5 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +1 -1
- package/scripts/sw-linear.sh +20 -9
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +2 -1
- package/scripts/sw-memory.sh +10 -1
- package/scripts/sw-mission-control.sh +1 -1
- package/scripts/sw-model-router.sh +4 -1
- package/scripts/sw-otel.sh +4 -4
- package/scripts/sw-oversight.sh +1 -1
- package/scripts/sw-pipeline-composer.sh +3 -1
- package/scripts/sw-pipeline-vitals.sh +4 -6
- package/scripts/sw-pipeline.sh +19 -56
- package/scripts/sw-pipeline.sh.mock +7 -0
- package/scripts/sw-pm.sh +5 -2
- package/scripts/sw-pr-lifecycle.sh +1 -1
- package/scripts/sw-predictive.sh +4 -1
- package/scripts/sw-prep.sh +3 -2
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +10 -4
- package/scripts/sw-quality.sh +1 -1
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +25 -1
- package/scripts/sw-regression.sh +2 -1
- package/scripts/sw-release-manager.sh +1 -1
- package/scripts/sw-release.sh +7 -5
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +1 -1
- package/scripts/sw-retro.sh +1 -1
- package/scripts/sw-scale.sh +11 -5
- package/scripts/sw-security-audit.sh +1 -1
- package/scripts/sw-self-optimize.sh +172 -7
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +4 -3
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +2 -1
- package/scripts/sw-stream.sh +8 -2
- package/scripts/sw-swarm.sh +12 -10
- package/scripts/sw-team-stages.sh +1 -1
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +3 -2
- package/scripts/sw-tmux-pipeline.sh +2 -1
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +1 -1
- package/scripts/sw-tracker-jira.sh +1 -0
- package/scripts/sw-tracker-linear.sh +1 -0
- package/scripts/sw-tracker.sh +24 -6
- package/scripts/sw-triage.sh +1 -1
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +1 -1
- package/scripts/sw-webhook.sh +1 -1
- package/scripts/sw-widgets.sh +2 -2
- package/scripts/sw-worktree.sh +1 -1
- package/dashboard/public/app.js +0 -4422
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// Fleet Map - Canvas2D topology with stage columns, animated pipeline particles
|
|
2
|
+
|
|
3
|
+
import { store } from "../core/state";
|
|
4
|
+
import { colors, STAGES, STAGE_HEX, STAGE_SHORT } from "../design/tokens";
|
|
5
|
+
import { formatDuration } from "../core/helpers";
|
|
6
|
+
import {
|
|
7
|
+
CanvasRenderer,
|
|
8
|
+
drawText,
|
|
9
|
+
drawCircle,
|
|
10
|
+
drawRoundRect,
|
|
11
|
+
type CanvasScene,
|
|
12
|
+
} from "../canvas/renderer";
|
|
13
|
+
import {
|
|
14
|
+
computeLayout,
|
|
15
|
+
type LayoutNode,
|
|
16
|
+
type StageColumn,
|
|
17
|
+
} from "../canvas/layout";
|
|
18
|
+
import { ParticleSystem } from "../canvas/particles";
|
|
19
|
+
import { hitTestNode, ZoomPan } from "../canvas/interactions";
|
|
20
|
+
import { drawTooltip, drawStageLabel } from "../canvas/overlays";
|
|
21
|
+
import * as api from "../core/api";
|
|
22
|
+
import type { FleetState, View } from "../types/api";
|
|
23
|
+
|
|
24
|
+
let renderer: CanvasRenderer | null = null;
|
|
25
|
+
let scene: FleetMapScene | null = null;
|
|
26
|
+
|
|
27
|
+
class FleetMapScene implements CanvasScene {
|
|
28
|
+
nodes: LayoutNode[] = [];
|
|
29
|
+
columns: StageColumn[] = [];
|
|
30
|
+
particles = new ParticleSystem();
|
|
31
|
+
zoomPan = new ZoomPan();
|
|
32
|
+
hoveredNode: LayoutNode | null = null;
|
|
33
|
+
predictions: Record<
|
|
34
|
+
number,
|
|
35
|
+
{ eta_s?: number; success_probability?: number; estimated_cost?: number }
|
|
36
|
+
> = {};
|
|
37
|
+
time = 0;
|
|
38
|
+
width = 0;
|
|
39
|
+
height = 0;
|
|
40
|
+
|
|
41
|
+
updateData(data: FleetState): void {
|
|
42
|
+
if (!data.pipelines) return;
|
|
43
|
+
const { nodes, columns } = computeLayout(
|
|
44
|
+
data.pipelines,
|
|
45
|
+
this.width,
|
|
46
|
+
this.height,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Smooth transition: find matching nodes and lerp
|
|
50
|
+
for (const newNode of nodes) {
|
|
51
|
+
const existing = this.nodes.find((n) => n.issue === newNode.issue);
|
|
52
|
+
if (existing) {
|
|
53
|
+
newNode.x = existing.x;
|
|
54
|
+
newNode.y = existing.y;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.nodes = nodes;
|
|
59
|
+
this.columns = columns;
|
|
60
|
+
|
|
61
|
+
// Fetch predictions for active pipelines
|
|
62
|
+
for (const p of data.pipelines) {
|
|
63
|
+
if (!this.predictions[p.issue]) {
|
|
64
|
+
api.fetchPredictions(p.issue).then((pred) => {
|
|
65
|
+
this.predictions[p.issue] = pred;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
update(dt: number): void {
|
|
72
|
+
this.time += dt;
|
|
73
|
+
|
|
74
|
+
// Smooth node movement
|
|
75
|
+
for (const node of this.nodes) {
|
|
76
|
+
const dx = node.targetX - node.x;
|
|
77
|
+
const dy = node.targetY - node.y;
|
|
78
|
+
node.x += dx * 5 * dt;
|
|
79
|
+
node.y += dy * 5 * dt;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Emit trail particles for active nodes
|
|
83
|
+
for (const node of this.nodes) {
|
|
84
|
+
if (node.status === "active" || node.status === "running") {
|
|
85
|
+
if (Math.random() < 0.3) {
|
|
86
|
+
this.particles.emit(
|
|
87
|
+
node.x + (Math.random() - 0.5) * node.radius,
|
|
88
|
+
node.y + (Math.random() - 0.5) * node.radius,
|
|
89
|
+
"trail",
|
|
90
|
+
node.color,
|
|
91
|
+
1,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Ambient particles along columns
|
|
98
|
+
if (Math.random() < 0.05) {
|
|
99
|
+
const col = this.columns[Math.floor(Math.random() * this.columns.length)];
|
|
100
|
+
if (col) {
|
|
101
|
+
this.particles.emit(
|
|
102
|
+
col.x + Math.random() * col.width,
|
|
103
|
+
Math.random() * this.height,
|
|
104
|
+
"ambient",
|
|
105
|
+
col.color,
|
|
106
|
+
1,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.particles.update(dt);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
115
|
+
// Background
|
|
116
|
+
ctx.fillStyle = colors.bg.abyss;
|
|
117
|
+
ctx.fillRect(0, 0, width, height);
|
|
118
|
+
|
|
119
|
+
ctx.save();
|
|
120
|
+
this.zoomPan.apply(ctx);
|
|
121
|
+
|
|
122
|
+
// Stage columns
|
|
123
|
+
for (const col of this.columns) {
|
|
124
|
+
// Column separator
|
|
125
|
+
ctx.fillStyle = colors.bg.deep;
|
|
126
|
+
ctx.fillRect(col.x, 0, col.width, height);
|
|
127
|
+
|
|
128
|
+
// Column border
|
|
129
|
+
ctx.strokeStyle = colors.bg.foam + "40";
|
|
130
|
+
ctx.lineWidth = 1;
|
|
131
|
+
ctx.beginPath();
|
|
132
|
+
ctx.moveTo(col.x, 0);
|
|
133
|
+
ctx.lineTo(col.x, height);
|
|
134
|
+
ctx.stroke();
|
|
135
|
+
|
|
136
|
+
// Stage label at top
|
|
137
|
+
drawStageLabel(ctx, col.name, col.x + col.width / 2, 20, col.color);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Connection lines between pipeline stages
|
|
141
|
+
for (const node of this.nodes) {
|
|
142
|
+
if (node.stageIndex > 0) {
|
|
143
|
+
const prevCol = this.columns[node.stageIndex - 1];
|
|
144
|
+
if (prevCol) {
|
|
145
|
+
const fromX = prevCol.x + prevCol.width / 2;
|
|
146
|
+
ctx.globalAlpha = 0.15;
|
|
147
|
+
ctx.strokeStyle = node.color;
|
|
148
|
+
ctx.lineWidth = 1;
|
|
149
|
+
ctx.setLineDash([4, 4]);
|
|
150
|
+
ctx.beginPath();
|
|
151
|
+
ctx.moveTo(fromX, node.y);
|
|
152
|
+
ctx.lineTo(node.x, node.y);
|
|
153
|
+
ctx.stroke();
|
|
154
|
+
ctx.setLineDash([]);
|
|
155
|
+
ctx.globalAlpha = 1;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Particles (behind nodes)
|
|
161
|
+
this.particles.draw(ctx);
|
|
162
|
+
|
|
163
|
+
// Pipeline nodes
|
|
164
|
+
for (const node of this.nodes) {
|
|
165
|
+
const isHovered = this.hoveredNode === node;
|
|
166
|
+
const r = isHovered ? node.radius * 1.2 : node.radius;
|
|
167
|
+
|
|
168
|
+
// Glow
|
|
169
|
+
if (node.status !== "failed") {
|
|
170
|
+
ctx.globalAlpha = 0.2 + 0.1 * Math.sin(this.time * 2 + node.issue);
|
|
171
|
+
drawCircle(ctx, node.x, node.y, r + 6, undefined, node.color, 2);
|
|
172
|
+
ctx.globalAlpha = 1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Node circle
|
|
176
|
+
drawCircle(ctx, node.x, node.y, r, node.color);
|
|
177
|
+
|
|
178
|
+
// Issue number
|
|
179
|
+
drawText(ctx, "#" + node.issue, node.x, node.y - 4, {
|
|
180
|
+
font: "monoSm",
|
|
181
|
+
color: colors.bg.abyss,
|
|
182
|
+
align: "center",
|
|
183
|
+
baseline: "middle",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Progress arc
|
|
187
|
+
if (node.progress > 0 && node.progress < 1) {
|
|
188
|
+
ctx.beginPath();
|
|
189
|
+
ctx.arc(
|
|
190
|
+
node.x,
|
|
191
|
+
node.y,
|
|
192
|
+
r + 3,
|
|
193
|
+
-Math.PI / 2,
|
|
194
|
+
-Math.PI / 2 + node.progress * Math.PI * 2,
|
|
195
|
+
);
|
|
196
|
+
ctx.strokeStyle = colors.accent.cyan;
|
|
197
|
+
ctx.lineWidth = 2;
|
|
198
|
+
ctx.stroke();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
ctx.restore();
|
|
203
|
+
|
|
204
|
+
// Tooltip (drawn in screen space)
|
|
205
|
+
if (this.hoveredNode) {
|
|
206
|
+
drawTooltip(
|
|
207
|
+
ctx,
|
|
208
|
+
this.hoveredNode,
|
|
209
|
+
this.predictions[this.hoveredNode.issue],
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// HUD: pipeline count
|
|
214
|
+
drawText(
|
|
215
|
+
ctx,
|
|
216
|
+
`${this.nodes.length} active pipeline${this.nodes.length !== 1 ? "s" : ""}`,
|
|
217
|
+
16,
|
|
218
|
+
height - 30,
|
|
219
|
+
{
|
|
220
|
+
font: "caption",
|
|
221
|
+
color: colors.text.muted,
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
drawText(ctx, `${this.particles.count} particles`, 16, height - 16, {
|
|
225
|
+
font: "tiny",
|
|
226
|
+
color: colors.text.muted,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
onResize(width: number, height: number): void {
|
|
231
|
+
this.width = width;
|
|
232
|
+
this.height = height;
|
|
233
|
+
// Recompute layout
|
|
234
|
+
const data = store.get("fleetState");
|
|
235
|
+
if (data?.pipelines) {
|
|
236
|
+
const { nodes, columns } = computeLayout(data.pipelines, width, height);
|
|
237
|
+
this.nodes = nodes;
|
|
238
|
+
this.columns = columns;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
onMouseMove(x: number, y: number): void {
|
|
243
|
+
const world = this.zoomPan.screenToWorld(x, y);
|
|
244
|
+
this.hoveredNode = hitTestNode(this.nodes, world.x, world.y);
|
|
245
|
+
if (renderer) {
|
|
246
|
+
renderer.getCanvas().style.cursor = this.hoveredNode
|
|
247
|
+
? "pointer"
|
|
248
|
+
: "default";
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
onMouseClick(x: number, y: number): void {
|
|
253
|
+
const world = this.zoomPan.screenToWorld(x, y);
|
|
254
|
+
const node = hitTestNode(this.nodes, world.x, world.y);
|
|
255
|
+
if (node) {
|
|
256
|
+
this.particles.burstAt(node.x, node.y, node.color);
|
|
257
|
+
import("../core/router").then(({ switchTab }) => {
|
|
258
|
+
switchTab("pipelines");
|
|
259
|
+
import("./pipelines").then(({ fetchPipelineDetail }) => {
|
|
260
|
+
fetchPipelineDetail(node.issue);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
onMouseWheel(delta: number): void {
|
|
267
|
+
this.zoomPan.zoom(delta, this.width / 2, this.height / 2);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const fleetMapView: View = {
|
|
272
|
+
init() {
|
|
273
|
+
const container = document.getElementById("panel-fleet-map");
|
|
274
|
+
if (!container) return;
|
|
275
|
+
|
|
276
|
+
container.innerHTML =
|
|
277
|
+
'<div class="fleet-map-canvas" style="width:100%;height:calc(100vh - 160px);position:relative;"></div>';
|
|
278
|
+
const canvasContainer = container.querySelector(
|
|
279
|
+
".fleet-map-canvas",
|
|
280
|
+
) as HTMLElement;
|
|
281
|
+
|
|
282
|
+
renderer = new CanvasRenderer(canvasContainer);
|
|
283
|
+
scene = new FleetMapScene();
|
|
284
|
+
renderer.setScene(scene);
|
|
285
|
+
renderer.start();
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
render(data: FleetState) {
|
|
289
|
+
if (scene) scene.updateData(data);
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
destroy() {
|
|
293
|
+
if (renderer) {
|
|
294
|
+
renderer.destroy();
|
|
295
|
+
renderer = null;
|
|
296
|
+
}
|
|
297
|
+
scene = null;
|
|
298
|
+
},
|
|
299
|
+
};
|