Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/renderer/src/assets/ds-components.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
60 changes: 45 additions & 15 deletions src/renderer/src/components/BackendPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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()
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/renderer/src/components/StatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ function NotificationBell(): React.JSX.Element {
<div className="notif__itembody">
<div className="notif__title">{n.title}</div>
{n.msg && <div className="notif__msg">{n.msg}</div>}
{n.link && (
<a className="notif__link" href={n.link.href} target="_blank" rel="noreferrer">
{n.link.label}
</a>
)}
{n.code && <code className="notif__code">{n.code}</code>}
</div>
</div>
Expand Down
22 changes: 20 additions & 2 deletions src/renderer/src/redux/notificationsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
}
Expand Down
Loading