Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
99a9be6
Fold approved visual design into the spec
MotherRisi Jun 30, 2026
543417d
Add Phase 2 UI Foundation plan (interface-based, TDD, UI + background)
MotherRisi Jun 30, 2026
22c14f2
feat(core): extract ITelnetConnection/ISession, add Reconnecting/Erro…
HarryCordewener Jun 30, 2026
a17e5c9
feat(core): add AnsiPalette (design 16-colour + xterm-256 cube/graysc…
HarryCordewener Jun 30, 2026
d84ffc4
feat(core): add SegmentStyle (StyledSegment to inline CSS render cont…
HarryCordewener Jun 30, 2026
e4ecad3
feat(core): add SessionManager (tab lifecycle) behind ISessionManager
HarryCordewener Jun 30, 2026
1d9709e
feat(core): add SessionsViewModel (tabs, input, history) over interfaces
HarryCordewener Jun 30, 2026
28f297b
feat(ui): add bUnit test project + OutputView component (render contr…
HarryCordewener Jun 30, 2026
1154266
fix(core): prune SessionsViewModel history for closed sessions; untra…
HarryCordewener Jun 30, 2026
24208cc
spec: choose SQLite/EF Core + FTS5 backing store; update phasing (UI …
MotherRisi Jun 30, 2026
30367c3
Add Phases 3-6 shared contracts + parallel workstream map
MotherRisi Jun 30, 2026
14548a1
feat(core): add Phase 3-6 shared domain models, interfaces, and test …
HarryCordewener Jun 30, 2026
1396397
feat(data): scaffold SharpClient.Data with AppDbContext and WorldStore
HarryCordewener Jun 30, 2026
6c573a7
feat(data): add WorldStore integration tests and SDD report
HarryCordewener Jun 30, 2026
35523bc
feat(data): add SessionHistory full-text search via SQLite FTS5
HarryCordewener Jun 30, 2026
ba0f9f2
docs(sdd): add Line C SessionHistory implementation report
HarryCordewener Jun 30, 2026
b04c3fe
fix(data): wrap WorldStore.UpdateWorldAsync delete+reinsert in one tr…
MotherRisi Jun 30, 2026
7ba85cb
feat(core): implement TriggerEngine and AliasEngine with TUnit tests
HarryCordewener Jun 30, 2026
4f78bc9
feat(web): scaffold SharpClient.Web Blazor Server host
HarryCordewener Jun 30, 2026
187c10f
feat(core): add CharacterName/WorldName to ISession + SessionsViewMod…
HarryCordewener Jun 30, 2026
2e7580e
feat(ui): add SessionTabs, InputBar, SessionScreen Razor components
HarryCordewener Jun 30, 2026
f19542f
feat(ui): add bUnit tests for SessionTabs and InputBar with UiFakeSes…
HarryCordewener Jun 30, 2026
c7b1275
feat(web): add DemoSession, 3-session DI seed, SessionScreen on Home
HarryCordewener Jun 30, 2026
848d9a4
Add World Manager + UI-components plans (to be executed)
MotherRisi Jun 30, 2026
9657eb2
feat(core): add ISessionLauncher and WorldManagerViewModel
HarryCordewener Jun 30, 2026
db225df
feat(ui): add WorldManager, WorldEditor, CharacterEditor components
HarryCordewener Jun 30, 2026
98f4a20
feat(web): add Worlds page and World Manager wiring
HarryCordewener Jun 30, 2026
511c769
fix(web): use explicit unicode ESC escapes for demo ANSI so output re…
MotherRisi Jun 30, 2026
11b3949
feat(core): add GMCP/negotiation events, ISession default members, Se…
HarryCordewener Jun 30, 2026
c668a26
feat(ui): add ProtocolPanel component, ProtocolPanelViewModel, Sessio…
HarryCordewener Jun 30, 2026
0e013e5
feat(web): add sample NegotiationLog/GmcpLog to DemoSession, wire Pro…
HarryCordewener Jun 30, 2026
4e8e7f1
feat(app): wire real platform services and DI registrations for MAUI …
HarryCordewener Jun 30, 2026
3833062
feat(app): replace template UI with SharpClient screens and design to…
HarryCordewener Jun 30, 2026
2c2128a
docs(app): add MAUI integration report
HarryCordewener Jun 30, 2026
293339b
feat(core): add TriggerAliasEditorViewModel
HarryCordewener Jun 30, 2026
72c0041
feat(core): test TriggerAliasEditorViewModel
HarryCordewener Jun 30, 2026
c3ad47d
feat(ui): add TriggerEditor razor component
HarryCordewener Jun 30, 2026
1dbe5ad
feat(web): add sc-rule-* CSS styles for trigger/alias editor
HarryCordewener Jun 30, 2026
cac1fd1
feat(ui): bUnit tests for TriggerEditor component
HarryCordewener Jun 30, 2026
7f62666
feat(web): wire TriggerAliasEditorViewModel + /worlds/{id}/rules page
HarryCordewener Jun 30, 2026
18be765
feat(core): add IPreferences interface and FakePreferences test fake
HarryCordewener Jun 30, 2026
5ea3cf3
feat(core): add SettingsViewModel with IPreferences persistence and C…
HarryCordewener Jun 30, 2026
cc0fb6d
feat(ui): add SettingsView.razor component and CSS glow/scanlines/set…
HarryCordewener Jun 30, 2026
1cebc02
feat(web): wire SettingsViewModel into web host with live CSS var inj…
HarryCordewener Jun 30, 2026
67f4a7b
feat(app): add MauiPreferences, wire SettingsViewModel into MAUI host…
HarryCordewener Jun 30, 2026
316e66b
fix(worlds): add Edit World UI trigger and fix DeleteWorldAsync secre…
HarryCordewener Jun 30, 2026
dbebb72
test(presentation): add UpdateWorld, UpdateCharacter, DeleteWorld-sec…
HarryCordewener Jun 30, 2026
88c7bc6
fix(triggers): guard all mutation methods against null _world (item D)
HarryCordewener Jun 30, 2026
ac2b930
fix(launcher): dispose session on SendAsync failure to prevent resour…
HarryCordewener Jun 30, 2026
bac2e7f
fix(css): replace display:contents on .sc-shell with display:block (i…
HarryCordewener Jun 30, 2026
fb08302
fix(settings): label MinColumns slider as coming-soon and emit --min-…
HarryCordewener Jun 30, 2026
6a73828
fix(ui): make connect-string input type=password in CharacterEditor (…
HarryCordewener Jun 30, 2026
a36cd75
chore(app): remove dead template cruft from MAUI project (items J/K)
HarryCordewener Jun 30, 2026
9077f42
fix(settings): clamp unknown accent to default on VM construction (it…
HarryCordewener Jun 30, 2026
7e9bf31
docs(session): add thread-safety NOTE comment above scrollback/log fi…
HarryCordewener Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
bin/
obj/
.vs/
.idea/
*.user

# Machine-specific tool paths (Android SDK / JDK)
Expand Down
73 changes: 73 additions & 0 deletions .superpowers/sdd/lineC-history-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Line C — SessionHistory FTS5 Implementation Report

**Date:** 2026-06-30
**Branch:** feat/phase2-ui-foundation
**Status:** DONE

---

## What was built

`SharpClient.Data.SessionHistory : ISessionHistory` — full-text search over session
history lines using SQLite FTS5 via `Microsoft.Data.Sqlite` raw SQL.

### Files created / modified

| File | Action |
|------|--------|
| `src/SharpClient.Data/SessionHistory.cs` | Created |
| `src/SharpClient.Data/SharpClient.Data.csproj` | Added `Microsoft.Data.Sqlite` 10.0.9 explicit top-level reference |
| `tests/SharpClient.Data.Tests/SessionHistoryTests.cs` | Created |
| `.superpowers/sdd/lineC-history-report.md` | Created (this file) |

---

## Design decisions

### FTS5 table schema
```sql
CREATE VIRTUAL TABLE IF NOT EXISTS session_history
USING fts5(character_id UNINDEXED, line, sequence UNINDEXED);
```
- `character_id UNINDEXED` — stored but not tokenised; exact-match filter not needed (search is global, then filtered by caller if required).
- `sequence UNINDEXED` — stored for potential future use; always written as `0` because the authoritative monotonic sequence is the FTS5 implicit `rowid`.
- `CREATE VIRTUAL TABLE IF NOT EXISTS` is called on every operation (cheap, idempotent) — avoids maintaining a mutable per-instance flag and the IDisposable burden a SemaphoreSlim would impose.

### Sequence derivation
`HistoryHit.Sequence` = FTS5 `rowid` read back in `SELECT character_id, line, rowid`. The implicit rowid is SQLite's internal auto-increment integer, which is guaranteed monotonically increasing for append-only use. No race condition because SQLite is single-writer on a local file.

### FTS5 query sanitisation
`SanitiseFtsQuery(string)` splits the user input by whitespace, doubles any embedded `"` characters (FTS5 escape convention), wraps each token in `"…"`, and joins with spaces. This produces a conjunction of FTS5 phrase literals, making all FTS5 operators (`*`, `AND`, `OR`, `NOT`, `^`, unmatched `"`) inert. Empty / whitespace-only input returns `null` and the caller returns an empty list immediately without executing a MATCH query.

Example: `foo "bar` → `"foo" """bar"` (FTS5 phrases for literal `foo` and literal `"bar`).

### Connection strategy
A fresh `SqliteConnection` is opened per operation (`await using`). This is correct for a local app with no concurrency requirements; no connection pooling or shared-connection complexity needed.

---

## Test results

| Suite | Total | Passed | Failed | Warnings |
|-------|-------|--------|--------|----------|
| `SharpClient.Data.Tests` | 16 | 16 | 0 | 0 |
| `SharpClient.Tests` | 49 | 49 | 0 | 0 |

### SessionHistoryTests coverage (7 + 1 bonus test)

1. `AppendThenSearchReturnsMatchingLinesWithCorrectCharacterId` — basic round-trip; single term; correct CharacterId.
2. `MultiWordSearchMatchesLinesContainingAllTerms` — two-term AND semantics.
3. `SearchAcrossTwoCharactersReturnsCorrectCharacterIds` — per-character ID integrity.
4. `LimitIsRespectedWhenMoreMatchesExist` — 10 inserts, limit 3 → 3 results.
5. `SearchWithNoMatchReturnsEmptyList` — no false positives.
6. `EmptyOrWhitespaceQueryReturnsEmptyListWithoutException` — empty string and whitespace-only.
7. `QueryWithFtsSpecialCharactersDoesNotThrow` — `"unclosed`, `*`, `"""` all sanitised cleanly.
8. `SequenceValuesArePositiveAndDistinctPerAppend` — rowid-based sequences are positive and unique.

---

## Concerns / notes

- FTS5 is available in `SQLitePCLRaw.lib.e_sqlite3` (the e_sqlite3 build ships with FTS5 compiled in); tests confirm it works.
- The `sequence UNINDEXED` column is written as `0`; if callers ever need the stored value to equal the rowid, a post-insert `UPDATE` would be needed. For now, `HistoryHit.Sequence` always comes from `rowid` via SELECT.
- No `NuGetAuditSuppress` was needed for `Microsoft.Data.Sqlite` — the advisory only covers `SQLitePCLRaw.lib.e_sqlite3` which is already suppressed in both csproj files.
108 changes: 108 additions & 0 deletions .superpowers/sdd/lineC-worldstore-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Line C — WorldStore Implementation Report

**Date:** 2026-06-30
**Branch:** feat/phase2-ui-foundation
**Status:** DONE

---

## What Was Built

### New Projects

| Project | Path | Role |
|---|---|---|
| `SharpClient.Data` | `src/SharpClient.Data/` | EF Core SQLite persistence library |
| `SharpClient.Data.Tests` | `tests/SharpClient.Data.Tests/` | TUnit integration tests |

Both added to `SharpClient.slnx` under `/src/` and `/tests/` folders respectively.

### Files Created

- `src/SharpClient.Data/SharpClient.Data.csproj` — `net10.0` classlib; references `SharpClient.Core`; `Microsoft.EntityFrameworkCore.Sqlite 10.0.9`; targeted `NuGetAuditSuppress` for GHSA-2m69-gcr7-jv3q (see Warnings section).
- `src/SharpClient.Data/AppDbContext.cs` — `DbContext` with `DbSet<World>`, `DbSet<Character>`, `DbSet<TriggerRule>`, `DbSet<AliasRule>`; `OnModelCreating` configures all relationships.
- `src/SharpClient.Data/WorldStore.cs` — `IWorldStore` implementation.
- `tests/SharpClient.Data.Tests/SharpClient.Data.Tests.csproj` — TUnit executable; `OutputType=Exe`; `TestingPlatformDotnetTestSupport=true`.
- `tests/SharpClient.Data.Tests/WorldStoreTests.cs` — 8 tests with a `TempFileAppStorage` helper.

---

## EF Core Approach

### Schema

`TriggerRule` and `AliasRule` are shared entity types used by both `World` and `Character`. Two separate EF relationships are configured per entity type:

- `World → Triggers/Aliases` via **shadow FK `"WorldId"`** (nullable) on the rule tables, `OnDelete(Cascade)`.
- `Character → Triggers/Aliases` via **shadow FK `"CharacterId"`** (nullable) on the rule tables, `OnDelete(Cascade)`.

Result: `TriggerRules` and `AliasRules` tables each have two nullable FK columns (`WorldId`, `CharacterId`). A world-scope rule has `WorldId` set and `CharacterId` null; a character-scope rule is the reverse.

`World → Characters` uses the explicit `Character.WorldId` property FK.

### UpdateWorldAsync Strategy

**Delete-then-add with two `SaveChangesAsync` calls:**

1. `_db.ChangeTracker.Clear()` — detach any previously tracked entities so the subsequent load is fresh (critical when the same `AppDbContext` was used for `AddWorldAsync`; without this, EF's identity resolution returns the already-tracked modified instance and the deletion can conflict with in-memory "Added" child entities).
2. Load the existing `World` graph fully (all `Include`/`ThenInclude`).
3. `Remove(existing)` — EF's client-side cascade marks all loaded children as `Deleted`.
4. `SaveChangesAsync` — commits the deletion.
5. `_db.ChangeTracker.Clear()` — release the deleted entities.
6. `Add(world)` — attach the incoming graph as `Added`.
7. `SaveChangesAsync` — commits the insertion.

**Why two commits instead of one:** EF Core batches a same-PK DELETE and INSERT into a single command batch. SQLite's UNIQUE constraint fires before the row is deleted, causing `SqliteException: UNIQUE constraint failed`. Two commits guarantees the delete is physically committed before the insert.

**Why ChangeTracker.Clear() is needed:** After `AddWorldAsync`, the `world` object is tracked as `Unchanged`. The test mutates it (rename, add CharC). `FirstOrDefaultAsync` with identity resolution returns the same tracked (now `Modified`) instance. `Remove` then cascades to the `Added` CharC, producing an unexpected extra delete attempt. Clearing the tracker before the load prevents this.

---

## Test Results

| Suite | Total | Passed | Failed |
|---|---|---|---|
| `SharpClient.Data.Tests` | 8 | 8 | 0 |
| `SharpClient.Tests` (existing) | 49 | 49 | 0 |
| `SharpClient.UI.Tests` (existing) | 2 | 2 | 0 |

### Data.Tests coverage

- `AddWorldThenGetWorldsReturnsFullyPopulatedGraph` — full graph (2 chars, world rules, char rules), counts correct.
- `AddWorldRoundTripsTriggerRuleFields` — `Kind`, `Pattern`, `Action`, `ActionValue`, `Enabled=false`.
- `AddWorldRoundTripsAliasRuleFields` — `Pattern`, `Expansion`, `Enabled=true`.
- `AddWorldRoundTripsConnectSecretKey` — nullable string preserved.
- `UpdateWorldRenameAndAddCharacterReflectedInGetWorlds` — rename + add character.
- `UpdateWorldRemoveCharacterNotReturnedByGetWorlds` — removed child not returned.
- `DeleteWorldGetWorldsReturnsEmpty` — store empty after delete.
- `DeleteWorldCascadesChildrenNoOrphanRows` — direct counts on `Characters`, `TriggerRules`, `AliasRules` all zero.

---

## Warnings Resolved

### NU1903 — SQLitePCLRaw.lib.e_sqlite3 vulnerability (GHSA-2m69-gcr7-jv3q)

`Microsoft.EntityFrameworkCore.Sqlite 10.0.9` pulls in `SQLitePCLRaw.lib.e_sqlite3 2.1.11`, which NuGet flags as a known high-severity vulnerability. Version 2.1.11 is the **latest available release** — no patched version exists. A targeted `<NuGetAuditSuppress>` item was added to both Data and Data.Tests project files (not `Directory.Build.props`) to suppress only this specific advisory.

### CA1707 — Underscore in test method names

TUnit tests typically use `MethodName_State_Expected` naming. `AnalysisLevel=latest-recommended` includes CA1707 as an error. All test method names were written in `PascalCase` (e.g., `AddWorldRoundTripsTriggerRuleFields`) to match the existing test conventions in `SharpClient.Tests`.

### TUnitAssertions0015 — `.IsEqualTo(true/false)` deprecated

TUnit 1.57.0 requires `.IsTrue()` / `.IsFalse()` in place of `.IsEqualTo(true/false)`.

### CS9051 — File-local type in public member signature

`TempFileAppStorage` was initially declared `file sealed class`; changed to `internal sealed class`.

### CA1001 — Type owns disposable field

Keeping `AppDbContext` as a field on the test class triggers CA1001. Resolved by creating `AppDbContext` locally (via `await using`) inside each test method rather than storing it as a class field.

---

## Concerns

None. The two-SaveChanges update approach is a known limitation (two round-trips, not a single atomic transaction). For a MUSH client settings store with no concurrent writers this is acceptable. If atomicity becomes a requirement, wrapping both commits in a `BeginTransaction` / `CommitTransaction` block is a straightforward addition.
86 changes: 86 additions & 0 deletions .superpowers/sdd/maui-integration-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# MAUI Integration Report — SharpClient.App

## Services Added (`src/SharpClient.App/Services/`)

| File | Interface | Notes |
|------|-----------|-------|
| `MauiAppStorage.cs` | `IAppStorage` | Returns `FileSystem.AppDataDirectory/sharpclient.db` |
| `MauiSecretStore.cs` | `ISecretStore` | Delegates to `Microsoft.Maui.Storage.SecureStorage.Default` (Android Keystore) |
| `MauiNotifier.cs` | `INotifier` | **Stub** — logs via `ILogger<MauiNotifier>` using `[LoggerMessage]` source-gen; push/local-notification support deferred |
| `TelnetSessionLauncher.cs` | `ISessionLauncher` | Uses `ITelnetConnectionFactory` (Core abstraction, see below) to open a TCP telnet connection, wraps it in `Session`, auto-sends the connect string from `ISecretStore` |

## Core Additions (`src/SharpClient.Core/Connection/`)

| File | Purpose |
|------|---------|
| `ITelnetConnectionFactory.cs` | Interface: `ITelnetConnection CreateConnection()` |
| `TelnetConnectionFactory.cs` | Wraps `ITelnetInterpreterFactory` from TNC; returns `new TelnetConnection(factory)` |

**Why the extra abstraction?**
`ITelnetInterpreterFactory` (from TelnetNegotiationCore) resolves fine for `net10.0` but causes
`CS0246` under `net10.0-android`. Root cause: TNC's net10.0 DLL references `Stateless,
Version=4.0.0.0, PublicKeyToken=93038f0927583c9a`; the available `stateless` 5.20.0 package DLL
does not carry that public-key token, so Roslyn silently fails to resolve TNC's type metadata in
the Android compilation context. Placing the TNC-dependent class in Core (compiled for plain
`net10.0`) avoids this; App only references Core interfaces.

## DI Registrations (`MauiProgram.cs`)

```
AddTelnetClient() // registers ITelnetInterpreterFactory
IAppStorage → MauiAppStorage singleton
ISecretStore → MauiSecretStore singleton
INotifier → MauiNotifier singleton
AppDbContext transient (SQLite, net10.0-android safe)
IWorldStore → WorldStore transient (depends on DbContext)
ISessionHistory → SessionHistory transient (uses IAppStorage path directly)
SessionManager (concrete + ISessionManager) singleton
SessionsViewModel singleton
ProtocolPanelViewModel singleton
WorldManagerViewModel transient
ISessionLauncher → TelnetSessionLauncher transient
ITelnetConnectionFactory → TelnetConnectionFactory singleton
ITriggerEngine → TriggerEngine singleton (stateless)
IAliasEngine → AliasEngine singleton (stateless)
```

Lifetimes follow the Web app pattern; `AppDbContext`/`IWorldStore`/`WorldManagerViewModel` are
transient rather than scoped because MAUI Blazor Hybrid has no HTTP request scope.

## UI Changes

- `Components/_Imports.razor` — added `SharpClient.Core.*` and `SharpClient.UI.Components` usings
- `Components/Pages/Home.razor` — renders `<SessionScreen Vm="@Vm" ProtocolVm="@ProtocolVm" />`
- `Components/Pages/Worlds.razor` — new page at `/worlds`, renders `<WorldManager Vm="@Vm" />`
- `Counter.razor`, `Weather.razor` — deleted
- `Components/Layout/MainLayout.razor` — replaced with sc-nav bar (Session / Worlds links), matches Web
- `wwwroot/app.css` — replaced with full design-token CSS from SharpClient.Web (dark theme, sc-* classes)
- `wwwroot/index.html` — added JetBrains Mono + Space Grotesk Google Fonts `<link>`s; removed unused bootstrap reference

## Android 0-Warning Build Notes

- `NuGetAuditSuppress` added for `GHSA-2m69-gcr7-jv3q` (SQLitePCLRaw 2.1.11 advisory, mirrors Data project)
- `TelnetNegotiationCore 2.5.0` added as direct `PackageReference` (required for `AddTelnetClient()` extension method; transitive exposure from Core does not flow to `net10.0-android`)
- `MauiNotifier` uses `[LoggerMessage]` source-generation to satisfy CA1848/CA1873 (TreatWarningsAsErrors=true)
- No broad `#pragma warning disable` or `SuppressTrimAnalysisWarnings` needed; Debug build does not run the linker

## Stubbed / Deferred

- **INotifier** → logs to ILogger only; replace `MauiNotifier` with a `Plugin.LocalNotification`-backed implementation when push/in-app notifications are needed
- **Android INTERNET permission** → `AndroidManifest.xml` not modified; add `<uses-permission android:name="android.permission.INTERNET" />` before first device run
- **EF Core migrations** → `AppDbContext.EnsureCreated()` is called lazily by `WorldStore`; no migration runner wired
- **Preferences/Settings screen** — not implemented in this integration
- **No demo/seed sessions** — app starts with the World Manager in its empty state

## Build Result

```
dotnet build src/SharpClient.App/SharpClient.App.csproj -f net10.0-android
→ Build succeeded. 0 Warning(s) 0 Error(s)

dotnet run --project tests/SharpClient.Tests/...
→ Passed! total: 89 failed: 0 succeeded: 89

SharpClient.UI.Tests build: 0 warnings, 0 errors
SharpClient.Data.Tests build: 0 warnings, 0 errors
```
4 changes: 4 additions & 0 deletions SharpClient.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
<Folder Name="/src/">
<Project Path="src/SharpClient.App/SharpClient.App.csproj" />
<Project Path="src/SharpClient.Core/SharpClient.Core.csproj" />
<Project Path="src/SharpClient.Data/SharpClient.Data.csproj" />
<Project Path="src/SharpClient.UI/SharpClient.UI.csproj" />
<Project Path="src/SharpClient.Web/SharpClient.Web.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/SharpClient.Data.Tests/SharpClient.Data.Tests.csproj" />
<Project Path="tests/SharpClient.Tests/SharpClient.Tests.csproj" />
<Project Path="tests/SharpClient.UI.Tests/SharpClient.UI.Tests.csproj" />
</Folder>
</Solution>
Loading