Skip to content

UX: fix-now batch, layout model, Client registry, trigger builder, Light settings - #96

Draft
HarryCordewener wants to merge 79 commits into
mainfrom
claude/project-thread-izbu0c
Draft

HarryCordewener wants to merge 79 commits into
mainfrom
claude/project-thread-izbu0c

Conversation

@HarryCordewener

@HarryCordewener HarryCordewener commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Requested by Grave · project thread

The fix-now batch from the UX audit, plus the first wave of the UX plan.

What changed

Fix-now batch

  • Status row at 80 columns no longer wraps onto a second row. Wrapping took a row off every pane and re-sent NAWS. The encoding cell is dropped first, then the ⌃P hint.
  • Meters no longer vanish below 120 columns. When 82/100 does not fit they show 82% instead.
  • F2 and F4 at 80 columns use a single-column layout (ScreenChrome.TooNarrowForTwo and Single) instead of drawing their editors off the right edge.
  • Header and action bars shorten their text instead of cutting off their keys. Headers (SpreadTitle) shorten from the middle, so the close key stays.
  • /connect <host> [port] [tls] saves a world and dials it. /connect <name> opens a world that is already configured.
  • Window fonts: JetBrains Mono and Symbols Nerd Font Mono are embedded in sharpmuterm-window, with their licences copied to licenses/.

Wave 1

  • Layout model (Core/Layouts):
    • Built-in Roleplay and Combat layouts, plus layouts the player saves (config.json layouts).
    • WorldDefinition.Layout and CharacterDefinition.Layout.
    • LayoutResolver tries the character's layout, then the world's, then the accepted suggestion, then Roleplay.
    • LayoutSuggester suggests Combat when vitals arrive, and Roleplay when MSSP reports a MUSH-family codebase.
    • PanelAvailability says which panels have data yet. LayoutFold turns a layout into the existing PaneNode/SplitNode tree.
    • No schema bump. Nothing in the hosts uses it yet.
  • SharpMUTerm.Client, a new project with no UI dependencies:
    • ContributionRegistry with six kinds: commands, panels, settings sections, status cells, overlays and output decorators.
    • ⌃P, the pointer menu and DispatchCommand now read their commands from the registry. The catalog was checked against the old one across 12,288 states before the old one was deleted.
    • New suite SharpMUTerm.Client.Tests, also added to ci.yml.
  • Trigger builder:
    • A ⌃P and right-click entry, Make a trigger from the selection, builds a rule from the selected text. Numbers, quoted speech and player names that GMCP reported become wildcards.
    • New Trigger.MatchType: contains, starts with, * wildcards, or regex. Regex is the default, so old configs load unchanged.
    • While a pattern is being edited, F2 shows how many recent lines it matches, with three samples. Bounded at 2,000 lines, with a 100 ms timeout.
    • New snapshot view trigger-builder.
  • Accessibility:
    • The ten pane tints were re-picked using the Machado 2009 colour-blindness simulation. The worst pair went from ΔE 0.28 to 2.22 (deuteranopia, Dark theme).
    • The tint name now appears on the rail row and tab tooltips.
    • Settings screens follow the Light and Solarized Dark themes (D6). ScreenTheme is passed through every view; there is no global state. Dark is byte-identical.
    • 13 settings views were added to FrameContrastTests.

Testing

  • dotnet build -c Release SharpMUTerm.slnx: 0 errors.
    • It warns NU1903 for SixLabors.ImageSharp 3.1.12 (GHSA-v76p-62qx-wwq2). This is a new advisory, not caused by this PR.
  • Suites:
    • Core 1420/1421. AReadOnlyCacheDirectory_DegradesToMemoryOnly fails only because this sandbox runs as root.
    • Graphics 99/99, Scripting 68/68, Web 37/37, Client 18/18, Tui 2175/2175, Avalonia 222/222.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx

