Skip to content

M5: MXP + Pueblo parsers, emoji, clickable links, packaging - #2

Merged
HarryCordewener merged 71 commits into
mainfrom
claude/muglyph-implementation-5f51l3
Jul 28, 2026
Merged

M5: MXP + Pueblo parsers, emoji, clickable links, packaging#2
HarryCordewener merged 71 commits into
mainfrom
claude/muglyph-implementation-5f51l3

Conversation

@HarryCordewener

@HarryCordewener HarryCordewener commented Jul 27, 2026

Copy link
Copy Markdown
Member

Advances MuGlyph into milestone M5 (full parity & polish) on top of the merged M1–M4 foundation, and clears the deferred review items from PR #1.

Status

  • ✅ Clean Release build (0 warnings). 299 tests pass — Core 200, Graphics 57, Scripting 42.

Protocols & text (Core, UI-agnostic, tested)

  • MxpParser and PuebloParser — incremental, line-oriented parsers (same shape as AnsiParser, unified behind a new ILineParser) turning markup into styled spans: formatting, colours (via WebColors), entities, line breaks, and clickable <SEND>/<A> links & commands. (44 MXP + 32 Pueblo tests.)
  • SpanInteraction — an optional click behaviour (send-command / hyperlink) on StyledSpan, produced by the parsers and realised by the UI.
  • EmojiSubstitutor — opt-in emoticon (:) → 🙂) and :shortcode: (:fire: → 🔥) substitution.
  • WorldDefinition gains ContentFormat (Ansi/Mxp/Pueblo) and Emoji settings; WorldSession selects the parser and applies emoji per world.

TUI integration

  • OutputView tracks per-cell interactions and activates a command/link span on click; MuGlyphApp sends the command / opens the link, shows a GMCP-driven stat line, and captures spawn-routed output.

Deferred review fixes (from PR #1's CodeRabbit review)

  • TelnetSession: clean up the transport on connect failure, clear the interpreter on disconnect (so reconnect works), and honour cancellation on send.
  • WorldSession: dispose a prior session before reconnecting.
  • KittyGraphicsProtocol: reject placeholder image ids above 24 bits instead of truncating.
  • ScriptException: also parse the [source]:LINE: runtime-error format.

Packaging

  • Self-contained single-file publish profiles (linux-x64, win-x64) and a tagged release workflow (docs/PACKAGING.md). The single-file knobs live only in the publish profiles, so ordinary dotnet build/run and CI never require a runtime identifier.

Roadmap note (per discussion)

docs/PLAN.md records the still-open M5+ items (dedicated spawn/input windows, puppets, map view) and a concrete plan for an in-TUI web view (AngleSharp text render reusing SpanInteraction, <img> through the existing Kitty/Sixel/half-block pipeline, optional Playwright snapshot mode).

Verification note

A headless environment can't visually verify a TUI or render Kitty graphics, so the Graphics and Tui layers are build-verified; all protocol/parser/engine logic lives in MuClient.Core and is covered by tests.

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added a SharpConsoleUI-based workspace with tmux-style multi-pane layouts, tabs, command palette, status bar, settings screens, and session snapshot/restore.
    • Added MXP/Pueblo markup rendering with styles, colors, clickable commands/links, and improved entity handling.
    • Added an in-app web view for HTML fetching/rendering and navigation.
    • Added shared trigger sets, per-character automation, timers, emoji substitution, and versioned JSON schema migration.
    • Added single-file Linux/Windows packaging and tag-based release artifacts.
  • Bug Fixes
    • Improved parser boundary behavior (no interaction “leakage”), connection/disconnect cancellation cleanup, and stricter validation for image IDs and script/runtime error reporting.
  • Documentation
    • Updated README and added packaging/screenshots documentation to reflect the new UI, configuration schema, and release flow.

Advance MuGlyph into milestone M5 (full parity & polish) with tested,
UI-agnostic protocol work plus TUI integration and packaging.

Protocols & text (Core)
- MxpParser and PuebloParser: incremental, line-oriented parsers that turn
  markup into styled spans — formatting, colours (WebColors), entities, line
  breaks, and clickable SEND/A links/commands. Unified behind ILineParser
  alongside AnsiParser.
- SpanInteraction: an optional click behaviour (send-command / hyperlink) on
  StyledSpan, produced by the parsers and realised by the UI.
- EmojiSubstitutor: opt-in emoticon (:) -> emoji) and :shortcode: substitution.
- WorldDefinition gains ContentFormat (Ansi/Mxp/Pueblo) and Emoji settings;
  WorldSession selects the parser and applies emoji per world.

TUI (M5 integration)
- OutputView tracks per-cell interactions and activates command/link spans on
  click; MuGlyphApp sends commands / opens links, shows a GMCP-driven stat
  line, and captures spawn-routed output.

Deferred review fixes
- TelnetSession: clean up on connect failure and clear the interpreter on
  disconnect so reconnect works; honour cancellation on send.
- WorldSession: dispose a prior session before reconnecting.
- KittyGraphicsProtocol: reject placeholder image ids above 24 bits.
- ScriptException: also parse the "[source]:LINE:" runtime-error format.

Packaging
- Self-contained single-file publish profiles (linux-x64, win-x64) and a
  tagged release workflow; docs/PACKAGING.md. Publish knobs are profile-scoped
  so ordinary build/run/CI never require a RID.

299 tests pass (Core 200, Graphics 57, Scripting 42).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Too many files!

This PR contains 187 files, which is 37 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to Pro+ to raise the limit.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c3aaa5f7-8b98-474d-a523-e344c4359ab6

📥 Commits

Reviewing files that changed from the base of the PR and between 3819826 and 7ff976b.

