feat: scaffold BuildPulse v0.1 cockpit

This commit is contained in:
OpenClaw Bot
2026-05-07 00:11:35 +02:00
parent 8f0ba43728
commit bdf8773797
18 changed files with 4835 additions and 0 deletions
+980
View File
@@ -0,0 +1,980 @@
import { useEffect, useMemo, useState } from 'react'
import type { ChangeEvent } from 'react'
import './index.css'
import { createMarkdownPackage, createJsonExport, createPulseJsonl } from './features/export/exporters'
import { loadAppState, replaceAppState, saveAppState, validateAppState } from './store/storage'
import { FEATURE_COLUMNS, FEATURE_PRIORITIES, FEATURE_STATUSES, PULSE_TYPES, RISK_LEVELS } from './store/types'
import type { AppState, Feature, FeatureColumn, ParkingLotItem, PulseEvent, RiskLevel, TabKey } from './store/types'
import { arrayToLines, formatDateTime, linesToArray, nowIso, slugify } from './utils/format'
const TABS: Array<{ key: TabKey; label: string }> = [
{ key: 'feature-plan', label: 'Feature Plan' },
{ key: 'parking-lot', label: 'Parking Lot' },
{ key: 'pulse-log', label: 'Pulse Log' },
{ key: 'export', label: 'Export' },
]
const initialFeatureDraft = {
title: '',
description: '',
column: 'now' as FeatureColumn,
priority: 'must' as (typeof FEATURE_PRIORITIES)[number],
status: 'idea' as (typeof FEATURE_STATUSES)[number],
acceptanceCriteria: '',
scopeNotes: '',
}
const initialParkingDraft = {
title: '',
description: '',
reasonParked: '',
futurePlacement: '',
riskLevel: 'medium' as RiskLevel,
}
const initialPulseDraft = {
featureId: '',
source: 'manual',
agentId: 'jimmi',
pulseType: 'INTENT' as (typeof PULSE_TYPES)[number],
message: '',
confidence: '0.9',
evidenceRefs: '',
traceId: '',
structuredPayload: '{}',
}
const columnLabels: Record<FeatureColumn, string> = {
now: 'Now',
next: 'Next',
later: 'Later',
done: 'Done',
}
const downloadText = (filename: string, text: string, contentType = 'text/plain;charset=utf-8') => {
const blob = new Blob([text], { type: contentType })
const url = URL.createObjectURL(blob)
const anchor = document.createElement('a')
anchor.href = url
anchor.download = filename
anchor.click()
URL.revokeObjectURL(url)
}
function App() {
const [appState, setAppState] = useState<AppState>(() => loadAppState())
const [activeTab, setActiveTab] = useState<TabKey>('feature-plan')
const [statusMessage, setStatusMessage] = useState('Seeded with BuildPulse so you can dogfood it immediately.')
const [selectedFeatureId, setSelectedFeatureId] = useState<string | null>(null)
const [selectedParkingId, setSelectedParkingId] = useState<string | null>(null)
const [selectedPulseId, setSelectedPulseId] = useState<string | null>(null)
const [featureDraft, setFeatureDraft] = useState(initialFeatureDraft)
const [parkingDraft, setParkingDraft] = useState(initialParkingDraft)
const [pulseDraft, setPulseDraft] = useState(initialPulseDraft)
const [pulseTypeFilter, setPulseTypeFilter] = useState('all')
const [pulseFeatureFilter, setPulseFeatureFilter] = useState('all')
const [pulseSourceFilter, setPulseSourceFilter] = useState('all')
useEffect(() => {
saveAppState(appState)
}, [appState])
const groupedFeatures = useMemo(
() =>
FEATURE_COLUMNS.reduce<Record<FeatureColumn, Feature[]>>((acc, column) => {
acc[column] = appState.features.filter((feature) => feature.column === column)
return acc
}, { now: [], next: [], later: [], done: [] }),
[appState.features],
)
const selectedFeature = useMemo(
() => appState.features.find((feature) => feature.id === selectedFeatureId) ?? null,
[appState.features, selectedFeatureId],
)
const selectedParkingItem = useMemo(
() => appState.parking_lot.find((item) => item.id === selectedParkingId) ?? null,
[appState.parking_lot, selectedParkingId],
)
const selectedPulse = useMemo(
() => appState.pulses.find((pulse) => pulse.id === selectedPulseId) ?? null,
[appState.pulses, selectedPulseId],
)
const filteredPulses = useMemo(() => {
return [...appState.pulses]
.filter((pulse) => (pulseTypeFilter === 'all' ? true : pulse.pulse_type === pulseTypeFilter))
.filter((pulse) => (pulseFeatureFilter === 'all' ? true : pulse.feature_id === pulseFeatureFilter))
.filter((pulse) => (pulseSourceFilter === 'all' ? true : pulse.source === pulseSourceFilter || pulse.agent_id === pulseSourceFilter))
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
}, [appState.pulses, pulseFeatureFilter, pulseSourceFilter, pulseTypeFilter])
const uniqueSources = useMemo(
() => Array.from(new Set(appState.pulses.flatMap((pulse) => [pulse.source, pulse.agent_id]).filter(Boolean))).sort(),
[appState.pulses],
)
const markdownPackage = useMemo(() => createMarkdownPackage(appState), [appState])
const updateProject = (field: keyof AppState['project'], value: string) => {
setAppState((current) => ({
...current,
project: {
...current.project,
[field]: value,
updated_at: nowIso(),
},
}))
}
const beginFeatureEdit = (feature: Feature) => {
setSelectedFeatureId(feature.id)
setFeatureDraft({
title: feature.title,
description: feature.description,
column: feature.column,
priority: feature.priority,
status: feature.status,
acceptanceCriteria: arrayToLines(feature.acceptance_criteria),
scopeNotes: feature.scope_notes,
})
}
const resetFeatureDraft = () => {
setSelectedFeatureId(null)
setFeatureDraft(initialFeatureDraft)
}
const saveFeature = () => {
if (!featureDraft.title.trim()) {
setStatusMessage('Feature title is required. Tiny cockpit, tiny guardrails.')
return
}
const timestamp = nowIso()
const acceptanceCriteria = linesToArray(featureDraft.acceptanceCriteria)
if (selectedFeatureId) {
setAppState((current) => ({
...current,
features: current.features.map((feature) =>
feature.id === selectedFeatureId
? {
...feature,
title: featureDraft.title.trim(),
description: featureDraft.description.trim(),
column: featureDraft.column,
priority: featureDraft.priority,
status: featureDraft.status,
acceptance_criteria: acceptanceCriteria,
scope_notes: featureDraft.scopeNotes.trim(),
updated_at: timestamp,
}
: feature,
),
}))
setStatusMessage('Feature updated.')
} else {
const title = featureDraft.title.trim()
const id = `feature_${slugify(title)}_${Date.now().toString(36)}`
setAppState((current) => ({
...current,
features: [
{
id,
title,
description: featureDraft.description.trim(),
column: featureDraft.column,
priority: featureDraft.priority,
status: featureDraft.status,
acceptance_criteria: acceptanceCriteria,
scope_notes: featureDraft.scopeNotes.trim(),
created_at: timestamp,
updated_at: timestamp,
},
...current.features,
],
}))
setStatusMessage(`Feature “${title}” added.`)
}
resetFeatureDraft()
}
const deleteFeature = (featureId: string) => {
setAppState((current) => ({
...current,
features: current.features.filter((feature) => feature.id !== featureId),
}))
if (selectedFeatureId === featureId) resetFeatureDraft()
setStatusMessage('Feature removed. Related pulses stay intact with a graceful missing-feature label.')
}
const quickMoveFeature = (featureId: string, column: FeatureColumn) => {
setAppState((current) => ({
...current,
features: current.features.map((feature) =>
feature.id === featureId ? { ...feature, column, updated_at: nowIso() } : feature,
),
}))
}
const beginParkingEdit = (item: ParkingLotItem) => {
setSelectedParkingId(item.id)
setParkingDraft({
title: item.title,
description: item.description,
reasonParked: item.reason_parked,
futurePlacement: item.possible_future_placement,
riskLevel: item.risk_level,
})
}
const resetParkingDraft = () => {
setSelectedParkingId(null)
setParkingDraft(initialParkingDraft)
}
const saveParkingItem = () => {
if (!parkingDraft.title.trim()) {
setStatusMessage('Parking Lot items need a title.')
return
}
const timestamp = nowIso()
if (selectedParkingId) {
setAppState((current) => ({
...current,
parking_lot: current.parking_lot.map((item) =>
item.id === selectedParkingId
? {
...item,
title: parkingDraft.title.trim(),
description: parkingDraft.description.trim(),
reason_parked: parkingDraft.reasonParked.trim(),
possible_future_placement: parkingDraft.futurePlacement.trim(),
risk_level: parkingDraft.riskLevel,
updated_at: timestamp,
}
: item,
),
}))
setStatusMessage('Parking Lot item updated.')
} else {
const title = parkingDraft.title.trim()
setAppState((current) => ({
...current,
parking_lot: [
{
id: `parked_${slugify(title)}_${Date.now().toString(36)}`,
title,
description: parkingDraft.description.trim(),
reason_parked: parkingDraft.reasonParked.trim(),
possible_future_placement: parkingDraft.futurePlacement.trim(),
risk_level: parkingDraft.riskLevel,
created_at: timestamp,
updated_at: timestamp,
},
...current.parking_lot,
],
}))
setStatusMessage(`Parked “${title}” safely.`)
}
resetParkingDraft()
}
const deleteParkingItem = (itemId: string) => {
setAppState((current) => ({
...current,
parking_lot: current.parking_lot.filter((item) => item.id !== itemId),
}))
if (selectedParkingId === itemId) resetParkingDraft()
setStatusMessage('Parking Lot item removed.')
}
const convertParkingItemToFeature = (item: ParkingLotItem) => {
const timestamp = nowIso()
setAppState((current) => ({
...current,
features: [
{
id: `feature_${slugify(item.title)}_${Date.now().toString(36)}`,
title: item.title,
description: item.description,
column: 'next',
priority: 'could',
status: 'idea',
acceptance_criteria: [],
scope_notes: `Converted from Parking Lot. Original reason parked: ${item.reason_parked}`,
created_at: timestamp,
updated_at: timestamp,
},
...current.features,
],
parking_lot: current.parking_lot.filter((entry) => entry.id !== item.id),
pulses: [
{
id: `pulse_${Date.now().toString(36)}`,
timestamp,
project_id: current.project.id,
source: 'manual',
agent_id: current.settings.default_agent_id,
pulse_type: 'PARKED_IDEA',
message: `Converted parked idea “${item.title}” into a Next feature.`,
structured_payload: {},
confidence_score: 0.8,
evidence_refs: ['Converted from Parking Lot'],
},
...current.pulses,
],
}))
setStatusMessage(`Converted “${item.title}” into a feature.`)
if (selectedParkingId === item.id) resetParkingDraft()
}
const beginPulseEdit = (pulse: PulseEvent) => {
setSelectedPulseId(pulse.id)
setPulseDraft({
featureId: pulse.feature_id ?? '',
source: pulse.source,
agentId: pulse.agent_id,
pulseType: pulse.pulse_type,
message: pulse.message,
confidence: String(pulse.confidence_score),
evidenceRefs: arrayToLines(pulse.evidence_refs),
traceId: pulse.trace_id ?? '',
structuredPayload: JSON.stringify(pulse.structured_payload ?? {}, null, 2),
})
}
const resetPulseDraft = () => {
setSelectedPulseId(null)
setPulseDraft(initialPulseDraft)
}
const savePulse = () => {
if (!pulseDraft.message.trim()) {
setStatusMessage('Pulse message is required.')
return
}
let parsedPayload: Record<string, unknown>
try {
parsedPayload = pulseDraft.structuredPayload.trim() ? JSON.parse(pulseDraft.structuredPayload) : {}
} catch {
setStatusMessage('Structured payload must be valid JSON.')
return
}
const timestamp = nowIso()
const pulse: PulseEvent = {
id: selectedPulseId ?? `pulse_${Date.now().toString(36)}`,
timestamp: selectedPulse?.timestamp ?? timestamp,
project_id: appState.project.id,
feature_id: pulseDraft.featureId || undefined,
source: pulseDraft.source.trim() || 'manual',
agent_id: pulseDraft.agentId.trim() || appState.settings.default_agent_id,
pulse_type: pulseDraft.pulseType,
message: pulseDraft.message.trim(),
structured_payload: parsedPayload,
confidence_score: Number(pulseDraft.confidence) || 0,
evidence_refs: linesToArray(pulseDraft.evidenceRefs),
trace_id: pulseDraft.traceId.trim() || undefined,
}
if (selectedPulseId) {
setAppState((current) => ({
...current,
pulses: current.pulses.map((entry) => (entry.id === selectedPulseId ? pulse : entry)),
}))
setStatusMessage('Pulse updated.')
} else {
setAppState((current) => ({
...current,
pulses: [pulse, ...current.pulses],
}))
setStatusMessage('Pulse added.')
}
resetPulseDraft()
}
const deletePulse = (pulseId: string) => {
setAppState((current) => ({
...current,
pulses: current.pulses.filter((pulse) => pulse.id !== pulseId),
}))
if (selectedPulseId === pulseId) resetPulseDraft()
setStatusMessage('Pulse deleted.')
}
const copyMarkdown = async (filename: string) => {
try {
await navigator.clipboard.writeText(markdownPackage[filename as keyof typeof markdownPackage])
setStatusMessage(`${filename} copied to clipboard.`)
} catch {
setStatusMessage('Clipboard copy failed. Browser said no.')
}
}
const handleImport = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
try {
const text = await file.text()
const parsed = JSON.parse(text)
if (!validateAppState(parsed)) {
setStatusMessage('Import failed: invalid BuildPulse schema or unsupported version.')
return
}
setAppState(replaceAppState(parsed))
setStatusMessage('Import complete. State replaced cleanly.')
} catch {
setStatusMessage('Import failed: invalid JSON.')
} finally {
event.target.value = ''
}
}
const currentFeatureCount = groupedFeatures.now.length
const recentPulsePreview = [...appState.pulses].sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, 3)
return (
<div className="app-shell">
<header className="hero-card">
<div>
<p className="eyebrow">BuildPulse v0.1</p>
<h1>{appState.project.name}</h1>
<p className="hero-copy">{appState.project.one_line_pitch}</p>
<p className="hero-goal">
<strong>Current goal:</strong> {appState.project.current_goal || 'Set a goal so the cockpit has a heading.'}
</p>
</div>
<div className="hero-stats">
<div>
<span>Now</span>
<strong>{currentFeatureCount}</strong>
</div>
<div>
<span>Parked</span>
<strong>{appState.parking_lot.length}</strong>
</div>
<div>
<span>Pulses</span>
<strong>{appState.pulses.length}</strong>
</div>
</div>
</header>
<section className="project-card card">
<div className="section-heading compact">
<div>
<h2>Project Summary</h2>
<p>Keep the mission clear without turning this into enterprise theater.</p>
</div>
</div>
<div className="form-grid project-grid">
<label>
Project name
<input value={appState.project.name} onChange={(event) => updateProject('name', event.target.value)} />
</label>
<label>
One-line pitch
<input
value={appState.project.one_line_pitch}
onChange={(event) => updateProject('one_line_pitch', event.target.value)}
/>
</label>
<label className="full-span">
Description
<textarea
rows={3}
value={appState.project.description}
onChange={(event) => updateProject('description', event.target.value)}
/>
</label>
<label className="full-span">
Current goal
<input value={appState.project.current_goal} onChange={(event) => updateProject('current_goal', event.target.value)} />
</label>
<label className="full-span">
Notes
<textarea rows={3} value={appState.project.notes} onChange={(event) => updateProject('notes', event.target.value)} />
</label>
</div>
</section>
<nav className="tab-bar" aria-label="Main views">
{TABS.map((tab) => (
<button
key={tab.key}
type="button"
className={tab.key === activeTab ? 'tab active' : 'tab'}
onClick={() => setActiveTab(tab.key)}
>
{tab.label}
</button>
))}
</nav>
<div className="quick-actions card">
<button type="button" onClick={() => { setActiveTab('feature-plan'); resetFeatureDraft() }}>
Add Feature
</button>
<button type="button" onClick={() => { setActiveTab('parking-lot'); resetParkingDraft() }}>
Park Idea
</button>
<button type="button" onClick={() => { setActiveTab('pulse-log'); resetPulseDraft() }}>
Add Pulse
</button>
<button type="button" onClick={() => setActiveTab('export')}>
Export
</button>
</div>
{activeTab === 'feature-plan' && (
<section className="view-stack">
<div className="section-heading">
<div>
<h2>Feature Plan</h2>
<p>Lead with focus, not overview. This is the calm what now? screen.</p>
</div>
</div>
<div className="board-grid">
{FEATURE_COLUMNS.map((column) => (
<article key={column} className="column card">
<div className="column-header">
<div>
<h3>{columnLabels[column]}</h3>
<p>{groupedFeatures[column].length} feature{groupedFeatures[column].length === 1 ? '' : 's'}</p>
</div>
</div>
<div className="column-body">
{groupedFeatures[column].length ? (
groupedFeatures[column].map((feature) => (
<button key={feature.id} type="button" className="item-card feature-card" onClick={() => beginFeatureEdit(feature)}>
<div className="item-card-header">
<strong>{feature.title}</strong>
<span className={`pill ${feature.priority}`}>{feature.priority}</span>
</div>
<p>{feature.description || 'No description yet.'}</p>
<div className="meta-row">
<span>{feature.acceptance_criteria.length} criteria</span>
<label>
<span className="sr-only">Move feature</span>
<select
value={feature.column}
onChange={(event) => {
event.stopPropagation()
quickMoveFeature(feature.id, event.target.value as FeatureColumn)
}}
>
{FEATURE_COLUMNS.map((entry) => (
<option key={entry} value={entry}>
{columnLabels[entry]}
</option>
))}
</select>
</label>
</div>
</button>
))
) : (
<div className="empty-state">No features here yet.</div>
)}
</div>
</article>
))}
</div>
<div className="editor-grid">
<section className="card editor-card">
<div className="section-heading compact">
<div>
<h3>{selectedFeature ? 'Edit Feature' : 'Add Feature'}</h3>
<p>Keep it small, clear, and testable.</p>
</div>
</div>
<div className="form-grid">
<label>
Title
<input value={featureDraft.title} onChange={(event) => setFeatureDraft((current) => ({ ...current, title: event.target.value }))} />
</label>
<label>
Column
<select value={featureDraft.column} onChange={(event) => setFeatureDraft((current) => ({ ...current, column: event.target.value as FeatureColumn }))}>
{FEATURE_COLUMNS.map((column) => (
<option key={column} value={column}>
{columnLabels[column]}
</option>
))}
</select>
</label>
<label>
Priority
<select value={featureDraft.priority} onChange={(event) => setFeatureDraft((current) => ({ ...current, priority: event.target.value as (typeof FEATURE_PRIORITIES)[number] }))}>
{FEATURE_PRIORITIES.map((priority) => (
<option key={priority} value={priority}>
{priority}
</option>
))}
</select>
</label>
<label>
Status
<select value={featureDraft.status} onChange={(event) => setFeatureDraft((current) => ({ ...current, status: event.target.value as (typeof FEATURE_STATUSES)[number] }))}>
{FEATURE_STATUSES.map((status) => (
<option key={status} value={status}>
{status}
</option>
))}
</select>
</label>
<label className="full-span">
Description
<textarea rows={3} value={featureDraft.description} onChange={(event) => setFeatureDraft((current) => ({ ...current, description: event.target.value }))} />
</label>
<label className="full-span">
Acceptance criteria (one per line)
<textarea rows={5} value={featureDraft.acceptanceCriteria} onChange={(event) => setFeatureDraft((current) => ({ ...current, acceptanceCriteria: event.target.value }))} />
</label>
<label className="full-span">
Scope notes
<textarea rows={3} value={featureDraft.scopeNotes} onChange={(event) => setFeatureDraft((current) => ({ ...current, scopeNotes: event.target.value }))} />
</label>
</div>
<div className="button-row">
<button type="button" onClick={saveFeature}>{selectedFeature ? 'Save Changes' : 'Add Feature'}</button>
<button type="button" className="ghost" onClick={resetFeatureDraft}>Clear</button>
{selectedFeature && (
<button type="button" className="danger" onClick={() => deleteFeature(selectedFeature.id)}>
Delete Feature
</button>
)}
</div>
</section>
<section className="card preview-card">
<div className="section-heading compact">
<div>
<h3>Recent Pulse</h3>
<p>Just enough movement to stay grounded.</p>
</div>
</div>
{recentPulsePreview.length ? (
<div className="list-stack">
{recentPulsePreview.map((pulse) => (
<div key={pulse.id} className="mini-pulse">
<strong>{pulse.pulse_type}</strong>
<span>{formatDateTime(pulse.timestamp)}</span>
<p>{pulse.message}</p>
</div>
))}
</div>
) : (
<div className="empty-state">No pulses yet. Add an INTENT or DECISION to start the log.</div>
)}
</section>
</div>
</section>
)}
{activeTab === 'parking-lot' && (
<section className="view-stack">
<div className="section-heading">
<div>
<h2>Parking Lot</h2>
<p>This is where useful distractions go so they do not hijack the build.</p>
</div>
</div>
<div className="editor-grid">
<section className="card editor-card">
<div className="section-heading compact">
<div>
<h3>{selectedParkingItem ? 'Edit Parked Idea' : 'Park a New Idea'}</h3>
<p>Capture it cleanly, then get back to the real work.</p>
</div>
</div>
<div className="form-grid">
<label>
Title
<input value={parkingDraft.title} onChange={(event) => setParkingDraft((current) => ({ ...current, title: event.target.value }))} />
</label>
<label>
Risk level
<select value={parkingDraft.riskLevel} onChange={(event) => setParkingDraft((current) => ({ ...current, riskLevel: event.target.value as RiskLevel }))}>
{RISK_LEVELS.map((level) => (
<option key={level} value={level}>
{level}
</option>
))}
</select>
</label>
<label className="full-span">
Description
<textarea rows={3} value={parkingDraft.description} onChange={(event) => setParkingDraft((current) => ({ ...current, description: event.target.value }))} />
</label>
<label className="full-span">
Reason parked
<textarea rows={3} value={parkingDraft.reasonParked} onChange={(event) => setParkingDraft((current) => ({ ...current, reasonParked: event.target.value }))} />
</label>
<label className="full-span">
Possible future placement
<input value={parkingDraft.futurePlacement} onChange={(event) => setParkingDraft((current) => ({ ...current, futurePlacement: event.target.value }))} />
</label>
</div>
<div className="button-row">
<button type="button" onClick={saveParkingItem}>{selectedParkingItem ? 'Save Changes' : 'Park Idea'}</button>
<button type="button" className="ghost" onClick={resetParkingDraft}>Clear</button>
{selectedParkingItem && (
<>
<button type="button" className="ghost" onClick={() => convertParkingItemToFeature(selectedParkingItem)}>
Convert to Feature
</button>
<button type="button" className="danger" onClick={() => deleteParkingItem(selectedParkingItem.id)}>
Delete
</button>
</>
)}
</div>
</section>
<section className="card list-card">
<div className="section-heading compact">
<div>
<h3>Parked Ideas</h3>
<p>Visible enough to trust, quiet enough not to derail you.</p>
</div>
</div>
<div className="list-stack">
{appState.parking_lot.length ? (
appState.parking_lot.map((item) => (
<button key={item.id} type="button" className="item-card parking-card" onClick={() => beginParkingEdit(item)}>
<div className="item-card-header">
<strong>{item.title}</strong>
<span className={`pill risk-${item.risk_level}`}>{item.risk_level}</span>
</div>
<p>{item.description || 'No description yet.'}</p>
<small>{item.reason_parked || 'No reason parked yet.'}</small>
</button>
))
) : (
<div className="empty-state">No parked ideas yet. That probably means you havent had a normal distracted brain day yet.</div>
)}
</div>
</section>
</div>
</section>
)}
{activeTab === 'pulse-log' && (
<section className="view-stack">
<div className="section-heading">
<div>
<h2>Pulse Log</h2>
<p>Manual now, future-compatible later. Keep the history honest.</p>
</div>
</div>
<div className="editor-grid">
<section className="card editor-card">
<div className="section-heading compact">
<div>
<h3>{selectedPulse ? 'Edit Pulse' : 'Add Pulse'}</h3>
<p>Log what happened, not a fantasy backlog status.</p>
</div>
</div>
<div className="form-grid">
<label>
Pulse type
<select value={pulseDraft.pulseType} onChange={(event) => setPulseDraft((current) => ({ ...current, pulseType: event.target.value as (typeof PULSE_TYPES)[number] }))}>
{PULSE_TYPES.map((pulseType) => (
<option key={pulseType} value={pulseType}>
{pulseType}
</option>
))}
</select>
</label>
<label>
Feature link
<select value={pulseDraft.featureId} onChange={(event) => setPulseDraft((current) => ({ ...current, featureId: event.target.value }))}>
<option value="">No linked feature</option>
{appState.features.map((feature) => (
<option key={feature.id} value={feature.id}>
{feature.title}
</option>
))}
</select>
</label>
<label>
Source
<input value={pulseDraft.source} onChange={(event) => setPulseDraft((current) => ({ ...current, source: event.target.value }))} />
</label>
<label>
Agent ID
<input value={pulseDraft.agentId} onChange={(event) => setPulseDraft((current) => ({ ...current, agentId: event.target.value }))} />
</label>
<label>
Confidence
<input value={pulseDraft.confidence} onChange={(event) => setPulseDraft((current) => ({ ...current, confidence: event.target.value }))} />
</label>
<label>
Trace ID
<input value={pulseDraft.traceId} onChange={(event) => setPulseDraft((current) => ({ ...current, traceId: event.target.value }))} />
</label>
<label className="full-span">
Message
<textarea rows={4} value={pulseDraft.message} onChange={(event) => setPulseDraft((current) => ({ ...current, message: event.target.value }))} />
</label>
<label className="full-span">
Evidence refs (one per line)
<textarea rows={4} value={pulseDraft.evidenceRefs} onChange={(event) => setPulseDraft((current) => ({ ...current, evidenceRefs: event.target.value }))} />
</label>
<label className="full-span">
Structured payload JSON
<textarea rows={4} value={pulseDraft.structuredPayload} onChange={(event) => setPulseDraft((current) => ({ ...current, structuredPayload: event.target.value }))} />
</label>
</div>
<div className="button-row">
<button type="button" onClick={savePulse}>{selectedPulse ? 'Save Changes' : 'Add Pulse'}</button>
<button type="button" className="ghost" onClick={resetPulseDraft}>Clear</button>
{selectedPulse && (
<button type="button" className="danger" onClick={() => deletePulse(selectedPulse.id)}>
Delete Pulse
</button>
)}
</div>
</section>
<section className="card list-card">
<div className="section-heading compact filters-heading">
<div>
<h3>Pulse Timeline</h3>
<p>Newest first, with just enough filtering to stay useful.</p>
</div>
<div className="filter-row">
<select value={pulseTypeFilter} onChange={(event) => setPulseTypeFilter(event.target.value)}>
<option value="all">All types</option>
{PULSE_TYPES.map((pulseType) => (
<option key={pulseType} value={pulseType}>
{pulseType}
</option>
))}
</select>
<select value={pulseFeatureFilter} onChange={(event) => setPulseFeatureFilter(event.target.value)}>
<option value="all">All features</option>
{appState.features.map((feature) => (
<option key={feature.id} value={feature.id}>
{feature.title}
</option>
))}
</select>
<select value={pulseSourceFilter} onChange={(event) => setPulseSourceFilter(event.target.value)}>
<option value="all">All sources</option>
{uniqueSources.map((source) => (
<option key={source} value={source}>
{source}
</option>
))}
</select>
</div>
</div>
<div className="list-stack">
{filteredPulses.length ? (
filteredPulses.map((pulse) => {
const linkedFeature = appState.features.find((feature) => feature.id === pulse.feature_id)
return (
<button key={pulse.id} type="button" className="item-card pulse-card" onClick={() => beginPulseEdit(pulse)}>
<div className="item-card-header">
<strong>{pulse.pulse_type}</strong>
<span>{formatDateTime(pulse.timestamp)}</span>
</div>
<p>{pulse.message}</p>
<small>
{linkedFeature?.title ?? (pulse.feature_id ? `${pulse.feature_id} (missing feature)` : 'No linked feature')} · {pulse.source} / {pulse.agent_id}
</small>
</button>
)
})
) : (
<div className="empty-state">No pulses match the current filters.</div>
)}
</div>
</section>
</div>
</section>
)}
{activeTab === 'export' && (
<section className="view-stack">
<div className="section-heading">
<div>
<h2>Export</h2>
<p>Ship clean context packages, not rambling AI bait.</p>
</div>
</div>
<div className="editor-grid">
<section className="card editor-card">
<div className="section-heading compact">
<div>
<h3>Data Exports</h3>
<p>Backups and handoffs first. Fancy multi-file packaging can wait.</p>
</div>
</div>
<div className="button-stack">
<button type="button" onClick={() => downloadText('buildpulse-export.json', createJsonExport(appState), 'application/json;charset=utf-8')}>
Download Full JSON
</button>
<button type="button" onClick={() => downloadText('pulses.jsonl', createPulseJsonl(appState), 'application/x-ndjson;charset=utf-8')}>
Download Pulse JSONL
</button>
<label className="import-label">
Import JSON
<input type="file" accept="application/json" onChange={handleImport} />
</label>
</div>
</section>
<section className="card list-card">
<div className="section-heading compact">
<div>
<h3>Markdown Package</h3>
<p>`CLAUDE_CONTEXT.md` is the decision-boundary file. Keep it sharp.</p>
</div>
</div>
<div className="list-stack markdown-list">
{Object.entries(markdownPackage).map(([filename, content]) => (
<div key={filename} className="markdown-card">
<div className="item-card-header">
<strong>{filename}</strong>
<div className="button-inline-row">
<button type="button" className="ghost small" onClick={() => copyMarkdown(filename)}>
Copy
</button>
<button type="button" className="ghost small" onClick={() => downloadText(filename, content)}>
Download
</button>
</div>
</div>
<pre>{content}</pre>
</div>
))}
</div>
</section>
</div>
</section>
)}
<footer className="status-bar">
<span>{statusMessage}</span>
<span>Stored locally · schema {appState.schema_version}</span>
</footer>
</div>
)
}
export default App