The status row wrapped to a second row at 80x24, the meters vanished below
120 columns, and F2/F4 drew their editors off the right-hand edge at 80.
The status row now drops its encoding and palette hints before wrapping,
meters fall back to a compact percentage form, two-column screens too
narrow for both columns show the one the keyboard is in, and header/footer
bars elide text rather than clipping their keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
--plain had no way to add a world on a first run: the settings screens are
not usable there. /connect <host> [port] [tls] saves a new world and dials
it; /connect <name> opens a configured one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
A machine with no Nerd Font installed drew every chrome icon in the window
as a box, and text fell back to whatever monospace face the system had.
The window now carries JetBrains Mono (OFL 1.1) and Symbols Nerd Font Mono
(MIT) as embedded resources, used after any preferred installed face, and
ships their licences beside the executable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
A header too narrow for its hints now shortens them from the middle, so the
key that closes the screen stays on it.

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

coderabbitai Bot commented Nov 4, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

… availability

Core only, no UI wiring. A layout (Layouts/LayoutDefinition) places panels by id
in regions with an order, a relative size, a collapsed flag and an optional
tab host. Roleplay and Combat are built in and read-only; LayoutLibrary saves,
updates, renames and deletes the player's layouts in AppConfiguration.Layouts,
turning an update of a built-in into a saved copy. LayoutResolver picks the
character's layout, then the world's, then the world's accepted suggestion,
then Roleplay, passing over ids that name nothing. LayoutSuggester suggests
Combat on vitals and Roleplay otherwise, saying whether the MSSP codebase was a
MUSH-family one. PanelAvailability says which panels have data for a session,
and LayoutFold folds a layout into the workspace's own split tree.

New config fields are optional with old-behaviour defaults, so no migration
and no schema version change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…mands from it

A new UI-agnostic project, SharpMUTerm.Client (Core only), holds the shared client shell's first
piece: ContributionRegistry, with six kinds of contribution (commands, panels, settings sections,
status cells, overlays, output decorators), each with a stable id unique within its kind and an owner.

The built-in ⌃P catalog moves out of Core's CommandCatalog.Build into BuiltInCommands, registered as
contributions; the Tui supplies the work (RunBuiltInCommand). The command surface, the pointer menu
and DispatchCommand all read the registry, so a package or host can add commands. The catalog is
unchanged entry for entry. New suite: SharpMUTerm.Client.Tests (in CI).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…match kinds, and a live test on F2

Ctrl+P > "Make a trigger from the selection" (and the output right-click menu) makes a rule from the
selected line, with numbers, quoted speech and known GMCP player names turned into * wildcards, and
opens F2 on it with the pattern being typed and the regex shown behind it. Triggers gain a match type
(contains the text, starts with, * wildcards, regular expression; regex stays the default), and F2's
pattern editor shows how many of the last 2,000 held lines match, with the newest three highlighted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…ind readers

Settings screens (D6): ScreenPalette's constants become roles spelt as the
Dark theme's values. A new immutable ScreenTheme, built once from the app's
theme, resolves renderer markup and control colours at the view boundary
(ScreenChrome.Panel/Band/VerticalRule, ScreenTheme.Colour), so renderers and
their tests are untouched and there is no mutable global. Dark resolves to
the constants byte for byte; Light mirrors L*, Solarized takes its
background's hue, and every ink is held to Contrast.Floor on its planes.
Threaded through every *ScreenView.Build (required), SettingsOverlay,
ComposeOverlay, ListOverlay and EditReviewOverlay. FrameContrastTests gains
13 settings views on all three themes; ScreenThemeTests checks that no Light
settings frame paints a dark-palette plane.

Tints: ColourVision (Core) simulates protanopia/deuteranopia/tritanopia
(Machado 2009) and measures CIEDE2000. The ten anchors were re-picked within
40 degrees of each name: the worst dichromat pair goes from 0.28 to 1.99, and
the worst pair for typical vision from 3.71 to 3.96. TintColourVisionTests
holds 1.9 / 3.5 on all themes. Names kept. A tinted character's rail and tab
tooltips name the colour, at no cell cost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
@HarryCordewener HarryCordewener changed the title UX fixes: 80-column layout, /connect, bundled window fonts UX: fix-now batch, layout model, Client registry, trigger builder, Light settings Nov 4, 2026
claude added 17 commits November 4, 2026 03:38
…rnings

