Files
zopu-code/repos/web/lib/work-os/work-unit-projection.ts
-Puter cc47007fa9 Slice 1 polish: archive dormant code, merge work-os into primitives, add futures
- Archive dormant apps (daemon, desktop, native, tui) and superseded
  packages/server into repos/ for reference
- Archive 21 dead web components (chat page shell, work-os cluster, strays)
  into repos/web/; keep reused message-rendering primitives in components/chat
- Merge @code/work-os into @code/primitives; work.ts absorbed via ./work
  subpath and barrel export; update all four importers
- Add docs/futures.md tracking audio/video (blocked at Flue + provider),
  web layout cleanup, and message rendering quality
- Repoint dev:zopu script to @code/agents (the live Flue server)
- Note repos/ reference-code convention in AGENTS.md
2026-07-27 16:34:17 +05:30

330 lines
10 KiB
TypeScript

import type { Doc, Id } from "@code/backend/convex/_generated/dataModel";
// ---------------------------------------------------------------------------
// Work Unit card projection — pure functions that turn Convex documents into
// the collapsed/expanded Work Unit views used by the Work OS surface.
//
// Counts are derived from issue-scoped evidence: events for artifacts/steps/
// PRs, and authenticated signalIssueAttachments for linked Signals. We never
// use project-wide artifact lists or signal lists.
// ---------------------------------------------------------------------------
export type ProjectEvent = Doc<"projectEvents">;
export type ProjectIssue = Doc<"projectIssues">;
/** A Signal linked to a Work Unit via signalIssueAttachments. */
export interface LinkedSignal {
readonly signalId: Id<"signals">;
readonly title: string;
readonly summary: string;
readonly sourceCount: number;
readonly createdAt: number;
}
export interface WorkUnitCard {
readonly issueId: Id<"projectIssues">;
readonly number: number;
readonly title: string;
readonly status: ProjectIssue["status"];
readonly summary: string;
readonly signalCount: number;
readonly currentActivity: string | null;
readonly stepCount: number;
readonly artifactCount: number;
readonly hasPullRequest: boolean;
readonly needsInput: boolean;
readonly updatedAt: number;
}
export interface WorkUnitActivityItem {
readonly label: string;
readonly detail: string;
readonly time: string;
readonly kind: string;
readonly createdAt: number;
}
export interface WorkUnitDetail {
readonly issue: ProjectIssue;
readonly activity: readonly WorkUnitActivityItem[];
readonly stepCount: number;
readonly artifactCount: number;
readonly artifactPaths: readonly string[];
readonly signalCount: number;
readonly linkedSignals: readonly LinkedSignal[];
readonly hasPullRequest: boolean;
readonly pullRequestUrl: string | null;
readonly pullRequestNumber: number | null;
readonly needsInput: boolean;
readonly latestSummary: string | null;
}
// ---------------------------------------------------------------------------
// Time formatting
// ---------------------------------------------------------------------------
export const formatRelativeTime = (timestamp: number): string => {
const minutes = Math.max(0, Math.round((Date.now() - timestamp) / 60_000));
if (minutes < 1) {
return "just now";
}
if (minutes < 60) {
return `${minutes}m ago`;
}
const hours = Math.round(minutes / 60);
if (hours < 24) {
return `${hours}h ago`;
}
return `${Math.round(hours / 24)}d ago`;
};
export const formatClockTime = (timestamp: number): string =>
new Intl.DateTimeFormat(undefined, {
hour: "numeric",
minute: "2-digit",
}).format(timestamp);
// ---------------------------------------------------------------------------
// Event filtering
// ---------------------------------------------------------------------------
export const eventsForIssue = (
events: readonly ProjectEvent[] | undefined,
issueId: Id<"projectIssues">
): readonly ProjectEvent[] =>
(events ?? []).filter((e) => e.issueId === issueId);
// ---------------------------------------------------------------------------
// Event-to-activity projection
// ---------------------------------------------------------------------------
interface ActivityProjection {
readonly label: string;
readonly detail: string;
}
const getString = (
data: Record<string, unknown>,
key: string,
fallback: string
): string => (typeof data[key] === "string" ? (data[key] as string) : fallback);
const ACTIVITY_HANDLERS: Record<
string,
(data: Record<string, unknown>, event: ProjectEvent) => ActivityProjection
> = {
"agent.completed": (data) => ({
detail: getString(data, "summary", "Run completed successfully"),
label: "Work completed",
}),
"agent.failed": (data) => ({
detail: getString(data, "error", "The run stopped unexpectedly"),
label: "Run failed",
}),
"agent.needs-input": () => ({
detail: "Waiting for a decision or missing information",
label: "Needs your input",
}),
"agent.working": () => ({
detail: "The project manager is progressing the issue",
label: "Agent working",
}),
"agent.workspace.created": (data) => ({
detail: `Workspace ready on ${getString(data, "branchName", "work branch")}`,
label: "Workspace prepared",
}),
"artifact.updated": (data) => ({
detail: `Evidence revised in ${getString(data, "path", "artifact")} (r${data.revision ?? "?"})`,
label: "Evidence updated",
}),
"gitea.lifecycle.updated": (data) => ({
detail: `${getString(data, "status", "updated")} on ${getString(data, "branch", "branch")}`,
label: "Git lifecycle",
}),
"gitea.pull_request.created": (data) => {
const pr = data.pullRequest as { number?: number } | undefined;
return {
detail: `Pull request #${pr?.number ?? "?"} opened for review`,
label: "Pull request opened",
};
},
"issue.created": (data) => ({
detail: `Issue #${data.number ?? "?"} entered the loop`,
label: "Work created",
}),
"issue.queued": (data) => ({
detail: `Queued #${data.number ?? "?"} for the project manager`,
label: "Work queued",
}),
"signal.attached": (data) => ({
detail: `Signal "${getString(data, "signalProblemTitle", "Unknown")}" linked to this Work Unit`,
label: "Signal linked",
}),
};
const projectEventToActivity = (event: ProjectEvent): ActivityProjection => {
const data = (event.data ?? {}) as Record<string, unknown>;
const handler = ACTIVITY_HANDLERS[event.kind];
if (handler) {
return handler(data, event);
}
return { detail: event.kind, label: "Activity" };
};
export const buildActivityTimeline = (
issueEvents: readonly ProjectEvent[]
): readonly WorkUnitActivityItem[] =>
[...issueEvents]
.toSorted((a, b) => b.createdAt - a.createdAt)
.map((event) => {
const projection = projectEventToActivity(event);
return {
createdAt: event.createdAt,
detail: projection.detail,
kind: event.kind,
label: projection.label,
time: formatRelativeTime(event.createdAt),
};
});
// ---------------------------------------------------------------------------
// PR extraction from issue-scoped events
// ---------------------------------------------------------------------------
export interface PullRequestInfo {
readonly hasPR: boolean;
readonly url: string | null;
readonly number: number | null;
}
export const extractPullRequestFromEvents = (
issueEvents: readonly ProjectEvent[]
): PullRequestInfo => {
const prEvent = issueEvents.find(
(e) => e.kind === "gitea.pull_request.created"
);
if (!prEvent) {
return { hasPR: false, number: null, url: null };
}
const data = prEvent.data as {
pullRequest?: { url?: string; number?: number };
};
const pr = data.pullRequest;
return {
hasPR: true,
number: pr?.number ?? null,
url: pr?.url ?? null,
};
};
// ---------------------------------------------------------------------------
// Latest agent summary
// ---------------------------------------------------------------------------
export const extractLatestSummary = (
issueEvents: readonly ProjectEvent[]
): string | null => {
const summaryEvent = [...issueEvents]
.toSorted((a, b) => b.createdAt - a.createdAt)
.find((e) => e.kind === "agent.completed" || e.kind === "agent.failed");
if (!summaryEvent) {
return null;
}
const data = summaryEvent.data as { summary?: string; error?: string };
if (summaryEvent.kind === "agent.completed") {
return data.summary ?? null;
}
return data.error ?? null;
};
// ---------------------------------------------------------------------------
// Artifact path extraction from issue-scoped artifact.updated events
// ---------------------------------------------------------------------------
export const extractArtifactPaths = (
issueEvents: readonly ProjectEvent[]
): readonly string[] => {
const paths = new Set<string>();
for (const event of issueEvents) {
if (event.kind !== "artifact.updated") {
continue;
}
const data = event.data as { path?: string };
if (typeof data.path === "string") {
paths.add(data.path);
}
}
return [...paths].toSorted();
};
// ---------------------------------------------------------------------------
// Card builder — derives counts from issue-scoped events + linked signals
// ---------------------------------------------------------------------------
export const buildWorkUnitCard = ({
issue,
issueEvents,
linkedSignals,
}: {
readonly issue: ProjectIssue;
readonly issueEvents: readonly ProjectEvent[];
readonly linkedSignals: readonly LinkedSignal[];
}): WorkUnitCard => {
const sorted = [...issueEvents].toSorted((a, b) => b.createdAt - a.createdAt);
const [latestEvent] = sorted;
const currentActivity = latestEvent
? projectEventToActivity(latestEvent).label
: null;
const prInfo = extractPullRequestFromEvents(issueEvents);
const summary = extractLatestSummary(issueEvents) ?? issue.body.slice(0, 140);
const artifactPaths = extractArtifactPaths(issueEvents);
return {
artifactCount: artifactPaths.length,
currentActivity,
hasPullRequest: prInfo.hasPR,
issueId: issue._id,
needsInput: issue.status === "needs-input",
number: issue.number,
signalCount: linkedSignals.length,
status: issue.status,
stepCount: issueEvents.length,
summary,
title: issue.title,
updatedAt: issue.updatedAt,
};
};
// ---------------------------------------------------------------------------
// Detail builder — same per-issue derivation, plus linked signals
// ---------------------------------------------------------------------------
export const buildWorkUnitDetail = ({
issue,
issueEvents,
linkedSignals,
}: {
readonly issue: ProjectIssue;
readonly issueEvents: readonly ProjectEvent[];
readonly linkedSignals: readonly LinkedSignal[];
}): WorkUnitDetail => {
const activity = buildActivityTimeline(issueEvents);
const prInfo = extractPullRequestFromEvents(issueEvents);
const artifactPaths = extractArtifactPaths(issueEvents);
return {
activity,
artifactCount: artifactPaths.length,
artifactPaths,
hasPullRequest: prInfo.hasPR,
issue,
latestSummary: extractLatestSummary(issueEvents),
linkedSignals,
needsInput: issue.status === "needs-input",
pullRequestNumber: prInfo.number,
pullRequestUrl: prInfo.url,
signalCount: linkedSignals.length,
stepCount: issueEvents.length,
};
};