From 4cfed90f37b2687a747dcafb035c184b305d4816 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Tue, 12 May 2026 23:23:57 +0200 Subject: [PATCH] feat: close buildpulse session loop --- package.json | 2 +- src/App.tsx | 141 +++++++++++++++++++++++++++++++++++--------------- src/index.css | 37 +++++++++++++ 3 files changed, 138 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 6b6f994..91f7192 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 8e541f4..7b944c4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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,44 +1229,78 @@ 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 = { - 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(), - 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.') + return { + pulse: { + 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(), + confidence_score: Number(pulseDraft.confidence) || 0, + evidence_refs: linesToArray(pulseDraft.evidenceRefs), + trace_id: pulseDraft.traceId.trim() || undefined, + } satisfies PulseEvent, + timestamp, } + } + const persistPulse = ( + pulse: PulseEvent, + successMessage: string, + featureProgress?: { featureId: string; status: (typeof FEATURE_STATUSES)[number]; column: FeatureColumn; timestamp: string }, + ) => { + setAppState((current) => ({ + ...current, + 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() {
-

BuildPulse v0.4.3

+

BuildPulse v0.4.4

BuildPulse

Current goal: {appState.project.current_goal || 'Classify new ideas before they become work.'} @@ -1830,7 +1878,7 @@ function App() {

-

v0.4.1 Handoff preview

+

v0.4.4 Build handoff

{handoffPreviewFeature.title}

Choose the target, tweak the brief if needed, then copy it cleanly.

@@ -1867,7 +1915,7 @@ function App() { ))}
-

Preview mode is the safer path when you want to edit wording or create a clean INTENT pulse.

+

Preview mode is the safer path when you want to edit wording, copy the brief, and mark the feature as actively building.

Switching targets resets the draft to that target’s preset.

@@ -1904,7 +1952,7 @@ function App() { if (ok) setHandoffPreviewOpen(false) }} > - Copy + INTENT Pulse + Copy + Start Session