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:
@@ -27,6 +27,7 @@ async function request<T>(method: string, url: string, body?: unknown): Promise<
|
||||
export const api = {
|
||||
get: <T>(url: string) => request<T>('GET', url),
|
||||
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),
|
||||
del: <T>(url: string) => request<T>('DELETE', url),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Boxes, Plus } from 'lucide-react'
|
||||
import { Boxes, FileText, Plus } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { AppShell } from '@/components/AppShell'
|
||||
import { MarkdownEditor } from '@/components/MarkdownEditor'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -15,9 +16,26 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
||||
import { api } from '@/lib/api'
|
||||
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 {
|
||||
id: string
|
||||
organizationId: string
|
||||
@@ -52,6 +70,8 @@ export function StructurePage() {
|
||||
const [divisionName, setDivisionName] = useState('')
|
||||
const [product, setProduct] = useState({ name: '', kind: 'Product', divisionId: 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 () => {
|
||||
if (!organizationId) return
|
||||
@@ -115,6 +135,30 @@ export function StructurePage() {
|
||||
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 (
|
||||
<AppShell>
|
||||
<div className="mx-auto max-w-4xl p-6">
|
||||
@@ -197,6 +241,9 @@ export function StructurePage() {
|
||||
<span className="text-muted-foreground">
|
||||
{p.divisionId ? divisions.find((d) => d.id === p.divisionId)?.name ?? '' : 'no division'}
|
||||
</span>
|
||||
<Button variant="ghost" size="sm" className="ml-auto" onClick={() => openIdentity(p)}>
|
||||
<FileText data-icon="inline-start" /> Identity
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{products.length === 0 && <p className="text-sm text-muted-foreground">No products yet.</p>}
|
||||
@@ -246,6 +293,33 @@ export function StructurePage() {
|
||||
</Card>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user