summaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-27 11:04:20 +0000
committerGitHub <noreply@github.com>2026-01-27 11:04:20 +0000
commitc618174e60e4632d36d7352d83399508c72b2f42 (patch)
treefbca74b921a57165aea046b959a44ab00589532f /frontend/src/components
parentb6f239c19f0d3130515f3745f842e17a69212295 (diff)
downloadsoryu-c618174e60e4632d36d7352d83399508c72b2f42.tar.gz
soryu-c618174e60e4632d36d7352d83399508c72b2f42.zip
Add Red Team CLI command and frontend UI (#39)
* Add Red Team CLI command and frontend UI Backend additions: - Add `makima red-team notify` CLI command for red team tasks - Add RedTeamCommand enum with Notify subcommand - Add red_team API client module for notify endpoint - Add RedTeamNotifyArgs with severity, task, file, context options Frontend additions: - Add ContractCreateModal with red team toggle and prompt input - Update ContractDetail with red-team tab for notifications - Update ContractList with red team enabled badge - Add TypeScript types for RedTeamNotification and related interfaces Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add CSS styles for Red Team frontend components Add comprehensive styling for: - Contract list and detail containers - Red team badge styling with gradient backgrounds - Tab navigation with red team specific styling - Red team notifications panel with severity indicators - Contract creation modal form elements - Task badges for supervisor and red team roles Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix missing local_only field in TUI CreateContractRequest Add the missing local_only field to the CreateContractRequest struct initialization in the TUI contract creation handler. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * [WIP] Heartbeat checkpoint - 2026-01-27 03:07:28 UTC --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/ContractCreateModal.tsx185
-rw-r--r--frontend/src/components/ContractDetail.tsx139
-rw-r--r--frontend/src/components/ContractList.tsx61
3 files changed, 363 insertions, 22 deletions
diff --git a/frontend/src/components/ContractCreateModal.tsx b/frontend/src/components/ContractCreateModal.tsx
new file mode 100644
index 0000000..e1d9732
--- /dev/null
+++ b/frontend/src/components/ContractCreateModal.tsx
@@ -0,0 +1,185 @@
+import React, { useState } from 'react'
+
+interface ContractCreateModalProps {
+ isOpen: boolean
+ onClose: () => void
+ onCreated: () => void
+}
+
+interface CreateContractForm {
+ name: string
+ description: string
+ contractType: string
+ redTeamEnabled: boolean
+ redTeamPrompt: string
+}
+
+export function ContractCreateModal({ isOpen, onClose, onCreated }: ContractCreateModalProps) {
+ const [form, setForm] = useState<CreateContractForm>({
+ name: '',
+ description: '',
+ contractType: 'simple',
+ redTeamEnabled: false,
+ redTeamPrompt: '',
+ })
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState<string | null>(null)
+
+ if (!isOpen) return null
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setLoading(true)
+ setError(null)
+
+ try {
+ const response = await fetch('/api/v1/contracts', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ name: form.name,
+ description: form.description || undefined,
+ contract_type: form.contractType,
+ red_team_enabled: form.redTeamEnabled,
+ red_team_prompt: form.redTeamEnabled && form.redTeamPrompt ? form.redTeamPrompt : undefined,
+ }),
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}))
+ throw new Error(errorData.message || `Failed to create contract: ${response.statusText}`)
+ }
+
+ // Reset form and close modal
+ setForm({
+ name: '',
+ description: '',
+ contractType: 'simple',
+ redTeamEnabled: false,
+ redTeamPrompt: '',
+ })
+ onCreated()
+ onClose()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unknown error')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+ <div className="modal-overlay" onClick={onClose}>
+ <div className="settings-modal contract-create-modal" onClick={(e) => e.stopPropagation()}>
+ <div className="modal-header">
+ <h2 className="modal-title">Create Contract</h2>
+ <button className="modal-close-btn" onClick={onClose}>
+ ×
+ </button>
+ </div>
+
+ <form onSubmit={handleSubmit}>
+ <div className="modal-content">
+ {error && (
+ <div className="form-error">
+ {error}
+ </div>
+ )}
+
+ <div className="settings-section">
+ <h3>Contract Details</h3>
+
+ <div className="setting-item">
+ <label>
+ Name
+ <input
+ type="text"
+ value={form.name}
+ onChange={(e) => setForm({ ...form, name: e.target.value })}
+ placeholder="Enter contract name"
+ required
+ className="form-input"
+ />
+ </label>
+ </div>
+
+ <div className="setting-item">
+ <label>
+ Description
+ <textarea
+ value={form.description}
+ onChange={(e) => setForm({ ...form, description: e.target.value })}
+ placeholder="Enter contract description (optional)"
+ className="form-textarea"
+ rows={3}
+ />
+ </label>
+ </div>
+
+ <div className="setting-item">
+ <label>
+ Contract Type
+ <select
+ value={form.contractType}
+ onChange={(e) => setForm({ ...form, contractType: e.target.value })}
+ className="form-select"
+ >
+ <option value="simple">Simple</option>
+ <option value="specification">Specification</option>
+ <option value="execute">Execute</option>
+ </select>
+ </label>
+ </div>
+ </div>
+
+ <div className="settings-section">
+ <h3>Red Team Monitoring</h3>
+
+ <div className="setting-item">
+ <label className="checkbox-label">
+ <input
+ type="checkbox"
+ checked={form.redTeamEnabled}
+ onChange={(e) => setForm({ ...form, redTeamEnabled: e.target.checked })}
+ />
+ <span>Enable Red Team monitoring</span>
+ </label>
+ <div className="setting-description">
+ Spawns a parallel task that monitors work output for quality and compliance
+ </div>
+ </div>
+
+ {form.redTeamEnabled && (
+ <div className="setting-item red-team-prompt-container">
+ <label>
+ Custom review criteria (optional)
+ <textarea
+ value={form.redTeamPrompt}
+ onChange={(e) => setForm({ ...form, redTeamPrompt: e.target.value })}
+ placeholder="Enter custom criteria for the red team to evaluate against... (e.g., 'Ensure all functions have proper error handling', 'Verify security best practices are followed')"
+ className="form-textarea"
+ rows={4}
+ />
+ </label>
+ <div className="setting-description">
+ Provide specific criteria for the red team to focus on during reviews
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+
+ <div className="modal-footer">
+ <button type="button" className="modal-btn secondary" onClick={onClose} disabled={loading}>
+ Cancel
+ </button>
+ <button type="submit" className="modal-btn primary" disabled={loading || !form.name.trim()}>
+ {loading ? 'Creating...' : 'Create Contract'}
+ </button>
+ </div>
+ </form>
+ </div>
+ </div>
+ )
+}
diff --git a/frontend/src/components/ContractDetail.tsx b/frontend/src/components/ContractDetail.tsx
index 72527ce..a9dd550 100644
--- a/frontend/src/components/ContractDetail.tsx
+++ b/frontend/src/components/ContractDetail.tsx
@@ -12,6 +12,8 @@ interface TaskSummary {
id: string
name: string
status: string
+ is_supervisor?: boolean
+ is_red_team?: boolean
}
interface ContractRepository {
@@ -30,6 +32,24 @@ interface Contract {
status: string
version: number
created_at: string
+ red_team_enabled?: boolean
+ red_team_prompt?: string
+}
+
+interface RedTeamNotification {
+ id: string
+ contract_id: string
+ red_team_task_id: string
+ related_task_id?: string
+ message: string
+ severity: 'info' | 'warning' | 'critical'
+ file_path?: string
+ context?: string
+ delivered: boolean
+ delivered_at?: string
+ acknowledged: boolean
+ acknowledged_at?: string
+ created_at: string
}
interface ContractWithRelations {
@@ -39,7 +59,7 @@ interface ContractWithRelations {
tasks: TaskSummary[]
}
-type Tab = 'overview' | 'files' | 'tasks' | 'repositories'
+type Tab = 'overview' | 'files' | 'tasks' | 'repositories' | 'red-team'
export function ContractDetail() {
const { id } = useParams<{ id: string }>()
@@ -47,6 +67,8 @@ export function ContractDetail() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<Tab>('overview')
+ const [notifications, setNotifications] = useState<RedTeamNotification[]>([])
+ const [notificationsLoading, setNotificationsLoading] = useState(false)
useEffect(() => {
async function fetchContract() {
@@ -70,6 +92,28 @@ export function ContractDetail() {
fetchContract()
}, [id])
+ // Fetch red team notifications when viewing red-team tab
+ useEffect(() => {
+ async function fetchNotifications() {
+ if (!id || activeTab !== 'red-team' || !data?.contract.red_team_enabled) return
+
+ try {
+ setNotificationsLoading(true)
+ const response = await fetch(`/api/v1/contracts/${id}/red-team/notifications`)
+ if (response.ok) {
+ const notificationData = await response.json()
+ setNotifications(notificationData.notifications || [])
+ }
+ } catch (err) {
+ console.error('Failed to fetch red team notifications:', err)
+ } finally {
+ setNotificationsLoading(false)
+ }
+ }
+
+ fetchNotifications()
+ }, [id, activeTab, data?.contract.red_team_enabled])
+
if (loading) {
return (
<div className="contract-detail-container">
@@ -108,7 +152,14 @@ export function ContractDetail() {
<Link to="/contracts" className="back-link">
Back to Contracts
</Link>
- <h1 className="contract-title">{contract.name}</h1>
+ <h1 className="contract-title">
+ {contract.name}
+ {contract.red_team_enabled && (
+ <span className="red-team-badge" title="Red Team monitoring enabled">
+ đŸ›Ąī¸ Red Team
+ </span>
+ )}
+ </h1>
{contract.description && (
<p className="contract-description">{contract.description}</p>
)}
@@ -144,6 +195,14 @@ export function ContractDetail() {
>
Repositories ({repositories.length})
</button>
+ {contract.red_team_enabled && (
+ <button
+ className={`tab-button red-team-tab ${activeTab === 'red-team' ? 'active' : ''}`}
+ onClick={() => setActiveTab('red-team')}
+ >
+ đŸ›Ąī¸ Red Team
+ </button>
+ )}
</div>
<div className="contract-tab-content">
@@ -194,8 +253,12 @@ export function ContractDetail() {
) : (
<ul className="task-list">
{tasks.map((task) => (
- <li key={task.id} className="task-item">
- <h3>{task.name}</h3>
+ <li key={task.id} className={`task-item ${task.is_red_team ? 'red-team-task' : ''}`}>
+ <h3>
+ {task.is_red_team && <span className="task-badge red-team">đŸ›Ąī¸</span>}
+ {task.is_supervisor && <span className="task-badge supervisor">👔</span>}
+ {task.name}
+ </h3>
<span className={`task-status status-${task.status}`}>
{task.status}
</span>
@@ -226,6 +289,74 @@ export function ContractDetail() {
)}
</div>
)}
+
+ {activeTab === 'red-team' && contract.red_team_enabled && (
+ <div className="tab-panel red-team-panel">
+ <h2>đŸ›Ąī¸ Red Team Monitoring</h2>
+
+ {contract.red_team_prompt && (
+ <div className="red-team-prompt">
+ <h3>Review Criteria</h3>
+ <p>{contract.red_team_prompt}</p>
+ </div>
+ )}
+
+ <div className="red-team-status">
+ <h3>Red Team Task</h3>
+ {(() => {
+ const redTeamTask = tasks.find(t => t.is_red_team)
+ if (redTeamTask) {
+ return (
+ <div className="red-team-task-info">
+ <span className="task-name">{redTeamTask.name}</span>
+ <span className={`task-status status-${redTeamTask.status}`}>
+ {redTeamTask.status}
+ </span>
+ </div>
+ )
+ }
+ return <p className="no-task">Red team task not yet spawned</p>
+ })()}
+ </div>
+
+ <div className="red-team-notifications">
+ <h3>Alerts ({notifications.length})</h3>
+ {notificationsLoading ? (
+ <p>Loading notifications...</p>
+ ) : notifications.length === 0 ? (
+ <p className="no-alerts">No alerts from red team</p>
+ ) : (
+ <ul className="notification-list">
+ {notifications.map((notification) => (
+ <li
+ key={notification.id}
+ className={`notification-item severity-${notification.severity}`}
+ >
+ <div className="notification-header">
+ <span className={`severity-badge ${notification.severity}`}>
+ {notification.severity === 'critical' && '🚨'}
+ {notification.severity === 'warning' && 'âš ī¸'}
+ {notification.severity === 'info' && 'â„šī¸'}
+ {notification.severity.toUpperCase()}
+ </span>
+ <span className="notification-time">
+ {new Date(notification.created_at).toLocaleString()}
+ </span>
+ </div>
+ <p className="notification-message">{notification.message}</p>
+ {notification.file_path && (
+ <span className="notification-file">File: {notification.file_path}</span>
+ )}
+ {notification.context && (
+ <pre className="notification-context">{notification.context}</pre>
+ )}
+ </li>
+ ))}
+ </ul>
+ )}
+ </div>
+ </div>
+ )}
</div>
</div>
)
diff --git a/frontend/src/components/ContractList.tsx b/frontend/src/components/ContractList.tsx
index 77012db..253b44f 100644
--- a/frontend/src/components/ContractList.tsx
+++ b/frontend/src/components/ContractList.tsx
@@ -1,5 +1,6 @@
-import React, { useEffect, useState } from 'react'
+import React, { useEffect, useState, useCallback } from 'react'
import { Link } from 'react-router-dom'
+import { ContractCreateModal } from './ContractCreateModal'
interface ContractSummary {
id: string
@@ -12,32 +13,35 @@ interface ContractSummary {
task_count: number
repository_count: number
created_at: string
+ // Red team fields
+ red_team_enabled?: boolean
}
export function ContractList() {
const [contracts, setContracts] = useState<ContractSummary[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
+ const [showCreateModal, setShowCreateModal] = useState(false)
- useEffect(() => {
- async function fetchContracts() {
- try {
- setLoading(true)
- const response = await fetch('/api/v1/contracts')
- if (!response.ok) {
- throw new Error(`Failed to fetch contracts: ${response.statusText}`)
- }
- const data = await response.json()
- setContracts(data.contracts || [])
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Unknown error')
- } finally {
- setLoading(false)
+ const fetchContracts = useCallback(async () => {
+ try {
+ setLoading(true)
+ const response = await fetch('/api/v1/contracts')
+ if (!response.ok) {
+ throw new Error(`Failed to fetch contracts: ${response.statusText}`)
}
+ const data = await response.json()
+ setContracts(data.contracts || [])
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Unknown error')
+ } finally {
+ setLoading(false)
}
+ }, [])
+ useEffect(() => {
fetchContracts()
- }, [])
+ }, [fetchContracts])
if (loading) {
return (
@@ -57,7 +61,15 @@ export function ContractList() {
return (
<div className="contract-list-container">
- <h1>Contracts</h1>
+ <div className="contract-list-header">
+ <h1>Contracts</h1>
+ <button
+ className="create-contract-btn"
+ onClick={() => setShowCreateModal(true)}
+ >
+ + New Contract
+ </button>
+ </div>
{contracts.length === 0 ? (
<p>No contracts found</p>
) : (
@@ -65,7 +77,14 @@ export function ContractList() {
{contracts.map((contract) => (
<li key={contract.id} className="contract-item">
<Link to={`/contracts/${contract.id}`}>
- <h2>{contract.name}</h2>
+ <h2>
+ {contract.name}
+ {contract.red_team_enabled && (
+ <span className="red-team-badge" title="Red Team monitoring enabled">
+ 🔍
+ </span>
+ )}
+ </h2>
{contract.description && <p>{contract.description}</p>}
<div className="contract-meta">
<span>Phase: {contract.phase}</span>
@@ -78,6 +97,12 @@ export function ContractList() {
))}
</ul>
)}
+
+ <ContractCreateModal
+ isOpen={showCreateModal}
+ onClose={() => setShowCreateModal(false)}
+ onCreated={fetchContracts}
+ />
</div>
)
}