mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
* Update Conversations component to enhance user messaging for budget limits. Changed the warning text for exhausted weekly inference budget to improve clarity and user experience. * feat(schemas): add new configuration option for vision model usage - Introduced a new optional boolean field `use_vision_model` in the schemas for enabling vision LLM for screenshot analysis. - Updated the screen intelligence schemas to include a required `consent` field for starting sessions, replacing the previous `sample_interval_ms` field. - Enhanced the `ttl_secs` field description for clarity and modified the `capture_policy` to `screen_monitoring` for better understanding of its purpose. * feat(CoreStateProvider): enhance state management with optimistic updates and error handling - Implemented optimistic local commits for `setAnalyticsEnabled` and `setOnboardingCompletedFlag` to provide instant UI feedback while ensuring state consistency through authoritative snapshot refreshes. - Added error handling for the `refresh` function calls in `setAnalyticsEnabled`, `setOnboardingCompletedFlag`, and `clearSession` to log failures, improving robustness in state management during user interactions. - Updated dependencies in the `useCallback` hooks to include `refresh`, ensuring proper state updates and synchronization with the core. * feat(paths): centralize runtime path resolution for user-scoped skills data - Introduced a new module `paths.rs` to handle the resolution of runtime paths for skills, ensuring that `skills_data` and `workspace` directories are scoped per user. - Updated `bootstrap_skill_runtime` and `bootstrap_skills_runtime` functions to utilize the new path resolution logic, improving consistency and clarity in directory management. - Enhanced error logging for directory creation failures to include the specific path that failed, aiding in debugging. - Added a new optional field `overlay_ttl_ms` in the autocomplete schemas to support overlay time-to-live configuration. * feat(ScreenIntelligencePanel): optimize config synchronization to prevent user edit clobbering - Introduced a reference to track the last synced configuration signature, ensuring that user edits are preserved during periodic updates from the CoreStateProvider. - Updated the effect to compare serialized configuration values, allowing for re-sync only when actual changes occur, enhancing user experience and preventing unintended data loss. * feat(SkillManager): implement initial sync after OAuth completion - Added functionality to trigger an initial data sync immediately after OAuth completion, ensuring users see fresh data without waiting for the next scheduled sync. - Updated comments to clarify the change in sync behavior due to recent modifications in the Rust core, which no longer auto-triggers sync on OAuth completion. * fix(UsageLimitModal, Conversations): enhance user messaging for budget limits - Updated warning messages in both UsageLimitModal and Conversations components to provide clearer information regarding weekly limits and reset times. - Improved clarity in user notifications to enhance overall experience when budget limits are reached. * refactor(SkillSetupModal): improve session mode handling for skill configuration - Updated the SkillSetupModal component to lock the mode at mount time, ensuring users remain in the setup wizard during their session even if the skill is marked as complete. - Simplified mode management by replacing the forceSetup state with a sessionMode state, allowing explicit mode switching while maintaining a consistent user experience. * feat(SkillManager): enhance setup flow for OAuth-based skills - Updated the `startSetup` method to handle OAuth-based skills more effectively by implementing a fallback to core RPC for skills without a frontend runtime. - Improved error handling to treat missing `onSetupStart` implementations as successful completion for pure OAuth skills, allowing the setup wizard to display the "Connected!" screen. - Added detailed logging for both local runtime and core RPC fallback scenarios to improve traceability during the setup process. * feat(Home): enhance local AI status handling and asset management - Introduced a new state for local AI assets, allowing for better tracking of model file readiness. - Updated the loading logic to fetch both local AI status and assets concurrently, improving performance and error handling. - Implemented a mechanism to hide the Local Model Runtime card once all models are fully downloaded, enhancing user experience. - Added comprehensive comments to clarify the logic behind model readiness checks based on asset states. * refactor(Credits): update credit balance structure and terminology - Renamed credit categories in the RewardsCouponSection and PayAsYouGoCard components for clarity, changing "General credits" to "Promo credits" and "Top-up credits" to "Team top-up." - Updated the credit balance API to reflect the new structure, replacing `balanceUsd` and `topUpBalanceUsd` with `promotionBalanceUsd` and `teamTopupUsd`. - Adjusted normalization logic in the credits API to accommodate the new credit balance fields. - Modified tests to ensure correct handling of the updated credit balance structure. * feat(Settings): reorganize billing settings and update descriptions - Added a new top-level billing section to the settings, promoting it out of the Account & Security category for better visibility. - Updated the description for the Account & Security section to remove billing references, focusing on recovery phrase, team management, and linked account access. - Adjusted the settings navigation to accommodate the new billing section, ensuring proper routing and user experience. * refactor(Config): change logging level from info to debug for environment overrides - Updated logging statements in the Config implementation to use debug level instead of info, reducing verbosity during runtime while maintaining necessary traceability for configuration loading. * feat(Overlay): implement overlay attention event handling and refactor overlay app structure - Introduced a new overlay module to manage attention events, allowing the core to publish messages to the overlay window. - Enhanced the OverlayApp component to handle dictation and attention events, improving user interaction with the overlay. - Refactored the overlay state management to support different modes (idle, stt, attention) and added auto-dismiss functionality for attention messages. - Removed the Browser Access Toggle from the Skills page, streamlining the UI and focusing on core functionalities. - Updated tests to reflect changes in the Skills component and removed unnecessary mocks related to browser access. * fix(OverlayBubbleChip): reset typewriter animation on new bubble identity - Updated the OverlayBubbleChip component to reset the typewriter animation correctly when a new bubble is displayed by using the `key` prop. - Refactored the cleanup logic in the useEffect hook to ensure proper interval management and state reset, enhancing the user experience with bubble transitions. * refactor(rest): streamline key_bytes_from_string function and improve readability - Simplified the condition for checking the ASCII key length and character restrictions in the key_bytes_from_string function. - Consolidated the import statements for base64 engines into a single line for better clarity. - Adjusted test data formatting for improved readability in the key_bytes_from_string_tests module. * enhance(logging): improve color detection logic for terminal output - Updated the color detection logic in the logging module to prioritize environment variables (`NO_COLOR`, `FORCE_COLOR`, `CLICOLOR_FORCE`) for better control over color output. - Added detailed comments explaining the color resolution order, enhancing code clarity and maintainability. * test(Home): add mock for openhumanLocalAiAssetsStatus in tests - Enhanced the Home and HomeBootstrapButtons test files by adding a mock implementation for openhumanLocalAiAssetsStatus, which resolves to an object with null result and empty logs. This improves the test setup for local AI asset status handling. * refactor(SkillSetupModal): improve session mode handling and loading state - Updated the SkillSetupModal component to ensure session mode is determined after the first snapshot resolution, preventing premature defaults to the setup wizard. - Introduced a loading state to display a message while waiting for the skill setup status, enhancing user experience during the modal's initial render. - Refactored the SkillManager to throw errors for real failures during setup, ensuring proper error handling and user feedback. * refactor(Config): simplify logging for invalid proxy scope values - Consolidated the logging statement for invalid OPENHUMAN_PROXY_SCOPE values into a single line, improving code readability while maintaining the warning functionality.
400 lines
12 KiB
TypeScript
400 lines
12 KiB
TypeScript
import debugFactory from 'debug';
|
|
import {
|
|
createContext,
|
|
type ReactNode,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
|
|
import {
|
|
type CoreAppSnapshot,
|
|
type CoreOnboardingTasks,
|
|
type CoreState,
|
|
getCoreStateSnapshot,
|
|
setCoreStateSnapshot,
|
|
} from '../lib/coreState/store';
|
|
import { syncAnalyticsConsent } from '../services/analytics';
|
|
import {
|
|
fetchCoreAppSnapshot,
|
|
getTeamInvites,
|
|
getTeamMembers,
|
|
listTeams,
|
|
updateCoreLocalState,
|
|
} from '../services/coreStateApi';
|
|
import {
|
|
openhumanUpdateAnalyticsSettings,
|
|
setOnboardingCompleted,
|
|
storeSession,
|
|
syncMemoryClientToken,
|
|
logout as tauriLogout,
|
|
} from '../utils/tauriCommands';
|
|
|
|
const log = debugFactory('core-state');
|
|
|
|
const POLL_MS = 2000;
|
|
const MAX_BOOTSTRAP_RETRIES = 5;
|
|
|
|
/** Extract only non-sensitive fields from an RPC/fetch error. */
|
|
function sanitizeError(error: unknown): { message?: string; code?: string; status?: number } {
|
|
if (error instanceof Error) {
|
|
return { message: error.message };
|
|
}
|
|
if (error && typeof error === 'object') {
|
|
const e = error as Record<string, unknown>;
|
|
return {
|
|
message: typeof e.message === 'string' ? e.message : undefined,
|
|
code: typeof e.code === 'string' ? e.code : undefined,
|
|
status: typeof e.status === 'number' ? e.status : undefined,
|
|
};
|
|
}
|
|
return { message: String(error) };
|
|
}
|
|
|
|
interface CoreStateContextValue extends CoreState {
|
|
refresh: () => Promise<void>;
|
|
refreshTeams: () => Promise<void>;
|
|
refreshTeamMembers: (teamId: string) => Promise<void>;
|
|
refreshTeamInvites: (teamId: string) => Promise<void>;
|
|
setAnalyticsEnabled: (enabled: boolean) => Promise<void>;
|
|
setOnboardingCompletedFlag: (value: boolean) => Promise<void>;
|
|
setEncryptionKey: (value: string | null) => Promise<void>;
|
|
setPrimaryWalletAddress: (value: string | null) => Promise<void>;
|
|
setOnboardingTasks: (value: CoreOnboardingTasks | null) => Promise<void>;
|
|
storeSessionToken: (token: string, user?: object) => Promise<void>;
|
|
clearSession: () => Promise<void>;
|
|
}
|
|
|
|
const CoreStateContext = createContext<CoreStateContextValue | null>(null);
|
|
|
|
function snapshotIdentity(snapshot: CoreAppSnapshot): string | null {
|
|
return snapshot.auth.userId ?? snapshot.currentUser?._id ?? null;
|
|
}
|
|
|
|
function normalizeSnapshot(
|
|
result: Awaited<ReturnType<typeof fetchCoreAppSnapshot>>
|
|
): CoreAppSnapshot {
|
|
return {
|
|
auth: result.auth,
|
|
sessionToken: result.sessionToken,
|
|
currentUser: result.currentUser,
|
|
onboardingCompleted: result.onboardingCompleted,
|
|
analyticsEnabled: result.analyticsEnabled,
|
|
localState: {
|
|
encryptionKey: result.localState.encryptionKey ?? null,
|
|
primaryWalletAddress: result.localState.primaryWalletAddress ?? null,
|
|
onboardingTasks: result.localState.onboardingTasks ?? null,
|
|
},
|
|
runtime: {
|
|
screenIntelligence: result.runtime?.screenIntelligence ?? null,
|
|
localAi: result.runtime?.localAi ?? null,
|
|
autocomplete: result.runtime?.autocomplete ?? null,
|
|
service: result.runtime?.service ?? null,
|
|
},
|
|
};
|
|
}
|
|
|
|
export default function CoreStateProvider({ children }: { children: ReactNode }) {
|
|
const [state, setState] = useState<CoreState>(() => getCoreStateSnapshot());
|
|
const snapshotRequestIdRef = useRef(0);
|
|
const teamsRequestIdRef = useRef(0);
|
|
const memoryTokenRef = useRef<string | null>(state.snapshot.sessionToken);
|
|
const bootstrapFailCountRef = useRef(0);
|
|
const refreshInFlightRef = useRef<Promise<void> | null>(null);
|
|
|
|
const commitState = useCallback((updater: (previous: CoreState) => CoreState) => {
|
|
setState(previous => {
|
|
const next = updater(previous);
|
|
setCoreStateSnapshot(next);
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const refreshCore = useCallback(async () => {
|
|
const requestId = ++snapshotRequestIdRef.current;
|
|
const snapshot = normalizeSnapshot(await fetchCoreAppSnapshot());
|
|
commitState(previous => {
|
|
if (requestId !== snapshotRequestIdRef.current) {
|
|
return previous;
|
|
}
|
|
|
|
const previousIdentity = snapshotIdentity(previous.snapshot);
|
|
const nextIdentity = snapshotIdentity(snapshot);
|
|
const shouldClearScopedCaches =
|
|
previousIdentity !== nextIdentity ||
|
|
(previous.snapshot.auth.isAuthenticated && !snapshot.auth.isAuthenticated);
|
|
|
|
return {
|
|
...previous,
|
|
isBootstrapping: false,
|
|
isReady: true,
|
|
snapshot,
|
|
teams: shouldClearScopedCaches ? [] : previous.teams,
|
|
teamMembersById: shouldClearScopedCaches ? {} : previous.teamMembersById,
|
|
teamInvitesById: shouldClearScopedCaches ? {} : previous.teamInvitesById,
|
|
};
|
|
});
|
|
syncAnalyticsConsent(snapshot.analyticsEnabled);
|
|
|
|
if (!snapshot.sessionToken) {
|
|
memoryTokenRef.current = null;
|
|
return;
|
|
}
|
|
|
|
if (memoryTokenRef.current !== snapshot.sessionToken) {
|
|
try {
|
|
await syncMemoryClientToken(snapshot.sessionToken);
|
|
memoryTokenRef.current = snapshot.sessionToken;
|
|
} catch (error) {
|
|
console.warn('[core-state] memory client sync failed during refresh:', error);
|
|
}
|
|
}
|
|
}, [commitState]);
|
|
|
|
/** Serialized refresh — all callers share the same in-flight promise. */
|
|
const refresh = useCallback(async () => {
|
|
if (refreshInFlightRef.current) {
|
|
return refreshInFlightRef.current;
|
|
}
|
|
const promise = refreshCore().finally(() => {
|
|
refreshInFlightRef.current = null;
|
|
});
|
|
refreshInFlightRef.current = promise;
|
|
return promise;
|
|
}, [refreshCore]);
|
|
|
|
const refreshTeams = useCallback(async () => {
|
|
const requestId = ++teamsRequestIdRef.current;
|
|
const identityAtStart = snapshotIdentity(getCoreStateSnapshot().snapshot);
|
|
const teams = await listTeams();
|
|
commitState(previous => {
|
|
if (requestId !== teamsRequestIdRef.current) {
|
|
return previous;
|
|
}
|
|
|
|
if (snapshotIdentity(previous.snapshot) !== identityAtStart) {
|
|
return previous;
|
|
}
|
|
|
|
return { ...previous, teams };
|
|
});
|
|
}, [commitState]);
|
|
|
|
const refreshTeamMembers = useCallback(
|
|
async (teamId: string) => {
|
|
const members = await getTeamMembers(teamId);
|
|
commitState(previous => ({
|
|
...previous,
|
|
teamMembersById: { ...previous.teamMembersById, [teamId]: members },
|
|
}));
|
|
},
|
|
[commitState]
|
|
);
|
|
|
|
const refreshTeamInvites = useCallback(
|
|
async (teamId: string) => {
|
|
const invites = await getTeamInvites(teamId);
|
|
commitState(previous => ({
|
|
...previous,
|
|
teamInvitesById: { ...previous.teamInvitesById, [teamId]: invites },
|
|
}));
|
|
},
|
|
[commitState]
|
|
);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
const doRefresh = async () => {
|
|
try {
|
|
await refresh();
|
|
bootstrapFailCountRef.current = 0;
|
|
} catch (error) {
|
|
if (!cancelled) {
|
|
bootstrapFailCountRef.current += 1;
|
|
const safe = sanitizeError(error);
|
|
log(
|
|
'refresh failed attempt=%d/%d error=%O',
|
|
bootstrapFailCountRef.current,
|
|
MAX_BOOTSTRAP_RETRIES,
|
|
safe
|
|
);
|
|
console.warn(
|
|
`[core-state] poll failed (attempt ${bootstrapFailCountRef.current}/${MAX_BOOTSTRAP_RETRIES}):`,
|
|
safe
|
|
);
|
|
if (bootstrapFailCountRef.current >= MAX_BOOTSTRAP_RETRIES) {
|
|
commitState(previous => {
|
|
if (previous.isBootstrapping) {
|
|
return { ...previous, isBootstrapping: false };
|
|
}
|
|
return previous;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const load = async () => {
|
|
await doRefresh();
|
|
if (!cancelled) {
|
|
const next = getCoreStateSnapshot();
|
|
if (next.snapshot.auth.isAuthenticated) {
|
|
await refreshTeams().catch(err => {
|
|
log('refreshTeams failed during bootstrap: %O', sanitizeError(err));
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
void load();
|
|
let timeoutId: number | null = null;
|
|
const scheduleNext = () => {
|
|
timeoutId = window.setTimeout(async () => {
|
|
await doRefresh();
|
|
if (!cancelled) {
|
|
scheduleNext();
|
|
}
|
|
}, POLL_MS);
|
|
};
|
|
scheduleNext();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
if (timeoutId !== null) {
|
|
window.clearTimeout(timeoutId);
|
|
}
|
|
};
|
|
}, [commitState, refresh, refreshTeams]);
|
|
|
|
const setAnalyticsEnabled = useCallback(
|
|
async (enabled: boolean) => {
|
|
await openhumanUpdateAnalyticsSettings({ enabled });
|
|
// Optimistic local commit for instant UI feedback, then re-pull the
|
|
// authoritative snapshot so the frontend cache matches the core.
|
|
commitState(previous => ({
|
|
...previous,
|
|
snapshot: { ...previous.snapshot, analyticsEnabled: enabled },
|
|
}));
|
|
syncAnalyticsConsent(enabled);
|
|
await refresh().catch(err => {
|
|
log('refresh failed after setAnalyticsEnabled: %O', sanitizeError(err));
|
|
});
|
|
},
|
|
[commitState, refresh]
|
|
);
|
|
|
|
const setOnboardingCompletedFlag = useCallback(
|
|
async (value: boolean) => {
|
|
await setOnboardingCompleted(value);
|
|
// Optimistic local commit for instant UI feedback, then re-pull the
|
|
// authoritative snapshot so the frontend cache matches the core.
|
|
commitState(previous => ({
|
|
...previous,
|
|
snapshot: { ...previous.snapshot, onboardingCompleted: value },
|
|
}));
|
|
await refresh().catch(err => {
|
|
log('refresh failed after setOnboardingCompletedFlag: %O', sanitizeError(err));
|
|
});
|
|
},
|
|
[commitState, refresh]
|
|
);
|
|
|
|
const updateLocalState = useCallback(
|
|
async (params: Parameters<typeof updateCoreLocalState>[0]) => {
|
|
await updateCoreLocalState(params);
|
|
await refresh();
|
|
},
|
|
[refresh]
|
|
);
|
|
|
|
const storeSessionToken = useCallback(
|
|
async (token: string, user?: object) => {
|
|
await storeSession(token, user ?? {});
|
|
try {
|
|
await syncMemoryClientToken(token);
|
|
memoryTokenRef.current = token;
|
|
} catch (error) {
|
|
console.warn('[core-state] memory client sync failed after session store:', error);
|
|
}
|
|
await refresh();
|
|
await refreshTeams().catch(err => {
|
|
log('refreshTeams failed after session store: %O', sanitizeError(err));
|
|
});
|
|
},
|
|
[refresh, refreshTeams]
|
|
);
|
|
|
|
const clearSession = useCallback(async () => {
|
|
// Bump the snapshot request counter before doing anything else so that
|
|
// any snapshot poll already in flight when the user clicked logout is
|
|
// invalidated on return — otherwise its stale "authenticated" result
|
|
// would clobber our cleared state a few hundred ms after logout.
|
|
snapshotRequestIdRef.current += 1;
|
|
await tauriLogout();
|
|
// Optimistic local clear for instant UI response.
|
|
commitState(previous => ({
|
|
...previous,
|
|
teams: [],
|
|
teamMembersById: {},
|
|
teamInvitesById: {},
|
|
snapshot: {
|
|
...previous.snapshot,
|
|
auth: { isAuthenticated: false, userId: null, user: null, profileId: null },
|
|
sessionToken: null,
|
|
currentUser: null,
|
|
onboardingCompleted: false,
|
|
},
|
|
}));
|
|
memoryTokenRef.current = null;
|
|
// Re-pull the authoritative snapshot from the core so the frontend
|
|
// cache matches whatever the core now reports. This mirrors the pattern
|
|
// used by storeSessionToken and ensures any downstream consumer reading
|
|
// from the snapshot sees the post-logout state immediately.
|
|
await refresh().catch(err => {
|
|
log('refresh failed after clearSession: %O', sanitizeError(err));
|
|
});
|
|
}, [commitState, refresh]);
|
|
|
|
const value = useMemo<CoreStateContextValue>(
|
|
() => ({
|
|
...state,
|
|
refresh,
|
|
refreshTeams,
|
|
refreshTeamMembers,
|
|
refreshTeamInvites,
|
|
setAnalyticsEnabled,
|
|
setOnboardingCompletedFlag,
|
|
setEncryptionKey: value => updateLocalState({ encryptionKey: value }),
|
|
setPrimaryWalletAddress: value => updateLocalState({ primaryWalletAddress: value }),
|
|
setOnboardingTasks: value => updateLocalState({ onboardingTasks: value }),
|
|
storeSessionToken,
|
|
clearSession,
|
|
}),
|
|
[
|
|
clearSession,
|
|
refresh,
|
|
refreshTeamInvites,
|
|
refreshTeamMembers,
|
|
refreshTeams,
|
|
setAnalyticsEnabled,
|
|
setOnboardingCompletedFlag,
|
|
state,
|
|
storeSessionToken,
|
|
updateLocalState,
|
|
]
|
|
);
|
|
|
|
return <CoreStateContext.Provider value={value}>{children}</CoreStateContext.Provider>;
|
|
}
|
|
|
|
export function useCoreState() {
|
|
const context = useContext(CoreStateContext);
|
|
if (!context) {
|
|
throw new Error('useCoreState must be used within CoreStateProvider');
|
|
}
|
|
return context;
|
|
}
|