From 2c78ba29166c21762eb62e54f145f426690cb2d6 Mon Sep 17 00:00:00 2001 From: Anurag-Wednesday Date: Fri, 31 Jul 2026 16:04:55 +0530 Subject: [PATCH] feat(pro): Day and Notifications live on Windows Two features in one PR because Notifications has no content without Day: proactive.ts builds its notifications from getDayPlan / getEventPrep (both from ahead.ts) plus listUpcomingEvents, so porting Notifications alone would have lit up an empty surface on Windows. Neither needs a pro-side change. A sweep for osascript|process.platform|darwin|win32|pgrep|pkill|execFile|spawn returns zero hits across day.ts, day-layout.ts, ahead.ts, ahead-heuristics.ts, calendar.ts, DayView.tsx, TodoCard.tsx, notify.ts, proactive.ts, proactive-window.ts, NotificationList.tsx, notification-target.ts and notification-routing.ts. No Mac/macOS/Cmd/Option copy in any of those surfaces or in either catalog entry, so no neutralization was needed. Day's two data sources both work on Windows now: the calendar comes from connectors (HTTP), and the activity half reads observations, which Replay's port put there. Notification delivery is Electron's Notification guarded by isSupported(), and core already calls setAppUserModelId (index.ts:301) - which Windows REQUIRES for a toast to appear at all. Also fixes the landing-screen landmine, which Day forced. App.tsx opened on `isPro && isMac() ? 'day' : 'models'` - a platform decision living OUTSIDE the capability seam. It was right only by accident: it agreed with the catalog while Day was macOS-only, and would have stranded a ported Day on Windows, with nav and gating lighting Day up from `platforms` while the landing screen still asked isMac(). The decision moves into proCatalog as a pure landingView(platform, isPro), so the landing screen can never disagree with nav and gating again. App.tsx no longer imports isMac. Tests: WIN_PORTED gains day + notifications, deriving eight assertions from the parameterized suite. Plus a new five-case landingView describe, including the stranded-Day regression guard (landingView('win32', true) must be 'day' - the old isMac rule returns 'models') and a DRY case asserting landingView agrees with the day feature's own platforms list on every platform. Verified by reverting to the old rule: two tests fail. Verified: npm test 3169 passed; node + web + pro typechecks clean; eslint 0 new errors (App.tsx's 9 are pre-existing, identical count on main). e2e 73 passed / 1 failed, the one failure being settings-sections "resource mode survives a relaunch" - a pre-existing core bug on main, fixed separately in #73 and not reachable from this change. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015U3TnnNULxfCb4TsjAGjiC --- src/renderer/src/App.tsx | 11 ++--- src/renderer/src/components/pro/proCatalog.ts | 35 ++++++++++++++- .../lib/__tests__/proCatalog.lookup.test.ts | 43 ++++++++++++++++++- 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 3e9ec678..d475c94d 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -17,8 +17,8 @@ import type { SearchHit } from './types' import { loadProFeaturesRenderer } from './bootstrap/loadProFeaturesRenderer' import { renderProView, type ProViewContext } from './bootstrap/proView' import { UpgradeScreen } from './components/pro/UpgradeScreen' -import { getProFeature, proFeatureComingSoon } from './components/pro/proCatalog' -import { currentPlatform, isMac } from './lib/device' +import { getProFeature, proFeatureComingSoon, landingView } from './components/pro/proCatalog' +import { currentPlatform } from './lib/device' import { NotificationProvider } from './hooks/NotificationProvider' import { useNotifications } from './hooks/useNotifications' import { ToastProvider } from './hooks/ToastProvider' @@ -226,9 +226,10 @@ function AppContent() { } }, []) - // Free users land on Models (download a model first, with the sidebar to - // explore); Mac Pro users land on Day. Never land on a locked or unavailable tab. - const [viewMode, setViewMode] = useState(isPro && isMac() ? 'day' : 'models') + // Where to open: derived from the per-feature capability seam (see landingView), so + // the landing screen can never disagree with nav and gating about whether Day is + // available on this platform. + const [viewMode, setViewMode] = useState(landingView(currentPlatform(), isPro)) const [selectedSessionId, setSelectedSessionId] = useState(null) const [selectedMemoryId, setSelectedMemoryId] = useState(null) // Version of a downloaded-and-staged update (null = none). Surfaced as a banner diff --git a/src/renderer/src/components/pro/proCatalog.ts b/src/renderer/src/components/pro/proCatalog.ts index 9a670272..35012cb2 100644 --- a/src/renderer/src/components/pro/proCatalog.ts +++ b/src/renderer/src/components/pro/proCatalog.ts @@ -62,7 +62,13 @@ export const PRO_FEATURES: ProFeature[] = [ 'Per-meeting prep: who’s in it and your open items', 'Priorities surfaced from what you actually did' ], - platforms: ['darwin'] + // Ported to Windows: the whole Day path is portable - day.ts, day-layout.ts, + // ahead.ts and calendar.ts carry no platform-native code. Its two data sources + // both work on Windows now: the calendar comes from connectors (HTTP), and the + // activity half reads observations, which Replay's port put on Windows. Landing + // on Day now routes through `landingView` rather than an `isMac()` check, so nav, + // gating, copy and the landing screen all agree. + platforms: ['darwin', 'win32'] }, { route: 'reflect', @@ -161,7 +167,12 @@ export const PRO_FEATURES: ProFeature[] = [ 'Approval queue for actions', 'Auto-extracted to-dos' ], - platforms: ['darwin'] + // Ported to Windows alongside Day, which produces its content: proactive.ts builds + // notifications from getDayPlan / getEventPrep, so shipping this without Day would + // have delivered an empty surface. Delivery is Electron's Notification (guarded by + // isSupported()), and core already sets the AppUserModelID that Windows requires + // for a toast to appear at all. notify.ts / proactive.ts carry no platform code. + platforms: ['darwin', 'win32'] }, { route: 'voice', @@ -262,3 +273,23 @@ export function proFeatureComingSoon( } return !featureSupportsPlatform(feature, platform) } + +/** + * Which view the app should OPEN on. Free users land on Models (they need a model + * before anything else works); Pro users land on Day — but only where Day is + * actually available. + * + * This lives here, beside the seam, because it is a per-feature platform decision and + * `platforms` is the single source of truth for those. It previously sat in App.tsx as + * `isPro && isMac() ? 'day' : 'models'`, which was right only by accident: it agreed + * with the catalog while Day was macOS-only, and would have stranded a ported Day on + * Windows — nav and gating would light Day up from `platforms` while the landing + * screen still asked `isMac()`. Route the decision through the seam so porting a + * feature never leaves a second place to update. + * + * Never land on a locked or unavailable tab. + */ +export function landingView(platform: DevicePlatform, isPro: boolean): 'day' | 'models' { + const day = getProFeature('day') + return isPro && day && featureSupportsPlatform(day, platform) ? 'day' : 'models' +} diff --git a/src/renderer/src/lib/__tests__/proCatalog.lookup.test.ts b/src/renderer/src/lib/__tests__/proCatalog.lookup.test.ts index bb53790b..e2197667 100644 --- a/src/renderer/src/lib/__tests__/proCatalog.lookup.test.ts +++ b/src/renderer/src/lib/__tests__/proCatalog.lookup.test.ts @@ -10,6 +10,7 @@ import { featureSupportsPlatform, proComingSoonHere, proFeatureComingSoon, + landingView, PRO_FEATURES, PRO_PAY_URL, type ProFeature @@ -38,7 +39,7 @@ const winPorted = (route: string): ProFeature => ({ // asserted against the catalog so a flipped `platforms` and this list can't drift. // Module-scoped because both the featureSupportsPlatform and proFeatureComingSoon // describes read it — the gate and the capability check must agree on one list. -const WIN_PORTED = new Set(['vault', 'clipboard', 'replay']) +const WIN_PORTED = new Set(['vault', 'clipboard', 'replay', 'day', 'notifications']) describe('getProFeature', () => { it('returns the matching feature for a known route', () => { @@ -169,6 +170,46 @@ describe('proFeatureComingSoon', () => { }) }) +describe('landingView reads the seam, not isMac (the stranded-Day guard)', () => { + it('sends free users to Models on every platform (they need a model first)', () => { + for (const p of ['darwin', 'win32', 'linux', 'unknown'] as const) { + expect(landingView(p, false), `free on ${p}`).toBe('models') + } + }) + + it('lands a Pro user on Day on macOS', () => { + expect(landingView('darwin', true)).toBe('day') + }) + + // THE regression guard for the rule this replaced. The old landing default was + // `isPro && isMac() ? 'day' : 'models'`, which returns 'models' on win32 no matter + // what the catalog says. With Day ported, nav and gating light it up from + // `platforms` — so an isMac-based landing screen would strand a Pro Windows user on + // Models. This fails the moment anything reintroduces that check. + it('lands a Pro user on Day on Windows now that Day is ported', () => { + expect(landingView('win32', true)).toBe('day') + }) + + // The other half: not "any non-Mac gets Day" either. Day is not ported to linux, so + // a Pro linux user must NOT be dropped onto an unavailable tab. + it('does not land a Pro user on Day where Day is unsupported', () => { + expect(landingView('linux', true)).toBe('models') + expect(landingView('unknown', true)).toBe('models') + }) + + // DRY: assert against the catalog rather than re-hardcoding the platform list, so + // this test and `platforms` can never drift. Porting Day to a new platform updates + // both sides from the one edit. + it('agrees with the day feature’s own platforms list on every platform', () => { + const day = getProFeature('day') + expect(day).toBeDefined() + for (const p of ['darwin', 'win32', 'linux', 'unknown'] as const) { + const expected = featureSupportsPlatform(day!, p) ? 'day' : 'models' + expect(landingView(p, true), `pro landing on ${p}`).toBe(expected) + } + }) +}) + describe('PRO_FEATURES data integrity', () => { it('has a non-empty catalog', () => { expect(PRO_FEATURES.length).toBeGreaterThan(0)