From cc63348344ae35260e8104ba580d6afca98495de Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Sun, 10 May 2026 06:23:42 +0200 Subject: [PATCH] feat: ship buildpulse 0.2.1 mobile triage flow --- package.json | 2 +- src/App.tsx | 991 ++++++++++++++++++++++++++------------------------ src/index.css | 438 ++++++++++++++++++++++ 3 files changed, 946 insertions(+), 485 deletions(-) diff --git a/package.json b/package.json index f1bfd21..4a148bb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "buildpulse", "private": true, - "version": "0.0.0", + "version": "0.2.1", "type": "module", "scripts": { "api": "node --env-file=../.env server/index.mjs", diff --git a/src/App.tsx b/src/App.tsx index 14ddf9a..c04f7c0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,13 +9,14 @@ import type { AiPlacement, AiRecommendation, AppState, Feature, FeatureColumn, P 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: 'feature-plan', label: 'Plan' }, + { key: 'parking-lot', label: 'Park' }, + { key: 'pulse-log', label: 'Pulse' }, { key: 'export', label: 'Export' }, ] const PROMPT_TARGETS = ['OpenClaw', 'Claude Code', 'Codex', 'Generic Agent'] as const +const PARKING_RISK_ORDER: RiskLevel[] = ['dangerous', 'high', 'medium', 'low'] const initialFeatureDraft = { title: '', @@ -115,8 +116,13 @@ function App() { acceptanceCriteria: '', parkingReason: '', }) - const [triageStatus, setTriageStatus] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle') + const [triageStatus, setTriageStatus] = useState<'idle' | 'loading' | 'ready' | 'error' | 'saved'>('idle') const [triageError, setTriageError] = useState('') + const [triageEditMode, setTriageEditMode] = useState(false) + const [triageSavedMessage, setTriageSavedMessage] = useState('') + const [showManualFeatureEditor, setShowManualFeatureEditor] = useState(false) + const [showManualParkingEditor, setShowManualParkingEditor] = useState(false) + const [showManualPulseEditor, setShowManualPulseEditor] = useState(false) const hasHydratedRemote = useRef(false) const initialLocalStateRef = useRef(appState) @@ -207,32 +213,14 @@ function App() { [appState.pulses, selectedPulseId], ) - const featurePulseMeta = useMemo(() => { - const meta = new Map() - - for (const pulse of [...appState.pulses].sort((a, b) => b.timestamp.localeCompare(a.timestamp))) { - if (!pulse.feature_id) continue - - const current = meta.get(pulse.feature_id) - if (current) { - current.count += 1 - continue - } - - meta.set(pulse.feature_id, { count: 1, latest: pulse }) - } - - return meta - }, [appState.pulses]) - - const selectedFeaturePulses = useMemo(() => { - if (!selectedFeature) return [] - - return [...appState.pulses] - .filter((pulse) => pulse.feature_id === selectedFeature.id) - .sort((a, b) => b.timestamp.localeCompare(a.timestamp)) - .slice(0, 4) - }, [appState.pulses, selectedFeature]) + const parkingByRisk = useMemo( + () => + PARKING_RISK_ORDER.map((risk) => ({ + risk, + items: appState.parking_lot.filter((item) => item.risk_level === risk), + })), + [appState.parking_lot], + ) const filteredPulses = useMemo(() => { return [...appState.pulses] @@ -247,6 +235,11 @@ function App() { [appState.pulses], ) + const triagePlacement = triageEditable.placement || triageRecommendation?.suggested_placement || 'parking_lot' + const triageIsFeaturePlacement = triagePlacement === 'now' || triagePlacement === 'next' || triagePlacement === 'later' + const triageNeedsClarification = triagePlacement === 'needs_clarification' + const triageSuggestedFuturePlacement = triagePlacement === 'later' ? 'Later' : triagePlacement === 'next' ? 'Next' : 'v0.3+' + const markdownPackage = useMemo(() => createMarkdownPackage(appState), [appState]) const sessionPrompt = useMemo( () => createAgentSessionPrompt(appState, { featureId: promptFeatureId || undefined, target: promptTarget }), @@ -347,15 +340,6 @@ function App() { 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 refreshFromBackend = async () => { setSyncAction('refreshing') setSyncStatus('syncing') @@ -409,9 +393,46 @@ function App() { const openTriage = (seed = '') => { setTriageOpen(true) setTriageError('') - setTriageStatus(triageRecommendation ? 'ready' : 'idle') + setTriageEditMode(false) + setTriageSavedMessage('') + setTriageStatus(triageRecommendation && !seed ? 'ready' : 'idle') if (seed) { setTriageDraft((current) => ({ ...current, rawIdea: seed })) + setTriageRecommendation(null) + } + } + + const resetTriageFlow = () => { + setTriageDraft(initialTriageDraft) + setTriageRecommendation(null) + setTriageSavedMessage('') + setTriageStatus('idle') + setTriageError('') + setTriageEditMode(false) + } + + const openDecisionPulse = (pulseId?: string | null) => { + if (!pulseId) return + setSelectedPulseId(pulseId) + setActiveTab('pulse-log') + setTriageOpen(false) + } + + const openSavedTriageItem = () => { + if (!triageRecommendation) return + + if (triageRecommendation.created_feature_id) { + setSelectedFeatureId(triageRecommendation.created_feature_id) + setActiveTab('feature-plan') + setTriageOpen(false) + return + } + + if (triageRecommendation.created_parking_item_id) { + setSelectedParkingId(triageRecommendation.created_parking_item_id) + setActiveTab('parking-lot') + setTriageOpen(false) + return } } @@ -469,6 +490,7 @@ function App() { setTriageStatus('loading') setTriageError('') + setTriageSavedMessage('') try { const recommendation = await triageIdeaWithAi({ @@ -501,6 +523,7 @@ function App() { parkingReason: fullRecommendation.suggested_parking_reason || fullRecommendation.reason, }) setTriageStatus('ready') + setTriageEditMode(false) setStatusMessage(`AI suggested ${placementLabels[fullRecommendation.suggested_placement]} with ${fullRecommendation.scope_risk} scope risk.`) } catch (error) { setTriageStatus('error') @@ -577,8 +600,11 @@ function App() { pulses: [pulse, ...current.pulses], ai_recommendations: [updatedRecommendation, ...current.ai_recommendations.filter((entry) => entry.id !== updatedRecommendation.id)], })) + setTriageRecommendation(updatedRecommendation) setSelectedFeatureId(featureId) setActiveTab('feature-plan') + setTriageStatus('saved') + setTriageSavedMessage(`Feature created: “${title}”. Decision pulse created.`) setStatusMessage(`Accepted AI triage as feature “${title}”. DECISION pulse logged.`) } @@ -609,7 +635,7 @@ function App() { title, description: triageEditable.description.trim(), reason_parked: triageEditable.parkingReason.trim() || triageRecommendation.reason, - possible_future_placement: triageEditable.placement === 'later' ? 'Later' : 'v0.3+', + possible_future_placement: triageSuggestedFuturePlacement, risk_level: triageEditable.risk, created_at: timestamp, updated_at: timestamp, @@ -619,8 +645,11 @@ function App() { pulses: [pulse, ...current.pulses], ai_recommendations: [updatedRecommendation, ...current.ai_recommendations.filter((entry) => entry.id !== updatedRecommendation.id)], })) + setTriageRecommendation(updatedRecommendation) setSelectedParkingId(parkingId) setActiveTab('parking-lot') + setTriageStatus('saved') + setTriageSavedMessage(`Idea parked: “${title}”. Decision pulse created.`) setStatusMessage(`Accepted AI triage as Parking Lot item “${title}”. DECISION pulse logged.`) } @@ -638,6 +667,9 @@ function App() { pulses: [pulse, ...current.pulses], ai_recommendations: [updatedRecommendation, ...current.ai_recommendations.filter((entry) => entry.id !== updatedRecommendation.id)], })) + setTriageRecommendation(updatedRecommendation) + setTriageStatus('saved') + setTriageSavedMessage('Recommendation rejected. Decision pulse created; no feature was created.') setStatusMessage('Rejected AI triage recommendation. DECISION pulse logged; no feature created.') } @@ -972,10 +1004,26 @@ function App() { : syncStatus === 'degraded' ? 'Sync degraded · local cache active' : 'Connecting…' - return (
-