Skip to content

feat: configurable shell keymap, AI permissions, GUI foundation#12

Merged
cuttlefisch merged 13 commits into
mainfrom
feat/lisp-machine-fixes-gui-foundation
Apr 18, 2026
Merged

feat: configurable shell keymap, AI permissions, GUI foundation#12
cuttlefisch merged 13 commits into
mainfrom
feat/lisp-machine-fixes-gui-foundation

Conversation

@cuttlefisch
Copy link
Copy Markdown
Owner

@cuttlefisch cuttlefisch commented Apr 18, 2026

Summary

Three lisp-machine fixes surfaced during dogfooding MAE with Claude Code in the embedded terminal:

  • Shell exit sequence now uses keymap systemCtrl-\ Ctrl-n was hardcoded in Rust, bypassing the keymap entirely. ShellInsert now participates in keymap dispatch like every other mode. Users can rebind via (define-key "shell-insert" "C-c C-c" "shell-normal-mode") in init.scm.

  • AI permission tier is configurablePermissionPolicy::default() auto-approved up to Shell with no way to set Privileged. Now configurable via config.toml (auto_approve_tier) and MAE_AI_PERMISSIONS env var. Tiers: readonly, standard, trusted (default), full.

  • Renderer trait extracted + GUI crate createdRenderer trait provides the backend-agnostic HAL that CLAUDE.md promised. TerminalRenderer implements it. New mae-gui crate (winit + skia-safe) provides GPU-backed alternative with monospace text rendering, theme color mapping, and winit→KeyPress input translation. Optional behind gui feature flag.

New files

  • crates/core/src/input.rsInputEvent enum (backend-agnostic input)
  • crates/gui/ — new crate: Cargo.toml, lib.rs, canvas.rs, input.rs, text.rs, theme.rs

Key changes

  • crates/renderer/src/lib.rsRenderer trait + TerminalRenderer implements it
  • crates/core/src/editor/keymaps.rsshell-insert keymap with default C-\ C-n binding
  • crates/mae/src/config.rsauto_approve_tier field + resolve_permission_policy()
  • crates/mae/src/main.rs — keymap-based shell key handling, --gui flag, permission config

Test plan

  • 1,329 tests pass (26 new), 0 failures
  • Clippy: 0 warnings across workspace
  • cargo fmt clean
  • Default behavior unchanged: Ctrl-\ Ctrl-n exits ShellInsert
  • Permission default unchanged: auto-approve up to Shell
  • Terminal renderer unchanged: mae works as before
  • Manual: verify (define-key "shell-insert" ...) rebinding works in init.scm
  • Manual: verify MAE_AI_PERMISSIONS=full mae auto-approves all tools

🤖 Generated with Claude Code

cuttlefisch and others added 13 commits April 18, 2026 02:09
…e 8 M1)

Three lisp-machine fixes surfaced during dogfooding with Claude Code:

1. Shell exit sequence (Ctrl-\ Ctrl-n) was hardcoded in Rust, bypassing
   the keymap system entirely. Now ShellInsert participates in keymap
   dispatch like every other mode — users can rebind via init.scm:
   (define-key "shell-insert" "C-c C-c" "shell-normal-mode")

2. AI permission tier was hardcoded to Shell. Now configurable via
   config.toml (auto_approve_tier) and MAE_AI_PERMISSIONS env var.
   Tiers: readonly, standard, trusted (default), full.

3. Renderer trait extracted as HAL — TerminalRenderer implements it,
   new mae-gui crate (winit + skia-safe) provides GPU-backed alternative.
   InputEvent type in mae-core abstracts over crossterm/winit input.

New crate: mae-gui (optional, behind "gui" feature flag)
New tests: 26 (total: 1,329)
Clippy: 0 warnings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds --gui CLI flag (prints foundation-only message for now),
MAE_AI_PERMISSIONS to --help env vars section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mae-gui depends on skia-safe which requires system fontconfig/freetype
libs not available in the GitHub Actions Ubuntu runner. Since the GUI
crate is behind an optional feature flag and can't run in CI anyway
(no display server), exclude it from check/test/clippy steps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire up `mae --gui` with a real event loop using winit's
pump_app_events() integrated into the tokio current_thread runtime.
Each iteration pumps winit events (~16ms for 60fps), then yields to
tokio::select! for AI/LSP/DAP/MCP channel draining.

Key changes:
- WinitCallback implements ApplicationHandler (window init, keyboard
  input, resize, close, modifier tracking)
- GUI reuses existing key dispatch via keypress_to_crossterm() bridge
- run_gui_loop() mirrors terminal loop's shell/agent/intent handling
- Fix init.scm load: inject editor state before evaluation so
  *buffer-name* et al. are defined
- Fix --gui flag being parsed as a file path argument

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Skia raster surface was rendering pixels into memory but never
presenting them to the OS window. Add softbuffer to blit the pixel
buffer to the winit window surface on each end_frame().

