Tui Bug Hunt
uw-syfi/vibesys
Drive the VibeSys terminal UI (TUI) headlessly via tmux against real Claude-Code-provider runs, to find and report display bugs, frontend/interaction glitches, backend/protocol problems, and…
A skill your agent uses when working on the Trilium Electron desktop app (apps/desktop) — adding or changing an electronApi method / IPC channel, touching preload.ts, main.ts, services/window.ts or…
$ npx skills add TriliumNext/Trilium --skill developing-electron-desktop -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install TriliumNext/Trilium developing-electron-desktop --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/TriliumNext/Trilium.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/developing-electron-desktop .claude/skills/developing-electron-desktop && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "developing-electron-desktop" agent skill from https://github.com/TriliumNext/Trilium/tree/main/.claude/skills/developing-electron-desktop into .claude/skills/developing-electron-desktop/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "developing-electron-desktop", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/TriliumNext/Trilium/tree/main/.claude/skills/developing-electron-desktopType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add TriliumNext/Trilium --skill developing-electron-desktop -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install TriliumNext/Trilium developing-electron-desktop --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/TriliumNext/Trilium.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/developing-electron-desktop .agents/skills/developing-electron-desktop && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "developing-electron-desktop" agent skill from https://github.com/TriliumNext/Trilium/tree/main/.claude/skills/developing-electron-desktop into .agents/skills/developing-electron-desktop/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "developing-electron-desktop", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add TriliumNext/Trilium --skill developing-electron-desktop -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install TriliumNext/Trilium developing-electron-desktop --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/TriliumNext/Trilium.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/developing-electron-desktop .cursor/skills/developing-electron-desktop && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "developing-electron-desktop" agent skill from https://github.com/TriliumNext/Trilium/tree/main/.claude/skills/developing-electron-desktop into .cursor/skills/developing-electron-desktop/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "developing-electron-desktop", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/TriliumNext/Trilium.git --path .claude/skills/developing-electron-desktop--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add TriliumNext/Trilium --skill developing-electron-desktop -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install TriliumNext/Trilium developing-electron-desktop --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/TriliumNext/Trilium.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/developing-electron-desktop .gemini/skills/developing-electron-desktop && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "developing-electron-desktop" agent skill from https://github.com/TriliumNext/Trilium/tree/main/.claude/skills/developing-electron-desktop into .gemini/skills/developing-electron-desktop/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "developing-electron-desktop", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install TriliumNext/Trilium developing-electron-desktopInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add TriliumNext/Trilium --skill developing-electron-desktop -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/TriliumNext/Trilium.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/developing-electron-desktop .github/skills/developing-electron-desktop && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "developing-electron-desktop" agent skill from https://github.com/TriliumNext/Trilium/tree/main/.claude/skills/developing-electron-desktop into .github/skills/developing-electron-desktop/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "developing-electron-desktop", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add TriliumNext/Trilium --skill developing-electron-desktop -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install TriliumNext/Trilium developing-electron-desktop --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/TriliumNext/Trilium.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/developing-electron-desktop .opencode/skills/developing-electron-desktop && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "developing-electron-desktop" agent skill from https://github.com/TriliumNext/Trilium/tree/main/.claude/skills/developing-electron-desktop into .opencode/skills/developing-electron-desktop/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "developing-electron-desktop", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
developing-electron-desktopA skill your agent uses when working on the Trilium Electron desktop app (apps/desktop) — adding or changing an electronApi method / IPC channel, touching preload.ts, main.ts, services/window.ts or…
Developing Electron Desktop is an agent skill from TriliumNext/Trilium. Use when working on the Trilium Electron desktop app (apps/desktop) — adding or changing an electronApi method / IPC channel, touching preload.ts, main.ts, services/window.ts or any main-process service (tray, printing, dialogs, import/export, spellcheck, autostart, security settings), the trilium-app:// protocol, launching or debugging the desktop build, or writing tests for desktop code. Covers the process/security model, the four-file recipe for a new Electron API (plus the handler-module map, the…
Its SKILL.md is about 5.7k tokens, which your agent loads only when the skill is triggered. The skill folder holds 4 other files, including scripts and reference files (for example `references/protocol-and-security-triage.md`).
It sits in Knowledge Management, covering Responsive design. It works with pnpm. The repository describes itself as: Build your personal knowledge base with Trilium Notes. The licence is AGPL-3.0.
4 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 134a865. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Ships 1 file in scripts/ (JavaScript), which the agent can run.
Shell commands in SKILL.md call:
pnpmnodenpxFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md. Its commands use pnpm and npx, which can reach the network depending on how they are called.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Developing Electron Desktop loads about 5.7k tokens when it runs, and up to ~7.9k if it reads all its reference files. Until then it costs about 229 tokens; SKILL.md has 2,328 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); the scripts in this folder are not scanned.
The full file from TriliumNext/Trilium at commit 134a865, republished under its AGPL-3.0 licence (© TriliumNext). 2,328 words, ~5,720 tokens.
.claude/skills/developing-electron-desktop/SKILL.md (or your agent's skills folder). This skill also uses 2 other files; get the full folder from GitHub.apps/desktop runs the server and the client in one Electron process: the main process boots @triliumnext/core + the Express app from @triliumnext/server, and the renderer is the ordinary apps/client bundle. There is no HTTP between them — see "How the renderer reaches the server" below. apps/desktop/src/main.ts is the entry point; services/window.ts creates windows and owns the window/spellcheck IPC.
apps/desktop/
src/main.ts # startup: platform provider, core init, Express app, windows, tray, IPC setup
src/preload.ts # the ONLY bridge renderer ↔ main (contextBridge → window.electronApi)
src/protocol.ts # trilium-app:// scheme → dispatch into Express in-process
src/ipc_messaging_provider.ts # replaces the WebSocket with ipcMain/webContents.send
src/platform_provider.ts # DesktopPlatformProvider (isElectron, getEnv, crash)
src/services/window.ts # BrowserWindow creation, webPreferences, window/spellcheck/nav IPC
src/services/*.ts # one module per concern: tray, printing, dialog, import, export,
# restore, shell, auto_launch, backup_passphrase, security_settings,
# custom_dictionary, onenote (+ loopback_oauth), referer, request,
# startup_metrics, web_contents_security
src/*.spec.ts, services/*.spec.ts # vitest, `pnpm --filter desktop test`
spec/build-checks/artifacts.spec.ts # verifies the built dist
e2e/ # Playwright against the built app (`pnpm --filter desktop e2e`)
scripts/build.ts # esbuild bundle + asset copy into dist/
electron-forge/ # packaging (forge.config.ts, icons, dmg, portable/safe-mode launchers)
# flip-fuses.ts + trim-locales.ts are ALSO run as scripts by the
# Flathub manifest — see the packaging-for-flathub skill before editingnodeIntegration: false, contextIsolation: true, webviewTag: true on every window (services/window.ts). The renderer has no Node, no require("electron"), no @electron/remote (removed — never reintroduce it). Everything crosses through the preload bridge.web_contents_security.ts vets every <webview> in will-attach-webview and decides window.open: the app shell's root URL becomes an extra window in the opener's renderer process (that is how the client opens new windows — no IPC channel), everything else is denied and allow-listed URLs are routed to the OS. If a new feature needs a popup or a webview privilege, change it there — never relax webPreferences at the call site.dialog.ts, import.ts, restore.ts) and hand back a location the user chose; import.ts mints single-use access grants rather than accepting a path; shell.ts gates every channel with a validator that throws. Follow the same shape for anything that touches the filesystem or the OS.security_settings.ts reads data_dir/security.json, backup_passphrase.ts uses the OS keyring — the passphrase must not travel inside the backup it protects.services/request.ts (ElectronRequestProvider) uses Electron's net so sync honours the system proxy; referer.ts keeps hosts that require an http(s) Referer working from the trilium-app:// origin.trilium-app://app/, a privileged custom scheme (protocol.ts). registerTriliumAppScheme() must run before app.ready (Electron ignores registerSchemesAsPrivileged afterwards and navigation aborts with (blocked:origin)); setupTriliumAppProtocol(expressAppPromise) installs the handler that synthesises a Node request/response and dispatches through the real Express app — session, CSRF, multer and error middleware all run. Requests arriving before the server is built simply wait on the promise. apps/server/src/services/electron_request.ts tags them so auth/CSRF middleware can tell them from external TCP traffic. The same two functions are reused by apps/edit-docs.ipc_messaging_provider.ts implements MessagingProvider over webContents.send / ipcMain.on, one client per webContents.id; the client side picks it up through window.electronApi.ws (apps/client/src/services/ws.ts). Don't open a TCP WebSocket from desktop code.coreInitializedPromise / expressAppPromise in main.ts).main() prologue runs before the databaseEverything from the top of main() down to dbProvider.loadFromFile(…) runs before app.on("ready") and long before initializeCore() wires core's SQL layer. Chromium switches (app.commandLine.appendSwitch, app.disableHardwareAcceleration) must be applied before ready, which forces that shape — current readers are lang via getElectronLocale(), smoothScrollEnabled (#10559) and hardwareAccelerationEnabled (#10572). Three rules hold in that window:
options.getOptionOrNull() always returns null there. It falls back to getSql(), which throws before the provider is wired, so an option-derived switch silently takes its default (#10559). Read pre-ready values with readDbOption(dbProvider, name) instead.app, config and dataDirs are static imports precisely so ready cannot fire before the database is open. Adding an await — including a await import(…) for something already statically available — reintroduces the race that lands a switch too late.BetterSqlite3Provider the switches read from is the same instance handed to initializeCore({ dbConfig: { provider } }); don't open a second connection. The single-instance lock check stays before the open, so a second launch exits without touching the file.Four files, always together:
packages/commons/src/lib/electron_api_interface.ts (ElectronWindowApi, ElectronShellApi, ElectronPrintingApi, …; the groups are listed on ElectronApi at the bottom of the file, each with a one-line purpose). New concern → new ElectronXxxApi interface and a new key on ElectronApi. Doc-comment the method: the client only sees this file.apps/desktop/src/preload.ts inside the matching group of the contextBridge.exposeInMainWorld("electronApi", { … } satisfies ElectronApi) literal. satisfies makes the typecheck fail until preload matches the interface. Keep the preload thin: marshal arguments to ipcRenderer.send / invoke / sendSync; no logic.ipcMain handler in the service module that owns the concern (services/shell.ts, services/printing.ts, …, each with a setupXxxHandlers() called from main.ts), or in setupWindowing() in services/window.ts for window/spellcheck/navigation. Channel names are kebab-case verbs (open-external, set-full-screen, backup-passphrase-set). Pick the IPC style by shape:ipcMain.on(channel, handler) — fire-and-forget (ipcRenderer.send);ipcMain.handle(channel, handler) — async request/response (ipcRenderer.invoke);ipcMain.on + event.returnValue = … — synchronous query (ipcRenderer.sendSync); use sparingly, it blocks the renderer.
Main → renderer events go the other way: webContents.send(channel, …) in main, an ipcRenderer.on subscription exposed as onXxx(callback) in preload.apps/desktop/src/preload.spec.ts (asserts the exposed API shape and that each method sends/invokes the right channel; extend the exposedApi assertions) and the owning service's *.spec.ts for the handler.Then call it from the client as window.electronApi?.group.method() — always optional-chained, since the same client runs in the browser. window.electronApi is declared in apps/client/src/types.d.ts; gate desktop-only UI on isElectron() from apps/client/src/services/utils.ts (server side: utils.isElectron).
main.ts calls its setupX()ipcMain.on/handle only registers when the module's setup function actually runs, and there is no startup error if you forget. The symptom is silent at the source: a send channel no-ops, and a sendSync channel hangs the renderer forever, because synchronous IPC blocks the whole renderer process waiting for a reply that never comes. The setup calls live in one block in main.ts (plus ipcMessaging.init() further down). Adding a module means adding the call there.
The preload API group name does not map 1:1 to a handler module — infer from this table, not from the group:
setup fn (called in main.ts) | module | channels it owns |
|---|---|---|
setupWindowing() | services/window.ts | the bulk (~31): window lifecycle, zoom, theme, title bar, full-screen, min/max, dev tools, background material, navigation-history*, clipboard (copy-image-to-clipboard, read-clipboard-text), spellchecker language channels, web-contents-action |
setupShellHandlers() | services/shell.ts | open-external, open-path, show-item-in-folder, open-file-url, download-url, open-custom |
setupPrintingHandlers() | services/printing.ts | print-note, export-as-pdf, export-as-pdf-preview, save-pdf, get-printers, print-from-preview, print-progress |
registerSecurityIpcHandlers() | services/security_settings.ts | security-set-backend-scripting, security-set-sql-console, security-set-lan-access (all three via registerToggleHandler) |
setupStartupMetricsIpc() | services/startup_metrics.ts | report-startup-metric |
setupSystemTray() | services/tray.ts | reload-tray |
setupCustomDictionary() | services/custom_dictionary.ts | add-word-to-dictionary |
ipcMessaging.init() | ipc_messaging_provider.ts | trilium-ws-from-renderer (the ws bridge; the channel names are the IPC_FROM_RENDERER/IPC_TO_RENDERER constants) |
Note the splits that defeat guessing by group: spellcheck's add-word-to-dictionary is in custom_dictionary.ts while the spellchecker-language channels are in window.ts, and the clipboard handlers live in window.ts, not a clipboard module.
Mismatch it and the failure is silent or fatal, never a clear error:
| Renderer need | preload call | main side | if mismatched |
|---|---|---|---|
| fire-and-forget, no return | ipcRenderer.send(ch, …) | ipcMain.on(ch, (event, …) => {}) | sending to a handle-only channel is a silent no-op |
| synchronous value (blocks renderer) | ipcRenderer.sendSync(ch, arg) | ipcMain.on(ch, (event) => { event.returnValue = x }) | forgetting event.returnValue hangs the renderer |
| async value (Promise) | ipcRenderer.invoke(ch, …) | ipcMain.handle(ch, async (event, …) => x) | no handle registered ⇒ the promise rejects "No handler registered for '<ch>'" |
| main → renderer push | ipcRenderer.on(ch, cb) + an unsubscribe | webContents.send(ch, data) (not ipcMain) | push channels have no ipcMain handler — don't "fix" their absence |
Multiplexed channel: navigation-history is one channel serving several preload methods via a method-name first argument and an event.returnValue switch.
An unhandled throw inside an ipcMain.on listener crashes the entire main process — there is no renderer-side rejection to catch it. shell.ts carries this warning verbatim next to open-custom. Wrap every handler body:
electron.ipcMain.on("my-channel", (_event, arg: string) => {
try {
doThing(validateArg(arg));
} catch (e) {
getLog().error(`my-channel failed: ${coreUtils.safeExtractMessageAndStackFromError(e)}`);
}
});For an ipcMain.handle whose contract is Promise<string>, the catch should also return the error string, since the renderer awaits it — that is what open-path/open-file-url do.
ipc-parity.mjsA direction-aware parity diff across interface ↔ preload ↔ ipcMain handlers ↔ spec. Run it after wiring a channel, or to audit drift:
node .claude/skills/developing-electron-desktop/scripts/ipc-parity.mjsIt reports renderer→main channels with no handler (these hang or no-op), transport/handler-kind mismatches, handler modules whose setupX() is never called, handlers with no preload caller (print-note/export-as-pdf are known legacy orphans), and preload channels with no preload.spec.ts assertion. It whitelists push-only channels and is channel-granular, so the multiplexed navigation-history doesn't false-positive. Exit code 1 on a fatal finding.
The renderer is XSS-reachable, so it is untrusted. Every fs/shell/url channel validates in the main process and throws on violation. The five validators in apps/desktop/src/services/shell.ts are exported and unit-tested:
validateOpenExternalUrl — scheme allowlist from SHELL_OPEN_EXTERNAL_PROTOCOLS (commons); blocks Follina (ms-msdt:/search-ms:), the smb:/ldap: NTLM leak, and file:/data:/jar:.validateOpenPath / validateOpenCustomPath — canonicalize and sandbox to the data dir / tmp dir; implicitly blocks UNC paths and traversal; reject null bytes and nonexistent files.validateOpenFileUrl — require file: with an empty hostname (blocks the file://attacker/share UNC NTLM leak); normalize file://C:/ → file:///C:/.validateDownloadUrl — same-origin lock by scheme + hostname + port. It cannot use URL.origin, because the custom scheme is opaque-origin ("null").Add a validator for any new channel that takes a path, a URL, or anything else the main process will act on.
import { t } from "i18next" with keys in apps/server/src/assets/translations/en/server.json. Never hardcode.process.platform; code shared with core uses isElectron()/isMac()/isWindows() from @triliumnext/core utils (functions, only after initializeCore()).src/preload.compiled.cjs, gitignored) — dev by scripts/electron-start.mts, prod by apps/desktop/scripts/build.ts — because Electron's sandboxed renderer can only load CJS preloads. Don't import ESM-only things into preload.ts.scripts/build.ts builds src/main.ts with buildBackend(..., { format: "esm" }): the
production entry is dist/main.mjs plus lazy chunks under dist/chunks/ (the generated
dist/package.json points Electron's main at it). The preload and image_worker.cjs stay
CJS — the sandboxed renderer can't load an ESM preload, and the worker is spawned by its
.cjs path. Three rules follow:
__dirname in bundled code means the bundle root, even inside a chunk — the ESM banner
in scripts/build-utils.ts resolves a chunk's __dirname one level up on purpose, because
bundled code locates preload.cjs, image_worker.cjs and assets/ as siblings of the
entry. Don't "simplify" the banner, and don't path-math around it in app code.const mod = await import("cjs-pkg"); const { x } = mod.default ?? mod;. Destructuring the namespace directly
yields undefined in split ESM output, and unit tests mock past it. After adding a seam,
run node .claude/skills/analyzing-backend-bundle/check-dynamic-imports.mjs apps/desktop/dist.import() at its (async) call site, so it
lands in a lazy chunk instead of the startup path — measured on identical boots, ESM +
seams took the desktop main process from 348 MB to 287 MB RSS. The
analyzing-backend-bundle skill has the measurement tools and the seam patterns.session.setSpellCheckerLanguages() force-enables spell check. Upstream runs prefs.SetBoolean(kSpellCheckEnable, !langs.empty()), so a non-empty language list clobbers an earlier setSpellCheckerEnabled(false) — the symptom is spell check reactivating on every launch even though the option is off (#10569). Set the languages first and setSpellCheckerEnabled(enabled) last; setupSpellcheckForSession() and applySpellcheckLanguages() in services/window.ts both re-assert the option afterwards for this reason.electron.net joins repeated header values with a bare comma, where Node's http joins cookie arrays with "; " — which is why server↔server sync never hits this and desktop sync does. A Cookie header replayed from a raw set-cookie array arrives as …HttpOnly,trilium.sid=x, which cookie.parse reads as one junk key: the session cookie is lost and sync 401s with "Logged in session not found" as soon as a response carries any second Set-Cookie (a load-balancer affinity cookie) before Trilium's. absorbSetCookies() in apps/server/src/services/request.ts merges by name into a single "; "-joined string; keep any header a new net-based request path sends pre-joined (#10548).| Command | What it does |
|---|---|
pnpm desktop:start | dev app on port 37743, data in apps/desktop/data, Electron profile in data-electron-37742; HTTP cache disabled in dev so stale prod assets don't shadow fresh output |
pnpm desktop:start-prod | build + run dist/ like a release (port 37841, separate data dirs) |
pnpm --filter desktop electron-forge:make / :package | full installers / unpacked app |
pnpm --filter desktop e2e | Playwright against dist/main.mjs (builds first) |
Known launch-time noise and failures — do not "fix" these in app code:
TypeError: Cannot read properties of undefined (reading 'commandLine') from main.ts (app.commandLine.appendSwitch(...)) can appear in the console of Electron-based launches (desktop:start, edit-docs:edit-docs). The app runs correctly; ignore it unless the user raises it as a bug.TypeError: Not running in an Electron environment! (from electron-is-dev) at startup means the shell inherited ELECTRON_RUN_AS_NODE=1 — common inside VS Code's extension host and AI coding agents. require("electron") then resolves to the npm stub's path string. Unset it before launching: unset ELECTRON_RUN_AS_NODE (bash/zsh) or Remove-Item Env:ELECTRON_RUN_AS_NODE -ErrorAction SilentlyContinue (PowerShell).gtk-version=3 and GlobalShortcutsPortal switches in main.ts are deliberate workarounds (Electron GTK 4 crash; Flatpak/Wayland global shortcuts).pnpm --filter desktop test [pattern] — Vitest, node environment, src/**/*.spec.ts. Specs vi.mock("electron", …) and assert on the recorded ipcMain/ipcRenderer calls (see preload.spec.ts, services/shell.spec.ts for the pattern). vitest.config.mts sets ELECTRON_OVERRIDE_DIST_PATH so a dynamic import("electron") doesn't blow up where the binary isn't installed — don't remove it.TRILIUM_INTEGRATION_TEST: "memory"), so it is slower than a client spec; keep the pattern narrow.spec/build-checks/artifacts.spec.ts asserts the contents of a built dist/ (client, assets, better-sqlite3, …). It is outside the default include, so run it explicitly (npx vitest run --config vitest.build.config.mts) after pnpm desktop:build when touching scripts/build.ts or the asset copies (schema.sql, llm/skills, share-theme/templates). Known broken as of 2026-08: it dies at import time — resource_dir.ts calls process.exit(1) under vitest because TRILIUM_RESOURCE_DIR is unset in that config — before running any assertion, with or without your changes. Fix the harness env or verify the dist by hand; don't read the failure as caused by your change.When the symptom isn't a missing channel but the renderer page itself failing — white screen, STATUS_BREAKPOINT, (blocked:origin), SSE that never streams, a blocked <webview>, a denied permission — the cause is protocol.ts or web_contents_security.ts, not the IPC bridge. Symptom → cause → fix table: references/protocol-and-security-triage.md.
| File | What it covers |
|---|---|
| references/protocol-and-security-triage.md | trilium-app:// and the WebContents boundary: STRIPPED_HEADERS/STATUS_BREAKPOINT, the privileged-scheme registration ordering, the SSE streaming bridge, the frame-origin guard and its trust model, <webview> attach hardening, the permission allowlist, window-open and navigation policy, the YouTube embed referer. |
| scripts/ipc-parity.mjs | Runnable parity check across interface ↔ preload ↔ handlers ↔ spec (see above). |
Related skills: writing-unit-tests (the vi.mock("electron") pattern these specs use), building-client-ui (the renderer side that calls window.electronApi), developing-capacitor-mobile (the other non-browser runtime).
© TriliumNext, AGPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 2 other files (scripts, references) in .claude/skills/developing-electron-desktop of TriliumNext/Trilium.
Open the folder on GitHubat commit 134a865
Developing Electron Desktop next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Developing Electron Desktop this skillTriliumNext/Trilium | 38k | — | ~5.7k | Automated safety check: Pass | AGPL-3.0 | |
| Tui Bug Huntuw-syfi/vibesys | 103 | — | ~6.5k | Automated safety check: Pass | MIT | |
| Aholo Viewer Frontend Designmanycoretech/aholo-viewer | 1.1k | — | ~348 | Automated safety check: Pass | Apache-2.0 | |
| @pierre/diffs Code Renderingpierrecomputer/pierre | 6.2k | 2 repos | ~803 | Automated safety check: Pass | Apache-2.0 | |
| Figma Design Knowledge GraphEgonex-AI/Understand-Anything | 85k | — | ~1k | Automated safety check: Pass | MIT | |
| Gearcoleco Debuggingdrhelius/Gearcoleco | 141 | — | ~3.5k | Automated safety check: Pass | GPL-3.0 |
uw-syfi/vibesys
Drive the VibeSys terminal UI (TUI) headlessly via tmux against real Claude-Code-provider runs, to find and report display bugs, frontend/interaction glitches, backend/protocol problems, and…
manycoretech/aholo-viewer
Guides visual design and polish for the Aholo Viewer website and Playground, staying inside the existing Astro site and scoped CSS layers with a restrained technical tone.
pierrecomputer/pierre
Guides an agent through using @pierre/diffs to render syntax-highlighted files and diffs, and to build editing and review surfaces in React or plain JavaScript.
Egonex-AI/Understand-Anything
Scans a Figma file through the Figma REST API and builds an interactive knowledge graph of its pages, screens, components and tokens for the design dashboard.
drhelius/Gearcoleco
Debug and trace ColecoVision and Super Game Module games using the Gearcoleco emulator MCP server.
theodo-group/debug-that
Debug applications using the dbg CLI debugger. An agent skill from theodo-group/debug-that.
TriliumNext/Trilium
A skill your agent uses when cutting, preparing, or debugging a Trilium release — bumping the monorepo version, tagging, or diagnosing a failed "Release" workflow run.
TriliumNext/Trilium
A skill your agent uses when adding a DB migration or a new column/field to a Becca entity in Trilium ("add a migration", "new column on notes/attributes", "ALTER TABLE", "add a field to…
TriliumNext/Trilium
A skill your agent uses when adding, moving, or wiring an internal REST endpoint in Trilium (a new /api/ route) — choosing between a core-shared handler (packages/trilium-core/src/routes/index.ts…
TriliumNext/Trilium
A skill your agent uses when adding, changing, or reviewing an LLM/MCP tool in Trilium (the defineTools definitions under packages/trilium-core/src/services/llm/tools/ —…
TriliumNext/Trilium
Write, extend, and review CKEditor 5 plugins in the Trilium (TriliumNext Notes) monorepo — the rich-text-note editor under packages/ckeditor5, whose plugins live in src/plugins/.
TriliumNext/Trilium
Testing CKEditor 5 plugins in the Trilium monorepo. An agent skill from TriliumNext/Trilium.
Works with
A skill your agent uses when working on the Trilium Electron desktop app (apps/desktop) — adding or changing an electronApi method / IPC channel, touching preload.ts, main.ts, services/window.ts or…. Developing Electron Desktop is an agent skill from TriliumNext/Trilium.ts or any main-process service (tray, printing, dialogs, import/export, spellcheck, autostart, security settings), the trilium-app:// protocol, launching or debugging the desktop build, or writing tests for desktop code.
Developing Electron Desktop fits situations like: working on the Trilium Electron desktop app (apps/desktop) — adding; changing an electronApi method / IPC channel; touching preload.ts; services/window.ts.
Run `npx skills add TriliumNext/Trilium --skill developing-electron-desktop -a claude-code`. Or copy the skill folder (.claude/skills/developing-electron-desktop in TriliumNext/Trilium) into .claude/skills/developing-electron-desktop in your project. Claude Code loads it when a task matches its description.
Run `npx skills add TriliumNext/Trilium --skill developing-electron-desktop -a codex`. Or copy the skill folder (.claude/skills/developing-electron-desktop in TriliumNext/Trilium) into .agents/skills/developing-electron-desktop in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add TriliumNext/Trilium --skill developing-electron-desktop -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/developing-electron-desktop, .gemini/skills/developing-electron-desktop, .github/skills/developing-electron-desktop and .opencode/skills/developing-electron-desktop in your project.
Going by SKILL.md and its folder, Developing Electron Desktop needs JavaScript for the scripts in its folder and the command-line tools its instructions call (pnpm, node and npx). Our summary lists: Node.js.
SKILL.md contains no URLs. Its commands use npx, which can reach the network depending on how they are called. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. The check reads SKILL.md only: the scripts in the folder are not scanned, so read them before running anything.
Developing Electron Desktop is published under the AGPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 5.7k tokens (SKILL.md is roughly 23k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 2.2k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Developing Electron Desktop: Tui Bug Hunt (uw-syfi/vibesys, 103 stars), Aholo Viewer Frontend Design (manycoretech/aholo-viewer, 1.1k stars), @pierre/diffs Code Rendering (pierrecomputer/pierre, 6.2k stars) and Figma Design Knowledge Graph (Egonex-AI/Understand-Anything, 85k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
TriliumNext (a GitHub organization) maintains it in TriliumNext/Trilium, which has 38,231 GitHub stars. The repository holds 22 skills in this directory. The repository was last updated on October 7, 2026.
Source: TriliumNext/Trilium on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.