Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<img src="https://img.shields.io/badge/Version-v1.25.0-6366F1" alt="v1.25.0">
<img src="https://img.shields.io/badge/Storage-IndexedDB_v8-F59E0B" alt="IndexedDB v8">
<img src="https://img.shields.io/badge/PWA-v3.0-5BB974?logo=pwa" alt="PWA v3.0">
<img src="https://img.shields.io/badge/i18n-19_locales-2861_keys-0EA5E9" alt="i18n 19 locales — 2861 keys">
<img src="https://img.shields.io/badge/i18n-19_locales-2869_keys-0EA5E9" alt="i18n 19 locales — 2869 keys">
<img src="https://img.shields.io/badge/Tests-6477%2B_%2F_532_files-22C55E" alt="6477+ tests / 532 files">
<img src="https://img.shields.io/codecov/c/github/qnbs/WorldScript-Studio?logo=codecov&label=Coverage" alt="Codecov Coverage">
<img src="https://img.shields.io/badge/License-MIT-22C55E" alt="License MIT">
Expand Down Expand Up @@ -396,7 +396,7 @@ Infrastructure-level features that keep the app fast and extensible as projects

### 🌐 Full Multi-Language Support

Shipped UI locales with **2861 i18n keys** across all 19 languages — zero hardcoded user-facing strings:
Shipped UI locales with **2869 i18n keys** across all 19 languages — zero hardcoded user-facing strings:

- 🇩🇪 **German** (Deutsch)
- 🇬🇧 **English**
Expand Down Expand Up @@ -505,7 +505,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and
| **PDF Export** | jsPDF | Client-side, configurable PDF document generation |
| **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) |
| **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking |
| **i18n** | Custom React Context (`I18nContext.tsx`) | 2861 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence |
| **i18n** | Custom React Context (`I18nContext.tsx`) | 2869 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence |
| **Testing** | Vitest 4.x (6477+ tests / 532 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy |
| **Visualization** | Force-directed graph | Interactive character relationship network |
Expand Down Expand Up @@ -707,7 +707,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt
**Current test metrics (2026-07-30, CI-reported):**
- **6477+ unit tests** across **532 test files** — all passing
- Coverage thresholds: lines ≥ 74 · branches ≥ 60 · functions ≥ 67 · statements ≥ 72 — enforced in CI (see Codecov badge for live metrics)
- i18n: **2861 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta)
- i18n: **2869 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta)

**CI-cloud-first workflow (recommended):** On constrained hardware run **`pnpm run lint && pnpm run i18n:check && pnpm run typecheck`** locally, then push and let CI handle coverage, E2E, Lighthouse, and Stryker. Authoritative numbers come from CI artifacts (Codecov, JUnit). After CI goes green, update the README badges and `AUDIT.md` quality-gate line from the reported metrics. See **[`docs/CI.md`](docs/CI.md) § Cloud CI-first vs local development** for the full post-merge doc-update checklist.

Expand Down
39 changes: 39 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,43 @@ listenerMiddleware.startListening({
},
});

