main.rs held eleven `const … : u32` colours, and spikes/floem-shell held a byte-identical copy of the same eleven. Every one was a judgement call made once, and no user could change any of them without recompiling. lumbridge-theme takes a syntax theme's five anchors — background, foreground, comment, and the git added/deleted/modified colours where the theme has them — and derives the whole role set. The frame is the editor background pushed one logarithmic contrast step away from the content, so the work surface is the brightest thing on screen; a theme already at black lifts its surface instead of sinking its frame, which is why a pitch-black theme still shows a seam. Adapted from Buzz's adaptive-theme.ts (block/buzz, Apache-2.0) as a specification, not as copied code. The golden vectors were taken by running the original under Node — a research pass had supplied Python-derived vectors and claimed they reproduced it byte-exactly, and they did not: Python rounds half-to-even, JavaScript rounds half-up, they disagree on exactly one channel value of 22.5, and that decides whether the luminance bisection converges a step early. github-dark's chrome is #171a1d, not #191c20. Provenance colours are separate roles from state colours, with a test holding them pairwise distinct in every theme, because decision 0012 colours a usage reading by where its number came from and never by how alarming it is. This changed no pixels, and that was verified rather than asserted: the only difference between before-and-after screenshots is the digits of a process ID. The check earned its keep — the mechanical rename had rewritten three user-facing strings, turning the sidebar's "ATTENTION · 0" into "theme.attention · 0" and "+ ADD PANEL" into "+ ADD theme.surface". A literal-by-literal diff now confirms zero strings changed. The default theme pins its roles to the previous constants to make that true; the anchors underneath are real, and a test bounds how far the pure derivation sits from them. The terminal ANSI palette keeps its own table, so 29 colour literals remain in main.rs, all terminal. The catalog, its attribution, and the picker are separate work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
336 lines
19 KiB
Markdown
336 lines
19 KiB
Markdown
# Architecture
|
|
|
|
## Recommended shape
|
|
|
|
Lumbridge should be two cooperating Rust processes:
|
|
|
|
```text
|
|
native desktop UI
|
|
| local authenticated IPC
|
|
Lumbridge session runtime
|
|
|-- local PTYs and process trees
|
|
|-- SSH/Tailscale transport to remote Lumbridge runtimes
|
|
|-- ACP clients and adapters
|
|
|-- repositories and worktrees
|
|
|-- usage/event ledger
|
|
`-- secret-store handles
|
|
```
|
|
|
|
Separating the UI from the session runtime lets terminals survive renderer
|
|
restarts, permits a future headless/SSH mode, and gives persistence one owner.
|
|
The first development build may run both in one process, but boundaries and IPC
|
|
messages should be real from the beginning.
|
|
|
|
## Planned crates
|
|
|
|
- `lumbridge-core`: IDs, commands, events, errors, capability and usage types.
|
|
- `lumbridge-runtime`: session ownership, supervision, recovery, IPC server.
|
|
- `lumbridge-pty`: portable PTY and process-tree adapters.
|
|
- `lumbridge-terminal`: VT parsing, scrollback, selection, search, render model.
|
|
- `lumbridge-theme`: derives the interface palette from a syntax theme's anchors.
|
|
- `lumbridge-acp`: ACP client, capability negotiation, transcript normalization.
|
|
- `lumbridge-harness`: manifests, launch profiles, hooks, PTY fallback adapters,
|
|
and documented status/usage probes.
|
|
- `lumbridge-provider`: BYOK providers and provider-neutral usage records.
|
|
- `lumbridge-storage`: SQLite migrations, event log, snapshots, retention.
|
|
- `lumbridge-remote`: OpenSSH/Tailscale command construction, framed stdio,
|
|
handshake, reconnect, heartbeat, and remote-runtime discovery.
|
|
- `lumbridge-secrets`: Keychain/libsecret adapters and redaction.
|
|
- `lumbridge-git`: repositories, worktrees, diffs, status, conflict state.
|
|
- `lumbridge-buzz`: credential-free Buzz event/broker preparation and pane-share
|
|
safety gates; platform adapters own signing and transport.
|
|
- `lumbridge-ui`: native desktop state and rendering.
|
|
- `lumbridge`: installable application entry point.
|
|
|
|
The scaffold currently contains `lumbridge-core`, `lumbridge-storage`,
|
|
`lumbridge-buzz`, `lumbridge-pty`, `lumbridge-runtime`, `lumbridge-terminal`,
|
|
`lumbridge-harness`, `lumbridge-theme`, `lumbridge-ui-fixture`, and the `lumbridge` application
|
|
itself. The GPUI shell was a spike in an excluded workspace until it graduated
|
|
into `apps/lumbridge`; `scripts/ci.sh` now takes `--headless` and `--ui` so the
|
|
non-UI crates still build in seconds, and `cargo deny check licenses` gates the
|
|
dependency closure that graduation made the product's. See decision 0017. `lumbridge-core` also contains the
|
|
first typed workspace command reducer and the usage ledger. Larger runtime
|
|
crates are added after their architecture spikes pass.
|
|
|
|
## Terminal path
|
|
|
|
The runtime owns PTYs, child process groups, resize signals, input ordering, and
|
|
raw output. The first `lumbridge-pty` boundary now uses `portable-pty` behind a
|
|
single-owner session, a fixed credential-free child environment, validated
|
|
sizes, an 8 KiB bounded output queue, resize/input/wait operations, and Unix
|
|
process-group cleanup. It deliberately does not emulate a terminal or persist
|
|
scrollback. The terminal engine turns output into immutable render snapshots and
|
|
bounded deltas for the UI. Scrollback is chunked and persisted separately from
|
|
the live screen to prevent large agent transcripts from blocking input.
|
|
|
|
The first `lumbridge-terminal` implementation wraps `alacritty_terminal` behind
|
|
a Lumbridge-owned input and immutable snapshot contract. It parses styled cells,
|
|
cursor state, alternate screen, terminal modes, title events, keyboard input,
|
|
bracketed paste, resize, and terminal-generated protocol replies. OSC 52
|
|
clipboard writes are disabled by default. The renderer never imports the
|
|
upstream terminal type, and protocol replies return through the same actor input
|
|
queue as human input. See decision 0005.
|
|
|
|
Each `lumbridge-runtime` actor owns one `PtySession` on a dedicated thread.
|
|
Bounded command and event queues serialize input, resize, close, and shutdown
|
|
against ordered raw-byte output. A generic pane-indexed registry now owns and
|
|
routes multiple actors without knowing whether their panels are attached. The
|
|
GPUI slice feeds three independent actor sessions through three terminal engines
|
|
while three non-terminal surfaces retain deterministic comparison output. Its
|
|
adapter groups adjacent cells into native GPUI paint runs and renders
|
|
ANSI/indexed/RGB colors, emphasis, hyperlinks, and cursor shapes. Window geometry
|
|
drives terminal rows and columns and resizes both the engine and PTY. The engine
|
|
now exposes retained-history offsets and page/top/bottom viewport movement
|
|
without writing scroll keys to the PTY. Text selection, mouse reporting, and a
|
|
lower-level terminal canvas remain. These actors still run in-process; moving the
|
|
same framework-neutral contract behind local authenticated IPC is the next
|
|
durability step. See decisions 0004 and 0005.
|
|
|
|
## Default workspace composition
|
|
|
|
The default desktop workspace is a responsive horizontal row of self-contained
|
|
vertical panels: one panel in compact windows, three at normal desktop widths,
|
|
and five on a 3440 px ultrawide. Inside every panel, the top 20% holds that pane's
|
|
agent, tool, context, goal, target, and status; the middle 60% holds its terminal
|
|
or work surface; and the bottom 20% holds that pane's answer, choices, chat, and
|
|
approval boundary. The selected panel alone owns keyboard input. Six surfaces
|
|
continue updating so performance comparisons remain meaningful, with a sliding
|
|
visible window keeping the selected pane onscreen. See decisions 0008 and 0009.
|
|
|
|
Panel attachment is independent from session lifetime. Detaching removes a pane
|
|
from the layout tree but retains its definition in the workspace's detached
|
|
registry and leaves its runtime-owned process alive. Reattaching restores that
|
|
identity without a launch. Terminating an attached or detached session is a
|
|
separate Execute-capability operation. The GPUI slice now layers an unbounded
|
|
panel registry above the six-surface comparison fixture. New Terminal, Browser,
|
|
Markdown, and Review panels receive monotonic local IDs, are inserted beside
|
|
the selection, and persist their identity, order, selection, and attachment in
|
|
a credential-free SQLite snapshot. Each Terminal panel is keyed into the
|
|
runtime registry by that durable panel ID. See decisions 0010 and 0011.
|
|
|
|
We should evaluate, not blindly copy, WezTerm, Zellij, RMUX, tmux, and cmux. The
|
|
first spike must compare a reusable terminal crate with a small first-party layer.
|
|
Remaining correctness cases include OSC 8 links, Kitty
|
|
keyboard/graphics negotiation, Unicode width, IME, mouse modes, shell integration,
|
|
and simultaneous automation plus human input.
|
|
|
|
## Workspace command plane
|
|
|
|
Human UI, local automation, remote clients, and agents reduce the same typed
|
|
workspace commands. Stable request IDs make retries idempotent; validated pane
|
|
IDs, split ratios, launch intents, and close dispositions prevent untyped JSON
|
|
from becoming the product API. A multi-command setup plan is reduced atomically.
|
|
Observe, Configure, and Execute capabilities are checked before mutation, and a
|
|
layout-only agent cannot smuggle an executable or arbitrary arguments into a
|
|
split request. Current code is an in-process reducer; serialization, durable
|
|
events, and authenticated IPC are subsequent layers. See decision 0006.
|
|
|
|
## UI decision gate
|
|
|
|
Do not lock the project to a webview or to Zed's application implementation
|
|
details. The measured spike compares:
|
|
|
|
1. GPUI for a Zed-like native model and excellent text-heavy interaction.
|
|
2. Floem for an independent native Rust model with existing editor primitives.
|
|
|
|
Floem is frozen: the scorecard records it failing the accessibility hard gate
|
|
with no AccessKit at its pinned revision, and comparability on the footer strip
|
|
was already lost when the usage ledger landed. It is retained as evidence at the
|
|
revision where the comparison was made, not maintained in parity. The product
|
|
ships on published `gpui 0.2.2`, which has no AccessKit either, behind a narrow
|
|
adapter that no-ops today and calls real roles on a Zed revision later — so that
|
|
gate is knowingly unmet and staged rather than quietly dropped. See decision
|
|
0017.
|
|
|
|
The original comparison bar was that the winner must render six busy panes smoothly, keep input latency low, support
|
|
IME/accessibility, package on macOS and both Linux targets, and avoid a license
|
|
or upstream-stability trap. Current Zed GPUI is the provisional winner because
|
|
its AccessKit semantics compile while the pinned Floem revision has no semantic
|
|
accessibility integration. This remains behind a narrow adapter until platform
|
|
assistive-technology, IME, packaging, licensing, and release measurements pass.
|
|
|
|
## Local-first and remote session path
|
|
|
|
SQLite is device-local. It stores workspace metadata, pane layouts, event and
|
|
usage history, remote routing profiles, and small snapshots. It never stores SSH
|
|
private keys, Tailscale credentials, provider API keys, or subscription tokens.
|
|
Secrets remain in the OS credential store or in the user's existing SSH agent.
|
|
|
|
A remote pane is not a local PTY wrapped around a long-lived `ssh` process. Its
|
|
durable owner is a per-user `lumbridge-runtime` on the destination:
|
|
|
|
```text
|
|
MacBook Lumbridge UI/runtime
|
|
|
|
|
| ssh host lumbridge remote connect --stdio
|
|
| or: tailscale ssh host lumbridge remote connect --stdio
|
|
v
|
|
remote per-user Lumbridge runtime -- Unix socket -- PTYs, agents, worktrees
|
|
|
|
|
`-- remote SQLite + chunked scrollback on that machine
|
|
```
|
|
|
|
The SSH child carries a versioned framed protocol over stdio. Normal OpenSSH
|
|
remains the default because it honors the user's config, agent, host keys,
|
|
ProxyJump, and Tailscale addresses. `tailscale ssh` is an explicit transport for
|
|
users who want Tailscale's SSH proxy and host-key path. Lumbridge does not
|
|
configure a tailnet, weaken ACLs, copy SSH keys, or require a listening TCP port.
|
|
|
|
The remote runtime assigns a stable session ID before acknowledging a launch.
|
|
On network loss the local pane becomes disconnected, the remote PTY continues,
|
|
and reconnect resumes from the last acknowledged output sequence. A second
|
|
authorized Lumbridge installation can attach to the same remote session after
|
|
the remote runtime arbitrates input ownership. Collaborative simultaneous input
|
|
is not part of the first release.
|
|
|
|
## Harness integration
|
|
|
|
Each harness is described by a versioned manifest: executable discovery, launch
|
|
arguments, environment allowlist, resume semantics, status/usage probes, ACP
|
|
command when available, and hook installation rules.
|
|
|
|
ACP is preferred because it provides structured prompts, plans, tool calls,
|
|
permissions, content blocks, and session lifecycle. A supervised PTY is always
|
|
available because terminal fidelity and arbitrary CLI compatibility are product
|
|
requirements. Harness-specific adapters translate both paths into the same event
|
|
model without pretending a PTY has capabilities it cannot prove.
|
|
|
|
Lumbridge Harness is a separate optional orchestrator, not a UI framework. The
|
|
DeepSeek Harness snapshot is a useful MIT-licensed reference for plugin seams,
|
|
durable session events, approvals, ACP, and agent lifecycle, but its developer-
|
|
preview Node/TypeScript/web composition is not embedded into the Rust desktop
|
|
process. If we maintain a fork, it lives in its own repository and communicates
|
|
through a versioned, capability-scoped protocol. A first-party Rust service may
|
|
later replace that implementation without changing the desktop contract.
|
|
|
|
The Harness can consume redacted workspace projections and emit suggestions or
|
|
typed command plans. Suggestions have no authority. Plan execution is routed
|
|
through the same command plane as human actions, and trace export to a hosted
|
|
model requires explicit scope and destination consent. See decision 0007.
|
|
|
|
## Theme model
|
|
|
|
Every colour in the interface is a named semantic role derived from a syntax
|
|
theme's five anchors, not a constant. `lumbridge-theme` holds the derivation and
|
|
no framework types, so the arithmetic is tested headless; the shell converts a
|
|
derived palette into GPUI colours once per theme change and owns the result.
|
|
|
|
The frame is the editor background pushed one logarithmic contrast step away
|
|
from the content, so the work surface is the brightest thing on screen; a theme
|
|
already at black lifts its surface instead. Provenance colours are separate
|
|
roles from state colours, with a test holding them pairwise distinct, because
|
|
decision 0012 colours a usage reading by its source and never by its value.
|
|
|
|
The default theme pins its roles to the eleven constants that preceded it, so
|
|
introducing the engine changed no pixels. The terminal ANSI palette and the
|
|
theme catalog are still fixed tables. See decision 0018.
|
|
|
|
## Usage model
|
|
|
|
Usage is an append-only observation stream, not a mutable percentage field.
|
|
Observations include account profile, provider, harness, model, units, time
|
|
window, reset time, provenance, confidence, and source timestamp. Projections are
|
|
derived views that can be recomputed as forecasting improves.
|
|
|
|
`lumbridge-core` now implements that stream. A bounded per-profile `UsageLedger`
|
|
accepts observations in time order, and `project` derives the footer view:
|
|
window fraction, reset, burn rate, and exhaustion. Facts keep their reported
|
|
provenance; every derived value is labelled estimated. The projection withholds
|
|
a burn rate from a single sample, withholds a window fraction without a reported
|
|
ceiling, withholds an exhaustion estimate that falls after the reset, treats an
|
|
unavailable observation as invalidating older facts, and reports an expired
|
|
window as rolled over rather than freezing its last percentage. `FooterUsage`
|
|
renders the five product-spec questions and returns explicit unavailable phrases
|
|
instead of placeholder numbers. See decision 0012.
|
|
|
|
`lumbridge-harness` is the other side of that boundary: it runs processes,
|
|
reads a clock, and parses untrusted wire text, then hands back observations.
|
|
Its first adapter probes Codex's documented `account/rateLimits/read` surface
|
|
over the app-server's JSON-RPC stdio protocol. The client cannot express a
|
|
request outside a two-variant enum and answers every server-to-client request
|
|
with `-32601`, so a harness asking Lumbridge for a credential is refused by
|
|
construction. All protocol decisions live in a pure state machine driven by
|
|
`&str` lines and an explicit timestamp, with a `replay_transcript` entry point
|
|
that verifies the whole path against a fixture without a process, a clock, or a
|
|
network. See decision 0013.
|
|
|
|
Its second adapter reads Claude Code's session transcripts, the only local
|
|
surface that reports what that harness spent. It is a tail follower with a
|
|
per-file byte offset, so its total is monotonic, and it reports nothing until
|
|
it has read the existing backlog to the end — a partially-read backlog is
|
|
indistinguishable from a burst of spend, and the ledger would derive a rate
|
|
from it. The transcript carries no window or ceiling, so that profile reports
|
|
consumption against none. The parser models four token counters and nothing
|
|
else, so the conversations in those files are not representable in a Lumbridge
|
|
value. See decision 0014.
|
|
|
|
Claude Code's subscription windows come from two further surfaces, because
|
|
neither answers the whole question. The CLI pipes a `rate_limits` object
|
|
carrying the five-hour and seven-day windows to whatever `statusLine` command
|
|
the user has configured, on every turn; a small installed bridge writes those
|
|
fields — and only those — to a local feed the probe tails, for free and without
|
|
a credential (decision 0015). The account usage endpoint supplies what the
|
|
status line cannot: the per-model `weekly_scoped` limits a Max plan meters
|
|
separately, and an answer on a cold start before any session has taken a turn.
|
|
Reaching it means reading Claude Code's stored access token, which `AGENTS.md`
|
|
now permits under a narrow named allowance — one documented question about the
|
|
user's own account, never persisted, never logged, never in argv, no refresh
|
|
token, identified as Lumbridge rather than as the harness, and switchable off
|
|
(decision 0016).
|
|
|
|
The endpoint is the authority whenever it answers; the status line covers the
|
|
interval between its deliberately slow refreshes. Both are `ProviderReported`
|
|
and share the window arithmetic, so one quota read two ways cannot produce two
|
|
numbers. Per-model limits arrive as profiles the probe announces at runtime,
|
|
since their names come from the response.
|
|
|
|
The GPUI shell now runs both probes; a probe contributes its own profiles on
|
|
top of the declared ones, so the Claude windows appear once they report. Its
|
|
footer strip
|
|
shows every declared harness at once, and a harness with no adapter renders
|
|
the honest gap.
|
|
|
|
Subscription balance is provider-specific and sometimes unavailable. BYOK calls
|
|
usually expose token counts but cost still depends on cached tokens, reasoning,
|
|
tool calls, and current pricing. Adapters normalize facts without erasing their
|
|
source or uncertainty.
|
|
|
|
## Buzz collaboration
|
|
|
|
Buzz channel messages, replies, agents, and attachments use the upstream Rust
|
|
SDK's signed Nostr semantics. SQLite stores a public identity and opaque
|
|
credential-store handle, never the private identity key. Pane images cross the
|
|
network only after local capture, redaction preview, explicit destination, and
|
|
confirmation. See `BUZZ_INTEGRATION.md` for the contract and test plan.
|
|
|
|
## Persistence
|
|
|
|
SQLite in WAL mode stores metadata, commands/events, normalized usage, and small
|
|
snapshots. Large scrollback chunks and binary attachments use content-addressed
|
|
files. A write-ahead event is committed before an external mutation is reported
|
|
as accepted. Startup replays incomplete operations and reconciles live children.
|
|
|
|
Default data roots are `~/Library/Application Support/ai.karti.lumbridge/` on
|
|
macOS and `${XDG_DATA_HOME:-~/.local/share}/lumbridge/` on Linux. Backups and
|
|
exports are explicit; Lumbridge does not synchronize the database through a
|
|
hidden hosted account.
|
|
|
|
## Security
|
|
|
|
- macOS secrets: Keychain; Linux secrets: Secret Service/libsecret, with an
|
|
explicit encrypted-file fallback only if the user enables it.
|
|
- Never pass keys in process arguments. Prefer inherited file descriptors or a
|
|
minimal child environment when a harness requires environment variables.
|
|
- IPC is local, authenticated, permission-restricted, and version-negotiated.
|
|
- Transcript and crash-report redaction happens before persistence or export.
|
|
- Repository trust, harness approval mode, and sandbox mode are visible per pane.
|
|
- Remote control and plugins are out of scope until a capability/permission model
|
|
exists.
|
|
|
|
## Platform adapters
|
|
|
|
Shared contracts cover PTY, process tree, notifications, secret store, paths,
|
|
autostart, updater, and packaging. macOS uses `forkpty`/process groups and native
|
|
Keychain. Ubuntu and Omarchy use Unix PTYs, cgroups/systemd scopes when available,
|
|
and Secret Service. Omarchy is treated as Arch Linux, not as a separate kernel.
|