Skip to content

macOS GUI: source-build fixes, robustness, packaging, bundled font, docs - #60

Merged
cuttlefisch merged 12 commits into
mainfrom
fix/gui-path-and-manual-kb
Jun 13, 2026
Merged

macOS GUI: source-build fixes, robustness, packaging, bundled font, docs#60
cuttlefisch merged 12 commits into
mainfrom
fix/gui-path-and-manual-kb

Conversation

@cuttlefisch

Copy link
Copy Markdown
Owner

First day building/running MAE's GUI from source on a Mac. This branch makes the GUI work from source, fixes everything that first macOS run surfaced (all regression-tested), updates packaging so the cask ships a GUI-by-default binary with a bundled font, and brings the docs in line.

Runtime / GUI

  • GUI by default when a display is available; transparent TUI fallback over SSH / tty / headless. --gui forces GUI (MAE.app launcher), --no-gui/--tui/-nw force terminal. Pure unit-tested detection.
  • Login-shell PATH resolution so claude (and other tools) resolve when launched from the GUI.
  • System-monospace font fallback + warning — a missing font never blocks launch.
  • Manual KB loaded read-only (no more dirtying the git asset) + in-memory fallback; dev builds discover assets/mae-manual.cozo.
  • Unified Scheme-eval path — human (SPC e b), MCP, and the AI peer all use the same primitive; hooks can name editor commands (e.g. format-before-save).

macOS-surfaced bugs (all fixed + regression-tested)

  • Long-list stack overflow → iterative ConsCell Drop (TCO itself verified sound via r7rs tests).
  • Watcher FSEvents paths canonicalized (auto-reload on symlinked dirs).
  • CozoDB tests 1092s → 56s (in-memory), /var/private/var path canonicalization, libc ioctl cast, font-resolver test refactor. Full suite green in ~187s.

Packaging

  • macOS formula now ships the GUI-capable mae (artifact renamed mae-macos-aarch64.tar.gz; caveats become accurate; tap url auto-syncs at release).
  • License-clean JetBrains Mono Nerd Font bundled in MAE.app + AppImage (all permissive sets; Font Logos excluded; Powerline confirmed MIT), loaded via MAE_FONT_DIR for text and icons. Recipe + attribution in assets/fonts/LICENSES.md.

Docs

  • New concept:fonts (font/icon config), corrected concept:gui launch behavior, concept:options (+font rows, init.scm-primary note), README launch commands, CLAUDE.md options note. Scheme-first framing (config.toml = legacy bootstrap).

Release notes / caveats

  • release.yml changes only execute on a release tag — validate on the first release or a workflow_dispatch dry run.
  • The .app is unsigned/un-notarized; the cask's xattr -cr postflight strips quarantine (Gatekeeper may still prompt on a pristine Mac).
  • Commits authored as heimdall@<host>, not the project email — can rebase-fix if desired.

🤖 Generated with Claude Code

heimdall and others added 12 commits June 13, 2026 00:11
Four fixes surfaced while running the GUI build from source:

1. Agent/shell PATH (SPC a a → claude not found)
   GUI apps launched from Finder/Dock inherit a minimal PATH, so `claude`
   (in /usr/local/bin) and version-manager shims were unreachable.
   `augment_path` now resolves the user's login+interactive shell PATH
   once (cached, timeout-guarded, sentinel-extracted to survive rc
   banners), like Emacs exec-path-from-shell, with the prior static
   home-dir augmentation kept as a fallback.

2. Manual KB help index (SPC h h → "no such KB node: index")
   The help system resolves nodes through the cozo-backed KbQueryLayer,
   but the ~380 seed nodes were only inserted into the in-memory KB —
   invisible to kb_get/help whenever no manual cozo was loaded (e.g. a
   source build). Now we always build an in-memory manual cozo: from the
   pre-built KB when found, else from the seed nodes. Also discover the
   workspace assets/mae-manual.cozo in source/dev builds (ancestor walk).

3. Manual KB is loaded read-only
   The pre-built KB is opened via a throwaway temp copy (sled writes
   recovery snapshots on open), so we never mutate a git-tracked asset or
   drift an install's checksum.

4. Unified Scheme eval path (SPC e b → "expected procedure, got void";
   SPC b s → "undefined variable: format-before-save")
   Per the core rule that the human, MCP clients, and the AI peer call
   the same primitives: interactive eval (eval-buffer/line/region) now
   routes through the same drain_pending_scheme_evals path as MCP/AI,
   replacing the non-yielding eval_for_repl that could not drain hooks
   fired mid-eval (e.g. option-change from set-option!). The hook drain
   also falls back to command dispatch when a hook names an editor
   command (e.g. format-before-save) rather than a Scheme function.

