mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-10-28 17:42:49 +00:00
## Summary System notifications on Windows: everything about them in one PR. 1. **Windows data profiles never showed system notifications** (`fix(windows): show system notifications from every data profile`). Tauri runs each data profile, such as **dev**, under its own AppUserModelID (`…client.scope.s<hash>`), and its notifications carry that ID; Windows shows a desktop app's notifications only under an ID it knows, and the installer's Start menu shortcut carries the stable ID alone. Windows recorded every notification (100 on the affected machine) and displayed none. Every launch now registers the running ID under `HKCU\Software\Classes\AppUserModelId\<identifier>` (`DisplayName` CodeNomad, `IconUri` the bundled web app icon `resources\server\public\pwa-192x192.png`, which the packaged-resource smoke check requires; `windows_notification_identity.rs`). Electron always uses the stable ID. 2. **The permission was lost at every restart on Windows.** The notification plugin's page script reports *denied* there until the page requests permission, though Windows never asks, so Settings asked again after each restart. The check now asks the native plugin command, which answers granted on desktop: granted once, kept. 3. **Notifications were held back whenever the window was visible** (`fix(notifications): hold system notifications back only while focused`). With **Notify when the app is focused** off (the default), an open window stays visible behind other applications or on another screen, so nothing was sent unless CodeNomad was minimized. Delivery is now skipped only while the window is visible and focused (`document.hasFocus()`). The setting's subtitle and the Settings help say *focused* in all ten languages; `dev-docs/SETTINGS.md` records the rule. Out of scope by decision: several CodeNomad windows open on the same project may each send the event's notification (as they already do when the setting is on), and focus inside a browser preview counts as leaving the page. ## Validation - Affected machine: no profile notification showed before; after registering its ID as above, a notification sent under it showed in the notification centre under CodeNomad. - `cargo test --bins -- windows_notification_identity` (writes the key, reads back both values). - `tests/browser/system-notification-focus.test.ts`: real routed notification from a visible page; nothing while focused, one once focus is lost, one while focused with the setting on; red on the previous visibility check. - `npm run typecheck --workspace @codenomad/ui`, `npm run test:help`.
138 lines
5 KiB
JavaScript
138 lines
5 KiB
JavaScript
#!/usr/bin/env node
|
|
const fs = require("fs")
|
|
const path = require("path")
|
|
const { spawnSync } = require("child_process")
|
|
const { MANAGED_NODE_VERSION } = require("./prepare-node-runtime.cjs")
|
|
|
|
const requiredPackages = [
|
|
"yaml",
|
|
"fastify",
|
|
"@fastify/static",
|
|
"@fastify/cors",
|
|
"@fastify/reply-from",
|
|
"@opencode/client",
|
|
"openai",
|
|
"pino",
|
|
"undici",
|
|
"zod",
|
|
"node-forge",
|
|
]
|
|
|
|
function parseArgs(argv) {
|
|
const options = {}
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index]
|
|
if (arg === "--resources") options.resources = argv[++index]
|
|
else if (arg === "--loading") options.loading = argv[++index]
|
|
else if (arg === "--target") options.target = argv[++index]
|
|
else throw new Error(`Unknown argument: ${arg}`)
|
|
}
|
|
if (!options.resources) throw new Error("Missing --resources <path>")
|
|
return options
|
|
}
|
|
|
|
function platformDirName() {
|
|
const platform = process.platform === "darwin" ? "darwin" : process.platform
|
|
const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : process.arch
|
|
return `${platform}-${arch}`
|
|
}
|
|
|
|
function targetParts(target) {
|
|
const [platform, ...archParts] = target.split("-")
|
|
const arch = archParts.join("-")
|
|
if (!platform || !arch) throw new Error(`Invalid target: ${target}`)
|
|
return { platform, arch }
|
|
}
|
|
|
|
function nodeBinary(resourcesRoot, target) {
|
|
const { platform } = targetParts(target)
|
|
const executable = platform === "win32" ? "node.exe" : path.join("bin", "node")
|
|
return path.join(resourcesRoot, "node", target, executable)
|
|
}
|
|
|
|
function canExecuteTarget(target) {
|
|
return target === platformDirName()
|
|
}
|
|
|
|
function run(command, args, options = {}) {
|
|
const result = spawnSync(command, args, { stdio: "inherit", ...options })
|
|
if (result.error) throw result.error
|
|
if (result.status !== 0) {
|
|
throw new Error(`${command} exited with code ${result.status ?? 1}`)
|
|
}
|
|
return result
|
|
}
|
|
|
|
function smokeServer(resourcesRoot, target) {
|
|
const serverRoot = path.join(resourcesRoot, "server")
|
|
const entrypoint = path.join(serverRoot, "dist", "bin.js")
|
|
// Remote Control's self-contained OpenTunnel bundle, loaded on demand.
|
|
const remoteTunnel = path.join(serverRoot, "dist", "remote-control", "tunnel-runtime.js")
|
|
// The web app's icon, which also names Tauri's notifications on Windows.
|
|
const appIcon = path.join(serverRoot, "public", "pwa-192x192.png")
|
|
const node = nodeBinary(resourcesRoot, target)
|
|
|
|
for (const requiredPath of [node, entrypoint, remoteTunnel, appIcon, path.join(serverRoot, "node_modules")]) {
|
|
if (!fs.existsSync(requiredPath)) throw new Error(`Missing packaged runtime path: ${requiredPath}`)
|
|
}
|
|
|
|
for (const packageName of requiredPackages) {
|
|
const packageRoot = path.join(serverRoot, "node_modules", ...packageName.split("/"))
|
|
if (!fs.existsSync(packageRoot)) throw new Error(`Missing packaged dependency: ${packageName}`)
|
|
}
|
|
|
|
console.log(`packaged server static checks ok for ${target}`)
|
|
|
|
if (!canExecuteTarget(target)) {
|
|
console.log(`skipping executable smoke for ${target} on host ${platformDirName()}`)
|
|
return
|
|
}
|
|
|
|
const version = run(node, ["--version"], { stdio: "pipe", encoding: "utf8" }).stdout.trim()
|
|
if (version !== MANAGED_NODE_VERSION) {
|
|
throw new Error(`Packaged Node version ${version} does not match ${MANAGED_NODE_VERSION}`)
|
|
}
|
|
console.log(`packaged Node version ok: ${version}`)
|
|
run(node, [entrypoint, "--version"])
|
|
|
|
const importScript = [
|
|
`for (const name of ${JSON.stringify(requiredPackages)}) await import(name);`,
|
|
`const tunnel = await import(${JSON.stringify(require("url").pathToFileURL(remoteTunnel).href)});`,
|
|
"if (typeof tunnel.openRemoteTunnel !== 'function') throw new Error('Remote Control tunnel runtime is incomplete');",
|
|
"console.log('packaged dependency imports ok');",
|
|
].join(" ")
|
|
|
|
// Resolve from the packaged server, not the build checkout. The V2 client is ESM-only.
|
|
run(node, ["--input-type=module", "-e", importScript], { cwd: serverRoot })
|
|
}
|
|
|
|
function smokeLoadingAssets(loadingRoot) {
|
|
if (!loadingRoot) return
|
|
|
|
const htmlPath = path.join(loadingRoot, "loading.html")
|
|
if (!fs.existsSync(htmlPath)) throw new Error(`Missing loading HTML: ${htmlPath}`)
|
|
|
|
const html = fs.readFileSync(htmlPath, "utf8")
|
|
const refs = [...html.matchAll(/(?:src|href)=["']([^"']+)["']/g)].map((match) => match[1])
|
|
for (const ref of refs) {
|
|
if (/^(?:https?:)?\/\//.test(ref)) continue
|
|
if (!ref.startsWith("/assets/") && !ref.startsWith("assets/")) continue
|
|
|
|
const relative = ref.replace(/^\//, "")
|
|
const target = path.join(loadingRoot, relative)
|
|
if (!fs.existsSync(target)) throw new Error(`Missing loading asset referenced by ${htmlPath}: ${ref}`)
|
|
}
|
|
|
|
console.log("loading asset references ok")
|
|
}
|
|
|
|
try {
|
|
const options = parseArgs(process.argv.slice(2))
|
|
const resourcesRoot = path.resolve(options.resources)
|
|
const target = options.target ?? platformDirName()
|
|
smokeServer(resourcesRoot, target)
|
|
smokeLoadingAssets(options.loading ? path.resolve(options.loading) : null)
|
|
} catch (error) {
|
|
console.error("[smoke-packaged-resources] failed:", error)
|
|
process.exit(1)
|
|
}
|