Memcode Desktop - #1
Open
timothyerwin wants to merge 25 commits into
Open
Conversation
Extend the stream-json schema for a richer GUI client (the upcoming Electron desktop app) — all additive to protocol v1: - diff event (structured file change: path + unified diff + counts) so a client renders its own syntax-highlighted diff instead of parsing ANSI - todos event (plan/checklist steps) for a plan panel - attachments on user_turn (local files as first-class context, not @path text) - resume on initialize (re-enter a prior session over the protocol) - remember/remember_scope on permission_response (persist an allow-rule) Payload types only; runtime wiring + driver handling follow.
- initialize.resume: read the first initialize before StartChat and re-enter a saved transcript via the new Session.ResumeSession (ResolveSession + SetResume). A GUI client can now reopen a prior thread over the same transport. - user_turn.attachments: resolve wire attachments to native input.Attachment via input.Resolve and arm them on the turn (new Session.AttachNext + a pending-atts merge in Submit), so dropped files flow through the existing attachment path instead of @path text. - Run restructured: initialize applied pre-StartChat (resume-aware), turns carry attachments, mode/pin still adjustable mid-session.
Wire the runtime -> protocol events a GUI client renders natively: - diff: emit a structured file change (path, language, unified diff, +/- counts, new-file flag) alongside the ANSI render at the edit sites (exec.go) - tool_call/tool_result: emit from the universal toolLineStat marker - todos: un-stub the driver's Todos to emit the plan/checklist Done via optional diffEmitter/toolEmitter interfaces the protocol driver implements (internal/agent/runtime/emit.go) — additive, touches no other UIObserver implementer (the TUI keeps rendering its own ANSI).
Add `memcode models --json` (the full model catalog — the GUI model picker reads this instead of embedding its own copy) and `memcode status --json` (login state, endpoint, core version/commit, and stream-json protocol version — for a Settings/ About page). Keeps the CLI/core the single source of truth for the desktop app.
Adds desktop/ — an Electron + Vite + React + TypeScript app that drives the
core CLI over the stream-json protocol:
- main/cli-bridge.ts spawns {"version":"1","type":"initialized","data":{"session_id":"","protocol":"1"}}, frames
commands, decodes NDJSON envelopes into typed events, correlates turns,
cancels, and shuts down cleanly.
- main/config.ts drives the CLI's own machine surface (status/models --json,
login) so Desktop embeds no catalog and stores no separate credentials.
- preload exposes a typed, contextIsolation-safe window.memcode allowlist;
shared/protocol.ts mirrors internal/wire/streamjson.go.
- renderer: chat + streaming, collapsible tool blocks, syntax-highlighted
diffs, plan/todo panel, approval + ask dialogs, drag-and-drop attachments,
model picker (from models --json), Settings (login/BYOK/about), status bar.
- packaging: electron-builder for mac/win/linux, a CI workflow that compiles
the core binary from the SAME commit and bundles it (no protocol skew),
conditional sign/notarize when creds are present, electron-updater on a
separate desktop-v* tag namespace.
Adds `memcode config get --json` — endpoint, login state, per-tier default models, and direct-provider BYOK key presence (presence only, never the secret value). Rounds out the machine-readable trio (models/status/config) a desktop client reads so the CLI stays the single source of truth. OwnKeyEnvs() derives the BYOK list from ownKeyVendors rather than hardcoding key names.
PROTOCOL.md section 2 drifted from the code (said `--output stream-json` and envelope key `v`); correct it to `agent --protocol stream-json` / `version` and document the full current event set as client->CLI and CLI->client tables, including the additive v1 events (diff, todos, session_state, usage), resume, first-class attachments, and remember-scope on approvals. Add contract tests locking the snake_case wire keys a desktop/SDK client mirrors (resume, attachments, diff, todos, remember_scope) and a protocol test that the diff/tool observer seams + plan hook emit real envelopes with the internal todo status mapped to the wire vocabulary.
…sidebar - `memcode config set [KEY=VALUE] [--stdin]` writes backend choices (endpoint, subscription source, *_API_KEY) to the global env file via authflow.SetGlobalEnv; --stdin keeps secrets out of argv; the gateway token stays login-managed. Emits the keys set (never values). - `memcode config sources --json` reports login state, whether a backend is configured, a custom endpoint / selected subscription source, and the subscriptions detected on this machine — everything a first-run wizard needs. - `memcode session recent --json` lists sessions (newest first) with a resumable flag (transcript on disk) for the GUI session sidebar. These let the desktop first-run wizard mirror the CLI wizard and drive resume, with the CLI still owning detection and credential storage.
… settings - SetupWizard mirrors the CLI first-run wizard: detected subscriptions (config sources), hosted sign-in, own API key, custom endpoint, skip — opens on launch when no backend is configured. - Native application menu (menu.ts): macOS top bar with an Account menu (Log In / Log Out / Run Setup), Settings, New Session, and standard Edit/View/Window/Help; items dispatch to the renderer over IPC.menuAction. - SessionSidebar lists the open repo's sessions (session recent --json) with a resumable flag; click to resume that thread (initialize.resume), + for a new one; refetches when a turn finishes. - Settings gains a BYOK section: save a provider key or custom endpoint via config set --stdin (secrets never hit argv), plus login/logout lifted to the app so the menu and Settings share one path. Verified: pnpm typecheck + build green; config sources/set and session recent --json smoke-tested against a from-source binary.
pnpm 10 blocks dependency postinstall scripts by default, so a fresh install skipped Electron's binary download and `pnpm dev` failed with 'Electron uninstall'. Declare onlyBuiltDependencies so the postinstalls run on install.
The app had no notion of a current repo, so it started cold at the picker every launch. Persist opened repos (userData JSON), auto-reopen the most recent on launch, and list recents on the welcome screen so switching back is one click.
The first cut was functional but barren. Rework the visual system toward the reference apps: layered dark surfaces instead of flat black, an inviting centered empty state with a logo mark and recents cards, a large rounded composer with an attach affordance and a circular send button (auto-growing textarea), a refined sidebar with a prominent New Session action, collapsible tool/diff cards with real surfaces, softer borders, on-theme scrollbars, and a consistent radius/spacing/type scale throughout.
- Theming: full light + dark token sets (light is the base, dark applies via the OS media query and an explicit toggle). View menu + header ◐ button flip it; choice persists. Follows the OS by default. - Wordmark: a proper 'memcode' wordmark (two-tone) in the header, and a large serif display treatment as the empty-state watermark. - Always-on shell: the sidebar and composer are always present; the main area shows a branded empty state (watermark + tagline) until the first message — so launching lands somewhere intentional, not a blank chat. - Lazy sessions: selecting a folder loads its session list but does NOT spawn a CLI session; that happens on the first message. Fixes 'opens into a blank chat even though I didn't open a folder' — it now opens on the empty state. - Open Folder in the File menu (Cmd+O); the composer/sidebar prompt to open one when no folder is selected.
- Replace the invented two-tone wordmark with the actual brand: MEMCODE in matrix/katakana digital-rain glyphs (ported from apps/www MatrixGlyphWordmark and the CLI banner.go) as the empty-state wordmark, and a heavy MEMCODE lockup in the header. Accent switched from blue to the brand violet. - Open Folder now lives in exactly two canonical places — the header repo button and File > Open Folder (Cmd+O). Removed the redundant empty-state button, the sidebar button, and the composer send-becomes-open behavior; the empty state just hints (Cmd+O) and lists recents.
- Remove the full-width header entirely (Codex/Hermes have none). Model picker moves into the composer; theme + settings are reached from the native menu; a thin draggable strip replaces the header for frameless window dragging. - Sidebar reserves the macOS traffic-light band (no more bleed under the OS controls) and is now grouped by PROJECT: each recent folder is a header with its resumable conversations nested under it, click to switch/resume. Only real (resumable) chats are listed, titled — no read-only junk, no metadata clutter. - App icon: the square matrix MEMCODE mark (build/icon.png from apps/www), set on the window/dock in dev and bundled by electron-builder for releases.
Names each session like ChatGPT/Claude do — a 3-6 word generated title instead of a raw first message (which could be a giant paste). Generated once per session off the engine goroutine via the cheap classify lane (forced-tool), persisted to a .memcode/sessions/<id>/title sidecar. session recent --json returns it (falling back to the task). CLI-owned so it's not desktop-only.
…top output)
The stream-json client was getting raw TUI output — ANSI codes, the route echo
('↳ steer — default…'), the '⇄ served by …' line, and per-tool markers — as
assistant text. Add a per-session 'structured' flag (set by the protocol driver)
that suppresses that operational chrome at its source; the client already
receives routing/tool info as structured events. Also strip any residual ANSI in
the protocol output seam. The TUI is unchanged (flag stays false).
The matrix logo.png is a hero graphic — its sparse bright glyphs on black average to a black square at icon sizes, and it had hard corners. Replace with a purpose-built icon: a rounded squircle in the brand ground with a bold lavender 'm' that reads at every size and stays on-brand.
The GUI model picker only applied at initialize, so changing it on a live session did nothing (you'd pick haiku and still get served the cheap lane). Add a set_model command that re-pins the running session via SetPin (the same path /model uses), resolved through the catalog. Factor pin resolution into applyPin, shared by initialize and set_model.
- Show the catalog display name (Grok 4.5, Opus 5) instead of the raw label. - Changing the picker now sends set_model to the live session, so it takes effect immediately instead of only on the next session.
Revert the invented monogram — use the actual logo.png (the matrix MEMCODE mark) with a rounded-squircle mask + transparent margins for the native macOS look.
The 'Authenticated / close this tab' page was bare system-font text. Add the hosted memcode wordmark (referenced by absolute URL — the browser just OAuth'd against memcode.ai, so it has connectivity, and a 300KB PNG shouldn't inflate the binary), a proper doctype/viewport, tightened typography, and a subtle fade-in. HTML-escape the error message while here.
…llback Replace the static image with the marketing site's MatrixGlyphWordmark canvas animation (static lit-glyph base + katakana rain, composited then clipped to the letterforms), ported to dependency-free vanilla JS so the self-contained callback page needs no assets. Restores the original heading/help text with proper vertical rhythm; HTML-escapes the error.
timothyerwin
added a commit
that referenced
this pull request
Aug 14, 2026
The #1 failure mode in both Hermes and OpenClaw: at-least-once inbound + in-memory dedup means a restart, reconnect, or provider retry re-runs an old message as a fresh (paid) agent turn. Fix it with a stable per-message identity carried on every Inbound (Telegram update_id, Discord message id, Slack event ts, GitHub delivery, WhatsApp wamid) and a dedicated SQLite state store: MarkProcessed is an atomic INSERT OR IGNORE, so it both survives restarts and guards two concurrent deliveries of the same id. Records prune after 30 days. Kept separate from the core event store so the spine's interface stays clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
//