Verified live via the MCP socket; 1115 mae-shell/mae-scheme lib tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unch

A missing "fancy" font must never block the GUI. The font chain previously
ended at the CSS-generic "monospace" family (which CoreText does not resolve
on macOS) and then hard-errored "no monospace font found", so a fresh machine
without JetBrains Mono / Fira Code / Cascadia Code could not open the GUI at
all (only relevant to source/app launches; the cask CLI `mae` is TUI).

Now each typeface (regular/bold/italic/bold-italic) resolves through:
preferred families (configured override + Nerd Font variants + popular coding
fonts) → system monospace families (Menlo/SF Mono/Monaco, DejaVu/Liberation/
Noto, Consolas, Courier New) → the platform default typeface
(`legacy_make_typeface`), which always returns a usable font. If none of the
preferred fonts are installed we log a warning and continue rather than fail.

Verified: with JetBrains Mono removed, the GUI now logs the warning and
initializes the window (previously it aborted on startup).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erflow

Dropping a long list recursed one native stack frame per element (the derived
drop of the `Rc<(Value, Value)>` cdr chain), overflowing the stack on long
lists — surfaced first on macOS, whose smaller thread stacks hit it sooner
(bench_list_operations aborted; it passes on Linux only by stack-size luck).

Back `Value::Pair` with a tuple struct `ConsCell(Value, Value)` whose `Drop`
walks the cdr chain iteratively, unlinking each solely-owned node so its own
drop is O(1). Shared sublists (refcount > 1) are left intact and dismantled by
their last owner. Field access stays `.0`/`.1`, so the change is confined to
the two construction sites. The VM's proper tail calls already let Scheme build
huge lists (verified by r7rs s3_5 tail-recursion tests); now dropping them is
safe too. Regression tests build + drop 1M-element and shared lists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`mae` with no flags now opens the GUI on a desktop session and transparently
falls back to the terminal UI when there is no graphics frontend — over SSH,
on a bare tty, or on a headless server. `--gui` forces the GUI (used by the
MAE.app launcher); `--no-gui`/`--tui`/`-nw` force the terminal UI. Previously
a GUI-enabled binary always tried to open a window, which fails without a
display.

Detection is factored into pure, unit-tested policy functions
(`display_available_from_env`, `should_use_gui`) covering the SSH/X11/Wayland/
macOS and force-flag matrices, so the behavior is verified in CI rather than
by one-off manual checks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d keys

