feat(FN-909): redesign Agent Manager UI with CSS class approach and design tokens
- Replace AgentDetailView and AgentsView components with CSS class-based styling approach - Integrate AI generation feature from FN-865 using CSS class methodology - Fix CSS class verification test regex and add missing .agent-detail-info rule - Refactor NewAgentDialog to align with new component structure - Redesign Agent Manager UI using global design tokens for consistent theming - Expand styles.css with comprehensive agent-specific CSS rules and design system variables - Add agent-css-classes.test.ts to verify CSS class coverage and correctness
This commit is contained in:
227
packages/dashboard/app/__tests__/agent-css-classes.test.ts
Normal file
227
packages/dashboard/app/__tests__/agent-css-classes.test.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const stylesPath = path.join(__dirname, "../styles.css");
|
||||
const stylesContent = fs.readFileSync(stylesPath, "utf-8");
|
||||
|
||||
// Agent component file paths to verify inline <style> blocks are removed
|
||||
const agentsViewContent = fs.readFileSync(path.join(__dirname, "../components/AgentsView.tsx"), "utf-8");
|
||||
const agentDetailViewContent = fs.readFileSync(path.join(__dirname, "../components/AgentDetailView.tsx"), "utf-8");
|
||||
const activeAgentsPanelContent = fs.readFileSync(path.join(__dirname, "../components/ActiveAgentsPanel.tsx"), "utf-8");
|
||||
const newAgentDialogContent = fs.readFileSync(path.join(__dirname, "../components/NewAgentDialog.tsx"), "utf-8");
|
||||
|
||||
/** Check that styles.css contains a CSS class definition for the given selector */
|
||||
function hasClass(cls: string): boolean {
|
||||
const escaped = cls.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
// Match standalone class or class within a grouped selector list
|
||||
return new RegExp(`${escaped}\\s*(,|\\{)`).test(stylesContent);
|
||||
}
|
||||
|
||||
describe("Agent CSS classes", () => {
|
||||
// Verify agent state CSS variables are defined in the global stylesheet
|
||||
it("should define --state-* CSS variables", () => {
|
||||
expect(stylesContent).toContain("--state-idle-bg:");
|
||||
expect(stylesContent).toContain("--state-idle-text:");
|
||||
expect(stylesContent).toContain("--state-idle-border:");
|
||||
expect(stylesContent).toContain("--state-active-bg:");
|
||||
expect(stylesContent).toContain("--state-active-text:");
|
||||
expect(stylesContent).toContain("--state-active-border:");
|
||||
expect(stylesContent).toContain("--state-paused-bg:");
|
||||
expect(stylesContent).toContain("--state-paused-text:");
|
||||
expect(stylesContent).toContain("--state-paused-border:");
|
||||
expect(stylesContent).toContain("--state-error-bg:");
|
||||
expect(stylesContent).toContain("--state-error-text:");
|
||||
expect(stylesContent).toContain("--state-error-border:");
|
||||
});
|
||||
|
||||
// Verify BEM button modifier classes exist
|
||||
it("should define BEM button modifier classes", () => {
|
||||
expect(hasClass(".btn--sm")).toBe(true);
|
||||
expect(hasClass(".btn--primary")).toBe(true);
|
||||
expect(hasClass(".btn--danger")).toBe(true);
|
||||
expect(hasClass(".btn--warning")).toBe(true);
|
||||
});
|
||||
|
||||
// Verify badge base class
|
||||
it("should define .badge base class", () => {
|
||||
expect(hasClass(".badge")).toBe(true);
|
||||
});
|
||||
|
||||
// Verify AgentMetricsBar classes
|
||||
it("should define AgentMetricsBar CSS classes", () => {
|
||||
expect(hasClass(".agent-metrics-bar")).toBe(true);
|
||||
expect(hasClass(".agent-metric-card")).toBe(true);
|
||||
expect(hasClass(".agent-metric-info")).toBe(true);
|
||||
expect(hasClass(".agent-metric-value")).toBe(true);
|
||||
expect(hasClass(".agent-metric-label")).toBe(true);
|
||||
});
|
||||
|
||||
// Verify AgentsView classes
|
||||
it("should define AgentsView CSS classes", () => {
|
||||
expect(hasClass(".agents-view")).toBe(true);
|
||||
expect(hasClass(".agents-view-header")).toBe(true);
|
||||
expect(hasClass(".agents-view-title")).toBe(true);
|
||||
expect(hasClass(".agents-view-controls")).toBe(true);
|
||||
expect(hasClass(".agents-view-content")).toBe(true);
|
||||
expect(hasClass(".agent-controls")).toBe(true);
|
||||
expect(hasClass(".agent-state-filter")).toBe(true);
|
||||
expect(hasClass(".agent-state-filter-select")).toBe(true);
|
||||
expect(hasClass(".agent-board")).toBe(true);
|
||||
expect(hasClass(".agent-board-card")).toBe(true);
|
||||
expect(hasClass(".agent-board-header")).toBe(true);
|
||||
expect(hasClass(".agent-board-icon")).toBe(true);
|
||||
expect(hasClass(".agent-board-badge")).toBe(true);
|
||||
expect(hasClass(".agent-board-health")).toBe(true);
|
||||
expect(hasClass(".agent-board-name")).toBe(true);
|
||||
expect(hasClass(".agent-board-id")).toBe(true);
|
||||
expect(hasClass(".agent-board-clickable")).toBe(true);
|
||||
expect(hasClass(".agent-board-actions")).toBe(true);
|
||||
expect(hasClass(".agent-list")).toBe(true);
|
||||
expect(hasClass(".agent-card")).toBe(true);
|
||||
expect(hasClass(".agent-card-header")).toBe(true);
|
||||
expect(hasClass(".agent-card-body")).toBe(true);
|
||||
expect(hasClass(".agent-card-actions")).toBe(true);
|
||||
expect(hasClass(".agent-info")).toBe(true);
|
||||
expect(hasClass(".agent-info--clickable")).toBe(true);
|
||||
expect(hasClass(".agent-icon")).toBe(true);
|
||||
expect(hasClass(".agent-icon--clickable")).toBe(true);
|
||||
expect(hasClass(".agent-meta")).toBe(true);
|
||||
expect(hasClass(".agent-name")).toBe(true);
|
||||
expect(hasClass(".agent-id")).toBe(true);
|
||||
expect(hasClass(".agent-badges")).toBe(true);
|
||||
expect(hasClass(".agent-card-chevron")).toBe(true);
|
||||
expect(hasClass(".agent-task")).toBe(true);
|
||||
expect(hasClass(".agent-heartbeat")).toBe(true);
|
||||
expect(hasClass(".agent-role-select")).toBe(true);
|
||||
expect(hasClass(".agent-empty")).toBe(true);
|
||||
expect(hasClass(".spin")).toBe(true);
|
||||
});
|
||||
|
||||
// Verify AgentDetailView classes
|
||||
it("should define AgentDetailView CSS classes", () => {
|
||||
expect(hasClass(".agent-detail-overlay")).toBe(true);
|
||||
expect(hasClass(".agent-detail-modal")).toBe(true);
|
||||
expect(hasClass(".agent-detail-loading")).toBe(true);
|
||||
expect(hasClass(".agent-detail-header")).toBe(true);
|
||||
expect(hasClass(".agent-detail-title")).toBe(true);
|
||||
expect(hasClass(".agent-detail-icon")).toBe(true);
|
||||
expect(hasClass(".agent-detail-info")).toBe(true);
|
||||
expect(hasClass(".agent-detail-badges")).toBe(true);
|
||||
expect(hasClass(".agent-detail-actions")).toBe(true);
|
||||
expect(hasClass(".agent-detail-tabs")).toBe(true);
|
||||
expect(hasClass(".agent-detail-tab")).toBe(true);
|
||||
expect(hasClass(".agent-detail-content")).toBe(true);
|
||||
expect(hasClass(".agent-detail-footer")).toBe(true);
|
||||
expect(hasClass(".agent-detail-id")).toBe(true);
|
||||
expect(hasClass(".dashboard-tab")).toBe(true);
|
||||
expect(hasClass(".dashboard-section")).toBe(true);
|
||||
expect(hasClass(".info-grid")).toBe(true);
|
||||
expect(hasClass(".info-item")).toBe(true);
|
||||
expect(hasClass(".info-label")).toBe(true);
|
||||
expect(hasClass(".info-value")).toBe(true);
|
||||
expect(hasClass(".inline-badge")).toBe(true);
|
||||
expect(hasClass(".stats-grid")).toBe(true);
|
||||
expect(hasClass(".stat-card")).toBe(true);
|
||||
expect(hasClass(".stat-value")).toBe(true);
|
||||
expect(hasClass(".stat-label")).toBe(true);
|
||||
expect(hasClass(".current-task")).toBe(true);
|
||||
expect(hasClass(".task-badge")).toBe(true);
|
||||
expect(hasClass(".metadata-json")).toBe(true);
|
||||
expect(hasClass(".logs-tab")).toBe(true);
|
||||
expect(hasClass(".logs-header")).toBe(true);
|
||||
expect(hasClass(".logs-count")).toBe(true);
|
||||
expect(hasClass(".streaming-indicator")).toBe(true);
|
||||
expect(hasClass(".streaming-dot")).toBe(true);
|
||||
expect(hasClass(".logs-container")).toBe(true);
|
||||
expect(hasClass(".logs-empty")).toBe(true);
|
||||
expect(hasClass(".log-entry")).toBe(true);
|
||||
expect(hasClass(".log-timestamp")).toBe(true);
|
||||
expect(hasClass(".log-agent")).toBe(true);
|
||||
expect(hasClass(".log-icon")).toBe(true);
|
||||
expect(hasClass(".log-text")).toBe(true);
|
||||
expect(hasClass(".log-detail")).toBe(true);
|
||||
expect(hasClass(".runs-tab")).toBe(true);
|
||||
expect(hasClass(".runs-empty")).toBe(true);
|
||||
expect(hasClass(".run-card")).toBe(true);
|
||||
expect(hasClass(".run-card--active")).toBe(true);
|
||||
expect(hasClass(".run-header")).toBe(true);
|
||||
expect(hasClass(".run-live-indicator")).toBe(true);
|
||||
expect(hasClass(".live-dot")).toBe(true);
|
||||
expect(hasClass(".run-id")).toBe(true);
|
||||
expect(hasClass(".run-status")).toBe(true);
|
||||
expect(hasClass(".run-details")).toBe(true);
|
||||
expect(hasClass(".config-tab")).toBe(true);
|
||||
expect(hasClass(".config-section")).toBe(true);
|
||||
expect(hasClass(".config-description")).toBe(true);
|
||||
expect(hasClass(".config-fields")).toBe(true);
|
||||
expect(hasClass(".config-field")).toBe(true);
|
||||
expect(hasClass(".config-hint")).toBe(true);
|
||||
expect(hasClass(".config-error")).toBe(true);
|
||||
expect(hasClass(".config-actions")).toBe(true);
|
||||
expect(hasClass(".config-saved-indicator")).toBe(true);
|
||||
expect(hasClass(".input--error")).toBe(true);
|
||||
});
|
||||
|
||||
// Verify ActiveAgentsPanel classes
|
||||
it("should define ActiveAgentsPanel CSS classes", () => {
|
||||
expect(hasClass(".active-agents-panel")).toBe(true);
|
||||
expect(hasClass(".active-agents-panel-header")).toBe(true);
|
||||
expect(hasClass(".active-agents-grid")).toBe(true);
|
||||
expect(hasClass(".live-agent-card")).toBe(true);
|
||||
expect(hasClass(".live-agent-card-header")).toBe(true);
|
||||
expect(hasClass(".live-agent-card-name")).toBe(true);
|
||||
expect(hasClass(".live-agent-pulse")).toBe(true);
|
||||
expect(hasClass(".live-agent-task")).toBe(true);
|
||||
expect(hasClass(".live-agent-card-transcript")).toBe(true);
|
||||
expect(hasClass(".live-agent-card-empty")).toBe(true);
|
||||
expect(hasClass(".live-agent-card-line")).toBe(true);
|
||||
expect(hasClass(".live-agent-card-footer")).toBe(true);
|
||||
expect(hasClass(".live-agent-streaming-dot")).toBe(true);
|
||||
});
|
||||
|
||||
// Verify NewAgentDialog classes
|
||||
it("should define NewAgentDialog CSS classes", () => {
|
||||
expect(hasClass(".agent-dialog-overlay")).toBe(true);
|
||||
expect(hasClass(".agent-dialog")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-header")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-header-title")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-body")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-footer")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-steps")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-step")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-field")).toBe(true);
|
||||
expect(hasClass(".agent-role-grid")).toBe(true);
|
||||
expect(hasClass(".agent-role-option")).toBe(true);
|
||||
expect(hasClass(".agent-role-option-icon")).toBe(true);
|
||||
expect(hasClass(".agent-role-option-label")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-summary")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-summary-row")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-summary-row-label")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-summary-row-value")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-required")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-optional")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-error")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-info")).toBe(true);
|
||||
expect(hasClass(".agent-dialog-loading")).toBe(true);
|
||||
});
|
||||
|
||||
// Verify no inline <style> blocks remain in agent components
|
||||
it("should not have inline <style> blocks in AgentsView", () => {
|
||||
expect(agentsViewContent).not.toContain("<style>");
|
||||
});
|
||||
|
||||
it("should not have inline <style> blocks in AgentDetailView", () => {
|
||||
expect(agentDetailViewContent).not.toContain("<style>");
|
||||
});
|
||||
|
||||
it("should not have inline <style> blocks in ActiveAgentsPanel", () => {
|
||||
expect(activeAgentsPanelContent).not.toContain("<style>");
|
||||
});
|
||||
|
||||
// Verify no inline style={{}} in NewAgentDialog
|
||||
it("should not have inline style={{}} attributes in NewAgentDialog", () => {
|
||||
const inlineStyleCount = (newAgentDialogContent.match(/style=\{\{/g) || []).length;
|
||||
expect(inlineStyleCount).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -407,184 +407,6 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast }: Agent
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.agent-detail-overlay {
|
||||
/* Agent state CSS variables - define fallback values */
|
||||
--state-idle-bg: rgba(139, 148, 158, 0.15);
|
||||
--state-idle-text: #8b949e;
|
||||
--state-idle-border: #8b949e;
|
||||
--state-active-bg: rgba(46, 160, 67, 0.15);
|
||||
--state-active-text: #3fb950;
|
||||
--state-active-border: #3fb950;
|
||||
--state-paused-bg: rgba(227, 179, 65, 0.15);
|
||||
--state-paused-text: #e3b541;
|
||||
--state-paused-border: #e3b541;
|
||||
--state-error-bg: rgba(248, 81, 73, 0.15);
|
||||
--state-error-text: #f85149;
|
||||
--state-error-border: #f85149;
|
||||
--text-secondary: var(--text-muted, #8b949e);
|
||||
|
||||
/* Component-local aliases for dashboard tokens */
|
||||
--bg-primary: var(--surface, #161b22);
|
||||
--accent: var(--todo, #58a6ff);
|
||||
--text-primary: var(--text, #e6edf3);
|
||||
--bg-hover: var(--card-hover, #282e36);
|
||||
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.agent-detail-modal {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.agent-detail-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 60px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.agent-detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.agent-detail-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.agent-detail-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
background: var(--accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.agent-detail-info h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
.agent-detail-badges {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-detail-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-detail-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 0 24px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.agent-detail-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.agent-detail-tab:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.agent-detail-tab.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
|
||||
.agent-detail-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.agent-detail-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.agent-detail-id {
|
||||
font-family: var(--font-mono);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.agent-detail-id:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.divider {
|
||||
color: var(--border);
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -711,107 +533,6 @@ function DashboardTab({
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
.dashboard-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.dashboard-section {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dashboard-section h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 16px 0;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.inline-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.current-task {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-badge {
|
||||
font-family: var(--font-mono);
|
||||
background: var(--bg-primary);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.metadata-json {
|
||||
background: var(--bg-primary);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -838,28 +559,6 @@ function LogsTab({
|
||||
<p className="text-muted">
|
||||
Agent logs are available when the agent is assigned to a task
|
||||
</p>
|
||||
|
||||
<style>{`
|
||||
.logs-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.logs-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logs-empty p {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -896,72 +595,6 @@ function LogsTab({
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.logs-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.logs-count {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.streaming-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--color-success, #3fb950);
|
||||
}
|
||||
|
||||
.streaming-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--color-success, #3fb950);
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.logs-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logs-empty p {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1020,43 +653,6 @@ function LogEntry({ entry, showTimestamp }: { entry: AgentLogEntry; showTimestam
|
||||
<span className="log-detail"> — {entry.detail}</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<style>{`
|
||||
.log-entry {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
margin: 2px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-agent {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-text {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.log-detail {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1078,21 +674,6 @@ function RunsTab({
|
||||
<Activity size={48} opacity={0.3} />
|
||||
<p>No runs yet</p>
|
||||
<p className="text-muted">Heartbeat runs will appear here</p>
|
||||
|
||||
<style>{`
|
||||
.runs-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
.runs-empty p {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1145,86 +726,6 @@ function RunsTab({
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<style>{`
|
||||
.runs-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.run-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.run-card--active {
|
||||
border-color: var(--cyan, #06b6d4);
|
||||
background: rgba(6, 182, 212, 0.05);
|
||||
}
|
||||
|
||||
.run-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.run-live-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-weight: 600;
|
||||
color: var(--cyan, #06b6d4);
|
||||
}
|
||||
|
||||
.live-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--cyan, #06b6d4);
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.run-id {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.run-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.run-status.active {
|
||||
color: var(--cyan, #06b6d4);
|
||||
}
|
||||
|
||||
.run-status.completed {
|
||||
color: var(--color-success, #3fb950);
|
||||
}
|
||||
|
||||
.run-status.failed {
|
||||
color: var(--color-error, #f85149);
|
||||
}
|
||||
|
||||
.run-status.terminated {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.run-details {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1534,81 +1035,6 @@ function ConfigTab({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.config-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.config-section h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.config-description {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.config-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.config-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.config-field label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.config-hint {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.config-error {
|
||||
font-size: 11px;
|
||||
color: var(--color-error, #f85149);
|
||||
}
|
||||
|
||||
.input--error {
|
||||
border-color: var(--color-error, #f85149) !important;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.config-saved-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--color-success, #3fb950);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -585,385 +585,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
.agents-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
/* Agent state CSS variables - define fallback values */
|
||||
--state-idle-bg: rgba(139, 148, 158, 0.15);
|
||||
--state-idle-text: #8b949e;
|
||||
--state-idle-border: #8b949e;
|
||||
--state-active-bg: rgba(46, 160, 67, 0.15);
|
||||
--state-active-text: #3fb950;
|
||||
--state-active-border: #3fb950;
|
||||
--state-paused-bg: rgba(227, 179, 65, 0.15);
|
||||
--state-paused-text: #e3b541;
|
||||
--state-paused-border: #e3b541;
|
||||
--state-error-bg: rgba(248, 81, 73, 0.15);
|
||||
--state-error-text: #f85149;
|
||||
--state-error-border: #f85149;
|
||||
--text-secondary: var(--text-muted, #8b949e);
|
||||
}
|
||||
|
||||
.agents-view-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.agents-view-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agents-view-title h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.agents-view-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agents-view-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.agent-controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.agent-state-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
transition: border-color var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.agent-state-filter:hover {
|
||||
border-color: var(--text-dim);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.agent-state-filter:focus-within {
|
||||
border-color: var(--todo);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.agent-state-filter-select {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-family: var(--font-primary);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.agent-create-form {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.agent-create-form .input {
|
||||
flex: 1;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
font-family: var(--font-primary);
|
||||
outline: none;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.agent-create-form .input:focus {
|
||||
border-color: var(--todo);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.agent-create-form .input::placeholder {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.agent-create-form .select {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
font-family: var(--font-primary);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.agent-create-form .select:focus {
|
||||
border-color: var(--todo);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.agent-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.agent-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.agent-board-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-top-width: 3px;
|
||||
border-radius: 8px;
|
||||
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.agent-board-card:hover {
|
||||
background: var(--card-hover);
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.agent-board-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-board-icon {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.agent-board-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.agent-board-health {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.agent-board-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.agent-board-id {
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.agent-board-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.agent-board-clickable:hover .agent-board-name {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.agent-board-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.agent-board-actions .btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.agent-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.agent-card {
|
||||
border: 1px solid var(--border);
|
||||
border-left-width: 4px;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.agent-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.agent-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.agent-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.agent-icon--clickable {
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.agent-icon--clickable:hover {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.agent-icon--clickable:focus {
|
||||
outline: 2px solid var(--accent, #58a6ff);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.agent-role-select {
|
||||
font-size: 14px;
|
||||
padding: 4px 8px;
|
||||
min-width: 120px;
|
||||
width: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.agent-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.agent-info--clickable {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
margin: -4px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.agent-info--clickable:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.agent-info--clickable:hover .agent-name {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.agent-card-chevron {
|
||||
color: var(--text-muted);
|
||||
margin-left: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.agent-info--clickable:hover .agent-card-chevron {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.agent-id {
|
||||
font-size: 12px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.agent-badges {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.agent-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.agent-task,
|
||||
.agent-heartbeat {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,17 +159,15 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
const selectedRole = AGENT_ROLES.find(r => r.value === role);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="agent-dialog-overlay" onClick={(e) => { if (e.target === e.currentTarget) handleClose(); }}>
|
||||
<div className="agent-dialog-overlay" onClick={(e) => { if (e.target === e.currentTarget) handleClose(); }}>
|
||||
<div className="agent-dialog" role="dialog" aria-modal="true" aria-label="Create new agent">
|
||||
{/* Header */}
|
||||
<div className="agent-dialog-header">
|
||||
<span style={{ fontWeight: 600, fontSize: 15 }}>New Agent</span>
|
||||
<span className="agent-dialog-header-title">New Agent</span>
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={handleClose}
|
||||
aria-label="Close"
|
||||
style={{ background: "none", border: "none", cursor: "pointer", color: "var(--text-muted)", fontSize: 18, lineHeight: 1 }}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -191,7 +189,7 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
{step === 0 && (
|
||||
<div>
|
||||
<div className="agent-dialog-field">
|
||||
<label htmlFor="agent-name">Name <span style={{ color: "var(--state-error-text, #f85149)" }}>*</span></label>
|
||||
<label htmlFor="agent-name">Name <span className="agent-dialog-required">*</span></label>
|
||||
<input
|
||||
id="agent-name"
|
||||
type="text"
|
||||
@@ -200,11 +198,10 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
autoFocus
|
||||
style={{ width: "100%", boxSizing: "border-box" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="agent-dialog-field">
|
||||
<label htmlFor="agent-title">Title <span style={{ color: "var(--text-muted)", fontWeight: 400 }}>(optional)</span></label>
|
||||
<label htmlFor="agent-title">Title <span className="agent-dialog-optional">(optional)</span></label>
|
||||
<input
|
||||
id="agent-title"
|
||||
type="text"
|
||||
@@ -212,7 +209,6 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
placeholder="e.g. Senior Code Reviewer"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
style={{ width: "100%", boxSizing: "border-box" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="agent-dialog-field">
|
||||
@@ -226,23 +222,22 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
onClick={() => setRole(r.value)}
|
||||
>
|
||||
<span className="agent-role-option-icon">{r.icon}</span>
|
||||
<span style={{ fontSize: 12, marginTop: 4 }}>{r.label}</span>
|
||||
<span className="agent-role-option-label">{r.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* AI-assisted generation */}
|
||||
<div style={{ marginTop: 8, borderTop: "1px solid var(--border)", paddingTop: 12 }}>
|
||||
<div className="agent-dialog-ai-generate">
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
className="btn btn--ai-generate"
|
||||
onClick={() => setIsGenerationModalOpen(true)}
|
||||
style={{ width: "100%", display: "flex", alignItems: "center", justifyContent: "center", gap: 6 }}
|
||||
>
|
||||
<span>✨</span>
|
||||
Generate with AI
|
||||
</button>
|
||||
<p style={{ color: "var(--text-muted)", fontSize: 11, textAlign: "center", margin: "6px 0 0" }}>
|
||||
<p className="agent-dialog-ai-hint">
|
||||
Describe your agent's role and let AI generate a specification
|
||||
</p>
|
||||
</div>
|
||||
@@ -254,7 +249,7 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
<div className="agent-dialog-field">
|
||||
<label>Model</label>
|
||||
{modelsLoading ? (
|
||||
<div style={{ color: "var(--text-muted)", fontSize: 13, padding: "8px 0" }}>Loading models…</div>
|
||||
<div className="agent-dialog-loading">Loading models…</div>
|
||||
) : (
|
||||
<CustomModelDropdown
|
||||
id="agent-model"
|
||||
@@ -277,7 +272,6 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
className="select"
|
||||
value={runtimeConfig.thinkingLevel}
|
||||
onChange={e => setRuntimeConfig(c => ({ ...c, thinkingLevel: e.target.value as ThinkingLevel }))}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<option value="off">Off</option>
|
||||
<option value="minimal">Minimal</option>
|
||||
@@ -296,7 +290,6 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
max={500}
|
||||
value={runtimeConfig.maxTurns}
|
||||
onChange={e => setRuntimeConfig(c => ({ ...c, maxTurns: Math.max(1, parseInt(e.target.value, 10) || 1) }))}
|
||||
style={{ width: "100%", boxSizing: "border-box" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,30 +297,30 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
|
||||
{step === 2 && (
|
||||
<div>
|
||||
<p style={{ color: "var(--text-muted)", fontSize: 13, marginTop: 0, marginBottom: 12 }}>
|
||||
<p className="agent-dialog-info">
|
||||
Review your agent configuration before creating.
|
||||
</p>
|
||||
<div className="agent-dialog-summary">
|
||||
<div className="agent-dialog-summary-row">
|
||||
<span style={{ color: "var(--text-muted)", fontSize: 13 }}>Name</span>
|
||||
<span style={{ fontWeight: 600 }}>
|
||||
{icon && <span style={{ marginRight: 6 }}>{icon}</span>}
|
||||
<span className="agent-dialog-summary-row-label">Name</span>
|
||||
<span className="agent-dialog-summary-row-value">
|
||||
{icon && <span className="agent-dialog-icon-prefix">{icon}</span>}
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
{title && (
|
||||
<div className="agent-dialog-summary-row">
|
||||
<span style={{ color: "var(--text-muted)", fontSize: 13 }}>Title</span>
|
||||
<span className="agent-dialog-summary-row-label">Title</span>
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="agent-dialog-summary-row">
|
||||
<span style={{ color: "var(--text-muted)", fontSize: 13 }}>Role</span>
|
||||
<span className="agent-dialog-summary-row-label">Role</span>
|
||||
<span>{selectedRole?.icon} {selectedRole?.label}</span>
|
||||
</div>
|
||||
<div className="agent-dialog-summary-row">
|
||||
<span style={{ color: "var(--text-muted)", fontSize: 13 }}>Model</span>
|
||||
<span style={{ fontSize: 13 }}>
|
||||
<span className="agent-dialog-summary-row-label">Model</span>
|
||||
<span>
|
||||
{selectedModel ? (
|
||||
<>
|
||||
<ProviderIcon provider={selectedModel.split("/")[0]} size="sm" />
|
||||
@@ -341,21 +334,21 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
})()}
|
||||
</>
|
||||
) : (
|
||||
<em style={{ color: "var(--text-muted)" }}>default</em>
|
||||
<em className="agent-dialog-summary-row-value--muted">default</em>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="agent-dialog-summary-row">
|
||||
<span style={{ color: "var(--text-muted)", fontSize: 13 }}>Thinking</span>
|
||||
<span style={{ textTransform: "capitalize" }}>{runtimeConfig.thinkingLevel}</span>
|
||||
<span className="agent-dialog-summary-row-label">Thinking</span>
|
||||
<span className="agent-dialog-summary-row-value--capitalize">{runtimeConfig.thinkingLevel}</span>
|
||||
</div>
|
||||
<div className="agent-dialog-summary-row">
|
||||
<span style={{ color: "var(--text-muted)", fontSize: 13 }}>Max Turns</span>
|
||||
<span className="agent-dialog-summary-row-label">Max Turns</span>
|
||||
<span>{runtimeConfig.maxTurns}</span>
|
||||
</div>
|
||||
</div>
|
||||
{error && (
|
||||
<p style={{ color: "var(--state-error-text, #f85149)", fontSize: 13, marginTop: 12 }}>{error}</p>
|
||||
<p className="agent-dialog-error">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -390,7 +383,6 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI-assisted agent generation modal */}
|
||||
<AgentGenerationModal
|
||||
@@ -399,6 +391,6 @@ export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAge
|
||||
onGenerated={handleGenerated}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ describe("AgentDetailView", () => {
|
||||
expect(screen.getByText(/Loading agent/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("defines CSS variables for agent state tokens in the style block", async () => {
|
||||
it("defines CSS variables for agent state tokens in the global stylesheet", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
@@ -90,15 +90,18 @@ describe("AgentDetailView", () => {
|
||||
expect(headings.some(h => h.textContent === "Test Agent")).toBe(true);
|
||||
});
|
||||
|
||||
// Verify state CSS variables are defined in the component's style block
|
||||
const styleElements = document.querySelectorAll("style");
|
||||
const allCss = Array.from(styleElements).map(el => el.textContent ?? "").join("");
|
||||
expect(allCss).toContain("--state-idle-bg");
|
||||
expect(allCss).toContain("--state-active-bg");
|
||||
expect(allCss).toContain("--state-paused-bg");
|
||||
expect(allCss).toContain("--state-error-bg");
|
||||
expect(allCss).toContain("--state-idle-text");
|
||||
expect(allCss).toContain("--state-active-text");
|
||||
// Verify state CSS variables are defined in the global stylesheet (styles.css)
|
||||
// (previously these were in inline style blocks, now they're in the global :root)
|
||||
const fs = await import("fs");
|
||||
const path = await import("path");
|
||||
const stylesPath = path.join(__dirname, "../../styles.css");
|
||||
const stylesContent = fs.readFileSync(stylesPath, "utf-8");
|
||||
expect(stylesContent).toContain("--state-idle-bg:");
|
||||
expect(stylesContent).toContain("--state-active-bg:");
|
||||
expect(stylesContent).toContain("--state-paused-bg:");
|
||||
expect(stylesContent).toContain("--state-error-bg:");
|
||||
expect(stylesContent).toContain("--state-idle-text:");
|
||||
expect(stylesContent).toContain("--state-active-text:");
|
||||
});
|
||||
|
||||
it("uses token-based state colors for badges instead of hardcoded hex", async () => {
|
||||
@@ -155,7 +158,7 @@ describe("AgentDetailView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses token-based color references in CSS instead of undefined vars", async () => {
|
||||
it("uses token-based color references for success and error states", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
@@ -168,20 +171,20 @@ describe("AgentDetailView", () => {
|
||||
expect(screen.getAllByText("active").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Navigate to Runs tab to trigger rendering of run-related style blocks
|
||||
// Navigate to Runs tab to trigger rendering of run-related content
|
||||
fireEvent.click(screen.getByText("Runs"));
|
||||
|
||||
// Verify style blocks use --color-success and --color-error with fallbacks
|
||||
// (not bare --success or --error which are undefined in the root CSS)
|
||||
await waitFor(() => {
|
||||
const styleElements = document.querySelectorAll("style");
|
||||
const allCss = Array.from(styleElements).map(el => el.textContent ?? "").join("");
|
||||
expect(allCss).toMatch(/var\(--color-success/);
|
||||
expect(allCss).toMatch(/var\(--color-error/);
|
||||
});
|
||||
// Verify that the global stylesheet defines --color-success and --color-error
|
||||
// (previously checked in inline style blocks, now verified by reading styles.css)
|
||||
const fs = await import("fs");
|
||||
const path = await import("path");
|
||||
const stylesPath = path.join(__dirname, "../../styles.css");
|
||||
const stylesContent = fs.readFileSync(stylesPath, "utf-8");
|
||||
expect(stylesContent).toMatch(/--color-success:/);
|
||||
expect(stylesContent).toMatch(/--color-error:/);
|
||||
});
|
||||
|
||||
it("defines component-local aliases for undefined CSS tokens", async () => {
|
||||
it("uses global design tokens instead of component-local aliases", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
@@ -194,14 +197,18 @@ describe("AgentDetailView", () => {
|
||||
expect(screen.getAllByText("active").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Verify that component-local aliases are defined for tokens used in the style block
|
||||
// These map to real global tokens so they don't fall back to browser defaults
|
||||
const styleElements = document.querySelectorAll("style");
|
||||
const allCss = Array.from(styleElements).map(el => el.textContent ?? "").join("");
|
||||
expect(allCss).toContain("--bg-primary: var(--surface");
|
||||
expect(allCss).toContain("--accent: var(--todo");
|
||||
expect(allCss).toContain("--text-primary: var(--text");
|
||||
expect(allCss).toContain("--bg-hover: var(--card-hover");
|
||||
// Previously the component defined local aliases like --bg-primary, --accent, etc.
|
||||
// Now these are replaced with direct global token references in the CSS classes.
|
||||
// Verify the global stylesheet defines the real tokens that the component uses.
|
||||
const fs = await import("fs");
|
||||
const path = await import("path");
|
||||
const stylesPath = path.join(__dirname, "../../styles.css");
|
||||
const stylesContent = fs.readFileSync(stylesPath, "utf-8");
|
||||
// The component classes now use --surface, --todo, --text, --card-hover directly
|
||||
expect(stylesContent).toMatch(/--surface:/);
|
||||
expect(stylesContent).toMatch(/--todo:/);
|
||||
expect(stylesContent).toMatch(/--text:/);
|
||||
expect(stylesContent).toMatch(/--card-hover:/);
|
||||
});
|
||||
|
||||
it("displays agent name in header after loading", async () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user