diff --git a/jsconfig.json b/jsconfig.json
new file mode 100644
index 0000000..b2c0e02
--- /dev/null
+++ b/jsconfig.json
@@ -0,0 +1,3 @@
+{
+  "include": ["./src/**/*"]
+}
diff --git a/src/common.scss b/src/common.scss
index 74d6c3a..a87d3f2 100644
--- a/src/common.scss
+++ b/src/common.scss
@@ -38,6 +38,37 @@ form {
     }
   }
 
+  button,
+  [type='submit'] {
+    display: inline-block;
+    padding: 0.4em 1em;
+    border-radius: 1em;
+    border: 1px solid rgba(0, 0, 0, 0.2);
+    color: #000;
+    cursor: pointer;
+
+    &:hover {
+      background-color: rgba(40, 235, 70, 0.5);
+      border-color: rgba(40, 235, 70, 0.5);
+    }
+
+    &:active {
+      background-color: rgba(40, 235, 70, 0.7);
+      border-color: rgba(40, 235, 70, 0.7);
+    }
+
+    &:disabled {
+      background-color: rgba(200, 200, 200, 0.5);
+      border-color: rgba(200, 200, 200, 0.5);
+      color: rgba(100, 100, 100, 0.5);
+      cursor: not-allowed;
+    }
+  }
+
+  [type='submit'] {
+    background-color: rgba(40, 235, 70, 0.3);
+  }
+
   .buttons {
     margin-top: 0.5em;
     padding-top: 0.5em;
@@ -69,6 +100,40 @@ form {
   }
 }
 
