Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { projectActions } from './features/project/projectSlice';
import { statusActions } from './features/status/statusSlice';
import { useApp } from './hooks/useApp';
import { useGlobalKeyboardShortcuts } from './hooks/useGlobalKeyboardShortcuts';
import { useNativeNotifications } from './hooks/useNativeNotifications';
import { usePushToTalk } from './hooks/usePushToTalk';
import { useTranslation } from './hooks/useTranslation';
import { runCommandById } from './services/commands/commandBuilder';
Expand Down Expand Up @@ -481,6 +482,9 @@ const App: FC<AppProps> = ({ isNewUser }) => {
// QNBS-v3: Push-to-Talk voice activation when configured
usePushToTalk();

// QNBS-v3 (T3): bootstrap native notification permission when the setting is on. No-op on the web.
useNativeNotifications();

const executeCommand = useCallback(
(id: string) =>
runCommandById(id, {
Expand Down
26 changes: 26 additions & 0 deletions app/listenerMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { TypedStartListening } from '@reduxjs/toolkit';
import { createListenerMiddleware, isRejected } from '@reduxjs/toolkit';
import { analyticsActions } from '../features/analytics/analyticsSlice';
// QNBS-v3: canonical selector — replaces a local type that misapplied the persisted-state shape to the live store
import { proForgeActions } from '../features/proForge/proForgeSlice';
import { selectProjectData } from '../features/project/projectSelectors';
import type { ProjectData } from '../features/project/projectSlice';
import { statusActions } from '../features/status/statusSlice';
Expand Down Expand Up @@ -39,6 +40,7 @@ export { isAnalyticsPersistenceAllowed };
// 6. Cross-Project — search index updated on save (always-on; promoted from enableCrossProjectSearch flag)
// 7. WorkerBus v2 — init/shutdown pools on enableWorkerBusV2 flag change (Phase 2)
// 8. Rust Compute — invalidate Rust availability cache on enableRustCompute toggle (Phase 2)
// 9. Desktop Notify — native OS notification on ProForge stageCompleted (Phase 2 / T3)
//
// All AI inference side effects (local/cloud) are intentionally NOT in this middleware —
// they belong in service-layer thunks (aiProviderService, localAiFacade) to keep the
Expand Down Expand Up @@ -538,6 +540,30 @@ listenerMiddleware.startListening({
},
});

// QNBS-v3 (T3): Desktop Notifications — ProForge stage ready for review.
// `stageCompleted` is the one long-running "AI generation finished in the background" signal in
// this app (pipeline stages can run for minutes), so the user may have tabbed away. No-op unless
// running in Tauri with the opt-in `desktop.desktopNotifications` setting on (checked inside
// `sendDesktopNotification` + here). Body text is not translated — matches the existing
// "Storage Nearly Full" notification precedent above; a static (non-hook) i18n accessor for
// middleware-dispatched strings is tracked as follow-up work, not part of this listener.
listenerMiddleware.startListening({
actionCreator: proForgeActions.stageCompleted,
effect: async (action, listenerApi) => {
const state = listenerApi.getState() as RootState;
if (!state.settings.desktop?.desktopNotifications) return;
Comment thread
qnbs marked this conversation as resolved.
Outdated
try {
const { sendDesktopNotification } = await import('../services/desktop/desktopNotifications');
await sendDesktopNotification(
'ProForge Pipeline',
`Stage "${action.payload.stage}" is ready for review.`,
);
Comment thread
qnbs marked this conversation as resolved.
Outdated
} catch (err) {
logger.warn('ProForge desktop notification failed', err);
}
},
});

/**
* QNBS-v3: Phase 2 — Cold-start WorkerBus v2 init. Mirrors initAdaptiveAiOnStartup:
* listeners only fire on transitions, so if the flag was already true in persisted state
Expand Down
9 changes: 9 additions & 0 deletions components/settings/DataSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ export const DataSection: FC = () => {
description: t('settings.data.libraryExport.successBody'),
}),
);
if (settings.desktop?.desktopNotifications) {
void import('../../services/desktop/desktopNotifications').then(
({ sendDesktopNotification }) =>
sendDesktopNotification(
t('settings.data.libraryExport.successTitle'),
t('settings.data.libraryExport.successBody'),
),
);
Comment thread
qnbs marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
Comment thread
qnbs marked this conversation as resolved.
setLibraryModalOpen(false);
setLibraryPassphrase('');
} catch {
Expand Down
14 changes: 14 additions & 0 deletions components/settings/DesktopSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export const DesktopSection: FC = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const minimizeToTray = useAppSelector((s) => s.settings.desktop?.minimizeToTray ?? false);
// QNBS-v3 (T3): opt-in native OS notifications for background-task completion.
const desktopNotifications = useAppSelector(
(s) => s.settings.desktop?.desktopNotifications ?? false,
);

if (!isTauriRuntime()) return null;

Expand All @@ -33,6 +37,16 @@ export const DesktopSection: FC = () => {
dispatch(settingsActions.setDesktopSettings({ minimizeToTray: checked }))
}
/>
{/* QNBS-v3 (T3): permission is requested lazily by useNativeNotifications() the next time
this flips true — no permission prompt just from opening Settings. */}
<ToggleSwitch
label={t('desktop.settings.desktopNotifications')}
hint={t('desktop.settings.desktopNotificationsHint')}
checked={desktopNotifications}
onChange={(checked) =>
dispatch(settingsActions.setDesktopSettings({ desktopNotifications: checked }))
}
/>
</CardContent>
</Card>
);
Expand Down
1 change: 1 addition & 0 deletions features/settings/settingsDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Settings } from '../../types';