⛔ Files ignored due to path filters (1)
  • tools/fonts/SharpMUTermMonoNerd.woff is excluded by !**/*.woff
📒 Files selected for processing (201)
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • .gitignore
  • CLAUDE.md
  • Directory.Build.props
  • Directory.Packages.props
  • MuGlyph.slnx
  • README.md
  • SharpMUTerm.slnx
  • docs/HANDOFF.md
  • docs/PACKAGING.md
  • docs/PLAN.md
  • docs/SCREENSHOTS.md
  • docs/design/README.md
  • docs/design/SharpMUTerm-TUI-v3.dc.html
  • docs/design/support.js
  • docs/superpowers/specs/2026-07-28-sharpmuterm-naming-design.md
  • examples/example.lua
  • src/MuClient.Core/Configuration/AppConfiguration.cs
  • src/MuClient.Core/Configuration/BeipMuImporter.cs
  • src/MuClient.Core/Configuration/WorldDefinition.cs
  • src/MuClient.Core/Text/StyledSpan.cs
  • src/MuClient.Graphics/MuClient.Graphics.csproj
  • src/MuClient.Scripting/MuClient.Scripting.csproj
  • src/MuClient.Tui/ColorMapper.cs
  • src/MuClient.Tui/MuClient.Tui.csproj
  • src/MuClient.Tui/MuGlyphApp.cs
  • src/MuClient.Tui/Program.cs
  • src/MuClient.Tui/Views/CommandInput.cs
  • src/MuClient.Tui/Views/OutputView.cs
  • src/SharpMUTerm.Core/Automation/Alias.cs
  • src/SharpMUTerm.Core/Automation/AliasEngine.cs
  • src/SharpMUTerm.Core/Automation/AutomationDefaults.cs
  • src/SharpMUTerm.Core/Automation/IntervalScheduler.cs
  • src/SharpMUTerm.Core/Automation/Macro.cs
  • src/SharpMUTerm.Core/Automation/MacroEngine.cs
  • src/SharpMUTerm.Core/Automation/TimerDefinition.cs
  • src/SharpMUTerm.Core/Automation/Trigger.cs
  • src/SharpMUTerm.Core/Automation/TriggerEngine.cs
  • src/SharpMUTerm.Core/Commands/CommandCatalog.cs
  • src/SharpMUTerm.Core/Commands/CommandItem.cs
  • src/SharpMUTerm.Core/Commands/CommandMatcher.cs
  • src/SharpMUTerm.Core/Configuration/AppConfiguration.cs
  • src/SharpMUTerm.Core/Configuration/CharacterDefinition.cs
  • src/SharpMUTerm.Core/Configuration/ConfigurationMigrator.cs
  • src/SharpMUTerm.Core/Configuration/ConfigurationStore.cs
  • src/SharpMUTerm.Core/Configuration/RgbJsonConverter.cs
  • src/SharpMUTerm.Core/Configuration/TerminalColorJsonConverter.cs
  • src/SharpMUTerm.Core/Configuration/TriggerSet.cs
  • src/SharpMUTerm.Core/Configuration/WorldDefinition.cs
  • src/SharpMUTerm.Core/Input/InputHistory.cs
  • src/SharpMUTerm.Core/Logging/HtmlLogSink.cs
  • src/SharpMUTerm.Core/Logging/ILogSink.cs
  • src/SharpMUTerm.Core/Logging/PlainTextLogSink.cs
  • src/SharpMUTerm.Core/Protocols/MxpParser.cs
  • src/SharpMUTerm.Core/Protocols/PuebloParser.cs
  • src/SharpMUTerm.Core/Session/SessionEvents.cs
  • src/SharpMUTerm.Core/Session/SessionManager.cs
  • src/SharpMUTerm.Core/Session/WorldSession.cs
  • src/SharpMUTerm.Core/SharpMUTerm.Core.csproj
  • src/SharpMUTerm.Core/Telnet/ITelnetSession.cs
  • src/SharpMUTerm.Core/Telnet/MsspConfigReader.cs
  • src/SharpMUTerm.Core/Telnet/TelnetEvents.cs
  • src/SharpMUTerm.Core/Telnet/TelnetSession.cs
  • src/SharpMUTerm.Core/Text/AnsiPalette.cs
  • src/SharpMUTerm.Core/Text/AnsiParser.cs
  • src/SharpMUTerm.Core/Text/EmojiSubstitutor.cs
  • src/SharpMUTerm.Core/Text/ILineParser.cs
  • src/SharpMUTerm.Core/Text/Meters.cs
  • src/SharpMUTerm.Core/Text/ScrollbackBuffer.cs
  • src/SharpMUTerm.Core/Text/SpanInteraction.cs
  • src/SharpMUTerm.Core/Text/StatusFormatter.cs
  • src/SharpMUTerm.Core/Text/StyledLine.cs
  • src/SharpMUTerm.Core/Text/StyledSpan.cs
  • src/SharpMUTerm.Core/Text/StyledText.cs
  • src/SharpMUTerm.Core/Text/TerminalColor.cs
  • src/SharpMUTerm.Core/Text/TextStyle.cs
  • src/SharpMUTerm.Core/Text/WebColors.cs
  • src/SharpMUTerm.Core/Theming/Theme.cs
  • src/SharpMUTerm.Core/Theming/ThemeLibrary.cs
  • src/SharpMUTerm.Core/Transport/ConnectionOptions.cs
  • src/SharpMUTerm.Core/Transport/ITransport.cs
  • src/SharpMUTerm.Core/Transport/TcpTransport.cs
  • src/SharpMUTerm.Core/Workspace/DropZones.cs
  • src/SharpMUTerm.Core/Workspace/LayoutNode.cs
  • src/SharpMUTerm.Core/Workspace/LayoutSolver.cs
  • src/SharpMUTerm.Core/Workspace/PaneCommands.cs
  • src/SharpMUTerm.Core/Workspace/Workspace.cs
  • src/SharpMUTerm.Core/Workspace/WorkspaceLayout.cs
  • src/SharpMUTerm.Core/Workspace/WorkspaceState.cs
  • src/SharpMUTerm.Core/Workspace/WorkspaceWindow.cs
  • src/SharpMUTerm.Core/Workspaces/RailModel.cs
  • src/SharpMUTerm.Graphics/CapabilityProbe.cs
  • src/SharpMUTerm.Graphics/GraphicsProtocol.cs
  • src/SharpMUTerm.Graphics/HalfBlockRenderer.cs
  • src/SharpMUTerm.Graphics/IImageSource.cs
  • src/SharpMUTerm.Graphics/InlineImage.cs
  • src/SharpMUTerm.Graphics/KittyGraphicsProtocol.cs
  • src/SharpMUTerm.Graphics/SharpMUTerm.Graphics.csproj
  • src/SharpMUTerm.Graphics/SixelEncoder.cs
  • src/SharpMUTerm.Graphics/TerminalCapabilities.cs
  • src/SharpMUTerm.Scripting/IScriptWorld.cs
  • src/SharpMUTerm.Scripting/ScriptException.cs
  • src/SharpMUTerm.Scripting/ScriptHost.cs
  • src/SharpMUTerm.Scripting/SharpMUTerm.Scripting.csproj
  • src/SharpMUTerm.Scripting/WorldSessionScriptBridge.cs
  • src/SharpMUTerm.Tui/AliasesScreenRenderer.cs
  • src/SharpMUTerm.Tui/CaptureLineRenderer.cs
  • src/SharpMUTerm.Tui/CommandPalette.cs
  • src/SharpMUTerm.Tui/CommandSurfaceRenderer.cs
  • src/SharpMUTerm.Tui/DemoScene.cs
  • src/SharpMUTerm.Tui/FreezeBarRenderer.cs
  • src/SharpMUTerm.Tui/Glyphs.cs
  • src/SharpMUTerm.Tui/GmcpStats.cs
  • src/SharpMUTerm.Tui/KeypadScreenRenderer.cs
  • src/SharpMUTerm.Tui/MarkupFormatter.cs
  • src/SharpMUTerm.Tui/OptionsScreenRenderer.cs
  • src/SharpMUTerm.Tui/Powerline.cs
  • src/SharpMUTerm.Tui/Program.cs
  • src/SharpMUTerm.Tui/Properties/PublishProfiles/linux-x64.pubxml
  • src/SharpMUTerm.Tui/Properties/PublishProfiles/win-x64.pubxml
  • src/SharpMUTerm.Tui/RailRenderer.cs
  • src/SharpMUTerm.Tui/SettingsOverlay.cs
  • src/SharpMUTerm.Tui/SharpMUTerm.Tui.csproj
  • src/SharpMUTerm.Tui/SharpMUTermApp.cs
  • src/SharpMUTerm.Tui/TabTitles.cs
  • src/SharpMUTerm.Tui/TimersScreenRenderer.cs
  • src/SharpMUTerm.Tui/TriggersScreenRenderer.cs
  • src/SharpMUTerm.Tui/WorldsScreenRenderer.cs
  • src/SharpMUTerm.Tui/WorldsScreenView.cs
  • src/SharpMUTerm.Web/HtmlStyledRenderer.cs
  • src/SharpMUTerm.Web/LineWriter.cs
  • src/SharpMUTerm.Web/SharpMUTerm.Web.csproj
  • src/SharpMUTerm.Web/WebPage.cs
  • src/SharpMUTerm.Web/WebPageFetcher.cs
  • tests/MuClient.Core.Tests/Configuration/ConfigurationTests.cs
  • tests/SharpMUTerm.Core.Tests/Automation/AliasAndMacroTests.cs
  • tests/SharpMUTerm.Core.Tests/Automation/IntervalSchedulerTests.cs
  • tests/SharpMUTerm.Core.Tests/Automation/TriggerEngineTests.cs
  • tests/SharpMUTerm.Core.Tests/Commands/CommandCatalogTests.cs
  • tests/SharpMUTerm.Core.Tests/Commands/CommandMatcherTests.cs
  • tests/SharpMUTerm.Core.Tests/Configuration/ConfigurationTests.cs
  • tests/SharpMUTerm.Core.Tests/Input/InputHistoryTests.cs
  • tests/SharpMUTerm.Core.Tests/Logging/LogSinkTests.cs
  • tests/SharpMUTerm.Core.Tests/Protocols/MxpParserTests.cs
  • tests/SharpMUTerm.Core.Tests/Protocols/ParserBoundaryTests.cs
  • tests/SharpMUTerm.Core.Tests/Protocols/PuebloParserTests.cs
  • tests/SharpMUTerm.Core.Tests/Session/FakeTelnetSession.cs
  • tests/SharpMUTerm.Core.Tests/Session/WorldSessionContentTests.cs
  • tests/SharpMUTerm.Core.Tests/Session/WorldSessionTests.cs
  • tests/SharpMUTerm.Core.Tests/SharpMUTerm.Core.Tests.csproj
  • tests/SharpMUTerm.Core.Tests/Telnet/FakeTransport.cs
  • tests/SharpMUTerm.Core.Tests/Telnet/TelnetSessionTests.cs
  • tests/SharpMUTerm.Core.Tests/Text/AnsiParserTests.cs
  • tests/SharpMUTerm.Core.Tests/Text/EmojiSubstitutorTests.cs
  • tests/SharpMUTerm.Core.Tests/Text/MetersAndStatusTests.cs
  • tests/SharpMUTerm.Core.Tests/Text/ScrollbackBufferTests.cs
  • tests/SharpMUTerm.Core.Tests/Text/StyledTextTests.cs
  • tests/SharpMUTerm.Core.Tests/Text/WebColorsAndInteractionTests.cs
  • tests/SharpMUTerm.Core.Tests/Theming/ThemeTests.cs
  • tests/SharpMUTerm.Core.Tests/Workspace/DropZonesTests.cs
  • tests/SharpMUTerm.Core.Tests/Workspace/LayoutSolverTests.cs
  • tests/SharpMUTerm.Core.Tests/Workspace/PaneCommandsTests.cs
  • tests/SharpMUTerm.Core.Tests/Workspace/RailModelTests.cs
  • tests/SharpMUTerm.Core.Tests/Workspace/WorkspaceLayoutTests.cs
  • tests/SharpMUTerm.Core.Tests/Workspace/WorkspaceStateTests.cs
  • tests/SharpMUTerm.Core.Tests/Workspace/WorkspaceTests.cs
  • tests/SharpMUTerm.Graphics.Tests/CapabilityProbeTests.cs
  • tests/SharpMUTerm.Graphics.Tests/HalfBlockRendererTests.cs
  • tests/SharpMUTerm.Graphics.Tests/InlineImageRendererTests.cs
  • tests/SharpMUTerm.Graphics.Tests/KittyGraphicsProtocolTests.cs
  • tests/SharpMUTerm.Graphics.Tests/KittyImageIdValidationTests.cs
  • tests/SharpMUTerm.Graphics.Tests/MemoryImageSourceTests.cs
  • tests/SharpMUTerm.Graphics.Tests/SharpMUTerm.Graphics.Tests.csproj
  • tests/SharpMUTerm.Graphics.Tests/SixelEncoderTests.cs
  • tests/SharpMUTerm.Scripting.Tests/FakeScriptWorld.cs
  • tests/SharpMUTerm.Scripting.Tests/ReloadTests.cs
  • tests/SharpMUTerm.Scripting.Tests/SandboxTests.cs
  • tests/SharpMUTerm.Scripting.Tests/ScriptHostTests.cs
  • tests/SharpMUTerm.Scripting.Tests/SharpMUTerm.Scripting.Tests.csproj
  • tests/SharpMUTerm.Tui.Tests/AliasesScreenRendererTests.cs
  • tests/SharpMUTerm.Tui.Tests/CaptureLineRendererTests.cs
  • tests/SharpMUTerm.Tui.Tests/CommandSurfaceRendererTests.cs
  • tests/SharpMUTerm.Tui.Tests/FreezeBarRendererTests.cs
  • tests/SharpMUTerm.Tui.Tests/KeypadScreenRendererTests.cs
  • tests/SharpMUTerm.Tui.Tests/MarkupFormatterTests.cs
  • tests/SharpMUTerm.Tui.Tests/OptionsScreenRendererTests.cs
  • tests/SharpMUTerm.Tui.Tests/PowerlineTests.cs
  • tests/SharpMUTerm.Tui.Tests/RailRendererTests.cs
  • tests/SharpMUTerm.Tui.Tests/SharpMUTerm.Tui.Tests.csproj
  • tests/SharpMUTerm.Tui.Tests/TabTitlesTests.cs
  • tests/SharpMUTerm.Tui.Tests/TimersScreenRendererTests.cs
  • tests/SharpMUTerm.Tui.Tests/TriggersScreenRendererTests.cs
  • tests/SharpMUTerm.Tui.Tests/WorldsScreenRendererTests.cs
  • tests/SharpMUTerm.Web.Tests/HtmlStyledRendererTests.cs
  • tests/SharpMUTerm.Web.Tests/SharpMUTerm.Web.Tests.csproj
  • tools/ansi_frame_to_image.py
  • tools/fonts/LICENSE-NerdFonts.txt
  • tools/fonts/OFL.txt
  • tools/fonts/README.md
  • tools/make-screenshots.sh

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Review on demand using usage pricing

Walkthrough

This PR adds MXP/Pueblo parsing, versioned character-based configuration, workspace and command models, a SharpConsoleUI TUI, an AngleSharp web view, single-file release automation, snapshot tooling, and expanded Core, Web, Graphics, and TUI tests.

Changes

MuGlyph platform expansion

Layer / File(s) Summary
Content parsing and interactions
src/MuClient.Core/Text/*, src/MuClient.Core/Protocols/*, tests/MuClient.Core.Tests/Protocols/*
Incremental ANSI, MXP, and Pueblo parsing now produces styled lines, colors, entities, and interactive command or hyperlink spans.
Configuration and sessions
src/MuClient.Core/Configuration/*, src/MuClient.Core/Session/*, src/MuClient.Core/Telnet/*
Configuration migrates to version 2 with characters and shared trigger sets; sessions select parsers, support emoji and auto-login, and tighten telnet cleanup.
Web view
src/MuClient.Web/*, src/MuClient.Tui/MuGlyphApp.cs, tests/MuClient.Web.Tests/*
HTML and non-HTML responses are fetched, rendered into styled lines, wrapped, linked, and displayed in auxiliary TUI windows.
Workspace and command state
src/MuClient.Core/Workspace/*, src/MuClient.Core/Commands/*, src/MuClient.Core/Input/*, src/MuClient.Tui/*
Pane trees, window routing, unread state, persistence, command matching, input history, rail data, and TUI screens are implemented.
SharpConsoleUI and snapshots
src/MuClient.Tui/MuGlyphApp.cs, src/MuClient.Tui/Program.cs, tools/*, docs/SCREENSHOTS.md
The TUI uses SharpConsoleUI, supports headless snapshots, renders GMCP status data, and converts ANSI frames to SVG or HTML.
Build and release delivery
.github/workflows/*, src/MuClient.Tui/Properties/PublishProfiles/*, docs/PACKAGING.md, MuGlyph.slnx
Web testing, Linux and Windows single-file publishing, tag-based release packaging, package centralization, and solution registration are added.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and names several core additions, including the new parsers, emoji/link handling, and packaging work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

A text-mode web view: read HTML pages (and follow MXP/Pueblo/HTML links)
inside the client, reusing the styled-line + interaction model.

MuClient.Web (new, UI-agnostic, tested)
- HtmlStyledRenderer: AngleSharp-backed HTML -> StyledLine[]. Block elements
  become breaks; inline elements map to TextStyle; <a href> and <img> become
  clickable SpanInteraction spans; headings, lists, <pre>, colours, entities,
  and word-wrapping to a target width. script/style/head stripped.
- WebPageFetcher: http(s)-only fetch with a byte cap and timeout, rendering
  HTML (or plain text for non-HTML) into a WebPage.
- 14 TUnit tests.

TUI
- WebView pane renders a WebPage's lines with scroll and clickable in-pane
  navigation (Navigate/Closed events). MuGlyphApp opens it via a `/web <url>`
  command or by following an output-pane link; Esc closes it.

CI runs the new Web test project. 313 tests pass across the solution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/MuClient.Core/Telnet/TelnetSession.cs (1)

244-269: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

_interpreter is nulled before it's ever disposed — interpreter leak on every disconnect/reconnect.

DisposeAsync (lines 281-293) calls DisconnectAsync() first, then only disposes _interpreter if (_interpreter is not null). Since DisconnectAsync now sets _interpreter = null at Line 267 before returning, that null-check in DisposeAsync always fails, so _interpreter.DisposeAsync() is never invoked. Because WorldSession.ConnectAsync also disposes the previous telnet session on every reconnect, this leaks one TelnetInterpreter instance per reconnect, not just at final teardown.

🔧 Proposed fix: dispose the interpreter inside `DisconnectAsync` before clearing the field
-        // Clear the interpreter so a subsequent ConnectAsync can reconnect and the "not
-        // connected" guards on the send methods observe the disconnected state.
-        _interpreter = null;
-        RaiseDisconnected(null);
+        // Clear the interpreter so a subsequent ConnectAsync can reconnect and the "not
+        // connected" guards on the send methods observe the disconnected state. Dispose it
+        // here (rather than relying on DisposeAsync, which calls DisconnectAsync first and
+        // would otherwise see it already null).
+        var interpreter = _interpreter;
+        _interpreter = null;
+        if (interpreter is not null)
+        {
+            await interpreter.DisposeAsync().ConfigureAwait(false);
+        }
+
+        RaiseDisconnected(null);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MuClient.Core/Telnet/TelnetSession.cs` around lines 244 - 269, Update
DisconnectAsync to dispose the current _interpreter asynchronously before
setting it to null, preserving the existing disconnected-state reset afterward.
Ensure the disposal occurs only when _interpreter is non-null and avoid relying
on DisposeAsync to perform cleanup after DisconnectAsync clears the field.
🧹 Nitpick comments (7)
tests/MuClient.Web.Tests/HtmlStyledRendererTests.cs (1)

100-105: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the image link interaction as well as its label.

This test passes if src stops producing an interactive span. Assert IsInteractive, hyperlink kind, and target to protect the advertised behavior.

Proposed fix
         var span = lines.SelectMany(l => l.Spans).First(s => s.Text.Contains("image"));
         await Assert.That(span.Text).Contains("a cat");
+        await Assert.That(span.IsInteractive).IsTrue();
+        await Assert.That(span.Interaction!.Kind).IsEqualTo(InteractionKind.Hyperlink);
+        await Assert.That(span.Interaction!.Target).IsEqualTo("pic.png");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/MuClient.Web.Tests/HtmlStyledRendererTests.cs` around lines 100 - 105,
Update Image_BecomesLabelledLink to assert the selected span is interactive, has
the hyperlink kind, and targets “pic.png”, while retaining the existing
assertion that its text contains “a cat”.
src/MuClient.Tui/GmcpStats.cs (1)

19-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider unit tests for GmcpStats.

Update/Summarize are pure, easily-testable logic (JSON merge + ordering) with no UI dependency, yet no tests are included in this cohort. Coverage here would also have caught the boolean-formatting issue above.

Also applies to: 64-89

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MuClient.Tui/GmcpStats.cs` around lines 19 - 59, Add unit tests for
GmcpStats.Update and Summarize, covering JSON property merging, changed versus
unchanged updates, ignored object and array values, invalid or non-object JSON,
package filtering, and deterministic summary ordering. Include boolean
formatting expectations so the tests catch the referenced formatting issue,
while keeping the tests independent of UI components.
src/MuClient.Tui/Views/OutputView.cs (1)

20-93: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated click/interaction-mapping logic between OutputView and WebView. Both views independently maintain a (Row, Col) → SpanInteraction dictionary rebuilt every draw, and near-identical OnMouseEvent single-click hit-testing — one root cause: no shared base for interactive-span rendering in this TUI.

  • src/MuClient.Tui/Views/OutputView.cs#L20-L93: extract the _cellInteractions map, OnMouseEvent hit-test, and per-cell registration-during-draw into a shared base/helper (e.g. an InteractiveTextView base or a small CellInteractionMap utility) that both views compose.
  • src/MuClient.Tui/Views/WebView.cs#L20-L96: have WebView reuse the same shared helper instead of re-implementing _cellInteractions/OnMouseEvent, narrowing to InteractionKind.Hyperlink via a filter passed to the shared dispatcher.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MuClient.Tui/Views/OutputView.cs` around lines 20 - 93, The interaction
mapping and click dispatch are duplicated across
src/MuClient.Tui/Views/OutputView.cs#L20-L93 and
src/MuClient.Tui/Views/WebView.cs#L20-L96. Extract the _cellInteractions
storage, per-cell registration, and OnMouseEvent hit-testing into a shared
InteractiveTextView base or CellInteractionMap helper; update OutputView to use
it for command and hyperlink interactions, and update WebView to reuse it with a
filter that accepts only InteractionKind.Hyperlink.
tests/MuClient.Core.Tests/Text/WebColorsAndInteractionTests.cs (1)

64-72: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the positive equality case too.

IsNotEqualTo passes even if SpanInteraction only has reference equality, so this does not actually verify that Interaction participates in StyledSpan equality/hashing correctly. Add two separately-constructed spans with identical interactions and assert they are equal (and share a hash code).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/MuClient.Core.Tests/Text/WebColorsAndInteractionTests.cs` around lines
64 - 72, Extend StyledSpan_CarriesInteraction_AndAffectsEquality with separately
constructed StyledSpan instances using identical SpanInteraction.Command values.
Assert these spans are equal and have matching hash codes, while preserving the
existing plain-versus-linked inequality assertions to verify interaction value
equality and hashing.
tests/MuClient.Core.Tests/Protocols/PuebloParserTests.cs (1)

139-147: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing coverage for an unclosed anchor crossing a line boundary.

There is no Pueblo equivalent of MxpParserTests.Send_BareWithoutClose_ClosesAtEndOfLine, which is why the leak flagged on src/MuClient.Core/Protocols/PuebloParser.cs Line 398 goes unnoticed. Add a test feeding "<A XCH_CMD=\"look\">here\nnext\n" and assert the second line's span is not interactive.

As per coding guidelines: "Maintain unit-test coverage for Core functionality."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/MuClient.Core.Tests/Protocols/PuebloParserTests.cs` around lines 139 -
147, Add a Pueblo parser test alongside Anchor_XchCmd_RevertsAfterClose that
parses the unclosed anchor input "<A XCH_CMD=\"look\">here\nnext\n" and verifies
the span for the second line is not interactive, covering closure at the line
boundary.

Source: Coding guidelines

tests/MuClient.Core.Tests/Protocols/MxpParserTests.cs (1)

232-240: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Extend this case to cover an interaction with nested formatting crossing the newline.

<SEND HREF="go">walk\n is the easy path; the leak surfaces when a formatting tag is opened inside the send and closed on the next line (see the comment on src/MuClient.Core/Protocols/MxpParser.cs Line 538). A test like Feed("<SEND HREF=\"go\"><B>walk\nmore</B> tail\n") asserting line 2's trailing span is non-interactive would lock the fix in.

As per coding guidelines: "Maintain unit-test coverage for Core functionality, including ANSI/SGR parsing, telnet round trips, and trigger, alias, and macro engines."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/MuClient.Core.Tests/Protocols/MxpParserTests.cs` around lines 232 -
240, Extend Send_BareWithoutClose_ClosesAtEndOfLine to feed a nested formatting
tag opened inside the SEND and closed on the following line, such as bold
formatting spanning the newline. Assert the second line’s trailing text span is
non-interactive, preserving coverage for the interaction-boundary behavior
described in MxpParser.

Source: Coding guidelines

src/MuClient.Core/Protocols/MxpParser.cs (1)

34-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant namespace qualification.

using MuClient.Core.Text; is already present at Line 3, so MuClient.Core.Text.ILineParser can just be ILineParser (same in PuebloParser).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MuClient.Core/Protocols/MxpParser.cs` at line 34, Remove the redundant
MuClient.Core.Text namespace qualification from the MxpParser declaration and
use the imported ILineParser type directly. Apply the same simplification to the
PuebloParser declaration while preserving both classes’ existing interface
implementations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release.yml:
- Around line 60-64: Move the “Attach to release” step out of the matrix job and
into a separate downstream release job that depends on both matrix publish jobs
completing. Download or collect both publish artifacts there, then invoke
softprops/action-gh-release@v2 once to upload all assets serially for the tag.

In `@CLAUDE.md`:
- Around line 30-31: Correct the project-count statement in the “M1 delivered,
plus substantial M2–M4 work” section of CLAUDE.md to state that MuGlyph.slnx
builds nine projects: five source projects and four test projects.

In `@docs/PACKAGING.md`:
- Around line 23-28: Update the direct osx-arm64 dotnet publish example in
PACKAGING.md to include the IncludeNativeLibrariesForSelfExtract=true MSBuild
property alongside PublishSingleFile=true, matching the profile-based
single-file configuration.

In `@docs/PLAN.md`:
- Around line 117-125: The in-TUI web view is delivered, but its roadmap and
README statuses are inconsistent. In docs/PLAN.md lines 117-125, remove
WebViewPane from “Still open” or limit the entry to remaining Chromium/image
enhancements; in README.md lines 72-75, update “What works today” to describe
the delivered web view consistently with the roadmap.

In `@src/MuClient.Core/Protocols/MxpParser.cs`:
- Around line 538-557: Update CloseInteractionsAtBoundary and the
formatting-frame cleanup so frames remaining after a line boundary cannot
restore a stale SavedInteraction; clear SavedInteraction (and reset mutable
SpanStart to 0 or otherwise rebase it) for affected frames while preserving
deferred-interaction finalization. Ensure a later CloseTag cannot reinstate the
interaction after _interaction has been cleared.

In `@src/MuClient.Core/Protocols/PuebloParser.cs`:
- Around line 94-105: Update PuebloParser.Flush to call
CloseInteractionsAtBoundary before checking _lineSpans.Count, matching the
boundary handling in MxpParser and ensuring unclosed anchor interactions do not
persist into subsequent flushed lines.

In `@src/MuClient.Core/Session/WorldSession.cs`:
- Around line 177-202: Update ApplyEmoji to evaluate emoji substitution using
the complete StyledLine text, preserving adjacent-character boundary context
across span seams. Remap the substituted text back onto the original StyledSpan
ranges while retaining each span’s Style and Interaction, and return the
original line when no substitution occurs.

In `@src/MuClient.Scripting/ScriptException.cs`:
- Around line 35-56: The TryExtractLine method must anchor line extraction to
MoonSharp script-reference headers instead of scanning arbitrary punctuation in
the message. Update syntax parsing to locate the “:(” marker and runtime parsing
to locate the `"chunk":LINE:` header before parsing the numeric line, then add
regression coverage for messages containing brackets and parentheses while
preserving existing valid syntax and runtime extraction.

In `@src/MuClient.Tui/GmcpStats.cs`:
- Line 50: Update the stat value conversion around property.Value.ToString() in
GmcpStats so JsonValueKind.True and JsonValueKind.False render as lowercase
“true” and “false”; preserve the existing conversion behavior for numeric,
string, and other JSON value kinds.

In `@src/MuClient.Tui/MuGlyphApp.cs`:
- Around line 175-197: Handle exceptions from the fire-and-forget LoadWebAsync
call initiated by OpenWeb, including failures from FetchAsync or disposal races,
and surface a clear error through the existing active/system message mechanism
instead of allowing the task exception to go unobserved. Preserve the current
successful web-view display behavior.

In `@src/MuClient.Tui/Views/WebView.cs`:
- Around line 76-81: Update WebView.Scroll so the maximum scroll offset accounts
for the viewport height, limiting _scroll to the last offset that displays a
full screen of content rather than _lines.Count - 1. Preserve the zero lower
bound and existing SetNeedsDraw behavior, using the view’s available height
symbol.

In `@src/MuClient.Web/HtmlStyledRenderer.cs`:
- Around line 89-94: Update the "hr" handling in HtmlStyledRenderer so the
horizontal rule uses the renderer’s configured/requested width instead of the
hardcoded 40-column length. Preserve the existing styling, preformatted output,
line breaks, and blank lines while sizing the generated rule to that width.

In `@src/MuClient.Web/LineWriter.cs`:
- Around line 139-160: The span coalescing loop should avoid repeated string
concatenation that causes quadratic allocations for large preformatted content.
Replace the accumulated runText handling in the visible coalescing method with a
StringBuilder, append matching segment text, flush its value when styles or
links change and at the end, and add a regression test covering many same-style
preformatted spans.

In `@src/MuClient.Web/WebPageFetcher.cs`:
- Around line 54-68: Update the fetch flow to derive finalUri from
response.RequestMessage?.RequestUri ?? uri after redirects, then use finalUri
for HtmlStyledRenderer and both WebPage constructors so page identity and
relative links resolve from the final location. Add a regression test covering
an automatic redirect and verifying the final URI is used.

---

Outside diff comments:
In `@src/MuClient.Core/Telnet/TelnetSession.cs`:
- Around line 244-269: Update DisconnectAsync to dispose the current
_interpreter asynchronously before setting it to null, preserving the existing
disconnected-state reset afterward. Ensure the disposal occurs only when
_interpreter is non-null and avoid relying on DisposeAsync to perform cleanup
after DisconnectAsync clears the field.

---

Nitpick comments:
In `@src/MuClient.Core/Protocols/MxpParser.cs`:
- Line 34: Remove the redundant MuClient.Core.Text namespace qualification from
the MxpParser declaration and use the imported ILineParser type directly. Apply
the same simplification to the PuebloParser declaration while preserving both
classes’ existing interface implementations.

In `@src/MuClient.Tui/GmcpStats.cs`:
- Around line 19-59: Add unit tests for GmcpStats.Update and Summarize, covering
JSON property merging, changed versus unchanged updates, ignored object and
array values, invalid or non-object JSON, package filtering, and deterministic
summary ordering. Include boolean formatting expectations so the tests catch the
referenced formatting issue, while keeping the tests independent of UI
components.

In `@src/MuClient.Tui/Views/OutputView.cs`:
- Around line 20-93: The interaction mapping and click dispatch are duplicated
across src/MuClient.Tui/Views/OutputView.cs#L20-L93 and
src/MuClient.Tui/Views/WebView.cs#L20-L96. Extract the _cellInteractions
storage, per-cell registration, and OnMouseEvent hit-testing into a shared
InteractiveTextView base or CellInteractionMap helper; update OutputView to use
it for command and hyperlink interactions, and update WebView to reuse it with a
filter that accepts only InteractionKind.Hyperlink.

In `@tests/MuClient.Core.Tests/Protocols/MxpParserTests.cs`:
- Around line 232-240: Extend Send_BareWithoutClose_ClosesAtEndOfLine to feed a
nested formatting tag opened inside the SEND and closed on the following line,
such as bold formatting spanning the newline. Assert the second line’s trailing
text span is non-interactive, preserving coverage for the interaction-boundary
behavior described in MxpParser.

In `@tests/MuClient.Core.Tests/Protocols/PuebloParserTests.cs`:
- Around line 139-147: Add a Pueblo parser test alongside
Anchor_XchCmd_RevertsAfterClose that parses the unclosed anchor input "<A
XCH_CMD=\"look\">here\nnext\n" and verifies the span for the second line is not
interactive, covering closure at the line boundary.

In `@tests/MuClient.Core.Tests/Text/WebColorsAndInteractionTests.cs`:
- Around line 64-72: Extend StyledSpan_CarriesInteraction_AndAffectsEquality
with separately constructed StyledSpan instances using identical
SpanInteraction.Command values. Assert these spans are equal and have matching
hash codes, while preserving the existing plain-versus-linked inequality
assertions to verify interaction value equality and hashing.

In `@tests/MuClient.Web.Tests/HtmlStyledRendererTests.cs`:
- Around line 100-105: Update Image_BecomesLabelledLink to assert the selected
span is interactive, has the hyperlink kind, and targets “pic.png”, while
retaining the existing assertion that its text contains “a cat”.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c0d17429-5fa1-4f26-a2d0-06d120e03ac2

📥 Commits

Reviewing files that changed from the base of the PR and between a9d8504 and 28ead03.

📒 Files selected for processing (41)
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • CLAUDE.md
  • Directory.Packages.props
  • MuGlyph.slnx
  • README.md
  • docs/PACKAGING.md
  • docs/PLAN.md
  • src/MuClient.Core/Configuration/WorldDefinition.cs
  • src/MuClient.Core/Protocols/MxpParser.cs
  • src/MuClient.Core/Protocols/PuebloParser.cs
  • src/MuClient.Core/Session/WorldSession.cs
  • src/MuClient.Core/Telnet/TelnetSession.cs
  • src/MuClient.Core/Text/AnsiParser.cs
  • src/MuClient.Core/Text/EmojiSubstitutor.cs
  • src/MuClient.Core/Text/ILineParser.cs
  • src/MuClient.Core/Text/SpanInteraction.cs
  • src/MuClient.Core/Text/StyledSpan.cs
  • src/MuClient.Core/Text/WebColors.cs
  • src/MuClient.Graphics/KittyGraphicsProtocol.cs
  • src/MuClient.Scripting/ScriptException.cs
  • src/MuClient.Tui/GmcpStats.cs
  • src/MuClient.Tui/MuClient.Tui.csproj
  • src/MuClient.Tui/MuGlyphApp.cs
  • src/MuClient.Tui/Properties/PublishProfiles/linux-x64.pubxml
  • src/MuClient.Tui/Properties/PublishProfiles/win-x64.pubxml
  • src/MuClient.Tui/Views/OutputView.cs
  • src/MuClient.Tui/Views/WebView.cs
  • src/MuClient.Web/HtmlStyledRenderer.cs
  • src/MuClient.Web/LineWriter.cs
  • src/MuClient.Web/MuClient.Web.csproj
  • src/MuClient.Web/WebPage.cs
  • src/MuClient.Web/WebPageFetcher.cs
  • tests/MuClient.Core.Tests/Protocols/MxpParserTests.cs
  • tests/MuClient.Core.Tests/Protocols/PuebloParserTests.cs
  • tests/MuClient.Core.Tests/Session/WorldSessionContentTests.cs
  • tests/MuClient.Core.Tests/Text/EmojiSubstitutorTests.cs
  • tests/MuClient.Core.Tests/Text/WebColorsAndInteractionTests.cs
  • tests/MuClient.Graphics.Tests/KittyImageIdValidationTests.cs
  • tests/MuClient.Web.Tests/HtmlStyledRendererTests.cs
  • tests/MuClient.Web.Tests/MuClient.Web.Tests.csproj

Comment thread .github/workflows/release.yml Outdated
Comment thread CLAUDE.md Outdated
Comment thread docs/PACKAGING.md
Comment thread docs/PLAN.md Outdated
Comment thread src/SharpMUTerm.Core/Protocols/MxpParser.cs
Comment thread src/SharpMUTerm.Tui/SharpMUTermApp.cs
Comment thread src/MuClient.Tui/Views/WebView.cs Outdated
Comment thread src/SharpMUTerm.Web/HtmlStyledRenderer.cs
Comment thread src/MuClient.Web/LineWriter.cs Outdated
Comment thread src/MuClient.Web/WebPageFetcher.cs Outdated
claude added 2 commits July 28, 2026 00:44
Resolve the actionable CodeRabbit findings on PR #2.

Correctness
- TelnetSession: dispose the interpreter inside DisconnectAsync before clearing
  it, so it no longer leaks once per disconnect/reconnect.
- MxpParser/PuebloParser: close interaction frames at line/prompt boundaries so
  an unclosed or nested <SEND>/<A> can't make later lines clickable; Pueblo now
  mirrors MXP (CompleteLine + Flush). Regression tests added.
- WorldSession/EmojiSubstitutor: substitute emoji across the whole line
  (EmojiSubstitutor.ApplyToLine) so word boundaries are judged correctly across
  span seams, preserving per-span style/interaction. Tests added.
- ScriptException: anchor line extraction to the ":(" / "]:" header markers so a
  bracket/paren in the message text can't mislead it.
- GmcpStats: render JSON booleans as lowercase true/false.
- WebPageFetcher: resolve page identity and relative links against the final
  (post-redirect) URI.
- LineWriter: StringBuilder coalescing to avoid O(n^2) on large same-style runs.
- HtmlStyledRenderer: size <hr> to the requested width.
- MuGlyphApp: observe and surface exceptions from the fire-and-forget web fetch.
- WebView: clamp scroll to the last full screen.

Delivery / docs
- release.yml: serialize release creation in a downstream job so matrix jobs
  don't race softprops/action-gh-release on the same tag.
- Doc fixes: nine projects (CLAUDE.md), web-view marked delivered (PLAN/README),
  osx-arm64 single-file native-libs note (PACKAGING.md), drop redundant
  namespace qualification on the parsers.

323 tests pass (Core 209, Graphics 57, Scripting 42, Web 15).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
@HarryCordewener
HarryCordewener force-pushed the claude/muglyph-implementation-5f51l3 branch from 6652d2d to ebe8f89 Compare July 28, 2026 01:13
claude added 3 commits July 28, 2026 01:22
Reshape the config schema around the design's connection model:

- WorldDefinition is now a *server* holding CharacterDefinitions; a
  character is the connection unit and carries login (connect string,
  auto-login, on-connect/disconnect) and per-character logging.
- Automation moves off the world into world-independent TriggerSets on
  AppConfiguration; characters opt in by name. A session composes its
  trigger/alias/macro engines from the union of the character's sets.
- WorldSession gains Character, SessionKey (world.character), composed
  ScriptFiles, and character auto-login on connect. SessionManager gets a
  character+sets Open overload and SessionKey lookup.
- Add ConfigurationMigrator (v1→v2): lifts each world's legacy
  triggers/aliases/macros/logging into a generated trigger set + default
  character, run automatically on deserialize.
- Drop the BeipMU importer (not needed).
- WorldDefinition gains an Accent colour for per-world window traceability.

Tests updated for the new shape; 325 passing across the solution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Add MuClient.Core.Workspace — a UI-agnostic model of the design's
tmux-style pane tree, so the layout logic is fully unit-testable before
any Terminal.Gui hosting:

- LayoutNode tree: PaneNode leaves (a tab strip of window ids + active
  tab + frozen flag) and SplitNode interior nodes (row/col, fractional
  sizes summing to 1).
- WorkspaceLayout owns the tree plus focus/zoom state and all mutation:
  split-focused (moving non-active tabs into a new sibling), close, cycle
  focus, zoom, freeze, add/remove/move windows, split-with-window at an
  edge, and reorder active tab. Every op maintains the invariants — no
  empty panes, no single-child splits, always ≥1 pane, valid focus/zoom —
  via a prune-and-fix pass that collapses the tree.

17 workspace tests; 342 passing across the solution. Docs updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Add the Workspace aggregate tying the pane tree to per-window state, so
the design's tab strips / spawn routing / rail have a pure, tested core
ahead of Terminal.Gui hosting:

- WorkspaceWindow: per-window state (id, title, WindowKind, owning
  world.character, unread count, unsent-input marker) independent of pane
  placement.
- Workspace: keeps the layout tree and the window registry consistent —
  OpenWindow places a tab, CloseWindow removes it (pruning empties),
  RouteSpawn finds-or-creates a background spawn window per SpawnTarget
  and accrues unread while it is not the visible tab, ActivateWindow makes
  it visible and clears unread.
- WorkspaceLayout.AddWindow gains an `activate` flag so spawns open in the
  background.

Renamed the namespace MuClient.Core.Workspace → .Workspaces so the
Workspace type isn't shadowed by its own namespace leaf. 351 passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

77-78: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the unsupported macOS packaging claim.

The supplied .github/workflows/release.yml publishes only linux-x64 and win-x64, with matching Linux and Windows profiles. Either remove macOS here or add the corresponding workflow matrix and publish profile.

Suggested correction
-- **Packaging** — self-contained single-file publishing for Linux/Windows/macOS (see
+- **Packaging** — self-contained single-file publishing for Linux/Windows (see
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 77 - 78, Update the Packaging bullet in README.md to
remove the unsupported macOS claim, leaving only Linux and Windows platforms
consistent with the release workflow and available publish profiles.
🧹 Nitpick comments (1)
src/MuClient.Core/Workspace/WorkspaceLayout.cs (1)

122-131: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

AddWindow silently redirects to the focused pane on an unresolved paneId.

Unlike MoveWindowToPane and SplitWithWindow, which return false when the target pane id doesn't resolve, AddWindow falls back to FocusedPane silently (paneId is null ? FocusedPane : FindPane(paneId) ?? FocusedPane). A caller passing a stale/removed pane id would have the window routed to the wrong pane with no signal. Consider returning bool and failing explicitly when a non-null paneId doesn't resolve, matching sibling APIs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MuClient.Core/Workspace/WorkspaceLayout.cs` around lines 122 - 131,
Update AddWindow to return bool and resolve a non-null paneId without falling
back to FocusedPane; return false immediately when FindPane cannot resolve the
requested pane, otherwise detach and add the window to the resolved or focused
pane, then return true. Preserve activation and pruning behavior, and align the
result semantics with MoveWindowToPane and SplitWithWindow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/MuClient.Core/Configuration/CharacterDefinition.cs`:
- Around line 12-13: The CharacterDefinition.Password property is serialized
into config.json as plaintext. Update Password and the ConfigurationStore
serialization/save flow to exclude it from default JSON persistence, and
retrieve or store it through an established secure credential-store mechanism
keyed by character; do not leave a plaintext fallback.

In `@src/MuClient.Core/Configuration/ConfigurationMigrator.cs`:
- Around line 66-114: Update MigrateWorld so a newly created trigger set is also
assigned to existing characters when hasAutomation is true, rather than leaving
it orphaned. Preserve the early return for populated characters when no
automation remains; for populated characters with automation, iterate the
characters and add setName to each character’s triggerSets using the existing
JSON structure without overwriting existing references. Add a migration test
covering characters already present alongside legacy automation fields.

In `@src/MuClient.Core/Workspace/WorkspaceLayout.cs`:
- Around line 3-8: The class documentation for WorkspaceLayout should update its
invariant description to allow the terminal state of one lone pane with an empty
Tabs list after the final pane is closed. Keep the existing guarantees for pane
structure and focus, and explicitly indicate that consumers must not assume a
pane always has an active tab.
- Around line 222-244: Update DetachWindow so pane.ActiveIndex is decremented
only when the removed tab index is strictly before the active index; leave it
unchanged when removing the active tab, then retain the existing ClampActive()
call for tail removals. Add or update coverage for removing an active tab from a
non-edge position while preserving non-active removal behavior.

---

Outside diff comments:
In `@README.md`:
- Around line 77-78: Update the Packaging bullet in README.md to remove the
unsupported macOS claim, leaving only Linux and Windows platforms consistent
with the release workflow and available publish profiles.

---

Nitpick comments:
In `@src/MuClient.Core/Workspace/WorkspaceLayout.cs`:
- Around line 122-131: Update AddWindow to return bool and resolve a non-null
paneId without falling back to FocusedPane; return false immediately when
FindPane cannot resolve the requested pane, otherwise detach and add the window
to the resolved or focused pane, then return true. Preserve activation and
pruning behavior, and align the result semantics with MoveWindowToPane and
SplitWithWindow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 977a5ca3-1d0d-4193-abdb-9d686f4d83a4

📥 Commits

Reviewing files that changed from the base of the PR and between 28ead03 and 5e2c81e.

📒 Files selected for processing (35)
  • .github/workflows/release.yml
  • CLAUDE.md
  • README.md
  • docs/PACKAGING.md
  • docs/PLAN.md
  • src/MuClient.Core/Configuration/AppConfiguration.cs
  • src/MuClient.Core/Configuration/BeipMuImporter.cs
  • src/MuClient.Core/Configuration/CharacterDefinition.cs
  • src/MuClient.Core/Configuration/ConfigurationMigrator.cs
  • src/MuClient.Core/Configuration/ConfigurationStore.cs
  • src/MuClient.Core/Configuration/TriggerSet.cs
  • src/MuClient.Core/Configuration/WorldDefinition.cs
  • src/MuClient.Core/Protocols/MxpParser.cs
  • src/MuClient.Core/Protocols/PuebloParser.cs
  • src/MuClient.Core/Session/SessionManager.cs
  • src/MuClient.Core/Session/WorldSession.cs
  • src/MuClient.Core/Telnet/TelnetSession.cs
  • src/MuClient.Core/Text/EmojiSubstitutor.cs
  • src/MuClient.Core/Workspace/LayoutNode.cs
  • src/MuClient.Core/Workspace/WorkspaceLayout.cs
  • src/MuClient.Scripting/ScriptException.cs
  • src/MuClient.Tui/GmcpStats.cs
  • src/MuClient.Tui/MuGlyphApp.cs
  • src/MuClient.Tui/Views/WebView.cs
  • src/MuClient.Web/HtmlStyledRenderer.cs
  • src/MuClient.Web/LineWriter.cs
  • src/MuClient.Web/WebPageFetcher.cs
  • tests/MuClient.Core.Tests/Configuration/ConfigurationTests.cs
  • tests/MuClient.Core.Tests/Protocols/ParserBoundaryTests.cs
  • tests/MuClient.Core.Tests/Session/WorldSessionContentTests.cs
  • tests/MuClient.Core.Tests/Session/WorldSessionTests.cs
  • tests/MuClient.Core.Tests/Text/EmojiSubstitutorTests.cs
  • tests/MuClient.Core.Tests/Text/WebColorsAndInteractionTests.cs
  • tests/MuClient.Core.Tests/Workspace/WorkspaceLayoutTests.cs
  • tests/MuClient.Web.Tests/HtmlStyledRendererTests.cs
💤 Files with no reviewable changes (1)
  • src/MuClient.Core/Configuration/BeipMuImporter.cs
🚧 Files skipped from review as they are similar to previous changes (14)
  • docs/PACKAGING.md
  • src/MuClient.Scripting/ScriptException.cs
  • tests/MuClient.Core.Tests/Text/WebColorsAndInteractionTests.cs
  • src/MuClient.Core/Telnet/TelnetSession.cs
  • src/MuClient.Tui/GmcpStats.cs
  • tests/MuClient.Web.Tests/HtmlStyledRendererTests.cs
  • src/MuClient.Tui/Views/WebView.cs
  • src/MuClient.Tui/MuGlyphApp.cs
  • src/MuClient.Web/LineWriter.cs
  • src/MuClient.Core/Protocols/MxpParser.cs
  • tests/MuClient.Core.Tests/Session/WorldSessionContentTests.cs
  • src/MuClient.Web/WebPageFetcher.cs
  • src/MuClient.Core/Protocols/PuebloParser.cs
  • src/MuClient.Web/HtmlStyledRenderer.cs

Comment thread src/MuClient.Core/Configuration/CharacterDefinition.cs Outdated
Comment thread src/SharpMUTerm.Core/Configuration/ConfigurationMigrator.cs
Comment thread src/SharpMUTerm.Core/Workspace/WorkspaceLayout.cs
Comment thread src/SharpMUTerm.Core/Workspace/WorkspaceLayout.cs
claude added 19 commits July 28, 2026 01:46
- CharacterDefinition.Password is now [JsonIgnore]: never persisted to
  plaintext config.json (in-memory/session only; secure credential store
  is a follow-up).
- ConfigurationMigrator: when a v1 world already has characters *and*
  legacy automation, wire the lifted trigger set into each existing
  character instead of orphaning it in an unreferenced set.
- WorkspaceLayout.DetachWindow: decrement the active index only for a tab
  strictly before it, so removing the active tab keeps focus on the next
  tab (consistent regardless of position).
- WorkspaceLayout.AddWindow returns bool and fails (no-op) on an
  unresolved paneId, matching MoveWindowToPane/SplitWithWindow.
- WorkspaceLayout doc: note the lone empty-pane terminal state so
  consumers don't assume ActiveTab is always non-null.
- README: drop the unsupported macOS packaging claim (workflow builds
  linux-x64 + win-x64 only).

New tests cover the migration-with-existing-characters case, password
non-persistence, and active-tab removal. 357 passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Two more pure, UI-free primitives the Terminal.Gui pane hosting needs,
kept in Core so they're unit-tested before any view code:

- LayoutSolver: turns the split tree + a bounds rect into per-pane
  PaneRects, reserving a 1-cell divider between siblings, honouring each
  split's fractional sizes (cumulative rounding so no cell drifts), and
  collapsing to the single zoomed pane when one is zoomed. Because
  geometry is derived from the bounds on every call, a terminal resize is
  just a re-solve with the new bounds — the view re-solves per draw.
- PaneCommands: the tmux ⌃B keymap (| - z o x < >) → PaneCommand, plus an
  Apply that drives the WorkspaceLayout, keeping the keymap out of view
  code.

379 passing across the solution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Replace the prerelease Terminal.Gui v2 base (whose Application API was
[Obsolete] mid-migration and which had no native inline-graphics) with
SharpConsoleUI — a stable, net10-targeting compositor framework that
natively provides split layouts, tabs, resizable/mouse windows,
Spectre-style markup, and the Kitty graphics protocol. The switch is
contained to MuClient.Tui because MuClient.Core is UI-agnostic;
Core/Scripting/Web/Graphics are untouched.

- MuGlyphApp rebuilt on ConsoleWindowSystem + WindowBuilder/Controls: a
  MarkupControl output pane, a PromptControl command input, a status
  line, Ctrl+Q quit, background events marshalled via EnqueueOnUIThread,
  and NAWS re-advertised on window resize.
- MarkupFormatter converts the UI-agnostic StyledLine model to
  Spectre-style markup: truecolor fg/bg via the theme, bold/italic/
  underline/etc., and clickable MXP/Pueblo/web spans as [link=…] (custom
  mux:send:/mux:prompt: schemes dispatched on LinkClicked). Bracket
  escaping included.
- The web view opens as a second window rendering the page as markup.
- Removed the Terminal.Gui views (OutputView/CommandInput/WebView/
  ColorMapper); swapped the package reference.
- New MuClient.Tui.Tests (TUnit) covers MarkupFormatter; wired into CI.
  387 passing across the solution (Tui build-verified; visual pass on a
  real terminal, per project norms).

Docs (README/CLAUDE/PLAN) updated to record SharpConsoleUI as the TUI
base.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Turn the single output pane into a SharpConsoleUI TabControl of output
windows, driven by the tested Core.Workspaces model (a single pane whose
tabs are the windows):

- Each window (main + trigger-routed spawn windows + the web view) is a
  MarkupControl tab; the tab's Tag carries the window id.
- A trigger SpawnTarget now actually routes its line to a spawn window
  (created on first use) instead of just announcing it; background tabs
  accrue an unread badge via Workspace.RouteSpawn/NoteActivity.
- Selecting a tab activates that window in the model (clearing unread);
  TabTitles renders each header as "title (unread) ✎" from window state.
- The web view opens as a tab rather than a separate window.

TabTitles is a pure helper with unit tests; 391 passing across the
solution (Tui build-verified, visual pass on a real terminal).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
The single command prompt now remembers a separate draft per window:
typing stores the draft for the visible tab and sets its unsent-input
marker (Workspace.SetUnsentInput → the ✎ badge); switching tabs restores
that window's draft; sending or clearing the line drops the draft and
marker. Builds on the already-tested Workspace state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Make the tabbed windows navigable from the keyboard: Ctrl+N (and Ctrl+Tab
where the terminal reports it) cycles to the next window tab; Ctrl+W
closes the active spawn/web window (the main window is protected) and
drops its pane, draft, and Workspace state. Ctrl+Q still quits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Add a way to capture images of the UI with no terminal or connection —
using SharpConsoleUI's HeadlessConsoleDriver:

- `muglyph --snapshot [--size WxH] [--out file]` builds the app on the
  headless driver, loads a demo scene (a room with clickable exits, a
  Chat spawn window with unread, an input draft), renders one frame, and
  writes the raw ANSI. Force-exits after capture (the framework keeps
  worker threads alive).
- tools/ansi_frame_to_image.py parses the cursor-addressed truecolor
  frame into a grid and emits a self-contained SVG or HTML (no deps).
- tools/make-screenshots.sh regenerates docs/screenshots/*.
- docs/SCREENSHOTS.md documents the pipeline (+ VHS for animated demos).
- README embeds the generated screenshot.

The frame is deterministic (desktop panels off under headless), so it can
double as a CI golden file. Committed a first demo frame + SVG/HTML.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Remove the README screenshot embed and the committed sample frames — the
current shell isn't at the design's fidelity yet, so it shouldn't be the
project's face. The snapshot tooling (muglyph --snapshot,
tools/ansi_frame_to_image.py, make-screenshots.sh, docs/SCREENSHOTS.md)
stays so images can be regenerated once the UI matches the design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Add the tested, UI-agnostic models the design's UI needs, so the view
layer is correct-by-construction and verifiable:

- Commands: CommandItem/CommandGroup, CommandCatalog (generates the ⌃P
  catalog from live workspace state — switch-character and go-to-window
  entries, stateful logging/zoom/freeze labels, GO TO/WORLD/TERMINAL/
  LAYOUT groups) and CommandMatcher (prefix > substring > subtitle >
  fuzzy-subsequence ranking, case-insensitive, empty query = all).
- Workspaces.RailModel: projects worlds→characters→windows into the
  connection-rail rows (accent, connected dot, active marker, unread,
  ✎ unsent, hosting pane; windows expanded only under the active
  character; "no characters" placeholder).
- Text.Meters (HP/EN bars + keepalive sparkline) and Text.StatusFormatter
  (Corvid@aetherfall › prompt + destination/drafts/char-count gutter).

24 new tests; 285 Core tests, all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Wire the design's command surface: Ctrl+P toggles a modal search box
over the live-generated catalog (CommandCatalog), ranked by
CommandMatcher and rendered grouped GO TO / WORLD / TERMINAL / LAYOUT by
CommandSurfaceRenderer (pure, unit-tested — Order + Render). ↑↓ walk the
flattened results, ⏎ dispatches the selected id, Esc closes.

DispatchCommand runs what the current tabbed shell supports (go-to
window, zoom/freeze/close/split on the layout model, clear window,
reconnect/disconnect) and notes commands not yet wired.

CommandSurfaceRenderer has its own tests; solution builds, Core 285 +
Tui 15 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Restructure the window toward the design's regions: a header row
(☰ glyph·tui + log/graphics/⌃P hints, sticky top), the tabbed pane area
(fill), the input prompt (sticky bottom, now the › glyph), and a status
bar (sticky bottom): connection dot + character + state, HP/EN meters
rendered from GMCP Char.Vitals via Core.Text.Meters, host, and the
palette hint. GmcpStats.GetInt feeds the meters. The demo scene seeds
sample vitals so snapshots show the bars.

Builds; 300 tests green (Core 285 + Tui 15).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Replace the Run()-on-a-worker-thread render (which raced the framework's
input+render pump and hung) with a single inline ConsoleWindowSystem
.ForceRender() — one render cycle, no loop, no thread, no exit race. The
HeadlessConsoleDriver writes the composited frame to the console, which
we capture by redirecting Console.Out for that one call. Measured end to
end: ctor+scene+render ≈ 240ms, deterministic, exit 0.

Fix identified by a background investigation agent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Adds the left connection rail from the design handoff. RailRenderer (pure,
tested) maps RailModel rows to markup: a CONNECTIONS header, each world with an
accent spine and host line, characters with a connected dot (●/○) and active
marker (▸), and — under the active character only — its windows with unread,
unsent (✎), and hosting-pane detail. A world with no characters prints
"no characters".

MuGlyphApp hosts it in a HorizontalGrid (rail column 30 wide + splitter + the
pane area flexing to fill), projects config worlds + workspace windows into rail
rows via BuildRail(), and refreshes on every activity/connection change. Accents
fall back to a per-world palette when a world hasn't set its own. The headless
demo scene seeds two demo worlds so snapshots exercise the rail.

Verified by a 120x32 --snapshot frame reconstruction: header, world spines,
active/inactive characters, window rows, splitter, and status meters all render.
421 tests green (7 new RailRenderer tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Threads the world accent (its own, or a per-world palette fallback) through the
chrome so the regions read as one workspace: the header shows the active world
with its accent spine and character (`▚ Aetherfall · Corvid`) plus a
connected-count summary, and the status bar's `●` picks up the same accent. The
rail already carried accents; ActiveWorld()/AccentFor()/AccentHex() centralise
the lookup shared by all three.

Also fixes a real, non-deterministic snapshot hang: `--snapshot` constructed the
window system while stdin was still attached, and the framework's console input
setup blocks forever when stdin is an interactive TTY or an open pipe — so the
frame never rendered when launched in the foreground (it only "worked" when
stdin happened to be /dev/null, e.g. backgrounded or redirected). Detaching
stdin (Console.SetIn(TextReader.Null)) in the snapshot branch makes it return
EOF immediately, so the frame renders deterministically however it's launched.
Proven: 4/4 foreground runs now emit an identical 12757-byte frame where they
previously hung.

421 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Completes the input region from the design. The prompt is now bound to the
focused character (`Corvid@Aetherfall ›` rather than a bare `›`), and a thin dim
gutter sits above it showing the destination window, any other windows holding
unsent drafts, and the live character count (`→ Aardwolf  15`). Both strings come
from the already-tested StatusFormatter; UpdateInputChrome() refreshes them on
every input/tab/activity change.

Verified via a 120x30 snapshot reconstruction: gutter and character prompt render
in order above the status bar. 307 Core+Tui tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
The workspace split-tree model was fully wired (split-right/down move non-active
tabs into a sibling pane) but the view only ever showed one TabControl. Now the
pane area renders the tree itself: each leaf pane is a TabControl, a row split
becomes a proportional column grid and a column split a row grid, with a
draggable splitter between children, and a zoomed pane renders full-area.

Tab management is now model-driven end to end. BuildWorkspaceRow() projects the
layout (honouring zoom) into a control tree tracked by pane id; RebuildPaneArea()
swaps the fresh row into the live window (RemoveContent + InsertControl) on every
layout change — split, close, zoom, and first-seen spawn/web windows. NextWindow,
CloseActiveWindow, Activate, and tab-change all operate on the focused pane's
TabControl, and the split commands from the ⌃P surface now visibly reshape the
workspace. Per-pane content controls (scrollback) persist across rebuilds.

Proven via snapshot: single pane renders unchanged (no regression), and a forced
split renders two panes side by side — left "Aardwolf" showing the room, right
"Chat" showing the chat lines, divider between — matching the rail's pane labels.
307 Core+Tui tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Adds Ctrl+O to move focus to the next pane in a split, routing the input line to
that pane's active window (draft restored) and focusing its tab strip — splits
are navigable from the keyboard, not just the ⌃P surface.

Docs: README and PLAN now describe the delivered multi-pane workspace (rail,
split panes, command surface, accents, status meters, input gutter) instead of
listing it as upcoming, and the "still open" list narrows to freeze-view,
settings dialogs, and mouse/drag. Reconciles the web-view status the roadmap and
README already agreed on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Adds a CI step that runs `muglyph --snapshot` and asserts the frame contains the
workspace markers (CONNECTIONS rail header, a seeded world, a character, the
palette hint). Because snapshot mode renders one frame and exits, a return of the
stdin-block hang would stall the step (caught by the job timeout), and any
UI-render regression drops a token — turning the manual snapshot checks into an
automated end-to-end guard. Verified locally against the Release build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
claude added 7 commits July 28, 2026 15:51
WorkspaceState captures/restores a workspace — its windows (id/title/kind/
session/owner/capture) and the full pane tree with focus and zoom — so the last
session can be saved and resumed. Restore rebuilds a live Workspace via new
restore constructors on Workspace and WorkspaceLayout (the latter advances its
pane counter past restored ids so future splits never collide). Scrollback and
transient badges are intentionally excluded. Round-trips through the config's
JSON serializer. 4 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
The app now rebuilds its workspace from AppConfiguration.LastSession at
construction (ResumeOrNew) — real startup and the demo share the exact
'load config → resume last session' path, so snapshots reflect genuine
behaviour rather than a bespoke scene. Corrupt saved state falls back to a
fresh workspace.

The demo is now a config: DemoScene.Build() assembles worlds, characters,
trigger sets, and a saved LastSession (Corvid's main + a Chat spawn in one
pane); LoadDemoScene only feeds scrollback into the already-resumed windows.
AppConfiguration gains LastSession (round-trips through the store; +1 test).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
On clean exit the live app captures its workspace into config.LastSession and
writes it to the default config path, so the next launch resumes the same panes
and windows. A save failure is swallowed — it never affects the exit code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Replace the framework's fixed double-line (║/═) grid splitters with thin solid
1-cell dividers in the subtle border colour, so panes and the rail read as a
single calm line instead of a busy double rule — and output no longer appears
to bleed across the divider. Shorten the wordmark to 'glyph' and render the
menu affordance as a filled accent 'button' (menu glyph + wordmark, padded a
space each side).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Header is now an Oh-My-Posh-style powerline ribbon — a clickable 'glyph' button
(on-brand violet) flowing through the world and character on solid triangle
separators () into the menu-bar band (a distinct chrome background). Nerd
Font separators, colours resolved through the theme/accents. Powerline is a
pure, tested renderer (3 tests).

Input region gets its own subtly-elevated background (gutter + prompt) so it's
easy to tell apart from the output above and status below, and the gutter's
bare char count now reads '15 chars'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
- Remove the input gutter entirely — the input bar's own background is enough
  to set it apart; its character count moves to the status bar ('15 chars', or
  the 'back to draft' hint while recalling history).
- Status bar drops the HP/EN meters (kept the keepalive sparkline, host,
  encoding) and gains the live char count.
- Header spans the full console: the menu + world/character powerline ribbon
  stays left, while the connection/LOG/graphics cluster is right-aligned to the
  far edge (re-aligned on resize). The clock is removed.
- Give the input field its own elevated background.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Bump the input field's elevated background to a more distinct blue-grey so the
input bar reads apart from the output body at a glance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

🧹 Nitpick comments (10)
tests/MuClient.Core.Tests/Text/MetersAndStatusTests.cs (1)

27-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the obsolete HasLength assertion.

Proposed fix
-        await Assert.That(spark).HasLength(4);
+        await Assert.That(spark).Length().IsEqualTo(4);
#!/bin/bash
set -eu

rg -n 'HasLength\(|Length\(\)\.IsEqualTo' \
  tests/MuClient.Core.Tests/Text/MetersAndStatusTests.cs
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/MuClient.Core.Tests/Text/MetersAndStatusTests.cs` at line 27, Replace
the obsolete HasLength assertion for spark with the current supported assertion
API, preserving the expected length of 4. Update any matching obsolete length
assertions in MetersAndStatusTests identified by the provided search.
tools/make-screenshots.sh (2)

24-25: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Parses the same frame twice; reuse the grid or note the intent. Minor, but each render invocation re-reads and re-parses $name.ansi to emit SVG then HTML. Adding a dual-output mode to ansi_frame_to_image.py would halve the work and keep both outputs provably in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/make-screenshots.sh` around lines 24 - 25, Update the screenshot
generation flow around ansi_frame_to_image.py so each $name.ansi frame is parsed
only once while producing both the SVG and HTML outputs. Add or use a
dual-output capability that writes the two requested destinations from the same
parsed grid, preserving the existing output filenames and formats.

16-23: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Hardcoded DLL path breaks if the assembly name or output layout changes.

$ROOT/src/MuClient.Tui/bin/Release/net10.0/muglyph.dll assumes the assembly is named muglyph and that no RuntimeIdentifier is set (which would nest the output under net10.0/<rid>/). The publish profiles in this PR add RID-specific publishing, so this is worth pinning down. Driving the app through dotnet run --project avoids the guess entirely and matches how the rest of the repo invokes .NET projects.

♻️ Proposed refactor
-echo "Building muglyph…"
-dotnet build "$ROOT/src/MuClient.Tui/MuClient.Tui.csproj" -c Release >/dev/null
-
-DLL="$ROOT/src/MuClient.Tui/bin/Release/net10.0/muglyph.dll"
+PROJ="$ROOT/src/MuClient.Tui/MuClient.Tui.csproj"
+
+echo "Building muglyph…"
+dotnet build "$PROJ" -c Release >/dev/null
 
 render() {
   local name="$1" size="$2"
   echo "Rendering $name ($size)…"
-  dotnet "$DLL" --snapshot --size "$size" --out "$OUT/$name.ansi"
+  dotnet run --project "$PROJ" -c Release --no-build -- \
+    --snapshot --size "$size" --out "$OUT/$name.ansi"

Alternatively resolve the path with dotnet build "$PROJ" -c Release --getProperty:TargetPath.

#!/bin/bash
# Confirm assembly name and any RuntimeIdentifier in the TUI project
fd -g 'MuClient.Tui.csproj' --exec cat -n {}
fd -g '*.pubxml' --exec sh -c 'echo "== {}"; cat {}'

Based on learnings: "Use dotnet build as the build verification signal; run TUnit projects with dotnet run --project, rather than relying on dotnet test."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/make-screenshots.sh` around lines 16 - 23, Replace the hardcoded DLL
path used by render() with project-based execution via dotnet run --project,
reusing the MuClient.Tui project path and Release configuration established by
the build step. Preserve the existing snapshot arguments, size handling, output
naming, and build verification behavior without assuming an assembly name or
output directory layout.
tools/ansi_frame_to_image.py (1)

96-101: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Wide glyphs advance the column by one, so East-Asian/emoji cells shift the rest of the row.

The parser assumes every printable char is one cell. Nerd Font icons and box drawing are single-width so current snapshots are fine, but any double-width codepoint will misalign the remainder of the row against the terminal's own accounting. Worth a note in the docstring or a unicodedata.east_asian_width check if snapshots ever include such content.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/ansi_frame_to_image.py` around lines 96 - 101, Update the
printable-character parsing branch to account for East Asian and emoji
double-width glyphs instead of always incrementing col by one. Use a Unicode
width check (such as unicodedata.east_asian_width) and advance the column by the
glyph’s terminal width so subsequent cells remain aligned.
src/MuClient.Core/Workspace/Workspace.cs (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale "Terminal.Gui" references in doc comments — the TUI base was renamed to SharpConsoleUI. This PR consistently renames the UI framework everywhere else (CLAUDE.md, docs/PLAN.md, README.md), but these three doc comments still describe a "Terminal.Gui layer"/"Terminal.Gui key handler" consuming the pure Core model.

  • src/MuClient.Core/Workspace/Workspace.cs#L3-10: update "the Terminal.Gui layer renders from it and calls its operations" to reference SharpConsoleUI.
  • src/MuClient.Core/Workspace/LayoutSolver.cs#L10-15: update "the Terminal.Gui layer maps the resulting PaneRects onto views" to reference SharpConsoleUI.
  • src/MuClient.Core/Workspace/PaneCommands.cs#L31-36: update "the Terminal.Gui key handler resolves a key here" to reference SharpConsoleUI.

As per coding guidelines, **/MuClient.Core/**/*.cs: "Keep MuClient.Core UI-agnostic: transport, telnet, parsing, routing, scrollback, engines, and logging must not depend on UI frameworks." — leaving stale, wrong-framework references in Core's own doc comments works against that agnosticism in spirit and could mislead future contributors about which framework actually consumes these models.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MuClient.Core/Workspace/Workspace.cs` at line 1, Update the XML
documentation comments in Workspace, LayoutSolver, and PaneCommands to replace
each stale “Terminal.Gui” reference with “SharpConsoleUI,” preserving the
existing descriptions and keeping MuClient.Core implementation UI-agnostic.

