diff --git a/src/renderer/src/assets/ds-components.css b/src/renderer/src/assets/ds-components.css index a13e535..2789ee6 100644 --- a/src/renderer/src/assets/ds-components.css +++ b/src/renderer/src/assets/ds-components.css @@ -715,3 +715,22 @@ word-break: break-all; user-select: text; } + +/* ── notification action link (e.g. "Download tinyService") ───────────────── + A small button-styled anchor for the primary action of a notification. */ +.notif__link { + display: inline-block; + margin-top: 8px; + padding: 6px 12px; + border-radius: var(--radius-sm); + background: var(--brand); + color: var(--text-on-brand, #fff); + font-size: 12px; + font-weight: 600; + text-decoration: none; +} +.notif__link:hover { + filter: brightness(1.1); +} +htness(1.1); +} diff --git a/src/renderer/src/components/BackendPrompt.tsx b/src/renderer/src/components/BackendPrompt.tsx index 60f5cbe..7614ff2 100644 --- a/src/renderer/src/components/BackendPrompt.tsx +++ b/src/renderer/src/components/BackendPrompt.tsx @@ -6,6 +6,10 @@ // a persistent notification into the status-bar bell so it's easy to find and // stays until the user clears it. Never shown in the Electron build (which // auto-starts the backend). Renders nothing — it's a headless watcher. +// +// On Windows the primary path is the one-click tray-agent installer published +// from the tinyService repo (stable latest-release URL). Elsewhere — and as a +// fallback for developers — we still show the npx command. import { useEffect, useRef } from 'react' import { toast } from 'sonner' @@ -14,6 +18,10 @@ import { getArduinoService } from '@renderer/services/arduino/ArduinoServiceFact import { addNotification, useAppDispatch } from '@renderer/redux' const INSTALL_CMD = 'npx @mister-industries/tinyservice' +const DOWNLOAD_URL = + 'https://github.com/Mister-Industries/tinyService/releases/latest/download/tinyService-Setup.exe' + +const isWindows = navigator.userAgent.includes('Windows') export function BackendPrompt(): null { const dispatch = useAppDispatch() @@ -27,22 +35,44 @@ export function BackendPrompt(): null { const announce = (): void => { if (pushed.current || service.isConnected()) return pushed.current = true - const msg = - 'Compiling, uploading, and the serial monitor run through a small local backend. Run this command in a terminal — the app reconnects automatically. Use Chrome or Edge to allow the local connection from a hosted page.' - dispatch( - addNotification({ - tone: 'warn', - title: 'Start tinyService to build & flash', - msg, - code: INSTALL_CMD + + if (isWindows) { + const msg = + 'Compiling, uploading, and the serial monitor run through tinyService, a small app that lives in your system tray. Install it once — the app reconnects automatically.' + dispatch( + addNotification({ + tone: 'warn', + title: 'Install tinyService to build & flash', + msg, + link: { href: DOWNLOAD_URL, label: 'Download tinyService for Windows' }, + code: `Already have Node? You can also run: ${INSTALL_CMD}` + }) + ) + // Persistent (duration: Infinity) since it's an actionable prompt. + toast.warning('Install tinyService to build & flash', { + description: msg, + duration: Infinity, + action: { + label: 'Download', + onClick: () => window.open(DOWNLOAD_URL, '_blank') + } + }) + } else { + const msg = + 'Compiling, uploading, and the serial monitor run through a small local backend. Run this command in a terminal — the app reconnects automatically. Use Chrome or Edge to allow the local connection from a hosted page.' + dispatch( + addNotification({ + tone: 'warn', + title: 'Start tinyService to build & flash', + msg, + code: INSTALL_CMD + }) + ) + toast.warning('Start tinyService to build & flash', { + description: `${msg}\n\n${INSTALL_CMD}`, + duration: Infinity }) - ) - // Also surface a toast so it's seen immediately, not just in the bell. - // Persistent (duration: Infinity) since it's an actionable prompt. - toast.warning('Start tinyService to build & flash', { - description: `${msg}\n\n${INSTALL_CMD}`, - duration: Infinity - }) + } } // Assume connected during the initial grace period so we don't announce diff --git a/src/renderer/src/components/StatusBar.tsx b/src/renderer/src/components/StatusBar.tsx index a3e9b82..f197f93 100644 --- a/src/renderer/src/components/StatusBar.tsx +++ b/src/renderer/src/components/StatusBar.tsx @@ -126,6 +126,11 @@ function NotificationBell(): React.JSX.Element {
diff --git a/src/renderer/src/redux/notificationsSlice.ts b/src/renderer/src/redux/notificationsSlice.ts index a9aa6d2..b1d3b91 100644 --- a/src/renderer/src/redux/notificationsSlice.ts +++ b/src/renderer/src/redux/notificationsSlice.ts @@ -3,12 +3,18 @@ import { createAppSlice } from './createAppSlice' export type NotificationTone = 'ok' | 'error' | 'warn' | 'info' +export type NotificationLink = { + href: string + label: string +} + export type Notification = { id: string tone: NotificationTone title: string msg?: string code?: string + link?: NotificationLink at: number } @@ -30,14 +36,26 @@ export const notificationsSlice = createAppSlice({ initialState, reducers: (create) => ({ addNotification: create.reducer( - (state, { payload }: PayloadAction<{ tone: NotificationTone; title: string; msg?: string; code?: string }>) => { + ( + state, + { + payload + }: PayloadAction<{ + tone: NotificationTone + title: string + msg?: string + code?: string + link?: NotificationLink + }> + ) => { state.items.unshift({ id: nanoid(), at: Date.now(), tone: payload.tone, title: payload.title, msg: payload.msg, - code: payload.code + code: payload.code, + link: payload.link }) if (state.items.length > MAX) state.items.length = MAX }