diff --git a/neode-ui/public/assets/img/companion-phone-screen.webp b/neode-ui/public/assets/img/companion-phone-screen.webp
new file mode 100644
index 00000000..a8153380
Binary files /dev/null and b/neode-ui/public/assets/img/companion-phone-screen.webp differ
diff --git a/neode-ui/src/components/CompanionIntroOverlay.vue b/neode-ui/src/components/CompanionIntroOverlay.vue
index 5252b090..80730a43 100644
--- a/neode-ui/src/components/CompanionIntroOverlay.vue
+++ b/neode-ui/src/components/CompanionIntroOverlay.vue
@@ -59,6 +59,7 @@
class="flex-1 inline-flex items-center justify-center rounded-lg bg-orange-500/20 border border-orange-500/30 px-3 py-2.5 text-sm font-medium text-orange-400 hover:bg-orange-500/30 transition-colors text-center"
target="_blank"
rel="noopener noreferrer"
+ download
>
Download app
@@ -132,6 +133,7 @@
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as QRCode from 'qrcode'
import { IS_DEMO, DEMO_PASSWORD } from '@/composables/useDemoIntro'
+import { companionIntroRequested } from '@/composables/useCompanionIntro'
import { useLoginTransitionStore } from '@/stores/loginTransition'
import { rpcClient } from '@/api/rpc-client'
@@ -202,6 +204,18 @@ function maybeShow() {
}, 250)
}
+// Manual open (App Store banner etc.) — ignores the once-per-browser gate.
+watch(companionIntroRequested, (requested) => {
+ if (!requested) return
+ companionIntroRequested.value = false
+ if (calmTicker) {
+ clearInterval(calmTicker)
+ calmTicker = null
+ }
+ step.value = 'download'
+ visible.value = true
+})
+
watch(visible, async (isVisible) => {
if (!isVisible) return
qrDataUrl.value = await QRCode.toDataURL(companionDownloadUrl, {
diff --git a/neode-ui/src/composables/useCompanionIntro.ts b/neode-ui/src/composables/useCompanionIntro.ts
new file mode 100644
index 00000000..b05867c0
--- /dev/null
+++ b/neode-ui/src/composables/useCompanionIntro.ts
@@ -0,0 +1,13 @@
+import { ref } from 'vue'
+
+/**
+ * Cross-view trigger for the Remote Companion intro/pairing modal
+ * (CompanionIntroOverlay, mounted once in Dashboard.vue). Views like the
+ * App Store banner call openCompanionIntro() to pop it on demand — this
+ * bypasses the once-per-browser auto-show gate.
+ */
+export const companionIntroRequested = ref(false)
+
+export function openCompanionIntro(): void {
+ companionIntroRequested.value = true
+}
diff --git a/neode-ui/src/views/Discover.vue b/neode-ui/src/views/Discover.vue
index e23b6b5f..d26bf257 100644
--- a/neode-ui/src/views/Discover.vue
+++ b/neode-ui/src/views/Discover.vue
@@ -150,6 +150,9 @@
+
+