+ Work Units +
+From 5df8633eaed61aba077aec704a7f2a14dbfd6f9f Mon Sep 17 00:00:00 2001 From: -Puter <22245429+puterhimself@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:59:48 +0530 Subject: [PATCH 1/3] feat(web): Work OS surface with Work Units, Signals, dual-mode composer Lane C implementation of the Zopu Web Work OS for dogfooding: - Work Unit card/detail projections built from per-issue events only (artifact counts from distinct artifact.updated paths, PR from gitea events, activity timeline, agent summaries). Signal linkage is explicitly 0/unavailable until Lane A relations are integrated. - Persistent composer with Project and Work Unit mode switching. Project mode sends to the global Zopu agent; Work Unit mode sends to the issue-scoped project-manager agent identity. - Collapsed Work Unit cards show title, summary, signal/step/artifact counts, current activity, PR indicator, needs-input indicator. - Expanded Work Unit detail shows objective, timeline, artifacts, PR with directly usable link, needs-input alert, start action. - Project-level Signals panel in the sidebar (not attributed to any issue until a signal-to-issue relation exists). - Dark theme with calm, Apple-like visual direction. - 21 projection tests including 5 cross-issue isolation tests and 2 signal isolation tests proving one issue cannot inherit another issue's artifacts, PR, summary, or project signals. - Mobile-responsive: detail overlay on mobile, sidebar on desktop. No backend or schema changes. Uses existing Convex contracts and Flue agent transport throughout. --- .../components/work-os/conversation-panel.tsx | 53 +++ .../src/components/work-os/empty-state.tsx | 54 +++ .../src/components/work-os/signals-panel.tsx | 71 +++ .../components/work-os/work-os-composer.tsx | 143 ++++++ .../src/components/work-os/work-os-header.tsx | 112 +++++ .../src/components/work-os/work-os-page.tsx | 401 +++++++++++++++++ .../src/components/work-os/work-unit-card.tsx | 131 ++++++ .../components/work-os/work-unit-detail.tsx | 219 +++++++++ apps/web/src/hooks/work-os/use-work-os.ts | 179 ++++++++ .../lib/work-os/work-unit-projection.test.ts | 422 ++++++++++++++++++ .../src/lib/work-os/work-unit-projection.ts | 314 +++++++++++++ apps/web/src/root.tsx | 6 +- apps/web/src/routes/app/dashboard/page.tsx | 4 +- packages/ui/src/styles/globals.css | 48 +- 14 files changed, 2128 insertions(+), 29 deletions(-) create mode 100644 apps/web/src/components/work-os/conversation-panel.tsx create mode 100644 apps/web/src/components/work-os/empty-state.tsx create mode 100644 apps/web/src/components/work-os/signals-panel.tsx create mode 100644 apps/web/src/components/work-os/work-os-composer.tsx create mode 100644 apps/web/src/components/work-os/work-os-header.tsx create mode 100644 apps/web/src/components/work-os/work-os-page.tsx create mode 100644 apps/web/src/components/work-os/work-unit-card.tsx create mode 100644 apps/web/src/components/work-os/work-unit-detail.tsx create mode 100644 apps/web/src/hooks/work-os/use-work-os.ts create mode 100644 apps/web/src/lib/work-os/work-unit-projection.test.ts create mode 100644 apps/web/src/lib/work-os/work-unit-projection.ts diff --git a/apps/web/src/components/work-os/conversation-panel.tsx b/apps/web/src/components/work-os/conversation-panel.tsx new file mode 100644 index 0000000..84cd8fb --- /dev/null +++ b/apps/web/src/components/work-os/conversation-panel.tsx @@ -0,0 +1,53 @@ +import { + Conversation, + ConversationContent, +} from "@code/ui/components/ai-elements/conversation"; +import { LoaderCircle, MessagesSquare } from "lucide-react"; + +import { ChatMessage } from "@/components/chat/chat-message"; +import { ChatThinkingResponse } from "@/components/chat/chat-thinking-response"; +import type { ChatAgentState } from "@/lib/chat/types"; + +interface ConversationPanelProps { + readonly agent: ChatAgentState; + readonly title: string; + readonly emptyHint: string; +} + +export const ConversationPanel = ({ + agent, + emptyHint, + title, +}: ConversationPanelProps) => { + if (agent.status === "connecting" && !agent.historyReady) { + return ( +
{title}
++ {emptyHint} +
++ Import a repository to start the Work OS loop. Turn a clear outcome into + a Work Unit that Zopu can start, verify, and review. +
+ +Loading signals…
++ No Signals detected for this project yet. +
+{signal.title}
++ {signal.summary} +
++ {error.message || "Message failed to send"} +
+ ) : null} + +Zopu
++ {selectedProject.name} +
+ ) : ( +No project
+ )} +No Work Units yet
++ Create one above, or describe what you need in the conversation. +
++ No Work Units yet. Create one or chat with Zopu. +
+{os.error}
++ Work Units +
++ Work Unit #{issue.number} +
++ {issue.body} +
+ {canStart ? ( + + ) : null} ++ This work unit needs your input +
++ Resolve the open question or decision to unblock the agent. +
+{detail.latestSummary}
+ + ) : null} + + {/* Signals — no signal-to-issue relation exists yet. Show explicit + unavailable state rather than falsely presenting project Signals. */} + } title="Signals"> ++ No Signals linked to this Work Unit yet. +
++ Linked Signals will appear here once attachment is available. +
+ + + {/* Activity timeline */} + } + title={`Timeline (${detail.stepCount} steps)`} + > + {detail.activity.length > 0 ? ( +{item.label}
++ {item.detail} +
++ {item.time} +
++ No activity recorded yet. +
+ )} + + + {/* Artifacts */} + } + title={`Artifacts (${detail.artifactCount})`} + > + {detail.artifactPaths.length > 0 ? ( ++ No artifacts produced yet. +
+ )} + + + {/* Pull request */} + {detail.hasPullRequest ? ( + } + title="Pull request" + > + {detail.pullRequestUrl ? ( ++ {detail.pullRequestNumber + ? `PR #${detail.pullRequestNumber} ready for review` + : "Pull request ready"} +
+ ++ A pull request was opened but the URL is unavailable. +
+ )} + + ) : null} +- No Signals linked to this Work Unit yet. -
-- Linked Signals will appear here once attachment is available. -
+ {/* Linked Signals — authenticated via signalIssueAttachments */} + } + title={`Signals (${detail.signalCount})`} + > + {detail.linkedSignals.length > 0 ? ( +{signal.title}
++ {signal.summary} +
++ {signal.sourceCount} source + {signal.sourceCount === 1 ? "" : "s"} ·{" "} + {formatRelativeTime(signal.createdAt)} +
++ No Signals linked to this Work Unit. +
+ )} {/* Activity timeline */} diff --git a/apps/web/src/hooks/work-os/use-work-os.ts b/apps/web/src/hooks/work-os/use-work-os.ts index 8f82751..9a306e1 100644 --- a/apps/web/src/hooks/work-os/use-work-os.ts +++ b/apps/web/src/hooks/work-os/use-work-os.ts @@ -14,6 +14,7 @@ import { eventsForIssue, } from "@/lib/work-os/work-unit-projection"; import type { + LinkedSignal, WorkUnitCard, WorkUnitDetail, } from "@/lib/work-os/work-unit-projection"; @@ -28,7 +29,6 @@ export interface SignalItem { export type ComposerMode = "project" | "work-unit"; export interface WorkOsState { - // Project readonly projects: ReturnType