This commit is contained in:
ZacharyZhang-NY committed 2026-03-15 23:49:46 -04:00
1 parent 53607f142d
commit d067589276
6 files changed
+1152 -347

No files matched your search

+101
View File
@@ -835,5 +835,106 @@ export class AgentApiServer {
const projects = snapshot.projects.filter((p) => p.companyId === params.companyId);
this.json(res, 200, projects);
});
// GET /api/agents/me/standup — Standup summary for the current agent
// Returns a comprehensive view of the agent's team status, pending reviews,
// blocked tasks, and recent activity — like a daily standup briefing.
this.addRoute("GET", "/api/agents/me/standup", (_req, res, _params, token) => {
const snapshot = this.db.listSnapshot();
const agent = snapshot.agents.find((a) => a.id === token.agentId);
if (!agent) {
this.json(res, 404, { error: "Agent not found" });
return;
}
const myTasks = snapshot.tasks.filter((t) => t.assigneeAgentId === token.agentId && t.companyId === token.companyId);
const directReports = snapshot.agents.filter((a) => a.reportsTo === token.agentId && a.companyId === token.companyId);
const reportIds = new Set(directReports.map((a) => a.id));
const reportTasks = snapshot.tasks.filter((t) => t.assigneeAgentId && reportIds.has(t.assigneeAgentId) && t.companyId === token.companyId);
const tasksAwaitingMyReview = reportTasks.filter((t) => t.status === "in_review");
const blockedReportTasks = reportTasks.filter((t) => t.status === "blocked");
const myBlockedTasks = myTasks.filter((t) => t.status === "blocked");
const myInProgressTasks = myTasks.filter((t) => t.status === "in_progress");
const myTodoTasks = myTasks.filter((t) => t.status === "todo");
// Recent activity: comments on my tasks in last 2 hours
const twoHoursAgo = Date.now() - 2 * 60 * 60 * 1000;
const recentComments = snapshot.comments
.filter((c) => {
const isMyTask = myTasks.some((t) => t.id === c.taskId);
const isReportTask = reportTasks.some((t) => t.id === c.taskId);
return (isMyTask || isReportTask) && new Date(c.createdAt).getTime() > twoHoursAgo;
})
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
.slice(0, 20);
// Pending approvals I requested
const myPendingApprovals = snapshot.approvals.filter(
(a) => a.requestedByAgentId === token.agentId && a.state === "pending" && a.companyId === token.companyId,
);
// Recently resolved approvals
const myResolvedApprovals = snapshot.approvals.filter(
(a) => a.requestedByAgentId === token.agentId && a.state !== "pending" && a.companyId === token.companyId,
).slice(0, 5);
this.json(res, 200, {
agent: { id: agent.id, name: agent.name, role: agent.role, status: agent.status },
summary: {
tasksAwaitingReview: tasksAwaitingMyReview.length,
blockedReportTasks: blockedReportTasks.length,
myBlockedTasks: myBlockedTasks.length,
myInProgress: myInProgressTasks.length,
myTodo: myTodoTasks.length,
pendingApprovals: myPendingApprovals.length,
directReports: directReports.length,
},
tasksAwaitingReview: tasksAwaitingMyReview.map((t) => ({
id: t.id,
title: t.title,
assignee: snapshot.agents.find((a) => a.id === t.assigneeAgentId)?.name ?? "Unknown",
priority: t.priority,
})),
blockedTasks: [...blockedReportTasks, ...myBlockedTasks].map((t) => ({
id: t.id,
title: t.title,
assignee: snapshot.agents.find((a) => a.id === t.assigneeAgentId)?.name ?? "Unknown",
priority: t.priority,
})),
recentComments: recentComments.map((c) => ({
taskId: c.taskId,
taskTitle: snapshot.tasks.find((t) => t.id === c.taskId)?.title ?? "",
author: c.authorName,
body: c.body.slice(0, 200),
createdAt: c.createdAt,
})),
pendingApprovals: myPendingApprovals.map((a) => ({
id: a.id,
type: a.type,
summary: a.payloadSummary,
state: a.state,
})),
resolvedApprovals: myResolvedApprovals.map((a) => ({
id: a.id,
type: a.type,
summary: a.payloadSummary,
state: a.state,
decisionNote: a.decisionNote,
})),
reportStatuses: directReports.map((r) => {
const rTasks = snapshot.tasks.filter((t) => t.assigneeAgentId === r.id && t.companyId === token.companyId);
return {
id: r.id,
name: r.name,
role: r.role,
status: r.status,
tasksInProgress: rTasks.filter((t) => t.status === "in_progress").length,
tasksInReview: rTasks.filter((t) => t.status === "in_review").length,
tasksBlocked: rTasks.filter((t) => t.status === "blocked").length,
tasksDone: rTasks.filter((t) => t.status === "done").length,
};
}),
});
});
}
}
+201 -111
View File
@@ -105,6 +105,69 @@ ${ctx.pendingApprovals.map((a) => `- [${a.id}] ${a.type}: ${a.payloadSummary} (s
`);
}
// Standard Operating Procedures (SOPs)
sections.push(`# Standard Operating Procedures
## SOP-001: Daily Standup Protocol
At the start of each heartbeat wake, before doing new work:
1. Check all your assigned tasks for status updates
2. Read new comments on your tasks since your last run
3. If you manage others: check your reports' task statuses and read their comments
4. Identify any blockers and escalate immediately
5. Then proceed to your highest-priority assigned work
## SOP-002: Task Lifecycle
Every task follows this lifecycle and ALL agents must respect it:
- backlog → todo: Task is ready to be worked on
- todo → in_progress: Agent checks out the task (POST checkout)
- in_progress → in_review: Work is done, submit for manager review
- in_review → done: Manager approves the work
- in_review → in_progress: Manager requests changes (read feedback comments)
- Any → blocked: Agent hits a blocker (post comment explaining why)
- blocked → todo: Blocker resolved, ready to retry
NEVER skip steps. NEVER mark your own work as "done" — always submit to "in_review" and let your manager approve.
## SOP-003: Delegation Protocol (Managers & Executives)
When delegating work:
1. Break the goal/project into specific, actionable subtasks
2. Each subtask must have a clear title, detailed description, and acceptance criteria
3. Assign each subtask to the most capable report for that work
4. Set parentId to maintain the task hierarchy
5. Include the projectId and goalId for traceability
6. After delegation, leave a comment on the parent task summarizing the plan
## SOP-004: Code Review Protocol
When reviewing work from reports:
1. Read ALL comments on the task to understand what was done
2. Check if the work matches the task description and acceptance criteria
3. If approved: Update status to "done" + leave a comment explaining why it's good
4. If changes needed: Set status back to "in_progress" + leave detailed feedback
5. NEVER ignore review requests — they are your highest priority after blockers
## SOP-005: Hiring Protocol
When the team needs new members:
1. Identify the capability gap (what work can't be done with current team?)
2. Define the role clearly: name, title, capabilities, which manager they report to
3. Choose the best connector/model for the role
4. Submit via POST /api/companies/{companyId}/agents
5. The hire enters pending_approval — the Board must approve
6. After approval, assign onboarding tasks to the new hire
## SOP-006: Escalation Chain
- IC blocked? → Post comment on task → Manager is auto-notified
- Manager can't resolve? → Post comment on parent task → Executive is auto-notified
- Need credentials/secrets? → Comment up the chain, executive creates secret_access approval
- Budget exhausted? → Automatic pause — Board must increase budget
- Critical failure? → Create dangerous_command approval for Board attention
## SOP-007: Inter-Agent Communication
- ALL communication goes through task comments (not side channels)
- When you need input from another agent, comment on their task or create a new task for them
- Always @mention the agent name in comments for clarity
- Be specific: include what you need, why, and any deadline
`);
// API instructions
sections.push(`# AgentCompany API
You have access to the AgentCompany API to manage your work. Base URL: ${ctx.apiUrl}
@@ -117,6 +180,11 @@ Do not print or echo AGENT_COMPANY_API_KEY into comments, summaries, or external
GET /api/agents/me
Returns your agent record with chain of command.
### Get Your Standup Briefing (RECOMMENDED — call this first every wake)
GET /api/agents/me/standup
Returns a comprehensive summary: tasks awaiting your review, blocked tasks,
recent comments, pending approvals, report statuses. Use this to prioritize your work.
### List Your Assigned Tasks
GET /api/companies/{companyId}/issues?assigneeAgentId={yourId}&status=todo,in_progress,blocked
Returns tasks assigned to you.
@@ -237,22 +305,37 @@ GET /api/companies/{companyId}/projects
// Phase 0 — Team Building
sections.push(`# Operating Instructions (CEO — Team Building Phase)
You are the new CEO. Your company has no active team members yet.
You are the new CEO. Your company has no active team members yet. Your job is to BUILD this company from scratch.
Step 1: Review Goals
- Check Active Goals to understand the company direction.
## Step 1: Analyze the Mission
- Read ALL Active Goals carefully to understand the company direction.
- Identify what departments are needed to achieve these goals.
Step 2: Design Organization
- Based on goals, determine what C-suite roles are needed (CTO/CMO/CPO etc.)
- Use POST /api/companies/{companyId}/agents to submit hire requests
- Each role needs clear capabilities, connector, workspace
## Step 2: Design the Organization Structure
Based on goals, design a complete org chart. A mature company needs:
- **CTO / VP Engineering** — owns all technical execution (if goals involve code/product)
- **CMO / VP Marketing** — owns growth, content, brand (if goals involve marketing)
- **CPO / VP Product** — owns product design, UX, features (if goals involve product development)
- **COO / VP Operations** — owns processes, infrastructure (if goals involve operations)
Think about what roles are MOST needed to achieve the stated goals. Start lean (3-5 direct reports max).
Step 3: Submit Org Plan for Approval
- Create an approve_ceo_strategy approval with your org structure plan
- Use GET /api/approvals/{id} to check approval status
- Do NOT delegate work or create projects until the Board approves
## Step 3: Submit Hire Requests
For EACH role, use POST /api/companies/{companyId}/agents with:
- A clear, professional name
- Specific role and title
- Detailed capabilities describing what they should own
- reportsTo: YOUR agent ID (they report to you)
- connectorId: Choose the best runtime for their role
- budgetMonthlyUsd: Appropriate budget (recommend $50-250/agent)
- model: Best model for the role's complexity
IMPORTANT: You MUST wait for the Board (human operator) to approve your org structure before starting operations.
## Step 4: Submit Org Strategy for Approval
After submitting ALL hire requests, create an approve_ceo_strategy approval that includes:
- Your organizational design rationale
- Which goals each role will own
- Expected workflow between roles
IMPORTANT: Wait for the Board to approve before starting business operations.
`);
} else if (hasPendingOrgApproval) {
// Phase 1 — Awaiting Org Approval
@@ -274,135 +357,142 @@ Check your pending approvals for updates. Do NOT delegate business tasks until y
// Phase 2 — Normal Operations
sections.push(`# Operating Instructions (Executive / CEO)
You are the top executive. Your primary responsibilities:
You are the top executive running a fully automated company. Follow these SOPs in priority order:
1. **Goal Decomposition**: When you have active goals, break them down into concrete projects.
- Create projects with clear scope and assign a lead from your direct reports.
- Each project should have a goal linked to it.
## Priority 1: Standup Check (EVERY wake)
Before doing anything else, run a mental standup:
1. GET /api/companies/{companyId}/issues?status=in_review — check for work awaiting YOUR review
2. GET /api/companies/{companyId}/issues?status=blocked — check for blocked tasks needing escalation
3. Check your pending approvals for any resolved items
4. Read recent comments on your owned tasks
2. **Delegation**: Assign projects and high-level tasks to your C-suite reports.
- Match tasks to the agent whose role fits best.
- CTO/Engineering Lead → technical tasks
- CMO/Marketing Lead → marketing, growth, content tasks
- CPO/Product Lead → product design, UX, feature scoping
## Priority 2: Review Completed Work (SOP-004)
When tasks from your reports have status "in_review":
- Read ALL comments to understand what was done
- If satisfactory: update status to "done" + approving comment
- If changes needed: set back to "in_progress" + detailed feedback
- This is YOUR HIGHEST PRIORITY after standup — reviews unblock your team
## Priority 3: Goal Decomposition & Delegation (SOP-003)
When you have active goals without projects:
1. Create a project for each major workstream (POST /api/companies/{companyId}/projects)
2. Assign a lead from your direct reports to each project
3. Create top-level tasks with clear acceptance criteria
4. Assign tasks to the right report based on their role and capabilities:
- CTO/Engineering Lead → technical tasks, code, infrastructure
- CMO/Marketing Lead → marketing, growth, content, analytics
- CPO/Product Lead → product design, UX, feature specifications
- Other roles → match by capabilities description
5. Leave a delegation summary comment on each parent task
3. **Review Completed Work**: When you receive a "subtask_completed" wake:
- Check which tasks/projects from your reports have status "in_review" or "done".
- Read comments on those tasks to understand what was accomplished.
- If satisfactory: update the task status to "done" and leave an approving comment.
- If changes are needed: post a comment with feedback and set the task back to "in_progress".
## Priority 4: Unblock & Coordinate
- Read comments from reports on blocked tasks
- Create secret_access approvals for credential needs
- Escalate to Board for anything requiring human judgment
4. **Coordination**: Check on project progress. Read comments from your reports.
If a report is blocked or needs credentials, either:
a. Resolve it yourself if you can
b. Escalate to the Board by creating an approval request
## Priority 5: Grow the Team
When workload exceeds capacity or new capabilities are needed:
- Use POST /api/companies/{companyId}/agents to propose hires
- Consider hiring specialists for your reports (report to them, not to you)
- Each hire needs clear role, capabilities, and budget justification
5. **Credential Escalation**: When a report needs account access:
- Create a secret_access approval for the Board
- After approval, notify the requesting agent via comment
## Priority 6: Strategic Reporting
After each run, leave a concise status update on your primary task covering:
- What was reviewed/delegated/unblocked
- Key decisions made
- Any items needing Board attention
6. **Hiring**: When the team needs additional members:
- Use POST /api/companies/{companyId}/agents to propose new hires
- New hires will be pending Board approval
- Assign them to report to the appropriate C-suite lead
7. **Reporting**: After each run, leave a summary comment on your primary task.
IMPORTANT: Do NOT try to do all the work yourself. Your job is to DELEGATE, REVIEW, and COORDINATE.
CRITICAL: You are the CEO. DELEGATE work — do NOT try to code, write content, or design yourself. Your job is to ORCHESTRATE.
`);
}
} else if (isManager) {
// Step 2d: Manager with credential escalation instruction
sections.push(`# Operating Instructions (Manager)
You are a middle manager. Your primary responsibilities:
You are a middle manager running a team within an automated company. Follow these SOPs in priority order:
1. **Task Breakdown**: When assigned a project or high-level task, break it into smaller, actionable subtasks.
- Use POST /api/companies/{companyId}/issues to create subtasks.
- Set assigneeAgentId to one of your direct reports.
- Set parentId to link subtasks to the parent task.
## Priority 1: Standup Check (EVERY wake — SOP-001)
1. Check tasks with status "in_review" from your reports — these MUST be reviewed first
2. Check for "blocked" tasks from your reports — second priority
3. Read new comments on your tasks since last run
4. Check if your manager has posted feedback on your own tasks
2. **Delegation**: Assign subtasks to your direct reports based on their capabilities.
- Match each report's capabilities to the right subtask.
- Each subtask should have a clear title and description explaining what needs to be done.
## Priority 2: Review Completed Work (SOP-004)
This is your CORE DUTY — reviews unblock your team's progress:
1. Find all tasks from your reports with status "in_review"
2. Read ALL comments and understand what was done
3. If satisfactory: Update to "done" + leave approving comment explaining what was good
4. If changes needed: Set back to "in_progress" + leave SPECIFIC, ACTIONABLE feedback
5. NEVER delay reviews — they are your highest priority after standup
3. **Review Completed Work**: When you receive a "subtask_completed" wake:
- Check which subtasks have status "in_review" or "done".
- Read comments on those tasks to understand what was done.
- If the work is satisfactory: update the subtask status to "done" and leave an approving comment.
- If changes are needed: post a comment explaining what needs to change, and set the subtask status back to "in_progress".
- This is your core management duty — do NOT skip reviews.
## Priority 3: Task Breakdown & Delegation (SOP-003)
When assigned a project or high-level task:
1. Analyze the work and break it into 3-7 specific subtasks
2. Each subtask needs: clear title, detailed description with acceptance criteria, priority
3. Assign each to the best-fit report based on their capabilities
4. Set parentId and projectId for proper hierarchy
5. Leave a planning comment on the parent task describing your breakdown
4. **Ask Questions / Give Feedback**: Use comments to communicate with your reports.
- POST /api/issues/{issueId}/comments to ask a question or give feedback on any task.
- The task assignee will be automatically woken and notified.
## Priority 4: Unblock Your Reports
When reports post comments about blockers:
- If you can resolve it: do so and notify via comment
- If you need credentials: escalate by commenting on YOUR parent task (your manager sees it)
- If you need more people: propose a hire with POST /api/companies/{companyId}/agents
5. **Progress Tracking**: Check comments and task statuses of your reports.
- GET /api/companies/{companyId}/issues to see all tasks and their statuses.
- GET /api/issues/{issueId}/comments to read discussion on specific tasks.
## Priority 5: Progress Reporting
After each run, update your task status and leave a summary for your manager:
- What was reviewed and approved
- What was delegated
- What's blocked and why
- Overall progress percentage estimate
6. **Escalation**: If you or your reports need credentials:
- Post a comment on the parent task explaining the need.
- Your manager will be notified and escalate further.
- Do NOT create secret_access approvals directly — escalate upward.
7. **Hiring**: If you need additional team members to handle workload:
- Use POST /api/companies/{companyId}/agents to propose new hires that report to you.
- New hires require Board approval before they can start working.
8. **Reporting**: Update your task status and leave summary comments for your manager.
CRITICAL: You are a manager. Break down work and delegate it. Do NOT try to do IC work yourself unless you have no reports.
`);
} else {
// Step 2d: IC with credential escalation instruction
sections.push(`# Operating Instructions (Individual Contributor)
You are an individual contributor. Your primary responsibilities:
You are an individual contributor — a skilled specialist in an automated company. Follow these SOPs:
1. **Execute Tasks**: Work on your assigned tasks using your capabilities and tools.
- Focus on tasks that match your role and capabilities.
- Use the tools available in your workspace (CLI, code editors, etc.) to produce deliverables.
## Priority 1: Check for Feedback (EVERY wake — SOP-001)
1. Read ALL new comments on your assigned tasks
2. If your manager has set a task back to "in_progress": read their feedback and address it
3. If there are new comments with questions: respond promptly
4. Check if any of your tasks have been unblocked
2. **Checkout First**: Always checkout a task before starting work on it.
- POST /api/issues/{issueId}/checkout to claim the task.
## Priority 2: Execute Your Highest-Priority Task
Find your most important assigned task and work on it:
1. **Checkout**: POST /api/issues/{issueId}/checkout to claim it
2. **Understand**: Read the description and all comments carefully
3. **Plan**: If the task is complex, break it into subtasks for yourself
4. **Execute**: Do the actual work using your tools and workspace
5. **Document**: Leave a progress comment after each meaningful step
6. **Submit**: When done, set status to "in_review" + comment describing what you did
3. **Break Down Complex Work**: If your assigned task is large or complex:
- Create subtasks for yourself using POST /api/companies/{companyId}/issues.
- Set assigneeAgentId to your own ID and parentId to the original task.
- This helps you track progress on multi-step work.
## Task Execution Workflow (SOP-002)
- ALWAYS checkout before working
- For large tasks: create subtasks assigned to yourself (parentId = original task)
- Leave progress comments frequently — your manager monitors these
- When work is complete, set to "in_review" — NEVER set your own work to "done"
- Wait for manager approval before moving on
4. **Report Progress**: Leave comments on your tasks with progress updates.
- POST /api/issues/{issueId}/comments with a summary of what you did.
- Your manager will be automatically notified of your comments.
## Handling Blockers (SOP-006)
If you're stuck:
1. Set task status to "blocked"
2. Post a detailed comment: what's blocking you, what you've tried, what you need
3. Your manager is auto-notified and will help
4. For credentials: comment explaining what API key/account you need and why
5. **Submit for Review**: When your work is complete, update the task status to "in_review".
- PATCH /api/issues/{issueId} with status "in_review" and a comment describing what was done.
- Your manager will be automatically notified to review your work.
- Wait for your manager's feedback before marking as "done".
## Handling Review Feedback
When your task is set back to "in_progress" with feedback:
1. Read ALL feedback comments carefully
2. Address each point specifically
3. Re-submit to "in_review" with a comment explaining the changes
6. **Ask Questions**: If requirements are unclear or you need guidance:
- Post a comment on your task with your question.
- Your manager will be notified and can respond.
- Check comments on your task for any feedback or answers.
7. **Handle Review Feedback**: When your manager posts a comment or sets your task back to "in_progress":
- Read the comment to understand what changes are needed.
- Make the changes and submit for review again.
8. **Report Blockers**: If you're blocked (need credentials, unclear requirements, etc.):
- Update task status to "blocked"
- Leave a comment explaining what's needed
- Your manager will be notified
9. **Request Credentials**: If you need credentials (API keys, account access, etc.):
- Post a comment on your task explaining what you need and why
- Your manager will be notified and help resolve it
- Only create a secret_access approval directly if you have NO manager
10. **Propose Hires**: If you believe the team needs additional members:
- Use POST /api/companies/{companyId}/agents to propose a new hire.
- Set reportsTo to your manager's agent ID.
- The Board must approve before the hire becomes active.
## Quality Standards
- Always test your work before submitting for review
- Leave clear comments so your manager can understand what was done
- If requirements are ambiguous, ask first rather than guessing
`);
}
+52 -214
View File
@@ -36,6 +36,7 @@ import type {
import type { SidebarBadges } from "@shared/contracts";
import { CompanyOnboarding } from "./components/CompanyOnboarding";
import { ConnectorEditor } from "./components/connectors";
import { Dashboard, KanbanBoard } from "./components/dashboard";
import { EntityWorkspace } from "./components/EntityWorkspace";
import {
ActivityList,
@@ -128,6 +129,7 @@ export function App() {
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null);
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
const [taskViewMode, setTaskViewMode] = useState<"list" | "kanban">("list");
const logBufferRef = useRef<Record<string, string>>({});
const [, setLogRevision] = useState(0);
const [badges, setBadges] = useState<SidebarBadges | null>(null);
@@ -530,218 +532,27 @@ export function App() {
className="space-y-14"
>
{section === "overview" ? (
<>
{/* Status metrics */}
<section className="space-y-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Current status</div>
<div className="grid grid-cols-2 border-y border-[color:var(--line)] lg:grid-cols-4">
{overviewRows.map((row, index) => (
<div
key={row.label}
className={`px-4 py-4 lg:px-5 lg:first:pl-0 lg:last:pr-0 ${
index > 0 ? "border-l border-[color:var(--line)]" : ""
} ${index >= 2 ? "border-t border-[color:var(--line)] lg:border-t-0" : ""}`}
>
<div className="text-[12px] font-medium text-[color:var(--muted)]">{row.label}</div>
<div className={`mono text-[18px] font-semibold tracking-[-0.01em] sm:mt-2 ${row.tone || "text-[color:var(--text)]"}`}>{row.value}</div>
</div>
))}
</div>
</section>
{/* Live runs */}
{liveRuns.length > 0 ? (
<section className="space-y-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--success)]">Live runs</div>
<ListFrame>
{liveRuns.slice(0, 5).map((run) => {
const agent = companyAgents.find((a) => a.id === run.agentId);
const task = tasks.find((t) => t.id === run.taskId);
return (
<ListRow key={run.id} onClick={() => { setSelectedRunId(run.id); setConsoleOpen(true); }}>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="truncate text-[14px] font-medium text-[color:var(--text)]">{task?.title ?? run.id.slice(0, 8)}</div>
<div className="mt-1 flex items-center gap-3 text-[11px] uppercase tracking-[0.12em] text-[color:var(--muted)]">
{agent ? <span>{agent.name}</span> : null}
<span>{run.connectorId.replace("_local", "")}</span>
{run.model ? <span className="mono text-[10px] normal-case tracking-normal">{run.model}</span> : null}
</div>
</div>
<StatusPill status={run.status} />
</div>
</ListRow>
);
})}
</ListFrame>
</section>
) : null}
{/* Needs attention: pending approvals + failed runs */}
{(pendingApprovals.length > 0 || failedRuns.length > 0) ? (
<section className="space-y-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">Needs attention</div>
<ListFrame>
{pendingApprovals.slice(0, 4).map((approval) => {
const requester = companyAgents.find((a) => a.id === approval.requestedByAgentId);
return (
<ListRow key={approval.id} onClick={() => { setSelectedApprovalId(approval.id); setSection("approvals"); }}>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="truncate text-[14px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div>
<div className="mt-1 flex items-center gap-3 text-[11px] uppercase tracking-[0.12em] text-[color:var(--muted)]">
<span>{approval.type.replaceAll("_", " ")}</span>
{requester ? <span>by {requester.name}</span> : null}
</div>
</div>
<StatusPill status={approval.state} />
</div>
</ListRow>
);
})}
{failedRuns.slice(0, 3).map((run) => {
const agent = companyAgents.find((a) => a.id === run.agentId);
return (
<ListRow key={run.id} onClick={() => { setSelectedRunId(run.id); setConsoleOpen(true); }}>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="truncate text-[14px] font-medium text-[color:var(--text)]">{run.summary || "Run ended with an error"}</div>
<div className="mt-1 flex items-center gap-3 text-[11px] text-[color:var(--danger)]">
{agent ? <span className="uppercase tracking-[0.12em]">{agent.name}</span> : null}
{run.errorMessage ? <span className="truncate">{run.errorMessage}</span> : null}
</div>
</div>
<StatusPill status={run.status} />
</div>
</ListRow>
);
})}
</ListFrame>
</section>
) : null}
{/* Budget alerts */}
{(() => {
const budgetWarningAgents = companyAgents.filter(
(a) => a.budgetMonthlyUsd > 0 && (a.spentMonthlyUsd / a.budgetMonthlyUsd) >= 0.8,
);
if (budgetWarningAgents.length === 0) return null;
return (
<section className="space-y-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--danger)]">Budget alerts</div>
<ListFrame>
{budgetWarningAgents.map((agent) => {
const pct = agent.budgetMonthlyUsd > 0 ? Math.round((agent.spentMonthlyUsd / agent.budgetMonthlyUsd) * 100) : 0;
const overBudget = agent.spentMonthlyUsd >= agent.budgetMonthlyUsd;
return (
<ListRow key={agent.id} onClick={() => { setSelectedAgentId(agent.id); setSection("agents"); }}>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="text-[14px] font-medium text-[color:var(--text)]">{agent.name}</div>
<div className="mt-1 flex items-center gap-3 text-[11px] text-[color:var(--muted)]">
<span className="uppercase tracking-[0.12em]">{agent.role}</span>
<span className={overBudget ? "text-[color:var(--danger)]" : "text-[color:var(--warn)]"}>
{formatMoney(agent.spentMonthlyUsd)} / {formatMoney(agent.budgetMonthlyUsd)} ({pct}%)
</span>
</div>
</div>
<StatusPill status={overBudget ? "error" : "degraded"} />
</div>
</ListRow>
);
})}
</ListFrame>
</section>
);
})()}
{/* Connector health */}
<section className="space-y-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Connector health</div>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
{snapshot.connectors.map((connector) => {
const isReady = connector.status === "ready" || connector.status === "degraded";
const isError = connector.status === "error" || connector.status === "auth_required";
return (
<button
key={connector.id}
type="button"
onClick={() => { setSelectedConnectorId(connector.id); setSection("connectors"); }}
className={`focus-ring flex items-center gap-3 rounded-[10px] border px-4 py-3 text-left transition duration-150 hover:bg-[color:var(--panel-soft)] ${
isError ? "border-[color:var(--danger)] border-opacity-30" : "border-[color:var(--line)]"
}`}
>
<div className={`h-2 w-2 shrink-0 rounded-full ${
isReady ? "bg-[color:var(--success)]" : isError ? "bg-[color:var(--danger)]" : "bg-[color:var(--muted)]"
}`} />
<div className="min-w-0">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{connector.label}</div>
<div className="truncate text-[11px] text-[color:var(--muted)]">{connector.status.replaceAll("_", " ")}</div>
</div>
{!connector.capabilityMatrix.costAttribution ? (
<span className="ml-auto shrink-0 rounded bg-[color:var(--panel-soft)] px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-[0.12em] text-[color:var(--muted)]">no cost</span>
) : null}
</button>
);
})}
</div>
</section>
{/* Open work */}
<section className="space-y-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Open work</div>
<ListFrame>
{tasks.length === 0 ? (
<div className="py-8 text-center text-[13px] text-[color:var(--muted)]">No work queued</div>
) : (
tasks.slice(0, 8).map((task) => {
const run = runs.find((entry) => entry.id === task.activeRunId) ?? null;
const assignee = companyAgents.find((agent) => agent.id === task.assigneeAgentId) ?? null;
return (
<ListRow key={task.id} onClick={() => { setSection("tasks"); setSelectedTaskId(task.id); }}>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="truncate text-[14px] font-medium text-[color:var(--text)]">{task.title}</div>
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] uppercase tracking-[0.12em] text-[color:var(--muted)]">
<span className={task.priority === "critical" ? "text-[color:var(--danger)]" : task.priority === "high" ? "text-[color:var(--warn)]" : ""}>{task.priority}</span>
{assignee ? <span>{assignee.name}</span> : null}
</div>
</div>
<StatusPill status={run?.status ?? task.status} />
</div>
</ListRow>
);
})
)}
</ListFrame>
</section>
{/* Workspace issues */}
{(() => {
const problemWorkspaces = companyWorkspaces.filter(
(w) => w.state !== "ready",
);
if (problemWorkspaces.length === 0) return null;
return (
<section className="space-y-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">Workspace issues</div>
<ListFrame>
{problemWorkspaces.map((ws) => (
<ListRow key={ws.id} onClick={() => setSection("workspaces")}>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="text-[14px] font-medium text-[color:var(--text)]">{ws.name}</div>
<div className="mt-1 truncate text-[11px] text-[color:var(--muted)] mono">{ws.localPath}</div>
</div>
<StatusPill status={ws.state} />
</div>
</ListRow>
))}
</ListFrame>
</section>
);
})()}
</>
<Dashboard
agents={companyAgents}
tasks={tasks}
runs={runs}
approvals={approvals}
goals={companyGoals}
projects={companyProjects}
activity={filteredActivity}
costs={companyCosts}
connectors={snapshot.connectors}
onNavigate={(targetSection, entityId) => {
setSection(targetSection);
if (entityId) {
if (targetSection === "agents") setSelectedAgentId(entityId);
else if (targetSection === "tasks") setSelectedTaskId(entityId);
else if (targetSection === "approvals") setSelectedApprovalId(entityId);
else if (targetSection === "goals") setSelectedGoalId(entityId);
else if (targetSection === "projects") setSelectedProjectId(entityId);
}
}}
/>
) : null}
{section === "connectors" && selectedConnector ? (
@@ -864,8 +675,35 @@ export function App() {
{section === "tasks" ? (
<>
<Panel title="Task queue" action={<ActionButton label="New task" icon={ListChecks} tone="accent" onClick={() => setSelectedTaskId("__new__")} />}>
<TaskQueue tasks={tasks} runs={runs} onSelect={setSelectedTaskId} selectedTaskId={selectedTask?.id ?? null} />
<Panel
title="Task queue"
action={
<div className="flex items-center gap-2">
<div className="flex rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)]">
<button
type="button"
onClick={() => setTaskViewMode("list")}
className={`px-2.5 py-1.5 text-[11px] font-medium transition ${taskViewMode === "list" ? "bg-[color:var(--panel-soft)] text-[color:var(--text)]" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}
>
<ListChecks size={14} />
</button>
<button
type="button"
onClick={() => setTaskViewMode("kanban")}
className={`px-2.5 py-1.5 text-[11px] font-medium transition ${taskViewMode === "kanban" ? "bg-[color:var(--panel-soft)] text-[color:var(--text)]" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}
>
<Kanban size={14} />
</button>
</div>
<ActionButton label="New task" icon={ListChecks} tone="accent" onClick={() => setSelectedTaskId("__new__")} />
</div>
}
>
{taskViewMode === "kanban" ? (
<KanbanBoard tasks={tasks} agents={companyAgents} runs={runs} onSelectTask={setSelectedTaskId} />
) : (
<TaskQueue tasks={tasks} runs={runs} onSelect={setSelectedTaskId} selectedTaskId={selectedTask?.id ?? null} />
)}
</Panel>
<Modal open={selectedTaskId === "__new__"} title="New task" onClose={() => setSelectedTaskId(null)}>
<TaskCreator companyId={currentCompany!.id} goals={companyGoals} projects={companyProjects} agents={companyAgents} workspaces={companyWorkspaces} onSaved={async () => { setSelectedTaskId(null); await refresh(); }} />
+740
View File
@@ -0,0 +1,740 @@
import { useMemo } from "react";
import { motion } from "framer-motion";
import {
ArrowRight,
Briefcase,
CheckCircle,
Cpu,
CurrencyCircleDollar,
Heartbeat,
Lightning,
Robot,
ShieldCheck,
Target,
TreeStructure,
Warning,
XCircle,
} from "@phosphor-icons/react";
import type {
ActivityRecord,
AgentRecord,
ApprovalRecord,
ConnectorRecord,
CostRecord,
GoalRecord,
ProjectRecord,
RunRecord,
SectionId,
TaskRecord,
} from "@shared/types";
import { formatMoney } from "../lib/formatters";
import { StatusPill } from "./ui";
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
interface DashboardProps {
agents: AgentRecord[];
tasks: TaskRecord[];
runs: RunRecord[];
approvals: ApprovalRecord[];
goals: GoalRecord[];
projects: ProjectRecord[];
activity: ActivityRecord[];
costs: CostRecord[];
connectors: ConnectorRecord[];
onNavigate: (section: SectionId, entityId?: string) => void;
}
/* ------------------------------------------------------------------ */
/* Health Score Computation */
/* ------------------------------------------------------------------ */
function computeHealthScore(props: Pick<DashboardProps, "agents" | "tasks" | "runs" | "approvals" | "connectors">): number {
let score = 100;
const { agents, tasks, runs, approvals, connectors } = props;
// Penalize for failed runs (recent)
const failedRuns = runs.filter((r) => r.status === "failed" || r.status === "timed_out");
score -= Math.min(failedRuns.length * 5, 25);
// Penalize for blocked tasks
const blockedTasks = tasks.filter((t) => t.status === "blocked");
score -= Math.min(blockedTasks.length * 8, 20);
// Penalize for pending approvals (bottleneck)
const pendingApprovals = approvals.filter((a) => a.state === "pending");
score -= Math.min(pendingApprovals.length * 5, 15);
// Penalize for agents in error state
const errorAgents = agents.filter((a) => a.status === "error");
score -= Math.min(errorAgents.length * 10, 20);
// Penalize for no ready connectors
const readyConnectors = connectors.filter((c) => c.status === "ready");
if (readyConnectors.length === 0 && connectors.length > 0) score -= 15;
// Penalize for overbudget agents
const overBudget = agents.filter((a) => a.budgetMonthlyUsd > 0 && a.spentMonthlyUsd >= a.budgetMonthlyUsd);
score -= Math.min(overBudget.length * 5, 10);
return Math.max(0, Math.min(100, score));
}
function healthLabel(score: number): { label: string; tone: string } {
if (score >= 90) return { label: "Excellent", tone: "text-[color:var(--success)]" };
if (score >= 70) return { label: "Good", tone: "text-[color:var(--success)]" };
if (score >= 50) return { label: "Fair", tone: "text-[color:var(--warn)]" };
if (score >= 30) return { label: "Poor", tone: "text-[color:var(--warn)]" };
return { label: "Critical", tone: "text-[color:var(--danger)]" };
}
/* ------------------------------------------------------------------ */
/* Health Ring */
/* ------------------------------------------------------------------ */
function HealthRing({ score }: { score: number }) {
const radius = 38;
const circumference = 2 * Math.PI * radius;
const offset = circumference - (score / 100) * circumference;
const { label, tone } = healthLabel(score);
const strokeColor = score >= 70
? "var(--success)"
: score >= 50
? "var(--warn)"
: "var(--danger)";
return (
<div className="flex items-center gap-5">
<div className="relative h-[90px] w-[90px] shrink-0">
<svg viewBox="0 0 88 88" className="h-full w-full -rotate-90">
<circle cx="44" cy="44" r={radius} fill="none" stroke="var(--line)" strokeWidth="6" />
<motion.circle
cx="44"
cy="44"
r={radius}
fill="none"
stroke={strokeColor}
strokeWidth="6"
strokeLinecap="round"
strokeDasharray={circumference}
initial={{ strokeDashoffset: circumference }}
animate={{ strokeDashoffset: offset }}
transition={{ duration: 1.2, ease: "easeOut" }}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className={`text-[22px] font-bold ${tone}`}>{score}</span>
</div>
</div>
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Company Health</div>
<div className={`text-[16px] font-semibold ${tone}`}>{label}</div>
</div>
</div>
);
}
/* ------------------------------------------------------------------ */
/* Metric Card */
/* ------------------------------------------------------------------ */
function MetricCard({
label,
value,
icon: Icon,
tone = "default",
onClick,
subtitle,
}: {
label: string;
value: string | number;
icon: React.ElementType;
tone?: "default" | "success" | "warn" | "danger" | "accent";
onClick?: () => void;
subtitle?: string;
}) {
const toneMap = {
default: "text-[color:var(--text)]",
success: "text-[color:var(--success)]",
warn: "text-[color:var(--warn)]",
danger: "text-[color:var(--danger)]",
accent: "text-[color:var(--accent)]",
};
const bgMap = {
default: "bg-[color:var(--panel-soft)]",
success: "bg-[color:var(--success-soft)]",
warn: "bg-[color:var(--warn-soft)]",
danger: "bg-[color:var(--danger-soft)]",
accent: "bg-[color:var(--accent-soft)]",
};
return (
<button
type="button"
onClick={onClick}
className="focus-ring flex items-center gap-3.5 rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3.5 text-left transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm"
>
<div className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-[8px] ${bgMap[tone]}`}>
<Icon size={20} weight="fill" className={toneMap[tone]} />
</div>
<div className="min-w-0 flex-1">
<div className="text-[11px] font-medium uppercase tracking-[0.06em] text-[color:var(--muted)]">{label}</div>
<div className={`text-[20px] font-bold tracking-[-0.02em] leading-tight ${toneMap[tone]}`}>{value}</div>
{subtitle ? <div className="mt-0.5 truncate text-[11px] text-[color:var(--muted)]">{subtitle}</div> : null}
</div>
</button>
);
}
/* ------------------------------------------------------------------ */
/* Agent Workforce Grid */
/* ------------------------------------------------------------------ */
const statusGlow: Record<string, string> = {
running: "ring-2 ring-[color:var(--success)] ring-opacity-50",
active: "ring-1 ring-[color:var(--success)] ring-opacity-30",
idle: "",
paused: "opacity-60",
error: "ring-2 ring-[color:var(--danger)] ring-opacity-50",
pending_approval: "ring-1 ring-[color:var(--warn)] ring-opacity-40",
terminated: "opacity-40",
};
function AgentCard({ agent, onClick }: { agent: AgentRecord; onClick: () => void }) {
const initials = agent.name
.split(/\s+/)
.slice(0, 2)
.map((w) => w[0])
.join("")
.toUpperCase();
const isActive = agent.status === "running" || agent.status === "active";
return (
<button
type="button"
onClick={onClick}
className={`focus-ring group relative flex flex-col items-center gap-2 rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-3 text-center transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm ${statusGlow[agent.status] ?? ""}`}
>
<div className="relative">
<div className={`flex h-11 w-11 items-center justify-center rounded-full text-[14px] font-bold ${
isActive ? "bg-[color:var(--success-soft)] text-[color:var(--success)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
}`}>
{initials}
</div>
{agent.status === "running" ? (
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5 items-center justify-center">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-[color:var(--success)]" />
</span>
) : (
<span
className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--panel)]"
style={{
backgroundColor: agent.status === "error"
? "var(--danger)"
: agent.status === "active"
? "var(--success)"
: agent.status === "pending_approval"
? "var(--warn)"
: agent.status === "paused" || agent.status === "terminated"
? "var(--muted)"
: "var(--muted)",
}}
/>
)}
</div>
<div className="w-full min-w-0">
<div className="truncate text-[12px] font-semibold text-[color:var(--text)]">{agent.name}</div>
<div className="truncate text-[10px] text-[color:var(--muted)]">{agent.title || agent.role}</div>
</div>
</button>
);
}
function WorkforceGrid({ agents, onSelectAgent }: { agents: AgentRecord[]; onSelectAgent: (id: string) => void }) {
if (agents.length === 0) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-10 text-center">
<Robot size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No agents hired yet</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">Create a CEO agent to start building your company</div>
</div>
);
}
return (
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{agents.map((agent) => (
<AgentCard key={agent.id} agent={agent} onClick={() => onSelectAgent(agent.id)} />
))}
</div>
);
}
/* ------------------------------------------------------------------ */
/* Activity Stream */
/* ------------------------------------------------------------------ */
const actionIcons: Record<string, React.ElementType> = {
created: CheckCircle,
updated: Lightning,
deleted: XCircle,
started: Heartbeat,
completed: CheckCircle,
failed: Warning,
approved: ShieldCheck,
rejected: XCircle,
};
function timeAgo(dateStr: string): string {
const now = Date.now();
const then = new Date(dateStr).getTime();
const diff = Math.max(0, now - then);
const seconds = Math.floor(diff / 1000);
if (seconds < 60) return "just now";
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
function ActivityFeed({ activity, agents, limit = 15 }: { activity: ActivityRecord[]; agents: AgentRecord[]; limit?: number }) {
const recentActivity = useMemo(() => {
return [...activity]
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
.slice(0, limit);
}, [activity, limit]);
if (recentActivity.length === 0) {
return (
<div className="flex h-full items-center justify-center py-8 text-center">
<div className="text-[13px] text-[color:var(--muted)]">No activity yet. Start by creating agents and tasks.</div>
</div>
);
}
return (
<div className="space-y-0.5">
{recentActivity.map((item, index) => {
const Icon = actionIcons[item.action] ?? Lightning;
const agent = agents.find((a) => a.id === item.actor);
const actorName = agent?.name ?? (item.actor === "board" ? "Board" : item.actor.slice(0, 8));
return (
<motion.div
key={item.id}
initial={index === 0 ? { opacity: 0, y: -8 } : false}
animate={{ opacity: 1, y: 0 }}
className="flex items-start gap-3 rounded-[6px] px-3 py-2 transition hover:bg-[color:var(--panel-soft)]"
>
<div className="mt-0.5 shrink-0">
<Icon size={14} className="text-[color:var(--muted)]" />
</div>
<div className="min-w-0 flex-1">
<div className="text-[12px] leading-relaxed text-[color:var(--text)]">
<span className="font-medium">{actorName}</span>
{" "}
<span className="text-[color:var(--muted)]">{item.action}</span>
{" "}
<span className="text-[color:var(--muted-strong)]">{item.entityType}</span>
{item.detail ? (
<span className="text-[color:var(--muted)]"> — {item.detail.slice(0, 60)}{item.detail.length > 60 ? "..." : ""}</span>
) : null}
</div>
<div className="mt-0.5 text-[10px] text-[color:var(--muted)]">{timeAgo(item.createdAt)}</div>
</div>
</motion.div>
);
})}
</div>
);
}
/* ------------------------------------------------------------------ */
/* Workflow Pipeline (visual task flow) */
/* ------------------------------------------------------------------ */
function TaskPipeline({ tasks }: { tasks: TaskRecord[] }) {
const stages = useMemo(() => {
const counts = { backlog: 0, todo: 0, in_progress: 0, in_review: 0, done: 0, blocked: 0 };
for (const t of tasks) {
if (t.status in counts) counts[t.status as keyof typeof counts]++;
}
return [
{ label: "Backlog", count: counts.backlog, tone: "text-[color:var(--muted)]", bg: "bg-[color:var(--panel-soft)]" },
{ label: "Todo", count: counts.todo, tone: "text-[color:var(--warn)]", bg: "bg-[color:var(--warn-soft)]" },
{ label: "In Progress", count: counts.in_progress, tone: "text-[color:var(--accent)]", bg: "bg-[color:var(--accent-soft)]" },
{ label: "In Review", count: counts.in_review, tone: "text-[color:var(--warn)]", bg: "bg-[color:var(--warn-soft)]" },
{ label: "Done", count: counts.done, tone: "text-[color:var(--success)]", bg: "bg-[color:var(--success-soft)]" },
{ label: "Blocked", count: counts.blocked, tone: "text-[color:var(--danger)]", bg: "bg-[color:var(--danger-soft)]" },
];
}, [tasks]);
return (
<div className="flex items-center gap-1.5">
{stages.map((stage, i) => (
<div key={stage.label} className="flex items-center gap-1.5">
<div className={`flex items-center gap-2 rounded-[8px] px-3 py-2 ${stage.bg}`}>
<span className={`text-[18px] font-bold ${stage.tone}`}>{stage.count}</span>
<span className="text-[10px] font-medium uppercase tracking-[0.06em] text-[color:var(--muted)]">{stage.label}</span>
</div>
{i < stages.length - 1 ? (
<ArrowRight size={12} className="text-[color:var(--muted)]" />
) : null}
</div>
))}
</div>
);
}
/* ------------------------------------------------------------------ */
/* Main Dashboard */
/* ------------------------------------------------------------------ */
export function Dashboard(props: DashboardProps) {
const { agents, tasks, runs, approvals, goals, projects, activity, costs, connectors, onNavigate } = props;
const healthScore = useMemo(
() => computeHealthScore({ agents, tasks, runs, approvals, connectors }),
[agents, tasks, runs, approvals, connectors],
);
const liveRuns = useMemo(() => runs.filter((r) => r.status === "running" || r.status === "queued"), [runs]);
const failedRuns = useMemo(() => runs.filter((r) => r.status === "failed" || r.status === "timed_out" || r.status === "interrupted"), [runs]);
const pendingApprovals = useMemo(() => approvals.filter((a) => a.state === "pending"), [approvals]);
const activeTasks = useMemo(() => tasks.filter((t) => t.status === "in_progress" || t.status === "in_review"), [tasks]);
const activeGoals = useMemo(() => goals.filter((g) => g.status === "active"), [goals]);
const totalSpend = useMemo(() => costs.reduce((sum, c) => sum + (c.amountUsd ?? 0), 0), [costs]);
const activeAgents = useMemo(() => agents.filter((a) => a.status !== "terminated" && a.status !== "pending_approval"), [agents]);
const readyConnectors = useMemo(() => connectors.filter((c) => c.status === "ready"), [connectors]);
return (
<div className="space-y-8">
{/* Row 1: Health + Metrics */}
<div className="flex flex-col gap-6 lg:flex-row lg:items-start">
{/* Health Ring */}
<div className="shrink-0 rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
<HealthRing score={healthScore} />
<div className="mt-3 flex gap-3">
<div className="text-center">
<div className="text-[16px] font-bold text-[color:var(--text)]">{activeAgents.length}</div>
<div className="text-[10px] text-[color:var(--muted)]">Agents</div>
</div>
<div className="h-8 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className="text-[16px] font-bold text-[color:var(--text)]">{readyConnectors.length}/{connectors.length}</div>
<div className="text-[10px] text-[color:var(--muted)]">Connectors</div>
</div>
<div className="h-8 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className="text-[16px] font-bold text-[color:var(--text)]">{activeGoals.length}</div>
<div className="text-[10px] text-[color:var(--muted)]">Goals</div>
</div>
</div>
</div>
{/* Metric Cards */}
<div className="grid flex-1 grid-cols-2 gap-2 lg:grid-cols-3">
<MetricCard
label="Live Runs"
value={liveRuns.length}
icon={Heartbeat}
tone={liveRuns.length > 0 ? "success" : "default"}
onClick={() => onNavigate("tasks")}
subtitle={liveRuns.length > 0 ? `${liveRuns.filter((r) => r.status === "running").length} running` : "All quiet"}
/>
<MetricCard
label="Active Tasks"
value={activeTasks.length}
icon={Briefcase}
tone={activeTasks.length > 0 ? "accent" : "default"}
onClick={() => onNavigate("tasks")}
subtitle={`${tasks.filter((t) => t.status === "todo").length} queued`}
/>
<MetricCard
label="Approvals"
value={pendingApprovals.length}
icon={ShieldCheck}
tone={pendingApprovals.length > 0 ? "warn" : "default"}
onClick={() => onNavigate("approvals")}
subtitle={pendingApprovals.length > 0 ? "Needs your attention" : "All clear"}
/>
<MetricCard
label="Failed Runs"
value={failedRuns.length}
icon={Warning}
tone={failedRuns.length > 0 ? "danger" : "default"}
onClick={() => onNavigate("tasks")}
/>
<MetricCard
label="Total Spend"
value={formatMoney(totalSpend)}
icon={CurrencyCircleDollar}
onClick={() => onNavigate("costs")}
/>
<MetricCard
label="Projects"
value={projects.filter((p) => p.status === "in_progress").length}
icon={Target}
onClick={() => onNavigate("projects")}
subtitle={`${projects.length} total`}
/>
</div>
</div>
{/* Row 2: Task Pipeline */}
<section>
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Task Pipeline</div>
<button
type="button"
onClick={() => onNavigate("tasks")}
className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline"
>
View all <ArrowRight size={12} />
</button>
</div>
<div className="overflow-x-auto rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<TaskPipeline tasks={tasks} />
</div>
</section>
{/* Row 3: Workforce + Activity */}
<div className="grid gap-6 lg:grid-cols-5">
{/* Workforce */}
<section className="lg:col-span-3">
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Workforce</div>
<button
type="button"
onClick={() => onNavigate("orgchart")}
className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline"
>
Org chart <TreeStructure size={12} />
</button>
</div>
<WorkforceGrid agents={agents} onSelectAgent={(id) => onNavigate("agents", id)} />
</section>
{/* Activity Feed */}
<section className="lg:col-span-2">
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Recent Activity</div>
<button
type="button"
onClick={() => onNavigate("activity")}
className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline"
>
View all <ArrowRight size={12} />
</button>
</div>
<div className="max-h-[400px] overflow-y-auto rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2">
<ActivityFeed activity={activity} agents={agents} />
</div>
</section>
</div>
{/* Row 4: Connector health (compact) */}
<section>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Infrastructure</div>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5">
{connectors.map((connector) => {
const isReady = connector.status === "ready";
const isError = connector.status === "error" || connector.status === "auth_required";
const agentsUsing = agents.filter((a) => a.connectorId === connector.id).length;
return (
<button
key={connector.id}
type="button"
onClick={() => onNavigate("connectors")}
className={`focus-ring flex items-center gap-2.5 rounded-[8px] border px-3 py-2.5 text-left transition duration-150 hover:bg-[color:var(--panel-soft)] ${
isError ? "border-[color:var(--danger)] border-opacity-30" : "border-[color:var(--line)]"
}`}
>
<Cpu size={16} className={isReady ? "text-[color:var(--success)]" : isError ? "text-[color:var(--danger)]" : "text-[color:var(--muted)]"} />
<div className="min-w-0 flex-1">
<div className="truncate text-[12px] font-medium text-[color:var(--text)]">{connector.label}</div>
<div className="flex items-center gap-2 text-[10px] text-[color:var(--muted)]">
<StatusPill status={connector.status} />
{agentsUsing > 0 ? <span>{agentsUsing} agent{agentsUsing > 1 ? "s" : ""}</span> : null}
</div>
</div>
</button>
);
})}
</div>
</section>
{/* Row 5: Quick Actions / Needs Attention */}
{(pendingApprovals.length > 0 || failedRuns.length > 0) ? (
<section>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">Needs Attention</div>
<div className="space-y-1.5">
{pendingApprovals.slice(0, 5).map((approval) => {
const requester = agents.find((a) => a.id === approval.requestedByAgentId);
return (
<button
key={approval.id}
type="button"
onClick={() => onNavigate("approvals", approval.id)}
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--warn)] border-opacity-20 bg-[color:var(--warn-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
>
<ShieldCheck size={18} className="shrink-0 text-[color:var(--warn)]" />
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div>
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
<span className="uppercase tracking-[0.1em]">{approval.type.replaceAll("_", " ")}</span>
{requester ? <span>by {requester.name}</span> : null}
</div>
</div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
</button>
);
})}
{failedRuns.slice(0, 3).map((run) => {
const agent = agents.find((a) => a.id === run.agentId);
return (
<button
key={run.id}
type="button"
onClick={() => onNavigate("tasks")}
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--danger)] border-opacity-20 bg-[color:var(--danger-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
>
<XCircle size={18} className="shrink-0 text-[color:var(--danger)]" />
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{run.summary || "Run failed"}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
{agent?.name} — {run.errorMessage?.slice(0, 80) ?? "Unknown error"}
</div>
</div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
</button>
);
})}
</div>
</section>
) : null}
</div>
);
}
/* ------------------------------------------------------------------ */
/* Kanban Board */
/* ------------------------------------------------------------------ */
interface KanbanProps {
tasks: TaskRecord[];
agents: AgentRecord[];
runs: RunRecord[];
onSelectTask: (taskId: string) => void;
}
const KANBAN_COLUMNS: Array<{ status: TaskRecord["status"]; label: string; tone: string; dotColor: string }> = [
{ status: "backlog", label: "Backlog", tone: "text-[color:var(--muted)]", dotColor: "bg-[color:var(--muted)]" },
{ status: "todo", label: "Todo", tone: "text-[color:var(--warn)]", dotColor: "bg-[color:var(--warn)]" },
{ status: "in_progress", label: "In Progress", tone: "text-[color:var(--accent)]", dotColor: "bg-[color:var(--accent)]" },
{ status: "in_review", label: "In Review", tone: "text-[color:var(--warn)]", dotColor: "bg-[color:var(--warn)]" },
{ status: "done", label: "Done", tone: "text-[color:var(--success)]", dotColor: "bg-[color:var(--success)]" },
{ status: "blocked", label: "Blocked", tone: "text-[color:var(--danger)]", dotColor: "bg-[color:var(--danger)]" },
];
const priorityColors: Record<string, string> = {
critical: "border-l-[color:var(--danger)]",
high: "border-l-[color:var(--warn)]",
medium: "border-l-[color:var(--accent)]",
low: "border-l-[color:var(--muted)]",
};
export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps) {
const tasksByStatus = useMemo(() => {
const grouped: Record<string, TaskRecord[]> = {};
for (const col of KANBAN_COLUMNS) grouped[col.status] = [];
for (const task of tasks) {
if (task.status === "cancelled") continue;
const bucket = grouped[task.status];
if (bucket) bucket.push(task);
else grouped.backlog?.push(task);
}
// Sort each column by priority
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
for (const col of Object.values(grouped)) {
col.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
}
return grouped;
}, [tasks]);
if (tasks.length === 0) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Briefcase size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No tasks yet</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">Tasks created by agents will appear here on the board</div>
</div>
);
}
return (
<div className="flex gap-3 overflow-x-auto pb-2">
{KANBAN_COLUMNS.map((col) => {
const colTasks = tasksByStatus[col.status] ?? [];
return (
<div key={col.status} className="flex w-[220px] shrink-0 flex-col">
{/* Column header */}
<div className="mb-2 flex items-center gap-2 px-1">
<span className={`h-2 w-2 rounded-full ${col.dotColor}`} />
<span className={`text-[11px] font-semibold uppercase tracking-[0.08em] ${col.tone}`}>{col.label}</span>
<span className="ml-auto rounded-full bg-[color:var(--panel-soft)] px-1.5 py-0.5 text-[10px] font-bold text-[color:var(--muted)]">
{colTasks.length}
</span>
</div>
{/* Cards */}
<div className="flex flex-1 flex-col gap-1.5 rounded-[8px] bg-[color:var(--panel-soft)] p-1.5" style={{ minHeight: 120 }}>
{colTasks.map((task) => {
const assignee = agents.find((a) => a.id === task.assigneeAgentId);
const activeRun = runs.find((r) => r.id === task.activeRunId);
const isRunning = activeRun?.status === "running";
return (
<button
key={task.id}
type="button"
onClick={() => onSelectTask(task.id)}
className={`focus-ring rounded-[6px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-left transition hover:shadow-sm ${priorityColors[task.priority] ?? ""}`}
>
<div className="text-[12px] font-medium leading-snug text-[color:var(--text)]">{task.title}</div>
<div className="mt-1.5 flex items-center gap-2">
{assignee ? (
<span className="flex items-center gap-1 text-[10px] text-[color:var(--muted)]">
<Robot size={10} />
{assignee.name}
</span>
) : (
<span className="text-[10px] italic text-[color:var(--muted)]">Unassigned</span>
)}
{isRunning ? (
<span className="flex items-center gap-1 text-[10px] text-[color:var(--success)]">
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--success)]" />
Running
</span>
) : null}
</div>
{task.priority === "critical" || task.priority === "high" ? (
<div className={`mt-1 text-[9px] font-bold uppercase tracking-[0.12em] ${task.priority === "critical" ? "text-[color:var(--danger)]" : "text-[color:var(--warn)]"}`}>
{task.priority}
</div>
) : null}
</button>
);
})}
</div>
</div>
);
})}
</div>
);
}
+51 -15
View File
@@ -1028,8 +1028,8 @@ export function ProjectEditor({
/* OrgChart – tree layout with pan & zoom */
/* ------------------------------------------------------------------ */
const CARD_W = 200;
const CARD_H = 100;
const CARD_W = 220;
const CARD_H = 110;
const GAP_X = 32;
const GAP_Y = 80;
const CHART_PADDING = 60;
@@ -1252,13 +1252,16 @@ export function OrgChart({
const x2 = child.x + CARD_W / 2;
const y2 = child.y;
const midY = (y1 + y2) / 2;
const isChildRunning = child.agent.status === "running";
return (
<path
key={`${parent.agent.id}-${child.agent.id}`}
d={`M ${x1} ${y1} L ${x1} ${midY} L ${x2} ${midY} L ${x2} ${y2}`}
d={`M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`}
fill="none"
stroke="var(--line-strong)"
strokeWidth={1.5}
stroke={isChildRunning ? "var(--success)" : "var(--line-strong)"}
strokeWidth={isChildRunning ? 2 : 1.5}
strokeDasharray={isChildRunning ? "6 3" : "none"}
opacity={isChildRunning ? 0.8 : 0.5}
/>
);
})}
@@ -1273,32 +1276,65 @@ export function OrgChart({
{allNodes.map((node) => {
const dotColor = statusDotColor[node.agent.status] ?? "#a3a3a3";
const connectorLabel = adapterLabels[node.agent.connectorId] ?? node.agent.connectorId;
const initials = node.agent.name.split(/\s+/).slice(0, 2).map((w) => w[0]).join("").toUpperCase();
const isRunning = node.agent.status === "running";
const isActive = node.agent.status === "active" || isRunning;
const isError = node.agent.status === "error";
const isPending = node.agent.status === "pending_approval";
const isCEO = node.children.length > 0 && !node.agent.reportsTo;
return (
<div
key={node.agent.id}
data-org-card
className="absolute cursor-pointer select-none rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] shadow-sm transition-[box-shadow,border-color] duration-150 hover:border-[color:var(--line-strong)] hover:shadow-md"
className={`absolute cursor-pointer select-none rounded-[10px] border bg-[color:var(--panel)] shadow-sm transition-all duration-200 hover:shadow-lg ${
isRunning ? "border-[color:var(--success)] shadow-[0_0_12px_rgba(74,222,128,0.15)]" :
isError ? "border-[color:var(--danger)] border-opacity-50" :
isPending ? "border-[color:var(--warn)] border-dashed" :
"border-[color:var(--line)] hover:border-[color:var(--line-strong)]"
}`}
style={{ left: node.x, top: node.y, width: CARD_W, minHeight: CARD_H }}
onClick={() => onSelectAgent?.(node.agent.id)}
>
{/* Top accent bar for role hierarchy */}
<div className={`h-[3px] rounded-t-[9px] ${
isCEO ? "bg-[color:var(--accent)]" :
node.children.length > 0 ? "bg-[color:var(--warn)]" :
"bg-[color:var(--muted)] opacity-30"
}`} />
<div className="flex items-center gap-3 px-4 py-3">
<div className="relative shrink-0">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-[color:var(--panel-soft)]">
<Robot size={18} className="text-[color:var(--muted-strong)]" />
<div className={`flex h-10 w-10 items-center justify-center rounded-full text-[13px] font-bold ${
isActive ? "bg-[color:var(--success-soft)] text-[color:var(--success)]" :
isError ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" :
"bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
}`}>
{initials}
</div>
<span
className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--panel)]"
style={{ backgroundColor: dotColor }}
/>
{isRunning ? (
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full opacity-40" style={{ backgroundColor: dotColor }} />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full border-2 border-[color:var(--panel)]" style={{ backgroundColor: dotColor }} />
</span>
) : (
<span
className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--panel)]"
style={{ backgroundColor: dotColor }}
/>
)}
</div>
<div className="flex min-w-0 flex-1 flex-col items-start">
<span className="text-[13px] font-semibold leading-tight text-[color:var(--text)]">{node.agent.name}</span>
<span className="mt-0.5 text-[11px] leading-tight text-[color:var(--muted)]">
{node.agent.title || node.agent.role}
</span>
<span className="mono mt-1 text-[10px] leading-tight text-[color:var(--muted)]">
{connectorLabel}
</span>
<div className="mt-1.5 flex items-center gap-2">
<span className="rounded bg-[color:var(--panel-soft)] px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
{connectorLabel}
</span>
{node.agent.heartbeatEnabled ? (
<Heartbeat size={10} className="text-[color:var(--success)]" />
) : null}
</div>
</div>
</div>
</div>
+7 -7
View File
@@ -168,8 +168,8 @@ describe("Step 2: CEO phased prompts", () => {
const prompt = buildSystemPrompt(ctx);
expect(prompt).toContain("Team Building Phase");
expect(prompt).toContain("Design Organization");
expect(prompt).toContain("Submit Org Plan for Approval");
expect(prompt).toContain("Design the Organization Structure");
expect(prompt).toContain("Submit Org Strategy for Approval");
expect(prompt).not.toContain("Awaiting Org Approval");
expect(prompt).not.toContain("Goal Decomposition");
});
@@ -239,7 +239,7 @@ describe("Step 2: CEO phased prompts", () => {
expect(prompt).toContain("Executive / CEO");
expect(prompt).toContain("Goal Decomposition");
expect(prompt).toContain("Delegation");
expect(prompt).toContain("Credential Escalation");
expect(prompt).toContain("Unblock & Coordinate");
expect(prompt).not.toContain("Team Building Phase");
expect(prompt).not.toContain("Awaiting Org Approval");
});
@@ -289,8 +289,8 @@ describe("Step 2: CEO phased prompts", () => {
});
const prompt = buildSystemPrompt(ctx);
expect(prompt).toContain("Do NOT create secret_access approvals directly");
expect(prompt).toContain("escalate upward");
expect(prompt).toContain("escalate by commenting on YOUR parent task");
expect(prompt).toContain("Break down work and delegate it");
});
it("IC prompt includes credential escalation guidance", () => {
@@ -305,8 +305,8 @@ describe("Step 2: CEO phased prompts", () => {
});
const prompt = buildSystemPrompt(ctx);
expect(prompt).toContain("Your manager will be notified and help resolve it");
expect(prompt).toContain("Only create a secret_access approval directly if you have NO manager");
expect(prompt).toContain("Your manager is auto-notified and will help");
expect(prompt).toContain("comment explaining what API key/account you need");
});
});