From d5a76658935a8bee2b203479e3c3fc6aad9a5e09 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?=
Date: Sun, 7 Jun 2026 13:41:53 -0400
Subject: [PATCH] Create project collaboration surface
---
.../src/main/services/onboarding-service.ts | 25 ++++++-
.../src/renderer/src/pages/WorkPage.tsx | 4 ++
.../src/pages/work/ProjectOverview.tsx | 72 +++++++++++++++++++
AgentCompany-New/tests/e2e/desktop.spec.ts | 4 ++
.../tests/unit/domain-services.test.ts | 4 ++
5 files changed, 108 insertions(+), 1 deletion(-)
create mode 100644 AgentCompany-New/src/renderer/src/pages/work/ProjectOverview.tsx
diff --git a/AgentCompany-New/src/main/services/onboarding-service.ts b/AgentCompany-New/src/main/services/onboarding-service.ts
index cfb47d60..4c3722dd 100644
--- a/AgentCompany-New/src/main/services/onboarding-service.ts
+++ b/AgentCompany-New/src/main/services/onboarding-service.ts
@@ -9,6 +9,7 @@ import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
import { OrganizationService } from "./organization-service";
+import { ChatService } from "./chat-service";
interface OnboardingServiceDeps {
db: DatabaseSync;
@@ -18,10 +19,12 @@ interface OnboardingServiceDeps {
export class OnboardingService {
private readonly audit: AuditService;
private readonly organization: OrganizationService;
+ private readonly chat: ChatService;
constructor(private readonly deps: OnboardingServiceDeps) {
this.audit = new AuditService(deps.db);
this.organization = new OrganizationService(deps.db);
+ this.chat = new ChatService(deps.db);
}
bootstrap(input: BootstrapOnboardingInput): BootstrapOnboardingSummary {
@@ -65,6 +68,7 @@ export class OnboardingService {
});
const goalId = this.createGoal(orgId, ceoAgentId, input, now);
const projectId = this.createProject(orgId, goalId, ceoAgentId, input, now);
+ this.createProjectChatSpace(orgId, projectName(input));
if (workspaceId) this.bindWorkspaceToProject(workspaceId, projectId, now);
const taskId = this.createKickoffTask(orgId, goalId, projectId, workspaceId, ceoAgentId, input, now);
const runId = this.createTaskRun(orgId, taskId, ceoAgentId, providerId, workspaceId, now);
@@ -143,7 +147,7 @@ export class OnboardingService {
projectId,
orgId,
goalId,
- `Execution plan: ${input.goalTitle.trim()}`,
+ projectName(input),
input.companyDescription.trim(),
ceoAgentId,
"in_progress",
@@ -153,6 +157,16 @@ export class OnboardingService {
return projectId;
}
+ private createProjectChatSpace(orgId: string, name: string): string {
+ return this.chat.createSpace({
+ orgId,
+ spaceType: "project",
+ name: projectSpaceName(name),
+ topic: name,
+ visibility: "workspace",
+ });
+ }
+
private bindWorkspaceToProject(workspaceId: string, projectId: string, now: string): void {
this.deps.db.prepare("update workspaces set project_id = ?, updated_at = ? where id = ?")
.run(projectId, now, workspaceId);
@@ -250,3 +264,12 @@ export class OnboardingService {
].filter((line) => line.length > 0).join("\n\n");
}
}
+
+function projectName(input: BootstrapOnboardingInput): string {
+ return `Execution plan: ${input.goalTitle.trim()}`;
+}
+
+function projectSpaceName(name: string): string {
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/g, "");
+ return slug || "project";
+}
diff --git a/AgentCompany-New/src/renderer/src/pages/WorkPage.tsx b/AgentCompany-New/src/renderer/src/pages/WorkPage.tsx
index 492b04be..6a42e305 100644
--- a/AgentCompany-New/src/renderer/src/pages/WorkPage.tsx
+++ b/AgentCompany-New/src/renderer/src/pages/WorkPage.tsx
@@ -17,6 +17,7 @@ import {
} from "../../../shared/domain";
import { NewTaskDialog } from "./NewTaskDialog";
import { TaskDetailDialog } from "./work/TaskDetailDialog";
+import { ProjectOverview } from "./work/ProjectOverview";
import { canRun, latestTaskRun, nextStatus, prettyRunState, prettyStatus, providerFor, runTone } from "./work/task-helpers";
import type { PageProps } from "./types";
import { activeOrgId } from "./types";
@@ -64,6 +65,7 @@ export function WorkPage({ snapshot, actions }: PageProps) {
const workspaces = useMemo(() => snapshot.workspaces.filter((workspace) => workspace.orgId === orgId), [snapshot.workspaces, orgId]);
const providers = useMemo(() => snapshot.providers.filter((provider) => provider.orgId === orgId), [snapshot.providers, orgId]);
const taskRuns = useMemo(() => snapshot.taskRuns.filter((run) => run.orgId === orgId), [snapshot.taskRuns, orgId]);
+ const chatSpaces = useMemo(() => snapshot.chatSpaces.filter((space) => space.orgId === orgId), [snapshot.chatSpaces, orgId]);
const advance = (task: TaskRecord) => {
const next = nextStatus(task.status);
@@ -93,6 +95,8 @@ export function WorkPage({ snapshot, actions }: PageProps) {
{tasks.length} {tasks.length === 1 ? "task" : "tasks"} ยท {assigneeCount} {assigneeCount === 1 ? "assignee" : "assignees"}
+
+
{view === "board" && (
+ Projects
+
+ {projects.map((project) => (
+
task.projectId === project.id)}
+ lead={agents.find((agent) => agent.id === project.leadAgentId) ?? null}
+ workspace={workspaces.find((item) => item.projectId === project.id) ?? null}
+ room={chatSpaces.find((space) => space.spaceType === "project" && space.topic === project.name) ?? null}
+ />
+ ))}
+
+
+ );
+}
+
+function ProjectCard({
+ project,
+ tasks,
+ lead,
+ workspace,
+ room,
+}: {
+ project: ProjectRecord;
+ tasks: TaskRecord[];
+ lead: AgentRecord | null;
+ workspace: WorkspaceRecord | null;
+ room: ChatSpaceRecord | null;
+}) {
+ const done = tasks.filter((task) => task.status === TASK_STATUS.DONE).length;
+ return (
+
+
+
+
{project.name}
+
+
{project.status.replace(/_/g, " ")}
+
{done} / {tasks.length} tasks
+ {workspace &&
{workspace.name}}
+ {room &&
#{room.name}}
+
+
+ {lead && (
+
+ )}
+
+
+ );
+}
diff --git a/AgentCompany-New/tests/e2e/desktop.spec.ts b/AgentCompany-New/tests/e2e/desktop.spec.ts
index f47d9792..47d7c23d 100644
--- a/AgentCompany-New/tests/e2e/desktop.spec.ts
+++ b/AgentCompany-New/tests/e2e/desktop.spec.ts
@@ -34,6 +34,10 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
await page.getByRole("button", { name: "Launch" }).click();
await expect(page.getByRole("button", { name: /Acme Operations/ })).toBeVisible();
+ await page.getByRole("button", { name: "Work", exact: true }).click();
+ await expect(page.getByText("Projects", { exact: true })).toBeVisible();
+ await expect(page.getByText("Execution plan: Ship a verified control plane").first()).toBeVisible();
+ await expect(page.getByText("#execution-plan-ship-a-verified-control-plane").first()).toBeVisible();
await page.getByRole("button", { name: "Team" }).click();
await page.getByRole("button", { name: "Hire" }).click();
await page.getByLabel("Name").fill("Alex Chen");
diff --git a/AgentCompany-New/tests/unit/domain-services.test.ts b/AgentCompany-New/tests/unit/domain-services.test.ts
index ccd1c0fb..9908baef 100644
--- a/AgentCompany-New/tests/unit/domain-services.test.ts
+++ b/AgentCompany-New/tests/unit/domain-services.test.ts
@@ -617,6 +617,10 @@ describe("domain services", () => {
expect(state.workspaces).toHaveLength(1);
expect(state.goals[0]?.status).toBe("active");
expect(state.projects[0]?.leadAgentId).toBe(state.agents[0]?.id);
+ const projectRoom = state.chatSpaces.find((space) => space.spaceType === "project" && space.topic === state.projects[0]?.name);
+ expect(projectRoom).toMatchObject({ name: "execution-plan-ship-a-verified-control-plane", visibility: "workspace" });
+ expect(state.chatMemberships).toContainEqual(expect.objectContaining({ spaceId: projectRoom?.id, principalType: "employee" }));
+ expect(state.chatMemberships).toContainEqual(expect.objectContaining({ spaceId: projectRoom?.id, principalType: "agent", principalId: state.agents[0]?.id }));
expect(state.tasks[0]?.assigneeAgentId).toBe(state.agents[0]?.id);
expect(state.taskRuns[0]?.trigger).toBe("bootstrap");
expect(state.taskRuns[0]?.executionContextId).toMatch(/^ctx_/);