Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
140 lines
4.8 KiB
TypeScript
140 lines
4.8 KiB
TypeScript
import type { IntegrationSetupState, Me } from "@rakazo/contracts";
|
|
import { collectLocalProfileSignals, type UserProfileSignals } from "./user-profile-harvest";
|
|
|
|
/**
|
|
* Visible onboarding steps only — future discovery lives in docs/TODO, not the UI.
|
|
* Profile harvest seeds user MEMORY.md (help/vibe + Oo relationship contract — compounding via use,
|
|
* respectful pushback, anti-creep tone; `user-relationship-contract.ts`, `docs/OO-RELATIONSHIP.md`).
|
|
*/
|
|
export type DiscoverySourceId =
|
|
| "profile-basics"
|
|
| "model-ready"
|
|
| "connected-tools"
|
|
| "specialist-crew";
|
|
|
|
export type DiscoveryStatus = "pending" | "scanning" | "found" | "skipped";
|
|
|
|
export interface DiscoveryItem {
|
|
id: DiscoverySourceId;
|
|
status: DiscoveryStatus;
|
|
/** Honest one-line result — only set when we truly know something. */
|
|
detail?: string;
|
|
}
|
|
|
|
export type OnboardingDiscoveryPhase = "loading" | "model" | "integrations" | "assembly";
|
|
|
|
export interface DiscoveryContext {
|
|
phase: OnboardingDiscoveryPhase;
|
|
me?: Pick<
|
|
Me,
|
|
"name" | "email" | "needsModel" | "defaultProvider" | "defaultModel" | "computerHost"
|
|
> | null;
|
|
integrationSetup?: IntegrationSetupState | null;
|
|
connectedIntegrationCount?: number;
|
|
profileSignals?: UserProfileSignals;
|
|
profilePersisted?: boolean;
|
|
crewReady?: boolean;
|
|
}
|
|
|
|
export function summarizeProfileSignals(signals: UserProfileSignals): string {
|
|
const parts: string[] = [];
|
|
if (signals.name) parts.push(signals.name);
|
|
parts.push(signals.timezone);
|
|
if (signals.uiLocale && signals.uiLocale !== "en") parts.push(signals.uiLocale);
|
|
return parts.join(" · ");
|
|
}
|
|
|
|
export function countConfiguredIntegrations(
|
|
setup: IntegrationSetupState | null | undefined,
|
|
): number {
|
|
return setup?.providers.filter((provider) => provider.configured).length ?? 0;
|
|
}
|
|
|
|
export function computeDiscoveryItems(context: DiscoveryContext): DiscoveryItem[] {
|
|
const signals =
|
|
context.profileSignals ?? (context.me ? collectLocalProfileSignals(context.me) : null);
|
|
const configuredCount =
|
|
context.connectedIntegrationCount ?? countConfiguredIntegrations(context.integrationSetup);
|
|
const needsModel = context.me?.needsModel;
|
|
|
|
const profile: DiscoveryItem = (() => {
|
|
if (context.profilePersisted && signals) {
|
|
return { id: "profile-basics", status: "found", detail: summarizeProfileSignals(signals) };
|
|
}
|
|
if (context.phase === "assembly" && signals) {
|
|
return { id: "profile-basics", status: "scanning", detail: summarizeProfileSignals(signals) };
|
|
}
|
|
if (signals && context.me) {
|
|
return { id: "profile-basics", status: "found", detail: summarizeProfileSignals(signals) };
|
|
}
|
|
if (context.phase === "loading") {
|
|
return { id: "profile-basics", status: "scanning" };
|
|
}
|
|
return { id: "profile-basics", status: "pending" };
|
|
})();
|
|
|
|
const model: DiscoveryItem = (() => {
|
|
if (needsModel === false) {
|
|
const modelLabel =
|
|
context.me?.defaultProvider && context.me.defaultModel
|
|
? `${context.me.defaultProvider} / ${context.me.defaultModel}`
|
|
: undefined;
|
|
return { id: "model-ready", status: "found", detail: modelLabel };
|
|
}
|
|
if (context.phase === "model") {
|
|
return { id: "model-ready", status: "scanning" };
|
|
}
|
|
if (needsModel === true) {
|
|
return { id: "model-ready", status: "pending" };
|
|
}
|
|
return { id: "model-ready", status: "scanning" };
|
|
})();
|
|
|
|
const tools: DiscoveryItem = (() => {
|
|
if (configuredCount > 0) {
|
|
return {
|
|
id: "connected-tools",
|
|
status: "found",
|
|
detail: String(configuredCount),
|
|
};
|
|
}
|
|
if (context.phase === "integrations") {
|
|
return { id: "connected-tools", status: "scanning" };
|
|
}
|
|
if (context.integrationSetup && !context.integrationSetup.needsSetup) {
|
|
return { id: "connected-tools", status: "skipped" };
|
|
}
|
|
return { id: "connected-tools", status: "pending" };
|
|
})();
|
|
|
|
const crew: DiscoveryItem = (() => {
|
|
if (context.crewReady) {
|
|
return { id: "specialist-crew", status: "found", detail: "Chief of Staff" };
|
|
}
|
|
if (context.phase === "assembly") {
|
|
return { id: "specialist-crew", status: "scanning" };
|
|
}
|
|
return { id: "specialist-crew", status: "pending" };
|
|
})();
|
|
|
|
return [profile, model, tools, crew];
|
|
}
|
|
|
|
/** Onboarding UI: only steps in flight — no stub laundry list (weniger ist mehr). */
|
|
export function visibleDiscoveryItems(items: DiscoveryItem[]): DiscoveryItem[] {
|
|
return items;
|
|
}
|
|
|
|
export function discoveryProgress(items: DiscoveryItem[]): {
|
|
done: number;
|
|
total: number;
|
|
active: DiscoveryItem | null;
|
|
} {
|
|
const done = items.filter((item) => item.status === "found" || item.status === "skipped").length;
|
|
const active =
|
|
items.find((item) => item.status === "scanning") ??
|
|
items.find((item) => item.status === "pending") ??
|
|
null;
|
|
return { done, total: items.length, active };
|
|
}
|