Product identity editor (Slice 2): author a product's PRODUCT.md in-app

Each product on the Structure page gets an "Identity" action that opens the Markdown
editor (Edit/Preview, frontmatter-aware) wired to GET/PUT /products/{id}/identity, with
a starter PRODUCT.md template. Adds api.put. Saving makes the brief shared by every
agent across the product's teams (injected by the assembler from Slice 1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-15 18:16:59 +03:30
parent 56d41a231f
commit e579aaff91
2 changed files with 76 additions and 1 deletions
+1
View File
@@ -27,6 +27,7 @@ async function request<T>(method: string, url: string, body?: unknown): Promise<
export const api = { export const api = {
get: <T>(url: string) => request<T>('GET', url), get: <T>(url: string) => request<T>('GET', url),
post: <T>(url: string, body?: unknown) => request<T>('POST', url, body), post: <T>(url: string, body?: unknown) => request<T>('POST', url, body),
put: <T>(url: string, body?: unknown) => request<T>('PUT', url, body),
patch: <T>(url: string, body?: unknown) => request<T>('PATCH', url, body), patch: <T>(url: string, body?: unknown) => request<T>('PATCH', url, body),
del: <T>(url: string) => request<T>('DELETE', url), del: <T>(url: string) => request<T>('DELETE', url),
} }
+75 -1
View File
@@ -1,7 +1,8 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { Boxes, Plus } from 'lucide-react' import { Boxes, FileText, Plus } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { AppShell } from '@/components/AppShell' import { AppShell } from '@/components/AppShell'
import { MarkdownEditor } from '@/components/MarkdownEditor'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
@@ -15,9 +16,26 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { useAuth } from '@/store/auth' import { useAuth } from '@/store/auth'
// A starter PRODUCT.md so an empty product gets useful structure to fill in.
const IDENTITY_TEMPLATE = (name: string) =>
`---
product: ${name}
goals:
domain:
conventions:
glossary:
---
# About ${name}
Describe what this product is, who it serves, and the conventions every agent on it should follow.
This identity is shared by every agent across the product's teams.
`
interface Division { interface Division {
id: string id: string
organizationId: string organizationId: string
@@ -52,6 +70,8 @@ export function StructurePage() {
const [divisionName, setDivisionName] = useState('') const [divisionName, setDivisionName] = useState('')
const [product, setProduct] = useState({ name: '', kind: 'Product', divisionId: NONE }) const [product, setProduct] = useState({ name: '', kind: 'Product', divisionId: NONE })
const [team, setTeam] = useState({ name: '', productId: NONE }) const [team, setTeam] = useState({ name: '', productId: NONE })
const [identity, setIdentity] = useState<{ productId: string; name: string; content: string } | null>(null)
const [savingIdentity, setSavingIdentity] = useState(false)
const load = useCallback(async () => { const load = useCallback(async () => {
if (!organizationId) return if (!organizationId) return
@@ -115,6 +135,30 @@ export function StructurePage() {
toast.success('Team created.') toast.success('Team created.')
}) })
// Open the product's shared identity (PRODUCT.md) — load current text, or start from the template.
const openIdentity = async (p: Product) => {
try {
const current = await api.get<{ identity: string | null }>(`/api/orgboard/products/${p.id}/identity`)
setIdentity({ productId: p.id, name: p.name, content: current.identity ?? IDENTITY_TEMPLATE(p.name) })
} catch (err) {
toast.error((err as Error).message)
}
}
const saveIdentity = async () => {
if (!identity) return
setSavingIdentity(true)
try {
await api.put(`/api/orgboard/products/${identity.productId}/identity`, { identity: identity.content })
toast.success(`Identity saved for ${identity.name} — every agent on it now shares it.`)
setIdentity(null)
} catch (err) {
toast.error((err as Error).message)
} finally {
setSavingIdentity(false)
}
}
return ( return (
<AppShell> <AppShell>
<div className="mx-auto max-w-4xl p-6"> <div className="mx-auto max-w-4xl p-6">
@@ -197,6 +241,9 @@ export function StructurePage() {
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{p.divisionId ? divisions.find((d) => d.id === p.divisionId)?.name ?? '' : 'no division'} {p.divisionId ? divisions.find((d) => d.id === p.divisionId)?.name ?? '' : 'no division'}
</span> </span>
<Button variant="ghost" size="sm" className="ml-auto" onClick={() => openIdentity(p)}>
<FileText data-icon="inline-start" /> Identity
</Button>
</div> </div>
))} ))}
{products.length === 0 && <p className="text-sm text-muted-foreground">No products yet.</p>} {products.length === 0 && <p className="text-sm text-muted-foreground">No products yet.</p>}
@@ -246,6 +293,33 @@ export function StructurePage() {
</Card> </Card>
</div> </div>
</div> </div>
{identity && (
<Sheet open onOpenChange={(o) => !o && setIdentity(null)}>
<SheetContent className="flex w-full flex-col gap-0 overflow-y-auto sm:max-w-2xl">
<SheetHeader>
<SheetTitle>Product identity {identity.name}</SheetTitle>
<SheetDescription>
A shared PRODUCT.md (goals, domain, conventions) injected into every agent run on this
product, across all its teams. Treated as data, never as instructions.
</SheetDescription>
</SheetHeader>
<div className="flex flex-col gap-4 px-4 pb-6">
<MarkdownEditor
rows={22}
mono
frontmatter
value={identity.content}
onChange={(content) => setIdentity({ ...identity, content })}
/>
<div className="flex items-center justify-end gap-2">
<Button variant="ghost" onClick={() => setIdentity(null)}>Cancel</Button>
<Button disabled={savingIdentity} onClick={saveIdentity}>Save identity</Button>
</div>
</div>
</SheetContent>
</Sheet>
)}
</AppShell> </AppShell>
) )
} }