Skip to content

Commit 9d4dbd1

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/e2e-linux-ci-default
# Conflicts: # src/openhuman/agent/loop_/context_guard.rs
2 parents c09a70b + 6406ec2 commit 9d4dbd1

32 files changed

Lines changed: 3661 additions & 26 deletions

‎.claude/memory.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Quick reference for anyone starting with Claude on this project. Updated by the
3939
- **OnboardingOverlay is a portal, not a route** — mounted in `App.tsx`, renders via `createPortal` at z-[9999]. There is no `/onboarding` route in `AppRoutes.tsx`. Gating is purely Redux + workspace flag.
4040
- **Deferred onboarding** — `onboardingDeferredByUser` in `authSlice.ts` (persisted via redux-persist) durably tracks when a user clicks "Set up later". `SetupBanner.tsx` provides the resume path.
4141
- **`selectHasIncompleteOnboarding` is unused** in production code — only tested. Don't use it for new features.
42+
- **Logout must clear onboarding state** — `_clearToken` resets `isOnboardedByUser` + `isAnalyticsEnabledByUser`. Workspace flag (`.skip_onboarding` file) is cleared via `openhumanWorkspaceOnboardingFlagSet(false)` in SettingsHome logout, clearAllAppData, and UserProvider auth recovery. All three paths must stay in sync.
4243
- **`formatBytes` / `formatEta` / `progressFromStatus`** — shared in `app/src/utils/localAiHelpers.ts`. Home.tsx and LocalModelPanel.tsx still have local copies (can be migrated later).
4344
- **Notification z-index stacking** — ErrorReportNotification: z-[10000] bottom-right. OnboardingOverlay: z-[9999]. LocalAIDownloadSnackbar: z-[9998] bottom-left.
4445
- **React Compiler lint** — `useCallback` deps must match the full inferred closure. Using `user?._id` as dep when the closure captures `user` triggers `preserve-manual-memoization`. Use `user` as the dep instead.

‎.github/workflows/release-packages.yml‎

Lines changed: 454 additions & 0 deletions
Large diffs are not rendered by default.

‎.github/workflows/release.yml‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,48 @@ jobs:
678678
echo "Checking staple..."
679679
xcrun stapler validate "$APP_PATH" || echo "WARNING: Staple validation failed"
680680
681+
- name: Package CLI tarball and upload to release
682+
if: matrix.settings.platform != 'windows-latest'
683+
shell: bash
684+
env:
685+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
686+
VERSION: ${{ needs.prepare-release.outputs.version }}
687+
MATRIX_PLATFORM: ${{ matrix.settings.platform }}
688+
MATRIX_TARGET: ${{ matrix.settings.target }}
689+
run: |
690+
set -euo pipefail
691+
692+
TARBALL="openhuman-core-${VERSION}-${MATRIX_TARGET}.tar.gz"
693+
WORK=$(mktemp -d)
694+
trap 'rm -rf "$WORK"' EXIT
695+
696+
if [[ "$MATRIX_PLATFORM" == "macos-latest" ]]; then
697+
# Prefer the notarized+signed binary extracted from inside the .app bundle
698+
APP_PATH="${{ steps.locate-app.outputs.app_path }}"
699+
BIN=$(find "$APP_PATH/Contents/MacOS" -maxdepth 1 -name "openhuman-core-*" \
700+
! -name "*.sig" 2>/dev/null | head -1 || true)
701+
[[ -z "$BIN" ]] && BIN=$(find "$APP_PATH/Contents/Resources" -maxdepth 1 \
702+
-name "openhuman-core-*" ! -name "*.sig" 2>/dev/null | head -1 || true)
703+
if [[ -z "$BIN" ]]; then
704+
echo "[pkg] Falling back to target dir binary (no notarized sidecar found)"
705+
BIN="${{ steps.cli-paths.outputs.cli_path }}"
706+
fi
707+
else
708+
BIN="${{ steps.cli-paths.outputs.cli_path }}"
709+
fi
710+
711+
cp "$BIN" "$WORK/openhuman-core"
712+
chmod +x "$WORK/openhuman-core"
713+
tar -czf "$TARBALL" -C "$WORK" openhuman-core
714+
715+
# openssl dgst works on both macOS and Linux runners
716+
openssl dgst -sha256 -r "$TARBALL" | awk '{print $1}' > "${TARBALL}.sha256"
717+
718+
gh release upload "v${VERSION}" "$TARBALL" "${TARBALL}.sha256" \
719+
--repo tinyhumansai/openhuman --clobber
720+
721+
echo "[pkg] Uploaded $TARBALL and ${TARBALL}.sha256"
722+
681723
- name: Upload standalone CLI artifacts
682724
uses: actions/upload-artifact@v4
683725
with:

