[eric] onboarding: harvest fires when you open ChatGPT/Claude in-app, then sharpens saved suggestions

This commit is contained in:
ciregenz
2026-07-15 19:35:03 -07:00
parent 2e3de37a54
commit b57de38049
4 changed files with 68 additions and 0 deletions
+2
View File
@@ -70,6 +70,8 @@ class AppSettings(BaseModel):
personalized_greeting: Optional[str] = None
personalized_starters: list["PersonalizedStarter"] = Field(default_factory=list)
personalized_automations: list["PersonalizedAutomation"] = Field(default_factory=list)
# Distilled from the user's provider chat history the first time they open ChatGPT/Claude in-app; re-feeds prep to sharpen suggestions.
personalized_usage_summary: Optional[str] = None
# Suppresses preflight suggestion modal entries the user dismissed; keyed by ToolDefinition.name, value ISO timestamp.
dismissed_mcp_suggestions: dict[str, str] = Field(default_factory=dict)
analytics_opt_in: bool = True
@@ -0,0 +1,61 @@
import { useEffect, useRef } from 'react';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { updateSettingsPatch } from '@/shared/state/settingsSlice';
import { summarizeUsage, type ProviderUsage, type UsageProvider } from '@/shared/providerUsage';
import { runPrep } from '@/app/components/OnboardingV3/onboardingV3Api';
const HOST_PROVIDER: Array<{ host: string; provider: UsageProvider }> = [
{ host: 'chatgpt.com', provider: 'codex' },
{ host: 'claude.ai', provider: 'claude' },
];
function providerForUrl(url: string): UsageProvider | null {
try {
const host = new URL(url).hostname;
for (const hp of HOST_PROVIDER) if (host === hp.host || host.endsWith('.' + hp.host)) return hp.provider;
} catch { /* not a url */ }
return null;
}
// The first-run provider login always escapes the app (magic-link / embed-block), so the ONLY moment the browser partition holds a readable session is when the user opens ChatGPT/Claude in an in-app card themselves. Catch exactly that: harvest silently offscreen, then re-feed prep so the saved greeting + starters sharpen to what they actually work on. At most one successful run per provider per session; a not-logged-in read (empty summary) leaves it armed to retry on the next visit.
export function useOpportunisticUsageHarvest(): void {
const dispatch = useAppDispatch();
const providerKey = useAppSelector((s) => {
const found = new Set<string>();
for (const card of Object.values(s.dashboardLayout.browserCards)) {
for (const tab of card.tabs) { const p = providerForUrl(tab.url); if (p) found.add(p); }
}
return Array.from(found).sort().join(',');
});
const doneRef = useRef<Set<string>>(new Set());
const busyRef = useRef(false);
useEffect(() => {
if (!providerKey || busyRef.current) return;
const provider = providerKey.split(',').find((p) => p && !doneRef.current.has(p)) as UsageProvider | undefined;
if (!provider) return;
const harvestUsage = window.openswarm?.harvestUsage;
if (typeof harvestUsage !== 'function') return;
busyRef.current = true;
// Let the page settle so the session cookie is live before the offscreen read.
const timer = window.setTimeout(async () => {
try {
const raw = (await harvestUsage(provider)) as ProviderUsage | null;
const summary = summarizeUsage(raw);
if (!summary) return;
doneRef.current.add(provider);
await dispatch(updateSettingsPatch({ personalized_usage_summary: summary }));
const prep = await runPrep(null, [], [], summary);
if (prep && prep.starters.length > 0) {
await dispatch(updateSettingsPatch({
personalized_greeting: prep.greeting || null,
personalized_starters: prep.starters,
personalized_automations: prep.automations,
}));
}
} catch { /* fail-open */ }
finally { busyRef.current = false; }
}, 3000);
return () => window.clearTimeout(timer);
}, [providerKey, dispatch]);
}
@@ -18,6 +18,7 @@ import { useSubAgentLifecycle } from '../lifecycle/useSubAgentLifecycle';
import { useDashboardLifecycle } from '../lifecycle/useDashboardLifecycle';
import { useWelcomeDraft } from '../lifecycle/useWelcomeDraft';
import { useOnboardingRevealSeed } from '../lifecycle/useOnboardingRevealSeed';
import { useOpportunisticUsageHarvest } from '../lifecycle/useOpportunisticUsageHarvest';
import { useDashboardThumbnail } from './useDashboardThumbnail';
import { useSiblingRestack } from '../lifecycle/useSiblingRestack';
import { useAgentSpawn } from '../lifecycle/useAgentSpawn';
@@ -174,6 +175,9 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
createWelcomeDraft,
});
// Silently harvest the user's provider chat history the first time they open ChatGPT/Claude in-app, then sharpen their saved suggestions.
useOpportunisticUsageHarvest();
// ---- Auto-reveal / collapse / unreveal sub-agent cards ----
useSubAgentLifecycle({
isActive,
@@ -85,6 +85,7 @@ export interface AppSettings {
personalized_greeting?: string | null;
personalized_starters?: PersonalizedStarter[];
personalized_automations?: PersonalizedAutomation[];
personalized_usage_summary?: string | null;
}
export interface PersonalizedStarter {