Fix blank Focus + calm-standup nag chrome (found dogfooding live)

Poking at the live app (a real repo with no open issues) surfaced two empty-state gaps:

- Focus (Morning service) rendered a bare header — no cards, no empty state —
  because scheduleFocus returns a FocusView with empty now/next/later rather than
  undefined, so the `if (!focus)` guard never fired. Guard on 'no now/next/later'
  too → shows the 'Nothing to pour' EmptyState.
- Standup's calm sentinel nag (id 0) still rendered '#0 · steeping 0d · blocks'
  chrome and was a click target (would try to open issue #0). Render the calm
  case as a plain reassurance pill, no chrome, not clickable.

Both verified against the live christian/commitea repo (all issues closed → no
open work). desktop tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Croissant Le Doux
2026-07-10 15:57:30 -04:00
parent 1000d053b3
commit 50db790f86
2 changed files with 59 additions and 36 deletions

View File

@@ -7,9 +7,10 @@ import { EmptyState } from '../shell/states.js'
import { Badge, Button, Card, IconButton, Tag } from '../ui/index.js' import { Badge, Button, Card, IconButton, Tag } from '../ui/index.js'
/** /**
* Morning service — the Now/Next/Later focus cards + the burn-up cone. * Morning service — the Now/Next/Later focus cards + the burn-up cone, from the
* `focus` (scheduler) and `forecast` (Monte Carlo) override the demo fixtures * scheduler (`focus`) and Monte Carlo forecast (`forecast`). Both come from the
* when gitea is configured; both fall back to the handoff demo otherwise. * reconciled backlog; with no open work the scheduler has nothing to pour, so we
* show the empty state.
*/ */
export function FocusScreen({ export function FocusScreen({
onOpenIssue, onOpenIssue,
@@ -66,7 +67,9 @@ export function FocusScreen({
</Card> </Card>
) )
if (!focus) { // The scheduler returns a FocusView even when the backlog has no open work —
// treat "no now/next/later" as empty, not just an absent `focus`.
if (!focus || (!focus.now && !focus.next && !focus.later)) {
return ( return (
<EmptyState <EmptyState
icon="coffee" icon="coffee"

View File

@@ -161,39 +161,59 @@ export function StandupScreen({
</Section> </Section>
<Section overline="Stale blockers" order={3}> <Section overline="Stale blockers" order={3}>
<div {s.nag.id === 0 ? (
role="button" // Calm sentinel — nothing is over-steeping; render just the reassurance,
tabIndex={0} // no issue chrome, no click target (there's no #0 to open).
onClick={() => onOpenIssue(NAG_REF)} <div
onKeyDown={(e) => { style={{
if (e.key === 'Enter') onOpenIssue(NAG_REF) display: 'flex',
}} gap: 10,
style={{ alignItems: 'center',
display: 'flex', background: 'var(--paper-2)',
gap: 10, borderRadius: 'var(--radius-2)',
alignItems: 'flex-start', padding: '12px 14px',
cursor: 'pointer', }}
background: 'var(--warn-tint)', >
borderRadius: 'var(--radius-2)', <span style={{ color: 'var(--ok)', display: 'inline-flex' }}>
padding: '12px 14px', <Icon name="circle-check" size={15} />
}} </span>
> <p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0 }}>{s.nag.text}</p>
<span style={{ color: 'var(--warn)', display: 'inline-flex', marginTop: 2 }}>
<Icon name="clock" size={15} />
</span>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<span style={{ font: '500 12.5px var(--font-mono)', color: 'var(--ink-1)' }}>#{s.nag.id}</span>
<Badge tone="warn" dot>
steeping {s.nag.days}
</Badge>
<span style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)' }}>
blocks {s.nag.blocks.join(', ')}
</span>
</div>
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '5px 0 0' }}>{s.nag.text}</p>
</div> </div>
</div> ) : (
<div
role="button"
tabIndex={0}
onClick={() => onOpenIssue(NAG_REF)}
onKeyDown={(e) => {
if (e.key === 'Enter') onOpenIssue(NAG_REF)
}}
style={{
display: 'flex',
gap: 10,
alignItems: 'flex-start',
cursor: 'pointer',
background: 'var(--warn-tint)',
borderRadius: 'var(--radius-2)',
padding: '12px 14px',
}}
>
<span style={{ color: 'var(--warn)', display: 'inline-flex', marginTop: 2 }}>
<Icon name="clock" size={15} />
</span>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<span style={{ font: '500 12.5px var(--font-mono)', color: 'var(--ink-1)' }}>#{s.nag.id}</span>
<Badge tone="warn" dot>
steeping {s.nag.days}
</Badge>
<span style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)' }}>
blocks {s.nag.blocks.join(', ')}
</span>
</div>
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '5px 0 0' }}>{s.nag.text}</p>
</div>
</div>
)}
</Section> </Section>
<footer <footer