Feature-parity checklist toward VRCX + VRCNext, our own version. Sources: VRCX, VRCNext.
Legend: [x] done · [~] partial · [ ] todo ·
- [~] Performance pass — top priority. App feels laggy. Investigate:
- Virtualise long lists — rail sections now capped at 150 rows (+N more note); paged tabs
- Throttle/debounce re-renders; avoid full innerHTML rebuilds on every update
- Audit all
setIntervalpollers — staggered on launch (friendDiff 8s/notif 14s/groups 22s) + 429 backoff guard on every poller - [~] Lazy-load tab content only when visible; pause canvas (spectrum/OSC graph) off-tab — canvas has
offsetParentguards; full tab lazy-load still todo - Reuse the API cache everywhere; add request backoff on 429 (interceptor + isRateLimited + stale-cache fallback)
- Profile with DevTools; check GPU/CPU (hardware accel already off)
- [~] Full friends scan — paginated online+active+offline; verify none missing
- Name-change tracking — logged in History (friend-diff)
- VRCX import — best-effort import button (verify vs real VRCX.sqlite3)
- [~] Perf — friends panel/radar no longer rebuild every 3s; idle stopwatch fixed; tiered friend cache (offline 5min/online 90s), self cache, world poll 5s, SQLite write debounced 8s + events capped 8000, rightbar 120s; paged lists (Friend Den, My Groups, Mutuals, Blocked, Favs). (rail offline virtualisation still todo)
- Friend list + right panel (Same World / In-Game / On Web / Offline, collapsible)
- Friend Den, Event Scout (multi-group), Pawprints (per-world time)
- Unified Search (users/worlds/groups) + paste ID/URL loader
- Profile modal: Info (badges), Groups, Mutuals (friends+groups), Content, Favs
- Social actions: Add Friend↔Unfriend, Invite, Request Invite, Boop, Favorite
- World + Group detail modals; clickable cards everywhere
- History (SQLite): player join/leave, friend add/remove, world visits
- Auto-Greeter (auto-accept friend requests, all/allow-list)
- Discord Rich Presence (world + HR + song; buttons auto-drop over IPC)
- Pinned Discord Rich Presence Application ID to
1534208250046578790, no longer user-editable — disabled the Discord Application ID field in the Discord tab and force the fixed ID inrenderer.jsandmain.js'sdiscord:starthandler regardless of saved config. - [~] Discord Activity (VRChat status panel) + official shared Discord bot. New standalone
backend, own package.json/Docker, deployed separately — never bundled into the app — lives on
its own branch in this same GitHub repo
(
feature/discord-backend-server), not as a subfolder here. It holds the shared bot token and OAuth client secret, which can never live in a distributed Electron app. Backend + Electron-side code (identity login, official-bot mode, status push) are implemented and locally verified (curl against the running backend,node --check/require-resolution on every changed file). Defaults to the officialhttps://nekosuneappsvrc.nekosunevr.co.ukdeployment (DEFAULT_NEKOSUNE_BACKEND_URLinmain.js), but self-hosters can override it via the Backend URL field on the Voice Bot card (nekosuneBackendUrlsetting) to point at their own instance instead. Not deployed by this session — still needed before this works end to end: actually deploy that official host, fill in that branch's.env(bot token, OAuth client secret, JWT secret), register the OAuth redirect + Activities URL Mapping in the Developer Portal for app1534208250046578790, and get Activities enabled for that app. See that branch'sREADME.mdfor the full one-time setup checklist. - Fixed: Discord Rich Presence's elapsed timer reset on every status change instead of
showing real uptime. The code read a
startTimeproperty directly offdiscord-rpc'sClientobject, which doesn't exist at all (confirmed against the library's own source) - so the timestamp anchor was alwaysundefined, and Discord fell back to its own "time since last update" display, which recalculates on every single presence push. Now anchors to a realDate.now()taken once on connect, reused for every update after. Verified with a mocked Discord client: identical timestamp across multiple simulated status changes. - Fixed: Multi-line chatbox's Now Playing line never showed progress/duration, only the
bare song name - even though
{songbar}/{songtime}were already fully working tokens for Status presets.chatboxComposer.js's hardcodednowPlayingline just never referenced them. Now appends the progress bar + elapsed/duration when a source reports one, falls back to just the song name when it doesn't (e.g. some sources don't report duration at all). - VRChat auto-status (login + 2FA), Radar, Weather, VRChat Tools (yt-dlp/cache)
- Heart rate (Pulsoid + HypeRate) + session analytics
- Param Lab (OSC), Photo Relay (screenshots→Discord), Soundpad, SpotiOSC, DiscordOSC, Discord voice bot
- Auto-launch / start-minimized / per-feature auto-start
- Green UI, icon rail, notifications flyout, auto seasonal themes, hidden scrollbars
- Video/media link tracking — video URLs parsed from the log → History
- Name-change tracking — friend renames logged to History (name_change)
- Activity heatmap — day×hour event heatmap on the History page
- Instance join/leave history — enter + leave-with-duration logged to History
- GameLog — join/leave/world (w/ duration)/friend/name-change/video/portals/invites/group events
- VRC Notes editor — read + write your note on a user (profile Info tab)
- Social status presets — save/apply status + statusDescription combos (Profile Editor)
- Last-seen / time-together — shown on the profile Info tab (from History)
- Block / mute — toggle on profiles + a Blocked/Muted list (Settings) with Remove
- Trust/feedback view — visual trust ladder (Visitor → New User → User → Known User → Trusted User) with current level highlighted, shown in the profile modal info tab
- Local quick-search — instant offline friend search on the Search page
- Avatar search — via the Avatars page (provider-based)
- Favorites backup — export favorite worlds + friends to JSON; import re-adds them via the API (avatar favs not yet included — no dedicated API endpoint)
- Data export/import — settings + history to JSON
- Screenshot metadata — embed world/instance/players into the PNG (VRChat picture metadata), and read it back in a gallery
- Media Library — VRChat screenshot gallery (open on click); date/world filters todo
- Avatar/world image upload management (without Unity) — advanced, optional
- VRChat server status — online-user count in the top bar (poll /visits)
- Configured Start — launch companion apps (+ optional VRChat)
- Data export/import — settings + history to JSON
- Crash recovery / auto-rejoin — opt-in; relaunches last instance if VRChat closes
- Fixed: auto-rejoin treated every normal VRChat close as a crash. It only checked "is
VRChat.exe still running", which is equally true after a real crash and after just closing the
game normally - so it couldn't actually tell the two apart, and always assumed the worst. Now
cross-checks Windows' own crash reporter (WER): a real crash (unhandled exception, access
violation, etc.) always logs an "Application Error" event (Application log, Event ID 1000) for
the crashing process, a normal quit never does. Cross-checked this against 26 real historical
VRChat logs pulled from the user's own backup: none contained an actual engine/native crash
dump — every "abrupt-looking" ending was just a routine, non-fatal in-world Udon script
exception (
NullReferenceExceptionmid-EXTERN-call, common and harmless) — and VRChat's own clean-quit log marker (VRCApplication: HandleApplicationQuit) only appeared in 12 of the 26, confirming that "the log doesn't end cleanly" alone (the other heuristic considered) would have kept false-positiving on more than half of them, reproducing the exact bug reported. Verified both directions with a real, unmocked Windows Event Log query: correctly silent with no crash event present, and correctly fires the rejoin when a matching event is simulated. - Auto-rejoin now uses VRChat's own
vrchat://launch?id=...protocol handler (same mechanism VRCX and other community tools use) instead of only the vrchat.com web page, which just prompts the browser to hand off to the client anyway -vrchat://launches directly. Confirmed for real against this machine's actual VRChat install:HKCR\vrchat\shell\open\commandexists and points at VRChat's ownlaunch.exe. Falls back to the web link automatically if that registration isn't present. Verified both branches with the real (unmocked) registry check plus mocked process/event state for the crash-detection side. - Custom themes —
themeSelectdropdown +uiThemesetting; 30-min recheck;blackgreendefault; seasonal events always win - Registry tools — VRChat registry backup/restore (Windows registry under VRChat)
- Multiple dashboards / customizable widgets (VRCX-style configurable panels)
- Group alerts — polls watched groups' posts and events → toast + History
- Notifications — rich (invite world+link, boop, requestInvite, group), cached in SQLite until accepted/dismissed; top-right flyout + Notify sidebar tab; toast on new
- Read ToN directly from the VRChat output log (ToNSaveManager now OPTIONAL).
modules/integrations/ton/tonLogReader.jstails the newestoutput_log_*.txtand parses save codes ([START]…[END]), round type + map (This round is taking place at …), terror IDs (Killers have been set - …), deaths (You died.), round end (Verified Round End), stuns and damage. Runs alongside the WS; drives live state when the WS isn't connected. Captured save codes are auto-decoded to achievements and marked on the board (so achievements stay current without ToNSaveManager). - ToN UI: connect card relabelled — ToNSaveManager is optional; the log reader is the default.
- Terror names from killer IDs —
tonLogReader.jsnow looks up each ID intonData.get().terrors(the roster fetched from terror.moe, ordered by internal ID); falls back toTerror #Nif the cache is empty or the index is out of range. - Lifetime stats from the log — the log only has the current session, so all-time rounds/deaths/etc. still need ToNSaveManager or a decoded save code. Session counters work from the log; consider deriving lifetime totals from a decoded save.
- Catch up historical rounds/saves from the log on first read (currently the initial full-log pass is suppressed to avoid flooding history; consider importing the session's past save codes as backups on startup).
- About page — app info, NekoSuneVR creator, version, links, update check, contributors auto-detected from the GitHub API.
- Deeper collaborator / collab-code auto-detection — beyond GitHub contributors:
parse
Co-Authored-By:trailers from git history and any in-source@author/credit comment markers, and surface named collaborations on the About page. - Supporters card — Patreon/Ko-fi supporters shown on the About page with Discord avatar
(animated .gif for Nitro users, static .png otherwise, matching Discord's own rules exactly)
and name-on-hover, linking out to the general Patreon/Ko-fi pages
(
patreon.com/c/nekosunevr,ko-fi.com/nekosunevr), plus a note pointing supporters who haven't linked their Discord tolinkup.nekosunevr.co.ukyet so they show up too. Fetched from main.js (app:supportersIPC) to avoid renderer CORS restrictions, fails soft to "could not load" if the linkup site is unreachable.- New endpoint on the separate NekoSuneLinkupSite repo
(
D:\DEV\NekoSuneVRAPPS\Websites\NekoSuneLinkupSite\SocialLinkUpOnly\api\publicSupporters.js, registered asGET /api/supporters): public, no API key at all (by design — a key embedded in a shipped desktop app isn't actually secret once someone unpacks it, so a genuinely public read-only endpoint is the correct shape here, not a leakable one). Reuses the site's existing, already-tested supporter-status logic (services/supporterStatus.js,services/discordapi.js) rather than re-deriving "is this person currently supporting" itself. Returns onlydiscordId,username,platform,avatarUrl- no email, no payment amounts/dates, no internal ids, and banned users are excluded. - Found and fixed a real staleness bug while building this: the site's existing
loadPatreonSupporterMap()only writesLinkedAccount.isSupportingback to the database for patrons still found in the live Patreon fetch - someone who fully churned (removed from Patreon entirely, not just downgraded) never gets their staleisSupporting: truecleared in the DB by that function. The new endpoint doesn't trust that DB flag for its final decision; it uses the freshly-computed status map the function actually returns instead (which does default everyone to inactive first). Verified with a mocked test covering all four cases: active Patreon included, churned-but-DB-stale Patreon excluded, active Ko-fi included, inactive Ko-fi excluded. - Not deployed by this session — implemented and tested in isolation (mocked DB/Discord/ Patreon calls, since this is a live production site with a real database I don't have local access to), but the linkup site is a separate repo/deployment the user manages; needs their own review + deploy before the About page's supporters card will show real data instead of "could not load supporters."
- New endpoint on the separate NekoSuneLinkupSite repo
(
- In-app update installs, via a standalone updater app — went through a few iterations
this session (in-app Electron download → a
/passivePowerShell+msiexec helper → the final design below) before landing on a fully separate helper app, since the thing doing the replacing can't live inside the files being replaced.updater/is its own small, independent Electron app (ownpackage.json,main.js/preload.js/index.html/renderer.js) with a custom branded, animated UI (a floating/glowing logo, shimmering gradient progress bar) — not the plain default installer UI. "Update available" → "Download & install" now just hands the release asset URL to this helper and quits; the helper does everything else visibly: download with a real progress bar, install, relaunch.- Packaged as
updater.exeon Windows specifically (an electron-builderportabletarget — a genuine single-file standalone executable, not a.ps1script), built fresh by CI for each platform and bundled directly into both the NSISSetup.exeand the.msiviaextraFiles/extraResources(.github/workflows/build.ymlbuildsupdater/before the main app on every OS leg and normalizes its output to a fixed path/name first, so the main build's config doesn't need to know electron-builder's per-arch output folder naming). - Cross-platform: Windows runs the
.msiviamsiexec(no longer silent - runs the normal installer UI, not/passive, so it's clearly visible something is installing); Mac extracts the release.zipand swaps it in for the existing.appbundle (ditto, ditto - macOS built-in, not a bundled tool); Linux replaces an AppImage in place, or opens a.debwith the desktop's own installer since that needs root this helper can't safely provide unattended. - Verified on Windows: built the actual
updater.exevia electron-builder, ran it for real against a live download URL and a fake install target, confirmed it downloads a real file with correct progress mechanics, correctly waits out (or skips) the parent PID, and reaches the actualmsiexecinstall step. Found and fixed a real bug this way: passing PID0(a special "current process group" value toprocess.kill, not a real caller PID) could hang the wait-for-exit loop forever. - Mac and Linux paths are NOT verified against real hardware - implemented from documented, standard platform behavior (this dev environment is Windows-only), flagged honestly rather than claimed tested.
- Considered and explicitly declined (per discussion): a full Discord/Squirrel-style
versioned-folder install architecture (
Update.exe+app-X.Y.Z/folders + a thin launcher stub) — that's a genuinely different packaging pipeline (Squirrel.Windows/electron-winstaller) that would replace the working MSI/NSIS build entirely; kept the existing, proven MSI/NSIS install and added the standalone updater on top of it instead.
- Fixed: updater failed with
EPERMtrying to download into the install directory. It tried the install directory first (e.g.Program Files\NekoSuneAPPS\) and only fell back to temp iffs.accessSync(dir, W_OK)said that wasn't writable — but that check isn't a reliable predictor of real write access on Windows, confirmed by an actualEPERMin the wild despite it passing. Now always downloads to temp, regardless. - Fixed: updater could fail with an opaque, unrecoverable "Command failed: ...Setup-x.x.x.exe
/S" in the wild ("already running" style failure) — it only waited for the old app's PID to
disappear plus a flat 500ms delay before running the silent NSIS install once, with no retry
and no captured stderr/exit code. Now polls whether the install target file is actually
unlockable (not just "the pid is gone"), retries the silent install up to 3× with backoff,
surfaces the real exit code/stderr, and the updater window shows Retry /
Open download folder buttons on failure instead of a dead-end static error. The updater
helper also has its own single-instance lock so a previously stuck attempt can't block a new one.
(Note: the block above documenting "Windows runs the .msi via msiexec" is stale — 1.0.61
switched Windows installs to the NSIS
Setup.exevia/S, msiexec/.msiis no longer what's actually run; flagging here rather than silently rewriting that history entry.)
- Mirrors the app into a floating VR panel via SteamVR —
modules/vr/overlay/(openvrOverlay.jsfor the raw OpenVR FFI bindings,vrOverlayController.jsfor the screenshot-and-push loop). Settings → "VR Overlay" card, Windows + SteamVR only. First slice is view-only — see the app floating in your headset, not yet clickable — click-through (controller-to-mouse translation) is a planned follow-up once this base mirror is confirmed working on real hardware. - Talks to OpenVR via
koffi(FFI), not a compiled native addon — there's no pure-JS way to call a C++ SDK, and koffi needs no build step (lower AV-flag/CI-fragility risk than node-gyp, the same reasoning that ruled outnode-global-key-listenerearlier in this project). Chosen explicitly over the alternative (a small native Node addon) after discussing the tradeoff. - Function signatures and the
VR_IVROverlay_FnTablestruct layout are transcribed directly from Valve's ownopenvr_capi.h(OpenVR's official C-compatible API surface, published specifically for FFI bindings from other languages) — sourced from a copy bundled inside an old, otherwise-unrelated npm package (ovrjs), not guessed or hallucinated. Only the methods actually called (CreateOverlay,DestroyOverlay,ShowOverlay,HideOverlay,SetOverlayWidthInMeters,SetOverlayTransformAbsolute,SetOverlayFromFile) get a real typed callback prototype; every other field in the 60+-method struct is an opaque pointer purely to keep the byte offsets correct, since every field is pointer-sized regardless of which function it points to. - Verified live against a real, running SteamVR install on this dev machine: DLL discovery
(via the Steam install path in the registry, with hardcoded fallbacks),
VR_InitInternal,VR_GetGenericInterface, and error-description lookups all behave exactly as documented. Confirmed thatVRApplication_Utilitycan initialize without a headset but is explicitly denied theIVROverlayinterface ("not available to utility applications"), whileVRApplication_Overlaycorrectly fails with "Hmd Not Found" when no headset is detected — and that failure surfaces as a clear, actionable error message instead of a crash. Also confirmed the full 64-fieldVR_IVROverlay_FnTablestruct definition is valid and correctly sized (512 bytes) via koffi. - UPDATE: confirmed broken on real hardware, and the straightforward fix crashes. A real
headset test (via a partner) hit
"table.CreateOverlay is not a function"— thekoffi.decode(ptr, PtrType, '*')call wasn't actually dereferencing the raw OpenVR interface pointer into a usable struct. Reproduced this safely usingIVRSystem(works without a headset) as a stand-in, and found the correct-looking fix (koffi.decode(ptr, StructType)to read the struct, thenkoffi.decode(rawFnPtrField, protoType)to get a callable function from each field) - but actually calling the resulting function segfaults the whole process, reproduced three times through slightly different argument-marshaling approaches. This is a real, unresolved problem with calling through a struct-of-raw-function-pointers ("vtable") via koffi, not yet a proven-safe pattern. Left the current safe-but-non-functional error in place rather than ship something that crashes. Needs real investigation (koffi's own examples/issue tracker, or reconsidering the FFI approach) before this feature can actually work. - Follow-ups, not done this pass: click-through interactivity (translating controller
laser-pointer intersection into real mouse events on the mirrored UI); the long-standing "VR
gear battery" placeholder (
modules/vrchat/vr/vrBattery.js, previously blocked on "no well-maintained pure-Node OpenVR binding") can now likely be wired for real using the same koffi/openvr_capi.h approach and anIVRSystemstruct, since that blocker no longer applies — once the vtable-calling crash above is actually resolved.
- Removed macOS builds — VRChat doesn't run on Mac, so a Mac build of a VRChat companion
app wasn't serving a real purpose. Removed the
macos-latestmatrix leg and its updater-helper build step from.github/workflows/build.yml, and themacelectron-builder config from bothpackage.jsonandupdater/package.json. Windows and Linux builds unaffected — verified both still built and released successfully. - VirusTotal scanning in the release pipeline — CI submits the Windows installers
(
.exe/.msi) to VirusTotal after they build and links the scan report in the release notes, so "installers are unsigned, SmartScreen may warn" doesn't have to be taken purely on faith. Handles the large-file upload flow (VirusTotal's direct/filesendpoint caps at 32MB; the NSIS installer is bigger, so it goes through/files/upload_urlinstead) and polls for the analysis to finish (up to ~5 minutes) before summarizing flagged-engine counts with a link to the full report. Opt-in via aVT_API_KEYrepo/org secret (a free VirusTotal API key) — skips cleanly with no report section in the notes if that secret isn't reachable by this repo.
- Avatar detail — modal with image, author, platforms, performance, wear/favourite
- Switch avatar — Wear button (
PUT /avatars/{id}/select) - Delete personal avatar — Delete with Yes/No confirm
- Create instance (Public / Friends+ / Friends / Invite+ / Invite) via
POST /instances - Self-invite + Invite friends (picker) + shareable launch link in world modal
- Invite people to a group via friend picker
- Group detail: members, roles, posts, gallery, your permissions
- Friend-picker modal (searchable, multi-select)
- Confirm (Yes/No) modal
Confirmed from the VRCNext repo — gaps not already listed above.
- [~] Profile editor (your own) — status/status-text/bio/pronouns/bio-links done; pfp & banner upload todo
- Messenger / message-slot editor — edit invite & response message slots (Messenger tab)
- Multi-Invite — friend-picker multi-select invite to instance/group
- Inventory — icons / emoji / stickers / prints (with image proxy for auth-gated images)
- Avatar browse — configurable providers (avtrdb + custom VRCX-style endpoints) → wear/favourite
- Group posts + group image gallery — shown in the group detail modal
- Create group instances — POST /instances type=group + groupAccessType, with world picker (my worlds + favourites) + access/region selectors in the group modal, auto self-invite
- Media Library — local screenshot gallery (folders/metadata filters todo)
- Configured Start — launch companion apps (SlimeVR, VRCFaceTracking, …) + optional VRChat
- VRCVideoCacher — install/update (official release download) + start/stop the local proxy from the VRChat Tools tab (custom URL via
vvcUrlsetting) - Design customization — dashboard welcome-screen background + launcher accent colour (note: we intentionally ship fixed green + seasonal; make this opt-in)
- Fast-Fetch cache — TTL cache + in-flight dedupe for user/world/group/friends
- Right panel: Favorites section — favorited friends shown at top of the rail
- Crash detect + auto-rejoin — see System section above (WER Event ID 1000 +
vrchat://relaunch)
- Neko HUD (VR overlay) —
⚠️ needs native OpenVR overlay binding (same blocker as VR battery). Ship a small C#/C++ OpenVR helper exe and spawn it. - Playspace Shift —
⚠️ needs native VR input (grip/stick) — same OpenVR helper. - VoxBoard (voice-triggered soundboard) — needs offline speech model (VOSK ~50 MB); add as optional download.
- Avatar Sizer — done via VRChat's native OSC height-scaling API (
/avatar/eyeheight+/avatar/eyeheightmin/max+/avatar/eyeheightscalingallowed), which works on any avatar with no avatar-specific exposed parameters needed.modules/vrchat/osc/avatarScaling.js+ global hotkeys (Settings → Tools → Avatar Scaling) via a PowerShellWH_KEYBOARD_LLhook (modules/vrchat/osc/keyHookPs.js), only running while the feature is connected or recording a key. - VR gear battery — real OpenVR helper to replace the current stub (
modules/vrchat/vr/vrBattery.js).
- Discord RP buttons — implemented ("Join World" + "VRChat Profile"); buttons work over local IPC RPC. Note: you can't see your own buttons in Discord, only other people viewing your profile can — this is a Discord client limitation, not a code issue.
- Favorites page (dedicated sidebar) listing worlds/avatars/friends with inline remove (currently add/remove via modals + Favs tab).
- [~] Friends panel: avatars for offline friends now show with logo.png fallback; group-by-favorite and online count badge todo.
- Rate-limit guard — 429 backoff interceptor + isRateLimited(); every poller skips while rate-limited; stale-cache fallback.
- Cache profile/world/group lookups (VRCX "Fast Fetch") to cut API calls.
- Verify all VRChat write-actions live (favorite tags, requestInvite slots, invite instanceId format).
- Fixed: Home tab's VRChat News card could show "Could not load news." with no way to tell
why, and no fallback even if news had loaded fine moments earlier. Direct testing against
the live
hello.vrchat.comRSS feed this session found no reproducible parse/fetch bug — most likely a transient network blip on affected machines — so hardened rather than "fixed" a specific root cause: one retry on failure, real errors logged to console instead of swallowed, and falls back to the last successfully loaded news (marked "(cached)") instead of blanking the card. - Fixed: Weather never actually showed up in the VRChat chatbox even after enabling it
and adding
{weather}to a Status preset. It wasn't a fetch/data bug —{weather}worked as a token, but it had no dedicated Chatbox-tab source row (unlike Now Playing/Stats/Heart Rate), so it could only ever be typed manually into a preset, silently sharing the single rotation slot with every other preset. Added a proper "Weather" row with its own Off/Own line/Rotate toggle inmodules/vrchat/chatbox/chatboxComposer.js+renderer.js's source grid, and added the missing{weather}entry to the "Show all tokens" list. - Fixed: Linux build could freeze into "Not Responding" using OSCQR/ShazamOSC's
capture-source picker —
desktopCapturer.getSources()(main.js) had no timeout and could hang indefinitely under Wayland with no PipeWire portal available, freezing the whole app rather than just erroring. Added a 6s timeout race on both capture-source call sites, enabled Chromium's PipeWire capturer + Ozone auto-detection on Linux startup, and made the IPC handler resolve with an error field instead of rejecting. Also fixedcrashGuard.jsrunning Windows-only commands (tasklist/reg query/PowerShell) unconditionally on every platform every 15s with no guard (now a no-op off Windows), addedkoffi's native binaries toasarUnpack(VR Overlay's OpenVR bindings couldn'tdlopenfrom inside app.asar on any platform), and gave the Linux electron-builder target the same explicitfileswhitelist Windows already had (previously inherited the broad top-level glob and shipped extra files). Not verified on real Linux hardware — this dev environment is Windows-only, so these are targeted fixes for the specific reported symptoms (the exact "Failed to get sources" error message, an actual freeze/hang, and file-based evidence like the.mount_nekosuAppImage path in the reporter's screenshot), not a full Linux QA pass.
- Rebuild the app's overall layout/theme/navigation to match the look and feel of VRCNext — same layout style, not the same internal structure/feature set (ours stays different under the hood). Shipped in 1.0.61: wider sidebar with icon+label nav and section headers, Home tab as the default landing page (replaces Chatbox), VRChat news feed pulled from hello.vrchat.com as the homepage news card, welcome hero, metric tiles, Quick Access shortcuts, Recently Visited Worlds.
- VRChat Quick Launch tab (MVP) — multi-profile simultaneous VRChat launching via
VRChat's
--profile=Nflag, matching the multi-account launcher tooling VRCX/VRCNext-adjacent community tools provide. Seemodules/vrchat/launcher/quickLaunch.js+ the#quicklaunchtab. Covers: profiles (add/remove/VR toggle/description), Debug GUI / SDK log / UDON log / max FPS / free-text custom params, Instance info (Create/Join/Local/None, Create shares one instance across every profile launched together), Launch / Launch-all (2s stagger). Auto-detects the VRChat exe via the registeredvrchat://protocol handler, with a manual Browse override.- Per-instance OSC ports — turned out not to need a proxy at all: VRChat has a real,
documented launch flag
--osc=<inPort>:<outIP>:<outPort>(confirmed against docs.vrchat.com), so each profile N just gets passed--osc=9000+2N:127.0.0.1:9001+2Ndirectly (profile 0 = VRChat's own defaults, 9000/9001).renderer.js'sqlSyncOscPorts()then automatically mirrors every non-default profile's ports into Settings → OSC's existing "extra targets"/"extra receivers" lists (tracked via aquickLaunchOscManagedsettings key so it only touches entries it added itself, never the user's own manual OSC config) — so Chatbox/AudioLink/avatar-param OSC reaches every launched profile automatically, with no manual port entry needed. - Follow-up: MIDI device selection per profile. Deferred, not started.
- Follow-up: "Auto-layout" — automatically tile the launched VRChat windows on screen.
Would need raw Win32 window enumeration/positioning (via
koffi, already a dependency). Deferred, not started.
- Per-instance OSC ports — turned out not to need a proxy at all: VRChat has a real,
documented launch flag
- i18n foundation —
modules/i18n/i18n.js(main) + IPC (i18n:languages/i18n:strings)- renderer
t()/applyLanguage()sweep ([data-i18n]text,[data-i18n-ph]placeholders, nav labels viadata-tab). First-run language picker modal (shown once, ifuiLanguagesetting is unset) + a Settings → Language card for changing it later; switches instantly, no restart. Seeded with 102 languages (modules/i18n/locales/*.json, flat key→string maps, every non-English locale merges overen.jsonso a missing key always falls back to English instead of breaking) — the initial 7 (en, ja, es, ru, pl, nl, de) plus every language requested afterward: ko, zh, fr, ms, no, pt, ar, bn, hi, id, or, qu, sw, ta, ur, vi, wuu, xh, yo, zu, af, sq, am, hy, az, eu, be, bs, bg, my, ca, ceb, ny, co, hr, cs, da, eo, et, fi, fy, ka, el, ha, haw, he, hmn, hu, is, it, jv, kn, kk, km, rw, rn, ky, lo, lv, lt, lb, mk, mg, ml, mt, mi, mr, mn, ne, ps, fa, pa, ro, sr, si, sk, sl, so, su, sv, tl, tg, tt, te, th, bo, ti, to, tr, tk, ug, uk, uz, cy, yi. All 102 files validated for JSON correctness and exact key parity againsten.json(76 keys each). Several lower-resource languages (Quechua, Oriya, Chichewa, Kirundi, Wu Chinese, Xhosa, Yoruba, Zulu, Hawaiian, Hmong, and a few others) were flagged by the translating passes as worth a native-speaker review — functionally complete but some technical-UI terms are best-effort borrowings rather than fixed conventional terminology.
- renderer
- Coverage is partial by design — this pass only tags the sidebar nav, common
buttons, and the newly-added Avatar Scaling / Translator / Live Typing / language-picker UI.
The rest of the app (300–500+ static strings in
index.html, 400+ dynamicsetText/template-literal call sites inrenderer.js) is still hardcoded English. Sweeping it incrementally (tag moredata-i18n, wrap more dynamic strings int()) is ongoing work — add more locales here too as requested (a handful more beyond the initial 7 were flagged as wanted).
- Desktop-audio speech-to-text —
modules/integrations/osc/stt/desktopSttModule.js(renderer, reuses thegetDisplayMedia({audio:true})capture technique fromshazamOscModule.js) +modules/ai/speechToText.js(main). Both engines, user-selectable in the Translation tab: cloud (OpenAI/Groq Whisper-compatible/audio/transcriptions, sends the raw webm clip directly — no decode needed) and local (@huggingface/transformersrunning a small Whisper model fully offline in WASM, no native binary; renderer decodes the clip to 16kHz mono PCM viaOfflineAudioContextfirst, since that's the input shape the local model needs). Verified: both npm packages install and load cleanly, tesseract.js's worker + language download works in this environment. Not verified end-to-end: the full getDisplayMedia→MediaRecorder→transcribe round-trip needs a real desktop session with actual audio and a live API key — recommend a manual smoke test in the built app. - Bidirectional: transcribed text runs through the existing Translator
(
translateWithSettings) before being sent to chatbox and/or spoken aloud — same translate step Live Typing uses. - OCR screen-translate —
modules/integrations/osc/ocr/ocrTranslateModule.js, samegetDisplayMedia→ canvas capture scaffolding asoscQrModule.jswithjsQRswapped for Tesseract.jsrecognize(). 15 common OCR languages in the picker (English, Japanese, Spanish, Russian, German, French, Chinese, Korean, Arabic, Portuguese, Italian, Dutch, Polish, Ukrainian, Vietnamese) — Tesseract's language codes don't map 1:1 to the app's i18n codes, so this is a separate, smaller list. - Fixed: OCR failed to start with "worker script...must be an absolute path" in a
packaged build. Same root cause as an earlier ffmpeg-static fix this session: tesseract.js
computes its worker script path via its own internal
__dirname, which resolves to a path insideapp.asar, andworker_threadscan't load a script from inside an asar archive at all (unlikerequire()/fs, which Electron transparently redirects). tesseract.js is already unpacked from the asar (asarUnpack); the worker path just needed to actually point there - patched to swapapp.asarforapp.asar.unpackedin the computed default. A no-op in dev, where there's no asar at all. Verified by creating and tearing down a real tesseract worker with the fix applied. - TTS output,
modules/ai/ttsProviders.js, 15 engines selectable in the Translation tab, ported from TTS-Voice-Wizard for feature parity: Windows built-in (SAPI via PowerShellSystem.Speech.Synthesis, text piped over stdin — not interpolated into the command — so spoken text can't break out of the PowerShell command; verified working, detected real installed voices and played audio in this environment), TikTok TTS, ElevenLabs, OpenAI TTS, Google Cloud TTS, Azure Cognitive Speech, Amazon Polly (via@aws-sdk/client-polly— SigV4 request signing isn't reasonably hand-rolled correctly, so this is the one new npm dependency this pass added), IBM Watson TTS, Deepgram Aura, VoiceForge, UberDuck (multi-step: submit → poll → download), TTS Monster (submit → download), GLaDOS TTS (self-hosted) and Moonbase Voices (self-hosted local app), plus the existing self-hosted Piper/XTTS/other option. All new HTTP-based engines' request shapes were verified against mocked responses matching each vendor's real documented API. Note: where the reference project routes an engine through its own paid gateway (Google, IBM Watson, Deepgram all go through a Heroku backend there), this instead calls the real vendor API directly with the user's own credentials — no third-party paywall in between. - Fixed TikTok TTS — was hardcoded to a single community proxy (
gesserit.co) which had gone down; now tries a short list of known-working worker proxies in order and checks all the response field names different proxies use (data/audio/audioUrl) instead of just one. Verified live — successfully generated real audio in this environment. - Fixed: TikTok TTS had no voice picker. The unified TTS card's TikTok engine option had no way to choose a voice at all (always used the hardcoded default). Added a voice select populated from the same list the old standalone TikTok TTS card used, then removed that old standalone card entirely (it was in the Live tab, moved into the Translation tab at runtime, and became fully redundant once the unified card covered the same feature).
- TTS output-device picker — routes
<audio>-based engines to any enumerated output device viasetSinkId. Doesn't apply to SAPI/local engines that play through the OS directly. - Routing TTS into VRChat's mic input — not solvable in pure software. Windows has no way to expose one app's audio output as another app's microphone input without some kind of virtual audio device (VB-Cable, Voicemeeter, etc.), which requires installing a driver — there's no way around that one step. If the user installs one themselves, the output-device picker above will route TTS into it, which VRChat can then pick up as a mic.
- Fixed packaged-app crash: "Could not start screen capture — worker script... must be
an absolute path" (OCR) and the same class of failure would've hit local Whisper too.
tesseract.js/tesseract.js-coreload aworker_threadsscript, and@huggingface/transformers's dependencies (onnxruntime-node,sharp) load native.nodebinaries — neither can be loaded from inside an asar archive (Node needs a real file on disk for both). Addedbuild.asarUnpackinpackage.jsonfor all of these so electron-builder extracts them toapp.asar.unpacked/instead; Electron then transparently resolves paths into the unpacked location. This only manifests in a packaged build, notnpm start— couldn't be caught by this session's dev-mode smoke tests, only surfaced once actually installed.
- Wake-word assistant —
modules/vrchat/assistant/jarvisAssistant.js(renderer),modules/ai/assistantBrain.js(main, LLM-based command interpreter). Listens through an actual microphone (getUserMedia, selectable input device — same enumerateDevices() pattern as the AudioLink mic picker), checks each transcript for the configured wake word (defaultnova, user-customizable — deliberately not a common assistant name), and only acts on speech addressed to it. Reuses the Desktop STT card's speech-to-text engine/API-key settings, but captures its own separate mic audio — it does not listen to desktop/system audio (that's what the Desktop STT card itself is for, a different use case: translating what you hear, not commands you speak). - Fixed: was listening to the wrong audio source entirely. Originally used
getDisplayMedia(desktop/system audio, i.e. what's playing through your speakers) for the wake-word loop, so it could never hear the user's own voice no matter what mic was selected elsewhere — that's what "not detecting right mic" / "heard but no wake word" traced back to. The instant-replay screen+audio capture for SOS clips is now a separate, opt-in capture (its own checkbox, only requests a screen-share prompt if enabled) instead of being the same stream the wake-word listener used. - Fixed: "Sorry, I couldn't reach my brain just now" gave no way to diagnose the actual
problem.
assistantBrain.js's command-interpretation call (which reuses the IntelliChat AI provider settings) was swallowing the real HTTP error and always returning the same generic message. Now surfaces the actual cause (e.g. "Incorrect API key provided", or a connection error if a local provider like Ollama isn't running) directly in the reply. Also added upfront validation: if the AI provider was never configured at all (blank base URL and blank key — as opposed to a deliberately keyless provider like Ollama, which has a non-empty base URL),setLive(true)now fails immediately with a clear message pointing at Settings → IntelliChat, instead of only discovering the problem on the first spoken command. - Fixed: "who's online" always said nobody was, even when friends genuinely were. The
assistant's
getFriendswiring calledapi.vrchatFriends()(vrchatApi.getFriends(), offline bucket = false), whose friend objects (pickFriend()) never carry anonlinefield at all — that flag is only ever set by the reconciled all-friends call, which fetches both the online and offline buckets separately and tags each one.f.onlinewas alwaysundefinedfor every friend, so theonlinefilter was always empty regardless of who was actually online. Switched toapi.vrchatAllFriends(), which returns friend objects withonlinecorrectly set. Verified with a test using the real response shape fromvrchatApi.js's_getAllFriends(). - Commands: "is
<friend>online / which world" (reuses the friends list's existinglocation/worldId/instanceTypefields, same ones the Friends panel already renders), "who's online", "what's my status", "change my status to<text>" (setsstatusDescriptiononly — the assistant can never touch the bio field, enforced both in the LLM system prompt and in the code path itself, verified by a unit test), "what time/date is it" (answered from the system clock directly — never guessed by the LLM, which has no real way to know the actual current time), and free-form conversational replies for anything else. Responses are spoken aloud via the TTS engine above only — never posted to the VRChat chatbox (a spoken reply has no reason to also be text in-world). - SOS — manual trigger only. Either an explicit spoken "sos" command or the button in
the UI; never auto-triggered. On trigger: invites everyone in a configured trusted-friends
list (by display name, matched against your live friends list) to your current instance,
saves the rolling instant-replay clip (last 1/5/10 min of shared desktop video+audio,
configurable) to
Videos/NekoSuneAPPS/(created automatically if missing), and additionally uploads it to a configured Discord webhook if set, so those friends can see what happened before they arrive. Saving locally always happens, independent of whether a webhook is configured, so the clip is never lost to a failed/missing upload. - Fixed: assistant silently doing nothing. The most common cause is the cloud STT engine
being selected with no API key ever entered (a very likely fresh-install state) — every clip
then failed transcription with a generic error that looked identical to "the wake word didn't
match" from the outside.
setLive(true)now validates config upfront and fails immediately with a clear, specific message instead of silently retrying forever. The status line also now shows the actual configured wake word, and surfaces what was transcribed even when it doesn't match the wake word, so it's obvious whether STT is hearing anything at all versus just not recognizing the chosen word. - Soft emotional check-in — separate from SOS, and never triggers it. Every transcribed
clip (whether or not it's addressed to the assistant) is checked against a small lexical
cue-list (
modules/vrchat/assistant/emotionCues.js) for distress or tiredness language. This is honest keyword/phrase pattern-matching on the transcribed text — not real voice-tone or prosody analysis (pitch, pace, pauses), which would need raw-audio feature extraction, a much bigger undertaking. If a cue matches, the assistant just asks a caring check-in question ("are you doing okay? want me to notify someone?") — the user decides whether to actually trigger SOS. Never escalates on its own. - Not verified end-to-end — same caveat as the Desktop STT feature above: the full continuous-listening loop, the friend-status/status-query/status-set VRChat API calls, and the instant-replay buffer's long-running recording behavior all need a real account + a live desktop session to smoke-test; only the pure dispatch/command logic was unit-tested here (mocked callbacks, no live VRChat/audio).
- Never posts to chatbox — TTS only.
sendChatboxMessagewas removed from the assistant entirely;respond()only ever calls the configured TTS engine. - World-creation brainstorming partner + general Alexa-like assistant. Expanded the
command-interpreter system prompt (
assistantBrain.js) so the assistant is also a genuinely opinionated creative partner for VRChat world ideas/mechanics/pacing/naming, and a general conversational assistant (weather, news/current events, general questions, small talk) — while explicitly refusing to help write, debug, or explain code even if asked, since that's not what this assistant is for. - Weather + web search actions. Added
get_weather(reuses the app's existing Weather feature) andsearch_webactions. Web search is a user-selectable provider: SearXNG (modules/ai/webSearch.js, JSON API against a self-hosted instance, e.g.https://searxng.nekosunevr.co.uk/) or DuckDuckGo's Instant Answer API (no setup, no key, but explicitly limited — it only returns an abstract/infobox-style answer and often nothing at all for ordinary queries; offered as a no-setup fallback, not a like-for-like replacement). Results are summarized into a short spoken answer by the same AI provider used for command interpretation. - Fixed: SOS instant-replay clips were corrupted/unplayable ("clip is broken"). The
rolling buffer recorded continuously with a single
MediaRecorderand pruned old chunks by age — but only the very first chunk of a continuous recording contains the container's header (EBML/Segment/Tracks info), so once that chunk aged out, every exported clip was a webm file with no valid header at all. Most players either refused it or showed a single static/garbage frame, which is exactly what was reported. Rewrote the buffer to rotate to a fresh, fully self-contained recording every 10 seconds (REPLAY_SEGMENT_MS) and prune whole aged-out segments instead of chunks, so every segment always has its own valid header. Verified with a real ffmpeg-generated two-segment test that the exported result decodes cleanly end-to-end. - Clips are now real, playable .mp4 files (were .webm). The main process now stitches the
kept segments together with a bundled
ffmpeg-staticbinary (concatdemuxer + transcode to H.264/AAC,+faststart) instead of a raw byte-concatenation, producing one proper seekablesos-clip-*.mp4inVideos/NekoSuneAPPS/. The same finished .mp4 (not the raw segments) is what gets uploaded to a configured Discord webhook, so the webhook attachment can't hit the same corruption issue either.ffmpeg-static's binary is unpacked from the asar (likesharp/onnxruntime) since it can't be executed from inside the archive. - Fixed, then partially reverted: replay clips weren't actually capturing VRChat. First attempt made the shared screen-capture picker (used by OSCQR/Shazam/Desktop STT/OCR/the SOS replay buffer) auto-prefer a source whose window title contains "VRChat" over the whole screen — this backfired into clips coming out solid black, because Chromium's window-level capture on Windows uses GDI/BitBlt, which can't see hardware-accelerated DirectX/Vulkan swapchain content (exactly what a game renders into). Reverted to capturing the whole screen, which goes through the Desktop Duplication API and correctly composites whatever the GPU actually drew. If VRChat is on a second monitor, use the manual capture-source picker in Settings to choose that screen explicitly.
- Fixed: saving a clip spiked CPU/GPU (multiple
ffmpeg.exeat very high usage). The segment-rotation fix above always recorded webm/vp8 (software-encoded by Chromium) and then fully re-encoded through software libx264 on every save, regardless of source codec. Now records mp4/h264+aac directly when the system exposes a hardware encoder for it (Windows via Media Foundation — the normal case), which offloads the entire background recording off the CPU, not just the save step; saving then just stream-copies (remuxes) the already-correct-codec segments into the final file instead of re-encoding them, which is effectively free. Falls back to webm + a real transcode only on systems without a hardware mp4 encoder exposed to Chromium, or if a stream-copy remux ever fails for some other reason. Verified both the hardware-codec stream-copy path and the software fallback path decode cleanly end-to-end. - The fallback transcode (used when a stream-copy isn't possible) auto-detects and prefers a GPU encoder instead of always using CPU-only libx264: tries NVENC (Nvidia), then Quick Sync (Intel), then AMF (AMD), each confirmed with an actual throwaway encode (not just checking the encoder's compiled in — the hardware/drivers might still not be there), caching whichever one actually works for the rest of the process's lifetime, and only falling back to libx264 if none of them do. Verified live: this machine's NVENC was detected and a real webm→mp4 transcode through it produced a clean, fully decodable file.
- Fixed: voice assistant kept "hearing" the word "you" out of nowhere and burning through
the cloud STT rate limit. Root cause: every listen cycle transcribed unconditionally, even
ones that were just silence/room noise — precisely the condition Whisper is known to
hallucinate short stock phrases on ("you" being the classic one). Added a silence gate
(
isSilentSamples, RMS energy over the decoded clip against avadThreshold— exposed as a "Mic sensitivity" setting: quiet room / normal / noisy room) that skips transcription entirely for near-silent clips, plus a small second-layer filter for known Whisper hallucination phrases ("you", "thank you", "thanks for watching", etc.) so none of them ever reach wake-word matching or the "overheard" status line. Verified all three paths (silence skipped, hallucination filtered, real speech passes through) with a mocked capture/transcribe cycle. - Voice assistant no longer cuts off a long command mid-sentence. Recording used to be a
fixed
clipSeconds(default 4s) regardless of what was being said.captureClipnow uses live end-of-speech detection (recordUntilSilence): it waits at leastclipSeconds, then keeps extending in ~150ms polls for as long as it keeps detecting speech, only stopping once there's been ~1.2s of real silence after the last speech - up to a 15s hard cap so continuous background noise can't hold the mic open forever. An idle cycle (nothing said at all) still ends right at the minimum window, same as before. Verified with mocked audio: pure silence ends at ~1.1s, 3 seconds of continuous "speech" extends to ~4.1s before cutting off, and continuous noise hits the 15s cap. - Voice assistant: time in other regions. "What time is it in Eastern Time?" previously
always answered with local time regardless of what was asked.
get_timenow carries an optional IANA timezone (resolved by the AI provider itself) plus a human-readable place label;modules/vrchat/assistant/timezones.jsis a deterministic alias-table fallback covering common USA/Canada/EU/Asia names for when a smaller/local model doesn't reliably produce a real IANA name, and asks for clarification on names too broad to guess a single zone for (e.g. "Asia" alone spans ~9 time zones). Verified against USA, Canada, EU, and Asia example phrasings plus the ambiguous/unknown-place edge cases. - More local Whisper models + GPU acceleration. Added Medium, Large v3, and Large v3
Turbo (OpenAI's own ~8x-faster distilled variant — "faster whisper" without a separate native
runtime like the Python/CTranslate2 faster-whisper project, keeping this app's no-native-binary
approach for STT). Downloads happen automatically the first time a model is actually selected
and used, never eagerly, and reuse
@huggingface/transformers's own cache on subsequent runs (no re-download). Added a live download-progress bar since the larger models are multi-GB. GPU is now used automatically (DirectML on Windows / CUDA on Linux / CoreML on Mac) with a real fallback to CPU — initially tried the library's owndevice:'auto', which turned out to crash ("DML EP can only be used with CPU EPs") because it unconditionally mixes in a WebGPU provider that isn't actually usable from the main process; fixed by picking the platform GPU device explicitly with an actual try-then-fall-back-to-CPU. Verified live: DML pipeline creation succeeds standalone on this machine, and a full transcription run completes without error. - Fixed: local Whisper crashed with "ENOTDIR" and models appeared to re-download every
time.
@huggingface/transformers's own default cache lives under its own node_modules folder (.../node_modules/@huggingface/transformers/dist/.cache/), which on a per-machine Windows install is under Program Files — not writable by a normal, non-elevated user. Models now download to<userData>/models/whisper/<repo>/<model-name>/...instead (e.g.%AppData%/nekosuneapps/models/whisper/onnx-community/whisper-tiny.en/), always writable and easy to find/inspect. Verified live: files land in exactly that structure, and — since@huggingface/transformers's file cache only ever exposes a file at its final path once fully downloaded (downloads land in a.tmp.<pid>.<random>file first, renamed into place only on completion) — a second run with the same model loads straight from disk with an unchanged file timestamp, confirming no re-download and no risk of ever running against a partial file.
Prompted by Avatar Scaling's scale always resetting to 1.00m on restart. Cross-referenced every
<input>/<select>/<textarea> with an id in index.html (249 total) against what
renderer.js actually saves/restores, then manually verified every candidate (the mechanical
scan alone has a high false-positive rate for anything saved via a generic/indirect mechanism,
e.g. the TTS engine fields, which already save correctly through TTS_ALL_FIELD_IDS).
- Avatar Scaling scale never persisted at all — confirmed root cause of the reported
bug. The slider/number input called
setScale()but neversaveSetting, and the controller was always constructed with the defaultscale: 1. Now saved (debounced to twice/second, since a held hotkey ticks every 50ms) and restored on boot. - Avatar Scaling "Safety limits" default was inconsistent — the checkbox defaults to
checked in the HTML, but the boot-restore code's fallback default was
false, so a fresh install would silently start with safety off despite showing the box checked. Fixed totrue. - Hotkey recording gave no reason when it silently failed ("hotkey broken, doesn't
save"). The save/restore/re-arm wiring itself checked out correct (recording a key saves it
immediately and re-registers it on boot if enabled; the underlying PowerShell
WH_KEYBOARD_LLhook was verified end-to-end with a real synthetic keypress). The actual gap: if the hook process fails to actually install (blocked by antivirus, a restrictive execution policy, no .NET, etc.), "Record" just silently sat until an 8s timeout and reported the same generic "no key captured" — indistinguishable from "nothing was pressed in time", and since nothing ever got captured, nothing ever got saved either.keyHookPs.jsnow tracks why the hook process exited; the record flow health-checks after 2.5s and surfaces that real reason instead of waiting out the full timeout. -
enableVr(VR gear battery) had no persistence at all — only calledvrStart()/vrStop(), neversaveSetting, and had no boot-restore. Fixed to match the identicalenableNet/enableWindowpattern elsewhere in the same file. - Rusk Laserdome / Twitch Interactive settings only saved as a side effect of clicking
Start — editing a field while the feature was already running (or before ever starting it)
had no way to persist. Added autosave-on-change for both, carefully preserving whichever
enabledflag is already stored so a field edit alone can never flip a feature to auto-start on next launch (verified with a test for both the "never started" and "already running" cases). - Everything else flagged by the initial mechanical scan (~90 more elements) was verified to already persist correctly through a generic/object-based save (TTS engines, AI provider, autostart checkboxes, weather, Discord OSC/SpotiOSC, RealisticOscLeash, OSC Digital Clock, ShazamOSC, ranks, ToN notify/manager) or is intentionally transient by design (search/query boxes, one-off action parameters like which soundpad slot to trigger, live manual faders, credential fields that save on an explicit Connect action like Discord/HeartRate providers, and the VRChat login password specifically must never be saved at all).
- Run
npm install(addsdiscord.js,sql.js). - VRChat-API features need login on the VRChat tab (cookies stored locally; password never stored).
- History DB:
nekosuneapps-history.sqlitein the app's user-data folder. - New this session: Avatar Scaling, Translator, Live Typing chatbox, and the
i18n foundation (see sections above). No new npm dependencies were added — the
global-hotkey approach was switched from a third-party key-listener package (flagged by
antivirus) to a PowerShell-based
WH_KEYBOARD_LLhook (modules/vrchat/osc/keyHook.ps1+keyHookPs.js), matching the existing shell-out pattern already used bymediaKeys.js.