update
This commit is contained in:
1 parent
53607f142d
commit
d067589276
6 files changed
+1152
-347
No files matched your search
@@ -835,5 +835,106 @@ export class AgentApiServer {
|
|||||||
const projects = snapshot.projects.filter((p) => p.companyId === params.companyId);
|
const projects = snapshot.projects.filter((p) => p.companyId === params.companyId);
|
||||||
this.json(res, 200, projects);
|
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
@@ -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
|
// API instructions
|
||||||
sections.push(`# AgentCompany API
|
sections.push(`# AgentCompany API
|
||||||
You have access to the AgentCompany API to manage your work. Base URL: ${ctx.apiUrl}
|
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
|
GET /api/agents/me
|
||||||
Returns your agent record with chain of command.
|
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
|
### List Your Assigned Tasks
|
||||||
GET /api/companies/{companyId}/issues?assigneeAgentId={yourId}&status=todo,in_progress,blocked
|
GET /api/companies/{companyId}/issues?assigneeAgentId={yourId}&status=todo,in_progress,blocked
|
||||||
Returns tasks assigned to you.
|
Returns tasks assigned to you.
|
||||||
@@ -237,22 +305,37 @@ GET /api/companies/{companyId}/projects
|
|||||||
// Phase 0 — Team Building
|
// Phase 0 — Team Building
|
||||||
sections.push(`# Operating Instructions (CEO — Team Building Phase)
|
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
|
## Step 1: Analyze the Mission
|
||||||
- Check Active Goals to understand the company direction.
|
- Read ALL Active Goals carefully to understand the company direction.
|
||||||
|
- Identify what departments are needed to achieve these goals.
|
||||||
|
|
||||||
Step 2: Design Organization
|
## Step 2: Design the Organization Structure
|
||||||
- Based on goals, determine what C-suite roles are needed (CTO/CMO/CPO etc.)
|
Based on goals, design a complete org chart. A mature company needs:
|
||||||
- Use POST /api/companies/{companyId}/agents to submit hire requests
|
- **CTO / VP Engineering** — owns all technical execution (if goals involve code/product)
|
||||||
- Each role needs clear capabilities, connector, workspace
|
- **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
|
## Step 3: Submit Hire Requests
|
||||||
- Create an approve_ceo_strategy approval with your org structure plan
|
For EACH role, use POST /api/companies/{companyId}/agents with:
|
||||||
- Use GET /api/approvals/{id} to check approval status
|
- A clear, professional name
|
||||||
- Do NOT delegate work or create projects until the Board approves
|
- 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) {
|
} else if (hasPendingOrgApproval) {
|
||||||
// Phase 1 — Awaiting Org Approval
|
// 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
|
// Phase 2 — Normal Operations
|
||||||
sections.push(`# Operating Instructions (Executive / CEO)
|
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.
|
## Priority 1: Standup Check (EVERY wake)
|
||||||
- Create projects with clear scope and assign a lead from your direct reports.
|
Before doing anything else, run a mental standup:
|
||||||
- Each project should have a goal linked to it.
|
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.
|
## Priority 2: Review Completed Work (SOP-004)
|
||||||
- Match tasks to the agent whose role fits best.
|
When tasks from your reports have status "in_review":
|
||||||
- CTO/Engineering Lead → technical tasks
|
- Read ALL comments to understand what was done
|
||||||
- CMO/Marketing Lead → marketing, growth, content tasks
|
- If satisfactory: update status to "done" + approving comment
|
||||||
- CPO/Product Lead → product design, UX, feature scoping
|
- 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
|
- 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:
|
## Priority 4: Unblock & Coordinate
|
||||||
- Check which tasks/projects from your reports have status "in_review" or "done".
|
- Read comments from reports on blocked tasks
|
||||||
- Read comments on those tasks to understand what was accomplished.
|
- Create secret_access approvals for credential needs
|
||||||
- If satisfactory: update the task status to "done" and leave an approving comment.
|
- Escalate to Board for anything requiring human judgment
|
||||||
- If changes are needed: post a comment with feedback and set the task back to "in_progress".
|
|
||||||
|
|
||||||
4. **Coordination**: Check on project progress. Read comments from your reports.
|
## Priority 5: Grow the Team
|
||||||
If a report is blocked or needs credentials, either:
|
When workload exceeds capacity or new capabilities are needed:
|
||||||
a. Resolve it yourself if you can
|
- Use POST /api/companies/{companyId}/agents to propose hires
|
||||||
b. Escalate to the Board by creating an approval request
|
- 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:
|
## Priority 6: Strategic Reporting
|
||||||
- Create a secret_access approval for the Board
|
After each run, leave a concise status update on your primary task covering:
|
||||||
- After approval, notify the requesting agent via comment
|
- What was reviewed/delegated/unblocked
|
||||||
|
- Key decisions made
|
||||||
|
- Any items needing Board attention
|
||||||
|
|
||||||
6. **Hiring**: When the team needs additional members:
|
CRITICAL: You are the CEO. DELEGATE work — do NOT try to code, write content, or design yourself. Your job is to ORCHESTRATE.
|
||||||
- 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.
|
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
} else if (isManager) {
|
} else if (isManager) {
|
||||||
// Step 2d: Manager with credential escalation instruction
|
// Step 2d: Manager with credential escalation instruction
|
||||||
sections.push(`# Operating Instructions (Manager)
|
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.
|
## Priority 1: Standup Check (EVERY wake — SOP-001)
|
||||||
- Use POST /api/companies/{companyId}/issues to create subtasks.
|
1. Check tasks with status "in_review" from your reports — these MUST be reviewed first
|
||||||
- Set assigneeAgentId to one of your direct reports.
|
2. Check for "blocked" tasks from your reports — second priority
|
||||||
- Set parentId to link subtasks to the parent task.
|
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.
|
## Priority 2: Review Completed Work (SOP-004)
|
||||||
- Match each report's capabilities to the right subtask.
|
This is your CORE DUTY — reviews unblock your team's progress:
|
||||||
- Each subtask should have a clear title and description explaining what needs to be done.
|
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:
|
## Priority 3: Task Breakdown & Delegation (SOP-003)
|
||||||
- Check which subtasks have status "in_review" or "done".
|
When assigned a project or high-level task:
|
||||||
- Read comments on those tasks to understand what was done.
|
1. Analyze the work and break it into 3-7 specific subtasks
|
||||||
- If the work is satisfactory: update the subtask status to "done" and leave an approving comment.
|
2. Each subtask needs: clear title, detailed description with acceptance criteria, priority
|
||||||
- If changes are needed: post a comment explaining what needs to change, and set the subtask status back to "in_progress".
|
3. Assign each to the best-fit report based on their capabilities
|
||||||
- This is your core management duty — do NOT skip reviews.
|
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.
|
## Priority 4: Unblock Your Reports
|
||||||
- POST /api/issues/{issueId}/comments to ask a question or give feedback on any task.
|
When reports post comments about blockers:
|
||||||
- The task assignee will be automatically woken and notified.
|
- 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.
|
## Priority 5: Progress Reporting
|
||||||
- GET /api/companies/{companyId}/issues to see all tasks and their statuses.
|
After each run, update your task status and leave a summary for your manager:
|
||||||
- GET /api/issues/{issueId}/comments to read discussion on specific tasks.
|
- 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:
|
CRITICAL: You are a manager. Break down work and delegate it. Do NOT try to do IC work yourself unless you have no reports.
|
||||||
- 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.
|
|
||||||
`);
|
`);
|
||||||
} else {
|
} else {
|
||||||
// Step 2d: IC with credential escalation instruction
|
// Step 2d: IC with credential escalation instruction
|
||||||
sections.push(`# Operating Instructions (Individual Contributor)
|
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.
|
## Priority 1: Check for Feedback (EVERY wake — SOP-001)
|
||||||
- Focus on tasks that match your role and capabilities.
|
1. Read ALL new comments on your assigned tasks
|
||||||
- Use the tools available in your workspace (CLI, code editors, etc.) to produce deliverables.
|
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.
|
## Priority 2: Execute Your Highest-Priority Task
|
||||||
- POST /api/issues/{issueId}/checkout to claim the 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:
|
## Task Execution Workflow (SOP-002)
|
||||||
- Create subtasks for yourself using POST /api/companies/{companyId}/issues.
|
- ALWAYS checkout before working
|
||||||
- Set assigneeAgentId to your own ID and parentId to the original task.
|
- For large tasks: create subtasks assigned to yourself (parentId = original task)
|
||||||
- This helps you track progress on multi-step work.
|
- 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.
|
## Handling Blockers (SOP-006)
|
||||||
- POST /api/issues/{issueId}/comments with a summary of what you did.
|
If you're stuck:
|
||||||
- Your manager will be automatically notified of your comments.
|
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".
|
## Handling Review Feedback
|
||||||
- PATCH /api/issues/{issueId} with status "in_review" and a comment describing what was done.
|
When your task is set back to "in_progress" with feedback:
|
||||||
- Your manager will be automatically notified to review your work.
|
1. Read ALL feedback comments carefully
|
||||||
- Wait for your manager's feedback before marking as "done".
|
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:
|
## Quality Standards
|
||||||
- Post a comment on your task with your question.
|
- Always test your work before submitting for review
|
||||||
- Your manager will be notified and can respond.
|
- Leave clear comments so your manager can understand what was done
|
||||||
- Check comments on your task for any feedback or answers.
|
- If requirements are ambiguous, ask first rather than guessing
|
||||||
|
|
||||||
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.
|
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+52
-214
@@ -36,6 +36,7 @@ import type {
|
|||||||
import type { SidebarBadges } from "@shared/contracts";
|
import type { SidebarBadges } from "@shared/contracts";
|
||||||
import { CompanyOnboarding } from "./components/CompanyOnboarding";
|
import { CompanyOnboarding } from "./components/CompanyOnboarding";
|
||||||
import { ConnectorEditor } from "./components/connectors";
|
import { ConnectorEditor } from "./components/connectors";
|
||||||
|
import { Dashboard, KanbanBoard } from "./components/dashboard";
|
||||||
import { EntityWorkspace } from "./components/EntityWorkspace";
|
import { EntityWorkspace } from "./components/EntityWorkspace";
|
||||||
import {
|
import {
|
||||||
ActivityList,
|
ActivityList,
|
||||||
@@ -128,6 +129,7 @@ export function App() {
|
|||||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
||||||
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null);
|
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null);
|
||||||
const [selectedProjectId, setSelectedProjectId] = 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 logBufferRef = useRef<Record<string, string>>({});
|
||||||
const [, setLogRevision] = useState(0);
|
const [, setLogRevision] = useState(0);
|
||||||
const [badges, setBadges] = useState<SidebarBadges | null>(null);
|
const [badges, setBadges] = useState<SidebarBadges | null>(null);
|
||||||
@@ -530,218 +532,27 @@ export function App() {
|
|||||||
className="space-y-14"
|
className="space-y-14"
|
||||||
>
|
>
|
||||||
{section === "overview" ? (
|
{section === "overview" ? (
|
||||||
<>
|
<Dashboard
|
||||||
{/* Status metrics */}
|
agents={companyAgents}
|
||||||
<section className="space-y-3">
|
tasks={tasks}
|
||||||
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Current status</div>
|
runs={runs}
|
||||||
<div className="grid grid-cols-2 border-y border-[color:var(--line)] lg:grid-cols-4">
|
approvals={approvals}
|
||||||
{overviewRows.map((row, index) => (
|
goals={companyGoals}
|
||||||
<div
|
projects={companyProjects}
|
||||||
key={row.label}
|
activity={filteredActivity}
|
||||||
className={`px-4 py-4 lg:px-5 lg:first:pl-0 lg:last:pr-0 ${
|
costs={companyCosts}
|
||||||
index > 0 ? "border-l border-[color:var(--line)]" : ""
|
connectors={snapshot.connectors}
|
||||||
} ${index >= 2 ? "border-t border-[color:var(--line)] lg:border-t-0" : ""}`}
|
onNavigate={(targetSection, entityId) => {
|
||||||
>
|
setSection(targetSection);
|
||||||
<div className="text-[12px] font-medium text-[color:var(--muted)]">{row.label}</div>
|
if (entityId) {
|
||||||
<div className={`mono text-[18px] font-semibold tracking-[-0.01em] sm:mt-2 ${row.tone || "text-[color:var(--text)]"}`}>{row.value}</div>
|
if (targetSection === "agents") setSelectedAgentId(entityId);
|
||||||
</div>
|
else if (targetSection === "tasks") setSelectedTaskId(entityId);
|
||||||
))}
|
else if (targetSection === "approvals") setSelectedApprovalId(entityId);
|
||||||
</div>
|
else if (targetSection === "goals") setSelectedGoalId(entityId);
|
||||||
</section>
|
else if (targetSection === "projects") setSelectedProjectId(entityId);
|
||||||
|
}
|
||||||
{/* 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>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{section === "connectors" && selectedConnector ? (
|
{section === "connectors" && selectedConnector ? (
|
||||||
@@ -864,8 +675,35 @@ export function App() {
|
|||||||
|
|
||||||
{section === "tasks" ? (
|
{section === "tasks" ? (
|
||||||
<>
|
<>
|
||||||
<Panel title="Task queue" action={<ActionButton label="New task" icon={ListChecks} tone="accent" onClick={() => setSelectedTaskId("__new__")} />}>
|
<Panel
|
||||||
<TaskQueue tasks={tasks} runs={runs} onSelect={setSelectedTaskId} selectedTaskId={selectedTask?.id ?? null} />
|
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>
|
</Panel>
|
||||||
<Modal open={selectedTaskId === "__new__"} title="New task" onClose={() => setSelectedTaskId(null)}>
|
<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(); }} />
|
<TaskCreator companyId={currentCompany!.id} goals={companyGoals} projects={companyProjects} agents={companyAgents} workspaces={companyWorkspaces} onSaved={async () => { setSelectedTaskId(null); await refresh(); }} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1028,8 +1028,8 @@ export function ProjectEditor({
|
|||||||
/* OrgChart – tree layout with pan & zoom */
|
/* OrgChart – tree layout with pan & zoom */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
const CARD_W = 200;
|
const CARD_W = 220;
|
||||||
const CARD_H = 100;
|
const CARD_H = 110;
|
||||||
const GAP_X = 32;
|
const GAP_X = 32;
|
||||||
const GAP_Y = 80;
|
const GAP_Y = 80;
|
||||||
const CHART_PADDING = 60;
|
const CHART_PADDING = 60;
|
||||||
@@ -1252,13 +1252,16 @@ export function OrgChart({
|
|||||||
const x2 = child.x + CARD_W / 2;
|
const x2 = child.x + CARD_W / 2;
|
||||||
const y2 = child.y;
|
const y2 = child.y;
|
||||||
const midY = (y1 + y2) / 2;
|
const midY = (y1 + y2) / 2;
|
||||||
|
const isChildRunning = child.agent.status === "running";
|
||||||
return (
|
return (
|
||||||
<path
|
<path
|
||||||
key={`${parent.agent.id}-${child.agent.id}`}
|
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"
|
fill="none"
|
||||||
stroke="var(--line-strong)"
|
stroke={isChildRunning ? "var(--success)" : "var(--line-strong)"}
|
||||||
strokeWidth={1.5}
|
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) => {
|
{allNodes.map((node) => {
|
||||||
const dotColor = statusDotColor[node.agent.status] ?? "#a3a3a3";
|
const dotColor = statusDotColor[node.agent.status] ?? "#a3a3a3";
|
||||||
const connectorLabel = adapterLabels[node.agent.connectorId] ?? node.agent.connectorId;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={node.agent.id}
|
key={node.agent.id}
|
||||||
data-org-card
|
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 }}
|
style={{ left: node.x, top: node.y, width: CARD_W, minHeight: CARD_H }}
|
||||||
onClick={() => onSelectAgent?.(node.agent.id)}
|
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="flex items-center gap-3 px-4 py-3">
|
||||||
<div className="relative shrink-0">
|
<div className="relative shrink-0">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-[color:var(--panel-soft)]">
|
<div className={`flex h-10 w-10 items-center justify-center rounded-full text-[13px] font-bold ${
|
||||||
<Robot size={18} className="text-[color:var(--muted-strong)]" />
|
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>
|
</div>
|
||||||
<span
|
{isRunning ? (
|
||||||
className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--panel)]"
|
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5">
|
||||||
style={{ backgroundColor: dotColor }}
|
<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>
|
||||||
<div className="flex min-w-0 flex-1 flex-col items-start">
|
<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="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)]">
|
<span className="mt-0.5 text-[11px] leading-tight text-[color:var(--muted)]">
|
||||||
{node.agent.title || node.agent.role}
|
{node.agent.title || node.agent.role}
|
||||||
</span>
|
</span>
|
||||||
<span className="mono mt-1 text-[10px] leading-tight text-[color:var(--muted)]">
|
<div className="mt-1.5 flex items-center gap-2">
|
||||||
{connectorLabel}
|
<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)]">
|
||||||
</span>
|
{connectorLabel}
|
||||||
|
</span>
|
||||||
|
{node.agent.heartbeatEnabled ? (
|
||||||
|
<Heartbeat size={10} className="text-[color:var(--success)]" />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -168,8 +168,8 @@ describe("Step 2: CEO phased prompts", () => {
|
|||||||
|
|
||||||
const prompt = buildSystemPrompt(ctx);
|
const prompt = buildSystemPrompt(ctx);
|
||||||
expect(prompt).toContain("Team Building Phase");
|
expect(prompt).toContain("Team Building Phase");
|
||||||
expect(prompt).toContain("Design Organization");
|
expect(prompt).toContain("Design the Organization Structure");
|
||||||
expect(prompt).toContain("Submit Org Plan for Approval");
|
expect(prompt).toContain("Submit Org Strategy for Approval");
|
||||||
expect(prompt).not.toContain("Awaiting Org Approval");
|
expect(prompt).not.toContain("Awaiting Org Approval");
|
||||||
expect(prompt).not.toContain("Goal Decomposition");
|
expect(prompt).not.toContain("Goal Decomposition");
|
||||||
});
|
});
|
||||||
@@ -239,7 +239,7 @@ describe("Step 2: CEO phased prompts", () => {
|
|||||||
expect(prompt).toContain("Executive / CEO");
|
expect(prompt).toContain("Executive / CEO");
|
||||||
expect(prompt).toContain("Goal Decomposition");
|
expect(prompt).toContain("Goal Decomposition");
|
||||||
expect(prompt).toContain("Delegation");
|
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("Team Building Phase");
|
||||||
expect(prompt).not.toContain("Awaiting Org Approval");
|
expect(prompt).not.toContain("Awaiting Org Approval");
|
||||||
});
|
});
|
||||||
@@ -289,8 +289,8 @@ describe("Step 2: CEO phased prompts", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const prompt = buildSystemPrompt(ctx);
|
const prompt = buildSystemPrompt(ctx);
|
||||||
expect(prompt).toContain("Do NOT create secret_access approvals directly");
|
expect(prompt).toContain("escalate by commenting on YOUR parent task");
|
||||||
expect(prompt).toContain("escalate upward");
|
expect(prompt).toContain("Break down work and delegate it");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("IC prompt includes credential escalation guidance", () => {
|
it("IC prompt includes credential escalation guidance", () => {
|
||||||
@@ -305,8 +305,8 @@ describe("Step 2: CEO phased prompts", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const prompt = buildSystemPrompt(ctx);
|
const prompt = buildSystemPrompt(ctx);
|
||||||
expect(prompt).toContain("Your manager will be notified and help resolve it");
|
expect(prompt).toContain("Your manager is auto-notified and will help");
|
||||||
expect(prompt).toContain("Only create a secret_access approval directly if you have NO manager");
|
expect(prompt).toContain("comment explaining what API key/account you need");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user