‎app/src/components/OnboardingOverlay.tsx‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,24 @@ const OnboardingOverlay = () => {
2727
const isDeferred = useAppSelector(selectOnboardingDeferred);
2828
const [hasWorkspaceFlag, setHasWorkspaceFlag] = useState<boolean | null>(null);
2929
const [dismissed, setDismissed] = useState(false);
30+
const [userLoadTimedOut, setUserLoadTimedOut] = useState(false);
3031

31-
// Check workspace flag once user is loaded
32+
// Timeout: if user profile hasn't loaded after 3s but we have token + bootstrap,
33+
// proceed anyway so onboarding isn't permanently invisible.
3234
useEffect(() => {
33-
if (!token || !isAuthBootstrapComplete || !user?._id) return;
35+
if (!token || !isAuthBootstrapComplete || user?._id) return;
36+
37+
const timer = setTimeout(() => setUserLoadTimedOut(true), 3000);
38+
return () => clearTimeout(timer);
39+
}, [token, isAuthBootstrapComplete, user?._id]);
40+
41+
// User is ready when profile loaded or timeout elapsed.
42+
// Note: userLoadTimedOut is sticky across sessions but harmless — when token
43+
// is null (logged out) the early-return guard prevents any visible effect,
44+
// and the workspace flag check doesn't require userId.
45+
const userReady = !!user?._id || userLoadTimedOut;
46+
useEffect(() => {
47+
if (!token || !isAuthBootstrapComplete || !userReady) return;
3448

3549
let mounted = true;
3650
const check = async () => {
@@ -47,7 +61,7 @@ const OnboardingOverlay = () => {
4761
return () => {
4862
mounted = false;
4963
};
50-
}, [token, isAuthBootstrapComplete, user?._id, isOnboarded]);
64+
}, [token, isAuthBootstrapComplete, userReady, isOnboarded]);
5165

5266
const handleComplete = useCallback(() => {
5367
setDismissed(true);
@@ -60,8 +74,8 @@ const OnboardingOverlay = () => {
6074
setDismissed(true);
6175
}, [dispatch, user]);
6276

63-
// Don't show if not logged in, bootstrap not complete, or user not loaded
64-
if (!token || !isAuthBootstrapComplete || !user?._id) return null;
77+
// Don't show if not logged in, bootstrap not complete, or user not ready
78+
if (!token || !isAuthBootstrapComplete || !userReady) return null;
6579

6680
// Still loading workspace flag
6781
if (hasWorkspaceFlag === null) return null;

‎app/src/components/settings/SettingsHome.tsx‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { skillManager } from '../../lib/skills/manager';
44
import { persistor } from '../../store';
55
import { clearToken } from '../../store/authSlice';
66
import { useAppDispatch } from '../../store/hooks';
7-
import { logout as tauriLogout } from '../../utils/tauriCommands';
7+
import {
8+
openhumanWorkspaceOnboardingFlagSet,
9+
logout as tauriLogout,
10+
} from '../../utils/tauriCommands';
811
import SettingsHeader from './components/SettingsHeader';
912
import SettingsMenuItem from './components/SettingsMenuItem';
1013
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
@@ -18,6 +21,11 @@ const SettingsHome = () => {
1821

1922
const handleLogout = async () => {
2023
await dispatch(clearToken());
24+
try {
25+
await openhumanWorkspaceOnboardingFlagSet(false);
26+
} catch (err) {
27+
console.warn('[Settings] Failed to clear workspace onboarding flag:', err);
28+
}
2129
try {
2230
await tauriLogout();
2331
} catch (err) {
@@ -28,6 +36,11 @@ const SettingsHome = () => {
2836

2937
const clearAllAppData = async () => {
3038
await dispatch(clearToken());
39+
try {
40+
await openhumanWorkspaceOnboardingFlagSet(false);
41+
} catch (err) {
42+
console.warn('[Settings] Failed to clear workspace onboarding flag:', err);
43+
}
3144
try {
3245
await tauriLogout();
3346
} catch (err) {

‎app/src/providers/UserProvider.tsx‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { useEffect } from 'react';
22

33
import { clearToken, setAuthBootstrapComplete, setToken } from '../store/authSlice';
44
import { useAppDispatch, useAppSelector } from '../store/hooks';
5-
import { getAuthState, getSessionToken, isTauri } from '../utils/tauriCommands';
5+
import {
6+
getAuthState,
7+
getSessionToken,
8+
isTauri,
9+
openhumanWorkspaceOnboardingFlagSet,
10+
} from '../utils/tauriCommands';
611

712
const AUTH_BOOTSTRAP_TIMEOUT_MS = 5000;
813

@@ -50,6 +55,11 @@ const UserProvider = ({ children }: { children: React.ReactNode }) => {
5055
}
5156
} else if (!authState.is_authenticated && token) {
5257
await dispatch(clearToken());
58+
try {
59+
await openhumanWorkspaceOnboardingFlagSet(false);
60+
} catch (err) {
61+
console.warn('[auth] Failed to clear workspace onboarding flag:', err);
62+
}
5363
}
5464
} catch (err) {
5565
console.warn('[auth] Failed to restore session token from core RPC:', err);

‎app/src/store/__tests__/authSlice.test.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ import { configureStore } from '@reduxjs/toolkit';
22
import { describe, expect, it } from 'vitest';
33

44
import authReducer, {
5+
clearToken,
56
setAnalyticsForUser,
7+
setEncryptionKeyForUser,
68
setOnboardedForUser,
9+
setOnboardingDeferredForUser,
710
setOnboardingTasksForUser,
11+
setPrimaryWalletAddressForUser,
812
setToken,
913
} from '../authSlice';
1014
import teamReducer from '../teamSlice';
@@ -87,4 +91,49 @@ describe('authSlice', () => {
8791
);
8892
expect(store.getState().auth.hasIncompleteOnboardingByUser.u1).toBe(false);
8993
});
94+
95+
it('clearToken resets all per-user state', async () => {
96+
const store = createStore();
97+
98+
// Populate all per-user fields
99+
store.dispatch(setToken('jwt-abc'));
100+
store.dispatch(setOnboardedForUser({ userId: 'u1', value: true }));
101+
store.dispatch(setAnalyticsForUser({ userId: 'u1', enabled: true }));
102+
store.dispatch(setEncryptionKeyForUser({ userId: 'u1', key: 'aes-hex' }));
103+
store.dispatch(setPrimaryWalletAddressForUser({ userId: 'u1', address: '0xabc' }));
104+
store.dispatch(setOnboardingDeferredForUser({ userId: 'u1', deferred: true }));
105+
store.dispatch(
106+
setOnboardingTasksForUser({
107+
userId: 'u1',
108+
tasks: {
109+
accessibilityPermissionGranted: true,
110+
localModelConsentGiven: true,
111+
localModelDownloadStarted: true,
112+
enabledTools: ['shell'],
113+
connectedSources: ['telegram'],
114+
},
115+
})
116+
);
117+
118+
// Verify state is populated
119+
const before = store.getState().auth;
120+
expect(before.token).toBe('jwt-abc');
121+
expect(before.isOnboardedByUser.u1).toBe(true);
122+
expect(before.isAnalyticsEnabledByUser.u1).toBe(true);
123+
expect(before.encryptionKeyByUser.u1).toBe('aes-hex');
124+
125+
// Dispatch clearToken thunk
126+
await store.dispatch(clearToken());
127+
128+
// Verify everything is reset
129+
const after = store.getState().auth;
130+
expect(after.token).toBeNull();
131+
expect(after.isOnboardedByUser).toEqual({});
132+
expect(after.onboardingTasksByUser).toEqual({});
133+
expect(after.hasIncompleteOnboardingByUser).toEqual({});
134+
expect(after.isAnalyticsEnabledByUser).toEqual({});
135+
expect(after.encryptionKeyByUser).toEqual({});
136+
expect(after.primaryWalletAddressByUser).toEqual({});
137+
expect(after.onboardingDeferredByUser).toEqual({});
138+
});
90139
});

‎app/src/store/authSlice.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ const authSlice = createSlice({
5656
},
5757
_clearToken: state => {
5858
state.token = null;
59+
state.isOnboardedByUser = {};
5960
state.onboardingTasksByUser = {};
6061
state.hasIncompleteOnboardingByUser = {};
62+
state.isAnalyticsEnabledByUser = {};
6163
state.encryptionKeyByUser = {};
6264
state.primaryWalletAddressByUser = {};
6365
state.onboardingDeferredByUser = {};

0 commit comments

Comments
 (0)