Guidance for AI coding agents working in supergrok-zed-rs.
Read this file, RESEARCH.md, and the epic #1 before changing architecture, auth, HTTP contracts, or process lifecycle.
This file is for agents. Humans start with README.md.
macOS menubar app that runs an in-process OpenAI-compatible HTTP proxy so Zed can use SuperGrok (OAuth subscription) or an xAI API key.
Protocol reference (behavior to match): andypduk/supergrok-zed (Python FastAPI + Swift menubar).
Process model is different (do not copy LaunchAgent lifecycle):
| Topic | Python reference | This repo (locked) |
|---|---|---|
| Proxy process | LaunchAgent / separate serve | In-process with the app |
| Quit app | Connector often keeps running | Quit stops the proxy |
| UI | Swift LSUIElement |
GPUI settings + tray-icon (or NSStatusItem) |
| Config paths | ~/.config/supergrok-zed/ |
Same (drop-in) |
| Platform | macOS | macOS only first release |
Target Cargo workspace (create as work lands; see issues #9+):
| Crate | Owns | Must not own |
|---|---|---|
supergrok-config |
Paths, TOML schema, defaults, env overrides, aliases, timeouts, auth_mode, completions_backend, completions_enabled |
Network, GPUI, Keychain |
supergrok-auth |
api_key / oauth, refresh, Keychain + env file, server Bearer helpers |
Axum routes, GPUI widgets |
supergrok-proxy |
Axum router, bind policy, /healthz, /v1/models, /v1/chat/completions, upstream client, SSE stream, retries |
Tray / GPUI |
supergrok-completions |
Experimental POST /v1/completions (FIM → Responses, optional chat) |
Auth storage, UI |
supergrok-zed (bin) |
Entry, Tokio + GPUI lifecycle, tray, settings UI, start/stop proxy, graceful quit | Domain logic that belongs in libs |
Dependency direction:
supergrok-zed → proxy, completions, auth, config, GPUI/tray
supergrok-proxy → config, auth, (completions for route mount)
supergrok-completions → config (+ thin types); no UI
supergrok-auth → config
supergrok-config → thin (serde/toml/dirs only)
Where to put code
- New OpenAI HTTP surface →
supergrok-proxy(orsupergrok-completionsif FIM-only). - Secrets / refresh →
supergrok-auth. - New TOML field →
supergrok-configfirst, then wire consumers. - Menu / tray / settings windows →
supergrok-zedonly. - Prefer injected
Arcstate / traits; no global mutable credential statics. - Libraries must not depend on GPUI or tray-icon.
Run from the workspace root. Prefer stable Rust.
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo build -p supergrok-zed # macOS binary (when present)Scoped work:
cargo test -p supergrok-config
cargo test -p supergrok-proxyDo not require a real XAI_API_KEY for automated tests. Mock upstream.
Manual smoke (after binary exists):
curl -s http://127.0.0.1:8787/healthz # → {"status":"ok"}
# Quit app → connection refused (proxy must die with the process)If port 8787 is busy, the Python connector may still be running — stop it.
Drop-in compatibility with the Python connector:
| Artifact | Path |
|---|---|
| Config | ~/.config/supergrok-zed/config.toml |
| Secrets | ~/.config/supergrok-zed/env (mode 0600 on write) |
In-process must load the env file (Python often relied on LaunchAgent source). Process env still overrides after load.
Defaults (paths, env names, and HTTP contracts match the Python connector; default_model / aliases are grok-4.6 — documented delta):
host = "127.0.0.1"
port = 8787
default_model = "grok-4.6"
upstream = "https://api.x.ai/v1"
request_timeout_s = 600
connect_timeout_s = 10
max_retries = 2
log_level = "info"
log_prompts = false
auth_mode = "api_key" # or "oauth"
completions_backend = "responses" # or "chat"
completions_enabled = true # false → local POST /v1/completions is 404
[aliases]
supergrok = "grok-4.6"
grok = "grok-4.6"Env overrides (same names as Python):
SUPERGROK_ZED_HOST, SUPERGROK_ZED_PORT, SUPERGROK_ZED_DEFAULT_MODEL, SUPERGROK_ZED_UPSTREAM, SUPERGROK_ZED_LOG_LEVEL, SUPERGROK_ZED_CONFIG, SUPERGROK_ZED_ALLOW_REMOTE, SUPERGROK_ZED_AUTH_MODE, SUPERGROK_ZED_COMPLETIONS_BACKEND, SUPERGROK_ZED_COMPLETIONS_ENABLED, SUPERGROK_ZED_ENV.
- Secrets never go in
config.toml. - Config tests use temp dirs (
tempfile), never the developer’s real home config.
| Method | Path | Notes |
|---|---|---|
GET |
/healthz |
{"status":"ok"}; no auth, no upstream |
GET |
/v1/models |
Local catalog only |
POST |
/v1/chat/completions |
Proxy to {upstream}/chat/completions; stream + non-stream |
POST |
/v1/completions |
Experimental FIM/prompt → Responses (default) or chat fallback → text_completion JSON. 404 not_found_error when completions_enabled is false |
- Ignore client
Authorization(and similar). Inject only server Bearer for the active auth mode. - Model alias rewrite server-side (
supergrok→ configured target). - Streaming: forward raw bytes for chat SSE; keep client/upstream open until body fully drained; no full-body buffering; do not retry mid-stream after first byte to client.
- Retries: statuses
429,502,503,504+ connect/timeout; backoff with jitter (match Python:min(0.5 * 2^n, 8) * (0.5 + rand)); honormax_retries. - Missing auth on chat/completions → 401 OpenAI-shaped
auth_error(not on/healthz). - Invalid JSON body → 400
invalid_request_error. /v1/completions: do not call xAI legacy/completions. Default is{upstream}/responses(store: false). Search off means omitsearch_parametersand do not send Agent Tools /web_search(ortools: []). Optionalcompletions_backend = "chat"mapsmessage.content→choices[0].text. Never fail over between Responses and chat; same-URL retries only. Always returnobject: "text_completion". Quality is experimental.
Exactly one active mode: auth_mode = "api_key" | "oauth" (invalid → api_key).
| Mode | Bearer source | Other mode’s secrets |
|---|---|---|
api_key |
XAI_API_KEY |
Ignore OAuth vars |
oauth |
XAI_ACCESS_TOKEN |
Ignore API key |
OAuth (parity with reference):
- Token URL:
https://auth.x.ai/oauth2/token - Public client id:
b1a00492-073a-47ea-816f-4c329264a828 - Refresh skew ~60s; single-flight lock; one 401 → refresh → retry; on hard failure clear tokens
- Persist access/refresh/expiry to Keychain (preferred) and
envfile write-through (0600, bash-safe quoting)
Subscription OAuth and developer API keys are separate billing. Some tiers return 403 on OAuth-backed API — surface clearly; allow fallback to api_key.
Browser OAuth sign-in lives in the app UI (issue #7), not in headless unit tests.
- Default bind
127.0.0.1only. Refuse0.0.0.0/::unlessSUPERGROK_ZED_ALLOW_REMOTEis truthy (1/true/yes/on). - No connector auth on the local socket — loopback is the control plane.
log_promptsdefault false. Never log API keys, access/refresh tokens, orAuthorizationheaders.- Do not commit secrets, real
envfiles, or credential-bearing configs. - Prefer
secrecy/ careful redaction for in-memory secrets where practical.
- Axum on a dedicated multi-thread Tokio runtime owned by the binary.
- Never run the accept loop or long blocking I/O on the GPUI main/UI thread.
- Shared long-lived
reqwest::Clientfor upstream. - UI ↔ proxy: channels /
Arcstate / cancellation token; graceful shutdown on Quit. - Quit = cancel proxy + unbind port + exit. No orphan listeners.
- Upstream GPUI has no merged system tray API (PR #44047 closed). Hybrid:
tray-icon(or NSStatusItem) + GPUI windows. - Create tray on the main thread; do not run a second competing NSApp loop.
- Tray callbacks must marshal safely into GPUI state updates.
- Process must stay alive with zero windows until Quit (menubar app /
LSUIElementpattern). - Tray health = local
/healthz(or bind state), not upstream model calls. - Spike tray + GPUI early (issue #6) before deep UI features. If hybrid fails, document plan B (tray shell + GPUI settings only) — do not invent multi-OS tray.
| Layer | Rules |
|---|---|
| Lib unit/integration | No GPUI, no real tray, no Keychain UI dialogs |
| Upstream | Mock only (wiremock / httpmock / local Axum) — never hit api.x.ai in CI |
| Auth | Fake credential provider; mode isolation; refresh lock; 401 retry once |
| Streaming | Multi-chunk delayed SSE; early-close regression; client cancel aborts upstream |
| Completions | Table-driven FIM→Responses/chat mapping in supergrok-completions |
| Config | Temp dirs only |
| Binary/UI | Manual smoke or #[ignore] — default CI is headless lib tests |
Match Python behavioral contracts where documented in RESEARCH.md and issues. Prefer red-green tests for proxy/auth before wiring UI.
- Reintroduce LaunchAgent / launchd as the default product path.
- Make quit leave the proxy running.
- Trust or forward client Authorization.
- Bind all interfaces by default or for “demo convenience.”
- Put GPUI/tray dependencies in library crates.
- Buffer entire streamed chat bodies.
- Treat experimental
/v1/completionsas full OpenAI Completions parity without fixtures/docs. - Add Windows/Linux tray “support” in the first release without a product decision.
- Use nightly-only features unless the repo explicitly opts in.
- Commit secrets, tokens, or private keys.
- Log tokens or prompt bodies when
log_promptsis false. - Invent a second top-level JSON object in Zed
settings.jsondocs (Zed loads the first object only) — merge keys into the existing root.
-
cargo fmt --checkandclippy -D warningsclean for touched crates - Code lives in the correct crate (boundary table)
- Contracts preserved: loopback, ignore client auth, mode isolation, SSE lifetime
- No secret logging; no secrets in git
- Tests with mocked upstream for proxy/auth changes
- Product deltas vs Python still accurate if you touch lifecycle docs
| Priority | Issue | Topic |
|---|---|---|
| Epic | #1 | Full plan |
| Start | #9 | Workspace + config |
| Parallel | #6 | Tray + GPUI spike |
| Then | #10 → #2 → #8 → #4 → #5 → #7 → #3 | Proxy, stream, auth, completions, app, OAuth UI, packaging |
Implement tickets in dependency order. Prefer implement-ticket / focused PRs over large mixed diffs.
- RESEARCH.md — primary sources (Python connector, Zed Edit Prediction, GPUI tray status, xAI notes)
- Plan expert input: rust-expert (crate layout, Tokio+GPUI, streaming), python-expert (drop-in contracts)
- Do not reverse locked product decisions without updating RESEARCH.md / issue #1 and this file together