All notable changes are documented here. Morgoth uses a phase-based development model; each phase corresponds to a cohesive slice of functionality.
222/222 tests passing.
Five features completing the MQ story and adding multi-pane workflow tools.
- MQ client scripts (
bin/morgoth-send,bin/morgoth-list): command-line tools for sending messages and listing panes without knowing MQ internals.morgoth-sendaccepts--to-uuid,--to-role, and--kind notify; resolves roles to UUIDs via~/.morgoth/registry.jsonusingjq. - Registry title field:
write_registrynow includestitlein each pane entry; called on every OSC 2 title change to keep the registry current - CONCLAVE manifest (
~/.morgoth/manifest.json): newwrite_manifest(panes)writes version, socket path, registry path, FIFO dir, and live Claude pane count on startup and on title changes; monitor pane displays Claude pane count and socket path in its MQ section - Input broadcast (
^B+B): togglesbcastmode; when active, every keystroke is written to all alive terminal panes simultaneously — essential for running commands across multiple Claude Code instances. RedBROADCASTpill appears on the status bar. (Note:broadcastis a reserved Sigil keyword; field is namedbcast.) - Notification API (
recipient:"notify"): external processes push transient status bar messages by sending{"recipient":"notify","kind":"notify","payload":"..."}to the Unix socket — no pane UUID required. The inline inbox drain handler (replacing the now-deletedmq_dispatch_inboxhelper) routes notify-kind messages torender_status_barwith screen access. - Pane swap (
^B+<,^B+>): swap the focused pane with its array neighbor, reordering the visual grid; focus follows the moved pane
Replaces the fragile claude-in.txt + claude-pipe.sh file-based IPC with a
proper per-pane message queue.
- In-process delivery:
mq_send→mq_dispatch_inboxdelivers text from copy-mode yank directly to any pane withrole="claude"— zero latency - Role detection:
detect_role(title)classifies panes as "claude", "terminal", or "monitor" from OSC 2 window title; updated on every title change - Pane UUID: each pane now has a stable
uuid_v4()id; integer position index removed fromPane·new/MonitorPane·newsignatures - Per-pane FIFOs:
~/.morgoth/fifos/<uuid>— external processes can write plain text; polled non-blocking alongside PTY fds each event loop iteration - Unix domain socket:
~/.morgoth/morgoth.sock— structured JSON clients can send{"recipient":"<uuid>","kind":"paste","payload":"..."}messages - Persistent log: messages appended to
~/.morgoth/messages.jsonl; replayed into pane inboxes on startup viamq_load_pending - Registry:
~/.morgoth/registry.jsonwritten on startup and pane state changes - Stdlib additions:
mkfifo,Sys·opennative path,O_NONBLOCK,Sys·bind_unix,Sys·connect_unix,Sys·getenv, nativeSys·listen/Sys·accept/Sys·socket - Removed:
bin/claude-pipe.sh(replaced by FIFO/socket);launch.shno longer starts the watcher background process
First public release. 200/200 tests passing.
- Layout engine with grid-based tiling
- Screen buffer with dirty-cell diff rendering
- PTY management (fake, replaced in Phase 5)
- Input routing with leader-key state machine
- Signal handling and graceful shutdown
Sys·read_string,Sys·poll_fd,Sys·spawn_bg,Sys·spawn_pty,Sys·kill,Sys·waitpidadded to Sigil stdlib
- VTerm terminal emulator: ANSI state machine (normal/escape/CSI modes)
- SGR color attributes, CUP/ED/EL cursor commands
- Scrollback buffer with configurable eviction cap
- JSON configuration (
~/.morgoth/config.json) - System monitor plugin reading
~/.claude/stats-cache.json - Dynamic terminal size via
TIOCGWINSZ+SIGWINCH - Adaptive sleep replacing busy-wait
- DEC private modes + alternate screen (
?1049h/l) - OSC/DCS sequence handling; pane titles via OSC 2
- Extended CSI sequences: IL, DL,
@, P, X, E, F; SGR inverse/defaults - Input escape forwarding: arrow keys, mouse SGR to focused pane
- Integration test validating
⎇/⎉chain in VTerm
- Real
tcgetattr/cfmakeraw/tcsetattrvia libc (native feature) - Real
sigactionfor SIGTERM/SIGINT/SIGWINCH terminal_init()saves and restores termios on shutdown- HMR (hot module reload) gated by
cfg.hmrflag launch.shsafe wrapper withsttysave/restore trap
libc::openpty+O_NONBLOCKon master fdfork/setsid/TIOCSCTTY/dup2/execvpfor child shells- Native
libc::read/libc::write/libc::closefor real fds TIOCSWINSZpropagation viaPty·set_size- Fd boundary: real OS fds
< 4000; fake counters start at4000+
vterm_resize(vt, new_rows, new_cols): in-place grid resize preserving scrollback- SIGWINCH handler uses
vterm_resize(was incorrectly creating a new VTerm) - Pane inner dimensions (
h-2,w-2) passed toPty·set_size, not outer
- Create terminal and monitor panes at runtime (
^B+c,^B+m) - Close pane with confirmation (
^B+x) - Zoom/unzoom focused pane (
^B+z) recompute_grid: auto-fits N panes minimizing aspect-ratio distortionrelayout_panes: extracted DRY relayout used by SIGWINCH/create/close- Configurable
max_panes(default 12)
- Vim-style copy mode (
^B+[) h/j/k/l,0/$,w/b,g/G,Ctrl-U/Ctrl-Dnavigation- Character and line selection (
Space,V) - Yank to OSC 52 clipboard (
Enter) and delivery to Claude pane via MQ - Freeze render guard prevents screen corruption during copy mode
- Load pane layout from
~/.morgoth/profiles/default.jsonat startup - Save current layout with
^B+S - Fallback to single terminal pane when no profile exists
- Enter search with
/(from copy mode) or^B+/(from normal mode) - Incremental search: cursor jumps to nearest match on each keystroke
- Smartcase: lowercase = case-insensitive; any uppercase = case-sensitive
Ctrl-N/Ctrl-Pnavigate during input;n/Nnavigate after accept- Match highlight: current = yellow, others = cyan
- True color: packed-int SGR
38;2;R;G;Bencoding, stored per-cell - Unicode width: fullwidth characters occupy 2 cells;
flush_dirtyskips placeholder cells;render_borderuses display width - Status bar: dynamic mode pill, pane info, context-sensitive hints
- Double-line borders for focused pane; single-line for unfocused
- Pane numbering in title bars (
N:title/[N:title]) - Styled status bar: mode pill (NORMAL/COPY/SEARCH/CONFIRM) + right-aligned version
- Quit confirmation (
^B+q→y/nprompt) - Help overlay (
^B+?) - Direct pane focus (
^B+1through^B+9) ^B+^Bpassthrough of literal leader key to focused pane- Configurable leader key in
config.json - Security:
validate_shell,sanitize_title, buffer caps,max_panesclamp - Mouse click focuses pane at click coordinates; SGR mouse forwarded to child
- Adaptive sleep:
Sys·poll_fd(0, POLL_INTERVAL_MS)only when idle - PTY drain loop: 16 KB chunks, 64 KB per-pane frame budget
- Coalesced
flush_dirty: one call per iteration, not per pane - Smart
Grid·set: comparesch/fg/bgbefore marking dirty (zero dirty cells in steady state) concat_allinflush_dirty: O(n) output instead of O(n²)
Sys·poll_fds(fds_array, timeout_ms)in Sigil stdlib- Event loop: one
libc::pollsyscall per iteration (was N+2 per pane) had_activity_prevflag drives adaptive timeout inpoll_fds
save_sessionwrites~/.morgoth/session.jsonon confirmed quitload_session+ 3-second restore prompt on startupvterm_restore_scrollbackrepopulates scrollback buffer
list_profilesenumerates~/.morgoth/profiles/*.json- Profile picker overlay (
^B+p):j/knavigate,Enterloads,ESCcancels ^B+Ssaves to active profile (not alwaysdefault)
load_bindingsparsesbindingssection ofconfig.json- All leader actions configurable:
new_terminal,close_pane,zoom_toggle,copy_mode,save_profile,help, etc.
^B+|: add pane, force grid to same rows + 1 col^B+-: add pane, force grid to + 1 rowforced_rows/forced_colsstate; resets on SIGWINCH if layout no longer fits
- Monitor pane shows git branch (
git rev-parse --abbrev-ref HEAD) - Monitor shows active task from
~/.claude/current-taskwhen present - Copy-mode yank (
Enter) writes to~/.morgoth/claude-in.txt bin/claude-pipe.sh: watchesclaude-in.txt, injects content into focused Claude Code pane viatmux send-keysusing inherited$TMUX_PANElaunch.shstartsclaude-pipe.shin background when$TMUXis setMORGOTH=1env var set for all child shells
$SHELLenv var used as default shell (overrides config default)- Pane death notification: title becomes
[exited N], border dims to fg 8 TERM=xterm-256colorandCOLORTERM=truecolorexported for child shells
- CSI
<prefix (SGR mouse from child processes) silently ignored instead of crashing withcannot parse '<' as integer - Session restore prompt drains stdin before main loop to prevent response keys from leaking to the focused bash pane
- Profile picker
fgvariable declaredmut(was immutable, caused crash) - Session restore poll changed from non-blocking (0ms) to 3-second timeout
- Monitor refresh tick resets status bar to clear transient yank messages