The palette's ranking and grouping move into SharpMUTerm.Client
(CommandPaletteModel, FuzzyMatch, RecentCommands, KeyGlyphs) so the window's
palette can list the same rows; Core's CommandMatcher is removed. Matching is
word-by-word subsequence with word-start/consecutive/prefix scoring and
highlighted matches. An empty query lists RECENT (in memory, eight) and
SUGGESTED (from CommandContext) before the groups. Keys sit in a right-aligned
column in one glyph vocabulary and are never elided.

F4 marks a binding that wins over a client key (MacroKeys.YieldedBy) with an
amber triangle and a shadowed duplicate as dead; ⌃P lists "Key clashes (N)"
while N > 0. New snapshot views menu-fuzzy and menu-recent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…uggestion

A character's resolved layout is realised as one pane per panel that has data
(LayoutFold -> LayoutRealiser -> WorkspaceLayout.ReplaceTree); output, chat and
map are the existing windows, every other panel a panel:<id>:<key> window whose
control comes from a factory registered in TerminalPanels. Re-folds are rate
limited and rebuild only when the drawn panels change, and stop once the player
rearranges the panes. A restored workspace wins at startup; only the live entry
point applies layouts on its own. Alt+M and Ctrl+P > Layouts open the list
(apply, save changes, save as new, use for world/character); F5 gains a layout
row on worlds and characters; a world's first connect accepts a suggestion and
says so once. LayoutCapture reads an arrangement back into placements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…, meter table

F7 and F8 are drawn from settings sections declared in SharpMUTerm.Client
(BuiltInSettings): each field has a group heading and a SettingBinding over
the configuration, whose default is read off a fresh AppConfiguration. F7 is
now "Text & display", grouped under Colour & legibility, Text, Links &
pictures, Status row & meters, Activity and Sound, and windowed to the
screen height so nothing runs off a 24-row terminal.

- `/` on any settings screen opens a search over every declared field and
  every screen title, with a per-section match count; Enter goes to the
  field on its own screen, Esc puts the screen back.
- A field away from its default is marked "● changed"; `r` resets it
  through the same save funnel. The header names `r reset` only there.
- F7's meters are a table: label, value path, maximum path and a live
  `now` column from the focused session's game data, [+ meter], Delete to
  remove, a box of received paths while the cursor is in the table, and
  the path fields offer those paths with their values.

Snapshot views settings-search, settings-meters, settings-meters-pick and
settings-changed, in the parity list and FrameContrastTests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…hread-izbu0c

# Conflicts:
#	CHANGELOG.md
#	src/SharpMUTerm.Tui/SharpMUTermApp.cs
Four panels plug into the layouts seam (TerminalPanels.Register from
RegisterTerminalPanels): who is here from GMCP Room.Players and the room's
item list (a click puts look <name> on the command line), a one-row vitals
strip of reserved fields with bars, values, a low mark and the combat
target, a quick bar of function-key macros run through the key's own path,
and the F1 editor docked as a pane sharing F1's draft. While the composer's
pane is in front it is armed: the focus pin targets its editor, Esc hands the
keyboard back to the command line, and arriving at the pane arms it again.
Each is on Ctrl+P and has a snapshot view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
A headless app runs OnUiThread inline, so the picture host's deliveries ran
on the thread pool while the test or snapshot appended the next line. The
reserved-picture draw and the append that reads ReservedPicture.Drawn raced
over the pane buffer, and a row could be left blank: the intermittent
APictureStopsAtTheFirstLineWithoutItsCells. Headless picture work is now
queued and run at the next frame or while InlineImagesSettled is awaited.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
… cells

