feat: discipline buildpulse today commands

This commit is contained in:
OpenClaw Bot
2026-05-12 22:58:33 +02:00
parent 1654173540
commit 579cffd874
3 changed files with 199 additions and 26 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "buildpulse", "name": "buildpulse",
"private": true, "private": true,
"version": "0.4.2", "version": "0.4.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"api": "node --env-file=../.env server/index.mjs", "api": "node --env-file=../.env server/index.mjs",
+120 -25
View File
@@ -148,6 +148,7 @@ function App() {
const [showManualFeatureEditor, setShowManualFeatureEditor] = useState(false) const [showManualFeatureEditor, setShowManualFeatureEditor] = useState(false)
const [showManualParkingEditor, setShowManualParkingEditor] = useState(false) const [showManualParkingEditor, setShowManualParkingEditor] = useState(false)
const [showManualPulseEditor, setShowManualPulseEditor] = useState(false) const [showManualPulseEditor, setShowManualPulseEditor] = useState(false)
const [todayCommandDrawerOpen, setTodayCommandDrawerOpen] = useState(false)
const hasHydratedRemote = useRef(false) const hasHydratedRemote = useRef(false)
const initialLocalStateRef = useRef(appState) const initialLocalStateRef = useRef(appState)
const triageStatusRef = useRef<HTMLDivElement | null>(null) const triageStatusRef = useRef<HTMLDivElement | null>(null)
@@ -1531,7 +1532,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.2</p> <p className="eyebrow">BuildPulse v0.4.3</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.'}
@@ -2429,16 +2430,123 @@ function App() {
<h2>Capture Brief Build</h2> <h2>Capture Brief Build</h2>
<p>One decision at a time. Capture the idea, pick the next move, hand it to an agent.</p> <p>One decision at a time. Capture the idea, pick the next move, hand it to an agent.</p>
</div> </div>
<div className="button-inline-row">
<button type="button" className="primary-triage-button" onClick={() => openTriage()}>
+ Add Idea
</button>
<button type="button" className="ghost" onClick={() => setShowManualFeatureEditor((current) => !current)}>
Manual
</button>
</div>
</div> </div>
<div className="today-command-bar" aria-label="Today primary workflow">
<button type="button" className="primary-triage-button today-primary-command" onClick={() => openTriage()}>
Capture new idea
</button>
<button
type="button"
className="ghost today-drawer-toggle"
aria-expanded={todayCommandDrawerOpen}
aria-controls="today-command-drawer"
onClick={() => setTodayCommandDrawerOpen((current) => !current)}
>
{todayCommandDrawerOpen ? 'Hide commands' : 'Commands'}
</button>
</div>
{todayCommandDrawerOpen && (
<div id="today-command-drawer" className="today-command-drawer" aria-label="Secondary Today commands">
<div>
<p className="eyebrow">Build</p>
<button
type="button"
className="drawer-command primary"
disabled={!focusFeature}
onClick={() => {
if (!focusFeature) return
setTodayCommandDrawerOpen(false)
openHandoffPreview(focusFeature.id, 'OpenClaw')
}}
>
Start AI session
</button>
<button
type="button"
className="drawer-command"
disabled={!focusFeature}
onClick={() => {
if (!focusFeature) return
setTodayCommandDrawerOpen(false)
openFeatureResultCapture(focusFeature.id)
}}
>
Record result
</button>
<button
type="button"
className="drawer-command"
disabled={!focusFeature}
onClick={() => {
if (!focusFeature) return
setTodayCommandDrawerOpen(false)
beginFeatureEdit(focusFeature)
}}
>
Focus details
</button>
</div>
<div>
<p className="eyebrow">Sort</p>
<button
type="button"
className="drawer-command"
onClick={() => {
setTodayCommandDrawerOpen(false)
setShowManualFeatureEditor((current) => !current)
}}
>
Manual feature
</button>
<button
type="button"
className="drawer-command"
onClick={() => {
setTodayCommandDrawerOpen(false)
setActiveTab('parking-lot')
}}
>
Parked ideas
</button>
<button
type="button"
className="drawer-command"
onClick={() => {
setTodayCommandDrawerOpen(false)
setActiveTab('pulse-log')
}}
>
Pulse history
</button>
</div>
<div>
<p className="eyebrow">Ship</p>
<button
type="button"
className="drawer-command"
onClick={() => {
setTodayCommandDrawerOpen(false)
setActiveTab('roadmap')
}}
>
Release view
</button>
<button
type="button"
className="drawer-command"
onClick={() => {
setTodayCommandDrawerOpen(false)
setActiveTab('export')
}}
>
Session handoff
</button>
</div>
</div>
)}
<div className="today-grid"> <div className="today-grid">
<article className="focus-panel today-focus-card"> <article className="focus-panel today-focus-card">
<p className="eyebrow">Focus</p> <p className="eyebrow">Focus</p>
@@ -2451,17 +2559,7 @@ function App() {
<span className="pill">{focusFeature.status}</span> <span className="pill">{focusFeature.status}</span>
<span className="pill">{columnLabels[focusFeature.column]}</span> <span className="pill">{columnLabels[focusFeature.column]}</span>
</div> </div>
<div className="button-inline-row sticky-action-row"> <p className="today-card-note">Use Commands for build, result, and detail actions.</p>
<button type="button" className="primary small" onClick={() => openHandoffPreview(focusFeature.id, 'OpenClaw')}>
Start AI Session
</button>
<button type="button" className="ghost small" onClick={() => openFeatureResultCapture(focusFeature.id)}>
Record Result
</button>
<button type="button" className="ghost small" onClick={() => beginFeatureEdit(focusFeature)}>
Details
</button>
</div>
</> </>
) : ( ) : (
<p>No focus feature yet. Add an idea and let triage decide if it deserves to become work.</p> <p>No focus feature yet. Add an idea and let triage decide if it deserves to become work.</p>
@@ -2477,17 +2575,14 @@ function App() {
<small>{selectedReleaseBlockers.length ? `${selectedReleaseBlockers.length} required item(s) still open` : 'No required blockers detected'}</small> <small>{selectedReleaseBlockers.length ? `${selectedReleaseBlockers.length} required item(s) still open` : 'No required blockers detected'}</small>
<small>{selectedRelease?.forbidden_feature_titles.length ? `Forbidden right now: ${selectedRelease.forbidden_feature_titles.slice(0, 3).join(', ')}` : 'No forbidden-work list yet'}</small> <small>{selectedRelease?.forbidden_feature_titles.length ? `Forbidden right now: ${selectedRelease.forbidden_feature_titles.slice(0, 3).join(', ')}` : 'No forbidden-work list yet'}</small>
</div> </div>
<button type="button" className="ghost small" onClick={() => setActiveTab('roadmap')}>Release</button> <p className="today-card-note">Release controls live in Commands.</p>
</article> </article>
<article className="focus-panel today-decision-card"> <article className="focus-panel today-decision-card">
<p className="eyebrow">Needs decision</p> <p className="eyebrow">Needs decision</p>
<h3>{duplicateParkingGroups.length ? `${duplicateParkingGroups.length} duplicate idea group${duplicateParkingGroups.length === 1 ? '' : 's'}` : 'No obvious duplicate pile-ups'}</h3> <h3>{duplicateParkingGroups.length ? `${duplicateParkingGroups.length} duplicate idea group${duplicateParkingGroups.length === 1 ? '' : 's'}` : 'No obvious duplicate pile-ups'}</h3>
<p>{appState.parking_lot.length} parked · {appState.pulses.length} pulses</p> <p>{appState.parking_lot.length} parked · {appState.pulses.length} pulses</p>
<div className="button-inline-row"> <p className="today-card-note">Decision controls live in Commands.</p>
<button type="button" className="ghost small" onClick={() => setActiveTab('parking-lot')}>Ideas</button>
<button type="button" className="ghost small" onClick={() => setActiveTab('pulse-log')}>History</button>
</div>
</article> </article>
</div> </div>
</section> </section>
+78
View File
@@ -2722,3 +2722,81 @@ select {
line-height: 1.12; line-height: 1.12;
} }
} }
/* v0.4.3 — interaction discipline: one obvious Today path, secondary commands in a drawer. */
.today-command-bar {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.7rem;
align-items: stretch;
margin: 1rem 0;
}
.today-primary-command {
min-height: 3rem;
justify-content: center;
font-size: 1rem;
}
.today-drawer-toggle {
min-width: 9rem;
justify-content: center;
}
.today-command-drawer {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.75rem;
margin: -0.2rem 0 1rem;
padding: 0.75rem;
border: 1px solid rgba(125, 211, 252, 0.18);
border-radius: 12px;
background: rgba(3, 7, 18, 0.72);
}
.drawer-command {
width: 100%;
justify-content: flex-start;
margin-top: 0.45rem;
border-color: rgba(148, 163, 184, 0.16);
background: rgba(148, 163, 184, 0.06);
}
.drawer-command.primary {
justify-content: center;
border-color: rgba(125, 211, 252, 0.5);
background: linear-gradient(135deg, rgba(14, 165, 233, 0.92), rgba(59, 130, 246, 0.92));
color: #f8fbff;
}
.drawer-command:disabled {
cursor: not-allowed;
opacity: 0.42;
}
.today-card-note {
margin-top: 0.75rem;
color: #8fa0bb;
font-size: 0.82rem;
}
@media (max-width: 760px) {
.today-command-bar {
grid-template-columns: 1fr;
gap: 0.45rem;
margin: 0.75rem 0 0.55rem;
}
.today-drawer-toggle {
width: 100%;
min-width: 0;
}
.today-command-drawer {
grid-template-columns: 1fr;
gap: 0.55rem;
margin: 0 0 0.45rem;
padding: 0.6rem;
border-radius: 9px;
}
}