diff --git a/package.json b/package.json
index 7b4f644b6e5dea8b3be139dd1aa3c8cc733917aa..88c91d05bbb06f423da68daa39486cb0a629d5cb 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
     "bowser": "1.9.4",
     "chromedriver": "2.43.1",
     "classnames": "2.2.6",
+    "computed-style-to-inline-style": "3.0.0",
     "copy-webpack-plugin": "^4.5.1",
     "core-js": "2.5.7",
     "css-loader": "^1.0.0",
@@ -99,13 +100,13 @@
     "redux-throttle": "0.1.1",
     "rimraf": "^2.6.1",
     "scratch-audio": "0.1.0-prerelease.20181023202904",
-    "scratch-blocks": "0.1.0-prerelease.1541019511",
+    "scratch-blocks": "0.1.0-prerelease.1541605931",
     "scratch-l10n": "3.0.20181031234255",
-    "scratch-paint": "0.2.0-prerelease.20181029151725",
-    "scratch-render": "0.1.0-prerelease.20181029125804",
+    "scratch-paint": "0.2.0-prerelease.20181105210331",
+    "scratch-render": "0.1.0-prerelease.20181102130522",
     "scratch-storage": "1.2.0",
-    "scratch-svg-renderer": "0.2.0-prerelease.20181024192149",
-    "scratch-vm": "0.2.0-prerelease.20181030160328",
+    "scratch-svg-renderer": "0.2.0-prerelease.20181101210634",
+    "scratch-vm": "0.2.0-prerelease.20181107153250",
     "selenium-webdriver": "3.6.0",
     "startaudiocontext": "1.2.1",
     "style-loader": "^0.23.0",
diff --git a/src/components/alerts/alert.css b/src/components/alerts/alert.css
index c4f00a741912c0ea92b73d5f387b759f707dce4b..0787a9fb12436804149417d930db8afd01de5c71 100644
--- a/src/components/alerts/alert.css
+++ b/src/components/alerts/alert.css
@@ -30,7 +30,29 @@
 }
 
 .alert-close-button {
+    outline-style:none;
+}
+
+.alert-close-button-container {
     margin-top: 7px;
     margin-right: 4px;
     outline-style:none;
+    width: 30px;
+    height: 30px;
+}
+
+.connection-button {
+    padding: 0.55rem 0.9rem;
+    border-radius: 0.35rem;
+    background: #FF8C1A;
+    color: white;
+    font-weight: 700;
+    font-size: 0.77rem;
+    margin: 0.25rem;
+    border: none;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    margin-right: 13px;
+    outline-style:none;
 }
diff --git a/src/components/alerts/alert.jsx b/src/components/alerts/alert.jsx
index d0f65309e086f3cc2582131ce21677c2d9a4845e..2ec848984c77aaac28a9a85f6a3bce048089b617 100644
--- a/src/components/alerts/alert.jsx
+++ b/src/components/alerts/alert.jsx
@@ -1,5 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import {FormattedMessage} from 'react-intl';
 
 import Box from '../box/box.jsx';
 import CloseButton from '../close-button/close-button.jsx';
@@ -9,7 +10,9 @@ import styles from './alert.css';
 const AlertComponent = ({
     iconURL,
     message,
-    onCloseAlert
+    onCloseAlert,
+    onReconnect,
+    showReconnect
 }) => (
     <Box
         className={styles.alert}
@@ -23,19 +26,37 @@ const AlertComponent = ({
             ) : null}
             {message}
         </div>
-        <CloseButton
-            className={styles.alertCloseButton}
-            color={CloseButton.COLOR_ORANGE}
-            size={CloseButton.SIZE_LARGE}
-            onClick={onCloseAlert}
-        />
+        {showReconnect ? (
+            <button
+                className={styles.connectionButton}
+                onClick={onReconnect}
+            >
+                <FormattedMessage
+                    defaultMessage="Reconnect"
+                    description="Button to reconnect the device"
+                    id="gui.connection.reconnect"
+                />
+            </button>
+        ) : null}
+        <Box
+            className={styles.alertCloseButtonContainer}
+        >
+            <CloseButton
+                className={styles.alertCloseButton}
+                color={CloseButton.COLOR_ORANGE}
+                size={CloseButton.SIZE_LARGE}
+                onClick={onCloseAlert}
+            />
+        </Box>
     </Box>
 );
 
 AlertComponent.propTypes = {
     iconURL: PropTypes.string,
     message: PropTypes.string,
-    onCloseAlert: PropTypes.func.isRequired
+    onCloseAlert: PropTypes.func.isRequired,
+    onReconnect: PropTypes.func,
+    showReconnect: PropTypes.bool.isRequired
 };
 
 export default AlertComponent;
diff --git a/src/components/alerts/alerts.jsx b/src/components/alerts/alerts.jsx
index 1835a50a820e7de90443449ecec60c54f33779f5..9a87f766c376a0562225e118c9ef53cea0d29f4f 100644
--- a/src/components/alerts/alerts.jsx
+++ b/src/components/alerts/alerts.jsx
@@ -15,10 +15,12 @@ const AlertsComponent = ({
     >
         {alertsList.map((a, index) => (
             <Alert
+                extensionId={a.extensionId}
                 iconURL={a.iconURL}
                 index={index}
                 key={index}
                 message={a.message}
+                showReconnect={a.showReconnect}
                 onCloseAlert={onCloseAlert}
             />
         ))}
diff --git a/src/components/backpack/backpack.css b/src/components/backpack/backpack.css
index 33585ca90de66225f89228aa5aa9a35f6798adca..6eddfa905c4a6f79eb1017d149b7030e32aa278a 100644
--- a/src/components/backpack/backpack.css
+++ b/src/components/backpack/backpack.css
@@ -71,5 +71,21 @@
 .backpack-item {
     min-width: 4rem;
     margin: 0 0.25rem;
+}
+
+.backpack-item img {
     mix-blend-mode: multiply; /* Make white transparent for thumnbnails */
 }
+
+.more {
+    background: $motion-primary;
+    color: $ui-white;
+    border: none;
+    outline: none;
+    font-weight: bold;
+    border-radius: 0.5rem;
+    font-size: 0.85rem;
+    padding: 0.5rem;
+    margin: 0.5rem;
+    cursor: pointer;
+}
diff --git a/src/components/backpack/backpack.jsx b/src/components/backpack/backpack.jsx
index f5f4500393bc73d80e888d7b7b90b97ad6cde567..f5d1a0ae280b81849055830aab20bd0b575a868a 100644
--- a/src/components/backpack/backpack.jsx
+++ b/src/components/backpack/backpack.jsx
@@ -25,10 +25,12 @@ const Backpack = ({
     error,
     expanded,
     loading,
+    showMore,
     onToggle,
     onDelete,
     onMouseEnter,
-    onMouseLeave
+    onMouseLeave,
+    onMore
 }) => (
     <div className={styles.backpackContainer}>
         <div
@@ -98,6 +100,18 @@ const Backpack = ({
                                         onDeleteButtonClick={onDelete}
                                     />
                                 ))}
+                                {showMore && (
+                                    <button
+                                        className={styles.more}
+                                        onClick={onMore}
+                                    >
+                                        <FormattedMessage
+                                            defaultMessage="More"
+                                            description="Load more from backpack"
+                                            id="gui.backpack.more"
+                                        />
+                                    </button>
+                                )}
                             </div>
                         ) : (
                             <div className={styles.statusMessage}>
@@ -129,9 +143,11 @@ Backpack.propTypes = {
     expanded: PropTypes.bool,
     loading: PropTypes.bool,
     onDelete: PropTypes.func,
+    onMore: PropTypes.func,
     onMouseEnter: PropTypes.func,
     onMouseLeave: PropTypes.func,
-    onToggle: PropTypes.func
+    onToggle: PropTypes.func,
+    showMore: PropTypes.bool
 };
 
 Backpack.defaultProps = {
@@ -140,6 +156,8 @@ Backpack.defaultProps = {
     dragOver: false,
     expanded: false,
     loading: false,
+    showMore: false,
+    onMore: null,
     onToggle: null
 };
 
diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css
index 3e4d763c4817107abd738ec6c4c8367aae8246d6..eb4f1c929a39974b58fe42c951057b2ab4454bc6 100644
--- a/src/components/gui/gui.css
+++ b/src/components/gui/gui.css
@@ -303,7 +303,7 @@ $fade-out-distance: 15px;
 /* Alerts */
 
 .alerts-container {
-    width: 448px;
+    width: 520px;
     z-index: $z-index-alerts;
     left: 0;
     right: 0;
diff --git a/src/components/menu-bar/login-dropdown.jsx b/src/components/menu-bar/login-dropdown.jsx
index 11d23856c8f35edbec895c3b311073b8cc7ec743..0116d7342dfff258c1b8205aac36eb182bf381c6 100644
--- a/src/components/menu-bar/login-dropdown.jsx
+++ b/src/components/menu-bar/login-dropdown.jsx
@@ -7,11 +7,38 @@ eventually be consolidated.
 import classNames from 'classnames';
 import PropTypes from 'prop-types';
 import React from 'react';
+import {defineMessages} from 'react-intl';
 
 import MenuBarMenu from './menu-bar-menu.jsx';
 
 import styles from './login-dropdown.css';
 
+// these are here as a hack to get them translated, so that equivalent messages will be translated
+// when passed in from www via gui's renderLogin() function
+const LoginDropdownMessages = defineMessages({ // eslint-disable-line no-unused-vars
+    username: {
+        defaultMessage: 'Username',
+        description: 'Label for login username input',
+        id: 'general.username'
+    },
+    password: {
+        defaultMessage: 'Password',
+        description: 'Label for login password input',
+        id: 'general.password'
+    },
+    signin: {
+        defaultMessage: 'Sign in',
+        description: 'Button text for user to sign in',
+        id: 'general.signIn'
+    },
+    needhelp: {
+        defaultMessage: 'Need Help?',
+        description: 'Button text for user to indicate that they need help',
+        id: 'login.needHelp'
+    }
+});
+
+
 const LoginDropdown = ({
     className,
     isOpen,
diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx
index f7af33ec0bf7eeda60b8d757e2870a71e99d3229..87af1e683916e3a26d886a5bf14f6a2c9d0cb279 100644
--- a/src/components/menu-bar/menu-bar.jsx
+++ b/src/components/menu-bar/menu-bar.jsx
@@ -150,12 +150,13 @@ class MenuBar extends React.Component {
         }
     }
     handleClickNew () {
-        // if canCreateNew===true, it's safe to replace current project, since we will auto-save first.
-        // else confirm first.
-        const readyToReplaceProject = this.props.canCreateNew ||
+        // if canSave===true and canCreateNew===true, it's safe to replace current project,
+        // since we will auto-save first. Else, confirm first.
+        const readyToReplaceProject = (this.props.canSave && this.props.canCreateNew) ||
             confirm('Replace contents of the current project?'); // eslint-disable-line no-alert
+        this.props.onRequestCloseFile();
         if (readyToReplaceProject) {
-            this.props.onClickNew(this.props.canCreateNew);
+            this.props.onClickNew(this.props.canSave && this.props.canCreateNew);
         }
     }
     handleClickRemix () {
@@ -337,37 +338,25 @@ class MenuBar extends React.Component {
                                 place={this.props.isRtl ? 'left' : 'right'}
                                 onRequestClose={this.props.onRequestCloseFile}
                             >
-                                <MenuItem
-                                    isRtl={this.props.isRtl}
-                                    onClick={this.handleClickNew}
-                                >
-                                    {newProjectMessage}
-                                </MenuItem>
+                                <MenuSection>
+                                    <MenuItem
+                                        isRtl={this.props.isRtl}
+                                        onClick={this.handleClickNew}
+                                    >
+                                        {newProjectMessage}
+                                    </MenuItem>
+                                </MenuSection>
                                 <MenuSection>
                                     {this.props.canSave ? (
                                         <MenuItem onClick={this.handleClickSave}>
                                             {saveNowMessage}
                                         </MenuItem>
-                                    ) : (this.props.showComingSoon ? (
-                                        <MenuItemTooltip
-                                            id="save"
-                                            isRtl={this.props.isRtl}
-                                        >
-                                            <MenuItem>{saveNowMessage}</MenuItem>
-                                        </MenuItemTooltip>
-                                    ) : [])}
+                                    ) : []}
                                     {this.props.canCreateCopy ? (
                                         <MenuItem onClick={this.handleClickSaveAsCopy}>
                                             {createCopyMessage}
                                         </MenuItem>
-                                    ) : (this.props.showComingSoon ? (
-                                        <MenuItemTooltip
-                                            id="copy"
-                                            isRtl={this.props.isRtl}
-                                        >
-                                            <MenuItem>{createCopyMessage}</MenuItem>
-                                        </MenuItemTooltip>
-                                    ) : [])}
+                                    ) : []}
                                     {this.props.canRemix ? (
                                         <MenuItem onClick={this.handleClickRemix}>
                                             {remixMessage}
@@ -376,8 +365,9 @@ class MenuBar extends React.Component {
                                 </MenuSection>
                                 <MenuSection>
                                     <SBFileUploader onUpdateProjectTitle={this.props.onUpdateProjectTitle}>
-                                        {(renderFileInput, loadProject) => (
+                                        {(className, renderFileInput, loadProject) => (
                                             <MenuItem
+                                                className={className}
                                                 onClick={loadProject}
                                             >
                                                 <FormattedMessage
@@ -391,8 +381,9 @@ class MenuBar extends React.Component {
                                             </MenuItem>
                                         )}
                                     </SBFileUploader>
-                                    <SB3Downloader>{downloadProject => (
+                                    <SB3Downloader>{(className, downloadProject) => (
                                         <MenuItem
+                                            className={className}
                                             onClick={this.handleCloseFileMenuAndThen(downloadProject)}
                                         >
                                             <FormattedMessage
@@ -752,7 +743,7 @@ const mapDispatchToProps = dispatch => ({
     onRequestCloseLanguage: () => dispatch(closeLanguageMenu()),
     onClickLogin: () => dispatch(openLoginMenu()),
     onRequestCloseLogin: () => dispatch(closeLoginMenu()),
-    onClickNew: canCreateNew => dispatch(requestNewProject(canCreateNew)),
+    onClickNew: needSave => dispatch(requestNewProject(needSave)),
     onClickRemix: () => dispatch(remixProject()),
     onClickSave: () => dispatch(updateProject()),
     onClickSaveAsCopy: () => dispatch(saveProjectAsCopy()),
diff --git a/src/components/menu/menu.jsx b/src/components/menu/menu.jsx
index 4e6a6b229170d5d8f0a7844c3fa58ff3425972bc..e69068052cb4e3b3027c3550bada15c1f0812e80 100644
--- a/src/components/menu/menu.jsx
+++ b/src/components/menu/menu.jsx
@@ -55,9 +55,10 @@ MenuItem.propTypes = {
 
 const addDividerClassToFirstChild = (child, id) => (
     React.cloneElement(child, {
-        className: classNames(child.className, {
-            [styles.menuSection]: id === 0
-        }),
+        className: classNames(
+            child.className,
+            {[styles.menuSection]: id === 0}
+        ),
         key: id
     })
 );
diff --git a/src/components/spinner/spinner.css b/src/components/spinner/spinner.css
new file mode 100644
index 0000000000000000000000000000000000000000..89d22534859ecdd0a82f5686f5fcbe38103387b0
--- /dev/null
+++ b/src/components/spinner/spinner.css
@@ -0,0 +1,54 @@
+@import "../../css/colors.css";
+
+.spinner {
+    width: 1rem;
+    height: 1rem;
+    display: inline-block;
+    position: relative;
+    border-radius: 50%;
+    border-width: .1875rem;
+    border-style: solid;
+    border-color: $ui-white-transparent;
+}
+
+.spinner::after, .spinner::before {
+    width: .625rem;
+    height: .625rem;
+    content: '';
+    border-radius: 50%;
+    display: block;
+}
+
+.spinner::after {
+    position: absolute;
+    top: -.1875rem;
+    left: -.1875rem;
+    border: .1875rem solid transparent;
+    border-top-color: $ui-white;
+    animation: spin 1.5s cubic-bezier(0.4, 0.1, 0.4, 1) infinite;
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+.spinner.orange {
+    border-color: $error-transparent;
+}
+
+.spinner.orange::after {
+    border-top-color: $error-primary;
+}
+
+.spinner.white {
+    border-color: $ui-white-transparent;
+}
+.spinner.white::after {
+    border-top-color: $ui-white;
+}
diff --git a/src/components/spinner/spinner.jsx b/src/components/spinner/spinner.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..08685ccb315220d25d1dd90f81b541f574808722
--- /dev/null
+++ b/src/components/spinner/spinner.jsx
@@ -0,0 +1,26 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import styles from './spinner.css';
+
+const SpinnerComponent = function (props) {
+    const {
+        className
+    } = props;
+    return (
+        <div
+            className={classNames(
+                styles.spinner,
+                className
+            )}
+        />
+    );
+};
+SpinnerComponent.propTypes = {
+    className: PropTypes.string
+};
+SpinnerComponent.defaultProps = {
+    className: ''
+};
+export default SpinnerComponent;
diff --git a/src/components/sprite-selector-item/sprite-selector-item.css b/src/components/sprite-selector-item/sprite-selector-item.css
index d29d15c82c0777cf8eecec14ae027972715cc506..7a2c1f4cea61ece8df041a7ae9bb05502a8d4652 100644
--- a/src/components/sprite-selector-item/sprite-selector-item.css
+++ b/src/components/sprite-selector-item/sprite-selector-item.css
@@ -42,6 +42,7 @@
 .sprite-image {
     margin: auto;
     user-select: none;
+    pointer-events: none;
     max-width: 32px;
     max-height: 32px;
 }
@@ -80,7 +81,7 @@
 .delete-button {
     position: absolute;
     top: 0.125rem;
-    z-index: 1;
+    z-index: auto;
 }
 
 [dir="ltr"] .delete-button {
diff --git a/src/components/sprite-selector/sprite-list.jsx b/src/components/sprite-selector/sprite-list.jsx
index c10362a44797f876322f4c133290c16b0c712154..17ba2dc215345c14ddc3f4291ca7cc2700927e20 100644
--- a/src/components/sprite-selector/sprite-list.jsx
+++ b/src/components/sprite-selector/sprite-list.jsx
@@ -57,7 +57,8 @@ const SpriteList = function (props) {
                     DragConstants.COSTUME,
                     DragConstants.SOUND,
                     DragConstants.BACKPACK_COSTUME,
-                    DragConstants.BACKPACK_SOUND].includes(draggingType);
+                    DragConstants.BACKPACK_SOUND,
+                    DragConstants.BACKPACK_CODE].includes(draggingType);
 
                 return (
                     <SortableAsset
diff --git a/src/containers/alert.jsx b/src/containers/alert.jsx
index cb38427ad751c6a73b85a2da7ee0565e9ecec21d..62ff58448a03c172687a9a3c5518b516b3f0b187 100644
--- a/src/containers/alert.jsx
+++ b/src/containers/alert.jsx
@@ -1,40 +1,68 @@
 import React from 'react';
 import bindAll from 'lodash.bindall';
 import PropTypes from 'prop-types';
+import {connect} from 'react-redux';
 
 import AlertComponent from '../components/alerts/alert.jsx';
+import {openConnectionModal} from '../reducers/modals';
+import {setConnectionModalExtensionId} from '../reducers/connection-modal';
 
 class Alert extends React.Component {
     constructor (props) {
         super(props);
         bindAll(this, [
-            'handleOnCloseAlert'
+            'handleOnCloseAlert',
+            'handleOnReconnect'
         ]);
     }
     handleOnCloseAlert () {
         this.props.onCloseAlert(this.props.index);
     }
+    handleOnReconnect () {
+        this.props.onOpenConnectionModal(this.props.extensionId);
+        this.handleOnCloseAlert();
+    }
     render () {
         const {
             index, // eslint-disable-line no-unused-vars
             iconURL,
-            message
+            message,
+            showReconnect
         } = this.props;
         return (
             <AlertComponent
                 iconURL={iconURL}
                 message={message}
+                showReconnect={showReconnect}
                 onCloseAlert={this.handleOnCloseAlert}
+                onReconnect={this.handleOnReconnect}
             />
         );
     }
 }
 
+const mapStateToProps = state => ({
+    state: state
+});
+
+const mapDispatchToProps = dispatch => ({
+    onOpenConnectionModal: id => {
+        dispatch(setConnectionModalExtensionId(id));
+        dispatch(openConnectionModal());
+    }
+});
+
 Alert.propTypes = {
+    extensionId: PropTypes.string,
     iconURL: PropTypes.string,
     index: PropTypes.number,
     message: PropTypes.string,
-    onCloseAlert: PropTypes.func.isRequired
+    onCloseAlert: PropTypes.func.isRequired,
+    onOpenConnectionModal: PropTypes.func,
+    showReconnect: PropTypes.bool.isRequired
 };
 
-export default Alert;
+export default connect(
+    mapStateToProps,
+    mapDispatchToProps
+)(Alert);
diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx
index cca9eaab2a62e8f50f4dc6469db8fee9498d5c15..1a22a185e956db246d371aa8e1a10eb5ca883e6c 100644
--- a/src/containers/backpack.jsx
+++ b/src/containers/backpack.jsx
@@ -29,11 +29,12 @@ class Backpack extends React.Component {
             'handleToggle',
             'handleDelete',
             'getBackpackAssetURL',
-            'refreshContents',
+            'getContents',
             'handleMouseEnter',
             'handleMouseLeave',
             'handleBlockDragEnd',
-            'handleBlockDragUpdate'
+            'handleBlockDragUpdate',
+            'handleMore'
         ]);
         this.state = {
             // While the DroppableHOC manages drop interactions for asset tiles,
@@ -42,8 +43,8 @@ class Backpack extends React.Component {
             blockDragOutsideWorkspace: false,
             blockDragOverBackpack: false,
             error: false,
-            offset: 0,
             itemsPerPage: 20,
+            moreToLoad: false,
             loading: false,
             expanded: false,
             contents: []
@@ -72,12 +73,12 @@ class Backpack extends React.Component {
     }
     handleToggle () {
         const newState = !this.state.expanded;
-        this.setState({expanded: newState, offset: 0}, () => {
+        this.setState({expanded: newState, contents: []}, () => {
             // Emit resize on window to get blocks to resize
             window.dispatchEvent(new Event('resize'));
         });
         if (newState) {
-            this.refreshContents();
+            this.getContents();
         }
     }
     handleDrop (dragInfo) {
@@ -98,39 +99,69 @@ class Backpack extends React.Component {
         }
         if (!payloader) return;
 
-        payloader(dragInfo.payload, this.props.vm)
-            .then(payload => saveBackpackObject({
-                host: this.props.host,
-                token: this.props.token,
-                username: this.props.username,
-                ...payload
-            }))
-            .then(this.refreshContents);
+        // Creating the payload is async, so set loading before starting
+        this.setState({loading: true}, () => {
+            payloader(dragInfo.payload, this.props.vm)
+                .then(payload => saveBackpackObject({
+                    host: this.props.host,
+                    token: this.props.token,
+                    username: this.props.username,
+                    ...payload
+                }))
+                .then(item => {
+                    this.setState({
+                        loading: false,
+                        contents: [item].concat(this.state.contents)
+                    });
+                })
+                .catch(error => {
+                    this.setState({error: true, loading: false});
+                    throw error;
+                });
+        });
     }
     handleDelete (id) {
-        deleteBackpackObject({
-            host: this.props.host,
-            token: this.props.token,
-            username: this.props.username,
-            id: id
-        }).then(this.refreshContents);
-    }
-    refreshContents () {
-        if (this.props.token && this.props.username) {
-            this.setState({loading: true, error: false});
-            getBackpackContents({
+        this.setState({loading: true}, () => {
+            deleteBackpackObject({
                 host: this.props.host,
                 token: this.props.token,
                 username: this.props.username,
-                offset: this.state.offset,
-                limit: this.state.itemsPerPage
+                id: id
             })
-                .then(contents => {
-                    this.setState({contents, loading: false});
+                .then(() => {
+                    this.setState({
+                        loading: false,
+                        contents: this.state.contents.filter(o => o.id !== id)
+                    });
                 })
-                .catch(() => {
+                .catch(error => {
                     this.setState({error: true, loading: false});
+                    throw error;
                 });
+        });
+    }
+    getContents () {
+        if (this.props.token && this.props.username) {
+            this.setState({loading: true, error: false}, () => {
+                getBackpackContents({
+                    host: this.props.host,
+                    token: this.props.token,
+                    username: this.props.username,
+                    offset: this.state.contents.length,
+                    limit: this.state.itemsPerPage
+                })
+                    .then(contents => {
+                        this.setState({
+                            contents: this.state.contents.concat(contents),
+                            moreToLoad: contents.length === this.state.itemsPerPage,
+                            loading: false
+                        });
+                    })
+                    .catch(error => {
+                        this.setState({error: true, loading: false});
+                        throw error;
+                    });
+            });
         }
     }
     handleBlockDragUpdate (isOutsideWorkspace) {
@@ -150,11 +181,14 @@ class Backpack extends React.Component {
             blockDragOverBackpack: false
         });
     }
-    handleBlockDragEnd (blocks) {
+    handleBlockDragEnd (blocks, topBlockId) {
         if (this.state.blockDragOverBackpack) {
             this.handleDrop({
                 dragType: DragConstants.CODE,
-                payload: blocks
+                payload: {
+                    blockObjects: blocks,
+                    topBlockId: topBlockId
+                }
             });
         }
         this.setState({
@@ -162,6 +196,9 @@ class Backpack extends React.Component {
             blockDragOutsideWorkspace: false
         });
     }
+    handleMore () {
+        this.getContents();
+    }
     render () {
         return (
             <DroppableBackpack
@@ -170,8 +207,10 @@ class Backpack extends React.Component {
                 error={this.state.error}
                 expanded={this.state.expanded}
                 loading={this.state.loading}
+                showMore={this.state.moreToLoad}
                 onDelete={this.handleDelete}
                 onDrop={this.handleDrop}
+                onMore={this.handleMore}
                 onMouseEnter={this.handleMouseEnter}
                 onMouseLeave={this.handleMouseLeave}
                 onToggle={this.props.host ? this.handleToggle : null}
diff --git a/src/containers/sb-file-uploader.jsx b/src/containers/sb-file-uploader.jsx
index ffa4e0122b8cd12265a573cb60050c2f8eaef973..f7bacfd7391db72a85483840f977bfe12c507c6a 100644
--- a/src/containers/sb-file-uploader.jsx
+++ b/src/containers/sb-file-uploader.jsx
@@ -107,13 +107,14 @@ class SBFileUploader extends React.Component {
         );
     }
     render () {
-        return this.props.children(this.renderFileInput, this.handleClick);
+        return this.props.children(this.props.className, this.renderFileInput, this.handleClick);
     }
 }
 
 SBFileUploader.propTypes = {
     canSave: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
     children: PropTypes.func,
+    className: PropTypes.string,
     intl: intlShape.isRequired,
     loadingState: PropTypes.oneOf(LoadingStates),
     onLoadingFinished: PropTypes.func,
@@ -123,6 +124,9 @@ SBFileUploader.propTypes = {
         loadProject: PropTypes.func
     })
 };
+SBFileUploader.defaultProps = {
+    className: ''
+};
 const mapStateToProps = state => ({
     loadingState: state.scratchGui.projectState.loadingState,
     vm: state.scratchGui.vm
diff --git a/src/containers/sb3-downloader.jsx b/src/containers/sb3-downloader.jsx
index 703674c62398a0a71a2424118d9155e556583c01..47df7bacaebe341abc571d3c9ca7e22e2d024e34 100644
--- a/src/containers/sb3-downloader.jsx
+++ b/src/containers/sb3-downloader.jsx
@@ -52,6 +52,7 @@ class SB3Downloader extends React.Component {
             children
         } = this.props;
         return children(
+            this.props.className,
             this.downloadProject
         );
     }
@@ -67,10 +68,14 @@ const getProjectFilename = (curTitle, defaultTitle) => {
 
 SB3Downloader.propTypes = {
     children: PropTypes.func,
+    className: PropTypes.string,
     onSaveFinished: PropTypes.func,
     projectFilename: PropTypes.string,
     saveProjectSb3: PropTypes.func
 };
+SB3Downloader.defaultProps = {
+    className: ''
+};
 
 const mapStateToProps = state => ({
     saveProjectSb3: state.scratchGui.vm.saveProjectSb3.bind(state.scratchGui.vm),
diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx
index c448df486b5861c998e7094e89ea55c12bc74fe3..f284be34e1990ee13936d1dfdc6cbcc204433faf 100644
--- a/src/containers/sprite-selector-item.jsx
+++ b/src/containers/sprite-selector-item.jsx
@@ -119,7 +119,7 @@ class SpriteSelectorItem extends React.Component {
     render () {
         const {
             /* eslint-disable no-unused-vars */
-            assetId,
+            asset,
             id,
             index,
             onClick,
diff --git a/src/containers/stage-selector.jsx b/src/containers/stage-selector.jsx
index e8f0fc9df0b2fefbb31f1c18ba52bc56f48bdc2f..08ffd3730a520da9a16a92af68fde070d8e3a1ee 100644
--- a/src/containers/stage-selector.jsx
+++ b/src/containers/stage-selector.jsx
@@ -12,6 +12,7 @@ import DragConstants from '../lib/drag-constants';
 import DropAreaHOC from '../lib/drop-area-hoc.jsx';
 import {emptyCostume} from '../lib/empty-assets';
 import sharedMessages from '../lib/shared-messages';
+import {fetchCode} from '../lib/backpack-api';
 
 import StageSelectorComponent from '../components/stage-selector/stage-selector.jsx';
 
@@ -22,7 +23,8 @@ const dragTypes = [
     DragConstants.COSTUME,
     DragConstants.SOUND,
     DragConstants.BACKPACK_COSTUME,
-    DragConstants.BACKPACK_SOUND
+    DragConstants.BACKPACK_SOUND,
+    DragConstants.BACKPACK_CODE
 ];
 
 const DroppableStage = DropAreaHOC(dragTypes)(StageSelectorComponent);
@@ -99,6 +101,12 @@ class StageSelector extends React.Component {
                 md5: dragInfo.payload.body,
                 name: dragInfo.payload.name
             }, this.props.id);
+        } else if (dragInfo.dragType === DragConstants.BACKPACK_CODE) {
+            fetchCode(dragInfo.payload.bodyUrl)
+                .then(blocks => {
+                    this.props.vm.shareBlocksToTarget(blocks, this.props.id);
+                    this.props.vm.refreshWorkspace();
+                });
         }
     }
     setFileInput (input) {
diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx
index cde8d4af41f8feb9d02b9ac101b53674b01e92c1..b48e3d232acd59d6a7428b3df89aa3e4404cd594 100644
--- a/src/containers/target-pane.jsx
+++ b/src/containers/target-pane.jsx
@@ -19,6 +19,7 @@ import {handleFileUpload, spriteUpload} from '../lib/file-uploader.js';
 import sharedMessages from '../lib/shared-messages';
 import {emptySprite} from '../lib/empty-assets';
 import {highlightTarget} from '../reducers/targets';
+import {fetchSprite, fetchCode} from '../lib/backpack-api';
 
 class TargetPane extends React.Component {
     constructor (props) {
@@ -166,8 +167,7 @@ class TargetPane extends React.Component {
         } else if (dragInfo.dragType === DragConstants.BACKPACK_SPRITE) {
             // TODO storage does not have a way of loading zips right now, and may never need it.
             // So for now just grab the zip manually.
-            fetch(dragInfo.payload.bodyUrl)
-                .then(response => response.arrayBuffer())
+            fetchSprite(dragInfo.payload.bodyUrl)
                 .then(sprite3Zip => this.props.vm.addSprite(sprite3Zip));
         } else if (targetId) {
             // Something is being dragged over one of the sprite tiles or the backdrop.
@@ -192,6 +192,12 @@ class TargetPane extends React.Component {
                     md5: dragInfo.payload.body,
                     name: dragInfo.payload.name
                 }, targetId);
+            } else if (dragInfo.dragType === DragConstants.BACKPACK_CODE) {
+                fetchCode(dragInfo.payload.bodyUrl)
+                    .then(blocks => {
+                        this.props.vm.shareBlocksToTarget(blocks, targetId);
+                        this.props.vm.refreshWorkspace();
+                    });
             }
         }
     }
@@ -231,6 +237,7 @@ class TargetPane extends React.Component {
 
 const {
     onSelectSprite, // eslint-disable-line no-unused-vars
+    onActivateBlocksTab, // eslint-disable-line no-unused-vars
     ...targetPaneProps
 } = TargetPaneComponent.propTypes;
 
diff --git a/src/lib/backpack-api.js b/src/lib/backpack-api.js
index 4a6f7f6b24ccf2e9fba256fbed97d28ccf5c8aa7..550d37d43d6bebfc7c329b4c49f62f38e2ad3490 100644
--- a/src/lib/backpack-api.js
+++ b/src/lib/backpack-api.js
@@ -4,6 +4,14 @@ import soundPayload from './backpack/sound-payload';
 import spritePayload from './backpack/sprite-payload';
 import codePayload from './backpack/code-payload';
 
+// Add a new property for the full thumbnail url, which includes the host.
+// Also include a full body url for loading sprite zips
+// TODO retreiving the images through storage would allow us to remove this.
+const includeFullUrls = (item, host) => Object.assign({}, item, {
+    thumbnailUrl: `${host}/${item.thumbnail}`,
+    bodyUrl: `${host}/${item.body}`
+});
+
 const getBackpackContents = ({
     host,
     username,
@@ -20,15 +28,7 @@ const getBackpackContents = ({
         if (error || response.statusCode !== 200) {
             return reject();
         }
-        // Add a new property for the full thumbnail url, which includes the host.
-        // Also include a full body url for loading sprite zips
-        // TODO retreiving the images through storage would allow us to remove this.
-        return resolve(response.body.map(item => (
-            Object.assign({}, item, {
-                thumbnailUrl: `${host}/${item.thumbnail}`,
-                bodyUrl: `${host}/${item.body}`
-            })
-        )));
+        return resolve(response.body.map(item => includeFullUrls(item, host)));
     });
 });
 
@@ -51,7 +51,7 @@ const saveBackpackObject = ({
         if (error || response.statusCode !== 200) {
             return reject();
         }
-        return resolve(response.body);
+        return resolve(includeFullUrls(response.body, host));
     });
 });
 
@@ -73,6 +73,22 @@ const deleteBackpackObject = ({
     });
 });
 
+// Two types of backpack items are not retreivable through storage
+// code, as json and sprite3 as arraybuffer zips.
+const fetchAs = (responseType, uri) => new Promise((resolve, reject) => {
+    xhr({uri, responseType}, (error, response) => {
+        if (error || response.statusCode !== 200) {
+            return reject();
+        }
+        return resolve(response.body);
+    });
+});
+
+// These two helpers allow easy fetching of backpack code and sprite zips
+// Use the curried fetchAs here so the consumer does not worry about XHR responseTypes
+const fetchCode = fetchAs.bind(null, 'json');
+const fetchSprite = fetchAs.bind(null, 'arraybuffer');
+
 export {
     getBackpackContents,
     saveBackpackObject,
@@ -80,5 +96,7 @@ export {
     costumePayload,
     soundPayload,
     spritePayload,
-    codePayload
+    codePayload,
+    fetchCode,
+    fetchSprite
 };
diff --git a/src/lib/backpack/block-to-image.js b/src/lib/backpack/block-to-image.js
new file mode 100644
index 0000000000000000000000000000000000000000..eac4753b3c914f33541a675055ff2da481440770
--- /dev/null
+++ b/src/lib/backpack/block-to-image.js
@@ -0,0 +1,58 @@
+import computedStyleToInlineStyle from 'computed-style-to-inline-style';
+import ScratchBlocks from 'scratch-blocks';
+
+/**
+ * Given a blockId, return a data-uri image that can be used to create a thumbnail.
+ * @param {string} blockId the ID of the block to imagify
+ * @return {Promise} resolves to a data-url of a picture of the blocks
+ */
+export default function (blockId) {
+    // Not sure any better way to access the scratch-blocks workspace than this...
+    const block = ScratchBlocks.getMainWorkspace().getBlockById(blockId);
+    const blockSvg = block.getSvgRoot().cloneNode(true /* deep */);
+
+    // Once we have the cloned SVG, do the rest in a setTimeout to prevent
+    // blocking the drag end from finishing promptly.
+    return new Promise(resolve => {
+        setTimeout(() => {
+            // Strip &nbsp; entities that cannot be inlined
+            blockSvg.innerHTML = blockSvg.innerHTML.replace(/&nbsp;/g, ' ');
+
+            // Create an <svg> element to put the cloned blockSvg inside
+            const NS = 'http://www.w3.org/2000/svg';
+            const svg = document.createElementNS(NS, 'svg');
+            svg.appendChild(blockSvg);
+
+            // Needs to be on the DOM to get CSS properties and correct sizing
+            document.body.appendChild(svg);
+
+            const padding = 10;
+            const extraHatPadding = 16;
+            const topPadding = padding + (blockSvg.getAttribute('data-shapes') === 'hat' ? extraHatPadding : 0);
+            const leftPadding = padding;
+            blockSvg.setAttribute('transform', `translate(${leftPadding} ${topPadding})`);
+
+            const bounds = blockSvg.getBoundingClientRect();
+            svg.setAttribute('width', bounds.width + (2 * padding));
+            svg.setAttribute('height', bounds.height + (2 * padding));
+
+            // We need to inline the styles set by CSS rules because
+            // not all the styles are set directly on the SVG. This makes the
+            // image styled the same way the block actually appears.
+            // TODO this doesn't handle images that are xlink:href in the SVG
+            computedStyleToInlineStyle(svg, {
+                recursive: true,
+                // Enumerate the specific properties we need to inline.
+                // Specifically properties that are set from CSS in scratch-blocks
+                properties: ['fill', 'font-family', 'font-size', 'font-weight']
+            });
+
+            const svgString = (new XMLSerializer()).serializeToString(svg);
+
+            // Once we have the svg as a string, remove it from the DOM
+            svg.parentNode.removeChild(svg);
+
+            resolve(`data:image/svg+xml;utf-8,${encodeURIComponent(svgString)}`);
+        }, 10);
+    });
+}
diff --git a/src/lib/backpack/code-payload.js b/src/lib/backpack/code-payload.js
index 6c190ea08bba98d173ca26c70eb409984142006d..3ec2e85c25671079023513c1bcb93ddd2bccc662 100644
--- a/src/lib/backpack/code-payload.js
+++ b/src/lib/backpack/code-payload.js
@@ -1,16 +1,20 @@
-import codeThumbnail from './code-thumbnail';
+import blockToImage from './block-to-image';
+import jpegThumbnail from './jpeg-thumbnail';
 
-const codePayload = code => {
+const codePayload = ({blockObjects, topBlockId}) => {
     const payload = {
         type: 'script', // Needs to match backpack-server type name
         name: 'code', // All code currently gets the same name
         mime: 'application/json',
-        body: btoa(JSON.stringify(code)), // Base64 encode the json
-        thumbnail: codeThumbnail // TODO make code thumbnail dynamic
+        body: btoa(JSON.stringify(blockObjects)) // Base64 encode the json
     };
 
-    // Return a promise to make it consistent with other payload constructors like costume-payload
-    return new Promise(resolve => resolve(payload));
+    return blockToImage(topBlockId)
+        .then(jpegThumbnail)
+        .then(thumbnail => {
+            payload.thumbnail = thumbnail.replace('data:image/jpeg;base64,', '');
+            return payload;
+        });
 };
 
 export default codePayload;
diff --git a/src/lib/backpack/code-thumbnail.js b/src/lib/backpack/code-thumbnail.js
deleted file mode 100644
index e657144bcb1c5c12a8fe99c6ffbb51a3234779df..0000000000000000000000000000000000000000
--- a/src/lib/backpack/code-thumbnail.js
+++ /dev/null
@@ -1,3 +0,0 @@
-// image/jpeg base64 encoded code thumbnail image
-// eslint-disable-next-line max-len
-export default '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';
diff --git a/src/lib/backpack/jpeg-thumbnail.js b/src/lib/backpack/jpeg-thumbnail.js
index 623daf3befdefb74a18b9ab98337484a5691c829..f0fbd28d25fe9406d8c111bba2849b4df3c53e0a 100644
--- a/src/lib/backpack/jpeg-thumbnail.js
+++ b/src/lib/backpack/jpeg-thumbnail.js
@@ -1,4 +1,4 @@
-const jpegThumbnail = dataUrl => new Promise(resolve => {
+const jpegThumbnail = dataUrl => new Promise((resolve, reject) => {
     const image = new Image();
     image.onload = () => {
         const canvas = document.createElement('canvas');
@@ -14,6 +14,9 @@ const jpegThumbnail = dataUrl => new Promise(resolve => {
         // TODO we can play with the `quality` option here to optimize file size
         resolve(canvas.toDataURL('image/jpeg', 0.92 /* quality */)); // Default quality is 0.92
     };
+    image.onerror = err => {
+        reject(err);
+    };
     image.src = dataUrl;
 });
 
diff --git a/src/lib/blocks.js b/src/lib/blocks.js
index 57283adbbc7033a20ab286719ce5b0b230df51a4..212831d0aaab7af48c4a76300403438923d9fa6e 100644
--- a/src/lib/blocks.js
+++ b/src/lib/blocks.js
@@ -220,5 +220,9 @@ export default function (vm) {
         return ScratchBlocks.StatusButtonState.NOT_READY;
     };
 
+    ScratchBlocks.FieldNote.playNote_ = function (noteNum, extensionId) {
+        vm.runtime.emit('PLAY_NOTE', noteNum, extensionId);
+    };
+
     return ScratchBlocks;
 }
diff --git a/src/lib/cloud-provider.js b/src/lib/cloud-provider.js
index ff3d34c40bc8472e4652d037fa09e82bb211bb73..9ca5424e3fb1941b94ce7bb603a76b99680afe79 100644
--- a/src/lib/cloud-provider.js
+++ b/src/lib/cloud-provider.js
@@ -79,11 +79,14 @@ class CloudProvider {
         msg.user = this.username;
         msg.project_id = this.projectId;
 
+        // Optional string params can use simple falsey undefined check
         if (dataName) msg.name = dataName;
-        if (dataValue) msg.value = dataValue;
-        if (dataIndex) msg.index = dataIndex;
         if (dataNewName) msg.new_name = dataNewName;
 
+        // Optional number params need different undefined check
+        if (typeof dataValue !== 'undefined') msg.value = dataValue;
+        if (typeof dataValue !== 'undefined') msg.index = dataIndex;
+
         const dataToWrite = JSON.stringify(msg);
         this.sendCloudData(dataToWrite);
     }
diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js
index 92748b20f61af930ede35531be858ead618d5b16..936859666aff40e8c4749299b753562ea7cadbdd 100644
--- a/src/lib/make-toolbox-xml.js
+++ b/src/lib/make-toolbox-xml.js
@@ -312,7 +312,7 @@ const sound = function (isStage, targetId) {
                 </shadow>
             </value>
         </block>
-        <block id="volume" type="sound_volume"/>
+        <block id="${targetId}_volume" type="sound_volume"/>
         ${categorySeparator}
     </category>
     `;
diff --git a/src/reducers/alerts.js b/src/reducers/alerts.js
index fefc97b37327eb2310dfa1b430afe1bfccf992cb..ee5ef0931596878c9c0cf59102921848fb2f8873 100644
--- a/src/reducers/alerts.js
+++ b/src/reducers/alerts.js
@@ -15,11 +15,14 @@ const reducer = function (state, action) {
         const newList = state.alertsList.slice();
         const newAlert = {message: action.data.message};
         const extensionId = action.data.extensionId;
+        newAlert.showReconnect = false;
         if (extensionId) { // if it's an extension
             const extension = extensionData.find(ext => ext.extensionId === extensionId);
             if (extension && extension.name) {
                 // TODO: is this the right place to assemble this message?
+                newAlert.extensionId = extensionId;
                 newAlert.message = `${newAlert.message} ${extension.name}.`;
+                newAlert.showReconnect = true;
             }
             if (extension && extension.smallPeripheralImage) {
                 newAlert.iconURL = extension.smallPeripheralImage;
diff --git a/src/reducers/project-state.js b/src/reducers/project-state.js
index 963ea9ec110f9538bd2e52eb89ac4e7bea5b7951..e264f18a48079260c7763471aebd51035ac03d51 100644
--- a/src/reducers/project-state.js
+++ b/src/reducers/project-state.js
@@ -379,8 +379,8 @@ const setProjectId = id => ({
     projectId: id
 });
 
-const requestNewProject = canCreateNew => {
-    if (canCreateNew) return {type: START_UPDATING_BEFORE_CREATING_NEW};
+const requestNewProject = needSave => {
+    if (needSave) return {type: START_UPDATING_BEFORE_CREATING_NEW};
     return {type: START_FETCHING_NEW};
 };
 
diff --git a/test/fixtures/100-100.svg b/test/fixtures/100-100.svg
new file mode 100644
index 0000000000000000000000000000000000000000..108f7aa1bc9c4c6cd0d5b4a4728ae0261d5342b7
Binary files /dev/null and b/test/fixtures/100-100.svg differ
diff --git a/test/fixtures/gh-3582-png.png b/test/fixtures/gh-3582-png.png
new file mode 100644
index 0000000000000000000000000000000000000000..cc1f1464e957dbddf5bc1aa4b8915b988bc97f0f
Binary files /dev/null and b/test/fixtures/gh-3582-png.png differ
diff --git a/test/integration/connection-modal.test.js b/test/integration/connection-modal.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..117be5ce5ed7031092fb40b8a120b68074d9cfcc
--- /dev/null
+++ b/test/integration/connection-modal.test.js
@@ -0,0 +1,66 @@
+import path from 'path';
+import SeleniumHelper from '../helpers/selenium-helper';
+
+const {
+    clickText,
+    clickXpath,
+    findByText,
+    getDriver,
+    getLogs,
+    loadUri
+} = new SeleniumHelper();
+
+const uri = path.resolve(__dirname, '../../build/index.html');
+
+let driver;
+
+// The tests below require Scratch Link to be unavailable, so we can trigger
+// an error modal. To make sure this is always true, we came up with the idea of
+// injecting javascript that overwrites the global Websocket object with one that
+// attempts to connect to a fake socket address.
+const websocketFakeoutJs = `var RealWebSocket = WebSocket;
+    WebSocket = function () {
+        return new RealWebSocket("wss://fake.fake");
+    }`;
+
+describe('Hardware extension connection modal', () => {
+    beforeAll(() => {
+        driver = getDriver();
+    });
+
+    afterAll(async () => {
+        await driver.quit();
+    });
+
+    test('Message saying Scratch Link is unavailable (BLE)', async () => {
+        await loadUri(uri);
+        await clickXpath('//button[@title="Try It"]');
+
+        await driver.executeScript(websocketFakeoutJs);
+
+        await clickXpath('//button[@title="Add Extension"]');
+
+        await clickText('micro:bit');
+        await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for modal to open
+        findByText('Scratch Link'); // Scratch Link is mentioned in the error modal
+
+        const logs = await getLogs();
+        await expect(logs).toEqual([]);
+    });
+
+    test('Message saying Scratch Link is unavailable (BT)', async () => {
+        await loadUri(uri);
+        await clickXpath('//button[@title="Try It"]');
+
+        await driver.executeScript(websocketFakeoutJs);
+
+        await clickXpath('//button[@title="Add Extension"]');
+
+        await clickText('EV3');
+        await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for modal to open
+        findByText('Scratch Link'); // Scratch Link is mentioned in the error modal
+
+        const logs = await getLogs();
+        await expect(logs).toEqual([]);
+    });
+});
diff --git a/test/integration/costumes.test.js b/test/integration/costumes.test.js
index 7aa3b969fc2e152cd461c1c649f392e5fc00dfc9..78c7b911e191c316c0c9b0bd4aa4439edafc32f5 100644
--- a/test/integration/costumes.test.js
+++ b/test/integration/costumes.test.js
@@ -129,4 +129,35 @@ describe('Working with costumes', () => {
         const logs = await getLogs();
         await expect(logs).toEqual([]);
     });
+
+    test('Adding an svg from file', async () => {
+        await loadUri(uri);
+        await clickXpath('//button[@title="Try It"]');
+        await clickText('Costumes');
+        const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
+        await driver.actions().mouseMove(el)
+            .perform();
+        await driver.sleep(500); // Wait for thermometer menu to come up
+        const input = await findByXpath('//input[@type="file"]');
+        await input.sendKeys(path.resolve(__dirname, '../fixtures/100-100.svg'));
+        await clickText('100-100', scope.costumesTab); // Name from filename
+        await clickText('100 x 100', scope.costumesTab); // Size is right
+        const logs = await getLogs();
+        await expect(logs).toEqual([]);
+    });
+
+    test('Adding a png from file (gh-3582)', async () => {
+        await loadUri(uri);
+        await clickXpath('//button[@title="Try It"]');
+        await clickText('Costumes');
+        const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
+        await driver.actions().mouseMove(el)
+            .perform();
+        await driver.sleep(500); // Wait for thermometer menu to come up
+        const input = await findByXpath('//input[@type="file"]');
+        await input.sendKeys(path.resolve(__dirname, '../fixtures/gh-3582-png.png'));
+        await clickText('gh-3582-png', scope.costumesTab);
+        const logs = await getLogs();
+        await expect(logs).toEqual([]);
+    });
 });
diff --git a/test/integration/menu-bar.test.js b/test/integration/menu-bar.test.js
index a5c4ede2b7e2b2d8d932d34bb1f257013bdf38b6..ca1a4308b7a97c72a1f824e559b00a1e64859722 100644
--- a/test/integration/menu-bar.test.js
+++ b/test/integration/menu-bar.test.js
@@ -31,25 +31,24 @@ describe('Menu bar settings', () => {
         await findByXpath('//*[li[span[text()="New"]] and not(@data-tip="tooltip")]');
     });
 
-    test('File->Save now should NOT be enabled', async () => {
+    test('File->Upload should be enabled', async () => {
         await loadUri(uri);
         await clickXpath('//button[@title="Try It"]');
         await clickXpath(
             '//div[contains(@class, "menu-bar_menu-bar-item") and ' +
             'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
         );
-        await findByXpath('//*[li[span[text()="Save now"]] and @data-tip="tooltip"]');
+        await findByXpath('//*[li[span[text()="Upload from your computer"]] and not(@data-tip="tooltip")]');
     });
 
-
-    test('File->Save as a copy should NOT be enabled', async () => {
+    test('File->Download should be enabled', async () => {
         await loadUri(uri);
         await clickXpath('//button[@title="Try It"]');
         await clickXpath(
             '//div[contains(@class, "menu-bar_menu-bar-item") and ' +
             'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
         );
-        await findByXpath('//*[li[span[text()="Save as a copy"]] and @data-tip="tooltip"]');
+        await findByXpath('//*[li[span[text()="Download to your computer"]] and not(@data-tip="tooltip")]');
     });
 
     test('Share button should NOT be enabled', async () => {
diff --git a/test/integration/sprites.test.js b/test/integration/sprites.test.js
index 83a07793dc7264c4a555c5d6d017802806003eaa..ca6a84ebaca6bdb7f5de3938e5410495e12023f6 100644
--- a/test/integration/sprites.test.js
+++ b/test/integration/sprites.test.js
@@ -74,4 +74,39 @@ describe('Working with sprites', () => {
         const logs = await getLogs();
         await expect(logs).toEqual([]);
     });
+
+    test('Adding a sprite by uploading a png', async () => {
+        await loadUri(uri);
+        await clickXpath('//button[@title="Try It"]');
+        const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
+        await driver.actions().mouseMove(el)
+            .perform();
+        await driver.sleep(500); // Wait for thermometer menu to come up
+        const input = await findByXpath('//input[@type="file"]');
+        await input.sendKeys(path.resolve(__dirname, '../fixtures/gh-3582-png.png'));
+        await clickText('gh-3582-png', scope.spriteTile);
+        const logs = await getLogs();
+        await expect(logs).toEqual([]);
+    });
+
+    // This test fails because uploading an SVG as a sprite changes the scaling
+    // Enable when this is fixed issues/3608
+    test.skip('Adding a sprite by uploading an svg (gh-3608)', async () => {
+        await loadUri(uri);
+        await clickXpath('//button[@title="Try It"]');
+        const el = await findByXpath('//button[@aria-label="Choose a Sprite"]');
+        await driver.actions().mouseMove(el)
+            .perform();
+        await driver.sleep(500); // Wait for thermometer menu to come up
+        const input = await findByXpath('//input[@type="file"]');
+        await input.sendKeys(path.resolve(__dirname, '../fixtures/100-100.svg'));
+        await clickText('100-100', scope.spriteTile); // Sprite is named for costume filename
+
+        // Check to make sure the size is right
+        await clickText('Costumes');
+        await clickText('100-100-costume1', scope.costumesTab); // The name of the costume
+        await clickText('100 x 100', scope.costumesTab); // The size of the costume
+        const logs = await getLogs();
+        await expect(logs).toEqual([]);
+    });
 });
diff --git a/test/unit/util/cloud-provider.test.js b/test/unit/util/cloud-provider.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..65ebbc06e4b50d36c6e111e6cea83ccf3679c795
--- /dev/null
+++ b/test/unit/util/cloud-provider.test.js
@@ -0,0 +1,44 @@
+import CloudProvider from '../../../src/lib/cloud-provider';
+
+// Disable window.WebSocket
+global.WebSocket = null;
+
+describe('CloudProvider', () => {
+    let cloudProvider = null;
+    let sentMessage = null;
+
+    beforeEach(() => {
+        cloudProvider = new CloudProvider();
+        // Stub connection
+        cloudProvider.connection = {
+            send: msg => {
+                sentMessage = msg;
+            }
+        };
+    });
+
+    test('updateVariable', () => {
+        cloudProvider.updateVariable('hello', 1);
+        const obj = JSON.parse(sentMessage);
+        expect(obj.method).toEqual('set');
+        expect(obj.name).toEqual('hello');
+        expect(obj.value).toEqual(1);
+    });
+
+    test('updateVariable with falsey value', () => {
+        cloudProvider.updateVariable('hello', 0);
+        const obj = JSON.parse(sentMessage);
+        expect(obj.method).toEqual('set');
+        expect(obj.name).toEqual('hello');
+        expect(obj.value).toEqual(0);
+    });
+
+    test('writeToServer with falsey index value', () => {
+        cloudProvider.writeToServer('method', 'name', 5, 0);
+        const obj = JSON.parse(sentMessage);
+        expect(obj.method).toEqual('method');
+        expect(obj.name).toEqual('name');
+        expect(obj.value).toEqual(5);
+        expect(obj.index).toEqual(0);
+    });
+});