export const defaultDesktopSettings: Settings['desktop'] = {
minimizeToTray: false,
desktopNotifications: false,
Comment thread
qnbs marked this conversation as resolved.
Comment thread
qnbs marked this conversation as resolved.
};

export const defaultVoiceSettings: Settings['voice'] = {
Expand Down
12 changes: 12 additions & 0 deletions hooks/useExportView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTransientUiStore } from '../app/transientUiStore';
import { selectAllCharacters, selectAllWorlds } from '../features/project/projectSelectors';
import { generateSynopsisThunk } from '../features/project/thunks/writingThunks';
import { statusActions } from '../features/status/statusSlice';
import { sendDesktopNotification } from '../services/desktop/desktopNotifications';
import { useTranslation } from './useTranslation';

type Format = 'md' | 'txt' | 'pdf' | 'docx' | 'epub' | 'norm-txt';
Expand Down Expand Up @@ -34,6 +35,10 @@ export const useExportView = () => {
const dispatch = useAppDispatch();
const projectState = useAppSelector((state) => state.project.present);
const project = projectState.data;
// QNBS-v3 (T3): gate the post-export native notification behind the opt-in desktop setting.
const desktopNotificationsEnabled = useAppSelector(
(state) => state.settings.desktop?.desktopNotifications ?? false,
Comment thread
qnbs marked this conversation as resolved.
);
const characters = useAppSelector(selectAllCharacters);
const worlds = useAppSelector(selectAllWorlds);

Expand Down Expand Up @@ -376,6 +381,12 @@ export const useExportView = () => {
a.click();
URL.revokeObjectURL(url);
}
if (desktopNotificationsEnabled) {
void sendDesktopNotification(
t('export.notify.completeTitle'),
t('export.notify.completeBody'),
);
Comment thread
qnbs marked this conversation as resolved.
Outdated
}
} catch (error) {
dispatch(
statusActions.addNotification({
Expand All @@ -393,6 +404,7 @@ export const useExportView = () => {
downloadPdf,
downloadDocx,
downloadEpub,
desktopNotificationsEnabled,
dispatch,
t,
]);
Expand Down
19 changes: 19 additions & 0 deletions hooks/useNativeNotifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* QNBS-v3 (T3): Bootstraps native OS notification permission when the user turns the
* `desktop.desktopNotifications` setting on. No-op on the web; never prompts unless the setting is
* on, so opening Settings alone never triggers an OS permission dialog.
*/

import { useEffect } from 'react';
import { useAppSelector } from '../app/hooks';
import { requestNotificationPermission } from '../services/desktop/desktopNotifications';
import { isTauriRuntime } from '../services/tauriRuntime';

export function useNativeNotifications(): void {
const enabled = useAppSelector((s) => s.settings.desktop?.desktopNotifications ?? false);

useEffect(() => {
if (!isTauriRuntime() || !enabled) return;
void requestNotificationPermission();
}, [enabled]);
}
Comment thread
qnbs marked this conversation as resolved.
16 changes: 15 additions & 1 deletion hooks/useTauriUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { useCallback, useEffect, useState } from 'react';
import { useAppSelector } from '../app/hooks';
import { sendDesktopNotification } from '../services/desktop/desktopNotifications';
import { isTauriRuntime } from '../services/tauriRuntime';
import { useTranslation } from './useTranslation';

export type TauriUpdateInfo = {
version: string;
Expand All @@ -9,6 +12,11 @@ export type TauriUpdateInfo = {

export function useTauriUpdater(options?: { autoCheck?: boolean }) {
const autoCheck = options?.autoCheck ?? false;
const { t } = useTranslation();
// QNBS-v3 (T3): gate the "update ready" native notification behind the opt-in desktop setting.
const desktopNotificationsEnabled = useAppSelector(
(state) => state.settings.desktop?.desktopNotifications ?? false,
);
Comment thread
qnbs marked this conversation as resolved.
const [update, setUpdate] = useState<TauriUpdateInfo | null>(null);
const [checking, setChecking] = useState(false);
const [installing, setInstalling] = useState(false);
Expand All @@ -29,6 +37,12 @@ export function useTauriUpdater(options?: { autoCheck?: boolean }) {
currentVersion,
available: true,
});
if (desktopNotificationsEnabled) {
void sendDesktopNotification(
t('desktop.notify.updateReadyTitle'),
t('desktop.notify.updateReadyBody', { version: result.version }),
);
}
Comment thread
qnbs marked this conversation as resolved.
Outdated
} else {
setUpdate({ version: currentVersion, currentVersion, available: false });
}
Expand All @@ -38,7 +52,7 @@ export function useTauriUpdater(options?: { autoCheck?: boolean }) {
} finally {
setChecking(false);
}
}, []);
}, [desktopNotificationsEnabled, t]);
Comment thread
qnbs marked this conversation as resolved.
Outdated

