Files
kandev/apps/web/src/app-shell.tsx
T
Clément Fiere f85b648d97 feat: notify user when a kandev update is detected (#1924)
* feat: notify user when a kandev update is detected

Adds a background-poller-driven update notification with a saved
preference for delivery channel (desktop / in-app banner / both) and
an enable/disable switch on the Updates settings page.

Backend:
- internal/system/updates: NotifySettings + NotifyStore persist the
  enable/channel preference on the existing install-wide settings
  table; notifyIfNewUpdate broadcasts a WS event the first time a
  newer release is observed, deduped durably via kandev_meta so a
  restart or repeat poll doesn't re-announce the same version.
- New GET/PUT /api/v1/system/updates/notification-settings endpoints.
- pkg/websocket: new system.update_available action.

Desktop (Tauri):
- native_notifications.rs: task_id is now optional (Option<String>,
  required only for session-scoped events) and the event allowlist
  accepts system.update_available: alongside the existing session
  events, so the update notification can use the native bridge.

Frontend:
- New UpdateNotificationsCard on the Updates settings page (Switch +
  channel Select), wired through the shared settings-save-provider
  floating Save/Discard action.
- useUpdateAvailableToast bridges the WS event to an in-app toast
  and/or the native/browser desktop notification, per the saved
  channel, deduped per version.

* docs: document the update notification channel setting

* fix: hydrate update notification settings and broadcast delivery ack

- Only mark a release notification as delivered when the broadcaster
  reports at least one connected client, closing a race where an
  update could be silently dropped before the first client connects.
- Hydrate updateNotificationSettings through the settings boot payload
  (loadSettingsInitialState -> hydrateState) instead of a component-
  level fetch, per the boot-payload -> store -> component data flow.
  Widen the hydration types (HydrationState) so callers can hydrate a
  partial system slice without fabricating the other system fields.
  UpdateNotificationsCard now only reads/writes the subscribed store
  state.
- Fix module-scope settings-fetch cache leaking across tests in
  use-update-available-toast.test.ts so the fetch-failure test
  actually exercises the catch/fallback path.
- Add API-client contract tests for fetchUpdateNotificationSettings
  and saveUpdateNotificationSettings (GET cache policy, PUT method
  and body).

* test: cover delayed hydration and boot-payload mapping for update notifications
2026-07-24 22:40:24 +01:00

61 lines
2.7 KiB
TypeScript

import { AppSidebar } from "@/components/app-sidebar/app-sidebar";
import { AppStatusSurfaceProvider } from "@/components/app-status-bar/app-status-surface-provider";
import { CommandPanel } from "@/components/command-panel";
import { ConfigChatProvider } from "@/components/config-chat/config-chat-provider";
import { DiffWorkerPoolProvider } from "@/components/diff-worker-pool-provider";
import { DesktopCommandHost } from "@/components/desktop-command-host";
import { GlobalCommands } from "@/components/global-commands";
import { LogBufferBridge } from "@/components/log-buffer-bridge";
import { QuickChatProvider } from "@/components/quick-chat/quick-chat-provider";
import { RecentTaskSwitcher } from "@/components/task/recent-task-switcher";
import { SessionFailureToastBridge } from "@/components/session-failure-toast-bridge";
import { TaskDeletedToastBridge } from "@/components/task-deleted-toast-bridge";
import { UpdateAvailableToastBridge } from "@/components/update-available-toast-bridge";
import { SidebarViewsSyncBridge } from "@/components/sidebar-views-sync-bridge";
import { ThemeProvider } from "@/components/theme-provider";
import { ToastProvider } from "@/components/toast-provider";
import { WebSocketConnector } from "@/components/ws-connector";
import { CommandRegistryProvider } from "@/lib/commands/command-registry";
import { Toaster as SonnerToaster } from "@kandev/ui/sonner";
import { TooltipProvider } from "@kandev/ui/tooltip";
type AppShellProps = {
children: React.ReactNode;
};
export function AppShell({ children }: AppShellProps) {
return (
<ThemeProvider>
<DiffWorkerPoolProvider>
<TooltipProvider>
<ToastProvider>
<SonnerToaster richColors position="top-right" />
<SessionFailureToastBridge />
<TaskDeletedToastBridge />
<UpdateAvailableToastBridge />
<SidebarViewsSyncBridge />
<LogBufferBridge />
<CommandRegistryProvider>
<DesktopCommandHost />
<WebSocketConnector />
<GlobalCommands />
<CommandPanel />
<RecentTaskSwitcher />
<ConfigChatProvider>
<QuickChatProvider>
<AppStatusSurfaceProvider>
<div className="flex min-h-0 flex-1 overflow-hidden">
<AppSidebar />
<main className="flex min-h-0 min-w-0 flex-1 flex-col">{children}</main>
</div>
</AppStatusSurfaceProvider>
</QuickChatProvider>
</ConfigChatProvider>
</CommandRegistryProvider>
</ToastProvider>
</TooltipProvider>
</DiffWorkerPoolProvider>
</ThemeProvider>
);
}