feat: generic project UI, branding fix, context/artifact separation

Web:
- Replace GitHub-specific import with generic public Git URL import
- Update project workspace page to use ProjectView shape (name, sources)
- Migrate use-project-workspace hook to importPublicGit action
- Fix status key needsInput → needs-input

Branding:
- Correct Zopo/zopo → Zopu/zopu in docs/PRODUCT.md, docs/DESIGN.md, docs/TECH.md

Artifacts:
- Cut to 8 operational artifacts (removed project/business/design.md)
- Update artifact seeds with generic context scaffolding
This commit is contained in:
-Puter
2026-07-23 18:31:48 +05:30
parent 224e98d0bc
commit e44759d7fe
8 changed files with 133 additions and 191 deletions

View File

@@ -18,19 +18,20 @@ import { useProjectWorkspace } from "@/hooks/use-project-workspace";
const statusStyle: Record<Doc<"projectIssues">["status"], string> = {
completed:
"border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
failed: "border-destructive/40 bg-destructive/10 text-destructive",
failed:
"border-destructive/40 bg-destructive/10 text-destructive",
"needs-input":
"border-orange-500/40 bg-orange-500/10 text-orange-700 dark:text-orange-300",
open: "border-border text-muted-foreground",
queued:
"border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300",
working: "border-blue-500/40 bg-blue-500/10 text-blue-700 dark:text-blue-300",
open: "border-border bg-muted/40 text-muted-foreground",
queued: "border-blue-500/40 bg-blue-500/10 text-blue-700 dark:text-blue-300",
working:
"border-blue-500/40 bg-blue-500/10 text-blue-700 dark:text-blue-300",
};
interface RepositoryFormProps {
readonly busy: boolean;
readonly onChange: (value: string) => void;
readonly onSubmit: () => Promise<void>;
readonly onSubmit: () => void;
readonly value: string;
}
@@ -41,36 +42,44 @@ const RepositoryForm = ({
value,
}: RepositoryFormProps) => (
<form
className="grid gap-3 border-b p-4"
className="space-y-2 border-b p-3"
onSubmit={(event) => {
event.preventDefault();
void onSubmit();
onSubmit();
}}
>
<div className="space-y-1">
<label className="text-xs font-medium" htmlFor="github-repository">
Public GitHub repository
<label className="text-xs font-medium" htmlFor="git-repository">
Public Git repository
</label>
<p className="text-xs text-muted-foreground">
Connect with an owner/name slug or repository URL.
Import with a public HTTP(S) Git URL.
</p>
</div>
<Input
id="github-repository"
id="git-repository"
onChange={(event) => onChange(event.target.value)}
placeholder="rivet-dev/rivet"
placeholder="https://github.com/owner/repo"
required
value={value}
/>
<Button disabled={busy || value.trim().length === 0} type="submit">
<Button className="w-full" disabled={busy} type="submit">
<GitFork data-icon="inline-start" />
{busy ? "Connecting" : "Connect repository"}
{busy ? "Importing" : "Import repository"}
</Button>
</form>
);
interface ProjectListProps {
readonly projects: readonly Doc<"projects">[] | undefined;
readonly projects: readonly {
readonly id: string;
readonly name: string;
readonly sources: readonly {
readonly host: string;
readonly repositoryPath: string;
readonly url: string;
}[];
}[] | undefined;
readonly selectedId: Id<"projects"> | null;
readonly onSelect: (projectId: Id<"projects">) => void;
}
@@ -85,7 +94,7 @@ const ProjectList = ({ projects, selectedId, onSelect }: ProjectListProps) => {
if (projects.length === 0) {
return (
<p className="p-3 text-xs leading-relaxed text-muted-foreground">
Connect a repository to create its project artifacts and issue queue.
Import a repository to create its project artifacts and issue queue.
</p>
);
}
@@ -94,19 +103,20 @@ const ProjectList = ({ projects, selectedId, onSelect }: ProjectListProps) => {
{projects.map((project) => (
<button
className={`w-full border px-3 py-2 text-left transition-colors ${
selectedId === project._id
selectedId === project.id
? "border-foreground/30 bg-muted"
: "border-transparent hover:bg-muted/60"
}`}
key={project._id}
onClick={() => onSelect(project._id)}
key={project.id}
onClick={() => onSelect(project.id as Id<"projects">)}
type="button"
>
<span className="block truncate text-sm font-medium">
{project.name}
</span>
<span className="block truncate text-xs text-muted-foreground">
{project.repoOwner}/{project.repoName}
{project.sources[0]?.host}
{project.sources[0]?.repositoryPath}
</span>
</button>
))}
@@ -130,35 +140,26 @@ interface ArtifactGridProps {
const ArtifactGrid = ({ artifacts }: ArtifactGridProps) => (
<section aria-labelledby="artifact-heading" className="space-y-3">
<div>
<h2 className="text-lg font-semibold" id="artifact-heading">
Project artifacts
</h2>
<p className="text-sm text-muted-foreground">
Canonical context staged into every issue workspace.
</p>
</div>
<h2 id="artifact-heading" className="text-sm font-semibold">
Project artifacts
</h2>
{artifacts === undefined ? (
<p className="text-sm text-muted-foreground">Loading artifacts...</p>
<p className="text-xs text-muted-foreground">Loading artifacts...</p>
) : artifacts.length === 0 ? (
<p className="text-xs text-muted-foreground">No artifacts yet.</p>
) : (
<div className="grid gap-2 md:grid-cols-2 xl:grid-cols-3">
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{artifacts.map((artifact) => (
<article className="min-w-0 border bg-card p-3" key={artifact._id}>
<div className="mb-2 flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
<FileText className="size-4 shrink-0 text-muted-foreground" />
<h3 className="truncate font-mono text-xs font-medium">
{artifact.path}
</h3>
</div>
<span className="shrink-0 text-[11px] text-muted-foreground">
r{artifact.revision}
</span>
</div>
<p className="line-clamp-4 whitespace-pre-wrap text-xs leading-relaxed text-muted-foreground">
{artifact.content}
<div
className="border p-3"
key={artifact._id}
>
<FileText className="mb-2 size-4 text-muted-foreground" />
<p className="truncate text-xs font-medium">{artifact.path}</p>
<p className="text-[10px] text-muted-foreground">
rev {artifact.revision}
</p>
</article>
</div>
))}
</div>
)}
@@ -169,7 +170,7 @@ interface IssueComposerProps {
readonly body: string;
readonly busy: boolean;
readonly onBodyChange: (value: string) => void;
readonly onSubmit: () => Promise<void>;
readonly onSubmit: () => void;
readonly onTitleChange: (value: string) => void;
readonly title: string;
}
@@ -183,47 +184,27 @@ const IssueComposer = ({
title,
}: IssueComposerProps) => (
<form
className="grid gap-3 border p-4"
className="space-y-3"
onSubmit={(event) => {
event.preventDefault();
void onSubmit();
onSubmit();
}}
>
<div>
<h2 className="font-semibold">Raise an issue</h2>
<p className="text-xs text-muted-foreground">
The issue becomes the identity for one Flue agent and AgentOS workspace.
</p>
</div>
<div className="space-y-1.5">
<label className="text-xs font-medium" htmlFor="issue-title">
Title
</label>
<Input
id="issue-title"
maxLength={160}
onChange={(event) => onTitleChange(event.target.value)}
required
value={title}
/>
</div>
<div className="space-y-1.5">
<label className="text-xs font-medium" htmlFor="issue-body">
Description
</label>
<Textarea
id="issue-body"
maxLength={10_000}
onChange={(event) => onBodyChange(event.target.value)}
required
rows={5}
value={body}
/>
</div>
<Button
disabled={busy || title.trim().length < 3 || body.trim().length < 10}
type="submit"
>
<h2 className="text-sm font-semibold">New issue</h2>
<Input
onChange={(event) => onTitleChange(event.target.value)}
placeholder="Issue title"
required
value={title}
/>
<Textarea
onChange={(event) => onBodyChange(event.target.value)}
placeholder="Describe the issue..."
required
rows={4}
value={body}
/>
<Button className="w-full" disabled={busy} type="submit">
<Plus data-icon="inline-start" />
{busy ? "Creating" : "Create issue"}
</Button>
@@ -237,89 +218,55 @@ interface IssueListProps {
issueId: Id<"projectIssues">,
issueNumber: number,
title: string
) => Promise<void>;
) => void;
}
const IssueList = ({ issues, pendingAction, onStart }: IssueListProps) => {
const renderContent = () => {
if (issues === undefined) {
return <p className="text-sm text-muted-foreground">Loading issues...</p>;
}
if (issues.length === 0) {
return (
<p className="border p-4 text-sm text-muted-foreground">
No issues yet. Describe a concrete change to start the workflow.
</p>
);
}
if (issues === undefined) {
return (
<div className="grid gap-2">
{issues.map((issue) => {
const canStart = ["open", "needs-input", "failed"].includes(
issue.status
);
const busy = pendingAction === `issue:${issue._id}`;
let actionLabel: string = issue.status;
if (canStart) {
actionLabel = "Start agent";
}
if (busy) {
actionLabel = "Dispatching";
}
return (
<article
className="grid gap-3 border bg-card p-4 md:grid-cols-[1fr_auto] md:items-center"
key={issue._id}
>
<div className="min-w-0">
<div className="mb-1 flex flex-wrap items-center gap-2">
<span className="font-mono text-xs text-muted-foreground">
#{issue.number}
</span>
<span
className={`border px-1.5 py-0.5 text-[11px] font-medium ${statusStyle[issue.status]}`}
>
{issue.status}
</span>
</div>
<h3 className="font-medium">{issue.title}</h3>
<p className="mt-1 line-clamp-2 text-sm text-muted-foreground">
{issue.body}
</p>
</div>
<Button
disabled={!canStart || busy}
onClick={() =>
void onStart(issue._id, issue.number, issue.title)
}
size="sm"
type="button"
variant={canStart ? "default" : "outline"}
>
{canStart ? (
<Play data-icon="inline-start" />
) : (
<Bot data-icon="inline-start" />
)}
{actionLabel}
</Button>
</article>
);
})}
</div>
<p className="text-xs text-muted-foreground">Loading issues...</p>
);
};
}
if (issues.length === 0) {
return (
<p className="text-xs text-muted-foreground">
No issues yet. Create one to start agent work.
</p>
);
}
return (
<section aria-labelledby="issue-heading" className="space-y-3">
<div className="flex items-center gap-2">
<GitPullRequest className="size-4 text-muted-foreground" />
<h2 className="text-lg font-semibold" id="issue-heading">
Issue queue
</h2>
</div>
{renderContent()}
</section>
<div className="space-y-2">
<h2 className="text-sm font-semibold">Issues</h2>
{issues.map((issue) => (
<div className="border p-3" key={issue._id}>
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium">
#{issue.number} · {issue.title}
</span>
<span
className={`rounded border px-2 py-0.5 text-[10px] font-medium ${statusStyle[issue.status]}`}
>
{issue.status}
</span>
</div>
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
{issue.body}
</p>
{(issue.status === "open" || issue.status === "failed") && (
<Button
className="mt-2"
disabled={pendingAction === `issue:${issue._id}`}
onClick={() => onStart(issue._id, issue.number, issue.title)}
size="sm"
variant="outline"
>
<Play data-icon="inline-start" />
{pendingAction === `issue:${issue._id}` ? "Starting" : "Start"}
</Button>
)}
</div>
))}
</div>
);
};
@@ -332,6 +279,7 @@ export const ProjectWorkspacePage = () => {
const handleIssueSubmit = workspace.raiseIssue;
const handleIssueTitleChange = workspace.setIssueTitle;
const handleIssueStart = workspace.startIssue;
const projectSource = workspace.selectedProject?.sources[0];
return (
<main className="min-h-svh bg-background text-foreground">
@@ -344,7 +292,7 @@ export const ProjectWorkspacePage = () => {
<div>
<p className="text-sm font-semibold">Project manager</p>
<p className="text-xs text-muted-foreground">
GitHub, Flue, AgentOS
Git · Flue · AgentOS
</p>
</div>
</div>
@@ -379,21 +327,19 @@ export const ProjectWorkspacePage = () => {
<div className="flex flex-col gap-3 border-b pb-5 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="text-xs text-muted-foreground">
Connected GitHub project
Connected repository
</p>
<h1 className="text-2xl font-semibold tracking-tight">
{workspace.selectedProject.repoOwner}/
{workspace.selectedProject.repoName}
{workspace.selectedProject.name}
</h1>
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
{workspace.selectedProject.description ??
"No repository description is available."}
{projectSource?.host}/{projectSource?.repositoryPath}
</p>
</div>
<Button
onClick={() =>
window.open(
workspace.selectedProject?.repoUrl,
projectSource?.url,
"_blank",
"noopener,noreferrer"
)
@@ -402,7 +348,7 @@ export const ProjectWorkspacePage = () => {
variant="outline"
>
<ExternalLink data-icon="inline-start" />
Open GitHub
Open repository
</Button>
</div>
@@ -429,7 +375,7 @@ export const ProjectWorkspacePage = () => {
<div className="max-w-sm space-y-2">
<GitFork className="mx-auto size-7 text-muted-foreground" />
<h1 className="text-xl font-semibold">
Connect your first repository
Import your first repository
</h1>
<p className="text-sm text-muted-foreground">
Zopu creates the project artifact set and opens an issue queue

View File

@@ -17,11 +17,12 @@ export const useProjectWorkspace = () => {
const [issueBody, setIssueBody] = useState("");
const [pendingAction, setPendingAction] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const connectGitHub = useAction(api.projects.connectGitHub);
const importPublicGit = useAction(api.projects.importPublicGit);
const createIssue = useMutation(api.projectIssues.create);
const beginIssue = useMutation(api.projectIssues.begin);
const markDispatchFailed = useMutation(api.projectIssues.markDispatchFailed);
const activeProjectId = selectedProjectId ?? projects?.[0]?._id ?? null;
const activeProjectId =
(selectedProjectId ?? (projects?.[0]?.id as unknown as Id<"projects">)) ?? null;
const artifacts = useQuery(
api.projectArtifacts.list,
activeProjectId ? { projectId: activeProjectId } : "skip"
@@ -35,8 +36,8 @@ export const useProjectWorkspace = () => {
setPendingAction("connect");
setError(null);
try {
const projectId = await connectGitHub({ repository });
setSelectedProjectId(projectId);
const outcome = await importPublicGit({ repositoryUrl: repository });
setSelectedProjectId(outcome.id as unknown as Id<"projects">);
setRepository("");
} catch (caughtError) {
setError(errorMessage(caughtError));
@@ -87,9 +88,8 @@ export const useProjectWorkspace = () => {
setPendingAction(null);
}
};
const selectedProject =
projects?.find((project) => project._id === activeProjectId) ?? null;
projects?.find((project) => project.id === (activeProjectId as unknown as string)) ?? null;
return {
artifacts,

View File

@@ -1,4 +1,4 @@
# Zopo Work OS — Product Design
# Zopu Work OS — Product Design
> **Status:** canonical working interaction/interface specification · **Audience:** product design, frontend/product engineering, agent experience · **Scope:** information architecture, screens, cards, interaction patterns, states, actions, principles · **Excludes:** backend and low-level infrastructure
@@ -281,7 +281,7 @@ Use explicit actions, never vague “continue”: **Approve plan; answer questio
```text
Deploy preview
Project: Zopo Web · Branch: work/W-103/team-invitations
Project: Zopu Web · Branch: work/W-103/team-invitations
Environment: Temporary preview · External impact: None
Expected duration: 20 minutes
[Deploy preview]
@@ -364,4 +364,4 @@ Within seconds, users can answer: **What should I focus on? What progressed with
## 25. Core experience statement
Zopo should feel like supervising a capable operating system, not operating a collection of agents: one workspace shows meaningful work; the user resolves the few things requiring judgment and continues conversation where needed; the system handles context, execution, progress tracking, and knowledge retention beneath a calm surface organized around persistent outcomes.
Zopu should feel like supervising a capable operating system, not operating a collection of agents: one workspace shows meaningful work; the user resolves the few things requiring judgment and continues conversation where needed; the system handles context, execution, progress tracking, and knowledge retention beneath a calm surface organized around persistent outcomes.

View File

@@ -1,10 +1,10 @@
# Zopo Work OS — Product
# Zopu Work OS — Product
> Working spec · product/design/ops/research/agent teams · concepts, behavior, boundaries, value. Excludes implementation, infra, tech choices, low-level UI.
## 1. Summary
Zopo Work OS is a persistent OS for technical founders, product engineers, and small teams managing the full lifecycle of building and growing software across products, companies, repos, customers, and functions. It replaces fragmented chats, trackers, docs, and manually coordinated AI agents with one coherent system organized around **units of work**.
Zopu Work OS is a persistent OS for technical founders, product engineers, and small teams managing the full lifecycle of building and growing software across products, companies, repos, customers, and functions. It replaces fragmented chats, trackers, docs, and manually coordinated AI agents with one coherent system organized around **units of work**.
One continuous workspace discovers work, builds understanding, coordinates execution, requests human judgment when needed, tracks outcomes, and retains knowledge. Not a better chat app or just another PM tool — it continuously converts company signals into structured, persistent, executable work.
@@ -26,7 +26,7 @@ Technical-first because code is a clear execution domain (measurable artifacts,
| **Across AI sessions** | Chat/thread-organized work forces repeated decisions: which conversation, what context to restate, which model/agent, where to store output, how it relates to existing work, what changed after shipping. Conversation becomes structure though conversations are temporary and work is persistent. |
| **Founders as coordination** | Small-team founders bridge every system/function: remember decisions, connect feedback to work, check blockers, move work between tools, review AI output, decide next steps. The founder becomes the human integration bus. |
Zopo removes this burden by maintaining a persistent model of work, context, state, evidence, execution, and outcomes.
Zopu removes this burden by maintaining a persistent model of work, context, state, evidence, execution, and outcomes.
## 4. Thesis
@@ -161,7 +161,7 @@ Accumulates several kinds; each retains its scope (personal prefs, project facts
| Boundary | Rule |
| --- | --- |
| **Control layer, not tool replacement** | Specialist systems (source control, support, comms, analytics, billing, deployment, docs) keep native records/actions. Zopo provides the unified model: what matters, its state, next step, whether outcome worked. |
| **Control layer, not tool replacement** | Specialist systems (source control, support, comms, analytics, billing, deployment, docs) keep native records/actions. Zopu provides the unified model: what matters, its state, next step, whether outcome worked. |
| **Own the work graph** | Connects organizations→projects→goals→signals→work units→dependencies→decisions→runs→artifacts→results→learnings. Durable source of coordination/context. |
| **Execution is replaceable** | Execution is a capability beneath the work unit. Users shouldn't care which agent/model/harness ran unless inspection is useful. |
| **No unnecessary AI config** | Users shouldn't normally choose model, thinking level, agent framework, context-window strategy, execution runtime, tool routing. System chooses by task/risk/cost/policy. |
@@ -225,4 +225,4 @@ Not intended to be: a full external-tool replacement; a general social collabora
## 20. Working statement
Zopo Work OS is an autonomous OS for technical founders and small product teams. It observes work across the company, turns fragmented signals into persistent units of work, progresses them via agents and tools, and brings the human in only when judgment, permission, or direction is required. One continuous workspace moves from idea/problem to verified, shipped, measurable outcome while preserving context and building durable understanding over time.
Zopu Work OS is an autonomous OS for technical founders and small product teams. It observes work across the company, turns fragmented signals into persistent units of work, progresses them via agents and tools, and brings the human in only when judgment, permission, or direction is required. One continuous workspace moves from idea/problem to verified, shipped, measurable outcome while preserving context and building durable understanding over time.

View File

@@ -1,4 +1,4 @@
# Zopo Work OS — Technical Architecture
# Zopu Work OS — Technical Architecture
> Working spec · engineering/platform/infra/security/agent teams · starts from an existing Better-T-Stack monorepo (web + Expo + shared packages).
@@ -107,7 +107,7 @@ Known first-party templates may run in lighter env; imported arbitrary repos def
```
/ README.md, AGENTS.md, product.md, design.md, tech.md
/.zopo/ project.yaml, services.yaml, policies.yaml
/.zopu/ project.yaml, services.yaml, policies.yaml
lifecycle/{setup,resume,verify,preview} skills/{testing,reviewing,deploying}/SKILL.md
/apps/
```

View File

@@ -1,6 +1,5 @@
import { ConvexError, v } from "convex/values";
import type { Id } from "./_generated/dataModel";
import { mutation, query } from "./_generated/server";
import { requireProjectMember } from "./authz";

View File

@@ -195,7 +195,7 @@ export const makeProjectMutationStore = (ctx: MutationCtx) => ({
// Action store — calls narrowly scoped internal mutations/queries
// ---------------------------------------------------------------------------
export const makeProjectActionStore = (ctx: ActionCtx) => ({
export const makeProjectActionStore = (_ctx: ActionCtx) => ({
getCurrentOrganization: async (userId: string) => {
return userId;
},

View File

@@ -1,8 +1,6 @@
import { v } from "convex/values";
import {
preparePublicGitSource,
decodePublicGitImportResult,
type ProjectImportOutcome,
type ProjectView,
type PublicGitImportResult,
@@ -11,7 +9,6 @@ import {
import { Effect } from "effect";
import type { Id } from "./_generated/dataModel";
import { internal } from "./_generated/api";
import { action, internalMutation, query } from "./_generated/server";
import { requireCurrentOrganization } from "./authz";
import { createInitialArtifacts } from "./artifactModel";