feat: close buildpulse session loop
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "buildpulse",
|
"name": "buildpulse",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.3",
|
"version": "0.4.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"api": "node --env-file=../.env server/index.mjs",
|
"api": "node --env-file=../.env server/index.mjs",
|
||||||
|
|||||||
+86
-27
@@ -358,6 +358,12 @@ function App() {
|
|||||||
[appState.pulses],
|
[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 triagePlacement = triageEditable.placement || triageRecommendation?.suggested_placement || 'parking_lot'
|
||||||
const triageIsFeaturePlacement = triagePlacement === 'now' || triagePlacement === 'next' || triagePlacement === 'later'
|
const triageIsFeaturePlacement = triagePlacement === 'now' || triagePlacement === 'next' || triagePlacement === 'later'
|
||||||
const triageNeedsClarification = triagePlacement === 'needs_clarification'
|
const triageNeedsClarification = triagePlacement === 'needs_clarification'
|
||||||
@@ -1223,14 +1229,15 @@ function App() {
|
|||||||
setPulseDraft(initialPulseDraft)
|
setPulseDraft(initialPulseDraft)
|
||||||
}
|
}
|
||||||
|
|
||||||
const savePulse = () => {
|
const buildPulseFromDraft = () => {
|
||||||
if (!pulseDraft.message.trim()) {
|
if (!pulseDraft.message.trim()) {
|
||||||
setStatusMessage('Pulse message is required.')
|
setStatusMessage('Pulse message is required.')
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = nowIso()
|
const timestamp = nowIso()
|
||||||
const pulse: PulseEvent = {
|
return {
|
||||||
|
pulse: {
|
||||||
id: selectedPulseId ?? `pulse_${Date.now().toString(36)}`,
|
id: selectedPulseId ?? `pulse_${Date.now().toString(36)}`,
|
||||||
timestamp: selectedPulse?.timestamp ?? timestamp,
|
timestamp: selectedPulse?.timestamp ?? timestamp,
|
||||||
project_id: appState.project.id,
|
project_id: appState.project.id,
|
||||||
@@ -1242,25 +1249,58 @@ function App() {
|
|||||||
confidence_score: Number(pulseDraft.confidence) || 0,
|
confidence_score: Number(pulseDraft.confidence) || 0,
|
||||||
evidence_refs: linesToArray(pulseDraft.evidenceRefs),
|
evidence_refs: linesToArray(pulseDraft.evidenceRefs),
|
||||||
trace_id: pulseDraft.traceId.trim() || undefined,
|
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) => ({
|
setAppState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
pulses: current.pulses.map((entry) => (entry.id === selectedPulseId ? pulse : entry)),
|
features: featureProgress
|
||||||
}))
|
? current.features.map((feature) =>
|
||||||
setStatusMessage('Pulse updated.')
|
feature.id === featureProgress.featureId
|
||||||
} else {
|
? {
|
||||||
setAppState((current) => ({
|
...feature,
|
||||||
...current,
|
status: featureProgress.status,
|
||||||
pulses: [pulse, ...current.pulses],
|
column: featureProgress.column,
|
||||||
}))
|
updated_at: featureProgress.timestamp,
|
||||||
setStatusMessage('Pulse added.')
|
|
||||||
}
|
}
|
||||||
|
: feature,
|
||||||
|
)
|
||||||
|
: current.features,
|
||||||
|
pulses: selectedPulseId ? current.pulses.map((entry) => (entry.id === selectedPulseId ? pulse : entry)) : [pulse, ...current.pulses],
|
||||||
|
}))
|
||||||
|
setStatusMessage(successMessage)
|
||||||
resetPulseDraft()
|
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) => {
|
const deletePulse = (pulseId: string) => {
|
||||||
setAppState((current) => ({
|
setAppState((current) => ({
|
||||||
...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)
|
const feature = appState.features.find((entry) => entry.id === featureId)
|
||||||
if (!feature) return null
|
if (!feature) return null
|
||||||
|
|
||||||
@@ -1324,22 +1364,30 @@ function App() {
|
|||||||
feature_id: feature.id,
|
feature_id: feature.id,
|
||||||
source: 'buildpulse',
|
source: 'buildpulse',
|
||||||
agent_id: target,
|
agent_id: target,
|
||||||
pulse_type: 'INTENT',
|
pulse_type: 'SESSION_START',
|
||||||
message: `Generated ${target} handoff for feature “${feature.title}”.`,
|
message: `Started ${target} build session for feature “${feature.title}”.`,
|
||||||
confidence_score: 0.9,
|
confidence_score: 0.9,
|
||||||
evidence_refs: [
|
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.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',
|
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) => ({
|
setAppState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
|
features: current.features.map((entry) => (entry.id === feature.id ? nextFeature : entry)),
|
||||||
pulses: [pulse, ...current.pulses],
|
pulses: [pulse, ...current.pulses],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return feature
|
return nextFeature
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyFocusedHandoff = async (featureId?: string, target = promptTarget, shouldLogIntent = false, promptOverride?: string) => {
|
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
|
let feature = resolvedFeatureId ? appState.features.find((entry) => entry.id === resolvedFeatureId) : null
|
||||||
if (resolvedFeatureId && shouldLogIntent) {
|
if (resolvedFeatureId && shouldLogIntent) {
|
||||||
feature = logHandoffIntent(resolvedFeatureId, target) ?? feature
|
feature = startFeatureSession(resolvedFeatureId, target) ?? feature
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatusMessage(
|
setStatusMessage(
|
||||||
feature
|
feature
|
||||||
? `${target} handoff for “${feature.title}” copied${shouldLogIntent ? ' and INTENT logged' : ''}.`
|
? `${target} handoff for “${feature.title}” copied${shouldLogIntent ? ', session started, and status moved to building' : ''}.`
|
||||||
: `${target} handoff copied${shouldLogIntent ? ' and INTENT logged' : ''}.`,
|
: `${target} handoff copied${shouldLogIntent ? ' and session started' : ''}.`,
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1532,7 +1580,7 @@ function App() {
|
|||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
<header className="mobile-shell-header card">
|
<header className="mobile-shell-header card">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">BuildPulse v0.4.3</p>
|
<p className="eyebrow">BuildPulse v0.4.4</p>
|
||||||
<h1>BuildPulse</h1>
|
<h1>BuildPulse</h1>
|
||||||
<p className="hero-goal compact-goal">
|
<p className="hero-goal compact-goal">
|
||||||
<strong>Current goal:</strong> {appState.project.current_goal || 'Classify new ideas before they become work.'}
|
<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">
|
<section className="card editor-sheet handoff-sheet" role="dialog" aria-modal="true" aria-label="Handoff preview">
|
||||||
<div className="section-heading compact">
|
<div className="section-heading compact">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">v0.4.1 Handoff preview</p>
|
<p className="eyebrow">v0.4.4 Build handoff</p>
|
||||||
<h3>{handoffPreviewFeature.title}</h3>
|
<h3>{handoffPreviewFeature.title}</h3>
|
||||||
<p>Choose the target, tweak the brief if needed, then copy it cleanly.</p>
|
<p>Choose the target, tweak the brief if needed, then copy it cleanly.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1867,7 +1915,7 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
<p className="tap-hint">Switching targets resets the draft to that target’s preset.</p>
|
||||||
<div className="button-inline-row">
|
<div className="button-inline-row">
|
||||||
<button type="button" className="ghost small" onClick={resetHandoffPreview}>Reset to preset</button>
|
<button type="button" className="ghost small" onClick={resetHandoffPreview}>Reset to preset</button>
|
||||||
@@ -1904,7 +1952,7 @@ function App() {
|
|||||||
if (ok) setHandoffPreviewOpen(false)
|
if (ok) setHandoffPreviewOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Copy + INTENT Pulse
|
Copy + Start Session
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="ghost" onClick={() => setHandoffPreviewOpen(false)}>
|
<button type="button" className="ghost" onClick={() => setHandoffPreviewOpen(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -2246,8 +2294,19 @@ function App() {
|
|||||||
<textarea rows={4} value={pulseDraft.evidenceRefs} onChange={(event) => setPulseDraft((current) => ({ ...current, evidenceRefs: event.target.value }))} />
|
<textarea rows={4} value={pulseDraft.evidenceRefs} onChange={(event) => setPulseDraft((current) => ({ ...current, evidenceRefs: event.target.value }))} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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">
|
<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>
|
<button type="button" className="ghost" onClick={resetPulseDraft}>Clear</button>
|
||||||
{selectedPulse && (
|
{selectedPulse && (
|
||||||
<button type="button" className="danger" onClick={() => deletePulse(selectedPulse.id)}>
|
<button type="button" className="danger" onClick={() => deletePulse(selectedPulse.id)}>
|
||||||
|
|||||||
@@ -2833,3 +2833,40 @@ select {
|
|||||||
white-space: nowrap;
|
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