const installUpdate = useCallback(async () => {
if (!isTauriRuntime()) return;
Expand Down
4 changes: 4 additions & 0 deletions locales/ar/desktop.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
"desktop.menu.settings": "Settings",
"desktop.menu.view": "View",
"desktop.menu.window": "Window",
"desktop.notify.updateReadyBody": "WorldScript Studio {{version}} is ready to install.",
"desktop.notify.updateReadyTitle": "Update ready",
"desktop.settings.desktopNotifications": "Desktop notifications",
"desktop.settings.desktopNotificationsHint": "Show a native OS notification when a background task finishes (export, encrypted backup, ProForge pipeline stage, app update).",
"desktop.settings.minimizeToTray": "Minimize to tray on close",
"desktop.settings.minimizeToTrayHint": "Keep WorldScript Studio running in the system tray instead of quitting when you close the window.",
"desktop.settings.sectionTitle": "Desktop",
Expand Down
6 changes: 4 additions & 2 deletions locales/ar/export.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
"export.manuscriptLabel": "المخطوطة",
"export.motivationLabel": "الدافع",
"export.notesLabel": "ملاحظات",
"export.notify.completeBody": "Your manuscript export finished successfully.",
"export.notify.completeTitle": "Export complete",
"export.options.copied": "تم النسخ!",
"export.options.downloadButton": "تنزيل الملف",
"export.options.title": "أدوات التصدير",
Expand All @@ -86,10 +88,10 @@
"export.pasteSection.importAsChapter": "استيراد كفصل",
"export.pasteSection.textPlaceholder": "الصق نصًا من Google Docs أو Notion هنا ‏(Ctrl+V)...",
"export.pasteSection.titlePlaceholder": "عنوان الفصل (اختياري)",
"export.preview.noContent": "اختر محتوى لرؤية معاينة.",
"export.preview.modeLabel": "وضع المعاينة",
"export.preview.modeText": "نص",
"export.preview.modeRendered": "معروض",
"export.preview.modeText": "نص",
"export.preview.noContent": "اختر محتوى لرؤية معاينة.",
"export.preview.title": "معاينة حيّة",
"export.title": "جناح النشر والتصدير",
"export.toggleSection": "تبديل ظهور {{title}}",
Expand Down
6 changes: 5 additions & 1 deletion locales/de/desktop.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@
"desktop.tray.quit": "Beenden",
"desktop.settings.sectionTitle": "Desktop",
"desktop.settings.minimizeToTray": "Beim Schließen in den Infobereich minimieren",
"desktop.settings.minimizeToTrayHint": "WorldScript Studio beim Schließen des Fensters im Infobereich weiterlaufen lassen, statt zu beenden."
"desktop.settings.minimizeToTrayHint": "WorldScript Studio beim Schließen des Fensters im Infobereich weiterlaufen lassen, statt zu beenden.",
"desktop.settings.desktopNotifications": "Desktop-Benachrichtigungen",
"desktop.settings.desktopNotificationsHint": "Native Betriebssystem-Benachrichtigung anzeigen, wenn eine Hintergrundaufgabe abgeschlossen ist (Export, verschlüsseltes Backup, ProForge-Pipeline-Phase, App-Update).",
"desktop.notify.updateReadyTitle": "Update bereit",
"desktop.notify.updateReadyBody": "WorldScript Studio {{version}} ist installationsbereit."
}
2 changes: 2 additions & 0 deletions locales/de/export.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
"export.emptySection": "Leerer Abschnitt",
"export.error.epubFailed": "EPUB-Export fehlgeschlagen: ",
"export.error.exportFailed": "Export fehlgeschlagen",
"export.notify.completeTitle": "Export abgeschlossen",
"export.notify.completeBody": "Der Export Ihres Manuskripts wurde erfolgreich abgeschlossen.",
"export.export": "Exportieren",
"export.exportFailed": "Export fehlgeschlagen",
"export.exportFormat": "Export-Format",
Expand Down
4 changes: 4 additions & 0 deletions locales/el/desktop.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
"desktop.menu.settings": "Settings",
"desktop.menu.view": "View",
"desktop.menu.window": "Window",
"desktop.notify.updateReadyBody": "WorldScript Studio {{version}} is ready to install.",
"desktop.notify.updateReadyTitle": "Update ready",
"desktop.settings.desktopNotifications": "Desktop notifications",
"desktop.settings.desktopNotificationsHint": "Show a native OS notification when a background task finishes (export, encrypted backup, ProForge pipeline stage, app update).",
"desktop.settings.minimizeToTray": "Minimize to tray on close",
"desktop.settings.minimizeToTrayHint": "Keep WorldScript Studio running in the system tray instead of quitting when you close the window.",
"desktop.settings.sectionTitle": "Desktop",
Expand Down
6 changes: 4 additions & 2 deletions locales/el/export.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
"export.manuscriptLabel": "Χειρόγραφο",
"export.motivationLabel": "Κίνητρο",
"export.notesLabel": "Σημειώσεις",
"export.notify.completeBody": "Your manuscript export finished successfully.",
"export.notify.completeTitle": "Export complete",
"export.options.copied": "Αντιγράφηκε!",
"export.options.downloadButton": "Λήψη αρχείου",
"export.options.title": "Εξαγωγή Controls",
Expand All @@ -86,10 +88,10 @@
"export.pasteSection.importAsChapter": "Εισαγωγή ως κεφάλαιο",
"export.pasteSection.textPlaceholder": "Επικολλήστε εδώ κείμενο από τα Έγγραφα Google ή το Notion (Ctrl+V)...",
"export.pasteSection.titlePlaceholder": "Τίτλος κεφαλαίου (προαιρετικό)",
"export.preview.noContent": "Επιλέξτε περιεχόμενο για να δείτε μια προεπισκόπηση.",
"export.preview.modeLabel": "Λειτουργία προεπισκόπησης",
"export.preview.modeText": "Κείμενο",
"export.preview.modeRendered": "Αποδοθέν",
"export.preview.modeText": "Κείμενο",
"export.preview.noContent": "Επιλέξτε περιεχόμενο για να δείτε μια προεπισκόπηση.",
"export.preview.title": "Ζωντανή προεπισκόπηση",
"export.title": "Εξαγωγή Publishing Suite",
"export.toggleSection": "Εναλλαγή ορατότητας για {{title}}",
Expand Down
6 changes: 5 additions & 1 deletion locales/en/desktop.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@
"desktop.tray.quit": "Quit",
"desktop.settings.sectionTitle": "Desktop",
"desktop.settings.minimizeToTray": "Minimize to tray on close",
"desktop.settings.minimizeToTrayHint": "Keep WorldScript Studio running in the system tray instead of quitting when you close the window."
"desktop.settings.minimizeToTrayHint": "Keep WorldScript Studio running in the system tray instead of quitting when you close the window.",
"desktop.settings.desktopNotifications": "Desktop notifications",
"desktop.settings.desktopNotificationsHint": "Show a native OS notification when a background task finishes (export, encrypted backup, ProForge pipeline stage, app update).",
"desktop.notify.updateReadyTitle": "Update ready",
"desktop.notify.updateReadyBody": "WorldScript Studio {{version}} is ready to install."
}
2 changes: 2 additions & 0 deletions locales/en/export.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"export.format.docx": "Word Document (.docx)",
"export.error.epubFailed": "EPUB export failed: ",
"export.error.exportFailed": "Export failed",
"export.notify.completeTitle": "Export complete",
"export.notify.completeBody": "Your manuscript export finished successfully.",
"export.compileWizard.openButton": "Compile wizard…",
"export.compileWizard.title": "Compile wizard",
"export.compileWizard.stepPreset": "Choose a preset",
Expand Down
6 changes: 5 additions & 1 deletion locales/es/desktop.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@
"desktop.tray.quit": "Salir",
"desktop.settings.sectionTitle": "Escritorio",
"desktop.settings.minimizeToTray": "Minimizar a la bandeja al cerrar",
"desktop.settings.minimizeToTrayHint": "Mantén WorldScript Studio en la bandeja del sistema en lugar de salir al cerrar la ventana."
"desktop.settings.minimizeToTrayHint": "Mantén WorldScript Studio en la bandeja del sistema en lugar de salir al cerrar la ventana.",
"desktop.settings.desktopNotifications": "Notificaciones de escritorio",
"desktop.settings.desktopNotificationsHint": "Muestra una notificación nativa del sistema cuando finaliza una tarea en segundo plano (exportación, copia de seguridad cifrada, fase del pipeline ProForge, actualización de la app).",
"desktop.notify.updateReadyTitle": "Actualización lista",
"desktop.notify.updateReadyBody": "WorldScript Studio {{version}} está listo para instalar."
}
2 changes: 2 additions & 0 deletions locales/es/export.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
"export.emptySection": "Sección vacía",
"export.error.epubFailed": "Error en la exportación EPUB: ",
"export.error.exportFailed": "Error en la exportación",
"export.notify.completeTitle": "Exportación completa",
"export.notify.completeBody": "La exportación de tu manuscrito ha finalizado correctamente.",
"export.export": "Exportar",
"export.exportFailed": "Error al exportar",
"export.exportFormat": "Formato de exportación",
Expand Down
4 changes: 4 additions & 0 deletions locales/eu/desktop.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
"desktop.menu.settings": "Settings",
"desktop.menu.view": "View",
"desktop.menu.window": "Window",
"desktop.notify.updateReadyBody": "WorldScript Studio {{version}} is ready to install.",
"desktop.notify.updateReadyTitle": "Update ready",
"desktop.settings.desktopNotifications": "Desktop notifications",
"desktop.settings.desktopNotificationsHint": "Show a native OS notification when a background task finishes (export, encrypted backup, ProForge pipeline stage, app update).",
"desktop.settings.minimizeToTray": "Minimize to tray on close",
"desktop.settings.minimizeToTrayHint": "Keep WorldScript Studio running in the system tray instead of quitting when you close the window.",
"desktop.settings.sectionTitle": "Desktop",
Expand Down
6 changes: 4 additions & 2 deletions locales/eu/export.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
"export.manuscriptLabel": "Eskuizkribu",
"export.motivationLabel": "Motibazioa",
"export.notesLabel": "Oharrak",
"export.notify.completeBody": "Your manuscript export finished successfully.",
"export.notify.completeTitle": "Export complete",
"export.options.copied": "Kopiatu!",
"export.options.downloadButton": "Deskargatu Fitxategia",
"export.options.title": "Esportazio Kontrolak",
Expand All @@ -86,10 +88,10 @@
"export.pasteSection.importAsChapter": "Inportatu kapitulu gisa",
"export.pasteSection.textPlaceholder": "Itsatsi Google Docs edo Notion-eko testua hemen (Ktrl+V)...",
"export.pasteSection.titlePlaceholder": "Kapituluaren izenburua (aukerakoa)",
"export.preview.noContent": "Hautatu edukia aurrebista ikusteko.",
"export.preview.modeLabel": "Aurrebista modua",
"export.preview.modeText": "Testua",
"export.preview.modeRendered": "Errendatua",
"export.preview.modeText": "Testua",
"export.preview.noContent": "Hautatu edukia aurrebista ikusteko.",
"export.preview.title": "Zuzeneko aurrebista",
"export.title": "Esportatu Argitalpen Suite",
"export.toggleSection": "Aldatu ikusgaitasuna {{title}}",
Expand Down
Loading
Loading