You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The reference product's value is mostly presentation: a scannable layout that opens
with a short "act on this" list, then themed "be aware" clusters, each line citing
its sources. SkyTwin already renders a briefing, but as a flat importance-ordered
list of decisions with no act/FYI split, no topic grouping, no source-type cues, and
no citations. The backend specs (01-07) produce the structured data — to-dos, topic
clusters, deadlines, per-signal source + citation — but none of it reaches the user
without a UI that renders it. This spec is the web + mobile surface for that data, and
it must be source-aware: an item from calendar, a file, or a voice note should
look and cite differently from an email, because SkyTwin is multi-source.
Current State
Verified 2026-06-06.
Routes (apps/web/public/js/app.js:33-54): #/ → renderDashboard, #/briefing → renderTwinBriefing, #/approvals, #/decisions. Pages render via
template literals into a single reused #page-content (app.js:653-680).
Dashboard briefing card (apps/web/public/js/pages/dashboard-view.js:142-184):
shows last scan's headline + "N handled on own, M waiting on you", lists up to 5
items as a colored bar (green auto / yellow needs-approval) + description +
reasoning + domain/confidence/urgency; overflow → "+N more on Decisions page". No act/FYI split, no topic grouping, no source cue, no citation.
Full briefing page (apps/web/public/js/pages/twin-briefing.js:180-264):
Markdown prose, Daily/Weekly tabs, per-Lifebook collapsible sections, history
sidebar. Prose is escaped text — no extracted to-dos, no source links.
Frontend conventions in force (CLAUDE.md:163-176, confirmed in code): no inline onclick; data-action attributes + delegated listener; singleton delegator wired
once on document, gated by window.location.hash, behind a _listenerWired
guard. Real examples: approvals.js:21-22,139-177 (_approvalsListenerWired,
hash-checks #/approvals); decisions.js:11,216-237; twin-briefing.js:22,34-38
(_briefingListenerWired, isOnBriefingRoute()); dashboard-view.js:581-650
(initDashboardGlobals()).
CSS (apps/web/public/css/styles.css:469-535): .card, .card-header, .card-title, .badge/.badge-success|warning|danger|info, table styles, and a
left-accent-border convention (border-left: 3px solid var(--…)). Enough primitives
to build the redesign without a new design system.
API (apps/api/src/routes/twin-briefings.ts:34-98, briefings.ts): /api/twin-briefings/latest returns { briefing: { prose_markdown, generated_at, read_at }, sections: [...] }; legacy /api/briefing/:userId returns briefing.items[] = { wouldAutoExecute, actionDescription, reasoning, domain, confidence, urgency }. Neither returns a to-do/FYI flag, topic cluster, source
type, or citation list yet — those come from specs 01/04/07.
Mobile (apps/mobile/src/screens/BriefingScreen.tsx:1-150, DashboardScreen.tsx): briefing headline card + unread badge + pending-approvals
link; recent decisions list (limit 5). Same gaps as web.
Absent everywhere: to-do vs FYI grouping, topic sub-headings, source-type
icons/labels, inline citations to originating signals.
Proposed Change
Redesign the briefing/digest surface (web #/briefing and dashboard card; mobile BriefingScreen) to render the structured payload from specs 01/04/07:
Two buckets, ordered: a To-dos section first (act-required, ≤7, urgency-
ordered, each with deadline chip when present), then Topics (awareness,
grouped by cluster from spec 04, each cluster collapsible).
Source-aware rows: every item shows a source-type indicator — icon + label for email, calendar, file, voice, app — so the user sees at a glance that a
to-do came from a voice note vs. an email. Drives home that SkyTwin is multi-source.
Citations: each item renders its signalRefs (from spec 04/05) as small
"source" chips linking to the originating signal/decision detail. Multiple sources
→ multiple chips (mirrors the reference product's "From: A · B", but every chip is
a real in-app link, never a raw external URL — security, per spec 06 and safety
invariant Live notification layer: SSE, approval expiry cron, push alerts #8).
Provenance/urgency affordances: keep the existing auto/needs-approval color
coding; add a small "untrusted source" marker on untrusted_external items so the
user knows an item originated from inbound content.
Implementation Details
API — extend /api/twin-briefings/latest (additive) to return the structured
payload spec 01 persists:
structured is null for old briefings → UI falls back to prose.
Web render — new renderDigest(container, data) (extend twin-briefing.js):
To-dos list, then topic clusters (reuse .card, .badge, left-accent-border).
Source indicator: a source-chip component keyed on sourceType (small CSS
additions to styles.css, reuse badge tokens).
Citations: data-action="open-signal" + data-signal-ref chips; the delegator
opens the decision/signal detail.
Event wiring (MUST follow the codebase pattern) — one module-level delegator
on document, guarded by _digestListenerWired, first line if (window.location.hash.split('?')[0] !== '#/briefing') return;. Read getCurrentUserId() inside the handler, not closed-over (CLAUDE.md:170-176). No
inline handlers. This is the exact trap the CLAUDE.md frontend section warns about;
the spec calls it out so the implementer doesn't stack listeners on re-render.
Empty/degraded states: no to-dos → "Nothing needs you right now." No structured payload (old row / LLM down) → render existing prose. Unknown sourceType → neutral chip, never a crash.
Mobile — mirror the two-bucket layout in BriefingScreen.tsx: a To-dos SectionList section + per-cluster Topic sections, source-type icon per row,
citation chips navigating to decision detail. Pull-to-refresh stays.
Accessibility — sections are real headings; source indicators have text labels
(not icon-only); citation chips are buttons with accessible names ("source: email").
Acceptance Criteria
Web #/briefing renders a To-dos section above a Topics section when structured
is present; To-dos are urgency-ordered and capped at 7 with overflow disclosure.
Each row shows a source-type indicator matching its sourceType
(email/calendar/file/voice/app); unknown types render a neutral chip without error.
Each row renders one citation chip per signalRef; clicking opens the in-app
signal/decision detail. No chip ever links to a raw external URL.
untrusted_external items show the untrusted marker.
The digest delegator is wired once, gated on #/briefing; navigating away and
back, and re-rendering after refresh, do not stack listeners or fire on other
routes (verified: only one handler invocation per click after N re-renders).
When structured is null, the page falls back to the existing prose render (no
regression for historical briefings).
Mobile BriefingScreen shows the same two-bucket, source-aware, cited layout.
No inline event-handler attributes are introduced anywhere in the new markup.
Tests written and passing. No degradation of existing functionality.
Testing Plan
Layer
What
Count
Unit
renderDigest markup: section order, cap+overflow, source chip per type
+5
Unit
Citation chips are in-app links; external URL never rendered as chip
+2
Unit
Fallback to prose when structured is null
+2
Unit (dom)
Delegator wired once; hash-gating; no stacking across re-renders
+3
Integration
API /latest returns structured; UI renders it end-to-end
API field is additive and nullable; UI is gated on structured presence. Roll back
by having the API omit structured (or a DIGEST_UI=off flag) → both surfaces fall
back to today's prose/list render. No data migration. New CSS classes are unused when
the old path renders.
Effort Estimate
API structured passthrough: ~2h
Web renderDigest + source chips + citations: ~6h
Web delegator wiring (pattern-correct): ~2h
CSS (source chips, section layout): ~2h
Mobile BriefingScreen: ~5h
Tests: ~5h
Total: ~3 days (web ~1.5d, mobile ~1d, tests/API ~0.5d).
Files Reference
File
Change
apps/web/public/js/pages/twin-briefing.js:180-264
New renderDigest + delegator (_digestListenerWired, hash-gated #/briefing)
Digest UI redesign: two-bucket, source-aware, cited
Context
The reference product's value is mostly presentation: a scannable layout that opens
with a short "act on this" list, then themed "be aware" clusters, each line citing
its sources. SkyTwin already renders a briefing, but as a flat importance-ordered
list of decisions with no act/FYI split, no topic grouping, no source-type cues, and
no citations. The backend specs (01-07) produce the structured data — to-dos, topic
clusters, deadlines, per-signal source + citation — but none of it reaches the user
without a UI that renders it. This spec is the web + mobile surface for that data, and
it must be source-aware: an item from calendar, a file, or a voice note should
look and cite differently from an email, because SkyTwin is multi-source.
Current State
Verified 2026-06-06.
apps/web/public/js/app.js:33-54):#/→renderDashboard,#/briefing→renderTwinBriefing,#/approvals,#/decisions. Pages render viatemplate literals into a single reused
#page-content(app.js:653-680).apps/web/public/js/pages/dashboard-view.js:142-184):shows last scan's headline + "N handled on own, M waiting on you", lists up to 5
items as a colored bar (green auto / yellow needs-approval) + description +
reasoning + domain/confidence/urgency; overflow → "+N more on Decisions page".
No act/FYI split, no topic grouping, no source cue, no citation.
apps/web/public/js/pages/twin-briefing.js:180-264):Markdown prose, Daily/Weekly tabs, per-Lifebook collapsible sections, history
sidebar. Prose is escaped text — no extracted to-dos, no source links.
onclick;data-actionattributes + delegated listener; singleton delegator wiredonce on
document, gated bywindow.location.hash, behind a_listenerWiredguard. Real examples:
approvals.js:21-22,139-177(_approvalsListenerWired,hash-checks
#/approvals);decisions.js:11,216-237;twin-briefing.js:22,34-38(
_briefingListenerWired,isOnBriefingRoute());dashboard-view.js:581-650(
initDashboardGlobals()).apps/web/public/css/styles.css:469-535):.card,.card-header,.card-title,.badge/.badge-success|warning|danger|info, table styles, and aleft-accent-border convention (
border-left: 3px solid var(--…)). Enough primitivesto build the redesign without a new design system.
apps/api/src/routes/twin-briefings.ts:34-98,briefings.ts):/api/twin-briefings/latestreturns{ briefing: { prose_markdown, generated_at, read_at }, sections: [...] }; legacy/api/briefing/:userIdreturnsbriefing.items[] = { wouldAutoExecute, actionDescription, reasoning, domain, confidence, urgency }. Neither returns a to-do/FYI flag, topic cluster, sourcetype, or citation list yet — those come from specs 01/04/07.
apps/mobile/src/screens/BriefingScreen.tsx:1-150,DashboardScreen.tsx): briefing headline card + unread badge + pending-approvalslink; recent decisions list (limit 5). Same gaps as web.
icons/labels, inline citations to originating signals.
Proposed Change
Redesign the briefing/digest surface (web
#/briefingand dashboard card; mobileBriefingScreen) to render the structured payload from specs 01/04/07:ordered, each with deadline chip when present), then Topics (awareness,
grouped by cluster from spec 04, each cluster collapsible).
email,calendar,file,voice,app— so the user sees at a glance that ato-do came from a voice note vs. an email. Drives home that SkyTwin is multi-source.
signalRefs(from spec 04/05) as small"source" chips linking to the originating signal/decision detail. Multiple sources
→ multiple chips (mirrors the reference product's "From: A · B", but every chip is
a real in-app link, never a raw external URL — security, per spec 06 and safety
invariant Live notification layer: SSE, approval expiry cron, push alerts #8).
coding; add a small "untrusted source" marker on
untrusted_externalitems so theuser knows an item originated from inbound content.
Implementation Details
/api/twin-briefings/latest(additive) to return the structuredpayload spec 01 persists:
{ "briefing": { "prose_markdown": "...", "generated_at": "...", "read_at": null }, "structured": { "todos": [{ "text","sourceType","signalRefs":[],"deadline":null,"provenance" }], "topics": [{ "domain","title","items":[{ "text","sourceType","signalRefs":[] }] }] }, "sections": [ ... ] // unchanged, for back-compat }structuredis null for old briefings → UI falls back to prose.renderDigest(container, data)(extendtwin-briefing.js):.card,.badge, left-accent-border).source-chipcomponent keyed onsourceType(small CSSadditions to
styles.css, reuse badge tokens).data-action="open-signal"+data-signal-refchips; the delegatoropens the decision/signal detail.
on
document, guarded by_digestListenerWired, first lineif (window.location.hash.split('?')[0] !== '#/briefing') return;. ReadgetCurrentUserId()inside the handler, not closed-over (CLAUDE.md:170-176). Noinline handlers. This is the exact trap the CLAUDE.md frontend section warns about;
the spec calls it out so the implementer doesn't stack listeners on re-render.
structuredpayload (old row / LLM down) → render existing prose. UnknownsourceType→ neutral chip, never a crash.BriefingScreen.tsx: a To-dosSectionListsection + per-cluster Topic sections, source-type icon per row,citation chips navigating to decision detail. Pull-to-refresh stays.
(not icon-only); citation chips are buttons with accessible names ("source: email").
Acceptance Criteria
#/briefingrenders a To-dos section above a Topics section whenstructuredis present; To-dos are urgency-ordered and capped at 7 with overflow disclosure.
sourceType(email/calendar/file/voice/app); unknown types render a neutral chip without error.
signalRef; clicking opens the in-appsignal/decision detail. No chip ever links to a raw external URL.
untrusted_externalitems show the untrusted marker.#/briefing; navigating away andback, and re-rendering after refresh, do not stack listeners or fire on other
routes (verified: only one handler invocation per click after N re-renders).
structuredis null, the page falls back to the existing prose render (noregression for historical briefings).
BriefingScreenshows the same two-bucket, source-aware, cited layout.Testing Plan
renderDigestmarkup: section order, cap+overflow, source chip per typestructuredis null/latestreturnsstructured; UI renders it end-to-endRollback Plan
API field is additive and nullable; UI is gated on
structuredpresence. Roll backby having the API omit
structured(or aDIGEST_UI=offflag) → both surfaces fallback to today's prose/list render. No data migration. New CSS classes are unused when
the old path renders.
Effort Estimate
structuredpassthrough: ~2hrenderDigest+ source chips + citations: ~6hTotal: ~3 days (web ~1.5d, mobile ~1d, tests/API ~0.5d).
Files Reference
apps/web/public/js/pages/twin-briefing.js:180-264renderDigest+ delegator (_digestListenerWired, hash-gated#/briefing)apps/web/public/js/pages/dashboard-view.js:142-184apps/web/public/css/styles.css:469-535source-chip, section, citation-chip styles (reuse tokens)apps/api/src/routes/twin-briefings.ts:34-98structuredto/latestapps/mobile/src/screens/BriefingScreen.tsxOut of Scope
todos/topics/sourceType/signalRefs) — specs01, 04, 05, 07 own that. This spec only renders it.
Related
(source type). Depends on at least 01 + 07 landing first.
hash-gating,
data-action,getCurrentUserId()inside handler).