Add goal, project, and workspace CRUD

Full-stack: IPC channels, command types, OrganizationService methods,
IPC handlers, preload bridge, desktop-api types, and UI dialogs on the
Work page. Users can now create goals and projects directly instead of
relying on the onboarding bootstrap.
This commit is contained in:
ZacharyZhang-NY committed 2026-06-07 17:36:25 -04:00
1 parent 2ef721774f
commit 5f92cd0d75
9 files changed
+218 -1

No files matched your search

@@ -16,6 +16,8 @@ import {
type WorkspaceRecord,
} from "../../../shared/domain";
import { NewTaskDialog } from "./NewTaskDialog";
import { GoalDialog } from "./work/GoalDialog";
import { ProjectDialog } from "./work/ProjectDialog";
import { TaskDetailDialog } from "./work/TaskDetailDialog";
import { ProjectOverview } from "./work/ProjectOverview";
import { canRun, latestTaskRun, nextStatus, prettyRunState, prettyStatus, providerFor, runTone } from "./work/task-helpers";
@@ -41,6 +43,8 @@ export function WorkPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const [view, setView] = useState<View>("board");
const [newTaskOpen, setNewTaskOpen] = useState(false);
const [newGoalOpen, setNewGoalOpen] = useState(false);
const [newProjectOpen, setNewProjectOpen] = useState(false);
const [priorityFilter, setPriorityFilter] = useState<TaskPriority | "all">("all");
const [assigneeFilter, setAssigneeFilter] = useState<string>("all");
const [detailTaskId, setDetailTaskId] = useState<string | null>(null);
@@ -88,6 +92,8 @@ export function WorkPage({ snapshot, actions }: PageProps) {
onPriority={setPriorityFilter}
onAssignee={setAssigneeFilter}
/>
<Button tone="ghost" onClick={() => setNewGoalOpen(true)}><Icon name="plus" size={13} /> Goal</Button>
<Button tone="ghost" onClick={() => setNewProjectOpen(true)}><Icon name="plus" size={13} /> Project</Button>
<Button tone="primary" onClick={() => setNewTaskOpen(true)}><Icon name="plus" size={13} /> New task</Button>
</TopbarActions>
<h1 className="page-h1">Work</h1>
@@ -124,6 +130,8 @@ export function WorkPage({ snapshot, actions }: PageProps) {
)}
{view === "timeline" && <TimelineView tasks={tasks} goals={goals} />}
<GoalDialog open={newGoalOpen} orgId={orgId} agents={agents} actions={actions} onClose={() => setNewGoalOpen(false)} />
<ProjectDialog open={newProjectOpen} orgId={orgId} goals={goals} agents={agents} actions={actions} onClose={() => setNewProjectOpen(false)} />
<NewTaskDialog open={newTaskOpen} orgId={orgId} snapshot={snapshot} actions={actions} onClose={() => setNewTaskOpen(false)} />
<TaskDetailDialog
task={detailTask}
@@ -0,0 +1,51 @@
import { type FormEvent } from "react";
import { Field } from "../../shared/ui";
import { Dialog } from "../../shared/Dialog";
import type { AgentRecord, AppSnapshot } from "../../../../shared/domain";
import type { SnapshotActions } from "../../app/useSnapshot";
function text(form: FormData, key: string): string {
return String(form.get(key) ?? "").trim();
}
function optional(form: FormData, key: string): string | null {
return text(form, key) || null;
}
export function GoalDialog({
open,
orgId,
agents,
actions,
onClose,
}: {
open: boolean;
orgId: string;
agents: AgentRecord[];
actions: SnapshotActions;
onClose: () => void;
}) {
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
void actions.run(() => window.agentCompany.createGoal({
orgId,
title: text(form, "title"),
description: text(form, "description"),
ownerAgentId: optional(form, "ownerAgentId"),
})).then((result) => { if (result) onClose(); });
};
return (
<Dialog title="New goal" open={open} submitLabel="Create" onClose={onClose} onSubmit={submit}>
<Field label="Title"><input className="input" name="title" required /></Field>
<Field label="Description"><textarea className="input" name="description" required /></Field>
<Field label="Owner agent">
<select className="input" name="ownerAgentId" defaultValue="">
<option value="">Unassigned</option>
{agents.map((agent) => <option key={agent.id} value={agent.id}>{agent.name} · {agent.role}</option>)}
</select>
</Field>
</Dialog>
);
}
@@ -0,0 +1,60 @@
import { type FormEvent } from "react";
import { Field } from "../../shared/ui";
import { Dialog } from "../../shared/Dialog";
import type { AgentRecord, GoalRecord } from "../../../../shared/domain";
import type { SnapshotActions } from "../../app/useSnapshot";
function text(form: FormData, key: string): string {
return String(form.get(key) ?? "").trim();
}
function optional(form: FormData, key: string): string | null {
return text(form, key) || null;
}
export function ProjectDialog({
open,
orgId,
goals,
agents,
actions,
onClose,
}: {
open: boolean;
orgId: string;
goals: GoalRecord[];
agents: AgentRecord[];
actions: SnapshotActions;
onClose: () => void;
}) {
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
void actions.run(() => window.agentCompany.createProject({
orgId,
goalId: optional(form, "goalId"),
name: text(form, "name"),
description: text(form, "description"),
leadAgentId: optional(form, "leadAgentId"),
})).then((result) => { if (result) onClose(); });
};
return (
<Dialog title="New project" open={open} submitLabel="Create" onClose={onClose} onSubmit={submit}>
<Field label="Name"><input className="input" name="name" required /></Field>
<Field label="Description"><textarea className="input" name="description" required /></Field>
<Field label="Goal">
<select className="input" name="goalId" defaultValue="">
<option value="">None</option>
{goals.map((goal) => <option key={goal.id} value={goal.id}>{goal.title}</option>)}
</select>
</Field>
<Field label="Lead agent">
<select className="input" name="leadAgentId" defaultValue="">
<option value="">Unassigned</option>
{agents.map((agent) => <option key={agent.id} value={agent.id}>{agent.name} · {agent.role}</option>)}
</select>
</Field>
</Dialog>
);
}