Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CI

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
build:
# OpenKey targets net10.0-windows and uses DPAPI, so it cannot build or test on Linux.
runs-on: windows-latest

steps:
- uses: actions/checkout@v4

# No version here: global.json pins the SDK, so CI and a developer machine agree.
- uses: actions/setup-dotnet@v4

- name: Restore
run: dotnet restore

# Directory.Build.props sets TreatWarningsAsErrors, and IsAotCompatible turns the
# trim/AOT analyzers on, so this is a strict gate rather than a formality.
- name: Build
run: dotnet build --no-restore -c Release

- name: Test
run: dotnet test --no-build -c Release --verbosity normal

# Proves the publish profile in OpenKey.csproj still produces the shipping artifact
# without anyone pasting flags from a README.
- name: Verify single-file publish
run: dotnet publish src/OpenKey/OpenKey.csproj -c Release -r win-x64 -o publish-check

- name: Confirm the exe exists
shell: pwsh
run: |
$exe = "publish-check/OpenKey.exe"
if (-not (Test-Path $exe)) { throw "Expected $exe to exist" }
$mb = [math]::Round((Get-Item $exe).Length / 1MB, 1)
Write-Host "OpenKey.exe is $mb MB"
48 changes: 48 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Release

on:
push:
tags: ["v*"]
workflow_dispatch:

permissions:
contents: write

jobs:
publish:
runs-on: windows-latest

strategy:
matrix:
rid: [win-x64, win-arm64]

steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4

- name: Test before shipping
run: dotnet test -c Release

# All publish flags live in OpenKey.csproj, so this line cannot drift from what was tested.
- name: Publish ${{ matrix.rid }}
run: dotnet publish src/OpenKey/OpenKey.csproj -c Release -r ${{ matrix.rid }} -o out/${{ matrix.rid }}

- name: Name the artifact by platform
shell: pwsh
run: |
New-Item -ItemType Directory -Force dist | Out-Null
Copy-Item "out/${{ matrix.rid }}/OpenKey.exe" "dist/OpenKey-${{ matrix.rid }}.exe"

- uses: actions/upload-artifact@v4
with:
name: OpenKey-${{ matrix.rid }}
path: dist/OpenKey-${{ matrix.rid }}.exe

# Release titles carry the version only — never a phase number. See docs/06.
- name: Attach to the release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
files: dist/OpenKey-${{ matrix.rid }}.exe
generate_release_notes: true
121 changes: 121 additions & 0 deletions BACKLOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Backlog

**What this file is for.** `docs/07-roadmap.md` says *what* is planned and *why*, organised by
tier. This file says *what state each item is in* and *what order it happens in*. Entries here link
to the roadmap rather than restating it — one canonical home per fact.

