+ {projects.map((project) => {
+ const [source] = project.sources;
+ const selected = selectedId === project.id;
+ return (
+
+ );
+ })}
- );
-};
-
-interface ArtifactGridProps {
- readonly artifacts: readonly Doc<"projectArtifacts">[] | undefined;
-}
-
-const renderArtifacts = (artifacts: ArtifactGridProps["artifacts"]) => {
- if (artifacts === undefined) {
- return
Loading artifacts...
No artifacts yet.
- {artifacts.map((artifact) => (
-
-
-
{artifact.path}
-
- rev {artifact.revision}
+
+
+
+
+
+
+ Project context stays attached to every issue and run.
+
+
+
+
);
};
-const ArtifactGrid = ({ artifacts }: ArtifactGridProps) => (
-
-
- Project artifacts
-
- {renderArtifacts(artifacts)}
-
-);
-
interface IssueComposerProps {
readonly body: string;
readonly busy: boolean;
- readonly onBodyChange: (value: string) => void;
- readonly onSubmit: () => void;
- readonly onTitleChange: (value: string) => void;
+ readonly onIssueBodyChange: (value: string) => void;
+ readonly onIssueSubmit: () => void;
+ readonly onIssueTitleChange: (value: string) => void;
readonly title: string;
}
const IssueComposer = ({
body,
busy,
- onBodyChange,
- onSubmit,
- onTitleChange,
+ onIssueBodyChange,
+ onIssueSubmit,
+ onIssueTitleChange,
title,
}: IssueComposerProps) => (
);
-interface IssueListProps {
- readonly issues: readonly Doc<"projectIssues">[] | undefined;
- readonly pendingAction: string | null;
- readonly onStart: (
+interface IssueCardProps {
+ readonly issue: Doc<"projectIssues">;
+ readonly selected: boolean;
+ readonly onIssueSelect: (issueId: Id<"projectIssues">) => void;
+ readonly onIssueStart: (
issueId: Id<"projectIssues">,
issueNumber: number,
title: string
) => void;
+ readonly pendingAction: string | null;
}
-const IssueList = ({ issues, pendingAction, onStart }: IssueListProps) => {
+const IssueCard = ({
+ issue,
+ onIssueSelect,
+ onIssueStart,
+ pendingAction,
+ selected,
+}: IssueCardProps) => {
+ const canStart = issue.status === "open" || issue.status === "failed";
+ const startPending = pendingAction === `issue:${issue._id}`;
+
+ return (
+
+
+
+
+ Updated{" "}
+ {new Intl.DateTimeFormat(undefined, {
+ hour: "numeric",
+ minute: "2-digit",
+ }).format(issue.updatedAt)}
+
+ {canStart ? (
+
+ ) : null}
+
+
+ );
+};
+
+const IssueList = ({
+ issues,
+ onIssueSelect,
+ onIssueStart,
+ pendingAction,
+ selectedId,
+}: {
+ readonly issues: readonly Doc<"projectIssues">[] | undefined;
+ readonly onIssueSelect: (issueId: Id<"projectIssues">) => void;
+ readonly onIssueStart: IssueCardProps["onIssueStart"];
+ readonly pendingAction: string | null;
+ readonly selectedId: Id<"projectIssues"> | null;
+}) => {
if (issues === undefined) {
- return
Loading issues...
;
+ return
;
}
if (issues.length === 0) {
return (
-
- No issues yet. Create one to start agent work.
-
+
+
+
+ No issues in this project yet
+
+
+ Create a small, outcome-shaped issue and it will become the first
+ visible loop.
+
+
);
}
return (
-
-
Issues
+
{issues.map((issue) => (
-
-
-
- #{issue.number} · {issue.title}
-
-
- {issue.status}
-
-
-
- {issue.body}
-
- {(issue.status === "open" || issue.status === "failed") && (
-
- )}
-
+
))}
);
};
+const Stat = ({
+ label,
+ tone,
+ value,
+}: {
+ readonly label: string;
+ readonly tone: string;
+ readonly value: number;
+}) => (
+
+
+ {label}
+
+
{value}
+
+);
+
+const ProjectStats = ({
+ summary,
+}: {
+ readonly summary: ProjectIssueSummary;
+}) => (
+
+
+
+
+
+);
+
+interface IssueDetailProps {
+ readonly issue: Doc<"projectIssues"> | undefined;
+ readonly onOpenPullRequest: (url: string) => void;
+ readonly onIssueStart: (
+ issueId: Id<"projectIssues">,
+ issueNumber: number,
+ title: string
+ ) => void;
+ readonly pendingAction: string | null;
+ readonly projectLoop: ProjectLoopView | null;
+}
+
+const IssueDetail = ({
+ issue,
+ onOpenPullRequest,
+ onIssueStart,
+ pendingAction,
+ projectLoop,
+}: IssueDetailProps) => {
+ if (!issue || !projectLoop) {
+ return (
+
+ );
+ }
+
+ const canStart = issue.status === "open" || issue.status === "failed";
+ const startPending = pendingAction === `issue:${issue._id}`;
+
+ return (
+
+ );
+};
+
export const ProjectWorkspacePage = () => {
const workspace = useProjectWorkspace();
- const handleRepositoryChange = workspace.setRepository;
- const handleRepositorySubmit = workspace.connectRepository;
- const handleProjectSelect = workspace.setSelectedProjectId;
+ const projectSource = workspace.selectedProject?.sources[0];
const handleIssueBodyChange = workspace.setIssueBody;
+ const handleIssueSelect = workspace.setSelectedIssueId;
+ const handleIssueStart = workspace.startIssue;
const handleIssueSubmit = workspace.raiseIssue;
const handleIssueTitleChange = workspace.setIssueTitle;
- const handleIssueStart = workspace.startIssue;
- const projectSource = workspace.selectedProject?.sources[0];
+ const handleProjectSelect = workspace.setSelectedProjectId;
+ const handleRepositoryChange = workspace.setRepository;
+ const handleRepositorySubmit = workspace.connectRepository;
return (
-
-
-
-
-
+
+
+
+
+ Z
-
-
Project manager
-
- Git · Flue · AgentOS
+
+
+
Zopu
+
+
+ Project loop
+
+
+
+ Connect context, move issues, review outcomes.
@@ -300,89 +706,199 @@ export const ProjectWorkspacePage = () => {
-
-