feat: close buildpulse session loop

This commit is contained in:
OpenClaw Bot
2026-05-12 23:23:57 +02:00
parent f09f132220
commit 4cfed90f37
3 changed files with 138 additions and 42 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "buildpulse",
"private": true,
"version": "0.4.3",
"version": "0.4.4",
"type": "module",
"scripts": {
"api": "node --env-file=../.env server/index.mjs",
+86 -27
View File
@@ -358,6 +358,12 @@ function App() {
[appState.pulses],
)
const pulseDraftFeature = useMemo(
() => appState.features.find((feature) => feature.id === pulseDraft.featureId) ?? null,
[appState.features, pulseDraft.featureId],
)
const pulseDraftCanUpdateProgress = !selectedPulseId && Boolean(pulseDraftFeature) && (pulseDraft.pulseType === 'RESULT' || pulseDraft.pulseType === 'TEST_RESULT')
const triagePlacement = triageEditable.placement || triageRecommendation?.suggested_placement || 'parking_lot'
const triageIsFeaturePlacement = triagePlacement === 'now' || triagePlacement === 'next' || triagePlacement === 'later'
const triageNeedsClarification = triagePlacement === 'needs_clarification'
@@ -1223,14 +1229,15 @@ function App() {
setPulseDraft(initialPulseDraft)
}
const savePulse = () => {
const buildPulseFromDraft = () => {
if (!pulseDraft.message.trim()) {
setStatusMessage('Pulse message is required.')
return
return null
}
const timestamp = nowIso()
const pulse: PulseEvent = {
return {
pulse: {
id: selectedPulseId ?? `pulse_${Date.now().toString(36)}`,
timestamp: selectedPulse?.timestamp ?? timestamp,
project_id: appState.project.id,
@@ -1242,25 +1249,58 @@ function App() {
confidence_score: Number(pulseDraft.confidence) || 0,
evidence_refs: linesToArray(pulseDraft.evidenceRefs),
trace_id: pulseDraft.traceId.trim() || undefined,
} satisfies PulseEvent,
timestamp,
}
}
if (selectedPulseId) {
const persistPulse = (
pulse: PulseEvent,
successMessage: string,
featureProgress?: { featureId: string; status: (typeof FEATURE_STATUSES)[number]; column: FeatureColumn; timestamp: string },
) => {
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.')
features: featureProgress
? current.features.map((feature) =>
feature.id === featureProgress.featureId
? {
...feature,
status: featureProgress.status,
column: featureProgress.column,
updated_at: featureProgress.timestamp,
}
: feature,
)
: current.features,
pulses: selectedPulseId ? current.pulses.map((entry) => (entry.id === selectedPulseId ? pulse : entry)) : [pulse, ...current.pulses],
}))
setStatusMessage(successMessage)
resetPulseDraft()
}
const savePulse = () => {
const draftPulse = buildPulseFromDraft()
if (!draftPulse) return
persistPulse(draftPulse.pulse, selectedPulseId ? 'Pulse updated.' : 'Pulse added.')
}
const savePulseWithFeatureProgress = (status: (typeof FEATURE_STATUSES)[number], column: FeatureColumn) => {
const draftPulse = buildPulseFromDraft()
if (!draftPulse) return
if (!draftPulse.pulse.feature_id) {
setStatusMessage('Link a feature before using the build-loop progress buttons.')
return
}
const feature = appState.features.find((entry) => entry.id === draftPulse.pulse.feature_id)
persistPulse(
draftPulse.pulse,
feature ? `Pulse added and “${feature.title}” moved to ${status}.` : `Pulse added and feature moved to ${status}.`,
{ featureId: draftPulse.pulse.feature_id, status, column, timestamp: draftPulse.timestamp },
)
}
const deletePulse = (pulseId: string) => {
setAppState((current) => ({
...current,
@@ -1312,7 +1352,7 @@ function App() {
}
}
const logHandoffIntent = (featureId: string, target: (typeof PROMPT_TARGETS)[number]) => {
const startFeatureSession = (featureId: string, target: (typeof PROMPT_TARGETS)[number]) => {
const feature = appState.features.find((entry) => entry.id === featureId)
if (!feature) return null
@@ -1324,22 +1364,30 @@ function App() {
feature_id: feature.id,
source: 'buildpulse',
agent_id: target,
pulse_type: 'INTENT',
message: `Generated ${target} handoff for feature “${feature.title}”.`,
pulse_type: 'SESSION_START',
message: `Started ${target} build session for feature “${feature.title}”.`,
confidence_score: 0.9,
evidence_refs: [
'Feature-detail handoff action',
'Today build-loop handoff action',
feature.release_id ? `Release: ${appState.releases.find((release) => release.id === feature.release_id)?.name || feature.release_id}` : 'No linked release',
feature.phase_id ? `Phase: ${appState.phases.find((phase) => phase.id === feature.phase_id)?.title || feature.phase_id}` : 'No linked phase',
],
}
const nextFeature = {
...feature,
column: feature.column === 'done' ? feature.column : ('now' as FeatureColumn),
status: feature.status === 'done' ? feature.status : ('building' as (typeof FEATURE_STATUSES)[number]),
updated_at: timestamp,
}
setAppState((current) => ({
...current,
features: current.features.map((entry) => (entry.id === feature.id ? nextFeature : entry)),
pulses: [pulse, ...current.pulses],
}))
return feature
return nextFeature
}
const copyFocusedHandoff = async (featureId?: string, target = promptTarget, shouldLogIntent = false, promptOverride?: string) => {
@@ -1355,13 +1403,13 @@ function App() {
let feature = resolvedFeatureId ? appState.features.find((entry) => entry.id === resolvedFeatureId) : null
if (resolvedFeatureId && shouldLogIntent) {
feature = logHandoffIntent(resolvedFeatureId, target) ?? feature
feature = startFeatureSession(resolvedFeatureId, target) ?? feature
}
setStatusMessage(
feature
? `${target} handoff for “${feature.title}” copied${shouldLogIntent ? ' and INTENT logged' : ''}.`
: `${target} handoff copied${shouldLogIntent ? ' and INTENT logged' : ''}.`,
? `${target} handoff for “${feature.title}” copied${shouldLogIntent ? ', session started, and status moved to building' : ''}.`
: `${target} handoff copied${shouldLogIntent ? ' and session started' : ''}.`,
)
return true
} catch {
@@ -1532,7 +1580,7 @@ function App() {
<div className="app-shell">
<header className="mobile-shell-header card">
<div>
<p className="eyebrow">BuildPulse v0.4.3</p>
<p className="eyebrow">BuildPulse v0.4.4</p>
<h1>BuildPulse</h1>
<p className="hero-goal compact-goal">
<strong>Current goal:</strong> {appState.project.current_goal || 'Classify new ideas before they become work.'}
@@ -1830,7 +1878,7 @@ function App() {
<section className="card editor-sheet handoff-sheet" role="dialog" aria-modal="true" aria-label="Handoff preview">
<div className="section-heading compact">
<div>
<p className="eyebrow">v0.4.1 Handoff preview</p>
<p className="eyebrow">v0.4.4 Build handoff</p>
<h3>{handoffPreviewFeature.title}</h3>
<p>Choose the target, tweak the brief if needed, then copy it cleanly.</p>
</div>
@@ -1867,7 +1915,7 @@ function App() {
</button>
))}
</div>
<p className="tap-hint">Preview mode is the safer path when you want to edit wording or create a clean INTENT pulse.</p>
<p className="tap-hint">Preview mode is the safer path when you want to edit wording, copy the brief, and mark the feature as actively building.</p>
<p className="tap-hint">Switching targets resets the draft to that targets preset.</p>
<div className="button-inline-row">
<button type="button" className="ghost small" onClick={resetHandoffPreview}>Reset to preset</button>
@@ -1904,7 +1952,7 @@ function App() {
if (ok) setHandoffPreviewOpen(false)
}}
>
Copy + INTENT Pulse
Copy + Start Session
</button>
<button type="button" className="ghost" onClick={() => setHandoffPreviewOpen(false)}>
Cancel
@@ -2246,8 +2294,19 @@ function App() {
<textarea rows={4} value={pulseDraft.evidenceRefs} onChange={(event) => setPulseDraft((current) => ({ ...current, evidenceRefs: event.target.value }))} />
</label>
</div>
{pulseDraftCanUpdateProgress && (
<div className="result-progress-strip" aria-label="Build-loop progress shortcuts">
<span>Finish the loop for {pulseDraftFeature?.title}</span>
<button type="button" className="ghost small" onClick={() => savePulseWithFeatureProgress('testing', 'now')}>
Save + Move to Testing
</button>
<button type="button" className="primary small" onClick={() => savePulseWithFeatureProgress('done', 'done')}>
Save + Mark Done
</button>
</div>
)}
<div className="button-row sheet-actions">
<button type="button" onClick={savePulse}>{selectedPulse ? 'Save Changes' : 'Add Pulse'}</button>
<button type="button" onClick={savePulse}>{selectedPulse ? 'Save Changes' : 'Add Pulse Only'}</button>
<button type="button" className="ghost" onClick={resetPulseDraft}>Clear</button>
{selectedPulse && (
<button type="button" className="danger" onClick={() => deletePulse(selectedPulse.id)}>
+37
View File
@@ -2833,3 +2833,40 @@ select {
white-space: nowrap;
}
}
/* v0.4.4 — close the build loop from result capture back into feature progress. */
.result-progress-strip {
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
align-items: center;
justify-content: flex-end;
margin-top: 0.9rem;
padding: 0.7rem;
border: 1px solid rgba(125, 211, 252, 0.2);
border-radius: 10px;
background: rgba(14, 165, 233, 0.08);
}
.result-progress-strip span {
margin-right: auto;
color: #c4d1e6;
font-size: 0.9rem;
font-weight: 800;
}
@media (max-width: 760px) {
.result-progress-strip {
display: grid;
grid-template-columns: 1fr;
justify-content: stretch;
}
.result-progress-strip span {
margin-right: 0;
}
.result-progress-strip button {
width: 100%;
}
}