// QNBS-v3: Native "ProForge stage ready for review" notification — the user may have tabbed away during a long-running pipeline stage.
listenerMiddleware.startListening({
actionCreator: proForgeActions.stageCompleted,
effect: async (action, listenerApi) => {
// QNBS-v3: capture the original (pre-effect) state synchronously before any await, per this file's listener state-capture contract.
const originalState = listenerApi.getOriginalState() as RootState;
if (!originalState.settings.desktop?.desktopNotifications) return;
// QNBS-v3: the reducer no-ops when there's no matching run/stage — mirror that here so an invalid action never notifies.
const stateAfter = listenerApi.getState() as RootState;
const stageResult = stateAfter.proForge.currentRun?.stages.find(
(s) => s.stage === action.payload.stage,
);
if (stageResult?.status !== 'awaitingReview') return;
try {
const [{ sendDesktopNotification }, { getStaticTranslation, getCurrentLanguage }] =
await Promise.all([
import('../services/desktop/desktopNotifications'),
import('../services/i18n/staticTranslate'),
]);
const lang = getCurrentLanguage();
const stageLabel = await getStaticTranslation(
`proforge.stageName.${action.payload.stage}`,
lang,
);
const [title, body] = await Promise.all([
getStaticTranslation('desktop.notify.proforgeStageReadyTitle', lang),
getStaticTranslation('desktop.notify.proforgeStageReadyBody', lang, {
stage: stageLabel,
}),
]);
await sendDesktopNotification(title, body);
} 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
15 changes: 15 additions & 0 deletions components/settings/DataSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ export const DataSection: FC = () => {
description: t('settings.data.libraryExport.successBody'),
}),
);
// QNBS-v3: notify only after a successful encrypted library export.
if (settings.desktop?.desktopNotifications) {
void import('../../services/desktop/desktopNotifications')
.then(({ sendDesktopNotification }) =>
sendDesktopNotification(
t('settings.data.libraryExport.successTitle'),
t('settings.data.libraryExport.successBody'),
),
)
.catch(() => {
// QNBS-v3 (T3): best-effort background notification — the export itself already
// succeeded and was toasted above, so a notification failure must never surface to
// the user nor become an unhandled promise rejection.
});
}
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
17 changes: 17 additions & 0 deletions docs/DEEPSOURCE-REVIEW-LOOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,23 @@ GitHub App resumes auto-reviewing, run **both** loops: CodeAnt for narrative/AI
reset/resolve a new task sharing the same released port — fixed by filtering on `msg.taskId` in
`workerBus.ts`), and a missing `port.start()` call in `workerPool.ts::spawnWorker()` (an
`addEventListener`-based `MessagePort` never dispatches without it per the WHATWG spec).
- **2026-08-01** — Same pattern recurred on PR #306 (`feat/tauri-native-notifications`, T3 desktop
notifications): adding one new listener block to `app/listenerMiddleware.ts` (a ~800-line god-file)
triggered `DeepSource: JavaScript` to report **63 "introduced" issues** across the *entire* file —
JS-0067 (top-level function declarations, the repo's idiomatic module style), JS-R1005 (cyclomatic
complexity on pre-existing listener effects untouched by this PR, e.g. `addDebouncedListener`'s
internal effect and three older auto-save/DuckDB listeners), and JS-C1002 (short param names `c`/`p`
in pre-existing predicates). None of the 63 findings land on the actual new code (the
`proForgeActions.stageCompleted` listener, ~line 551) — confirmed by diffing finding line ranges
against the PR's real diff. Re-confirmed via `gh api .../branches/main/protection`: only
`✅ CI Success` is a required status check; `DeepSource: JavaScript` stayed informational-only.
Per the 2026-08-01/#305 precedent, did **not** scatter `# skipcq` across pre-existing unrelated code
— logged here instead and proceeded once the real (required) CI gates were green. **Reinforces the
standing conclusion:** any PR that merely imports/touches this file will keep re-triggering this
same false-positive wave until a maintainer applies a dashboard rule-level ignore for JS-0067 (and
likely scopes JS-R1005/JS-C1002 similarly for this file, or the file is decomposed) — an agent
should not attempt that maintainer-only dashboard action nor a speculative mass-refactor of this
critical middleware file as a side effect of an unrelated feature PR.

---

Expand Down
2 changes: 2 additions & 0 deletions features/settings/settingsDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type { Settings } from '../../types';

export const defaultDesktopSettings: Settings['desktop'] = {
minimizeToTray: false,
// QNBS-v3: keep native desktop notifications opt-in by default.
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
2 changes: 2 additions & 0 deletions features/settings/settingsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ const settingsSlice = createSlice({
if (!state.aiMode) state.aiMode = 'hybrid';
// QNBS-v3: backfill openRouter for settings persisted before OpenRouter integration
if (!state.openRouter) state.openRouter = { ...DEFAULT_OPENROUTER_SETTINGS };
// QNBS-v3: merge (not replace) desktop settings so an older persisted envelope predating a new DesktopSettings field doesn't leave it undefined.
state.desktop = { ...defaultDesktopSettings, ...state.desktop };
},
setTheme(state, action: PayloadAction<Theme>) {
state.theme = action.payload;
Expand Down
Loading
Loading