Imported from sapategu/podomoro-deno (
AGENTS.md). Install upstream withnpx skills add sapategu/podomoro-deno. Copyright stays with the author.
AGENTS.md
Working notes for anyone — human or agent — changing this repository.
Cadence is a focus-session timer built on deno desktop (Deno 2.9), targeting Fedora / GNOME. Read
README.md first for what the app is; this file covers how to work on it and the runtime traps that cost real
time to discover.
Commands
deno task doctor # check this machine's desktop integration support
deno task dev # build + run
deno task build # distributable bundle -> dist/Cadence/
deno task test # 25 tests, no display server needed
deno task verify # check + lint + test — run before every commit
deno task verify is the gate. Do not commit with it failing.
Dependencies live in deno.json and are pinned in deno.lock. There is no install step; do not add a
node_modules or a package manager.
Verified runtime behaviour
These were established empirically against Deno 2.9.3 + laufey webview 0.5.0 on Fedora. The published docs at https://docs.deno.com/runtime/desktop/ are wrong or silent on every one of them. If a change depends on desktop API behaviour, verify it against the installed runtime rather than trusting doc prose — write a throwaway probe, build it, run it, read the error.
-
Menu items are an externally tagged union, not the flat objects the docs show:
{ item: { label, id?, accelerator?, enabled, checked? } } // enabled is REQUIRED { submenu: { label, items } } // items is REQUIRED "separator" { role: { role: "quit" } }A flat
{ label, id, enabled }fails withserde_v8 error: length mismatch, got: 3, expected: 1. That error means "wrong union shape", not "wrong argument count". -
setApplicationMenuandshowContextMenuareBrowserWindowmethods, notDeno.*functions.showContextMenu(x, y, items)takes three positional arguments in screen coordinates. -
executeJsresolves to{ ok, value }, not the bare value, and cannot await a promise — it returns{ ok: false, value: "Unsupported result type" }. -
Tray.setIconrequires raw PNG bytes (Uint8Array). A file path string is rejected withexpected typed ArrayBufferView. This is whysrc/png.tsexists. -
Deno.Trayis a no-op on Linux.new Deno.Tray(),setIcon,setTooltipandsetMenuall return cleanly,trayIdis always0,getBounds()is alwaysnull, and no StatusNotifierItem is ever registered on the session bus — verified on both thewebviewandcefbackends with a working AppIndicator host present. The icon simply never appears.TrayController.verifyRegistration()detects this by fingerprinting the watcher's registered-item list before and after init, and self-corrects once the runtime implements the tray. Do not "fix" the tray by removing that check. -
The constructor's
titleoption never reaches the window manager. A window that is not given an explicitsetTitle()renders with the literal title(null).navigate()then replaces the title with the URL, so set it again after navigating — seeopenAuxWindowinsrc/main.ts. -
setPositiondoes nothing on Wayland. A Wayland client cannot position its own surface; the call is accepted and the window stays put.setSize,setOpacityandsetAlwaysOnTopdo work. -
Frameless windows are unusable on Wayland.
frameless: trueworks, but WebKitGTK does not support-webkit-app-region: drag(CSS.supports(...)isfalse) andsetPositionis ignored, so nothing can move the window. Native decorations are deliberate — do not switch to a custom headerbar without solving dragging first. Native windowmousedown/mousemoveevents report window-relative coordinates in bothclientXandscreenX, so they cannot substitute for screen coordinates either. -
The first
BrowserWindowcreated is adopted as the main window and auto-navigated to/. Create it synchronously at startup, or the runtime makes its own and you end up with a second blank window.navigate()called later works normally — seeopenAuxWindowinsrc/main.ts. -
win.bind()bindings do not work. The name registers, then every call rejects withNo callback bound for: <name>. The app usesDeno.serve+fetch+ SSE instead. The binding API is typed insrc/native/types.tsfor whenever it is fixed — do not migrate to it without verifying it first. -
Permissions are baked in at compile time (
deno desktop -A ...), not at run time. A missing permission surfaces as aNotCapableerror inside the built app, not at build time. -
deno desktop main.tsbuilds; it does not run. The output is a directory bundle and the executable is the inner binary (dist/Cadence/Cadence, notdist/Cadence). -
Deno.dockis macOS-only. On Linux every call is accepted and silently ignored. Keep the calls wrapped intry/catchand never make behaviour depend on them.
Platform constraints (Fedora / GNOME)
- The tray needs a StatusNotifierItem host, which stock GNOME 40+ does not provide. Without
gnome-shell-extension-appindicatorthe icon never appears and nothing reports an error.src/native/tray.tsdetects this by looking forStatusNotifierWatcheron the session bus and surfaces it in the log, in/api/diagnostics, and as a banner in the UI. - Enabling that extension requires a logout/login.
gnome-extensions enablefails with "does not exist" until GNOME Shell rescans, and Wayland cannot restart the shell in place. Writing the UUID intoorg.gnome.shell enabled-extensionsviagsettingsworks ahead of time. - Never report a probe failure as a missing feature. A denied read and an absent package look identical to
naive code —
hasLibraryinscripts/doctor.tsreturns"unknown"rather thanfalsewhen nothing was scannable, because the earlier version claimed WebKitGTK was missing on a machine that had it.
Architecture rules
State flows one way:
Store ──snapshot──> tray icon / menus / window title / SSE ──> web views
^ |
└────────── handleMenuAction | POST /api/* <────────────────┘
src/state.tsmust not import anything from the desktop runtime. That is what keeps the timer and task model testable under plaindeno testwith no display server. Tests run headless; keep it that way.- Every native surface is a subscriber, never a second source of truth. If the tray and the window can disagree, the change is wrong.
- All menu ids funnel through
handleMenuActioninsrc/main.ts. Dynamic ids encode their target after a colon (ctx:delete:<uuid>). Add new ids to theMenuActionunion insrc/native/menu.ts. - Menus are rebuilt from a snapshot, not mutated. There is no API to toggle one item. Rebuilds are gated
behind a signature check in
renderNative— rebuilding on every 250ms tick makes the OS menu flicker and closes an open tray menu. - Modal dialogs block the Deno thread.
alert/confirm/promptstop the timer and queue HTTP requests behind them. Route every dialog throughsrc/native/dialogs.ts, which pauses the clock across the call deliberately. Do not use them for anything non-modal.
Conventions
- Comments explain why, not what. Several comments in this repo record a runtime trap that is invisible from the code — do not strip them as noise.
- Errors from native calls are logged and swallowed, never thrown into the timer loop: a failed tooltip update must not stop the clock.
deno fmtbefore committing (lineWidth110).deno lintmust be clean.- Cover new store behaviour in
tests/. Advance phases withskip()rather than waiting on real time.