Skip to content

Phase 2: UI foundation → feature-complete MUSH client (web preview + Android) - #2

Merged
HarryCordewener merged 55 commits into
feat/core-pipelinefrom
feat/phase2-ui-foundation
Jun 30, 2026
Merged

Phase 2: UI foundation → feature-complete MUSH client (web preview + Android)#2
HarryCordewener merged 55 commits into
feat/core-pipelinefrom
feat/phase2-ui-foundation

Conversation

@HarryCordewener

Copy link
Copy Markdown
Member

Phase 2 — UI Foundation → feature-complete client

Builds on Phase 1 (PR #1). Stacked on feat/core-pipeline — retarget to master once #1 merges. Everything is interface-based and unit/integration tested with no device required.

What's here

  • Interfaces & contracts: ITelnetConnection/ISession/ISessionManager, platform abstractions (ISecretStore, IAppStorage, INotifier, IPreferences), ISessionLauncher, ITelnetConnectionFactory, domain models (World/Character/TriggerRule/AliasRule). Extended ConnectionState (+Reconnecting/Error) + Session.StateChanged.
  • Rendering: AnsiPalette (design 16-colour + xterm-256), SegmentStyle (StyledSegment→CSS render contract from the approved design).
  • Sessions: SessionManager (tabs) + SessionsViewModel; SessionTabs/InputBar/OutputView/SessionScreen Razor components.
  • Worlds & Characters: WorldManagerViewModel + WorldManager/WorldEditor/CharacterEditor; Connect flow via ISessionLauncher; secrets via ISecretStore (keyed, never in plaintext).
  • Persistence (SharpClient.Data): EF Core SQLite WorldStore (transactional update) + SessionHistory full-text search via SQLite FTS5.
  • Engines: declarative TriggerEngine + AliasEngine (regex-guarded) + a triggers/aliases editor (/worlds/{id}/rules).
  • Negotiation: GMCP/MSSP surfaced via TNC plugins → ProtocolPanel (per-session debug drawer).
  • Settings: font / min-columns / max-size / accent / glow / scanlines, persisted via IPreferences, applied live as CSS variables.
  • Two hosts: SharpClient.Web (Blazor Server preview — view the UI on Linux with no Android SDK) and the real SharpClient.App MAUI Blazor Hybrid Android app wired with real platform services (SecureStorage, FileSystem, telnet launcher).

Quality

  • 171 tests passing (Core 122, UI/bUnit 33, Data 16); full solution and the net10.0-android head build at 0 warnings under TreatWarningsAsErrors.
  • Built subagent-driven: each feature TDD'd, spec+quality reviewed, fixed, then a consolidated correctness/UX sweep and this final whole-branch review (verdict: ready to merge).
  • Interface-based testing throughout: hand-written fakes, EF on temp SQLite, bUnit (C#) for components — no device needed.

Known follow-ups (non-blocking, tracked)

  • On-device telnet unverified (no emulator run); the TNC/Android metadata workaround is sound (strong-name not enforced at runtime on .NET-Android).
  • DeleteWorldAsync secret cleanup uses the in-memory cache; TelnetSessionLauncher could also dispose on a ConnectAsync throw.
  • WebPreferences is in-memory (no cross-reload persist); MinColumns→auto-font-size needs JS interop (slider labelled "coming soon").
  • Character-scope trigger rules + inline rule-edit UI; assorted coverage-gap tests + a regex cache (perf) — all documented.

🤖 Generated with Claude Code

MotherRisi and others added 30 commits June 30, 2026 00:30
- docs/design/: prototype (SharpClient.prototype.html), 24 screenshots, and
  design-tokens.md (Phosphor palette, ANSI 16-colour map, StyledSegment render
  contract, 5-state connection palette, screen inventory)
- spec: visual-design section, extended ConnectionState (+Reconnecting/Error),
  Session.StateChanged, interface-based architecture (§5.1), interface-based +
  bUnit testing (§8), updated phasing

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
…foundation done, web preview host next)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
New project src/SharpClient.Data (net10.0 classlib) referencing
SharpClient.Core. Adds Microsoft.EntityFrameworkCore.Sqlite 10.0.9
(with a targeted NuGetAuditSuppress for GHSA-2m69-gcr7-jv3q, the only
published version of SQLitePCLRaw.lib.e_sqlite3 2.1.11).

AppDbContext configures the World→Character→TriggerRule/AliasRule graph
with shadow FKs (WorldId, CharacterId) for rules shared between both
principal types. WorldStore implements IWorldStore; UpdateWorldAsync uses
a clear-tracker + delete-first + insert-second two-commit strategy to
avoid a SQLite UNIQUE constraint violation when replacing a same-PK row.

Project added to SharpClient.slnx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
8 TUnit tests on a per-test temp SQLite file cover: Add+Get full graph,
TriggerRule/AliasRule/ConnectSecretKey round-trips, Update (rename +
add/remove character), Delete + orphan-row cascade check. All 8 pass;
existing SharpClient.Tests (49) and UI.Tests (2) suites unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
Implements ISessionHistory using Microsoft.Data.Sqlite raw SQL on the same
db file as AppDbContext. FTS5 virtual table; user query sanitised by wrapping
each token in double-quoted FTS5 phrase literals (embedded " doubled).
Sequence derived from FTS5 implicit rowid. 16/16 Data.Tests pass; 49 core
tests still green; 0 warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
…ansaction

Prevents permanent data loss if the re-insert fails after the delete commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
Pure-logic engines for the declarative trigger and alias systems.
TriggerEngine parses ANSI lines, matches Substring/Regex rules, and
accumulates Send/Notify actions; Highlight applies a whole-line foreground
recolour (v1 simplification). AliasEngine finds the first enabled regex
alias and substitutes $0–$9 capture groups. Both guard against malformed
patterns (catch + skip, 100 ms timeout). 26 new TUnit tests, 0 warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
Adds src/SharpClient.Web — a Blazor Web App (Server interactivity) that
references SharpClient.UI and SharpClient.Core and renders the existing
OutputView component with a realistic ANSI sample feed styled per the
Phosphor design tokens.

Key changes:
- SharpClient.Web.csproj: references UI + Core; removes redundant props
  (inherited from Directory.Build.props)
- App.razor: Google Fonts (JetBrains Mono, Space Grotesk); Bootstrap removed
- wwwroot/app.css: full design-token CSS variables, terminal body backdrop,
  output container (.sc-output/.sc-line matching OutputView's class names),
  13px JetBrains Mono / 1.58 line-height / white-space:pre-wrap
- Components/Pages/Home.razor: 21-line sample feed via AnsiParser.Parse —
  covers bold, 16-colour fg (31/92/33/34/35/36/93/91/97), xterm-256
  (208/118/87/135), underline, inverse, plain and blank lines
- Layout stripped to bare @Body; Counter/Weather/NavMenu deleted
- Project added to SharpClient.slnx

Build: 0 warnings, 0 errors (net10.0, TreatWarningsAsErrors=true)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
…el.CloseAsync

Session ctor gains optional characterName/worldName default params keeping existing
call-sites compiling. FakeSession updated with settable identity props.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
SessionTabs renders tab strip with state-coloured dots and × close button.
InputBar binds to SessionsViewModel.Input with disabled-aware Send.
SessionScreen composes all three, subscribes to Vm.Changed for live updates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
…sion

UiFakeSession uses explicit event accessors to satisfy ISession without CS0067.
SessionTabsTests: tab count, error-dot colour, active-tab class.
InputBarTests: disabled state, enabled on input, click-send delivers command.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
DemoSession implements ISession with seeded ANSI scrollback; SendAsync echoes
input as a new scrollback line. Program.cs registers SessionManager + SessionsViewModel
as singletons and seeds Vesper@Sindome (Connected/rich feed), Thorne@GrapevineMUD
(Connecting), Doran@BatMUD (Error). Home page renders full SessionScreen component.
app.css gains Phosphor-themed tab-bar, output-area, input-bar, and empty-state styles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
…nders in colour

DemoSession/DemoSessionLauncher were missing the ESC prefix entirely; Program.cs
used raw ESC bytes (functional but invisible in diffs). Normalised all to backslash-u-001b.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
…ssion wiring

- GmcpMessage + NegotiationEvent records in Connection namespace
- ITelnetConnection: GmcpReceived + NegotiationReceived events
- TelnetConnection: AddDefaultMUDProtocols with GMCP + MSSP callbacks
- ISession: NegotiationLog/GmcpLog/ProtocolChanged as default interface members
- Session: accumulates both logs, dedupes GMCP by Package, fires ProtocolChanged
- FakeTelnetConnection: EmitGmcp + EmitNegotiation test helpers
- SessionProtocolTests: 6 tests all passing (89 total)

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

- ProtocolPanelViewModel: tracks active session NegotiationLog/GmcpLog,
  re-raises manager.Changed + session.ProtocolChanged as single Changed event
- ProtocolPanel.razor: shows negotiation rows (key+detail) and GMCP packages
  (package name + JSON pre); stable CSS classes for testability
- SessionScreen.razor: optional ProtocolVm parameter + { } toggle button in
  header; panel rendered below output when visible; existing tests unaffected
- ProtocolPanelTests: 6 bUnit tests (17 UI tests total, all passing)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
HarryCordewener and others added 25 commits June 30, 2026 03:19
…tocolPanel

- DemoSession: overrides NegotiationLog (TTYPE/NAWS/MCCP2/CHARSET/MSSP rows)
  and GmcpLog (Char.Vitals + Room.Info) with realistic static sample data
- Program.cs: register ProtocolPanelViewModel as singleton
- Home.razor: inject ProtocolPanelViewModel and pass to SessionScreen

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

Add MauiAppStorage, MauiSecretStore, MauiNotifier (stub), and
TelnetSessionLauncher to SharpClient.App/Services/. Register all
services in MauiProgram.cs: AddTelnetClient(), IAppStorage, ISecretStore,
INotifier, AppDbContext (transient), IWorldStore, ISessionHistory,
SessionManager/ISessionManager (singleton), SessionsViewModel,
ProtocolPanelViewModel, WorldManagerViewModel (transient), and
ITelnetConnectionFactory/TelnetConnectionFactory.

Add ITelnetConnectionFactory + TelnetConnectionFactory to SharpClient.Core
to work around a Roslyn metadata-loading failure for ITelnetInterpreterFactory
under net10.0-android (Stateless assembly version mismatch prevents Roslyn
from resolving TNC types in the MAUI Android TFM; Core compiles under plain
net10.0 where TNC types are fully accessible).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
…kens

Replace the default MAUI Blazor template pages with real SharpClient UI:
- Home (/) renders SessionScreen with injected SessionsViewModel +
  ProtocolPanelViewModel from the RCL
- /worlds renders WorldManager with injected WorldManagerViewModel
- Counter.razor and Weather.razor removed
- _Imports.razor adds SharpClient.Core.* and SharpClient.UI.Components usings
- MainLayout.razor replaced with sc-nav bar (Session / Worlds links)
- NavMenu.razor emptied (nav now in MainLayout)
- wwwroot/app.css replaced with full design-token CSS from SharpClient.Web
- wwwroot/index.html adds JetBrains Mono + Space Grotesk Google Fonts links
  and drops bootstrap.min.css reference (no longer needed with sc-* tokens)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp
…t leak

A: Add OpenEditWorld() method and Edit button on each world row in
   WorldManager.razor (stopPropagation so expand-toggle is not triggered).
B: DeleteWorldAsync now iterates world.Characters and removes each
   ConnectSecretKey via _secrets.RemoveAsync before deleting the world,
   mirroring DeleteCharacterAsync behaviour.
…rets, UpdateAlias tests

C: Tests for UpdateWorldAsync (rename persists, UpdateCount increments),
   UpdateCharacterAsync (secret key is reused not regenerated, value updated),
   and DeleteWorldAsync (secrets removed for all characters).
E: UpdateAliasAsync test - alias replaced in-place + store UpdateCount
   increments, mirroring the existing UpdateTriggerAsync test pattern.
Add 'if (_world is null) return;' at the start of all eight mutation
methods (Add/Update/Delete/Toggle Trigger and Alias). Toggle methods
now also return early if the rule id is not found, avoiding a spurious
UpdateWorldAsync call.
…ce leak (item F)

Wrap the post-connect SendAsync call in try/catch; on any exception
DisposeAsync the session before rethrowing so the TelnetConnection is
not leaked when the connect string send fails.
…tem G)

display:contents made min-height:100vh a dead rule and caused the
.sc-scanlines::before overlay to be cross-browser-fragile (some browsers
skip pseudo-elements on display:contents elements). Changed to display:block
so min-height:100vh and ::before work reliably.
Also added the missing .sc-shell, .sc-glow and .sc-scanlines::before rules
to the App (MAUI) CSS which previously had none of these.
…cols var (item H)

Add '(font auto-sizing — coming soon)' annotation to the Min columns slider
label so users know it is a forward-looking passthrough. RootStyleVariables
now emits --min-cols:{MinColumns} as a CSS custom property for future use.
No JS-interop auto-size formula added (deferred per spec).
…item I)

The connect string is a credential; masking it prevents shoulder-surfing
and browser autofill misidentification. Blank-to-keep-existing behaviour
is unchanged.
J: Remove the dead <div class='status-bar-safe-area'> from index.html
   (its CSS was already removed).
K: Delete NavMenu.razor and NavMenu.razor.css — the file contained only
   a comment noting navigation is handled by the sc-nav bar; nothing
   references it.
…em L)

If the stored accent is not in s_accentMeta (e.g. from an old config or
manual edit), clamp it to '#9b7ed4' (default purple) and persist the
correction so --acc2/soft/line custom properties don't desync.
Test: UnknownAccentIsClampedToDefaultOnConstruction verifies the clamp,
the pref correction, and that RootStyleVariables contains no stale value.
…elds (item M)

Documents that _scrollback, _negotiationLog, and _gmcpLog are not
thread-safe and that LineReceived/protocol events may fire on the
network thread; UI consumers must marshal. Adds a TODO for future
concurrent-access guard.
@HarryCordewener
HarryCordewener merged commit 7e40756 into feat/core-pipeline Jun 30, 2026
@HarryCordewener
HarryCordewener deleted the feat/phase2-ui-foundation branch June 30, 2026 19:23
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