Also update ROADMAP.md with a GUI feature status table showing what's
implemented (window, input, status bar, channels) vs. what's planned
(cursor, gutter, syntax colors, images, PDF, mouse).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shell PTY now receives the owning window's dimensions instead of the
full terminal size. Dynamic resize each tick detects split/focus changes.

Agent auto-approval: write .claude/settings.local.json with
mcp__mae-editor wildcard on shell spawn (auto_approve_tools config,
MAE_AGENTS_AUTO_APPROVE env override). Contributor guide in agents.rs
module docs.

New ai_permissions tool lets the AI introspect its own permission tier.
Register agent-list/agent-setup as interactive commands. Update KB
concept:agent-bootstrap docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
parse_key_seq (used by Scheme define-key) now handles angle-bracket
notation like <F1>, <Esc>, <C-x>. Previously only bare named keys
(escape, tab) and C-/M- prefixes worked — <F1> was parsed as four
literal characters. This fixes user init.scm keybindings like:

  (define-key "shell-insert" "<F1>" "shell-normal-mode")

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Log a warning when a Scheme (define-key ...) call produces an empty
key sequence instead of silently skipping. Also log key count at debug
level for successful bindings to aid troubleshooting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… tools

- Fix 3 bugs where editor.mode wasn't synced after focus/buffer changes:
  focus-left/right/up/down, alternate-file, and command palette SwitchBuffer
  now call sync_mode_to_buffer() to match ShellInsert/Normal to BufferKind
- Fix invisible cursor in AI conversation input by splitting the input
  line into spans and applying ui.cursor style to the character under cursor
- Add missing buffer_name parameter to buffer_read/write tool definitions
  (implementations already supported it via resolve_buffer_idx)
- Add set_option to ai_tool_names array (was missing, worked by accident)
- Add lsp_workspace_symbol tool: search symbols by name across the workspace
- Add lsp_document_symbols tool: list all symbols in a document hierarchy
- Full stack for new LSP tools: LspIntent, protocol types, client methods,
  manager dispatch, deferred AI tool handling, MCP re-export (42 tools total)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… update

- Extract ai_event_handler.rs (217 lines): unified AI event dispatch,
  deferred LSP timeout, MCP request handling — eliminates terminal/GUI
  duplication
- Extract shell_lifecycle.rs (185 lines): spawn, resize, reset, close,
  poll, input drain, viewport cache — eliminates terminal/GUI duplication
- Delete handle_ai_event_gui (120 lines of pure duplication)
- main.rs: 2,247 → 1,611 lines (-28%)
- Add Conversation::end_streaming() replacing 7 inline two-field resets
- Use std::mem::take over .drain(..).collect() in shell lifecycle (4 sites)
- ROADMAP.md: 1,329 → 1,369 tests, Phase 8 M2 features documented,
  Tier 2 auto-reload + :set marked DONE
- CLAUDE.md: Phase 4 LSP AI tools marked complete, Phase 8 updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move input_locked check from handle_key to event loop level so it
  covers ShellInsert mode (was bypassed when user was in a shell buffer)
- Add same check to GUI event loop (WinitCallback)
- Add input_lock AI/MCP tool: external agents (Claude Code) can now
  lock/unlock editor input during multi-step operations like self-tests
- Update self-test prompt to call input_lock before/after test execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MCP/AI tool calls can modify buffer content, leaving window cursor_row
past the end of the buffer. The next render/dispatch then calls
rope.line(cursor_row) which panics with "called Option::unwrap() on
a None value" in ropey.

Fix: add Editor::clamp_all_cursors() and call it at the top of both
terminal and GUI event loops, before ensure_scroll/render. This is a
safety net that catches all stale-cursor bugs regardless of source.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two related improvements to the MCP bridge:

1. Session-scoped input lock: MCP tool calls now auto-lock keyboard input
   while active and auto-unlock after 500ms of inactivity. Prevents user
   keystrokes from leaking into buffers during AI agent operations.
   Esc/Ctrl-C still provides immediate unlock.

2. Deferred MCP tools: LSP-dependent tools (lsp_definition, lsp_references,
   lsp_hover, lsp_workspace_symbol, lsp_document_symbols) now resolve
   asynchronously via the MCP bridge instead of failing with "not supported".
   Uses the same DeferredReply pattern as the built-in AI path with 15s timeout.

Also includes: visual anchor/last_visual clamping in clamp_all_cursors(),
search match recomputation after buffer_write operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@cuttlefisch cuttlefisch merged commit 9e5f062 into main Apr 18, 2026
7 checks passed
@cuttlefisch cuttlefisch deleted the feat/lisp-machine-fixes-gui-foundation branch April 18, 2026 12:38
cuttlefisch pushed a commit that referenced this pull request May 26, 2026
Add stance #12 documenting the Chibi-Scheme-derived exception system:
unified handler stack, continuable tagging, handler isolation.

Co-Authored-By: Claude Opus 4.6 <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