Tier numbers refer to the ladder in [`docs/07-roadmap.md`](docs/07-roadmap.md#tier-ladder--sort-rule).

---

## Done

### Production readiness pass — 2026-08-03

- Dependency upgrade: Spectre.Console 0.49.1 → 0.57.2, Markdig, Microsoft.Extensions, test
packages. SDK pinned via `global.json`; `win-arm64` added.
- Publish profile moved from README prose into `OpenKey.csproj`.
- `System.Text.Json` source generation for everything persisted and every request body.
- 14 correctness defects fixed — see [`CHANGELOG.md`](CHANGELOG.md) for the user-facing list.
- Console rebuilt: block-level streaming, `Theme`/`Glyphs`/`Components`, error cards, sentence-case
voice, ASCII glyph fallback, exit hold.
- Test suite 11 → 65, including the first coverage `ChatEngine` has ever had.
- Docs: `docs/architecture/`, user guide, testing guide, all known doc/code contradictions
resolved.
- Repo hygiene: `LICENSE` (MIT), `CHANGELOG.md`, `CONTRIBUTING.md`, `SECURITY.md`, this file, CI
and release workflows.

### Tier 2 and Tier 3 backlog, plus roadmap Quick Wins — 2026-08-03

- **`/new`** — start a fresh conversation, keeping the key. Was the most conspicuous missing verb:
clearing history previously meant `/reset`, which also deleted the key.
- **`/retry`** — resend the last message. Routed back through the host so a resend takes exactly
the same path as a typed message.
- **`/history`**, **`/export [path]`** (defaults to a timestamped file on the Desktop),
**`/copy`** (via `clip.exe` — a console app has no clipboard API without a UI framework).
- **`/theme default|dark|light|mono`**, persisted.
- **`config.json`** — `IConfigStore` / `JsonConfigStore`, matching the shape already documented in
`docs/05`. A pinned model now survives a restart, which it never could before because there was
nowhere to store it. Hand-edited values are normalised rather than trusted.
- **Real tokenizer** — `ITokenCounter` in Core (so Core keeps its zero package references) with a
cl100k-backed implementation in the host. Vocabulary embedded, not downloaded: OpenKey must work
on first run behind a captive portal and makes no network call except to OpenRouter.
- **OAuth port fallback** — four known callback ports tried in order instead of only 3000. Fixed
URLs, not random ones, since OpenRouter 409s on a varying callback.
- **Whole-turn budget** — two minutes across all attempts, checked *between* attempts only: a reply
that is actively arriving is working, however long it has taken.
- **Link URLs escaped** rather than bracket-filtered, which used to silently drop the target of any
URL containing a bracket.
- **Accessibility pass** — verified colour is never the only signal (`✓`/`✗` differ, error cards
name the problem in their title), with the `mono` palette as the standing test and a unit test
asserting no hue survives it.

Two items were resolved differently from how they were written, both noted here because the
deviation is the point:

- **`/stop` was not added.** A command cannot work while a reply streams — the app is not reading a
prompt — and Ctrl+C already cancels correctly. The real gap was that nothing said so, so the fix
is a one-off `(Ctrl+C to stop)` hint. A key-watcher was considered and rejected: it would swallow
type-ahead, and people routinely start composing the next message while a reply arrives.
- **Banner artwork was not added.** The roadmap asked for richer ASCII art; the console design
principle is that calm beats decorative, and the banner is the first thing a non-technical user
sees. Adding art would contradict the design it is supposed to serve.

Also fixed en route: `Microsoft.ML.Tokenizers` 2.0.0 pulls in `Microsoft.Bcl.Memory` 9.0.4, which
carries a known high-severity advisory (GHSA-73j8-2gch-69rq). NuGet audit failed the build; pinned
forward to 10.0.10.

### v0.1.0 — 2026-05-28

First release. See [`CHANGELOG.md`](CHANGELOG.md#010--2026-05-28).

---

## Next up

Ordered by user value within tier. Lowest tier wins.

Tier 2 and Tier 3 are complete — see **Done** above. What remains is Tier 4, which is Phase 5 work
and a step change in scope rather than more polish.

### Tier 4 — providers

13. **Anthropic provider** — see [roadmap Phase 5](docs/07-roadmap.md#phase-5--claude-code-provider-integration).
Worth building *before* the Claude Code subprocess provider: `IChatProvider.Id` and
`DisplayName` are currently never read by anything, so the multi-provider seam has never been
exercised. A second real provider is what proves the abstraction is right.
14. **Adopt `Microsoft.Extensions.AI` beneath `IChatProvider`** — `IChatClient` would supply
tool-calling middleware for Phase 3 and a wide provider ecosystem. It must sit *under* our
interface, never replace it: it is .NET-only and a browser or Android port could not implement
it. Rationale in
[`docs/architecture/08-decisions.md`](docs/architecture/08-decisions.md).

---

## Watching

Not scheduled; revisit when the trigger fires.

- **NativeAOT** — would cut the binary from ~42 MB to roughly 15–20 MB, remove the extract-to-temp
step on first run, and start faster. All three matter for the USB story. The old blocker
(Spectre reflection) is gone as of 0.55, and JSON source generation has landed, so the remaining
cost is measuring what the analyzers still report. `IsAotCompatible` is already on and the tree
is warning-clean.
- **`System.Net.ServerSentEvents`** — would replace the hand-rolled SSE reader. Preview-only today
(`11.0.0-preview.6`); adopt when it ships stable.
- **Bracketed paste** — a more robust multi-line paste than the current timing heuristic. Needs a
custom input reader and is Windows-Terminal-only, so it is not worth it yet.

---

## Deliberately not doing

Recorded so they are not proposed again. Full reasoning in
[`docs/architecture/08-decisions.md`](docs/architecture/08-decisions.md).

- `LiveDisplay` for the transcript — it destroys scrollback.
- `IHttpClientFactory` or Polly — transport retries would corrupt rotation's cooldown accounting.
- `Microsoft.Extensions.Hosting` — a REPL does not need a generic host.
- Paid models in any 1.x release.
- Telemetry, in any phase.
- An auto-update installer. Check-and-notify only.
102 changes: 102 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Changelog

Notable changes to OpenKey. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] — 2026-08-03

### Added

- `/new` starts a fresh conversation while keeping you signed in. Previously the only way to clear
history was `/reset`, which also deleted your key.
- `/retry` resends your last message; `/history` shows the conversation; `/copy` puts the last
reply on the clipboard; `/export` saves it as markdown, defaulting to your Desktop.
- `/theme default|dark|light|mono`, remembered between runs. `mono` drops colour entirely for
high-contrast setups or screenshots.
- Preferences are saved, so a model chosen with `/models` now survives a restart.
- Token counting uses a real tokenizer instead of a character estimate, so conversations are
trimmed more accurately as they grow.
- Browser sign-in falls back across several local ports instead of giving up when one is taken.
- Replies stream as they arrive. Each completed markdown block is rendered styled, so code fences
become panels and prose keeps its emphasis, while the still-arriving tail stays plain.
- Reply header showing which model answered and how long it took.
- Error cards that say what happened and what to do next, replacing raw error-kind names.
- `/models` shows model names and context sizes instead of raw ids.
- OpenKey holds the window open on exit and on failure when it was double-clicked, so parting
messages and errors are actually readable.
- Line editing and history at the prompt, courtesy of the Windows console reader.
- Multi-line paste is kept as one message.
- MIT `LICENSE`, `CONTRIBUTING.md`, `SECURITY.md`, `BACKLOG.md`, and GitHub Actions for CI and
releases.
- Architecture reference under `docs/architecture/`, a user guide, and a testing guide.

### Fixed

- Links whose address contained a bracket lost their target when displayed.
- A message that kept failing could retry for several minutes; it is now bounded, and a reply
that is genuinely arriving is never cut off.
- **Long replies from slow models always failed.** The HTTP timeout covered reading the response
body, so a healthy reply that took over 60 seconds was aborted, misread as a network fault, and
retried on another model that failed the same way. Deadlines now bound the wait for the next
token rather than the whole reply.
- **Replies were never saved.** Session persistence ran after the final chunk was yielded, and the
console stops reading at that point, so the code never executed. No conversation was ever
written to disk.
- **Resuming a conversation never worked.** `ChatMessage` had two constructors, so deserialization
threw an error the session store did not catch.
- **Public Wi-Fi sign-in pages crashed the app.** A captive portal replies to any request with HTML
and HTTP 200; parsing that threw, and with no top-level handler the window closed on the stack
trace.
- **One Ctrl+C disabled the session.** A single cancellation source lived for the whole process, so
after the first Ctrl+C every later message was cancelled before it started.
- **A mid-reply model switch showed the answer twice**, concatenated, while the saved history
stored it once.
- **`/reset` could strand you.** Abandoning setup left OpenKey running with no key: every message
failed, the error suggested `/reset`, and `/reset` returned to the same place.
- **An empty model list disabled the app for 24 hours** — it was cached with a full-day lifetime and
then crashed on every launch, unrecoverable without deleting `%APPDATA%\OpenKey` by hand.
- Replies that ended without an explicit completion signal were discarded as broken and retried,
even though they were complete.
- Free models priced as `0.000000` were misread as paid and hidden from `/models`.
- Cancelled and failed messages stayed in the conversation history and were saved later.
- A network outage put every model on cooldown, so OpenKey stayed broken after the network came
back.
- Requests a model cannot accept are no longer retried across every other model.
- Disk failures during a save no longer crash the app after a reply has been generated.
- Inline code was styled so that it was invisible on light terminals and indistinguishable from
body text on dark ones.
- Markdown blocks ran together with no spacing between them.
- Glyphs that render as boxes in the classic Windows console — including the spinner, which
appeared on every message — now fall back to plain ASCII.
- Output wider than 100 columns no longer stretches code blocks across the whole screen.
- Redirecting output to a file no longer exits the app immediately.

### Changed

- Interface language moved to plain sentence case; no screen shows acronyms like DPAPI or OAuth,
or internal error names.
- Model rotation is reported as one quiet line instead of a warning for each attempt.
- `/reset` states plainly that it erases the key *and* the conversation before asking to confirm.
- Dependencies updated: Spectre.Console 0.49.1 → 0.57.2, Markdig 1.2.0 → 1.3.2, and the
Microsoft.Extensions and test packages to their current releases.
- Publish settings moved into the project file, so a plain `dotnet publish` produces the shipping
binary.
- Windows on ARM (`win-arm64`) is built alongside `win-x64`.
- Test coverage grew from 11 tests to 84.
- A dependency carrying a known high-severity advisory (`Microsoft.Bcl.Memory` 9.0.4, pulled in
transitively) was pinned forward before it could ship.

## [0.1.0] — 2026-05-28

### Added

- First release. Windows console chat client for free OpenRouter models.
- Browser sign-in (PKCE) or paste an existing key; the key is encrypted for your Windows account.
- Automatic rotation across free models when one is rate-limited, with cooldown tracking.
- Conversation history and model cache under `%APPDATA%\OpenKey\`.
- Commands: `/about`, `/models`, `/model`, `/cls`, `/help`, `/reset`, `/quit`.
- Single self-contained `.exe` that runs from a USB stick with nothing installed.

[Unreleased]: https://github.com/corecompiled/OpenKey/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/corecompiled/OpenKey/releases/tag/v0.2.0
[0.1.0]: https://github.com/corecompiled/OpenKey/releases/tag/v0.1.0
23 changes: 14 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,12 @@ When the user mentions a new feature / QoL / roadmap item, slot it into existing
2. **If it's structurally new**, add a new section in `docs/07-roadmap.md` at the correct tier position.
3. **If it doesn't fit any tier cleanly**, add it to the **Quick Wins** flat list at the bottom of `docs/07-roadmap.md`.

Sort by the **tier ladder**:

| Tier | Meaning |
|------|---------|
| 1 | Exe-blocking (must ship for Phase 1) |
| 2 | Exe polish (Phase 1.1 / 1.2) |
| 3 | Current-UI features within an existing host |
| 4 | New providers (no UI change) |
| 5 | New UI surfaces (GUI, PWA, Android) |
Sort by the **tier ladder**, which is defined once in [`docs/07-roadmap.md`](docs/07-roadmap.md#tier-ladder--sort-rule). Don't restate it here — a copy in this file previously drifted out of sync with the canonical one, which is exactly what rule 5 of the hygiene checklist exists to prevent.

New items slot into the **lowest tier they legitimately belong to**, ordered within tier by user value. **One canonical home per item** — never silently duplicate across docs.

**Roadmap vs backlog.** `docs/07-roadmap.md` says *what* and *why*, grouped by tier. [`BACKLOG.md`](BACKLOG.md) says *what state* each item is in and *in what order* it happens. Backlog entries link to the roadmap rather than restating it.

## Cross-UI guarantee

All UIs (desktop console, future Avalonia GUI, future PWA, future Android APK) implement the **same** behavior defined in the contract docs.
Expand Down Expand Up @@ -94,6 +88,11 @@ If any item fails, fix before reporting the task done. Surface unresolvable conf
| File | Purpose | Contract? |
|------|---------|-----------|
| `CLAUDE.md` (this file) | Meta-rules for Claude sessions | no |
| `README.md` | Front page: what it is, quick start, doc index | no |
| `BACKLOG.md` | Execution state and order (roadmap says what/why) | no |
| `CHANGELOG.md` | Released changes, Keep a Changelog format | no |
| `CONTRIBUTING.md` | Build, test, house rules, contract-change process | no |
| `SECURITY.md` | Reporting, data handling, threat model | no |
| `docs/00-overview.md` | Pitch, principles, phase ladder, glossary | no |
| `docs/01-architecture.md` | Layers, `IChatProvider`, error taxonomy, cross-UI contract | **yes** |
| `docs/02-phase1-build.md` | Step-by-step Phase 1 build walkthrough | no (impl guide) |
Expand All @@ -102,6 +101,12 @@ If any item fails, fix before reporting the task done. Surface unresolvable conf
| `docs/05-persistence-and-reset.md` | `%APPDATA%` layout, DPAPI, `/reset` | **yes** |
| `docs/06-build-and-distribute.md` | `dotnet publish`, smoke test, USB distribution | no |
| `docs/07-roadmap.md` | Future phases, tier ladder, Quick Wins | no |
| `docs/08-user-guide.md` | End-user manual: commands, troubleshooting, FAQ | no |
| `docs/09-testing.md` | Test layout, helpers, conventions | no |
| `docs/architecture/` | Explanatory deep-dives; `01` stays normative over all of them | no |
| `docs/architecture/08-decisions.md` | Decisions **and explicit rejections**, with evidence | no |

Before proposing a library, a pattern, or an approach, check `docs/architecture/08-decisions.md` — several obvious-looking options were evaluated and rejected there for concrete, recorded reasons.

## Things never to do

Expand Down
Loading
Loading