Friday, 18 July 2026

Session History

CodeMirror syntax highlighting system, smart file-tree open, rokojori-auth login fix. Electron local dev fixes: stale cookie auth, clock tolerance, quit-on-login, credential persistence. Nav z-index fix.

What we built

File tree: single-click smart file open

Clicking a file in the file tree now opens it intelligently rather than always targeting any available panel:

  • If the file is already open in any panel, that panel's tab is focused — no duplicate open, no reload.
  • Otherwise, the next available editor panel of the correct type that is not pinned and not dirty is used; a new panel is created in the active section if none qualifies.
  • Pinned editors (_pinned property on the panel element) are excluded from the available pool entirely.

CodeMirror vendor syntax modes

Four minified CodeMirror 5 mode files added to source/vendor/ and loaded in editor.html:

  • cm-mode-clike.min.js — C/C++/Java; used for GLSL and GDShader
  • cm-mode-python.min.js — Python; used for GDScript (.gd)
  • cm-mode-shell.min.js — shell scripts (.sh)
  • cm-mode-yaml.min.js — YAML and YML

Extension mappings in code-panel's _resolveMode: .yaml/.ymlyaml, .shshell, .gdpython, .glsl/.gdshader/.gdshaderincclike, .csrokojori-cs.

CodeMirror lexer mode system — BrowserLexer + CodeMirrorLexerMode

A custom, self-contained browser lexer and CodeMirror mode wrapper built in source/components/code-panel/:

  • BrowserLexer.ts — zero external imports. Inlines makeSticky() (adds /y flag to regexes). BrowserMatcher uses sticky regex + lastIndex for positional matching. BrowserLexer holds named mode lists of matchers. Exports a cLikeLexer() factory with matchers for all C-like token types (comments, strings, numbers, operators, keywords, identifiers, etc.).
  • CodeMirrorLexerMode.ts — wraps any BrowserLexer into a CodeMirror 5 mode object. Supports multi-line block definitions (start regex → end regex → CSS class; state preserved across lines). Supports named keyword sets: sets of words that override the base CSS class for a given token type (e.g. mapping C# keywords from CWORDkeyword). Keyword sets are mutable at runtime — add/remove/update without recreating the mode. refresh(cm) forces CodeMirror to re-tokenize by re-setting the mode option.
  • CSharpMode.ts — creates csharpMode using cLikeLexer() with a multi-line /* ... */ comment block and a keyword set of ~70 C# keywords. Registered in CodeMirror as 'rokojori-cs'.

The browser-only design (no library-ts dependency) avoids the moduleResolution: "bundler" / ts-node conflict: library-ts compiles without .js extensions (works for ts-node), while browser ES modules require explicit extensions. Keeping the lexer self-contained in code-panel/ eliminates the tension entirely.

rokojori-auth: remove broken refresh-session redirect

The page-level middleware in rokojori-auth/source/server/index.ts was redirecting expired accessToken requests to /api/auth/refresh-session?redirect=... — a route that no longer exists. This blocked login entirely (redirect loop on first visit after token expiry). Fixed by replacing the entire error branch with next(): the middleware now passes through on any token error. Transparent refresh for Roject API calls is handled server-side by Roject's own jwtMiddleware.

Key decisions

Self-contained BrowserLexer instead of reusing library-ts CLikeLexer. Importing from library-ts pulled in extensionless relative imports that break the browser ES module loader (NS_ERROR_CORRUPTED_CONTENT). Adding .js extensions to library-ts imports broke ts-node (CommonJS cannot remap .js.ts). The cleanest fix was a purpose-built, dependency-free browser lexer duplicating only what the code editor needs.

Single-click open, not double-click. The board task said double-click, but single-click is more natural for an IDE file tree (matches VS Code, JetBrains). The smart-targeting logic (focus existing, skip pinned) makes single-click safe — it never disrupts an intentionally pinned panel.

Session 2 — Electron local dev fixes

Mobile: nav z-index fix

.pld-nav in project-list-default.css has position: fixed but no z-index. Stacking contexts created by position: relative project rows on mobile buried the nav underneath them. Fixed with z-index: 10. Overlays remain above at z-index: 200.

Bearer-before-cookie in extractToken

The root cause of Electron auth failures: extractToken() in source/auth-connector/source/server/auth.ts was checking the accessToken cookie before the Authorization header. Electron's Chromium session had a stale accessToken cookie that took priority over the fresh Bearer token injected via session.defaultSession.webRequest.onBeforeSendHeaders. Fixed by reversing the check order: Bearer header wins, cookie is the fallback.

Additionally, createMainWindow() now calls session.defaultSession.clearStorageData({ storages: ['cookies'] }) before creating the window, preventing the stale cookie from accumulating across Electron restarts.

JWT clock skew — JWT_CLOCK_TOLERANCE

Fresh tokens issued by the production auth server (1 h TTL) appeared expired immediately on the Windows dev machine because the local clock was ~65 minutes ahead of the server. Every jwt.verify call returned TokenExpiredError seconds after login.

Fix: jwt.verify accepts a clockTolerance option. An env var JWT_CLOCK_TOLERANCE (integer, seconds; default 0) is now read and passed as clockTolerance when non-zero. Set to 7200 in .env for local development. The underlying audit task (time must never depend on the user's clock) is on the board.

Electron startup token refresh + quit-on-login fix

Two issues fixed in electron/main.ts:

  • Startup refresh: on launch with saved tokens, the app now calls POST account.rokojori.com/api/auth/refresh before opening the main window. Fresh tokens are saved; if refresh fails the login window is shown instead. This prevents using expired access tokens on startup.
  • Quit-on-login race: createMainWindow() was made async (to await the cookie clear), but loginWindow?.close() was called before awaiting it. Zero open windows → window-all-closedapp.quit(). Fixed by: createMainWindow().then(() => loginWindow?.close()).

Electron login: credential persistence + remember-me

The login window (electron/login.html) was extended:

  • Remember me checkbox (checked by default): when checked, email and password are saved to userData/last-email.txt and userData/last-password.txt on successful login; when unchecked, any saved files are deleted.
  • Clear saved button: calls auth:clear-credentials IPC, wipes the fields, and disables itself. Hidden when no credentials are saved.
  • On load, both fields are pre-filled from saved values; focus goes to the Sign in button if both are filled, the password field if only email is saved, or the email field if nothing is saved.

IPC surface added to electron/preload.ts: lastEmail(), lastPassword(), clearCredentials().