@matt82198/aesop 0.1.0-beta.3 → 0.1.0-beta.5
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/CHANGELOG.md +79 -95
- package/CLAUDE-TEMPLATE.md +115 -0
- package/README.md +126 -245
- package/aesop.config.example.json +34 -26
- package/bin/CLAUDE.md +67 -0
- package/bin/cli.js +593 -36
- package/daemons/CLAUDE.md +36 -0
- package/daemons/backup-fleet.sh +92 -21
- package/daemons/run-watchdog.sh +95 -12
- package/dash/CLAUDE.md +32 -0
- package/dash/dash-extra.mjs +194 -21
- package/dash/watchdog-gui.sh +23 -6
- package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
- package/docs/CHECKPOINTING.md +100 -0
- package/docs/DISPATCH-MODEL.md +11 -7
- package/docs/FORENSICS.md +204 -0
- package/docs/GOVERNANCE.md +4 -2
- package/docs/HOOK-INSTALL.md +307 -0
- package/docs/HOW-THE-LOOP-WORKS.md +175 -0
- package/docs/MEMORY-TEMPLATE.md +61 -0
- package/docs/README.md +44 -0
- package/docs/RELEASING.md +159 -0
- package/docs/RELIABILITY.md +61 -0
- package/docs/RESTORE.md +397 -0
- package/docs/SCRIPTS-POLICY.md +97 -0
- package/docs/archive/README.md +3 -0
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
- package/docs/archive/spikes/tiered-cognition/README.md +27 -0
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
- package/docs/case-study-portfolio.md +61 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +61 -0
- package/hooks/claude/force-model-policy.mjs +159 -0
- package/hooks/pre-push-policy.sh +753 -0
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/.signal-state.json +3 -0
- package/monitor/ACTIONS.log +1 -0
- package/monitor/BRIEF.md +24 -0
- package/monitor/CHARTER.md +75 -88
- package/monitor/CLAUDE.md +40 -0
- package/monitor/SIGNALS.json +54 -0
- package/monitor/collect-signals.mjs +613 -61
- package/package.json +15 -3
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/skills/power/SKILL.md +161 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +24 -0
- package/state_store/projections.py +52 -0
- package/state_store/store.py +102 -0
- package/tools/CLAUDE.md +72 -0
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/tools/agent-forensics.sh +186 -0
- package/tools/alert_bridge.py +449 -0
- package/tools/buildlog.py +182 -0
- package/tools/ci_merge_wait.py +317 -0
- package/tools/ensure_state.py +94 -0
- package/tools/eod_sweep.py +239 -0
- package/tools/fleet_ledger.py +272 -0
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/healthcheck.py +296 -0
- package/tools/heartbeat.py +131 -0
- package/tools/inbox_drain.py +249 -0
- package/tools/launch_tui.py +51 -3
- package/tools/lock.mjs +211 -0
- package/tools/metrics_gate.py +205 -0
- package/tools/orchestrator_status.py +99 -0
- package/tools/power_selftest.py +386 -0
- package/tools/prepublish_scan.py +84 -0
- package/tools/proposals.mjs +248 -0
- package/tools/reconstitute.sh +467 -0
- package/tools/rotate_logs.py +228 -0
- package/tools/scanner_selftest.py +303 -0
- package/tools/secret_scan.py +138 -31
- package/tools/self_stats.py +509 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/stall_check.py +172 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_dash.py +513 -0
- package/tools/verify_submit_encoding.py +202 -0
- package/ui/CLAUDE.md +125 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/cost.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/agents.py +183 -0
- package/ui/api/__init__.py +84 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/api/submit.py +97 -0
- package/ui/api/tracker.py +116 -0
- package/ui/collectors.py +735 -0
- package/ui/config.py +123 -0
- package/ui/cost.py +260 -0
- package/ui/csrf.py +144 -0
- package/ui/handler.py +676 -0
- package/ui/render.py +49 -0
- package/ui/serve.py +68 -796
- package/ui/sse.py +183 -0
- package/ui/web/.gitattributes +13 -0
- package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
- package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/ui/web/index.html +13 -0
- package/ui/web/package-lock.json +2225 -0
- package/ui/web/package.json +26 -0
- package/ui/web/src/App.test.tsx +74 -0
- package/ui/web/src/App.tsx +142 -0
- package/ui/web/src/CONTRIBUTING-UI.md +49 -0
- package/ui/web/src/components/AgentRow.css +187 -0
- package/ui/web/src/components/AgentRow.test.tsx +209 -0
- package/ui/web/src/components/AgentRow.tsx +207 -0
- package/ui/web/src/components/AgentsPanel.css +108 -0
- package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
- package/ui/web/src/components/AgentsPanel.tsx +58 -0
- package/ui/web/src/components/AlertsPanel.css +88 -0
- package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
- package/ui/web/src/components/AlertsPanel.tsx +67 -0
- package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
- package/ui/web/src/components/BacklogPanel.tsx +122 -0
- package/ui/web/src/components/CostChart.css +110 -0
- package/ui/web/src/components/CostChart.test.tsx +144 -0
- package/ui/web/src/components/CostChart.tsx +152 -0
- package/ui/web/src/components/CostTable.css +93 -0
- package/ui/web/src/components/CostTable.test.tsx +165 -0
- package/ui/web/src/components/CostTable.tsx +94 -0
- package/ui/web/src/components/EventsFeed.css +68 -0
- package/ui/web/src/components/EventsFeed.test.tsx +36 -0
- package/ui/web/src/components/EventsFeed.tsx +31 -0
- package/ui/web/src/components/HealthHeader.css +137 -0
- package/ui/web/src/components/HealthHeader.test.tsx +278 -0
- package/ui/web/src/components/HealthHeader.tsx +281 -0
- package/ui/web/src/components/InboxForm.css +135 -0
- package/ui/web/src/components/InboxForm.test.tsx +208 -0
- package/ui/web/src/components/InboxForm.tsx +116 -0
- package/ui/web/src/components/MessagesTail.module.css +144 -0
- package/ui/web/src/components/MessagesTail.test.tsx +176 -0
- package/ui/web/src/components/MessagesTail.tsx +94 -0
- package/ui/web/src/components/ReposPanel.css +90 -0
- package/ui/web/src/components/ReposPanel.test.tsx +45 -0
- package/ui/web/src/components/ReposPanel.tsx +67 -0
- package/ui/web/src/components/Scorecard.css +106 -0
- package/ui/web/src/components/Scorecard.test.tsx +117 -0
- package/ui/web/src/components/Scorecard.tsx +85 -0
- package/ui/web/src/components/Timeline.module.css +151 -0
- package/ui/web/src/components/Timeline.test.tsx +215 -0
- package/ui/web/src/components/Timeline.tsx +99 -0
- package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
- package/ui/web/src/components/TrackerBoard.tsx +107 -0
- package/ui/web/src/components/TrackerCard.test.tsx +180 -0
- package/ui/web/src/components/TrackerCard.tsx +160 -0
- package/ui/web/src/components/TrackerForm.test.tsx +189 -0
- package/ui/web/src/components/TrackerForm.tsx +144 -0
- package/ui/web/src/lib/api.ts +218 -0
- package/ui/web/src/lib/format.test.ts +89 -0
- package/ui/web/src/lib/format.ts +103 -0
- package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
- package/ui/web/src/lib/sanitizeUrl.ts +38 -0
- package/ui/web/src/lib/types.ts +230 -0
- package/ui/web/src/lib/useHashRoute.test.ts +60 -0
- package/ui/web/src/lib/useHashRoute.ts +23 -0
- package/ui/web/src/lib/useSSE.ts +175 -0
- package/ui/web/src/main.tsx +10 -0
- package/ui/web/src/styles/global.css +179 -0
- package/ui/web/src/styles/theme.css +184 -0
- package/ui/web/src/styles/work.css +572 -0
- package/ui/web/src/test/fixtures.ts +385 -0
- package/ui/web/src/test/setup.ts +49 -0
- package/ui/web/src/views/Activity.module.css +43 -0
- package/ui/web/src/views/Activity.test.tsx +89 -0
- package/ui/web/src/views/Activity.tsx +31 -0
- package/ui/web/src/views/Cost.css +87 -0
- package/ui/web/src/views/Cost.test.tsx +142 -0
- package/ui/web/src/views/Cost.tsx +54 -0
- package/ui/web/src/views/Overview.css +51 -0
- package/ui/web/src/views/Overview.test.tsx +76 -0
- package/ui/web/src/views/Overview.tsx +46 -0
- package/ui/web/src/views/Work.test.tsx +82 -0
- package/ui/web/src/views/Work.tsx +79 -0
- package/ui/web/src/vite-env.d.ts +10 -0
- package/ui/web/tsconfig.json +22 -0
- package/ui/web/vite.config.ts +25 -0
- package/ui/web/vitest.config.ts +12 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentRow — Expandable agent row with lazy-loaded dispatch prompt.
|
|
3
|
+
*
|
|
4
|
+
* Expansion is keyboard-operable (Enter/Space on button) and scroll-preserving
|
|
5
|
+
* across prop updates (via key-based row identity).
|
|
6
|
+
*
|
|
7
|
+
* Expanded state shows:
|
|
8
|
+
* - Task/status/runtime/tokens + dispatch prompt (fetched from /agent?id=)
|
|
9
|
+
* - Cached with simple eviction (100-item LRU cache per session)
|
|
10
|
+
*
|
|
11
|
+
* D5: all interactive elements are real <button>/<a>, status colors from theme.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useState, useRef, useEffect } from 'react';
|
|
15
|
+
import type { Agent } from '../lib/types';
|
|
16
|
+
import { fetchAgent } from '../lib/api';
|
|
17
|
+
import { TESTIDS } from '../test/fixtures';
|
|
18
|
+
import './AgentRow.css';
|
|
19
|
+
|
|
20
|
+
interface AgentRowProps {
|
|
21
|
+
agent: Agent;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Simple LRU cache for agent details
|
|
25
|
+
const agentDetailCache = new Map<string, { data: any; timestamp: number }>();
|
|
26
|
+
const MAX_CACHE_SIZE = 100;
|
|
27
|
+
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
28
|
+
|
|
29
|
+
function getCachedAgentDetail(id: string): any | null {
|
|
30
|
+
const cached = agentDetailCache.get(id);
|
|
31
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
32
|
+
return cached.data;
|
|
33
|
+
}
|
|
34
|
+
if (cached) {
|
|
35
|
+
agentDetailCache.delete(id);
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function setCachedAgentDetail(id: string, data: any) {
|
|
41
|
+
if (agentDetailCache.size >= MAX_CACHE_SIZE) {
|
|
42
|
+
const firstKey = agentDetailCache.keys().next().value;
|
|
43
|
+
if (firstKey !== undefined) {
|
|
44
|
+
agentDetailCache.delete(firstKey);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
agentDetailCache.set(id, { data, timestamp: Date.now() });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Determine status color.
|
|
52
|
+
*/
|
|
53
|
+
function getStatusColor(status: string): string {
|
|
54
|
+
if (status === 'running') return 'var(--color-status-ok)';
|
|
55
|
+
if (status === 'idle') return 'var(--color-status-info)';
|
|
56
|
+
if (status === 'SUSPICIOUS' || status === 'HIGH') return 'var(--color-status-error)';
|
|
57
|
+
if (status === 'MED' || status === 'DRIFT') return 'var(--color-status-warn)';
|
|
58
|
+
return 'var(--color-status-neutral)';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function AgentRow({ agent }: AgentRowProps) {
|
|
62
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
63
|
+
const [detail, setDetail] = useState<any | null>(null);
|
|
64
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
65
|
+
const [error, setError] = useState<string | null>(null);
|
|
66
|
+
const expandButtonRef = useRef<HTMLButtonElement>(null);
|
|
67
|
+
|
|
68
|
+
// Load agent detail on expansion
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!isExpanded) return;
|
|
71
|
+
|
|
72
|
+
const cached = getCachedAgentDetail(agent.id);
|
|
73
|
+
if (cached) {
|
|
74
|
+
setDetail(cached);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setIsLoading(true);
|
|
79
|
+
setError(null);
|
|
80
|
+
|
|
81
|
+
fetchAgent(agent.id)
|
|
82
|
+
.then((data) => {
|
|
83
|
+
setCachedAgentDetail(agent.id, data);
|
|
84
|
+
setDetail(data);
|
|
85
|
+
})
|
|
86
|
+
.catch((err) => {
|
|
87
|
+
setError(err.message || 'Failed to load agent details');
|
|
88
|
+
})
|
|
89
|
+
.finally(() => {
|
|
90
|
+
setIsLoading(false);
|
|
91
|
+
});
|
|
92
|
+
}, [isExpanded, agent.id]);
|
|
93
|
+
|
|
94
|
+
const handleToggle = () => {
|
|
95
|
+
setIsExpanded(!isExpanded);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<li className="agent-row" data-testid={TESTIDS.agentRow} key={agent.id}>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
ref={expandButtonRef}
|
|
103
|
+
className="agent-row__toggle"
|
|
104
|
+
onClick={handleToggle}
|
|
105
|
+
aria-expanded={isExpanded}
|
|
106
|
+
aria-controls={`agent-detail-${agent.id}`}
|
|
107
|
+
aria-label={`${isExpanded ? 'Collapse' : 'Expand'} agent ${agent.id}`}
|
|
108
|
+
>
|
|
109
|
+
<span className="agent-row__toggle-icon">{isExpanded ? '▼' : '▶'}</span>
|
|
110
|
+
</button>
|
|
111
|
+
|
|
112
|
+
<div className="agent-row__header">
|
|
113
|
+
<span
|
|
114
|
+
className="agent-row__status-icon"
|
|
115
|
+
style={{ color: getStatusColor(agent.status) }}
|
|
116
|
+
aria-label={`Status: ${agent.status}`}
|
|
117
|
+
title={`Status: ${agent.status}`}
|
|
118
|
+
>
|
|
119
|
+
●
|
|
120
|
+
</span>
|
|
121
|
+
|
|
122
|
+
<span className="agent-row__id">{agent.id}</span>
|
|
123
|
+
|
|
124
|
+
<span className="agent-row__age">
|
|
125
|
+
{agent.age_s < 60 ? `${agent.age_s}s` : `${Math.floor(agent.age_s / 60)}m`}
|
|
126
|
+
</span>
|
|
127
|
+
|
|
128
|
+
<span className="agent-row__hint">{agent.hint}</span>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{isExpanded && (
|
|
132
|
+
<div className="agent-row__detail" id={`agent-detail-${agent.id}`} data-testid={TESTIDS.agentRowDetail}>
|
|
133
|
+
<div className="agent-row__detail-row">
|
|
134
|
+
<span className="agent-row__detail-label">Project:</span>
|
|
135
|
+
<span className="agent-row__detail-value">{agent.project}</span>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div className="agent-row__detail-row">
|
|
139
|
+
<span className="agent-row__detail-label">Status:</span>
|
|
140
|
+
<span
|
|
141
|
+
className="agent-row__detail-value"
|
|
142
|
+
style={{ color: getStatusColor(agent.status) }}
|
|
143
|
+
>
|
|
144
|
+
{agent.status}
|
|
145
|
+
</span>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="agent-row__detail-row">
|
|
149
|
+
<span className="agent-row__detail-label">Started:</span>
|
|
150
|
+
<span className="agent-row__detail-value">
|
|
151
|
+
{agent.startedAt ? new Date(agent.startedAt).toLocaleString() : 'unknown'}
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div className="agent-row__detail-row">
|
|
156
|
+
<span className="agent-row__detail-label">Last Activity:</span>
|
|
157
|
+
<span className="agent-row__detail-value">
|
|
158
|
+
{agent.lastActivity ? new Date(agent.lastActivity).toLocaleString() : 'unknown'}
|
|
159
|
+
</span>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div className="agent-row__detail-row">
|
|
163
|
+
<span className="agent-row__detail-label">Runtime:</span>
|
|
164
|
+
<span className="agent-row__detail-value">
|
|
165
|
+
{typeof agent.runtimeSeconds === 'number'
|
|
166
|
+
? `${Math.floor(agent.runtimeSeconds / 60)}m ${agent.runtimeSeconds % 60}s`
|
|
167
|
+
: 'unknown'}
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div className="agent-row__detail-row">
|
|
172
|
+
<span className="agent-row__detail-label">Tokens Used:</span>
|
|
173
|
+
<span className="agent-row__detail-value">
|
|
174
|
+
{typeof agent.tokensUsed === 'number' ? agent.tokensUsed.toLocaleString() : 'unknown'}
|
|
175
|
+
</span>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div className="agent-row__detail-row">
|
|
179
|
+
<span className="agent-row__detail-label">Task:</span>
|
|
180
|
+
<span className="agent-row__detail-value">{agent.taskLabel}</span>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{isLoading && (
|
|
184
|
+
<div className="agent-row__prompt">
|
|
185
|
+
<span className="agent-row__prompt-label">Dispatch Prompt:</span>
|
|
186
|
+
<div className="agent-row__prompt-loading">Loading...</div>
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{error && (
|
|
191
|
+
<div className="agent-row__prompt">
|
|
192
|
+
<span className="agent-row__prompt-label">Dispatch Prompt:</span>
|
|
193
|
+
<div className="agent-row__prompt-error">Error: {error}</div>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{detail && !isLoading && (
|
|
198
|
+
<div className="agent-row__prompt">
|
|
199
|
+
<span className="agent-row__prompt-label">Dispatch Prompt:</span>
|
|
200
|
+
<pre className="agent-row__prompt-text">{detail.dispatch_prompt}</pre>
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</li>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
.agents-panel {
|
|
2
|
+
flex: 1;
|
|
3
|
+
min-width: 0;
|
|
4
|
+
padding: var(--space-5);
|
|
5
|
+
background: var(--color-surface);
|
|
6
|
+
border: 1px solid var(--color-border);
|
|
7
|
+
border-radius: var(--radius-md);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.agents-panel h2 {
|
|
11
|
+
margin: 0 0 var(--space-4) 0;
|
|
12
|
+
font-size: var(--font-size-lg);
|
|
13
|
+
font-weight: var(--font-weight-bold);
|
|
14
|
+
color: var(--color-text);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.agents-panel__summaries {
|
|
18
|
+
display: grid;
|
|
19
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
20
|
+
gap: var(--space-3);
|
|
21
|
+
margin-bottom: var(--space-4);
|
|
22
|
+
padding: var(--space-3);
|
|
23
|
+
background: var(--color-bg-subtle);
|
|
24
|
+
border: 1px solid var(--color-border-subtle);
|
|
25
|
+
border-radius: var(--radius-md);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.agent-summary {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: var(--space-2);
|
|
32
|
+
padding: var(--space-2);
|
|
33
|
+
background: var(--color-surface);
|
|
34
|
+
border: 1px solid var(--color-border-subtle);
|
|
35
|
+
border-radius: var(--radius-sm);
|
|
36
|
+
font-size: var(--font-size-sm);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.agent-summary__project {
|
|
40
|
+
flex: 0 0 auto;
|
|
41
|
+
font-weight: var(--font-weight-medium);
|
|
42
|
+
color: var(--color-text);
|
|
43
|
+
min-width: fit-content;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.agent-summary__status {
|
|
47
|
+
flex: 0 0 auto;
|
|
48
|
+
padding: var(--space-1) var(--space-2);
|
|
49
|
+
border-radius: var(--radius-sm);
|
|
50
|
+
font-size: var(--font-size-xs);
|
|
51
|
+
font-weight: var(--font-weight-bold);
|
|
52
|
+
text-transform: uppercase;
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.agent-summary__status--running {
|
|
57
|
+
background: var(--color-status-ok-bg);
|
|
58
|
+
color: var(--color-status-ok);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.agent-summary__status--idle {
|
|
62
|
+
background: var(--color-status-info-bg);
|
|
63
|
+
color: var(--color-status-info);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.agent-summary__status--SUSPICIOUS,
|
|
67
|
+
.agent-summary__status--HIGH {
|
|
68
|
+
background: var(--color-status-error-bg);
|
|
69
|
+
color: var(--color-status-error);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.agent-summary__status--MED,
|
|
73
|
+
.agent-summary__status--DRIFT {
|
|
74
|
+
background: var(--color-status-warn-bg);
|
|
75
|
+
color: var(--color-status-warn);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.agent-summary__runtime {
|
|
79
|
+
flex: 1;
|
|
80
|
+
text-align: right;
|
|
81
|
+
font-family: var(--font-mono);
|
|
82
|
+
color: var(--color-text-muted);
|
|
83
|
+
min-width: fit-content;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.agents-panel__list {
|
|
87
|
+
list-style: none;
|
|
88
|
+
margin: 0;
|
|
89
|
+
padding: 0;
|
|
90
|
+
margin-top: var(--space-4);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.empty-state {
|
|
94
|
+
padding: var(--space-4);
|
|
95
|
+
text-align: center;
|
|
96
|
+
color: var(--color-text-muted);
|
|
97
|
+
font-size: var(--font-size-sm);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@media (max-width: 768px) {
|
|
101
|
+
.agents-panel {
|
|
102
|
+
padding: var(--space-3);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.agents-panel h2 {
|
|
106
|
+
font-size: var(--font-size-md);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { AgentsPanel } from './AgentsPanel';
|
|
4
|
+
import { fixtureAgents, TESTIDS } from '../test/fixtures';
|
|
5
|
+
|
|
6
|
+
vi.mock('./AgentRow', () => ({
|
|
7
|
+
AgentRow: ({ agent }: any) => <div data-testid="agent-row-mock">{agent.id}</div>,
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe('AgentsPanel', () => {
|
|
11
|
+
it('renders agents list with count', () => {
|
|
12
|
+
render(<AgentsPanel agents={fixtureAgents} />);
|
|
13
|
+
|
|
14
|
+
expect(screen.getByText(`Fleet Agents (${fixtureAgents.length})`)).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('renders each agent row', () => {
|
|
18
|
+
render(<AgentsPanel agents={fixtureAgents} />);
|
|
19
|
+
|
|
20
|
+
const rows = screen.getAllByTestId('agent-row-mock');
|
|
21
|
+
expect(rows).toHaveLength(fixtureAgents.length);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders empty state when no agents', () => {
|
|
25
|
+
render(<AgentsPanel agents={[]} />);
|
|
26
|
+
|
|
27
|
+
expect(screen.getByText('No agents running.')).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders empty state when agents is null', () => {
|
|
31
|
+
render(<AgentsPanel agents={null} />);
|
|
32
|
+
|
|
33
|
+
expect(screen.getByText('No agents running.')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('has correct data-testid', () => {
|
|
37
|
+
render(<AgentsPanel agents={fixtureAgents} />);
|
|
38
|
+
|
|
39
|
+
expect(screen.getByTestId(TESTIDS.agentRow)).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentsPanel — Fleet agents list with expandable rows.
|
|
3
|
+
* Each agent row displays project · status · runtime at a glance.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Agent } from '../lib/types';
|
|
7
|
+
import { AgentRow } from './AgentRow';
|
|
8
|
+
import { TESTIDS } from '../test/fixtures';
|
|
9
|
+
import './AgentsPanel.css';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format runtime in seconds to a readable string.
|
|
13
|
+
*/
|
|
14
|
+
function formatRuntime(seconds: number | undefined): string {
|
|
15
|
+
if (!seconds || seconds < 0) return 'unknown';
|
|
16
|
+
if (seconds < 60) return `${Math.floor(seconds)}s`;
|
|
17
|
+
const mins = Math.floor(seconds / 60);
|
|
18
|
+
if (mins < 60) return `${mins}m`;
|
|
19
|
+
const hours = Math.floor(seconds / 3600);
|
|
20
|
+
return `${hours}h`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface AgentsPanelProps {
|
|
24
|
+
agents: Agent[] | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function AgentsPanel({ agents }: AgentsPanelProps) {
|
|
28
|
+
if (!agents || agents.length === 0) {
|
|
29
|
+
return (
|
|
30
|
+
<section className="agents-panel" data-testid={TESTIDS.agentRow}>
|
|
31
|
+
<h2>Fleet Agents</h2>
|
|
32
|
+
<p className="empty-state">No agents running.</p>
|
|
33
|
+
</section>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<section className="agents-panel" data-testid={TESTIDS.agentRow}>
|
|
39
|
+
<h2>Fleet Agents ({agents.length})</h2>
|
|
40
|
+
<div className="agents-panel__summaries">
|
|
41
|
+
{agents.map((agent) => (
|
|
42
|
+
<div key={`${agent.id}-summary`} className="agent-summary">
|
|
43
|
+
<span className="agent-summary__project">{agent.project}</span>
|
|
44
|
+
<span className={`agent-summary__status agent-summary__status--${agent.status}`}>
|
|
45
|
+
{agent.status}
|
|
46
|
+
</span>
|
|
47
|
+
<span className="agent-summary__runtime">{formatRuntime(agent.runtimeSeconds)}</span>
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
<ul className="agents-panel__list">
|
|
52
|
+
{agents.map((agent) => (
|
|
53
|
+
<AgentRow key={agent.id} agent={agent} />
|
|
54
|
+
))}
|
|
55
|
+
</ul>
|
|
56
|
+
</section>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
.alerts-panel {
|
|
2
|
+
flex: 1;
|
|
3
|
+
min-width: 0;
|
|
4
|
+
padding: var(--space-5);
|
|
5
|
+
background: var(--color-surface);
|
|
6
|
+
border: 1px solid var(--color-border);
|
|
7
|
+
border-radius: var(--radius-md);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.alerts-panel h2 {
|
|
11
|
+
margin: 0 0 var(--space-4) 0;
|
|
12
|
+
font-size: var(--font-size-lg);
|
|
13
|
+
font-weight: var(--font-weight-bold);
|
|
14
|
+
color: var(--color-text);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.alerts-panel__list {
|
|
18
|
+
list-style: none;
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 0;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
gap: var(--space-3);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.alerts-panel__item {
|
|
27
|
+
display: flex;
|
|
28
|
+
gap: var(--space-3);
|
|
29
|
+
align-items: flex-start;
|
|
30
|
+
padding: var(--space-3);
|
|
31
|
+
background: var(--color-bg-subtle);
|
|
32
|
+
border-left: 4px solid var(--color-border);
|
|
33
|
+
border-radius: var(--radius-md);
|
|
34
|
+
font-size: var(--font-size-sm);
|
|
35
|
+
line-height: var(--line-height-normal);
|
|
36
|
+
word-break: break-word;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.alerts-panel__item[data-severity='error'] {
|
|
40
|
+
background: var(--color-status-error-bg);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.alerts-panel__item[data-severity='warn'] {
|
|
44
|
+
background: var(--color-status-warn-bg);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.alerts-panel__item[data-severity='info'] {
|
|
48
|
+
background: var(--color-status-info-bg);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.alerts-panel__severity {
|
|
52
|
+
display: inline-block;
|
|
53
|
+
min-width: 50px;
|
|
54
|
+
font-weight: var(--font-weight-bold);
|
|
55
|
+
font-size: var(--font-size-xs);
|
|
56
|
+
text-transform: uppercase;
|
|
57
|
+
letter-spacing: 0.05em;
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.alerts-panel__text {
|
|
62
|
+
color: var(--color-text);
|
|
63
|
+
font-family: var(--font-mono);
|
|
64
|
+
font-size: var(--font-size-xs);
|
|
65
|
+
flex-grow: 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.empty-state {
|
|
69
|
+
padding: var(--space-4);
|
|
70
|
+
text-align: center;
|
|
71
|
+
color: var(--color-text-muted);
|
|
72
|
+
font-size: var(--font-size-sm);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@media (max-width: 768px) {
|
|
76
|
+
.alerts-panel {
|
|
77
|
+
padding: var(--space-3);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.alerts-panel h2 {
|
|
81
|
+
font-size: var(--font-size-md);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.alerts-panel__item {
|
|
85
|
+
padding: var(--space-2);
|
|
86
|
+
gap: var(--space-2);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { AlertsPanel } from './AlertsPanel';
|
|
4
|
+
import { fixtureAlerts, fixtureAlertsEmpty, TESTIDS } from '../test/fixtures';
|
|
5
|
+
|
|
6
|
+
describe('AlertsPanel', () => {
|
|
7
|
+
it('renders alerts with severity styling', () => {
|
|
8
|
+
render(<AlertsPanel alerts={fixtureAlerts} />);
|
|
9
|
+
|
|
10
|
+
expect(screen.getByTestId(TESTIDS.alertsPanel)).toBeInTheDocument();
|
|
11
|
+
const alertLines = screen.getAllByTestId(TESTIDS.alertLine);
|
|
12
|
+
expect(alertLines).toHaveLength(fixtureAlerts.lines.length);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('displays alert severity as text', () => {
|
|
16
|
+
render(<AlertsPanel alerts={fixtureAlerts} />);
|
|
17
|
+
|
|
18
|
+
// First line has HIGH
|
|
19
|
+
expect(screen.getByText(/HIGH/)).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders empty state when no alerts', () => {
|
|
23
|
+
render(<AlertsPanel alerts={fixtureAlertsEmpty} />);
|
|
24
|
+
|
|
25
|
+
expect(screen.getByText('No alerts.')).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('renders empty state when alerts is null', () => {
|
|
29
|
+
render(<AlertsPanel alerts={null} />);
|
|
30
|
+
|
|
31
|
+
expect(screen.getByText('No alerts.')).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('applies severity-based styling to alert items', () => {
|
|
35
|
+
render(<AlertsPanel alerts={fixtureAlerts} />);
|
|
36
|
+
|
|
37
|
+
const alertItems = screen.getAllByTestId(TESTIDS.alertLine);
|
|
38
|
+
// First alert has HIGH severity
|
|
39
|
+
expect(alertItems[0]).toHaveAttribute('data-severity', 'error');
|
|
40
|
+
// Second alert has MED severity
|
|
41
|
+
expect(alertItems[1]).toHaveAttribute('data-severity', 'warn');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('displays alert text content', () => {
|
|
45
|
+
render(<AlertsPanel alerts={fixtureAlerts} />);
|
|
46
|
+
|
|
47
|
+
fixtureAlerts.lines.forEach((line) => {
|
|
48
|
+
expect(screen.getByText(line)).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AlertsPanel — Security alerts with severity styling.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Alert } from '../lib/types';
|
|
6
|
+
import { TESTIDS } from '../test/fixtures';
|
|
7
|
+
import './AlertsPanel.css';
|
|
8
|
+
|
|
9
|
+
interface AlertsPanelProps {
|
|
10
|
+
alerts: Alert | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Determine severity level from alert line.
|
|
15
|
+
*/
|
|
16
|
+
function getSeverity(line: string): 'error' | 'warn' | 'info' {
|
|
17
|
+
if (line.includes('HIGH') || line.includes('SUSPICIOUS')) return 'error';
|
|
18
|
+
if (line.includes('MED') || line.includes('DRIFT')) return 'warn';
|
|
19
|
+
return 'info';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get color for severity level.
|
|
24
|
+
*/
|
|
25
|
+
function getSeverityColor(severity: 'error' | 'warn' | 'info'): string {
|
|
26
|
+
if (severity === 'error') return 'var(--color-status-error)';
|
|
27
|
+
if (severity === 'warn') return 'var(--color-status-warn)';
|
|
28
|
+
return 'var(--color-status-info)';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function AlertsPanel({ alerts }: AlertsPanelProps) {
|
|
32
|
+
const hasAlerts = alerts && alerts.lines.length > 0;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<section className="alerts-panel" data-testid={TESTIDS.alertsPanel}>
|
|
36
|
+
<h2>Security Alerts</h2>
|
|
37
|
+
{!hasAlerts ? (
|
|
38
|
+
<p className="empty-state">No alerts.</p>
|
|
39
|
+
) : (
|
|
40
|
+
<ul className="alerts-panel__list">
|
|
41
|
+
{alerts.lines.map((line, idx) => {
|
|
42
|
+
const severity = getSeverity(line);
|
|
43
|
+
return (
|
|
44
|
+
<li
|
|
45
|
+
key={idx}
|
|
46
|
+
className="alerts-panel__item"
|
|
47
|
+
data-testid={TESTIDS.alertLine}
|
|
48
|
+
data-severity={severity}
|
|
49
|
+
style={{
|
|
50
|
+
borderLeftColor: getSeverityColor(severity),
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<span
|
|
54
|
+
className="alerts-panel__severity"
|
|
55
|
+
style={{ color: getSeverityColor(severity) }}
|
|
56
|
+
>
|
|
57
|
+
{severity.toUpperCase()}
|
|
58
|
+
</span>
|
|
59
|
+
<span className="alerts-panel__text">{line}</span>
|
|
60
|
+
</li>
|
|
61
|
+
);
|
|
62
|
+
})}
|
|
63
|
+
</ul>
|
|
64
|
+
)}
|
|
65
|
+
</section>
|
|
66
|
+
);
|
|
67
|
+
}
|