macOS FSEvents reports canonical paths (`/private/var/...`) while callers hold
the symlinked form (`/var/...`, `/tmp/...`). The watcher keyed its path→ids map
on the caller's paths, so removal events never matched — dropped node ids were
lost and stale KB nodes lingered (and emitted Upserted paths didn't match the
app's). Normalize (canonicalize, with a fallback for already-removed files) at
every boundary — seed, record_ids, and event handling — so paths compare equal
regardless of symlinks. Affects KB file watching / auto-reload for org dirs
under any symlinked path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First time running the full suite on macOS surfaced several environment
fragilities (all real, none flaky once understood):

- kb_graph_validation: used an on-disk sled CozoDB per test; sled's per-write
  fsync on APFS made it pathologically slow (~39s/test, 1092s total). Switch to
  in-memory CozoDB (open_mem) — same Datalog logic, 1092s -> 56s.
- project_tests: compared a raw path against a canonicalized one; macOS's
  /var -> /private/var symlink made them diverge. Canonicalize both sides.
- mcp_event_loop_integration: `ioctl(TIOCSCTTY)` failed to compile on macOS
  (request-arg type differs); cast to the inferred type.
- canvas font tests: relied on the "monospace" generic family, which CoreText
  does not resolve on macOS. Extract the renderer's font resolution into
  testable functions and route the tests through it (also adds coverage that a
  missing preferred font never blocks launch).

Full workspace suite now passes on macOS in ~187s (was 20+ min).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The macOS Homebrew formula shipped a TUI-only binary (`mae-macos-aarch64-tui.tar.gz`),
so `mae` from the command line never launched the GUI — only `MAE.app` (cask)
did, and the formula caveats ("mae file.rs # GUI mode") were misleading.

Now the formula ships the GUI-capable binary (same one in MAE.app). It launches
the GUI by default and falls back to the terminal UI over SSH / on a tty /
headless, or with `-nw` (see the display auto-detection in `feat(gui)`), so a
single binary serves both — matching the Linux formula. This makes the existing
caveats accurate.

Changes:
- Build only what's needed (shim, manual KB, daemon) then the GUI `mae`; drop
  the separate non-GUI build and the `mae-tui` copy.
- Rename the formula artifact `mae-macos-aarch64-tui.tar.gz` ->
  `mae-macos-aarch64.tar.gz` and ship the GUI binary; the cask's bundled CLI
  `mae` uses it too. Updated all artifact references (upload, release files,
  checksum, download table).
- The tap-update step now rewrites the formula `url` to the new filename so the
  tap stays in sync at release time (changing it ahead of a release would 404
  against the current release's assets).

Note: only runs on a release tag; validate on the next release (or a
workflow_dispatch dry run) before relying on it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a resolution tier between the preferred (installed) families and the
system-monospace fallback: if MAE_FONT_DIR is set (the MAE.app launcher points
it at the bundle's Resources/fonts), load the bundled JetBrains Mono static TTF
for each style directly via the font manager. This lets the intended font
render on machines without a system install, while a user-installed Nerd Font
(with icon glyphs) still wins because it sits earlier in the chain. The
mechanism is font-agnostic — it loads whatever JetBrainsMono-*.ttf files are
bundled (base font now; a license-clean patched Nerd Font later).

The warning about falling back to a system font now only fires when neither a
preferred nor the bundled font is available. Tests cover the per-style filename
mapping and that resolution never fails even with a nonexistent bundle dir.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bundles JetBrains Mono patched with Nerd Font glyphs so the intended font AND
icon glyphs render out of the box — no system font install required — while a
user-installed Nerd Font still takes precedence.

License: every bundled component is permissive/GPL-compatible (JetBrains Mono
OFL-1.1; glyph sets MIT / Apache-2.0 / OFL-1.1 / CC-BY-4.0). The patch
deliberately EXCLUDES the "Font Logos" set (unlicensed third-party brand/distro
logos, also a trademark concern). Full attribution + the reproducible patch
recipe are in assets/fonts/LICENSES.md; JetBrains Mono's OFL is in
assets/fonts/OFL.txt. Powerline Symbols was confirmed MIT (upstream
powerline/powerline), not the "vague" license the Nerd Fonts audit implied.

- assets/fonts/: 4 patched static TTFs (Regular/Bold/Italic/BoldItalic) +
  OFL.txt + LICENSES.md.
- release.yml: bundle assets/fonts into MAE.app/Contents/Resources/fonts and
  the Linux AppImage, with the launchers exporting MAE_FONT_DIR.
- canvas.rs: the icon typeface also falls back to the bundled font, so icon
  glyphs render from the bundle when no system Nerd Font is installed.

Verified locally: with system JetBrains Mono hidden and MAE_FONT_DIR pointing at
assets/fonts, the GUI initializes using the bundled font (no system-fallback
warning).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g surface

Bring the docs in line with this branch's behavior changes:

- New manual node concept:fonts — font_family/font_size/icon_font_family,
  the never-fail resolution order (preferred → bundled → system → default),
  the bundled license-clean JetBrains Mono Nerd Font, icon config, and
  MAE_FONT_DIR. Scheme-first examples (init.scm / :set), config.toml noted as
  legacy bootstrap.
- concept:gui — `mae` now launches the GUI by default when a display is
  available and falls back to the terminal UI over SSH/tty/headless; `--gui`
  forces GUI, `--no-gui`/`--tui`/`-nw` force terminal. Updated backend-selection
  and font bullets; links to concept:fonts.
- concept:options — added font-family / icon-font-family rows; explicit note
  that init.scm is the primary config surface and config.toml is legacy
  bootstrap (`:set-save` writes init.scm).
- index — link the new fonts node.
- README — corrected launch commands (mae = GUI by default; --no-gui/--gui).
- CLAUDE.md — options regression-guard now references init.scm round-trip
  (not config.toml) since :set-save persists to init.scm.

The prebuilt manual KB (assets/mae-manual.cozo) is regenerated from these .org
sources at release time (build-manual-kb); run `make manual-kb` to refresh it
locally. Verified the new node parses + ingests (235 nodes, no warnings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…al-kb

# Conflicts:
#	.github/workflows/release.yml
…sync)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant