Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<ViewMode>(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<ViewMode>(landingView(currentPlatform(), isPro))
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null)
const [selectedMemoryId, setSelectedMemoryId] = useState<number | null>(null)
// Version of a downloaded-and-staged update (null = none). Surfaced as a banner
Expand Down
35 changes: 33 additions & 2 deletions src/renderer/src/components/pro/proCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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'
}
43 changes: 42 additions & 1 deletion src/renderer/src/lib/__tests__/proCatalog.lookup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
featureSupportsPlatform,
proComingSoonHere,
proFeatureComingSoon,
landingView,
PRO_FEATURES,
PRO_PAY_URL,
type ProFeature
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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)
Expand Down
Loading