Source: Coding guidelines

src/MuClient.Tui/MuGlyphApp.cs (1)

1067-1079: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

title parameter is unused.

PaneContentFor never reads title; every call site computes one anyway. Drop it, or use it (e.g. for an accessibility/debug label).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MuClient.Tui/MuGlyphApp.cs` around lines 1067 - 1079, The title parameter
is unused in PaneContentFor. Remove title from the method signature and update
every call site to stop computing and passing it, unless the parameter is
intentionally needed for an accessibility or debugging label.
src/MuClient.Tui/WorldsScreenRenderer.cs (1)

128-135: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Padding is applied to the escaped string.

Escape(...).PadRight(13) counts [[/]] as two columns each, so a name containing a bracket misaligns the row. Pad on visible width (you already have VisibleLength) instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MuClient.Tui/WorldsScreenRenderer.cs` around lines 128 - 135, Update
CharacterRow so the escaped character name is padded based on visible display
width rather than raw markup length. Reuse the existing VisibleLength helper
when calculating the required padding, preserving the current 13-column layout
and escaping behavior.
src/MuClient.Tui/MarkupFormatter.cs (1)

131-141: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

tokens.Count == 0 is unreachable.

Hex(fg) is always added just above, so the null branch never fires and every span gets a tag. Either drop the dead check or skip tag emission when the style is fully default (cheaper output).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MuClient.Tui/MarkupFormatter.cs` around lines 131 - 141, The tokens.Count
== 0 check in the markup formatter is unreachable because Hex(fg) is always
added. Update the surrounding formatter method to remove the dead null branch
and return the generated tag directly, or explicitly detect a fully default
style before adding tokens so default spans can omit tag emission.
src/MuClient.Tui/RailRenderer.cs (1)

49-55: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Label[..1] can split a surrogate pair.

For a name starting with a non-BMP rune the collapsed rail emits a lone surrogate. StringInfo.GetTextElementEnumerator / EnumerateRunes would give a safe first grapheme.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MuClient.Tui/RailRenderer.cs` around lines 49 - 55, Update the
initial-character extraction in the RailRenderer character-row branch to avoid
slicing Label with Label[..1], which can split surrogate pairs. Use
EnumerateRunes or StringInfo.GetTextElementEnumerator to obtain the first
complete grapheme/rune while preserving the existing "?" fallback and Escape
processing.
src/MuClient.Tui/TimersScreenRenderer.cs (1)

38-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Left column isn't padded, so the divider is ragged.

TriggersScreenRenderer and WorldsScreenRenderer both pad the left column to a fixed visible width before joining; this one concatenates raw, so the divider zig-zags with row length.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MuClient.Tui/TimersScreenRenderer.cs` around lines 38 - 44, Update the
row construction in the timer screen renderer to pad each left-column value to
the same fixed visible width used by TriggersScreenRenderer and
WorldsScreenRenderer before appending the divider. Preserve the existing
empty-value handling and right-column placement while ensuring every “│” aligns
vertically.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLAUDE.md`:
- Around line 45-49: Update the Tui bullet in CLAUDE.md to remove the stale
“splits ... + rail layer on next” wording and describe split panes and the
connection rail as already rendered by the SharpConsoleUI shell, consistent with
docs/PLAN.md and README.md.

In `@docs/design/README.md`:
- Line 111: Correct the “Connection rail” description in the design README to
label 204 and 46 as pixel widths, not column counts, and align the wording with
the approximate column conversions documented later on the referenced line.
- Around line 15-21: The design documentation contains stale framework and
file-path references. Update docs/design/README.md to identify
Glyph-TUI-v3.dc.html correctly and replace Terminal.Gui v2, Theme, and
ColorMapper guidance with SharpConsoleUI guidance scoped to MuClient.Tui, while
preserving the instruction to rebuild the screens rather than port the HTML
structure.

In `@src/MuClient.Core/Commands/CommandCatalog.cs`:
- Line 3: Convert the namespace declarations to file-scoped form and remove each
corresponding closing brace: update src/MuClient.Core/Commands/CommandCatalog.cs
(3-3), src/MuClient.Core/Text/StatusFormatter.cs (1-1), and
src/MuClient.Tui/TabTitles.cs (3-3), plus
tests/MuClient.Tui.Tests/TimersScreenRendererTests.cs (5-5),
TriggersScreenRendererTests.cs (6-6), WorldsScreenRendererTests.cs (6-6),
AliasesScreenRendererTests.cs (5-5), CaptureLineRendererTests.cs (3-3),
CommandSurfaceRendererTests.cs (4-4), KeypadScreenRendererTests.cs (4-4), and
RailRendererTests.cs (5-5). Preserve all existing members while ensuring every
affected C# file uses a file-scoped namespace.

In `@src/MuClient.Core/Commands/CommandMatcher.cs`:
- Around line 79-111: Update FuzzySpread to evaluate all valid fuzzy subsequence
matches rather than greedily fixing the first matching start, and return the
smallest span between matched characters. Preserve null for queries that cannot
be matched while ensuring tighter runs receive the better score.

In `@src/MuClient.Core/Workspace/WorkspaceState.cs`:
- Around line 103-112: Update BuildNode to explicitly handle the "pane" type
when creating a PaneNode, and reject any other unknown LayoutNodeState.Type
value instead of treating it as a pane. Preserve the existing case-insensitive
"split" handling and use the established exception pattern for invalid
serialized node types.

In `@src/MuClient.Core/Workspaces/RailModel.cs`:
- Around line 66-72: Update the world-row construction flow around
RailRowKind.World to add the documented RailRowKind.Host row using the current
RailWorld.Host and RailWorld.Port values, formatted as the expected host:port
display, before processing characters. Preserve the existing empty-character
handling and continuation behavior.

In `@src/MuClient.Tui/AliasesScreenRenderer.cs`:
- Around line 106-108: Update the case-sensitive label in the alias rendering
logic around alias.CaseSensitive so the checkbox brackets are escaped as literal
markup text, while preserving the existing selected/unselected styling and
labels.

In `@src/MuClient.Tui/DemoScene.cs`:
- Around line 96-100: Update the wtf Alias pattern and substitution in the
DemoScene alias configuration so the $1 reference has a corresponding captured
argument, or remove $1 if wtf is intentionally argumentless; preserve the
existing say alias unchanged.

In `@src/MuClient.Tui/MarkupFormatter.cs`:
- Around line 143-151: Update LinkFor’s InteractionKind.Hyperlink branch to
percent-encode square brackets in interaction.Target before embedding it in the
link markup, while preserving the rest of the URL unchanged. Ensure
OnLinkClicked decodes the same encoding symmetrically when handling hyperlink
targets.

In `@src/MuClient.Tui/MuGlyphApp.cs`:
- Around line 1542-1554: Update CloseActiveWindow to also remove the closed
window’s entries from _lines and _freezePoints, alongside the existing _panes
and _drafts cleanup. Ensure reopening a window with the same
Workspace.SpawnWindowId does not reuse old scrollback or freeze-point state.
- Around line 469-481: Update AppendWindowLine to enforce
_config.ScrollbackLines when adding markup to each window buffer, trimming the
oldest entries whenever the buffer exceeds the configured limit. Keep the pane
append behavior unchanged and ensure BuildFrozenContent receives only the
bounded scrollback history.
- Around line 1441-1465: Update HandleMoveKey so _moveTargetPaneId is assigned
only when _moveLetters contains a pane whose value matches ch; leave the
existing target unchanged for unmatched move-mode letters, while preserving the
current rebuild and status-update behavior for valid matches.

In `@src/MuClient.Tui/OptionsScreenRenderer.cs`:
- Around line 53-57: Escape checkbox brackets in RenderRow, RightColumn, and
BuildEditor so all toggle glyphs emit markup-safe [[x]] and [[ ]] values;
optionally centralize this formatting in a shared Checkbox(bool) helper and use
it across the three affected files: src/MuClient.Tui/OptionsScreenRenderer.cs
lines 53-57, src/MuClient.Tui/TimersScreenRenderer.cs lines 100-101, and
src/MuClient.Tui/TriggersScreenRenderer.cs lines 167-169.

In `@tests/MuClient.Tui.Tests/CommandSurfaceRendererTests.cs`:
- Around line 23-24: Update the assertion for CommandSurfaceRenderer.Order in
the ordered ID test to use IsEquivalentTo with CollectionOrdering.Matching,
preserving the expected sequence and making the comparison order-sensitive.

In `@tools/ansi_frame_to_image.py`:
- Around line 11-12: Update the usage examples in the ansi_frame_to_image.py
module docstring to invoke ansi_frame_to_image.py instead of
ansi_frame_to_svg.py, preserving the existing command arguments and output
behavior.
- Around line 231-241: Update main to separate the --html flag from positional
input and output paths before reading data or assigning out_path. Ensure
stdin+--html and input-path+--html treat --html only as a flag, while preserving
HTML inference from .html output paths. Open input and output files with
deterministic context-managed handling.
- Around line 106-124: Extend apply_sgr to consume 256-colour sequences 38;5;<n>
and 48;5;<n> as complete parameters, preventing their components from being
processed as standalone SGR codes. Handle 39 and 49 by restoring DEFAULT_FG and
DEFAULT_BG, and map basic foreground/background palette codes, including bright
variants, to their corresponding colours while preserving existing truecolor and
attribute behavior.

In `@tools/fonts/README.md`:
- Around line 11-19: Add the complete SIL Open Font License 1.1 text and
copyright notice as tools/fonts/OFL.txt, add the applicable Nerd Fonts MIT
license text in the same directory, and update the Licensing section of the
README to reference both bundled files alongside the existing attribution and
usage details.

---

Nitpick comments:
In `@src/MuClient.Core/Workspace/Workspace.cs`:
- Line 1: Update the XML documentation comments in Workspace, LayoutSolver, and
PaneCommands to replace each stale “Terminal.Gui” reference with
“SharpConsoleUI,” preserving the existing descriptions and keeping MuClient.Core
implementation UI-agnostic.

In `@src/MuClient.Tui/MarkupFormatter.cs`:
- Around line 131-141: The tokens.Count == 0 check in the markup formatter is
unreachable because Hex(fg) is always added. Update the surrounding formatter
method to remove the dead null branch and return the generated tag directly, or
explicitly detect a fully default style before adding tokens so default spans
can omit tag emission.

In `@src/MuClient.Tui/MuGlyphApp.cs`:
- Around line 1067-1079: The title parameter is unused in PaneContentFor. Remove
title from the method signature and update every call site to stop computing and
passing it, unless the parameter is intentionally needed for an accessibility or
debugging label.

In `@src/MuClient.Tui/RailRenderer.cs`:
- Around line 49-55: Update the initial-character extraction in the RailRenderer
character-row branch to avoid slicing Label with Label[..1], which can split
surrogate pairs. Use EnumerateRunes or StringInfo.GetTextElementEnumerator to
obtain the first complete grapheme/rune while preserving the existing "?"
fallback and Escape processing.

In `@src/MuClient.Tui/TimersScreenRenderer.cs`:
- Around line 38-44: Update the row construction in the timer screen renderer to
pad each left-column value to the same fixed visible width used by
TriggersScreenRenderer and WorldsScreenRenderer before appending the divider.
Preserve the existing empty-value handling and right-column placement while
ensuring every “│” aligns vertically.

In `@src/MuClient.Tui/WorldsScreenRenderer.cs`:
- Around line 128-135: Update CharacterRow so the escaped character name is
padded based on visible display width rather than raw markup length. Reuse the
existing VisibleLength helper when calculating the required padding, preserving
the current 13-column layout and escaping behavior.

In `@tests/MuClient.Core.Tests/Text/MetersAndStatusTests.cs`:
- Line 27: Replace the obsolete HasLength assertion for spark with the current
supported assertion API, preserving the expected length of 4. Update any
matching obsolete length assertions in MetersAndStatusTests identified by the
provided search.

In `@tools/ansi_frame_to_image.py`:
- Around line 96-101: Update the printable-character parsing branch to account
for East Asian and emoji double-width glyphs instead of always incrementing col
by one. Use a Unicode width check (such as unicodedata.east_asian_width) and
advance the column by the glyph’s terminal width so subsequent cells remain
aligned.

In `@tools/make-screenshots.sh`:
- Around line 24-25: Update the screenshot generation flow around
ansi_frame_to_image.py so each $name.ansi frame is parsed only once while
producing both the SVG and HTML outputs. Add or use a dual-output capability
that writes the two requested destinations from the same parsed grid, preserving
the existing output filenames and formats.
- Around line 16-23: Replace the hardcoded DLL path used by render() with
project-based execution via dotnet run --project, reusing the MuClient.Tui
project path and Release configuration established by the build step. Preserve
the existing snapshot arguments, size handling, output naming, and build
verification behavior without assuming an assembly name or output directory
layout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5ad53285-dcb2-4269-a20c-861b35ec0b84

📥 Commits

Reviewing files that changed from the base of the PR and between 5e2c81e and 2ab9f7e.

⛔ Files ignored due to path filters (1)
  • tools/fonts/MuGlyphMonoNerd.woff is excluded by !**/*.woff
📒 Files selected for processing (88)
  • .github/workflows/ci.yml
  • .gitignore
  • CLAUDE.md
  • Directory.Packages.props
  • MuGlyph.slnx
  • README.md
  • docs/PLAN.md
  • docs/SCREENSHOTS.md
  • docs/design/Glyph-TUI-v3.dc.html
  • docs/design/README.md
  • docs/design/support.js
  • src/MuClient.Core/Automation/TimerDefinition.cs
  • src/MuClient.Core/Automation/TriggerEngine.cs
  • src/MuClient.Core/Commands/CommandCatalog.cs
  • src/MuClient.Core/Commands/CommandItem.cs
  • src/MuClient.Core/Commands/CommandMatcher.cs
  • src/MuClient.Core/Configuration/AppConfiguration.cs
  • src/MuClient.Core/Configuration/CharacterDefinition.cs
  • src/MuClient.Core/Configuration/ConfigurationMigrator.cs
  • src/MuClient.Core/Configuration/TriggerSet.cs
  • src/MuClient.Core/Configuration/WorldDefinition.cs
  • src/MuClient.Core/Input/InputHistory.cs
  • src/MuClient.Core/Text/Meters.cs
  • src/MuClient.Core/Text/StatusFormatter.cs
  • src/MuClient.Core/Text/StyledLine.cs
  • src/MuClient.Core/Workspace/DropZones.cs
  • src/MuClient.Core/Workspace/LayoutNode.cs
  • src/MuClient.Core/Workspace/LayoutSolver.cs
  • src/MuClient.Core/Workspace/PaneCommands.cs
  • src/MuClient.Core/Workspace/Workspace.cs
  • src/MuClient.Core/Workspace/WorkspaceLayout.cs
  • src/MuClient.Core/Workspace/WorkspaceState.cs
  • src/MuClient.Core/Workspace/WorkspaceWindow.cs
  • src/MuClient.Core/Workspaces/RailModel.cs
  • src/MuClient.Tui/AliasesScreenRenderer.cs
  • src/MuClient.Tui/CaptureLineRenderer.cs
  • src/MuClient.Tui/ColorMapper.cs
  • src/MuClient.Tui/CommandPalette.cs
  • src/MuClient.Tui/CommandSurfaceRenderer.cs
  • src/MuClient.Tui/DemoScene.cs
  • src/MuClient.Tui/FreezeBarRenderer.cs
  • src/MuClient.Tui/Glyphs.cs
  • src/MuClient.Tui/GmcpStats.cs
  • src/MuClient.Tui/KeypadScreenRenderer.cs
  • src/MuClient.Tui/MarkupFormatter.cs
  • src/MuClient.Tui/MuClient.Tui.csproj
  • src/MuClient.Tui/MuGlyphApp.cs
  • src/MuClient.Tui/OptionsScreenRenderer.cs
  • src/MuClient.Tui/Powerline.cs
  • src/MuClient.Tui/Program.cs
  • src/MuClient.Tui/RailRenderer.cs
  • src/MuClient.Tui/SettingsOverlay.cs
  • src/MuClient.Tui/TabTitles.cs
  • src/MuClient.Tui/TimersScreenRenderer.cs
  • src/MuClient.Tui/TriggersScreenRenderer.cs
  • src/MuClient.Tui/Views/CommandInput.cs
  • src/MuClient.Tui/Views/OutputView.cs
  • src/MuClient.Tui/WorldsScreenRenderer.cs
  • tests/MuClient.Core.Tests/Automation/TriggerEngineTests.cs
  • tests/MuClient.Core.Tests/Commands/CommandCatalogTests.cs
  • tests/MuClient.Core.Tests/Commands/CommandMatcherTests.cs
  • tests/MuClient.Core.Tests/Configuration/ConfigurationTests.cs
  • tests/MuClient.Core.Tests/Input/InputHistoryTests.cs
  • tests/MuClient.Core.Tests/Text/MetersAndStatusTests.cs
  • tests/MuClient.Core.Tests/Workspace/DropZonesTests.cs
  • tests/MuClient.Core.Tests/Workspace/LayoutSolverTests.cs
  • tests/MuClient.Core.Tests/Workspace/PaneCommandsTests.cs
  • tests/MuClient.Core.Tests/Workspace/RailModelTests.cs
  • tests/MuClient.Core.Tests/Workspace/WorkspaceLayoutTests.cs
  • tests/MuClient.Core.Tests/Workspace/WorkspaceStateTests.cs
  • tests/MuClient.Core.Tests/Workspace/WorkspaceTests.cs
  • tests/MuClient.Tui.Tests/AliasesScreenRendererTests.cs
  • tests/MuClient.Tui.Tests/CaptureLineRendererTests.cs
  • tests/MuClient.Tui.Tests/CommandSurfaceRendererTests.cs
  • tests/MuClient.Tui.Tests/FreezeBarRendererTests.cs
  • tests/MuClient.Tui.Tests/KeypadScreenRendererTests.cs
  • tests/MuClient.Tui.Tests/MarkupFormatterTests.cs
  • tests/MuClient.Tui.Tests/MuClient.Tui.Tests.csproj
  • tests/MuClient.Tui.Tests/OptionsScreenRendererTests.cs
  • tests/MuClient.Tui.Tests/PowerlineTests.cs
  • tests/MuClient.Tui.Tests/RailRendererTests.cs
  • tests/MuClient.Tui.Tests/TabTitlesTests.cs
  • tests/MuClient.Tui.Tests/TimersScreenRendererTests.cs
  • tests/MuClient.Tui.Tests/TriggersScreenRendererTests.cs
  • tests/MuClient.Tui.Tests/WorldsScreenRendererTests.cs
  • tools/ansi_frame_to_image.py
  • tools/fonts/README.md
  • tools/make-screenshots.sh
💤 Files with no reviewable changes (3)
  • src/MuClient.Tui/ColorMapper.cs
  • src/MuClient.Tui/Views/OutputView.cs
  • src/MuClient.Tui/Views/CommandInput.cs
🚧 Files skipped from review as they are similar to previous changes (12)
  • src/MuClient.Core/Configuration/TriggerSet.cs
  • MuGlyph.slnx
  • src/MuClient.Core/Configuration/CharacterDefinition.cs
  • src/MuClient.Tui/GmcpStats.cs
  • Directory.Packages.props
  • .github/workflows/ci.yml
  • src/MuClient.Core/Workspace/LayoutNode.cs
  • src/MuClient.Core/Configuration/ConfigurationMigrator.cs
  • src/MuClient.Core/Configuration/AppConfiguration.cs
  • tests/MuClient.Core.Tests/Configuration/ConfigurationTests.cs
  • tests/MuClient.Core.Tests/Workspace/WorkspaceLayoutTests.cs
  • src/MuClient.Core/Configuration/WorldDefinition.cs

Comment thread CLAUDE.md Outdated
Comment thread docs/design/README.md Outdated
Comment thread docs/design/README.md Outdated
@@ -0,0 +1,91 @@
using MuClient.Core.Workspaces;

namespace MuClient.Core.Commands;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use file-scoped namespaces consistently.

  • src/MuClient.Core/Commands/CommandCatalog.cs#L3-L3: change to namespace MuClient.Core.Commands; and remove the namespace-closing brace.
  • src/MuClient.Core/Text/StatusFormatter.cs#L1-L1: change to namespace MuClient.Core.Text; and remove the namespace-closing brace.
  • src/MuClient.Tui/TabTitles.cs#L3-L3: change to namespace MuClient.Tui; and remove the namespace-closing brace.
  • tests/MuClient.Tui.Tests/TimersScreenRendererTests.cs#L5-L5: change to namespace MuClient.Tui.Tests; and remove the namespace-closing brace.
  • tests/MuClient.Tui.Tests/TriggersScreenRendererTests.cs#L6-L6: change to namespace MuClient.Tui.Tests; and remove the namespace-closing brace.
  • tests/MuClient.Tui.Tests/WorldsScreenRendererTests.cs#L6-L6: change to namespace MuClient.Tui.Tests; and remove the namespace-closing brace.
  • tests/MuClient.Tui.Tests/AliasesScreenRendererTests.cs#L5-L5: change to namespace MuClient.Tui.Tests; and remove the namespace-closing brace.
  • tests/MuClient.Tui.Tests/CaptureLineRendererTests.cs#L3-L3: change to namespace MuClient.Tui.Tests; and remove the namespace-closing brace.
  • tests/MuClient.Tui.Tests/CommandSurfaceRendererTests.cs#L4-L4: change to namespace MuClient.Tui.Tests; and remove the namespace-closing brace.
  • tests/MuClient.Tui.Tests/KeypadScreenRendererTests.cs#L4-L4: change to namespace MuClient.Tui.Tests; and remove the namespace-closing brace.
  • tests/MuClient.Tui.Tests/RailRendererTests.cs#L5-L5: change to namespace MuClient.Tui.Tests; and remove the namespace-closing brace.

As per coding guidelines, **/*.cs must use file-scoped namespaces.

📍 Affects 11 files
  • src/MuClient.Core/Commands/CommandCatalog.cs#L3-L3 (this comment)
  • src/MuClient.Core/Text/StatusFormatter.cs#L1-L1
  • src/MuClient.Tui/TabTitles.cs#L3-L3
  • tests/MuClient.Tui.Tests/TimersScreenRendererTests.cs#L5-L5
  • tests/MuClient.Tui.Tests/TriggersScreenRendererTests.cs#L6-L6
  • tests/MuClient.Tui.Tests/WorldsScreenRendererTests.cs#L6-L6
  • tests/MuClient.Tui.Tests/AliasesScreenRendererTests.cs#L5-L5
  • tests/MuClient.Tui.Tests/CaptureLineRendererTests.cs#L3-L3
  • tests/MuClient.Tui.Tests/CommandSurfaceRendererTests.cs#L4-L4
  • tests/MuClient.Tui.Tests/KeypadScreenRendererTests.cs#L4-L4
  • tests/MuClient.Tui.Tests/RailRendererTests.cs#L5-L5
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MuClient.Core/Commands/CommandCatalog.cs` at line 3, Convert the
namespace declarations to file-scoped form and remove each corresponding closing
brace: update src/MuClient.Core/Commands/CommandCatalog.cs (3-3),
src/MuClient.Core/Text/StatusFormatter.cs (1-1), and
src/MuClient.Tui/TabTitles.cs (3-3), plus
tests/MuClient.Tui.Tests/TimersScreenRendererTests.cs (5-5),
TriggersScreenRendererTests.cs (6-6), WorldsScreenRendererTests.cs (6-6),
AliasesScreenRendererTests.cs (5-5), CaptureLineRendererTests.cs (3-3),
CommandSurfaceRendererTests.cs (4-4), KeypadScreenRendererTests.cs (4-4), and
RailRendererTests.cs (5-5). Preserve all existing members while ensuring every
affected C# file uses a file-scoped namespace.

Source: Coding guidelines

Comment thread src/MuClient.Core/Commands/CommandMatcher.cs Outdated
Comment thread tests/MuClient.Tui.Tests/CommandSurfaceRendererTests.cs Outdated
Comment thread tools/ansi_frame_to_image.py Outdated
Comment on lines +106 to +124
def apply_sgr(seq, fg, bg, bold):
codes = [int(x) if x else 0 for x in seq.split(";")] or [0]
k = 0
while k < len(codes):
c = codes[k]
if c == 0:
fg, bg, bold = DEFAULT_FG, DEFAULT_BG, False
elif c == 1:
bold = True
elif c == 22:
bold = False
elif c == 38 and k + 4 < len(codes) and codes[k + 1] == 2:
fg = (codes[k + 2], codes[k + 3], codes[k + 4])
k += 4
elif c == 48 and k + 4 < len(codes) and codes[k + 1] == 2:
bg = (codes[k + 2], codes[k + 3], codes[k + 4])
k += 4
k += 1
return fg, bg, bold

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

apply_sgr only understands truecolor; 256-colour and default-colour codes are silently mis-parsed.

38;5;<n> / 48;5;<n> fall through the truecolor branch, so 5 and <n> get re-interpreted as standalone SGR codes — ESC[38;5;0m would hit the c == 0 reset branch and wipe all attributes. 39/49 (default fg/bg) and the 30-37/40-47/90-97 basic palette are also ignored, leaving stale colours. Fine if the headless driver only ever emits truecolor, but a small guard makes the tool robust against a driver change.

🛡️ Proposed hardening
         elif c == 38 and k + 4 < len(codes) and codes[k + 1] == 2:
             fg = (codes[k + 2], codes[k + 3], codes[k + 4])
             k += 4
+        elif c == 38 and k + 2 < len(codes) and codes[k + 1] == 5:
+            k += 2  # 256-colour: skip, unsupported
+        elif c == 39:
+            fg = DEFAULT_FG
         elif c == 48 and k + 4 < len(codes) and codes[k + 1] == 2:
             bg = (codes[k + 2], codes[k + 3], codes[k + 4])
             k += 4
+        elif c == 48 and k + 2 < len(codes) and codes[k + 1] == 5:
+            k += 2
+        elif c == 49:
+            bg = DEFAULT_BG
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def apply_sgr(seq, fg, bg, bold):
codes = [int(x) if x else 0 for x in seq.split(";")] or [0]
k = 0
while k < len(codes):
c = codes[k]
if c == 0:
fg, bg, bold = DEFAULT_FG, DEFAULT_BG, False
elif c == 1:
bold = True
elif c == 22:
bold = False
elif c == 38 and k + 4 < len(codes) and codes[k + 1] == 2:
fg = (codes[k + 2], codes[k + 3], codes[k + 4])
k += 4
elif c == 48 and k + 4 < len(codes) and codes[k + 1] == 2:
bg = (codes[k + 2], codes[k + 3], codes[k + 4])
k += 4
k += 1
return fg, bg, bold
def apply_sgr(seq, fg, bg, bold):
codes = [int(x) if x else 0 for x in seq.split(";")] or [0]
k = 0
while k < len(codes):
c = codes[k]
if c == 0:
fg, bg, bold = DEFAULT_FG, DEFAULT_BG, False
elif c == 1:
bold = True
elif c == 22:
bold = False
elif c == 38 and k + 4 < len(codes) and codes[k + 1] == 2:
fg = (codes[k + 2], codes[k + 3], codes[k + 4])
k += 4
elif c == 38 and k + 2 < len(codes) and codes[k + 1] == 5:
k += 2 # 256-colour: skip, unsupported
elif c == 39:
fg = DEFAULT_FG
elif c == 48 and k + 4 < len(codes) and codes[k + 1] == 2:
bg = (codes[k + 2], codes[k + 3], codes[k + 4])
k += 4
elif c == 48 and k + 2 < len(codes) and codes[k + 1] == 5:
k += 2
elif c == 49:
bg = DEFAULT_BG
k += 1
return fg, bg, bold
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/ansi_frame_to_image.py` around lines 106 - 124, Extend apply_sgr to
consume 256-colour sequences 38;5;<n> and 48;5;<n> as complete parameters,
preventing their components from being processed as standalone SGR codes. Handle
39 and 49 by restoring DEFAULT_FG and DEFAULT_BG, and map basic
foreground/background palette codes, including bright variants, to their
corresponding colours while preserving existing truecolor and attribute
behavior.

Comment thread tools/ansi_frame_to_image.py
Comment thread tools/fonts/README.md Outdated
claude added 4 commits July 28, 2026 16:47
- Cap the UI-side per-window line buffer at ScrollbackLines (was unbounded),
  shifting freeze points down when trimming.
- CloseActiveWindow now clears _lines/_freezePoints so a reopened same-id spawn
  doesn't resurrect stale scrollback or a stale freeze split.
- Percent-encode [ and ] in hyperlink targets before embedding in [link=…] so
  remote MXP/Pueblo/HTML can't close the tag early and inject markup.
- Move mode: an unmapped a–j letter no longer clears the chosen target pane.
- WorkspaceState.Restore rejects unknown layout-node types instead of silently
  degrading them to panes.
- Drop the unreachable empty-tag branch in MarkupFormatter.StyleTag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Renderers: escape checkbox brackets ([[x]]/[[ ]]) in the trigger/timer/alias/
options screens so they render literally instead of being parsed as markup;
pad the timer left column to a fixed visible width; surrogate-safe rail
initials (EnumerateRunes); pad Worlds character rows on visible width.
Core: CommandMatcher.FuzzySpread now returns the tightest subsequence span;
Terminal.Gui→SharpConsoleUI doc comments (Workspace/LayoutSolver/PaneCommands/
WorkspaceLayout). Demo 'wtf' alias captures its argument.
Tests: escaped-markup expectations, order-sensitive command-order assertion,
TUnit Length().IsEqualTo.
Tooling: ansi_frame_to_image.py hardens apply_sgr (256-colour/default), parses
--html as a flag not a path, fixes the docstring name; bundle OFL.txt license
pointer. Docs: refresh stale Terminal.Gui / px-vs-cols references.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
The input row now reads as one solid full-width band. PromptControl fills
its field background to the right edge on its own, but it measures to content
width, so the band stopped mid-row; pin the control's Width to the window
(re-synced on resize and before snapshot capture) so the field spans the row,
and paint the prompt label with the same background (PromptMarkup) so the left
edge joins the band with no gap.

While wiring this, fix a regression: _inputRow was declared and passed to
AddControl but never built (null), so the input PromptControl was never added
to the window. Add _input directly and drop the dead field.

Bundle the full SIL OFL 1.1 text (OFL.txt) and the Nerd Fonts license
(LICENSE-NerdFonts.txt) alongside the screenshot font, as OFL 1.1 requires the
license text to travel with a redistributed subset; a link does not satisfy it.
Reference both from the fonts README. Ignore Python __pycache__.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Output panes stopped short of the console's right edge: the tab-header rule and
content ended mid-row with empty space beyond. Root cause (confirmed against
SharpConsoleUI source + docs/patterns.md): the control default HorizontalAlignment
is Left, so the workspace HorizontalGrid, the pane TabControls, the split grids,
and the spawn/frozen content grids all self-sized to content inside their slot
instead of stretching to fill it. Set WithAlignment(HorizontalAlignment.Stretch)
on each — matching the documented sidebar+content and Tabs (§12) patterns — so the
Flex/Star pane column now claims the full width and the pane fills to the edge.

Also split the bottom status bar like the header: the connection identity
(● name · state) stays pinned left while the keepalive sparkline, host/encoding,
char count, and palette hint form a cluster right-aligned to the far edge.

Verified via headless snapshots (default/split/freeze/spawn/history all fill to
col 120) and 310 Core + 90 Tui tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
CLAUDE.md (2)

84-85: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the remaining M1 instructions to match the new stack.

This table now correctly says TUnit and SharpConsoleUI, but the M1 checklist below still refers to xUnit test projects, Terminal.Gui, and only four projects. Those instructions are now contradictory and can mislead future contributors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 84 - 85, Update the M1 checklist in CLAUDE.md to
consistently reference TUnit test projects, SharpConsoleUI instead of
Terminal.Gui, and all five projects including Tui. Remove or revise the outdated
xUnit, Terminal.Gui, and four-project references while preserving the
checklist’s intended workflow.

99-101: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the obsolete TelnetNegotiationCore 1.0.0 dependency note.

CLAUDE.md still says TelnetNegotiationCore 1.0.0 only provides basic negotiation and ignores MCCP, MSDP, MXP, Pueblo, and ANSI, but the repo uses 2.5.3 and handles those capabilities. Update this note to the current package behavior, or remove it to avoid later confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 99 - 101, Remove the obsolete TelnetNegotiationCore
1.0.0 dependency note from CLAUDE.md, or update it to accurately describe the
repository’s current 2.5.3 usage and supported MCCP, MSDP, MXP, Pueblo, and ANSI
capabilities.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/fonts/LICENSE-NerdFonts.txt`:
- Line 5: Correct the spelling in the Nerd Fonts license notice by changing
“explict” to “explicit,” leaving the rest of the notice unchanged.

---

Outside diff comments:
In `@CLAUDE.md`:
- Around line 84-85: Update the M1 checklist in CLAUDE.md to consistently
reference TUnit test projects, SharpConsoleUI instead of Terminal.Gui, and all
five projects including Tui. Remove or revise the outdated xUnit, Terminal.Gui,
and four-project references while preserving the checklist’s intended workflow.
- Around line 99-101: Remove the obsolete TelnetNegotiationCore 1.0.0 dependency
note from CLAUDE.md, or update it to accurately describe the repository’s
current 2.5.3 usage and supported MCCP, MSDP, MXP, Pueblo, and ANSI
capabilities.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 87b88ac0-e140-47a0-98c6-f3b517ba4a8b

📥 Commits

Reviewing files that changed from the base of the PR and between 2ab9f7e and 08fb6db.

📒 Files selected for processing (28)
  • .gitignore
  • CLAUDE.md
  • docs/design/README.md
  • src/MuClient.Core/Commands/CommandMatcher.cs
  • src/MuClient.Core/Workspace/LayoutSolver.cs
  • src/MuClient.Core/Workspace/PaneCommands.cs
  • src/MuClient.Core/Workspace/Workspace.cs
  • src/MuClient.Core/Workspace/WorkspaceLayout.cs
  • src/MuClient.Core/Workspace/WorkspaceState.cs
  • src/MuClient.Tui/AliasesScreenRenderer.cs
  • src/MuClient.Tui/DemoScene.cs
  • src/MuClient.Tui/MarkupFormatter.cs
  • src/MuClient.Tui/MuGlyphApp.cs
  • src/MuClient.Tui/OptionsScreenRenderer.cs
  • src/MuClient.Tui/RailRenderer.cs
  • src/MuClient.Tui/TimersScreenRenderer.cs
  • src/MuClient.Tui/TriggersScreenRenderer.cs
  • src/MuClient.Tui/WorldsScreenRenderer.cs
  • tests/MuClient.Core.Tests/Text/MetersAndStatusTests.cs
  • tests/MuClient.Tui.Tests/AliasesScreenRendererTests.cs
  • tests/MuClient.Tui.Tests/CommandSurfaceRendererTests.cs
  • tests/MuClient.Tui.Tests/OptionsScreenRendererTests.cs
  • tests/MuClient.Tui.Tests/TimersScreenRendererTests.cs
  • tests/MuClient.Tui.Tests/TriggersScreenRendererTests.cs
  • tools/ansi_frame_to_image.py
  • tools/fonts/LICENSE-NerdFonts.txt
  • tools/fonts/OFL.txt
  • tools/fonts/README.md
🚧 Files skipped from review as they are similar to previous changes (23)
  • .gitignore
  • tools/fonts/README.md
  • src/MuClient.Tui/TimersScreenRenderer.cs
  • src/MuClient.Core/Workspace/PaneCommands.cs
  • tests/MuClient.Tui.Tests/CommandSurfaceRendererTests.cs
  • src/MuClient.Tui/DemoScene.cs
  • src/MuClient.Tui/TriggersScreenRenderer.cs
  • src/MuClient.Tui/WorldsScreenRenderer.cs
  • src/MuClient.Tui/MarkupFormatter.cs
  • tests/MuClient.Core.Tests/Text/MetersAndStatusTests.cs
  • src/MuClient.Tui/RailRenderer.cs
  • tests/MuClient.Tui.Tests/TimersScreenRendererTests.cs
  • src/MuClient.Tui/AliasesScreenRenderer.cs
  • tests/MuClient.Tui.Tests/TriggersScreenRendererTests.cs
  • docs/design/README.md
  • src/MuClient.Core/Workspace/Workspace.cs
  • tests/MuClient.Tui.Tests/AliasesScreenRendererTests.cs
  • tests/MuClient.Tui.Tests/OptionsScreenRendererTests.cs
  • src/MuClient.Core/Workspace/LayoutSolver.cs
  • src/MuClient.Core/Workspace/WorkspaceLayout.cs
  • src/MuClient.Tui/MuGlyphApp.cs
  • src/MuClient.Core/Commands/CommandMatcher.cs
  • src/MuClient.Core/Workspace/WorkspaceState.cs


There are various sources used under various licenses:

* Nerd Fonts source fonts, patched fonts, and folders with explict OFL SIL files are licensed under SIL OPEN FONT LICENSE Version 1.1 (see below).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the spelling in the license notice.

Change explict to explicit.

Proposed fix
-* Nerd Fonts source fonts, patched fonts, and folders with explict OFL SIL files are licensed under SIL OPEN FONT LICENSE Version 1.1 (see below).
+* Nerd Fonts source fonts, patched fonts, and folders with explicit OFL SIL files are licensed under SIL OPEN FONT LICENSE Version 1.1 (see below).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* Nerd Fonts source fonts, patched fonts, and folders with explict OFL SIL files are licensed under SIL OPEN FONT LICENSE Version 1.1 (see below).
* Nerd Fonts source fonts, patched fonts, and folders with explicit OFL SIL files are licensed under SIL OPEN FONT LICENSE Version 1.1 (see below).
🧰 Tools
🪛 LanguageTool

[grammar] ~5-~5: Ensure spelling is correct
Context: ... fonts, patched fonts, and folders with explict OFL SIL files are licensed under SIL OP...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/fonts/LICENSE-NerdFonts.txt` at line 5, Correct the spelling in the
Nerd Fonts license notice by changing “explict” to “explicit,” leaving the rest
of the notice unchanged.

Source: Linters/SAST tools

claude and others added 10 commits July 28, 2026 17:36
Open the command surface (☰/⌃P) in the headless snapshot so the menu can be
captured for docs — 'menu' over the single-pane workspace, 'menu-split' over a
two-pane (main | Chat) split. The palette opens a normal modal window, which
ForceRender paints over the resumed demo scene, same as the settings screens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
The M1 first-task checklist still named four projects, xUnit, and a Terminal.Gui
window, and the dependency note still described TelnetNegotiationCore 1.0.0 —
both contradicting the doc's own Locked Decisions and Repository State. Mark M1
as delivered and correct it to TUnit + SharpConsoleUI; update the TNC note to
2.5.3 (which negotiates MCCP/MSDP/MXP itself, while Pueblo and payload parsing
stay our layer).

The 'explict' typo CodeRabbit flagged in tools/fonts/LICENSE-NerdFonts.txt is
left as-is: it's a verbatim copy of the upstream Nerd Fonts license, and bundled
third-party license text should match the canonical source, typos included.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
The command surface (☰/⌃P) rendered with full window chrome — minimize/maximize/
close buttons and a corner resize grip — and a fixed 66×18 size that was too short
for the catalog, so it showed a scrollbar. Use the framework's own affordances to
make it a clean overlay: HideTitleButtons() drops the [_][+][X] controls and
Resizable(false) drops the resize grip (Esc still closes). Size the window to fit
the full unfiltered catalog (search row + all command lines + border), capped to
the usable desktop, so nothing scrolls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
The surface was a fixed 66 wide while its rows used ~45, leaving a dead gutter on
the right (where the scrollbar used to sit) and a selection highlight that only
wrapped the title. Size the window width to the widest rendered row (capped to the
desktop, same as the height fit) so the surface hugs its content, and pad the
selected row's accent highlight to the full inner width so it reads as one
continuous selection bar across title + subtitle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Rework F5 into a full-console layout: a header band (title + keyboard hints), a
two-column WORLDS-list / world-detail body, a full-width CHARACTER editing pane on
its own elevated background, and a footer action bar (Cancel/Save) pinned to the
bottom row. Add colour throughout — teal section accents, per-world accent spines,
light values against dim labels, an accent Save button. The renderer now takes
width/height (0 = natural mode, still used by the unit tests) and, when given the
console size, fills it: full-width bands, the edit-pane background, and the footer
at the last row.

Make the settings overlay host a clean full-screen surface: frameless (no title
bar, buttons, or resize grip), a deep panel background, and the panel stretched so
that background fills the console. All F2–F9 screens ride on this cleaner host; F5
passes the desktop dimensions so it lays out edge-to-edge.

90 Tui tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Rebuild the F5 editing pane on the framework's grid layout instead of hand-merged
markup columns. WorldsScreenRenderer now exposes each region as a pure markup block
(header, worlds list, detail, character form, trigger checklist, footer); a new
WorldsScreenView composes them into controls: a root grid (header band / body /
edit pane / footer) with the editing pane a HorizontalGrid whose left panel holds
the character form (left-aligned) and whose right panel holds the assigned trigger
sets — pushed right by a flex spacer so the block sits on the right while its [x]
checkboxes stay left-aligned. The edit grid's own background gives the full-width
elevated band. Footer Cancel/Save stays pinned to the last row; header/footer/edit
bands all span the full console.

The settings overlay now hosts an arbitrary control (frameless full-screen modal,
deep panel background); markup screens (F2–F4/F6–F9) are wrapped in a stretched
panel as before, while F5 supplies the composed control tree. Render() still merges
the same blocks for the unit tests. 90 Tui tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
The footer band (world/character position + Cancel/Save) sat directly against the
editing-pane band, so "character 1/2" read as if it belonged to the character
setup section. Insert a one-row panel-background gap between the editing pane and
the footer so the footer reads as its own bar. Three distinct bands now: edit pane,
gap, footer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
Mechanical rename of all five source projects and their five test
projects, plus the solution file. No behavioural change: 514 tests pass
before and after.

The repo previously carried three names for one product -- MuGlyph
(repo/product), MuClient.* (assemblies) and muglyph (binary). This
collapses the assembly axis onto the new product name; the brand strings
follow in the next commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covers every human-visible surface: product name, README (now positioning
the client against SharpMUSH and SharpClient), CLAUDE.md, docs, the
release workflow's artifact names, and the design reference.

Behavioural renames worth calling out:
- binary: muglyph -> sharpmuterm
- config dir: ~/.config/MuGlyph -> ~/.config/SharpMUTerm
- env vars: MUGLYPH_GRAPHICS/MUGLYPH_SIXEL -> SHARPMUTERM_*
- User-Agent: MuGlyph/0.1 -> SharpMUTerm/0.1, pointing at the new repo
- bundled subset font: MuGlyphMonoNerd.woff -> SharpMUTermMonoNerd.woff
- header ribbon brand chip: "glyph" -> "muterm"

No published releases exist, so none of these need compatibility shims.

Also corrected three stale docs found in passing: the test count (391 ->
514), a claim that CI runs `dotnet test` (it runs `dotnet run` per test
project, since TUnit on MTP dropped VSTest support in .NET 10), and
CLAUDE.md contradicting itself on the same point.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Records the criterion the name had to meet ("pay its own rent" -- the
name alone says what the thing is, no tagline), the alternatives weighed
and why each lost, the resulting surface, and the follow-ups this rename
deliberately does not cover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude and others added 3 commits July 28, 2026 19:39
A detailed handoff doc for the next session — outstanding backlog (panel
treatment for the other config screens, graphics/Kitty wiring, live config
editing, real-terminal verification) and the hard-won gotchas (TUnit test
invocation, the snapshot→SVG pipeline and its Chromium clipping trap,
SharpConsoleUI alignment/fill behavior, TelnetNegotiationCore 2.5.3, the
UI-agnostic-Core rule, and the intentional review skips).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015w2WJ5M22CZmg9md67zpeB
@HarryCordewener
HarryCordewener merged commit fa8479c into main Jul 28, 2026
3 checks passed
@HarryCordewener
HarryCordewener deleted the claude/muglyph-implementation-5f51l3 branch July 28, 2026 19:43
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.

2 participants