The window host draws native views in a layer above the cell view, fed through
IPointerPresenter (the scrollbars' channel): a Layouts button over the header's
three trailing blank cells, whose menu is built by the client from the layout
library's registry commands; native vitals gauges drawn from the VitalsModel the
cell strip now renders from; and a native map canvas (zoom, pan, click to walk
through the client's speedwalk). The cells underneath are unchanged, so no pane
rectangle moves and host parity is untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
Scrolling a pane back now splits it inside its own rectangle: the history
being read on top, a band ("scrolled back N rows · End returns · ⌥F
freezes"), and the newest lines still arriving below. It reuses freeze's
two-viewport layout with the window's own control and viewport kept on top,
so the scroll keys, wheel, search landing and status row are unchanged; the
tail is a re-fed copy of the buffer's last lines and indexes nothing. A
split pane is not "scrolled back" for unread badging. F7 ▸ SCROLLBACK turns
it off.

F5 ▸ join wraps (per world, off by default) joins rows a server broke at
its own column, at ingest and before the triggers, through a conservative
ServerWrapJoiner: full row, unpunctuated end, lower-case continuation, same
style, no prompts, structure or column layouts. A full row is held until
the next line or 250 ms of quiet.

New snapshot views: scrollback-split, reflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
The vitals strip and the quick bar are exactly one row, with no tab strip over a
one-tab strip, and the docked composer has an editor's floor. A pane's height
claim (PaneNode.FixedRows/MinRows) is derived from what it holds on every
rebuild (PanelSizing) and honoured by the grid and by LayoutSolver
(PaneExtent.DistributeRows); a divider beside a fixed strip neither hovers nor
drags, and the resize keys take rows from a pane beyond it or refuse out loud.

The status row leaves its meters out while the vitals strip is showing them.
Layout panels are WindowKind.Panel: no rail row, no Alt+number, no Go to entry,
so map and capture windows keep their digits. Alt+M renames (r) and deletes
(d, then y) saved layouts. New snapshot view: layout-roleplay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…onnect

A client with no worlds opens on a welcome: connect to a host (a form that
goes through /connect's own path, adding the character it names), browse
games from the package index (still fetched only when asked; says so in
place when no index is set), or open F5, with the /connect line spelt out
and a starting layout to pick (suggested by default, so the client's
suggestion still applies). Esc closes it for the run; /welcome and
Ctrl+P > Welcome reopen it. Under --plain (ClientHost.ScreenIsSeen false)
the choices are announced as commands and /welcome layout picks the layout.

The first connect of a configuration that started empty puts one chrome
row naming Ctrl+P, Alt+M, F1 and F5, owed through
AppConfiguration.KeyTourPending so it is shown once and survives a
restart. Ctrl+P > Key tour hides and shows it.

Snapshot views welcome, welcome-connect and tour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…hread-izbu0c

# Conflicts:
#	CHANGELOG.md
#	src/SharpMUTerm.Tui/ListOverlay.cs
#	tests/SharpMUTerm.Tui.Tests/FrameContrastTests.cs
In a stacked split no row is spent on a divider above a pane that draws its
own tab strip: the strip is the boundary, and its empty run past the last
tab is the divider's handle (hover cue, drag, double-click to even, tooltip).
Fixed strip panels sit flush with no divider on either side and, unfocused,
are recessed a step so they never share an unfocused pane's plane. Side by
side is unchanged. The freed rows go to the panes (a NAWS change).

PaneExtent.DividerRows is the one rule, read by LayoutSolver, PaneExtent,
PaneResize and the terminal's grid. Adds the split-stacked snapshot view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
claude added 30 commits November 4, 2026 08:25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…window

F10 is a declared list section (BuiltInSettings.Packages, PackageCollection):
an enabled switch whose binding replaces DisabledPackages, read-only fields
for name, version, games, author, description, sets, scripts, media, update
and characters, and the Space/c/Del/b actions. The cell modal and the
window's PackagePageView draw the same fields in the same order.

The window gets a package manager page: an Installed tab (switches, details,
copy sets, remove) and a Browse tab that fetches the index only when asked
and lists the focused world's packages first. Install opens a native review
drawn from the same PackageReviewModel as the cell review, leading with what
can act as the player; both end in AcceptPackage, which refuses a stale
review, so nothing is written until the player installs. Every gesture goes
to the client through IPackageHost.

A settings --view opens the native window in the window host; the parity
harness still compares cells. The demo scene serves a fake index from memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…w, expose output to screen readers

- telnet:// and telnets:// links connect through the same reader as /connect (ConnectEndpoint, TelnetLink),
  matched on the endpoint, never sending anything; credentials, paths, queries, control characters and bad
  ports are refused. --open-link drops every other argument. A second launch hands its link to the running
  client over a Unix domain socket (LinkHandoff). The .desktop file declares the schemes and the Inno Setup
  script registers them as an unchecked per-user task.
- Character passwords go to Windows Credential Manager, the Secret Service (secret-tool) or the macOS
  Keychain (CredentialStores, in Hosting/), with secrets.json behind them (CredentialVault). Existing entries
  migrate at load; an unreachable store keeps every reference through a save. Only the live entry points
  make a vault.
- The window and the terminal are titled Character@World — SharpMUTerm from the header's model
  (WindowTitle, ClientHost.Title: Window.Title, or OSC 2 with the title stack pushed and popped).
- The window's output view has an automation peer: a read-only document holding the front window's newest
  lines (IAnnouncer.Reading, TerminalViewAutomationPeer).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
In the desktop window the quit prompt, Ctrl+F, Ctrl+R, the Alt+M layouts
list and the first-run welcome are native modals over the window, drawn
from the Client model the cell modal draws and ending every gesture in the
cell key's method. One host member, ClientHost.NativeModals (INativeModals),
covers all five: the presenter owns stacking, focus return and theme. The
quit prompt is never refused and always ends in front, so closing the
window while F1 is open no longer hides the prompt under the composer.
The terminal is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…619a1f4ede77

# Conflicts:
#	CLAUDE.md
#	src/SharpMUTerm.Avalonia/AvaloniaConsoleDriver.cs
#	src/SharpMUTerm.Avalonia/WindowHost.cs
#	src/SharpMUTerm.Tui/Hosting/ClientHost.cs
The map, vitals, who-here and the quick bar can be popped out of their pane into
a window of their own in the desktop host: from a button on the docked native
view, the pane's right-click menu, or Ctrl+P (panel:popout:<window>), and docked
back from the floating window's Dock button, by closing it, or panel:dock:<window>.

- Floating is host-neutral: WorkspaceWindow.Floating (FloatingBounds, screen
  pixels) is saved with the workspace and PanelPlacement.Floating with a layout
  (LayoutCapture). The fold never reads it, so the terminal keeps the panel in
  its region and the flag survives for the next host that can float.
- Host fact: ClientHost.FloatingPanels (IFloatingPanels?, required; null in the
  terminal and headless). Ctrl+P families, the pane menu entry and the docked
  overlay's PopOut callback exist only where it is set; the surface catalog
  declares both actions OnlyOn: Window with the Ctrl+P id as the cell route.
- The pane a panel leaves goes back through the workspace operations: with the
  live layout owning the panes it is re-applied without the panel, so pane
  rectangles and NAWS equal a layout that never placed it.
- The floating window draws the panel's own native view from the same overlay
  and callbacks (OverlayFor), follows the character's plane and theme ink, never
  takes focus on opening, forwards keys and text to the command line, and docks
  when closed. Remembered places are clamped to the screens present.
- PaneOutputRects no longer throws when called before the main window exists, so
  resuming a workspace with a panel or map pane no longer fails at startup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
- Startup runs once: the window's welcome demo no longer raises its notice twice.
- The window's history-search-filter view opens native Ctrl+R with its query typed.
- search-landed lands through the native search in the window.
- A hovered toast freezes the stack so it keeps its place; toasts stack clear of the map legend.
- F10 fits 80x24: ListOverlay sizes from measured wrapped rows, confirm rows always emitted.
- Every native surface spells keys in words (NativeKeys.Words, Enter for the return glyph),
  with a runtime and source scan (NativeKeySpellingTests).
- The settings window takes the keyboard on open and returns it on close.
- The composer face is saved per character through PersistConfiguration.
- List editors remember their selection per section for the run.
- The command line goes idle while Alt+C sidebar navigation holds the keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
NativeTokens (Avalonia) derives the window's planes, inks, radii and type
ramp from the client's theme the way ScreenTheme does, with every ink held
to 4.5:1. Settings, list editor, package page, theme cards, menus, palette
and modals read it; Fluent's blue is gone. A test forbids literal font
sizes and colours outside the token file.

The settings window gets the palette's modal frame (title bar, close, Esc,
scrim) and docks to the right half when the trigger builder opens it.

Light themes: header fills deepened under light ink (4.5:1), muted header
text and the low-vitals cue held to the floor, in both hosts. The terminal
popups (Ctrl+P, quit, search, history, prefix panel, game data, message
log) follow ScreenTheme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
… welcome

- Away/activity/search/restore bars are sized to the pane (BarRule), elide the
  label first, and redraw in place on resize (SyncChromeBarWidths).
- PaneLine.Plain comes from the line's spans, so anchored patterns match
  highlighted lines in Ctrl+F and F2's live test.
- CharacterNames / DisplayName: "Corvid · Aetherfall" on every surface.
- Ctrl+F footer drop ladder; the surface fits 80 columns.
- Command palette naming, OUTPUT group, strict short-word fuzzy matching.
- F4 and quick bar keys in glyphs; F4 footer ladder.
- Window delivers numpad keys (HostKeys.DeliversNumpad, KeyMapper).
- Collapsed rail world marks and two-letter initials.
- Welcome copy in WelcomeModel, shared by both hosts; editable route combo.
- Reconnect hints name the key; F1 names an offline window's owner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…ngth

- Notices: a notice repeated while still on the row is said once; the
  status row is elided to its width in both hosts so it never wraps.
- Window toasts avoid native quick bars and docked composers and are
  hidden while settings, Ctrl+P, a native modal or the quit prompt is open.
- Join wraps: a pane narrower than the server's learnt wrap column gets a
  once-per-world notice with a "Join wraps" action (world:join-wraps:<w>);
  never switched on by default.
- Tab overflow chip "+N ▾" painted over a strip's clipped last cells,
  listing hidden tabs: native menu in the window, cell list in a
  terminal. Declared as SurfaceCatalog.TabOverflow.
- InputSettings.Rows defaults to 1.
- Composer: "N chars" (as sent) in F1 and the docked panel in both hosts;
  Alt+P toggles an "as sent" preview in F1, cells and native. New
  compose-preview view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
- Settings window: edge resize bands (WindowChrome.WithResizeBands, as the
  borderless main window does) under its own frame; the centred size is kept
  for the run (SettingsPresenter.RememberedSize), docked-right is not.
- Opening or closing the settings window re-syncs the desktop layer, so
  toasts hide and return at once rather than on the next frame.
- One spelling of a name: prompt "Corvid · Aetherfall ›", window title
  "Corvid · Aetherfall — SharpMUTerm", tab owner prefix "Corvid · Chat"
  (CharacterNames.Window; StatusFormatter.NameSeparator).
- Native palette placeholder starts with "Command palette"
  (CommandPalette.Title shared with the header search box).
- A press on a tab strip's +N chip is struck from the driver's frame before
  the framework sees it, so the tab drawn under the chip is not selected or
  closed; pinned in both hosts.
- Notices from packages, map and sound carry a Source label ("Sound:" on the
  row, a caption on a toast), not a word in the key chip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…wave 12)

- F2 and F4 at 80 columns draw their one column through ScreenChrome.Panel, so
  Light resolves it (was Dark's wells and inks, 1.05-1.34:1).
- Status row: notice inks and every bare [dim] held to 4.5:1 on the backdrop;
  Light's backdrop (rail, status row) lifted from #b7b7b9 to #e6e6e9.
- Command palette's selected row is Ink on Accent (both roles), 5.8:1 on Light.
- Window chrome's secondary ink is opaque and held to the 4.5:1 text floor.
- Spell check skips command tokens (+ @ & /) and every known name.
- Demo: F1/docked composer can send where the header says Corvid is
  connected, and no "nothing connects at start" notice over that.
- Who-here drops idle words before cutting names; the status room cell grows
  into spare width; the as-sent preview wraps at spaces; the window's docked
  composer stops saying "click to write" twice.
- FrameContrastTests walks 80x24 too and holds the status row and the
  palette's selected row to 4.5:1; ScreenThemeTests covers 80-column Light
  settings; NativeTokenTests covers the chrome's secondary ink.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
- F5 reconnect (per world): a dropped connection is dialled again after
  5/15/30/60/60 s with a status-row countdown and a notice carrying Stop;
  the player's own disconnect, quit, or a server close within 10 s of a
  command (a logout) never reconnects. Alt+R dials at once; Disconnect and
  Ctrl+P > Stop reconnecting cancel. Core AutoReconnect + WorldSession.LastClose.
- Attention: a trigger's "alert" flag, or F5 alert on = name & pages, raises
  WorldSession.AttentionRequested; the client asks the host (new required
  ClientHost.Attention / IAttention) only while away, with F7 "alert when away"
  on, one per 10 s. Window: taskbar flash / osascript / notify-send. Terminal:
  OSC 99 (kitty, o=unfocused), OSC 777 (WezTerm, Ghostty, foot), else BEL after
  30 s idle. The window title counts lines that arrived while behind.
- F5 command stacking (off by default): TypedCommands splits a typed line on
  the world's separator, with \ escapes and #N repeats (bounded at 100), before
  the alias engine; composer posts, grabs, scripts and macros are never split.
- Tests in Core and Tui; reconnect snapshot view; CLAUDE.md and CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…stion

- /help and Ctrl+P > Help: one Client model (HelpModel) drawn as a cell
  ListOverlay and as a native modal in the window. It lists how-to pages,
  the slash commands from SlashCommands.All (the table --help now prints too),
  every registry key for this host and the host's own chords. It is searchable
  with one View in both hosts. Enter places a slash command unsent, runs a key's
  command, or opens a page. Declared in SurfaceCatalog with parity probes.
- F1's Alt+W with no dictionary, and Ctrl+P > Get a spelling dictionary, offer
  LibreOffice's en_US/en_GB. Nothing is fetched until the player says yes.
  It is https only, capped while streaming and checked against a pinned SHA-256,
  then written into dictionaries/ beside config.json. The dictionaries root is
  null by default, so tests and snapshots fetch nothing. Native and cell offers
  share DictionaryOfferModel.
- While a pane is narrower than a world's wrap column, Ctrl+P suggests
  "Join server wraps for <world>" and F5 marks join wraps as recommended.
  It is still off by default.
- New views: help, help-topic, dictionary-offer, join-wraps, join-wraps-f5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…t and TinTin++

Core/Import reads a MUSHclient .mcl, a Mudlet XML package or profile save, a
Mudlet profile folder or .mpackage (read in memory, never extracted) and a
TinTin++ script, bounded and tolerant: a rule that cannot be carried over
exactly is skipped and listed with its reason, and script code is listed as
left out and never run. ImportPlan decides what is new without touching the
configuration; the rules go into a new "Imported from <client>" set and
nothing existing is overwritten. Passwords are stored only when the player
asks.

The review (ImportReviewModel) leads with what can act as the player and is
drawn by a cell overlay and a native card in the window; nothing is written
before Enter. Entry points: /import <path>, Ctrl+P > Import from another
client..., and the welcome. The window opens a native file picker
(ClientHost.FilePicker, required, null elsewhere). Declared in SurfaceCatalog
with parity probes; import-review snapshot view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
…oth hosts

The cell map's title is the current room's name, and MapViewModel.Nearby (each
exit and the room it leads to) fills a side column beside a wide grid or the rows
under a tall one, so both hosts name the same rooms.

Map editing, a Mudlet subset: rename, area, lock, weight, add/remove exit (to a
room id or the next room walked into), nudge, delete, note and symbol, with undo
of the last ten edits. Every edit is a Client MapEdit through ApplyMapEdit ->
MapEditing.Apply -> MapRecorder.Edit, on the UI thread under SyncRoot, marked
dirty and saved like a recorded room. Cells: Ctrl+P > Edit map, arrow keys and
an honest key footer, /map verbs for the edits that need words. Window: a
right-click room menu and a native inspector. Declared on the map surface;
SurfaceParityTests compares MapDigest after native and cell edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
- Ctrl+P > Script console: per character, output and errors with file:line, a
  time- and instruction-limited Lua prompt run through the host's gate, history,
  a read-only store viewer with delete, and Reload. One Client model drawn as
  cells and as a native modal; surface script-console with parity probes.
- F5 > channels (off/copy/move): Comm.Channel.Text into a spawn window per
  channel through SpawnLine; move gags the main-window echo.
- F5 > names: whole-word highlighted names with HighlightColours, at ingest.
- Ctrl+P > Start/Stop scene log: the focused window's poses (or every line) to
  an HTML or plain file under the log root with a who-was-present header;
  refused with no log root. F7 > Scene log holds the format and content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqqt7SPYixp6Cg4JDKRTwx
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