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.
- package/README.md +12 -11
- 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.test.ts +362 -0
- package/dashboard/src/core/api.ts +381 -0
- package/dashboard/src/core/helpers.ts +118 -0
- package/dashboard/src/core/router.test.ts +266 -0
- package/dashboard/src/core/router.ts +190 -0
- package/dashboard/src/core/sse.ts +38 -0
- package/dashboard/src/core/state.test.ts +235 -0
- package/dashboard/src/core/state.ts +150 -0
- package/dashboard/src/core/ws.test.ts +216 -0
- package/dashboard/src/core/ws.ts +143 -0
- package/dashboard/src/design/icons.test.ts +105 -0
- package/dashboard/src/design/icons.ts +131 -0
- package/dashboard/src/design/tokens.test.ts +204 -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/dashboard/vitest.config.ts +27 -0
- package/docs/AGI-WHATS-NEXT.md +15 -15
- package/package.json +16 -2
- package/scripts/lib/helpers.sh +30 -0
- package/scripts/lib/pipeline-quality-checks.sh +1 -1
- package/scripts/lib/pipeline-stages.sh +59 -0
- package/scripts/sw +86 -167
- package/scripts/sw-activity.sh +1 -1
- package/scripts/sw-adaptive.sh +1 -1
- 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 +230 -13
- 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 +2 -2
- 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 +8 -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 +1 -1
- 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 +1 -1
- 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 +4 -1
- 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 +16 -0
- 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 +4 -1
- package/scripts/sw-scale.sh +4 -1
- package/scripts/sw-security-audit.sh +1 -1
- package/scripts/sw-self-optimize.sh +113 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +2 -1
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +2 -1
- package/scripts/sw-stream.sh +1 -1
- package/scripts/sw-swarm.sh +6 -1
- 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 +1 -1
- package/scripts/sw-triage.sh +198 -11
- 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,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
|
+
};
|