+.help {
+  font-size: 0.9em;
+  color: #666;
+  margin-top: 0.5em;
+  margin-bottom: 1em;
+
+  a {
+    color: #888;
+    text-decoration: none;
+
+    &:hover {
+      color: #555;
+    }
+  }
+}
+
+.errors {
+  margin-top: 0.5em;
+  margin-bottom: 1em;
+
+  ul {
+    list-style-type: none;
+    padding: 0.5em;
+
+    li {
+      background: rgba(255, 200, 200, 0.3);
+      border: 1px solid rgba(255, 100, 100, 0.5);
+      border-radius: 1em;
+      margin-bottom: 0.5em;
+      padding: 0.5em;
+    }
+  }
+}
+
 .autocomplete__box {
   border: 0 !important;
   padding: 0 !important;
diff --git a/src/listeners/connect.js b/src/listeners/connect.js
index 4fe0183..dd10771 100644
--- a/src/listeners/connect.js
+++ b/src/listeners/connect.js
@@ -69,7 +69,6 @@ const Service = (() => {
       switch (message.type) {
         case 'get':
           const commands = await browser.commands.getAll();
-          console.log('Available commands', commands);
           port.postMessage(commands);
           break;
       }
diff --git a/src/options/App.vue b/src/options/App.vue
index 6037f68..81de8c8 100644
--- a/src/options/App.vue
+++ b/src/options/App.vue
@@ -3,11 +3,11 @@
     <Menu :hosts="hosts" :selectedTab="selectedTab" :selectedHost="selectedHost" :selectedHostOption="selectedHostOption" @select="select" />
 
     <div class="body">
-      <NewHost @add="addHost" v-if="selectedTab === 'add'" />
+      <HostForm @add="addHost" v-if="selectedTab === 'add'" />
       <Config v-else-if="selectedTab === 'config'" @reload="reload" />
-      <LocalCommands :host="selectedHost" v-else-if="selectedHost && selectedHostOption === 'localProc'" :bus="bus" />
+      <LocalCommands :host="selectedHost" v-else-if="selectedHost && selectedHostOption === 'actions'" :bus="bus" />
       <Run :host="hosts[selectedHost]" v-else-if="selectedHost && selectedHostOption === 'run'" :selectedAction="selectedAction" :selectedScript="selectedScript" />
-      <EditHost :host="hosts[selectedHost]" @save="editHost" @remove="removeHost" v-else-if="selectedHost" />
+      <HostForm :host="hosts[selectedHost]" @edit="editHost" @remove="removeHost" v-else-if="selectedHost" />
       <div class="none" v-else>Select an option from the menu</div>
     </div>
   </div>
@@ -17,8 +17,7 @@
 import Vue from 'vue';
 import mixins from '../utils';
 import Menu from './Menu';
-import NewHost from './NewHost';
-import EditHost from './EditHost';
+import HostForm from './HostForm';
 import LocalCommands from './LocalCommands';
 import Config from './Config';
 import Run from './Run';
@@ -28,8 +27,7 @@ export default {
   mixins: [mixins],
   components: {
     Menu,
-    NewHost,
-    EditHost,
+    HostForm,
     LocalCommands,
     Config,
     Run,
@@ -58,14 +56,20 @@ export default {
 
     async reload() {
       this.hosts = await this.getHosts();
+      this.selectedHost = this.getSelectedHost();
+      this.selectedTab = this.getSelectedTab();
+
+      switch (this.selectedTab) {
+        case 'run':
+          this.selectedHostOption = 'run';
+          break;
+        case 'actions':
+          this.selectedHostOption = 'actions';
+          break;
+      }
     },
 
     async addHost(form) {
-      if (!this.isHostFormValid(form)) {
-        this.notify('Invalid device parameter values', 'Device configuration error');
-        return;
-      }
-
       this.loading = true;
 
       try {
@@ -86,11 +90,6 @@ export default {
     },
 
     async editHost(form) {
-      if (!this.isHostFormValid(form)) {
-        this.notify('Invalid device parameter values', 'Device configuration error');
-        return;
-      }
-
       this.loading = true;
 
       try {
@@ -139,6 +138,18 @@ export default {
     },
   },
 
+  watch: {
+    selectedTab(newTab) {
+      this.setSelectedTab(newTab);
+    },
+
+    selectedHostOption(newOption) {
+      if (newOption?.length) {
+        this.setSelectedTab(newOption);
+      }
+    },
+  },
+
   created() {
     this.reload();
     this.initListeners();
diff --git a/src/options/Config.vue b/src/options/Config.vue
index 5d44703..7e70b9a 100644
--- a/src/options/Config.vue
+++ b/src/options/Config.vue
@@ -89,6 +89,9 @@ export default {
     },
 
     async reload() {
+      this.clearUrlArgs();
+      this.setSelectedTab('config');
+
       const config = await this.loadConfig();
       this.hosts = config.hosts || [];
       this.config = JSON.stringify(config, null, '  ');
diff --git a/src/options/EditHost.vue b/src/options/EditHost.vue
deleted file mode 100644
index 2c96c4f..0000000
--- a/src/options/EditHost.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-<template>
-  <div class="page edit">
-    <h2>Edit device {{ host.name }}</h2>
-    <form class="host-form" ref="form" @submit.prevent="$emit('save', $event.target)">
-      <input type="text" name="name" placeholder="Name" :value="host.name" autocomplete="off" :disabled="loading" />
-      <input type="text" name="address" placeholder="IP or hostname" :value="host.address" autocomplete="off" :disabled="loading" />
-      <input type="text" name="port" placeholder="HTTP port" autocomplete="off" :value="host.port" :disabled="loading" @keyup="onPortChange($refs.form)" />
-      <input type="text" name="websocketPort" :value="host.websocketPort" placeholder="Websocket port" autocomplete="off" :disabled="loading" />
-      <input type="text" name="token" placeholder="Access token" :value="host.token" autocomplete="off" :disabled="loading" />
-      <div class="row ssl">
-        <input type="checkbox" name="ssl" v-model="host.ssl" :disabled="loading" />
-        <label for="ssl">Use SSL</label>
-      </div>
-
-      <div class="buttons">
-        <input type="submit" value="Edit" :disabled="loading" />
-        <button type="button" @click="$emit('remove')" :disabled="loading">Remove</button>
-      </div>
-    </form>
-  </div>
-</template>
-
-<script>
-import mixins from '../utils';
-
-export default {
-  name: 'EditHost',
-  mixins: [mixins],
-  props: {
-    host: Object,
-  },
-};
-</script>
-
-<style lang="scss" scoped>
-form {
-  input[type='text'] {
-    .row.ssl {
-      display: flex;
-      align-items: center;
-    }
-  }
-}
-</style>
-
-<!-- vim:sw=2:ts=2:et: -->
diff --git a/src/options/HostForm.vue b/src/options/HostForm.vue
new file mode 100644
index 0000000..ca6a4d9
--- /dev/null
+++ b/src/options/HostForm.vue
@@ -0,0 +1,297 @@
+<template>
+  <div class="page" :class="{ action }">
+    <h2 v-if="action === 'add'">Add a Platypush device</h2>
+    <h2 v-else>Edit device {{ host.name || host.url }}</h2>
+
+    <p class="help">
+      You can bind a new Platypush device to the extension by specifying its name and base URL.<br /><br />
+      The Platypush service needs to have <code>backend.http</code> enabled and be reachable from the browser.<br /><br />
+
+      <b>Note:</b> If you want to connect to an HTTP-only Platypush service, you may need to disable HTTPS-only mode in your browser - or add an exception for the path of this
+      extension.<br />
+    </p>
+
+    <form class="host-form" ref="form" @submit.prevent="submit">
+      <label for="url">
+        <b>Service URL</b>
+
+        <p class="help">The base URL of the Platypush service, e.g. <code>http://localhost:8008</code>.<br /></p>
+
+        <input type="text" id="url" name="url" ref="url" placeholder="Base URL" autocomplete="off" :disabled="loading" @keyup="onUrlInput" @blur="onUrlInput" />
+      </label>
+
+      <label for="name">
+        <i>Device name</i>
+
+        <p class="help">
+          A human-readable name for the device, e.g. <code>My Platypush device</code>.<br />
+          If left empty, the device name will be retrieved from the Platypush service configuration.<br />
+        </p>
+        <input type="text" id="name" name="name" placeholder="Name" autocomplete="off" :disabled="loading" />
+      </label>
+
+      <label for="token">
+        <i>Access token</i>
+
+        <p class="help">
+          An optional access token to authenticate with the Platypush service.<br />
+          You can generate a token from the Platypush web interface, under <code>Settings &gt; Access tokens</code>.<br />
+          If left empty, the service will use the credentials of the currently logged-in user.<br />
+        </p>
+        <input type="text" id="token" name="token" placeholder="Access token" autocomplete="off" :disabled="loading" />
+      </label>
+
+      <div class="buttons" v-if="action === 'add'">
+        <input type="submit" value="Add" :disabled="loading || !formValid" />
+      </div>
+
+      <div class="buttons" v-else-if="action === 'edit'">
+        <input type="submit" value="Edit" :disabled="loading || !formValid" />
+        <button type="button" @click="$emit('remove')" :disabled="loading">Remove</button>
+      </div>
+    </form>
+
+    <div class="errors" v-if="errors.length">
+      <h3>Errors</h3>
+      <ul>
+        <li class="error" v-for="(error, index) in errors" :key="index">{{ error }}</li>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+import mixins from '../utils';
+
+export default {
+  mixins: [mixins],
+  emits: ['add', 'edit', 'remove'],
+  props: {
+    host: {
+      type: Object,
+    },
+  },
+
+  data() {
+    return {
+      errors: [],
+      formValid: false,
+    };
+  },
+
+  computed: {
+    action() {
+      return Object.keys(this.host || {}).length ? 'edit' : 'add';
+    },
+
+    values() {
+      return {
+        url: this.$refs.form?.url?.value?.trim() || '',
+        name: this.$refs.form?.name?.value?.trim() || '',
+        token: this.$refs.form?.token?.value?.trim() || '',
+      };
+    },
+  },
+
+  methods: {
+    async submit(event) {
+      this.loading = true;
+      this.errors = [];
+      const form = event.target;
+
+      try {
+        this.errors = await this.validateForm(form);
+        if (this.errors.length > 0) {
+          return;
+        }
+
+        this.$emit(this.action, form);
+      } catch (error) {
+        this.errors.push('An error occurred while adding the device: ' + error.message);
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    async validateForm(form) {
+      const errors = [];
+      let url = null;
+
+      try {
+        url = new URL(form.url.value);
+      } catch (e) {
+        errors.push('Invalid URL format');
+      }
+
+      if (url?.protocol !== 'http:' && url?.protocol !== 'https:') {
+        errors.push('URL must start with http:// or https://');
+      } else if (url?.origin) {
+        form.url.value = url.origin;
+      } else {
+        return errors;
+      }
+
+      try {
+        const config = await this.getConfig(url.origin);
+        if (!form.name.value?.length) {
+          form.name.value = config?.device_id || url.hostname;
+        }
+      } catch (e) {
+        errors.push('Failed to fetch device configuration: ' + e.message);
+      }
+
+      return errors;
+    },
+
+    async getConfig(url) {
+      let response = {};
+
+      try {
+        response = await axios({
+          method: 'post',
+          url: url + '/execute',
+          timeout: 5000,
+          data: {
+            type: 'request',
+            action: 'config.get',
+          },
+          headers: {
+            'Content-Type': 'application/json',
+            ...(this.values.token.length && { Authorization: `Bearer ${this.values.token}` }),
+          },
+        });
+
+        this.validateResponse(response);
+        return response.data.response.output;
+      } catch (e) {
+        if (e.response?.status === 401) {
+          // If the access token is set, then it's likely invalid - prompt the user to re-enter it
+          if (this.values.token.length) {
+            throw new Error('Invalid access token. Please check the token and try again.');
+          }
+
+          // Otherwise, the user is not authenticated to the service on this browser - redirect to the login page
+          window.open(`${url}/login?redirect=${encodeURIComponent(window.location.href)}`, '_blank');
+          throw new Error('You are not authenticated to the Platypush service on this host. Please log in and then try again.');
+        }
+
+        throw new Error(`Failed to fetch device configuration: ${e.message}`);
+      }
+    },
+
+    validateResponse(response) {
+      const errors = response.data.response.errors;
+      if (errors && errors.length) {
+        this.errors.push(...errors);
+        throw errors[0];
+      }
+    },
+
+    onUrlInput(event) {
+      const url = (event?.target?.value || this.$refs.form?.url?.value || '').trim();
+      if (!url.length) {
+        this.formValid = false;
+        return;
+      }
+
+      try {
+        new URL(url);
+        this.formValid = true;
+      } catch (e) {
+        this.formValid = false;
+      }
+    },
+
+    parseUrlArgs() {
+      const args = this.getUrlArgs();
+      if (args.url) {
+        this.$refs.form.url.value = args.url;
+      }
+
+      if (args.name) {
+        this.$refs.form.name.value = args.name;
+      }
+
+      if (args.token) {
+        this.$refs.form.token.value = args.token;
+      }
+    },
+
+    onHostChange() {
+      const action = this.host?.name ? 'edit' : 'add';
+      this.parseUrlArgs();
+      this.clearUrlArgs();
+      this.setSelectedTab(action);
+      this.setSelectedHost(this.host?.name);
+
+      if (!this.$refs.form?.url) {
+        return;
+      }
+
+      if (this.host?.url) {
+        this.$refs.form.url.value = this.host.url;
+      } else if (this.host?.name) {
+        this.$refs.form.url.value = (this.host.ssl ? 'https://' : 'http://') + this.host.address + (this.host.port || this.host.port?.length ? `:${this.host.port}` : '');
+      } else {
+        this.$refs.form.url.value = '';
+      }
+
+      if (this.host) {
+        this.$refs.form.name.value = this.host.name || '';
+        this.$refs.form.token.value = this.host.token || '';
+      }
+
+      this.formValid = true;
+    },
+  },
+
+  mounted() {
+    const action = this.getSelectedTab();
+    this.parseUrlArgs();
+    this.clearUrlArgs();
+    this.setSelectedTab(action);
+    this.setUrlArgs({
+      url: this.values.url,
+      name: this.values.name,
+      token: this.values.token,
+    });
+
+    if (action === 'edit') {
+      this.$nextTick(() => {
+        this.onHostChange();
+      });
+    } else if (this.values.url.length) {
+      this.onUrlInput();
+      this.$refs.form.requestSubmit();
+    } else {
+      this.formValid = false;
+    }
+
+    this.$nextTick(() => {
+      this.$refs?.form?.url?.focus();
+    });
+  },
+
+  watch: {
+    errors: {
+      handler(errors, oldErrors) {
+        errors
+          .filter(error => !oldErrors.includes(error))
+          ?.forEach(error => {
+            console.error(error);
+            this.notify(error, 'Device configuration error');
+          });
+      },
+    },
+
+    host: {
+      handler() {
+        this.onHostChange();
+      },
+    },
+  },
+};
+</script>
+
+<!-- vim:sw=2:ts=2:et: -->
diff --git a/src/options/Menu.vue b/src/options/Menu.vue
index 1a409bd..41f9608 100644
--- a/src/options/Menu.vue
+++ b/src/options/Menu.vue
@@ -54,7 +54,7 @@ export default {
   computed: {
     hostOptions() {
       return {
-        localProc: {
+        actions: {
           displayName: 'Stored Actions',
           iconClass: 'fas fa-puzzle-piece',
         },
diff --git a/src/options/NewHost.vue b/src/options/NewHost.vue
deleted file mode 100644
index 8e44277..0000000
--- a/src/options/NewHost.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<template>
-  <div class="page add">
-    <h2>Add a new device</h2>
-    <form class="host-form" ref="form" @submit.prevent="$emit('add', $event.target)">
-      <input type="text" name="name" placeholder="Name" autocomplete="off" :disabled="loading" />
-      <input type="text" name="address" placeholder="IP or hostname" @keyup="onAddrChange($refs.form)" autocomplete="off" :disabled="loading" />
-      <input type="text" name="port" value="8008" placeholder="HTTP port" @keyup="onPortChange($refs.form)" autocomplete="off" :disabled="loading" />
-      <input type="text" name="websocketPort" value="8009" placeholder="Websocket port" autocomplete="off" :disabled="loading" />
-      <input type="text" name="token" placeholder="Access token" autocomplete="off" :disabled="loading" />
-      <div class="row ssl">
-        <input type="checkbox" name="ssl" :disabled="loading" />
-        <label for="ssl">Use SSL</label>
-      </div>
-
-      <div class="buttons">
-        <input type="submit" value="Add" :disabled="loading" />
-      </div>
-    </form>
-  </div>
-</template>
-
-<script>
-import mixins from '../utils';
-
-export default {
-  name: 'NewHost',
-  mixins: [mixins],
-};
-</script>
-
-<style lang="scss" scoped>
-form {
-  input[type='text'] {
-    .row.ssl {
-      display: flex;
-      align-items: center;
-    }
-  }
-}
-</style>
-
-<!-- vim:sw=2:ts=2:et: -->
diff --git a/src/popup/App.vue b/src/popup/App.vue
index 680e37c..d6562e1 100644
--- a/src/popup/App.vue
+++ b/src/popup/App.vue
@@ -2,7 +2,7 @@
   <div class="container">
     <div class="no-hosts" v-if="!(hosts && Object.keys(hosts).length)">
       No devices found. Click
-      <a href="/options/options.html" target="_blank">here</a> to configure the extension.
+      <a href="/options/options.html#view=add" target="_blank">here</a> to configure the extension.
     </div>
 
     <div class="main" v-else>
diff --git a/src/utils.js b/src/utils.js
index b6f1696..0ab6a46 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -482,28 +482,21 @@ export default {
     },
 
     formToHost(form) {
-      return {
-        name: form.name.value,
-        address: form.address.value,
-        port: parseInt(form.port.value),
-        websocketPort: parseInt(form.websocketPort.value),
-        ssl: form.ssl.checked,
-        token: form.token.value,
-      };
-    },
-
-    onAddrChange(form) {
-      if (form.name.value.length && !form.address.value.startsWith(form.name.value)) {
-        return;
+      const url = new URL(form.url.value.trim());
+      const ssl = url.protocol === 'https:';
+      let port = parseInt(url.port);
+      if (!this.isPortValid(port)) {
+        port = ssl ? 443 : 80;
       }
 
-      form.name.value = form.address.value;
-    },
-
-    onPortChange(form) {
-      const port = form.port.value;
-      if (!this.isPortValid(port)) return;
-      form.websocketPort.value = '' + (parseInt(port) + 1);
+      return {
+        name: form.name.value,
+        address: url.hostname,
+        port: port,
+        websocketPort: port,
+        ssl: ssl,
+        token: form.token.value,
+      };
     },
 
     isPortValid(port) {
@@ -511,8 +504,55 @@ export default {
       return !isNaN(port) && port > 0 && port < 65536;
     },
 
-    isHostFormValid(form) {
-      return form.name.value.length && form.address.value.length && this.isPortValid(form.port.value) && this.isPortValid(form.websocketPort.value);
+    getUrlArgs() {
+      const hash = window.location.hash.slice(1);
+      const args = {};
+      if (!hash) {
+        return args;
+      }
+
+      const parts = hash.split('&');
+      parts.forEach(part => {
+        const [key, value] = part.split('=');
+        if (key && value) {
+          args[key] = decodeURIComponent(value);
+        }
+      });
+
+      return args;
+    },
+
+    setUrlArgs(args) {
+      const hash = Object.entries({ ...this.getUrlArgs(), ...args })
+        .filter(([key, value]) => key && value != null && (typeof value !== 'string' || value.length))
+        .map(([key, value]) => `${key}=${encodeURIComponent(value.toString())}`)
+        .join('&');
+
+      window.location.hash = hash;
+    },
+
+    clearUrlArgs() {
+      window.location.hash = '';
+    },
+
+    getSelectedTab() {
+      return this.getUrlArgs().view;
+    },
+
+    getSelectedHost() {
+      return this.getUrlArgs().host;
+    },
+
+    setSelectedHost(host) {
+      const args = this.getUrlArgs();
+      args.host = host;
+      this.setUrlArgs(args);
+    },
+
+    setSelectedTab(tab) {
+      const args = this.getUrlArgs();
+      args.view = tab;
+      this.setUrlArgs(args);
     },
   },
 };