feat: close buildpulse session loop
This commit is contained in:
+100
-41
@@ -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() {
|
||||
<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 target’s 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)}>
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user